badline 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +76 -0
  3. data/CLAUDE.md +1 -1
  4. data/README.md +130 -17
  5. data/doc/media.md +14 -2
  6. data/doc/pinned-behaviour.md +61 -0
  7. data/doc/snapshot-format.md +379 -70
  8. data/doc/snapshots.md +185 -10
  9. data/lib/badline/cbm2/acia.rb +10 -0
  10. data/lib/badline/cbm2/bus.rb +263 -0
  11. data/lib/badline/cbm2/cpu.rb +58 -0
  12. data/lib/badline/cbm2/crtc_video.rb +133 -0
  13. data/lib/badline/cbm2/ieee_drive.rb +18 -0
  14. data/lib/badline/cbm2/ieee_layout.rb +69 -0
  15. data/lib/badline/cbm2/ieee_ports.rb +104 -0
  16. data/lib/badline/cbm2/ieee_traps.rb +81 -0
  17. data/lib/badline/cbm2/keyboard_ports.rb +60 -0
  18. data/lib/badline/cbm2/keyed_modules.rb +67 -0
  19. data/lib/badline/cbm2/keyed_state.rb +91 -0
  20. data/lib/badline/cbm2/model.rb +44 -0
  21. data/lib/badline/cbm2/p500/control_ports.rb +80 -0
  22. data/lib/badline/cbm2/p500/ieee_layout.rb +41 -0
  23. data/lib/badline/cbm2/p500/keyed_state.rb +85 -0
  24. data/lib/badline/cbm2/p500/model.rb +39 -0
  25. data/lib/badline/cbm2/p500/pages.rb +57 -0
  26. data/lib/badline/cbm2/p500.rb +262 -0
  27. data/lib/badline/cbm2/sid_page.rb +19 -0
  28. data/lib/badline/cbm2.rb +267 -0
  29. data/lib/badline/{plus4 → chips}/acia/frames.rb +1 -1
  30. data/lib/badline/chips/acia/keyed_state.rb +47 -0
  31. data/lib/badline/chips/acia.rb +307 -0
  32. data/lib/badline/chips/crtc.rb +3 -2
  33. data/lib/badline/chips/riot/fast_forward.rb +72 -0
  34. data/lib/badline/chips/riot/keyed_parts.rb +69 -0
  35. data/lib/badline/chips/riot/timer.rb +80 -0
  36. data/lib/badline/chips/riot.rb +162 -0
  37. data/lib/badline/chips/rriot/keyed_state.rb +30 -0
  38. data/lib/badline/chips/rriot.rb +132 -0
  39. data/lib/badline/chips/tpi/keyed_state.rb +38 -0
  40. data/lib/badline/chips/tpi.rb +233 -0
  41. data/lib/badline/chips/vic/bank.rb +60 -5
  42. data/lib/badline/chips/vic/sprite.rb +4 -3
  43. data/lib/badline/drives/datasette/keyed_state.rb +9 -1
  44. data/lib/badline/drives/datasette.rb +27 -3
  45. data/lib/badline/drives/ieee_bus.rb +50 -4
  46. data/lib/badline/drives/model2031/ieee_port.rb +133 -0
  47. data/lib/badline/drives/model2031/keyed_state.rb +66 -0
  48. data/lib/badline/drives/model2031/parts.rb +25 -0
  49. data/lib/badline/drives/model2031.rb +92 -0
  50. data/lib/badline/drives/model8050/disk.rb +163 -0
  51. data/lib/badline/drives/model8050/fdc_bus.rb +102 -0
  52. data/lib/badline/drives/model8050/idle.rb +177 -0
  53. data/lib/badline/drives/model8050/ieee_port.rb +147 -0
  54. data/lib/badline/drives/model8050/ip_bus.rb +93 -0
  55. data/lib/badline/drives/model8050/keyed_state.rb +160 -0
  56. data/lib/badline/drives/model8050/mechanism.rb +230 -0
  57. data/lib/badline/drives/model8050/parts.rb +37 -0
  58. data/lib/badline/drives/model8050.rb +243 -0
  59. data/lib/badline/frontend/app.rb +10 -3
  60. data/lib/badline/frontend/boot.rb +1 -1
  61. data/lib/badline/frontend/controls.rb +51 -15
  62. data/lib/badline/frontend/gamepads.rb +3 -3
  63. data/lib/badline/frontend/keys.rb +61 -2
  64. data/lib/badline/frontend/media_slots.rb +1 -1
  65. data/lib/badline/frontend/menu/menu_media.rb +14 -11
  66. data/lib/badline/frontend/menu/menu_pages.rb +29 -6
  67. data/lib/badline/frontend/menu/menu_ports.rb +40 -4
  68. data/lib/badline/frontend/menu/pause_menu.rb +1 -1
  69. data/lib/badline/frontend/pet_keyboard.rb +23 -7
  70. data/lib/badline/frontend/timeline.rb +19 -10
  71. data/lib/badline/kernal_trap/dos/disk_commands.rb +80 -0
  72. data/lib/badline/kernal_trap/dos/status.rb +3 -0
  73. data/lib/badline/kernal_trap/dos.rb +18 -5
  74. data/lib/badline/kernal_trap/ieee.rb +7 -2
  75. data/lib/badline/kernal_trap/ieee_layout.rb +95 -43
  76. data/lib/badline/machine/ieee_drives.rb +194 -0
  77. data/lib/badline/machine/settling.rb +4 -0
  78. data/lib/badline/machine.rb +26 -8
  79. data/lib/badline/media/cbm2_media.rb +29 -0
  80. data/lib/badline/media/pet_media.rb +5 -5
  81. data/lib/badline/media/program.rb +6 -3
  82. data/lib/badline/media/true_drive.rb +50 -8
  83. data/lib/badline/media.rb +21 -8
  84. data/lib/badline/options/family.rb +15 -3
  85. data/lib/badline/options/help.rb +24 -6
  86. data/lib/badline/options/table.rb +25 -5
  87. data/lib/badline/options/validation.rb +47 -7
  88. data/lib/badline/options.rb +48 -15
  89. data/lib/badline/pet/bus/expansion.rb +99 -5
  90. data/lib/badline/pet/bus.rb +30 -12
  91. data/lib/badline/pet/cassettes.rb +22 -1
  92. data/lib/badline/pet/characters.rb +8 -2
  93. data/lib/badline/pet/crtc_video.rb +20 -7
  94. data/lib/badline/pet/ieee_drive.rb +17 -0
  95. data/lib/badline/pet/ieee_ports.rb +19 -1
  96. data/lib/badline/pet/ieee_traps.rb +10 -5
  97. data/lib/badline/pet/keyed_modules.rb +4 -1
  98. data/lib/badline/pet/keyed_parts.rb +10 -6
  99. data/lib/badline/pet/model.rb +56 -6
  100. data/lib/badline/pet/rom_sets.rb +30 -6
  101. data/lib/badline/pet.rb +45 -16
  102. data/lib/badline/plus4/acia.rb +4 -298
  103. data/lib/badline/plus4/keyed_parts.rb +0 -44
  104. data/lib/badline/plus4.rb +1 -0
  105. data/lib/badline/roms/8050/dos-2.7.rom +0 -0
  106. data/lib/badline/roms/8050/fdc-901869-01.rom +0 -0
  107. data/lib/badline/roms/8050/fdc-901885-04.rom +0 -0
  108. data/lib/badline/roms/README +144 -1
  109. data/lib/badline/roms/cbm2/basic128-02b.rom +0 -0
  110. data/lib/badline/roms/cbm2/basic128.rom +0 -0
  111. data/lib/badline/roms/cbm2/basic256.rom +0 -0
  112. data/lib/badline/roms/cbm2/characters-600.rom +0 -0
  113. data/lib/badline/roms/cbm2/characters-700.rom +0 -0
  114. data/lib/badline/roms/cbm2/kernal-03b.rom +0 -0
  115. data/lib/badline/roms/cbm2/kernal-04a.rom +0 -0
  116. data/lib/badline/roms/cbm2/p500-basic.rom +0 -0
  117. data/lib/badline/roms/cbm2/p500-kernal.rom +0 -0
  118. data/lib/badline/roms/dos2031.rom +0 -0
  119. data/lib/badline/roms/pet/characters-din.rom +0 -0
  120. data/lib/badline/roms/pet/editor4-80-b-50hz-8296.rom +0 -0
  121. data/lib/badline/snapshot/cbm2_setup.rb +53 -0
  122. data/lib/badline/snapshot/container.rb +9 -1
  123. data/lib/badline/snapshot/field_doc/markdown.rb +4 -2
  124. data/lib/badline/snapshot/field_doc.rb +39 -3
  125. data/lib/badline/snapshot/image.rb +79 -15
  126. data/lib/badline/snapshot/pet_setup.rb +30 -13
  127. data/lib/badline/snapshot/vice/cbm2.rb +93 -0
  128. data/lib/badline/snapshot/vice/cbm2_acia.rb +75 -0
  129. data/lib/badline/snapshot/vice/cbm2_crtc.rb +127 -0
  130. data/lib/badline/snapshot/vice/cbm2_mains.rb +49 -0
  131. data/lib/badline/snapshot/vice/cbm2_mem.rb +75 -0
  132. data/lib/badline/snapshot/vice/cbm2_restore.rb +204 -0
  133. data/lib/badline/snapshot/vice/cbm2_tpi.rb +58 -0
  134. data/lib/badline/snapshot/vice/cias.rb +13 -9
  135. data/lib/badline/snapshot/vice/p500.rb +96 -0
  136. data/lib/badline/snapshot/vice/p500_restore.rb +130 -0
  137. data/lib/badline/snapshot/vice/p500_vicii.rb +166 -0
  138. data/lib/badline/snapshot/vice/pet.rb +121 -0
  139. data/lib/badline/snapshot/vice/pet_cpu.rb +70 -0
  140. data/lib/badline/snapshot/vice/pet_crtc.rb +241 -0
  141. data/lib/badline/snapshot/vice/pet_drive.rb +180 -0
  142. data/lib/badline/snapshot/vice/pet_drive8050.rb +216 -0
  143. data/lib/badline/snapshot/vice/pet_mem.rb +93 -0
  144. data/lib/badline/snapshot/vice/pet_pias.rb +56 -0
  145. data/lib/badline/snapshot/vice/pet_restore.rb +214 -0
  146. data/lib/badline/snapshot/vice/pet_via.rb +165 -0
  147. data/lib/badline/snapshot/vice/sid_registers.rb +2 -1
  148. data/lib/badline/snapshot/vice/vicii_import.rb +10 -6
  149. data/lib/badline/snapshot/vice.rb +4 -0
  150. data/lib/badline/snapshot.rb +11 -3
  151. data/lib/badline/storage/crt_file.rb +7 -1
  152. data/lib/badline/storage/d64_image.rb +7 -0
  153. data/lib/badline/storage/d80_image.rb +17 -1
  154. data/lib/badline/storage/d81_image.rb +14 -0
  155. data/lib/badline/storage/disk_image/bam.rb +18 -3
  156. data/lib/badline/storage/disk_image/formatting.rb +69 -0
  157. data/lib/badline/storage/disk_image/writing.rb +35 -0
  158. data/lib/badline/storage/disk_image.rb +2 -0
  159. data/lib/badline/storage.rb +1 -0
  160. data/lib/badline/support/cbm2_checkpoint.rb +48 -0
  161. data/lib/badline/support/p500_checkpoint.rb +35 -0
  162. data/lib/badline/version.rb +1 -1
  163. data/lib/badline.rb +1 -0
  164. metadata +87 -2
@@ -0,0 +1,214 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Snapshot
5
+ module Vice
6
+ module PET
7
+ # Restores a PET from a snapshot xpet wrote. The modules badline
8
+ # reads become the PET's own keyed modules (Keyed), which it loads
9
+ # as it loads its own snapshot. Whatever they don't set stays as the
10
+ # machine has it at power-on, and the datasettes' tapes and the
11
+ # disk device 8 serves stay as they are. A snapshot with a true 2031,
12
+ # 8050 or 8250 puts one on the bus, with the disks badline's drive
13
+ # of that model had in it, and one without takes badline's off.
14
+ module Restore
15
+ # MAINCPU and PETMEM hold the machine itself, so a version badline
16
+ # doesn't read fails the restore. The others are left out and
17
+ # reported.
18
+ READ = [MainCPU::NAME, PETMem::NAME, CRTC::NAME, "PIA1", "PIA2", VIA::NAME].freeze
19
+ # The host's media, which a restore keeps.
20
+ KEPT = %w[BL-TAPE1 BL-TAPE2 BL-TRAPDOS].freeze
21
+ # The disks in a true drive's drives, which a restore keeps.
22
+ DISKS = %w[BL-D8-DISK BL-D8-DISK1].freeze
23
+
24
+ module_function
25
+
26
+ # Applies the modules badline reads to `computer`, and returns a
27
+ # Report. The snapshot goes into a new machine first, so one that
28
+ # fails leaves `computer` as it was, and one of a machine built
29
+ # another way fails too.
30
+ def import(container, computer)
31
+ unless computer.family == :pet
32
+ raise FormatError, "the snapshot is of a PET, not a #{Image::FAMILIES.fetch(computer.family.to_s)}"
33
+ end
34
+
35
+ setup = PET.setup(container)
36
+ ours = PETSetup.of(computer)
37
+ raise FormatError, "the snapshot is of a #{setup}, not a #{ours}" unless setup == ours
38
+
39
+ apply(container, setup.build)
40
+ had_drive = computer.true_drive
41
+ computer.power_cycle!
42
+ report = apply(container, computer)
43
+ kept = computer.mounted? ? ["the disk mounted in device 8 stays mounted"] : []
44
+ if had_drive && !computer.true_drive.equal?(had_drive)
45
+ kept << "the #{had_drive.model_name} on device #{had_drive.device} is taken off the bus"
46
+ end
47
+ Report.new(applied: report.applied, ignored: report.ignored + kept)
48
+ end
49
+
50
+ # Applies the modules badline reads to `computer`, a machine built
51
+ # as the snapshot's was, and returns the Report.
52
+ def apply(container, computer)
53
+ applied = []
54
+ ignored = []
55
+ sections = []
56
+ READ.each do |name|
57
+ section = container[name]
58
+ next if section.nil?
59
+
60
+ if reads?(section)
61
+ applied << name
62
+ else
63
+ raise FormatError, "#{section}: badline doesn't read this version" if required?(name)
64
+
65
+ ignored << "#{section}: badline doesn't read this version, left out"
66
+ end
67
+ end
68
+ disks = []
69
+ computer.save_modules.each do |section|
70
+ sections << section if KEPT.include?(section.name)
71
+ disks << section if DISKS.include?(section.name)
72
+ end
73
+ sections += drive(container, applied, ignored, kept_disks(container, computer, disks))
74
+ computer.load_modules(Keyed::Modules.new(modules(container, computer, applied) + sections))
75
+ Report.new(applied:, ignored: ignored + left_out(container, applied))
76
+ end
77
+
78
+ # The disks badline's true drive has, which stay in the drive the
79
+ # snapshot puts on the bus when it is of the same model.
80
+ def kept_disks(container, computer, disks)
81
+ drive = computer.true_drive
82
+ return [] if drive.nil?
83
+
84
+ if Drive8050.present?(container)
85
+ Drive8050.model(container) == drive.model_name ? disks : []
86
+ else
87
+ drive.model_name == "2031" ? disks.first(1) : []
88
+ end
89
+ end
90
+
91
+ # The true drive's modules where the snapshot has one badline
92
+ # reads, or none.
93
+ def drive(container, applied, ignored, disks)
94
+ return drive8050(container, applied, ignored, disks) if Drive8050.present?(container)
95
+ return [] unless Drive2031.present?(container)
96
+
97
+ names = [Drive2031::UNITS.first, Drive2031::CPU, Drive2031::VIA1, Drive2031::VIA2]
98
+ return [] unless readable?(container, names, "2031", ignored) { |section| Drive2031.reads?(section) }
99
+
100
+ applied.concat(names)
101
+ Drive2031.import(container, disks.first)
102
+ end
103
+
104
+ def drive8050(container, applied, ignored, disks)
105
+ model = Drive8050.model(container)
106
+ return [] unless readable?(container, Drive8050::NAMES, model || "8050", ignored) do |section|
107
+ Drive8050.reads?(section)
108
+ end
109
+
110
+ if model.nil?
111
+ ignored << "the true drive: badline doesn't read its #{Drive8050::UNIT}'s type, left out"
112
+ return []
113
+ end
114
+
115
+ applied.concat(Drive8050::NAMES)
116
+ Drive8050.import(container, Drive8050.reference(model), disks)
117
+ end
118
+
119
+ # Whether the snapshot has every module `names` lists at a version
120
+ # badline reads, saying which it doesn't in `ignored`.
121
+ def readable?(container, names, model, ignored, &reads)
122
+ unread = names.map { |name| container[name] }.reject { |section| section && reads.call(section) }
123
+ return true if unread.empty?
124
+
125
+ ignored << "the true #{model}: badline doesn't read #{unread.compact.join(', ')}, left out"
126
+ false
127
+ end
128
+
129
+ def required?(name) = [MainCPU::NAME, PETMem::NAME].include?(name)
130
+
131
+ def reads?(section)
132
+ case section.name
133
+ when MainCPU::NAME then MainCPU.reads?(section)
134
+ when PETMem::NAME then PETMem.reads?(section)
135
+ when CRTC::NAME then CRTC.reads?(section)
136
+ when VIA::NAME then VIA.reads?(section)
137
+ else PIAs.reads?(section)
138
+ end
139
+ end
140
+
141
+ # The PET's keyed modules from the ones `applied` names.
142
+ def modules(container, computer, applied)
143
+ model = computer.model
144
+ crtc = applied.include?(CRTC::NAME) ? container[CRTC::NAME] : nil
145
+ video = video(crtc, computer)
146
+ drive = driving?(crtc, video, model)
147
+ sections = [Keyed.section("BL-MACHINE") { |out| machine(out, container, computer, drive) },
148
+ Keyed.section("BL-RAM") do |out|
149
+ PETMem.import(container[PETMem::NAME], out, model.screen_bytes)
150
+ end]
151
+ sections << Keyed.section("BL-CPU") { |out| MainCPU.import(container[MainCPU::NAME], out) }
152
+ %w[PIA1 PIA2].each do |name|
153
+ next unless applied.include?(name)
154
+
155
+ sections << Keyed.section("BL-#{name}") do |out|
156
+ PIAs.import(container[name], out)
157
+ out.bool("cb.c1_high", drive) if name == "PIA1"
158
+ end
159
+ end
160
+ if applied.include?(VIA::NAME)
161
+ sections << Keyed.section("BL-VIA") { |out| VIA.import(container[VIA::NAME], out) }
162
+ end
163
+ sections + video
164
+ end
165
+
166
+ def machine(out, container, computer, drive)
167
+ out.text("family", "pet")
168
+ PETSetup.of(computer).write_fields(out)
169
+ out.int("cycles", FieldReader.new(container[MainCPU::NAME]).qword).bool("vertical_drive", drive)
170
+ end
171
+
172
+ # The vertical drive line, low in the CRTC's vertical sync or past
173
+ # the discrete video's rows, as the video's module, `video`, has it.
174
+ def driving?(crtc, video, model)
175
+ return true if crtc.nil?
176
+
177
+ fields = Keyed::Reader.parse(video.first)
178
+ if model.video == :discrete
179
+ fields.int("line", 0) < Badline::PET::DiscreteVideo::DISPLAYED_LINES
180
+ else
181
+ fields.int("vsync_lines", 0).zero?
182
+ end
183
+ end
184
+
185
+ # The discrete video's module, or the CRTC's, run on from xpet's
186
+ # counters to where badline's drops the vertical drive line on the
187
+ # cycle xpet's does (CRTC::CRTC_LEAD). The CRTC runs on from the
188
+ # start of the beam's line, or for a beam in the first cycles of
189
+ # its line, a frame less those cycles on, which lands on the same
190
+ # counters while the registers stood through the frame as they
191
+ # stand.
192
+ def video(crtc, computer)
193
+ return [] if crtc.nil?
194
+
195
+ model = computer.model
196
+ return [Keyed.section("BL-VIDEO") { |out| CRTC.import_discrete(crtc, out) }] if model.video == :discrete
197
+
198
+ counters = Keyed.section("BL-CRTC") do |out|
199
+ CRTC.import(crtc, out, CRTC.columns(model), CRTC.attributes(model))
200
+ end
201
+ cycles = (CRTC::CRTC_LEAD + CRTC.column_of(crtc)) % CRTC.frame_cycles(CRTC.registers(crtc))
202
+ fields = CRTC.run_on(CRTC.fresh_crtc(computer), Keyed::Reader.parse(counters), cycles)
203
+ [Keyed.section("BL-CRTC") { |out| CRTC.copy(fields, out) }]
204
+ end
205
+
206
+ def left_out(container, applied)
207
+ container.sections.reject { |section| READ.include?(section.name) || applied.include?(section.name) }
208
+ .map { |section| "#{section}: badline doesn't read this module, left out" }
209
+ end
210
+ end
211
+ end
212
+ end
213
+ end
214
+ end
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Snapshot
5
+ module Vice
6
+ module PET
7
+ # VIA 2.2, the 6522 at $E840: port A's and port B's output and
8
+ # direction registers, timer 1's latch and counter, timer 2's low
9
+ # and high latch, the value it was last loaded with and its counter,
10
+ # which timers will set their interrupt flag (timer 1 in bit 7,
11
+ # timer 2 in bit 6 while it counts cycles), the shift register, the
12
+ # ACR, the PCR, the interrupt flags and enable mask, timer 1's level
13
+ # on PB7 in bit 7, the shift register's clock ticks into its byte,
14
+ # CA2's level in bit 7, and whether timer 2's next underflow sets
15
+ # its flag. xpet saves each counter as a read on the next cycle
16
+ # finds it, where badline's holds it before that cycle counts.
17
+ #
18
+ # Written: timer 2's high latch and the value it was loaded with,
19
+ # which badline doesn't keep, as xpet has them before a program
20
+ # loads it, or while the shift register clocks off it, as its low
21
+ # byte reloads. Free-running, the shift register's ticks are only
22
+ # whether CB1 is low.
23
+ #
24
+ # Read: the registers and both timers with their latches, as they
25
+ # count from the next cycle, and the shift register where its clock
26
+ # ticks are. A load or reload under way, a shift clock tick due off
27
+ # timer 2 and the latched ports start afresh, and CB2's level
28
+ # follows from the PCR.
29
+ module VIA
30
+ NAME = "VIA"
31
+ MAJOR = 2
32
+ MINOR = 2
33
+ T1_ARMED = 0x80
34
+ T2_TIMED = 0x40
35
+ LEVELS = 0x60
36
+ # The shift register's clock ticks a byte takes, which an idle
37
+ # register's count stands at.
38
+ TICKS = 16
39
+ SIZE = 28
40
+ # The shift register's modes that clock off timer 2.
41
+ T2_SHIFTS = [1, 4, 5].freeze
42
+
43
+ module_function
44
+
45
+ def reads?(section) = section.major == MAJOR && section.minor == MINOR
46
+
47
+ # The VIA's module, under `name`, from its BL-VIA fields in `input`.
48
+ def export(input, name = NAME)
49
+ fields = FieldWriter.new
50
+ %w[ora ddra orb ddrb].each { |key| fields.byte(input.int(key, 0)) }
51
+ fields.word(input.int("t1.latch", 0xffff)).word(timer1(input))
52
+ t2 = timer2(input)
53
+ latch_low = input.int("t2.latch_low", 0xff)
54
+ fields.byte(latch_low).byte(0xff).word(shifting?(input) ? (t2 & 0xff00) | latch_low : 0xffff).word(t2)
55
+ fields.byte(armed(input)).byte(input.int("sr.data", 0))
56
+ fields.byte(input.int("acr", 0)).byte(input.int("pcr", 0))
57
+ fields.byte(input.int("ifr.flags", 0)).byte(input.int("ifr.enable", 0))
58
+ fields.byte(input.bool("t1.pb7", true) ? 0x80 : 0)
59
+ fields.byte(ticks(input))
60
+ fields.byte(LEVELS | (input.bool("ca.c2_output", true) ? 0x80 : 0)).zeros(2)
61
+ fields.byte(input.bool("t2.armed", false) ? 1 : 0).zeros(2).section(name, MAJOR, MINOR)
62
+ end
63
+
64
+ # Timer 1's counter as a read on the next cycle finds it: as it
65
+ # stands while a load holds it, the latch while it reloads, and one
66
+ # less otherwise.
67
+ def timer1(input)
68
+ counter = input.int("t1.counter", 0xffff)
69
+ return counter if input.bool("t1.hold", false)
70
+ return input.int("t1.latch", 0xffff) if input.bool("t1.reload", false)
71
+
72
+ (counter - 1) & 0xffff
73
+ end
74
+
75
+ # Timer 2's counter as a read on the next cycle finds it: as it
76
+ # stands while it counts pulses or a load holds it, and one less
77
+ # otherwise.
78
+ def timer2(input)
79
+ counter = input.int("t2.counter", 0xffff)
80
+ return counter if input.bool("t2.hold", false) || input.bool("t2.counting_pulses", false)
81
+
82
+ (counter - 1) & 0xffff
83
+ end
84
+
85
+ # The shift register's clock ticks into its byte: two a bit, the
86
+ # first one dropping CB1, TICKS when idle. Free-running, its bits
87
+ # never run out, and only whether CB1 is low counts.
88
+ def ticks(input)
89
+ bits = input.int("sr.bits", 0)
90
+ return TICKS if bits.zero?
91
+
92
+ low = input.bool("sr.clock", true) ? 0 : 1
93
+ return low if input.int("sr.mode", 0) == 4
94
+
95
+ ((8 - bits) * 2) + low
96
+ end
97
+
98
+ # The shift register from its mode, data and the ticks into its
99
+ # byte (#ticks), with the last bit put out on CB2, or CB2 high
100
+ # as it is at power-on while the register is off.
101
+ def shift_register(out, mode, data, ticks)
102
+ ticks = [ticks, TICKS].min
103
+ bits = ticks == TICKS ? 0 : 8 - (ticks / 2)
104
+ bits = 8 if mode == 4 && ticks < TICKS
105
+ out.int("sr.mode", mode).int("sr.data", data).int("sr.bits", bits).int("sr.delay", 0)
106
+ out.bool("sr.clock", ticks.even?).bool("sr.cb2", mode.zero? || ticks.zero? || data.anybits?(0x01))
107
+ end
108
+
109
+ # Whether the shift register clocks off timer 2.
110
+ def shifting?(input) = T2_SHIFTS.include?((input.int("acr", 0) >> 2) & 0x07)
111
+
112
+ # Timer 2 sets its flag, or clocks the shift register, while it
113
+ # counts cycles.
114
+ def armed(input)
115
+ pulses = input.bool("t2.count_pulses", false)
116
+ timed = !pulses && (input.bool("t2.armed", false) || shifting?(input))
117
+ (input.bool("t1.armed", false) ? T1_ARMED : 0) | (timed ? T2_TIMED : 0)
118
+ end
119
+
120
+ # The registers, the timers and the control lines by their BL-VIA
121
+ # keys, in a keyed module's writer.
122
+ def import(section, out)
123
+ bytes = FieldReader.new(section).bytes(SIZE)
124
+ acr = bytes[0x10]
125
+ pcr = bytes[0x11]
126
+ out.int("ora", bytes[0]).int("ddra", bytes[1]).int("orb", bytes[2]).int("ddrb", bytes[3])
127
+ out.int("acr", acr).int("pcr", pcr)
128
+ timers(out, bytes, acr)
129
+ shift_register(out, (acr >> 2) & 0x07, bytes[0x0f], bytes[0x15])
130
+ out.int("ifr.flags", bytes[0x12] & 0x7f).int("ifr.enable", bytes[0x13] & 0x7f)
131
+ control_lines(out, "ca.", pcr & 0x0f, bytes[0x16].anybits?(0x80))
132
+ control_lines(out, "cb.", pcr >> 4, true)
133
+ end
134
+
135
+ def timers(out, bytes, acr)
136
+ out.int("t1.latch", bytes[4] | (bytes[5] << 8)).int("t1.counter",
137
+ ((bytes[6] | (bytes[7] << 8)) + 1) & 0xffff)
138
+ out.bool("t1.hold", false)
139
+ out.bool("t1.reload", false).bool("t1.armed", bytes[0x0e].anybits?(T1_ARMED))
140
+ out.bool("t1.pb7", bytes[0x14].anybits?(0x80))
141
+ pulses = acr.anybits?(0x20)
142
+ t2 = bytes[0x0c] | (bytes[0x0d] << 8)
143
+ out.int("t2.latch_low", bytes[8]).int("t2.counter", pulses ? t2 : (t2 + 1) & 0xffff).bool("t2.hold", false)
144
+ out.bool("t2.low_reload", false).bool("t2.low_underflowed", false)
145
+ out.bool("t2.armed", bytes[0x19].anybits?(0x01))
146
+ out.bool("t2.count_pulses", pulses).bool("t2.counting_pulses", pulses)
147
+ end
148
+
149
+ # A handshake under way holds C2 low.
150
+ def control_lines(out, prefix, control, c2_high)
151
+ mode = control >> 1
152
+ handshake_low = mode == 4 && !c2_high
153
+ output = case mode
154
+ when 4 then !handshake_low
155
+ when 6 then false
156
+ else true
157
+ end
158
+ out.int("#{prefix}control", control).bool("#{prefix}handshake_low", handshake_low)
159
+ out.int("#{prefix}pulse", 0).bool("#{prefix}c2_output", output)
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end
@@ -38,7 +38,8 @@ module Badline
38
38
 
39
39
  def import(section, computer)
40
40
  registers = FieldReader.new(section).skip(4).bytes(0x20)
41
- registers.first(0x19).each_with_index { |value, reg| computer.sid.poke(0xd400 + reg, value) }
41
+ sid = computer.sid
42
+ registers.first(0x19).each_with_index { |value, reg| sid.poke(sid.start + reg, value) }
42
43
  end
43
44
  end
44
45
  end
@@ -4,20 +4,24 @@ module Badline
4
4
  module Snapshot
5
5
  module Vice
6
6
  module VICII
7
- # Reads a VIC-II module and puts it into badline's VIC.
7
+ # Reads a VIC-II module and puts it into badline's VIC. A reader of
8
+ # another layout passes no fields and sets the same state itself.
8
9
  class Import
9
10
  def initialize(fields)
10
11
  @fields = fields
12
+ return if fields.nil?
13
+
11
14
  read_beam
12
15
  read_counters
13
16
  read_sprites
14
17
  end
15
18
 
16
- def apply(computer)
17
- vic = computer.vic
18
- computer.address_bus.color_ram.tap do |ram|
19
- @color_ram.each_with_index { |value, offset| ram.poke(0xd800 + offset, value) }
20
- end
19
+ def apply(computer) = apply_to(computer.vic, computer.address_bus.color_ram, 0xd800)
20
+
21
+ # Puts it into `vic`, with its colour RAM into `color_ram` from
22
+ # `base`.
23
+ def apply_to(vic, color_ram, base)
24
+ @color_ram.each_with_index { |value, offset| color_ram.poke(base + offset, value) }
21
25
  load_registers(vic)
22
26
  position(vic)
23
27
  load_counters(vic)
@@ -13,6 +13,10 @@ require "badline/snapshot/vice/peripherals"
13
13
  require "badline/snapshot/vice/vicii"
14
14
  require "badline/snapshot/vice/vicii_import"
15
15
  require "badline/snapshot/vice/restore"
16
+ require "badline/snapshot/vice/pet"
17
+ require "badline/snapshot/vice/cbm2"
18
+ require "badline/snapshot/vice/p500"
19
+ require "badline/snapshot/vice/p500_restore"
16
20
 
17
21
  module Badline
18
22
  module Snapshot
@@ -36,8 +36,8 @@ module Badline
36
36
  # end of the instruction its CPU is in, and of an REU transfer under
37
37
  # way (Machine::Settling), so a save advances it by a few cycles, and a
38
38
  # second save straight after runs none. A VIC-20's, a C128's, a
39
- # Plus/4's or a PET's snapshot is in a container named as xvic, x128,
40
- # xplus4 or xpet names its machine.
39
+ # Plus/4's, a PET's or a CBM-II's snapshot is in a container named as
40
+ # xvic, x128, xplus4, xpet or xcbm2 names its machine.
41
41
  def dump(computer) = Image.of(computer).to_s
42
42
 
43
43
  def read(path) = Image.new(Container.parse(File.binread(path)), name: File.basename(path))
@@ -52,7 +52,7 @@ module Badline
52
52
  def restore(computer, path, &) = read(path).restore(computer, &)
53
53
 
54
54
  # A machine's snapshots, as .vsf files and in memory. C64, Vic20, C128,
55
- # Plus4 and PET each include it.
55
+ # Plus4, PET, CBM2 and CBM2::P500 each include it.
56
56
  module Files
57
57
  def save_snapshot(path) = Snapshot.save(self, path)
58
58
 
@@ -99,4 +99,12 @@ module Badline
99
99
  class PET
100
100
  include Snapshot::Files
101
101
  end
102
+
103
+ class CBM2
104
+ include Snapshot::Files
105
+
106
+ class P500
107
+ include Snapshot::Files
108
+ end
109
+ end
102
110
  end
@@ -8,6 +8,7 @@ module Badline
8
8
  SIGNATURE = "C64 CARTRIDGE ".b
9
9
  VIC20_SIGNATURE = "VIC20 CARTRIDGE ".b
10
10
  PLUS4_SIGNATURE = "PLUS4 CARTRIDGE ".b
11
+ CBM2_SIGNATURE = "CBM2 CARTRIDGE ".b
11
12
  CHIP_SIGNATURE = "CHIP".b
12
13
  CHIP_HEADER_SIZE = 0x10
13
14
  RAM_CHIP = 1
@@ -19,6 +20,9 @@ module Badline
19
20
  # What .write needs of an image: the fields a parsed CRTFile reads.
20
21
  Image = Data.define(:hardware_type, :subtype, :exrom, :game, :name, :chips)
21
22
 
23
+ # Whether the file at +path+ is signed as a CBM-II's cartridge.
24
+ def self.cbm2?(path) = File.binread(path, CBM2_SIGNATURE.length) == CBM2_SIGNATURE
25
+
22
26
  def self.write(path, image)
23
27
  File.binwrite(path, encode(image))
24
28
  end
@@ -36,7 +40,8 @@ module Badline
36
40
  end
37
41
 
38
42
  # `bytes` stands in for the file's contents. `machine` is the one the
39
- # image has to be for, :c64, :vic20 or :plus4, or nil for any.
43
+ # image has to be for, :c64, :vic20, :plus4 or :cbm2, or nil for
44
+ # any.
40
45
  def initialize(path = nil, bytes: nil, machine: :c64)
41
46
  parse(bytes || File.binread(path))
42
47
  return if machine.nil? || @machine == machine
@@ -60,6 +65,7 @@ module Badline
60
65
  return :c64 if bytes.start_with?(SIGNATURE)
61
66
  return :vic20 if bytes.start_with?(VIC20_SIGNATURE)
62
67
  return :plus4 if bytes.start_with?(PLUS4_SIGNATURE)
68
+ return :cbm2 if bytes.start_with?(CBM2_SIGNATURE)
63
69
 
64
70
  raise FormatError, "Missing CRT signature"
65
71
  end
@@ -52,6 +52,13 @@ module Badline
52
52
  def bam_tracks = 1..35
53
53
  def bam_count(track) = [18, 0, 4 * track]
54
54
  def bam_bitmap(track) = [18, 0, (4 * track) + 1]
55
+ def bitmap_size = 3
56
+
57
+ # The header links the directory and holds the BAM itself.
58
+ def header_link = [18, 1]
59
+ def dos_type = "2A"
60
+ def header_tail = 4
61
+ def bam_headers(_id) = []
55
62
  end
56
63
  end
57
64
  end
@@ -31,7 +31,7 @@ module Badline
31
31
  def directory_track = 39
32
32
  def directory_sector = 1
33
33
  def directory_interleave = 3
34
- def interleave = 6
34
+ def interleave = 5
35
35
 
36
36
  # Only the directory track is kept from files. The BAM's blocks on
37
37
  # track 38 are marked in use, and the rest of that track holds files.
@@ -43,6 +43,22 @@ module Badline
43
43
  def bam_tracks = 1..SIDE_TRACKS
44
44
  def bam_count(track) = [38, 3 * ((track - 1) / 50), 6 + (5 * ((track - 1) % 50))]
45
45
  def bam_bitmap(track) = [38, 3 * ((track - 1) / 50), 7 + (5 * ((track - 1) % 50))]
46
+ def bitmap_size = 4
47
+
48
+ # The header links the BAM's first block, and each BAM block the next,
49
+ # the last one the directory. A BAM block names the format and the
50
+ # first track it covers and the one past its last.
51
+ def header_link = [38, 0]
52
+ def dos_type = "2C"
53
+ def header_tail = 4
54
+
55
+ def bam_headers(_id)
56
+ groups = bam_tracks.each_slice(50).to_a
57
+ groups.each_with_index.map do |tracks, block|
58
+ link = block == groups.length - 1 ? [directory_track, directory_sector] : [38, 3 * (block + 1)]
59
+ [38, 3 * block, [*link, dos_type.getbyte(1), 0, tracks.first, tracks.last + 1]]
60
+ end
61
+ end
46
62
  end
47
63
  end
48
64
  end
@@ -21,6 +21,20 @@ module Badline
21
21
  def bam_tracks = 1..80
22
22
  def bam_count(track) = [40, track > 40 ? 2 : 1, 0x10 + (((track - 1) % 40) * 6)]
23
23
  def bam_bitmap(track) = [40, track > 40 ? 2 : 1, 0x11 + (((track - 1) % 40) * 6)]
24
+ def bitmap_size = 5
25
+
26
+ # The header links the directory's first block, at 40/3. Each BAM
27
+ # block links the next, names the format and its complement, and
28
+ # holds the ID and the I/O byte.
29
+ def header_link = [40, 3]
30
+ def dos_type = "3D"
31
+ def header_tail = 2
32
+
33
+ def bam_headers(id)
34
+ [[40, 1, [40, 2]], [40, 2, [0, 0xff]]].map do |track, sector, link|
35
+ [track, sector, [*link, 0x44, 0xbb, *id, 0xc0, 0]]
36
+ end
37
+ end
24
38
 
25
39
  def sectors_in(_track) = 40
26
40
  end
@@ -71,17 +71,32 @@ module Badline
71
71
  end
72
72
 
73
73
  # A chain's first block goes on the free track nearest the
74
- # directory. Each block after it goes the interleave further round
75
- # the same track, or on the next track out once that one is full.
74
+ # directory, from its sector 0. Each block after it looks from the
75
+ # interleave further round the track (#next_sector), on the same
76
+ # track or, once that one is full, from the same sector on the next
77
+ # track out.
76
78
  def next_block(track = nil, sector = nil)
77
79
  candidates = track ? [track, *tracks_after(track)] : allocation_order
80
+ start = track ? next_sector(track, sector) : 0
78
81
  candidates.each do |candidate|
79
- free = free_sector(candidate, candidate == track ? sector + interleave : 0)
82
+ free = free_sector(candidate, start)
80
83
  return [candidate, free] if free
81
84
  end
82
85
  nil
83
86
  end
84
87
 
88
+ # The interleave on from the sector. Past the track's last sector
89
+ # it comes round again one short, but never below sector 0, as
90
+ # the 2031, 8050, 8250 and 1581 DOS do.
91
+ def next_sector(track, sector)
92
+ start = sector + interleave
93
+ count = sectors_in(track)
94
+ return start if start < count
95
+
96
+ start -= count
97
+ start.positive? ? start - 1 : start
98
+ end
99
+
85
100
  def free_sector(track, first)
86
101
  count = sectors_in(track)
87
102
  (0...count).map { |i| (first + i) % count }.find { |s| block_free?(track, s) }