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
@@ -37,8 +37,11 @@ module Badline
37
37
  # The machines the doc is made from: each family at power-on, the C64
38
38
  # with each kind of expansion, cartridge and media, and a second SID,
39
39
  # the 264 series' models, its TCBM interface, cartridge, tape and
40
- # true drives, the 1541 and the 1551, and a CRTC PET, a 2001, an 8096
41
- # and a PET with a disk and tapes.
40
+ # true drives, the 1541 and the 1551, a CRTC PET, a 2001, an 8096,
41
+ # an 8296 with its user port jumpered, a PET with a disk and tapes and
42
+ # one with a true 2031, one with an 8050 and one with an 8250, and a
43
+ # 610, a 720, a 610 with a cartridge and one with a disk, and a PAL
44
+ # and an NTSC 510.
42
45
  def self.machines
43
46
  {
44
47
  "c64" => [C64.new, C64.new(ram_expansion: :plus256k), C64.new(reu: 512),
@@ -50,7 +53,11 @@ module Badline
50
53
  Plus4.new(sid_card: :mos8580, sid_card_address: 0xfe80),
51
54
  Plus4.new(ram_expansion: :csory256),
52
55
  plus4_with_cartridge, plus4_with_tape, plus4_with_drive, plus4_with_drive("1551")],
53
- "pet" => [PET.new, PET.new(model: "2001"), PET.new(model: "8096"), pet_with_media]
56
+ "pet" => [PET.new, PET.new(model: "2001"), PET.new(model: "8096"),
57
+ PET.new(model: "8296", characters: "din", jumpers: %w[userport]), pet_with_media,
58
+ pet_with_drive, pet_with_unit("8050"), pet_with_unit("8250")],
59
+ "cbm2" => [CBM2.new, CBM2.new(model: "720", kernal: "03b", mains_hz: 60), cbm2_with_cartridge,
60
+ cbm2_with_disk, CBM2::P500.new, CBM2::P500.new(model: "510ntsc")]
54
61
  }
55
62
  end
56
63
 
@@ -65,8 +72,37 @@ module Badline
65
72
  end
66
73
  end
67
74
 
75
+ # A PET with a 2031 holding a blank disk.
76
+ def self.pet_with_drive
77
+ PET.new.tap do |machine|
78
+ image = Storage::D64Image.new("", bytes: blank(174_848))
79
+ machine.plug_true_drive.insert(Drives::Model1541::Disk.decode(image))
80
+ end
81
+ end
82
+
83
+ # A PET with an 8050 or an 8250 holding a blank disk in drive 0.
84
+ def self.pet_with_unit(model)
85
+ image = if model == "8250"
86
+ Storage::D82Image.new("", bytes: blank(1_066_496))
87
+ else
88
+ Storage::D80Image.new("", bytes: blank(533_248))
89
+ end
90
+ PET.new.tap { |machine| machine.plug_drive8050(model).insert(Drives::Model8050::Disk.new(image)) }
91
+ end
92
+
68
93
  def self.blank(length) = Array.new(length, 0)
69
94
 
95
+ # A 610 with a disk in device 8 through the traps.
96
+ def self.cbm2_with_disk
97
+ CBM2.new.tap { |machine| machine.mount(Storage::D64Image.new("", read_only: true, bytes: blank(174_848))) }
98
+ end
99
+
100
+ # A 610 with an 8K cartridge ROM at $2000.
101
+ def self.cbm2_with_cartridge
102
+ chip = Storage::CRTFile::Chip.new(chip_type: 0, bank: 0, address: 0x2000, data: blank(0x2000))
103
+ CBM2.new.tap { |machine| machine.attach_cartridge([chip]) }
104
+ end
105
+
70
106
  def self.with_cartridge(type)
71
107
  chips = [0x8000, 0xa000].map do |address|
72
108
  Storage::CRTFile::Chip.new(chip_type: 2, bank: 0, address:, data: blank(0x2000))
@@ -34,7 +34,15 @@ module Badline
34
34
  # of a machine in memory (.of).
35
35
  class Image
36
36
  MACHINE = "BL-MACHINE"
37
- FAMILIES = { "c64" => "C64", "vic20" => "VIC-20", "c128" => "C128", "plus4" => "Plus/4", "pet" => "PET" }.freeze
37
+ FAMILIES = { "c64" => "C64", "vic20" => "VIC-20", "c128" => "C128", "plus4" => "Plus/4", "pet" => "PET",
38
+ "cbm2" => "CBM-II" }.freeze
39
+
40
+ # The snapshots VICE's emulators write that badline doesn't read, by
41
+ # the machine their container names.
42
+ UNREAD = {
43
+ Container::VIC20 => "VIC-20 snapshots, not xvic's", Container::C128 => "C128 snapshots, not x128's",
44
+ Container::PLUS4 => "Plus/4 snapshots, not xplus4's"
45
+ }.freeze
38
46
 
39
47
  attr_reader :container
40
48
 
@@ -54,18 +62,37 @@ module Badline
54
62
  end
55
63
 
56
64
  # The snapshot with VICE's modules ahead of the BL-* ones for a C64,
57
- # so x64sc opens it too. Raises FormatError for a Plus/4 or a PET,
58
- # whose snapshots badline doesn't write for xplus4 or xpet.
65
+ # a PET, a CBM-II's B series or a P500, so x64sc, xpet, xcbm2 or
66
+ # xcbm5x0 opens it too. Raises FormatError for a Plus/4, whose
67
+ # snapshots badline doesn't write for xplus4.
59
68
  def self.exported(computer)
60
69
  raise FormatError, "badline doesn't export a Plus/4's snapshot for VICE" if computer.family == :plus4
61
- raise FormatError, "badline doesn't export a PET's snapshot for VICE" if computer.family == :pet
62
70
 
63
- computer.settle_for_snapshot!
71
+ computer.settle_for_vice!
64
72
  sections = computer.save_modules
65
- sections = Vice.export(computer) + sections if computer.family == :c64
73
+ case computer.family
74
+ when :c64 then sections = Vice.export(computer) + sections
75
+ when :pet then sections = Vice::PET.export(computer, sections) + sections
76
+ when :cbm2 then sections = cbm2_export(computer, sections) + sections
77
+ end
66
78
  new(Container.new(sections, machine: machine_name(computer)))
67
79
  end
68
80
 
81
+ # xcbm2's or xcbm5x0's modules for a CBM-II.
82
+ def self.cbm2_export(computer, sections)
83
+ computer.is_a?(CBM2::P500) ? Vice::P500.export(computer, sections) : Vice::CBM2.export(computer, sections)
84
+ end
85
+
86
+ # The VICE emulator a machine's VICE modules are for.
87
+ def self.vice_emulator(computer)
88
+ case machine_name(computer)
89
+ when Container::PET then "xpet"
90
+ when Container::CBM2 then "xcbm2"
91
+ when Container::CBM2_5X0 then "xcbm5x0"
92
+ else "x64sc"
93
+ end
94
+ end
95
+
69
96
  # The machine as VICE's emulators name it.
70
97
  def self.machine_name(computer)
71
98
  case computer.family
@@ -73,6 +100,7 @@ module Badline
73
100
  when :c128 then Container::C128
74
101
  when :plus4 then Container::PLUS4
75
102
  when :pet then Container::PET
103
+ when :cbm2 then computer.is_a?(CBM2::P500) ? Container::CBM2_5X0 : Container::CBM2
76
104
  else Container::MACHINE
77
105
  end
78
106
  end
@@ -85,6 +113,7 @@ module Badline
85
113
  when "c128" then C128Setup.read_fields(machine)
86
114
  when "plus4" then Plus4Setup.read_fields(machine)
87
115
  when "pet" then PETSetup.read_fields(machine)
116
+ when "cbm2" then CBM2Setup.read_fields(machine)
88
117
  else C64Setup.read_fields(machine)
89
118
  end
90
119
  end
@@ -100,16 +129,16 @@ module Badline
100
129
 
101
130
  # A new machine, built as the snapshot's was and restored from it. A
102
131
  # badline snapshot restores from its BL-* modules alone, and a VICE
103
- # one through the modules badline reads. xvic's, x128's, xplus4's and
104
- # xpet's own modules aren't read. Yields a line for each thing left
105
- # out, or warns it without a block. `detached` restores it without
106
- # the host's files (Keyed::Modules).
132
+ # one through the modules badline reads, x64sc's, xpet's, xcbm2's or xcbm5x0's. xvic's,
133
+ # x128's and xplus4's own modules aren't read. Yields a line for
134
+ # each thing left out, or warns it without a block. `detached`
135
+ # restores it without the host's files (Keyed::Modules).
107
136
  def load(detached: false, &)
108
137
  refuse_old_layout
109
- raise FormatError, "badline reads only its own VIC-20 snapshots, not xvic's" if vic20? && !badline?
110
- raise FormatError, "badline reads only its own C128 snapshots, not x128's" if c128? && !badline?
111
- raise FormatError, "badline reads only its own Plus/4 snapshots, not xplus4's" if plus4? && !badline?
112
- raise FormatError, "badline reads only its own PET snapshots, not xpet's" if pet? && !badline?
138
+ refuse_unread
139
+ return load_xpet(&) if pet? && !badline?
140
+ return load_xcbm2(&) if cbm2? && !badline?
141
+ return load_xcbm5x0(&) if p500? && !badline?
113
142
  return load_vice(&) unless badline?
114
143
 
115
144
  modules = Keyed::Modules.new(sections, detached:)
@@ -130,12 +159,24 @@ module Badline
130
159
  # Whether the machine is a PET, as the container names it.
131
160
  def pet? = @container.machine == Container::PET
132
161
 
162
+ # Whether the machine is a CBM-II's B series, as the container names
163
+ # it.
164
+ def cbm2? = @container.machine == Container::CBM2
165
+
166
+ # Whether the machine is a P500, as the container names it.
167
+ def p500? = @container.machine == Container::CBM2_5X0
168
+
133
169
  # Restores `computer` from the snapshot, and returns the Report. The
134
170
  # snapshot goes into a new machine first, so one that fails, or is of
135
171
  # a machine built another way, leaves `computer` as it was.
136
172
  def restore(computer, &)
137
173
  refuse_old_layout
138
- report = badline? ? restore_modules(computer) : Vice::Restore.import(@container, computer)
174
+ report = if badline? then restore_modules(computer)
175
+ elsif pet? then Vice::PET::Restore.import(@container, computer)
176
+ elsif cbm2? then Vice::CBM2::Restore.import(@container, computer)
177
+ elsif p500? then Vice::P500::Restore.import(@container, computer)
178
+ else Vice::Restore.import(@container, computer)
179
+ end
139
180
  tell(report, &)
140
181
  report
141
182
  end
@@ -148,6 +189,24 @@ module Badline
148
189
  computer
149
190
  end
150
191
 
192
+ def load_xpet(&)
193
+ computer = Vice::PET.setup(@container).build
194
+ tell(Vice::PET::Restore.apply(@container, computer), &)
195
+ computer
196
+ end
197
+
198
+ def load_xcbm2(&)
199
+ computer = Vice::CBM2.setup(@container).build
200
+ tell(Vice::CBM2::Restore.apply(@container, computer), &)
201
+ computer
202
+ end
203
+
204
+ def load_xcbm5x0(&)
205
+ computer = Vice::P500.setup(@container).build
206
+ tell(Vice::P500::Restore.apply(@container, computer), &)
207
+ computer
208
+ end
209
+
151
210
  # The snapshot goes into a new machine first, so one that fails
152
211
  # leaves `computer` as it was.
153
212
  def restore_modules(computer)
@@ -164,6 +223,11 @@ module Badline
164
223
  report(modules, computer)
165
224
  end
166
225
 
226
+ def refuse_unread
227
+ unread = UNREAD[@container.machine]
228
+ raise FormatError, "badline reads only its own #{unread}" if unread && !badline?
229
+ end
230
+
167
231
  # A snapshot from a badline before the keyed modules, which only the
168
232
  # version that saved it reads.
169
233
  def refuse_old_layout
@@ -4,9 +4,10 @@ module Badline
4
4
  module Snapshot
5
5
  # How a PET was built: its model's name, one of PET::Model::ALL, its
6
6
  # RAM below $8000 in K and its editor's frame rate, the arguments
7
- # PET.new takes, and the ROMs it runs by file: BASIC, the editor, the
8
- # KERNAL and the characters.
9
- PETSetup = Data.define(:model, :ram_kb, :refresh_hz, :basic, :editor, :kernal, :characters)
7
+ # PET.new takes, the ROMs it runs by file: BASIC, the editor, the
8
+ # KERNAL and the characters, and the 8296 board's jumpers, by name and
9
+ # comma-separated.
10
+ PETSetup = Data.define(:model, :ram_kb, :refresh_hz, :basic, :editor, :kernal, :characters, :jumpers)
10
11
 
11
12
  class PETSetup
12
13
  MODELS = PET::Model::ALL.map(&:name).freeze
@@ -18,22 +19,25 @@ module Badline
18
19
  model = machine.model
19
20
  refresh_hz = machine.refresh_hz
20
21
  new(model: model.name, ram_kb: machine.ram.length >> 10, refresh_hz:, basic: model.basic,
21
- editor: model.editor(refresh_hz), kernal: model.kernal, characters: model.characters)
22
+ editor: model.editor(refresh_hz), kernal: model.kernal, characters: model.characters,
23
+ jumpers: machine.jumpers.join(","))
22
24
  end
23
25
 
24
- # The model by name, the RAM, the frame rate and the ROMs, in a keyed
25
- # snapshot module.
26
+ # The model by name, the RAM, the frame rate, the ROMs and the
27
+ # jumpers where a board has any, in a keyed snapshot module.
26
28
  def write_fields(out)
27
29
  out.text("model", model).int("ram_kb", ram_kb).int("refresh_hz", refresh_hz)
28
30
  out.text("rom.basic", basic).text("rom.editor", editor).text("rom.kernal", kernal)
29
31
  out.text("rom.characters", characters)
32
+ out.text("jumpers", jumpers) unless jumpers.empty?
30
33
  end
31
34
 
32
- # A ROM the snapshot doesn't name is the model's own.
35
+ # A ROM the snapshot doesn't name is the model's own, and a board
36
+ # without jumpers named has none fitted.
33
37
  def self.read_fields(input)
34
38
  profile = PET::Model.named(input.choice("model", MODELS, PET::Model::DEFAULT))
35
39
  ram_kb = input.int("ram_kb", profile.ram_kb)
36
- unless PET::Model::RAM_SIZES.include?(ram_kb)
40
+ unless PET::Model.ram_fits?(profile, ram_kb)
37
41
  raise FormatError, "the snapshot is of a PET with #{ram_kb}K of RAM, which badline doesn't build"
38
42
  end
39
43
 
@@ -46,26 +50,39 @@ module Badline
46
50
  new(model: profile.name, ram_kb:, refresh_hz:, basic: input.text("rom.basic", profile.basic),
47
51
  editor: input.text("rom.editor", profile.editor(refresh_hz)),
48
52
  kernal: input.text("rom.kernal", profile.kernal),
49
- characters: input.text("rom.characters", profile.characters))
53
+ characters: input.text("rom.characters", profile.characters), jumpers: input.text("jumpers", ""))
50
54
  end
51
55
 
52
- # A machine built this way, at power-on, with the BASIC and editor
53
- # PET::ROMSets names for its ROMs.
56
+ # A machine built this way, at power-on, with the BASIC, editor and
57
+ # character ROMs PET::ROMSets names for its ROMs.
54
58
  def build
55
59
  version = PET::ROMSets::BASICS.find { |_, roms| roms.first == basic }&.first
56
- PET.new(model:, ram_kb:, refresh_hz:, basic: version, editor: PET::ROMSets::EDITORS.key(editor))
60
+ PET.new(model:, ram_kb:, refresh_hz:, basic: version, editor: PET::ROMSets::EDITORS.key(editor),
61
+ characters: fitted_characters, jumpers: jumpers.split(","))
57
62
  rescue ArgumentError => e
58
63
  raise FormatError, "the snapshot is of a #{self}, which badline doesn't build: #{e.message}"
59
64
  end
60
65
 
61
- # The ROMs are named where they aren't the model's own.
66
+ # The ROMs are named where they aren't the model's own, and the
67
+ # jumpers where any are fitted.
62
68
  def to_s
63
69
  text = "#{model} with #{ram_kb}K at #{refresh_hz} Hz"
70
+ text = "#{text} jumpered #{jumpers}" unless jumpers.empty?
64
71
  roms = [basic, editor, kernal, characters]
65
72
  profile = PET::Model.named(model)
66
73
  stock = [profile.basic, profile.editor(refresh_hz), profile.kernal, profile.characters]
67
74
  roms == stock ? text : "#{text} running #{roms.join(', ')}"
68
75
  end
76
+
77
+ private
78
+
79
+ # The name of a character ROM other than the model's own, nil for its
80
+ # own.
81
+ def fitted_characters
82
+ return nil if characters == PET::Model.named(model).characters
83
+
84
+ PET::ROMSets::CHARACTERS.key(characters) || characters
85
+ end
69
86
  end
70
87
  end
71
88
  end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "badline/snapshot/vice/cbm2_mem"
4
+ require "badline/snapshot/vice/cbm2_tpi"
5
+ require "badline/snapshot/vice/cbm2_acia"
6
+ require "badline/snapshot/vice/cbm2_crtc"
7
+ require "badline/snapshot/vice/cbm2_mains"
8
+ require "badline/snapshot/vice/cbm2_restore"
9
+
10
+ module Badline
11
+ module Snapshot
12
+ module Vice
13
+ # xcbm2's modules for the CBM-II's B series: the CPU, the RAM, the
14
+ # CRTC, the CIA, both TPIs, the ACIA and the SID, written from
15
+ # badline's machine and read into it. Each module's file says what
16
+ # maps and what is approximated. The CPU's, the CIA's and the SID's
17
+ # are the PET's and the C64's. They go through the machine's own
18
+ # keyed fields (Keyed), which a save writes and a load reads, but for
19
+ # the CIA and the SID, which go through the chips as the C64's do.
20
+ module CBM2
21
+ # The models badline builds by the RAM board's size in K and
22
+ # whether the model line is a 700's.
23
+ MODELS = { [128, false] => "610", [256, false] => "620", [128, true] => "710", [256, true] => "720" }.freeze
24
+ # The modules xcbm2 resets the machine without, each as xcbm2 leaves
25
+ # them with nothing attached: no cartridge, no file system drive
26
+ # open, the cassette port on with a datasette in it, and nothing in
27
+ # the user port.
28
+ PERIPHERALS = [["CBM2CART", 0, 1, [0x00]], ["FSDRIVE", 0, 0, Array.new(258, 0)],
29
+ ["TAPEPORT", 1, 0, [0x01, 0x01]], ["USERPORT", 1, 0, [0x01, 0x00]]].freeze
30
+
31
+ module_function
32
+
33
+ # The modules xcbm2 3.10 reads, in its order, describing `machine`,
34
+ # which Machine::Settling has brought between instructions, from
35
+ # its keyed modules, `sections`.
36
+ def export(machine, sections)
37
+ readers = {}
38
+ sections.each { |section| readers[section.name] = Keyed::Reader.parse(section) }
39
+ machine.sid.catch_up!
40
+ cart, *rest = PERIPHERALS.map do |name, major, minor, bytes|
41
+ FieldWriter.new.bytes(bytes).section(name, major, minor)
42
+ end
43
+ [PET::MainCPU.write(machine, machine.tpi1.irq? ? 1 : 0), CBM2Mem.export(machine), cart,
44
+ CRTC.export(machine, reader(readers, "BL-CRTC"), reader(readers, "BL-VIDEO")),
45
+ CIAs.section(machine.cia, "CIA1"),
46
+ *TPI::NAMES.map { |name| TPI.export(name, reader(readers, "BL-#{name}")) },
47
+ ACIA.export(reader(readers, "BL-ACIA"), machine.cycles),
48
+ SIDRegisters.export(machine), SIDExtended.export(machine), *drives(machine, readers), *rest]
49
+ end
50
+
51
+ # The true drive's modules, as xpet's (PET::Drive2031,
52
+ # PET::Drive8050), or with no true drive, DRIVE8 to DRIVE11 as
53
+ # xcbm2 and xcbm5x0 save no drive.
54
+ def drives(machine, readers)
55
+ return no_drives unless machine.true_drive
56
+
57
+ PET::Drive2031.export(machine, readers) + PET::Drive8050.export(machine, readers)
58
+ end
59
+
60
+ def no_drives = PET::Drive2031::UNITS.map { |name| PET::Drive2031.unit(PET::Drive2031::NONE, name) }
61
+
62
+ def reader(readers, name) = readers[name] || Keyed::Reader.missing(name)
63
+
64
+ # How the machine xcbm2 saved was built: the model from the RAM
65
+ # board's size and the model line, and the mains from the model
66
+ # line too, 60 Hz on a 600 whose line says so and 50 Hz otherwise:
67
+ # xcbm2 has no 700 at 60 Hz. xcbm2 doesn't save its ROMs, so the
68
+ # KERNAL is 901244-04a, xcbm2's own.
69
+ def setup(container)
70
+ memory = memory(container)
71
+ ram_kb = CBM2Mem.ram_kb(memory)
72
+ line = CBM2Mem.line(memory)
73
+ unless CBM2Mem.mapped(memory).zero?
74
+ raise FormatError, "#{memory}: RAM in segment 15 beyond the B series', which badline doesn't build"
75
+ end
76
+
77
+ model = MODELS[[ram_kb, line == CBM2Mem::M700]]
78
+ raise FormatError, "#{memory}: a CBM-II with #{ram_kb}K of RAM, which badline doesn't build" if model.nil?
79
+
80
+ CBM2Setup.new(model:, kernal: "04a", mains_hz: line == CBM2Mem::M600_60HZ ? 60 : 50, refresh: true)
81
+ end
82
+
83
+ def memory(container)
84
+ section = container[CBM2Mem::NAME]
85
+ raise FormatError, "the snapshot has no #{CBM2Mem::NAME} module" if section.nil?
86
+ raise FormatError, "#{section}: badline doesn't read this version" unless CBM2Mem.reads?(section)
87
+
88
+ section
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Snapshot
5
+ module Vice
6
+ module CBM2
7
+ # Acia1 1.1, the 6551: the transmitter's and the receiver's data
8
+ # registers, the status, the command and control registers, the
9
+ # transmitter's state (0 idle, 2 a byte waiting), then four clocks:
10
+ # the cycles to the transmitter's next character end, a clock
11
+ # xcbm2 leaves 0, the cycles to its next receiver poll and another
12
+ # 0.
13
+ #
14
+ # Written: the registers, the status as a read finds it, and while
15
+ # the transmitter is on, the cycles to its character's end in both
16
+ # clocks. Read: the registers, the receiver full, the transmitter
17
+ # empty, the interrupt and the errors from the status, and the
18
+ # transmitter's character end from its clock, or one a character's
19
+ # time on when the clock has run out. Nothing is plugged
20
+ # in, in either, and a character being shifted out is lost.
21
+ module ACIA
22
+ NAME = "Acia1"
23
+ MAJOR = 1
24
+ MINOR = 1
25
+ WAITING = 2
26
+ ERRORS = 0x07
27
+ RECEIVER_FULL = 0x08
28
+ TRANSMITTER_EMPTY = 0x10
29
+ IRQ = 0x80
30
+
31
+ module_function
32
+
33
+ def reads?(section) = section.major == MAJOR && section.minor == MINOR
34
+
35
+ # The module from the ACIA's BL-ACIA fields in `input`, for a
36
+ # machine at `cycles`.
37
+ def export(input, cycles)
38
+ empty = input.bool("transmitter_empty", true)
39
+ status = input.int("errors", 0) | Chips::ACIA::DCD | Chips::ACIA::DSR
40
+ status |= RECEIVER_FULL if input.bool("receiver_full", false)
41
+ status |= TRANSMITTER_EMPTY if empty
42
+ status |= IRQ if input.bool("irq", false)
43
+ at = input.int("transmit_at", Chips::ACIA::NEVER)
44
+ due = at == Chips::ACIA::NEVER ? 0 : [at - cycles, 0].max
45
+ FieldWriter.new.byte(input.int("transmit_data", 0)).byte(input.int("receive_data", 0)).byte(status)
46
+ .byte(input.int("command", 0)).byte(input.int("control", 0)).byte(empty ? 0 : WAITING)
47
+ .dword(due).dword(0).dword(due).dword(0).section(NAME, MAJOR, MINOR)
48
+ end
49
+
50
+ # The ACIA by its BL-ACIA keys, in a keyed module's writer, for a
51
+ # machine at `cycles`.
52
+ def import(section, out, cycles)
53
+ fields = FieldReader.new(section)
54
+ transmit, receive, status, command, control = fields.bytes(5)
55
+ fields.skip(1)
56
+ due = fields.dword
57
+ out.int("command", command).int("control", control).int("errors", status & ERRORS)
58
+ out.bool("receiver_full", status.anybits?(RECEIVER_FULL))
59
+ out.bool("transmitter_empty", status.anybits?(TRANSMITTER_EMPTY)).bool("irq", status.anybits?(IRQ))
60
+ out.int("receive_data", receive).int("transmit_data", transmit).int("shifting", Chips::ACIA::IDLE)
61
+ out.int("transmit_remainder", 0)
62
+ return out.int("transmit_at", Chips::ACIA::NEVER) unless transmitting?(command)
63
+
64
+ out.optional_int("transmit_at", due.positive? ? cycles + due : nil)
65
+ end
66
+
67
+ def transmitting?(command)
68
+ control = command & 0x0c
69
+ control != 0 && control != 0x0c
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Snapshot
5
+ module Vice
6
+ module CBM2
7
+ # CRTC 1.2 as xcbm2 saves it, laid out as xpet's (PET::CRTC): the
8
+ # board's screen settings, the registers, the one selected, the
9
+ # beam, the picture's size and the frame's lines, then xcbm2's
10
+ # picture. xcbm2's board has the 6845's cursor and reads one byte a
11
+ # character from 2K of screen RAM. After the beam's line come the
12
+ # fields left in the cursor's blink phase, counted down from 32, and
13
+ # whether the cursor shows: xcbm2 blinks it 32 fields on and 32 off,
14
+ # where badline's 6845 blinks it 16 and 16, or 8 and 8.
15
+ #
16
+ # xcbm2 ticks the mains once a frame, as vertical sync ends, so its
17
+ # mains follows the CRTC (Mains).
18
+ #
19
+ # Written: the registers and the beam as badline's CRTC has them,
20
+ # and the cursor shown as badline's is, its phase scaled to xcbm2's.
21
+ # The picture is left blank, as xcbm2 paints it again. Read: the
22
+ # registers and the beam as xpet's are (PET::CRTC), the cursor shown
23
+ # as xcbm2's is, its phase scaled to badline's, and where the
24
+ # picture is in its frame. The picture starts again with the next
25
+ # frame.
26
+ module CRTC
27
+ NAME = PET::CRTC::NAME
28
+ SETTINGS = [0xff, 0x07, 0x00, 0x10, 0x00, 0x20, 0x00, 0xe0, 0xff, 0x3f].freeze
29
+ # The 6845's cursor, one byte a character, and not a 2001.
30
+ BOARD = [0x01, 0x01, 0x00].freeze
31
+ WIDTH = 704
32
+ # The picture's lines and the line its first row starts on, for a
33
+ # 600 and a 700.
34
+ FRAMES = { false => [266, 33], true => [366, 8] }.freeze
35
+ BLINK = 32
36
+ # Where R10 is in the module, and the blink's fields left.
37
+ R10_AT = PET::CRTC::SETTINGS.length + 5 + 10
38
+ BLINK_AT = PET::CRTC::SELECTED + 4
39
+ # R10's blink of 8 fields on and 8 off.
40
+ FAST_BLINK = 0x40
41
+
42
+ module_function
43
+
44
+ def reads?(section) = PET::CRTC.reads?(section)
45
+
46
+ def fresh_crtc(machine) = CBM2Setup.of(machine).build.crtc
47
+
48
+ # The module from the BL-CRTC and BL-VIDEO fields in `crtc_input`
49
+ # and `video_input`.
50
+ def export(machine, crtc_input, video_input)
51
+ registers = PET::CRTC.crtc_registers(crtc_input)
52
+ beam = beam(machine, crtc_input, registers)
53
+ height, top = FRAMES.fetch(machine.model.high_profile)
54
+ fields = FieldWriter.new.bytes(SETTINGS).word(0).bytes(BOARD).bytes(registers)
55
+ fields.byte(crtc_input.int("selected", 0)).byte(beam.column).byte(beam.row).byte(beam.raster)
56
+ fields.bytes(blink(video_input.int("frames", 0), registers[10])).byte(1)
57
+ fields.word(0).word(beam.start & PET::CRTC::SCREEN_BITS).byte(beam.vsync).byte(0).flag(beam.shown)
58
+ fields.word(WIDTH).word(height).byte(PET::CRTC::AFTER_SIZE).zeros(3).byte(top).byte(0)
59
+ fields.word(PET::CRTC.frame_lines(registers)).word(beam.frame_line).byte(1)
60
+ fields.dword(0).dword(WIDTH).dword(height).dword(WIDTH).zeros(WIDTH * (height + 4))
61
+ fields.section(NAME, PET::CRTC::MAJOR, PET::CRTC::MINOR)
62
+ end
63
+
64
+ # The beam where xcbm2's is with badline's CRTC where `input` has
65
+ # it (PET::CRTC::CRTC_LEAD). On a line's last character xcbm2's
66
+ # beam has moved on to the next line, a cycle before it starts.
67
+ def beam(machine, input, registers)
68
+ cycles = -PET::CRTC::CRTC_LEAD % PET::CRTC.frame_cycles(registers)
69
+ beam = PET::CRTC.crtc_beam(PET::CRTC.run_on(fresh_crtc(machine), input, cycles), registers)
70
+ return beam unless beam.column == registers[0]
71
+
72
+ PET::CRTC.crtc_beam(PET::CRTC.run_on(fresh_crtc(machine), input, cycles + 1), registers).with(column: 0xff)
73
+ end
74
+
75
+ # The fields left in xcbm2's blink phase and whether its cursor
76
+ # shows, for badline's fields counted, `frames`, and R10.
77
+ def blink(frames, r10)
78
+ fast = r10 & 0x60 == FAST_BLINK
79
+ shown = frames.nobits?(fast ? 0x08 : 0x10)
80
+ phase = fast ? (frames & 0x07) << 1 : frames & 0x0f
81
+ [BLINK - (phase << 1), shown ? 1 : 0]
82
+ end
83
+
84
+ # badline's fields counted for xcbm2's blink in `section`.
85
+ def frames(section)
86
+ r10 = FieldReader.new(section).skip(R10_AT).byte
87
+ left, shown = FieldReader.new(section).skip(BLINK_AT).bytes(2)
88
+ phase = ((BLINK - [left, BLINK].min) >> 1) & 0x0f
89
+ return (shown.zero? ? 0x08 : 0) | (phase >> 1) if r10 & 0x60 == FAST_BLINK
90
+
91
+ (shown.zero? ? 0x10 : 0) | phase
92
+ end
93
+
94
+ # The BL-CRTC module, run on from xcbm2's beam as xpet's is.
95
+ def import(section, machine)
96
+ counters = Keyed.section("BL-CRTC") { |out| PET::CRTC.import(section, out, 1, 0) }
97
+ frame = PET::CRTC.frame_cycles(PET::CRTC.registers(section))
98
+ cycles = (PET::CRTC::CRTC_LEAD + PET::CRTC.column_of(section)) % frame
99
+ fields = PET::CRTC.run_on(fresh_crtc(machine), Keyed::Reader.parse(counters), cycles)
100
+ Keyed.section("BL-CRTC") { |out| PET::CRTC.copy(fields, out) }
101
+ end
102
+
103
+ # The BL-VIDEO module for a CRTC whose BL-CRTC fields `crtc` holds:
104
+ # the cursor's blink, the line the frame paints next and the frame's
105
+ # layout. The picture stays as the machine has it.
106
+ def video(section, crtc)
107
+ registers = PET::CRTC.crtc_registers(crtc)
108
+ Keyed.section("BL-VIDEO") do |out|
109
+ out.int("frames", frames(section)).int("line", painted(crtc, registers)).bool("line_shown", false)
110
+ out.int("origin", Badline::CBM2::CRTCVideo::LEFT - ((registers[0] + 1 - registers[2]) << 3))
111
+ out.ints("layout", registers.values_at(0, 1, 2, 4, 5, 6, 9, 12, 13))
112
+ end
113
+ end
114
+
115
+ # The lines the frame has displayed so far.
116
+ def painted(crtc, registers)
117
+ lines = registers[9] + 1
118
+ shown = registers[6] * lines
119
+ return shown if crtc.bool("adjusting", false) || !crtc.bool("row_display", true)
120
+
121
+ [(crtc.int("row", 0) * lines) + crtc.int("line", 0), shown].min
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Snapshot
5
+ module Vice
6
+ module CBM2
7
+ # The mains on TPI1's I0. xcbm2 saves no mains: it ticks it once a
8
+ # frame of its CRTC, as vertical sync ends, whatever the mains is
9
+ # set to. A machine read from its snapshot has its mains' next
10
+ # falling edge where xcbm2's tick would come, and runs it at its own
11
+ # 50 or 60 Hz from there. A snapshot written for xcbm2 ticks where
12
+ # its CRTC says.
13
+ module Mains
14
+ # The cycle of the line where vertical sync ends that the tick
15
+ # latches on, counted as badline's CRTC counts columns.
16
+ TICK_COLUMN = 0
17
+
18
+ module_function
19
+
20
+ # The cycles to the mains' next falling edge for a CRTC whose
21
+ # BL-CRTC fields `crtc` holds.
22
+ def next_tick(crtc)
23
+ registers = crtc.bytes("registers", "".b).bytes
24
+ regs = Array.new(Chips::CRTC::MASKS.length) { |index| registers[index] || 0 }
25
+ line_cycles = regs[0] + 1
26
+ lines = regs[9] + 1
27
+ rows = regs[4] + 1
28
+ frame_lines = (rows * lines) + regs[5]
29
+ line = crtc.int("line", 0)
30
+ line += crtc.bool("adjusting", false) ? rows * lines : crtc.int("row", 0) * lines
31
+ tick_line = ((regs[7] * lines) + Chips::CRTC::VSYNC_LINES) % frame_lines
32
+ frame = line_cycles * frame_lines
33
+ cycles = ((tick_line * line_cycles) + TICK_COLUMN - ((line * line_cycles) + crtc.int("column", 0))) % frame
34
+ cycles.zero? ? frame : cycles
35
+ end
36
+
37
+ # The BL-MACHINE mains fields for a mains `half_period` cycles high
38
+ # and as long low, whose next falling edge is `cycles` away.
39
+ def write(out, cycles, half_period)
40
+ high = cycles <= half_period
41
+ countdown = high ? cycles : [cycles - half_period, half_period].min
42
+ out.int("mains_countdown", countdown).bool("mains_high", high)
43
+ high
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end