badline 0.14.0 → 0.15.1

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 (116) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +86 -0
  3. data/CLAUDE.md +22 -5
  4. data/README.md +101 -11
  5. data/doc/media.md +19 -0
  6. data/doc/pinned-behaviour.md +145 -0
  7. data/doc/snapshot-format.md +244 -46
  8. data/doc/snapshots.md +18 -1
  9. data/exe/badline-ruby +1 -1
  10. data/lib/badline/audio/bare_player.rb +4 -8
  11. data/lib/badline/audio/machine_player.rb +4 -26
  12. data/lib/badline/c128/banks.rb +1 -3
  13. data/lib/badline/c128/bus.rb +19 -1
  14. data/lib/badline/c128/mmu_pages.rb +7 -0
  15. data/lib/badline/c64/bus/extra_sids.rb +80 -0
  16. data/lib/badline/c64/bus.rb +1 -0
  17. data/lib/badline/c64/cartridge/action_replay.rb +6 -2
  18. data/lib/badline/c64/cartridge/comal80.rb +1 -0
  19. data/lib/badline/c64/cartridge/dinamic.rb +3 -1
  20. data/lib/badline/c64/cartridge/final_cartridge3.rb +1 -0
  21. data/lib/badline/c64/cartridge/fun_play.rb +2 -1
  22. data/lib/badline/c64/cartridge/game_system.rb +2 -1
  23. data/lib/badline/c64/cartridge/keyed_state.rb +155 -34
  24. data/lib/badline/c64/cartridge/magic_desk.rb +2 -1
  25. data/lib/badline/c64/cartridge/ocean.rb +1 -0
  26. data/lib/badline/c64/cartridge/pagefox.rb +1 -0
  27. data/lib/badline/c64/cartridge/rgcd.rb +2 -1
  28. data/lib/badline/c64/cartridge/super_games.rb +3 -2
  29. data/lib/badline/c64/cartridge/zaxxon.rb +1 -0
  30. data/lib/badline/c64/cartridge.rb +3 -3
  31. data/lib/badline/c64/keyed_modules.rb +6 -2
  32. data/lib/badline/c64/true_drives.rb +13 -1
  33. data/lib/badline/c64.rb +6 -0
  34. data/lib/badline/chips/ted/beam.rb +13 -14
  35. data/lib/badline/chips/ted/fetches.rb +199 -0
  36. data/lib/badline/chips/ted/keyed_state.rb +156 -0
  37. data/lib/badline/chips/ted/modes.rb +134 -0
  38. data/lib/badline/chips/ted/palette.rb +43 -0
  39. data/lib/badline/chips/ted/picture.rb +182 -0
  40. data/lib/badline/chips/ted/sound/output.rb +86 -0
  41. data/lib/badline/chips/ted/sound.rb +284 -0
  42. data/lib/badline/chips/ted.rb +53 -6
  43. data/lib/badline/chips/tia.rb +28 -0
  44. data/lib/badline/chips/vic/register_log.rb +4 -1
  45. data/lib/badline/drives/datasette/keyed_state.rb +14 -4
  46. data/lib/badline/drives/datasette.rb +22 -5
  47. data/lib/badline/drives/engine/core.rb +6 -14
  48. data/lib/badline/drives/engine/idle.rb +42 -4
  49. data/lib/badline/drives/engine/idle_cpu.rb +18 -0
  50. data/lib/badline/drives/engine/serial_connection.rb +25 -0
  51. data/lib/badline/drives/engine/sleep.rb +1 -0
  52. data/lib/badline/drives/engine.rb +1 -0
  53. data/lib/badline/drives/model1541/byte_ready.rb +19 -3
  54. data/lib/badline/drives/model1541/mechanism.rb +10 -1
  55. data/lib/badline/drives/model1541.rb +1 -0
  56. data/lib/badline/drives/model1551/bus.rb +138 -0
  57. data/lib/badline/drives/model1551/cable.rb +104 -0
  58. data/lib/badline/drives/model1551/cpu.rb +14 -0
  59. data/lib/badline/drives/model1551/keyed_state.rb +65 -0
  60. data/lib/badline/drives/model1551/port.rb +74 -0
  61. data/lib/badline/drives/model1551/tia.rb +92 -0
  62. data/lib/badline/drives/model1551.rb +170 -0
  63. data/lib/badline/drives/model1571.rb +1 -0
  64. data/lib/badline/drives/model1581.rb +1 -0
  65. data/lib/badline/frontend/boot.rb +23 -2
  66. data/lib/badline/frontend/controls.rb +7 -3
  67. data/lib/badline/frontend/keys.rb +14 -2
  68. data/lib/badline/frontend/media_slots.rb +11 -1
  69. data/lib/badline/frontend/menu/menu_dialogs.rb +14 -5
  70. data/lib/badline/frontend/menu/menu_media.rb +102 -14
  71. data/lib/badline/frontend/menu/menu_pages.rb +16 -11
  72. data/lib/badline/frontend/menu/menu_ports.rb +13 -1
  73. data/lib/badline/frontend/player/player_controller.rb +11 -4
  74. data/lib/badline/frontend/screen.rb +33 -12
  75. data/lib/badline/frontend/screens.rb +2 -1
  76. data/lib/badline/frontend/sound.rb +4 -4
  77. data/lib/badline/frontend/timeline.rb +19 -6
  78. data/lib/badline/machine/settling.rb +1 -1
  79. data/lib/badline/machine.rb +30 -6
  80. data/lib/badline/media/plus4_media.rb +68 -0
  81. data/lib/badline/media/program.rb +20 -6
  82. data/lib/badline/media/true_drive.rb +7 -5
  83. data/lib/badline/media.rb +33 -12
  84. data/lib/badline/options/family.rb +1 -0
  85. data/lib/badline/options/help.rb +32 -6
  86. data/lib/badline/options/option.rb +2 -1
  87. data/lib/badline/options/table.rb +29 -4
  88. data/lib/badline/options/validation.rb +66 -4
  89. data/lib/badline/options.rb +71 -11
  90. data/lib/badline/plus4/acia/frames.rb +55 -0
  91. data/lib/badline/plus4/acia.rb +276 -16
  92. data/lib/badline/plus4/bus.rb +245 -46
  93. data/lib/badline/plus4/cassette_sense.rb +21 -0
  94. data/lib/badline/plus4/keyboard_port.rb +35 -10
  95. data/lib/badline/plus4/keyed_modules.rb +116 -0
  96. data/lib/badline/plus4/keyed_parts.rb +172 -0
  97. data/lib/badline/plus4/keyed_state.rb +44 -0
  98. data/lib/badline/plus4/ram_expansion.rb +145 -0
  99. data/lib/badline/plus4/sid_card.rb +95 -0
  100. data/lib/badline/plus4/sound_mix.rb +51 -0
  101. data/lib/badline/plus4/tcbm_drive.rb +120 -0
  102. data/lib/badline/plus4/user_port.rb +11 -2
  103. data/lib/badline/plus4.rb +184 -26
  104. data/lib/badline/roms/README +9 -0
  105. data/lib/badline/roms/dos1551.rom +0 -0
  106. data/lib/badline/snapshot/container.rb +2 -0
  107. data/lib/badline/snapshot/field_doc/markdown.rb +7 -6
  108. data/lib/badline/snapshot/field_doc.rb +48 -7
  109. data/lib/badline/snapshot/image.rb +12 -3
  110. data/lib/badline/snapshot/plus4_setup.rb +81 -0
  111. data/lib/badline/snapshot.rb +9 -4
  112. data/lib/badline/storage/tap.rb +21 -12
  113. data/lib/badline/support/checkpoint.rb +3 -2
  114. data/lib/badline/support/plus4_checkpoint.rb +42 -0
  115. data/lib/badline/version.rb +1 -1
  116. metadata +29 -1
data/lib/badline/plus4.rb CHANGED
@@ -2,28 +2,53 @@
2
2
 
3
3
  require "badline/chips/ted"
4
4
  require "badline/chips/tia"
5
+ require "badline/drives/model1551"
5
6
  require "badline/plus4/model"
6
7
  require "badline/plus4/cpu"
7
8
  require "badline/plus4/cpu_port"
8
9
  require "badline/plus4/keyboard_port"
9
10
  require "badline/plus4/user_port"
11
+ require "badline/plus4/cassette_sense"
10
12
  require "badline/plus4/acia"
13
+ require "badline/plus4/sid_card"
14
+ require "badline/plus4/sound_mix"
15
+ require "badline/plus4/ram_expansion"
11
16
  require "badline/plus4/bus"
17
+ require "badline/plus4/tcbm_drive"
18
+ require "badline/snapshot/plus4_setup"
19
+ require "badline/plus4/keyed_parts"
20
+ require "badline/plus4/keyed_state"
21
+ require "badline/plus4/keyed_modules"
12
22
 
13
23
  module Badline
14
24
  # The Commodore 264 series: the Plus/4, the C16 and the C116. A 7501 on
15
25
  # the TED's clock, which runs it once or twice a TED cycle (Chips::TED's
16
26
  # Beam), the TED and, on the Plus/4, a 6551 ACIA and the user port's
17
- # 6529. Only the TED pulls IRQ, and nothing pulls NMI, which the 7501
18
- # doesn't have.
27
+ # 6529. The TED and the ACIA pull IRQ, and nothing pulls NMI, which the
28
+ # 7501 doesn't have.
19
29
  #
20
- # It runs headless only: the TED paints no picture yet. Device 8
21
- # serves disks through the KERNAL traps (C64::KernalTraps), and
22
- # cartridges go in cartridge 1's slots.
30
+ # The TED paints the picture a front end shows (#video, #timing). Device 8
31
+ # serves disks through the KERNAL traps (C64::KernalTraps), or a true
32
+ # 1541 or 1581 on the serial bus or a 1551 on TCBM does (TCBMDrive),
33
+ # cartridges go
34
+ # in the two cartridges' slots, and tapes play on the datasette, whose read
35
+ # line's level the KERNAL times with the TED's timers.
36
+ #
37
+ # A SID card, as third parties built for the 264s, puts a SID at
38
+ # $FD40-$FD5F, or at $FE80-$FE9F as Csory's does. The cards clock it from the TED's single clock, one SID
39
+ # cycle a TED cycle, 886,724 Hz on PAL, below the C64's 985,248 Hz, so a
40
+ # C64 tune plays about a semitone flat (plus4world forum threads 38287
41
+ # and 20130).
42
+ #
43
+ # A RAM expansion, Hannes's 256K or 1M or Csory's 256K, banks more RAM
44
+ # in through a register at $FD16 (RAMExpansion).
23
45
  class Plus4
24
46
  include IntegerHelper
25
47
  include Machine::KeyboardBuffer
26
48
  include Machine::Clocking
49
+ include Machine::Settling
50
+ include C64::TrueDrives
51
+ include TCBMDrive
27
52
  include C64::KernalTraps
28
53
 
29
54
  # The cycle by which the KERNAL has booted and BASIC printed READY,
@@ -31,15 +56,42 @@ module Badline
31
56
  # Plus/4, 126,000 on an NTSC one and sooner on a C16.
32
57
  INIT_THRESHOLD = 200_000
33
58
 
34
- attr_reader :model, :bus, :cpu, :ted, :port, :keyboard, :keyboard_port, :acia, :user_port, :datasette, :tias,
35
- :iec_bus, :cycles, :ram_kb
59
+ # The cycle by which a 1541 switched on with the machine has run its
60
+ # DOS's reset, the RAM test and the ROM checksum, and waits on the
61
+ # bus, with a margin, in TED cycles.
62
+ DRIVE_BOOT_CYCLES = 1_200_000
63
+
64
+ attr_reader :model, :bus, :cpu, :ted, :sound, :port, :keyboard, :keyboard_port, :joystick1, :joystick2, :acia,
65
+ :user_port, :datasette, :tias, :iec_bus, :cycles, :ram_kb, :drive1541, :init_threshold, :sid_card
36
66
 
37
67
  def family = :plus4
38
68
 
39
- def init_threshold = INIT_THRESHOLD
69
+ # The joysticks reach the TED's keyboard latch, so there are no C64
70
+ # control ports to plug a pot device into.
71
+ def control_ports = nil
40
72
 
41
73
  def ram = @bus.ram
42
74
 
75
+ # What a front end plays: the TED's sound, which answers #record and
76
+ # #drain_samples, mixed with the SID card's where one is fitted.
77
+ def sound_source = @sound_mix || @sound
78
+
79
+ # The chip whose #display a front end shows.
80
+ def video = @ted
81
+
82
+ # The TED's clock and raster, and the part of its picture xplus4 shows:
83
+ # all 384 pixels of a line, and 288 lines from line 276 on PAL or 242
84
+ # from line 248 on NTSC, running on past the frame's last line to its
85
+ # first, as the top border is the end of the frame. The TED draws 8
86
+ # pixels a cycle, so each shows one window pixel wide, as on the C64.
87
+ def timing
88
+ profile = @ted.profile
89
+ ntsc = profile == Chips::TED::NTSC
90
+ Timing.new(clock_hz: profile.clock_hz, cycles_per_line: Chips::TED::Beam::CYCLES_PER_LINE,
91
+ lines_per_frame: ntsc ? 262 : 312, crop: ntsc ? [0, 248, 384, 242] : [0, 276, 384, 288],
92
+ pixel_width: 1)
93
+ end
94
+
43
95
  # Reads and writes memory as the CPU sees it, through the bus.
44
96
  def peek(addr) = @bus.peek(addr)
45
97
 
@@ -48,46 +100,66 @@ module Badline
48
100
  # +model+ names one of Model::ALL, or c116 for the C16, and +ram+ the
49
101
  # RAM in K, one of Model::RAM_SIZES, when not the model's own. +tcbm+
50
102
  # plugs in a 1551's TCBM interface, the TIAs at $FEC0-$FEFF, with no
51
- # drive answering on the other side.
52
- def initialize(model: "plus4", ram: nil, tcbm: false, debug: false)
103
+ # drive answering on the other side. +true_drive_model+ names the drive
104
+ # plug_true_drive plugs in, "1541" or "1551". +sid_card+ fits a SID
105
+ # card with a SID of that model, :mos6581 or :mos8580, at
106
+ # +sid_card_address+, $FD40 or $FE80. +ram_expansion+
107
+ # fits a RAMExpansion of that kind, :hannes256, :hannes1m or
108
+ # :csory256, over 64K of the machine's own.
109
+ def initialize(model: "plus4", ram: nil, tcbm: false, true_drive_model: "1551", sid_card: nil,
110
+ sid_card_address: SIDCard::ADDRESSES.first, ram_expansion: nil, debug: false)
53
111
  @model = Model.named(model)
54
- @ram_kb = ram || @model.ram_kb
55
- raise ArgumentError, "no Plus/4 RAM size of #{@ram_kb}K" unless Model::RAM_SIZES.include?(@ram_kb)
112
+ @ram_kb = fitted_ram(ram, ram_expansion)
56
113
 
57
114
  @ted = Chips::TED.new(@model.ted)
58
115
  @keyboard = Input::Keyboard.new(matrix: KeyboardPort::MATRIX)
59
- @keyboard_port = KeyboardPort.new(@keyboard)
116
+ @joystick1 = Input::Joystick.new
117
+ @joystick2 = Input::Joystick.new
118
+ @keyboard_port = KeyboardPort.new(@keyboard, @joystick1, @joystick2)
60
119
  @ted.keyboard_port = @keyboard_port
61
120
  @iec_bus = Drives::SerialBus.new
62
121
  @datasette = Drives::Datasette.new(level: true)
63
122
  @port = CPUPort.new(serial_bus: @iec_bus, datasette: @datasette)
64
123
  plus4_parts = @model.plus4_parts
65
- @acia = plus4_parts ? ACIA.new : nil
124
+ @cycles = 0
125
+ @init_threshold = INIT_THRESHOLD
126
+ @acia_irq = false
127
+ @acia_due = ACIA::NEVER
128
+ @due = INIT_THRESHOLD
129
+ @sid_card = sid_card ? SIDCard.new(self, sid_card, sid_card_address) : nil
130
+ @acia = plus4_parts ? ACIA.new(self, @model.ted.clock_hz) : nil
66
131
  @user_port = plus4_parts ? UserPort.new(@datasette) : nil
67
- io = Bus::IOPage.new(keyboard_port: @keyboard_port, acia: @acia, user_port: @user_port)
68
- @bus = Bus.new(ted: @ted, port: @port, io:, model: @model, ram_kb: @ram_kb)
132
+ io = Bus::IOPage.new(keyboard_port: @keyboard_port, acia: @acia, user_port: @user_port,
133
+ cassette_sense: plus4_parts ? nil : CassetteSense.new(@datasette),
134
+ sid_card: sid_card_at(0xfd40))
135
+ @bus = Bus.new(ted: @ted, port: @port, io:, model: @model, ram_kb: @ram_kb, ram_expansion:)
136
+ @ted.connect(@bus)
137
+ @bus.plug_sid_card(sid_card_at(0xfe80))
69
138
  @tias = tcbm ? [Chips::TIA.new, Chips::TIA.new] : []
70
139
  @bus.plug_tcbm(drive8: @tias[0], drive9: @tias[1]) if tcbm
71
140
  @cpu = CPU.new(@bus, debug:)
72
- @cycles = 0
141
+ @sound = Chips::TED::Sound.new(self, @model.ted.clock_hz)
142
+ @ted.sound = @sound
143
+ @sound_mix = @sid_card ? SoundMix.new(@sound, @sid_card) : nil
73
144
  @init_handlers = []
74
145
  @pending_keys = nil
75
146
  @dos = nil
76
147
  @serial_trap = nil
77
148
  @save_trap = nil
78
149
  @capture_output = nil
150
+ init_drives(true_drive_model)
79
151
  end
80
152
 
81
153
  # One TED cycle: the TED moves on and decides the CPU's clock, then the
82
154
  # CPU runs its cycles in it, none while the TED holds the bus.
83
155
  def cycle!
84
- handle_init if @cycles == INIT_THRESHOLD
156
+ handle_due if @cycles >= @due
85
157
  feed_keyboard if @pending_keys
86
158
 
87
159
  ted = @ted
88
160
  ted.cycle!
89
161
  @datasette.cycle!
90
- @cpu.irq = ted.irq?
162
+ @cpu.irq = ted.irq? || @acia_irq
91
163
  slots = ted.cpu_slots
92
164
  if slots.zero?
93
165
  @cpu.stall!
@@ -95,35 +167,52 @@ module Badline
95
167
  clock_cpu(ted)
96
168
  clock_cpu(ted) if slots == 2
97
169
  end
170
+ @drive1541&.host_cycle!
171
+ @drive1581&.host_cycle!
172
+ @drive1551&.host_cycle!
98
173
 
99
174
  @cycles += 1
100
175
  end
101
176
 
102
177
  # The RESET line: the CPU, the port, the 6529s, the ACIA, the bank
103
- # latch and ROM selection. The TED has no reset pin.
178
+ # latch and ROM selection, a RAM expansion's register, the SID card,
179
+ # and through the serial bus's RESET line, the drives. The TED has no
180
+ # reset pin.
104
181
  def reset!
105
182
  @port.reset!
106
183
  @keyboard_port.reset!
107
184
  @user_port&.reset!
108
185
  @dos&.reset!
186
+ @drive1541&.reset!
187
+ @drive1581&.reset!
188
+ @drive1551&.reset!
109
189
  @acia&.reset!
190
+ @sid_card&.reset!
110
191
  @tias.each(&:reset!)
111
192
  @bus.reset!
112
193
  @cpu.reset!
113
194
  end
114
195
 
115
- # Switches the machine off and on: RAM and the TED start from their
116
- # power-on state, and the RES line resets everything else.
196
+ # Switches the machine off and on: RAM, the TED and its sound start
197
+ # from their power-on state, and the RES line resets everything else.
117
198
  def power_cycle!
118
199
  @bus.power_on!
119
200
  @ted.power_on!
201
+ @sound.power_on!
120
202
  reset!
121
203
  end
122
204
 
205
+ # The ACIA's IRQ line, and the cycle it next has something to do at.
206
+ def acia_changed(irq, due)
207
+ @acia_irq = irq
208
+ @acia_due = due
209
+ schedule
210
+ end
211
+
123
212
  # Calls the block with the exit code a VICE testprog writes to $FDCF.
124
213
  def install_debug_register(&) = @bus.install_debug_register(&)
125
214
 
126
- # Puts a cartridge's ROM chips in cartridge 1's slots (Bus#attach_cartridge)
215
+ # Puts a cartridge's ROM chips in their slots (Bus#attach_cartridge)
127
216
  # and switches the machine off and on, as a cartridge goes in with the
128
217
  # power off. The KERNAL starts it at reset when it signs itself with CBM
129
218
  # at $8007.
@@ -132,6 +221,36 @@ module Badline
132
221
  power_cycle!
133
222
  end
134
223
 
224
+ # Takes both cartridges out (Bus#detach_cartridges) and switches the
225
+ # machine off and on.
226
+ def detach_cartridges
227
+ @bus.detach_cartridges
228
+ power_cycle!
229
+ end
230
+
231
+ # The cartridge slots with a ROM in them (Bus#cartridge_slots).
232
+ def cartridge_slots = @bus.cartridge_slots
233
+
234
+ # The model and its RAM as the pause menu names them, such as
235
+ # "PLUS/4 PAL, 64K RAM".
236
+ def model_label
237
+ name = @model.plus4_parts ? "PLUS/4" : "C16"
238
+ standard = @model.ted == Chips::TED::NTSC ? "NTSC" : "PAL"
239
+ "#{name} #{standard}, #{@ram_kb}K RAM"
240
+ end
241
+
242
+ # The RAMExpansion fitted, or nil.
243
+ def ram_expansion = @bus.ram_expansion
244
+
245
+ # The RAM expansion as the pause menu names it, such as "HANNES, 256
246
+ # KB", or nil without one.
247
+ def ram_expansion_label
248
+ expansion = @bus.ram_expansion
249
+ return if expansion.nil?
250
+
251
+ "#{RAMExpansion::NAMES.fetch(expansion.kind)}, #{expansion.size_kb} KB"
252
+ end
253
+
135
254
  # Stores a PRG's bytes from its load address on, into RAM, and returns
136
255
  # that address.
137
256
  def load_prg(data)
@@ -144,6 +263,22 @@ module Badline
144
263
 
145
264
  private
146
265
 
266
+ # The SID card if it is at +address+, or nil.
267
+ def sid_card_at(address)
268
+ card = @sid_card
269
+ card if card && card.address == address
270
+ end
271
+
272
+ # The RAM in K: +ram+, or the model's own, or 64K under a RAM
273
+ # expansion, which needs that much.
274
+ def fitted_ram(ram, ram_expansion)
275
+ ram_kb = ram || (ram_expansion ? 64 : @model.ram_kb)
276
+ raise ArgumentError, "no Plus/4 RAM size of #{ram_kb}K" unless Model::RAM_SIZES.include?(ram_kb)
277
+ raise ArgumentError, "a RAM expansion needs 64K, not #{ram_kb}K" if ram_expansion && ram_kb != 64
278
+
279
+ ram_kb
280
+ end
281
+
147
282
  # The 264 KERNAL's keyboard buffer.
148
283
  def keyboard_buffer = Machine::KeyboardBuffer::PLUS4_LAYOUT
149
284
 
@@ -152,9 +287,32 @@ module Badline
152
287
 
153
288
  def trap_bus = @bus
154
289
 
155
- # Device 8, which the serial traps answer for, with no true drive on
156
- # the bus to take it.
157
- def serial_trap_device = KernalTrap::Routine::DEVICE
290
+ # A drive that powers on with the machine takes longer to boot than
291
+ # the KERNAL, and doesn't answer the bus until it has, so the
292
+ # machine's on_init handlers wait for it too.
293
+ def drive1541_attached
294
+ @init_threshold = [@init_threshold, DRIVE_BOOT_CYCLES].max if @cycles < @init_threshold
295
+ end
296
+
297
+ def drive1581_attached
298
+ @init_threshold = [@init_threshold, Drives::Model1581::BOOT_CYCLES].max if @cycles < @init_threshold
299
+ end
300
+
301
+ # What falls due at the start of a cycle: the on_init handlers at the
302
+ # init threshold, and the ACIA's next character.
303
+ def handle_due
304
+ handle_init if @cycles == @init_threshold
305
+ acia = @acia
306
+ acia.run!(@cycles) if acia && @cycles >= @acia_due
307
+ schedule
308
+ end
309
+
310
+ # The next cycle anything falls due at. A drive that raises the init
311
+ # threshold leaves it early, and handle_due looks again then.
312
+ def schedule
313
+ init = @cycles < @init_threshold ? @init_threshold : ACIA::NEVER
314
+ @due = [@acia_due, init].min
315
+ end
158
316
 
159
317
  # BA stops the CPU at a read, and a write goes ahead.
160
318
  def clock_cpu(ted)
@@ -79,6 +79,15 @@ Downloaded from
79
79
  https://www.zimmers.net/anonftp/pub/cbm/firmware/drives/new/1581/1581-rom.318045-02.bin
80
80
  SHA-1 01228eae6f066bd9b7b2b6a7fa3f667e41dad393.
81
81
 
82
+ dos1551.rom
83
+ -----------
84
+ The 1551 disk drive's DOS 2.6, 318008-01, 16 KB for $C000-$FFFF.
85
+ Drives::Model1551 loads it.
86
+
87
+ Downloaded from
88
+ https://www.zimmers.net/anonftp/pub/cbm/firmware/drives/new/1551/1551.318008-01.bin
89
+ SHA-1 fae3c788ad9a6cc2dbdfbcf6c0264b2ca921d55e.
90
+
82
91
  Commodore VIC-20 ROMs
83
92
  =====================
84
93
 
Binary file
@@ -34,6 +34,8 @@ module Badline
34
34
  VIC20 = "VIC20"
35
35
  # x128 names its machine C128.
36
36
  C128 = "C128"
37
+ # xplus4 names its machine PLUS4, for the C16 too.
38
+ PLUS4 = "PLUS4"
37
39
  GZIP_MAGIC = "\x1f\x8b".b
38
40
 
39
41
  # The VICE version block, when VICE wrote the file: four version
@@ -7,7 +7,7 @@ module Badline
7
7
  # its default, with keys that differ only in a unit's number, such as
8
8
  # voice1 to voice3, on one row.
9
9
  class Markdown
10
- NAMES = { "c64" => "C64", "vic20" => "VIC-20", "c128" => "C128" }.freeze
10
+ NAMES = { "c64" => "C64", "vic20" => "VIC-20", "c128" => "C128", "plus4" => "Plus/4" }.freeze
11
11
  TAG_TYPES = { Keyed::INT => "int", Keyed::BOOL => "bool", Keyed::BYTES => "bytes", Keyed::INTS => "ints",
12
12
  Keyed::TEXT => "text" }.freeze
13
13
 
@@ -31,11 +31,12 @@ module Badline
31
31
  `int & 0x..` are ints cut to the latch's width. A text that lists
32
32
  its values takes one of them. An optional int is absent when the
33
33
  latch holds nothing. A true drive's modules are named from its
34
- device number, `BL-D8` to `BL-D11`. Keys with `<N>` stand for each
35
- numbered unit. A default of "written only" is a key the load
36
- doesn't read, or reads only when the snapshot has it. Where the
37
- machines differ, the default is given for each, and a key only
38
- some of them have names them.
34
+ device number, `BL-D8` to `BL-D11`, and the SIDs past the first
35
+ are `BL-SID2` onwards, in the order they were fitted. Keys with
36
+ `<N>` stand for each numbered unit. A default of "written only" is
37
+ a key the load doesn't read, or reads only when the snapshot has
38
+ it. Where the machines differ, the default is given for each, and a
39
+ key only some of them have names them.
39
40
  MARKDOWN
40
41
 
41
42
  def initialize(loads)
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "badline"
4
4
  require "badline/c128"
5
+ require "badline/plus4"
5
6
 
6
7
  module Badline
7
8
  module Snapshot
@@ -27,29 +28,47 @@ module Badline
27
28
  # each module the keys its records hold, with their tags, and the fields the load read.
28
29
  Load = Data.define(:family, :written, :fields)
29
30
 
30
- # The machines the doc is made from: each family at power-on, and the
31
- # C64 with each kind of expansion, cartridge and media.
31
+ # The cartridges by CRT hardware type: EasyFlash, then a mapper of
32
+ # each kind of register: Ocean's bank, Comal-80's bank for both
33
+ # windows, Zaxxon's ROMH bank, and the Action Replay's and the
34
+ # Pagefox's control registers.
35
+ CARTRIDGES = [32, 5, 21, 18, 1, 53].freeze
36
+
37
+ # The machines the doc is made from: each family at power-on, the C64
38
+ # with each kind of expansion, cartridge and media, and a second SID,
39
+ # and the 264 series' models, its TCBM interface, cartridge, tape and
40
+ # true drives, the 1541 and the 1551.
32
41
  def self.machines
33
42
  {
34
- "c64" => [C64.new, C64.new(ram_expansion: :plus256k), C64.new(reu: 512), with_cartridge, with_trap_dos,
35
- with_tape, with_drive(:d64), with_drive(:d81)],
43
+ "c64" => [C64.new, C64.new(ram_expansion: :plus256k), C64.new(reu: 512),
44
+ *CARTRIDGES.map { |type| with_cartridge(type) }, with_trap_dos, with_tape, with_drive(:d64),
45
+ with_drive(:d81), with_extra_sids],
36
46
  "vic20" => [Vic20.new, with_rom],
37
- "c128" => [C128.new(mode: :c128), with_drive(:d71)]
47
+ "c128" => [C128.new(mode: :c128), with_drive(:d71)],
48
+ "plus4" => [Plus4.new, Plus4.new(model: "c16"), Plus4.new(tcbm: true), Plus4.new(sid_card: :mos6581),
49
+ Plus4.new(sid_card: :mos8580, sid_card_address: 0xfe80),
50
+ Plus4.new(ram_expansion: :csory256),
51
+ plus4_with_cartridge, plus4_with_tape, plus4_with_drive, plus4_with_drive("1551")]
38
52
  }
39
53
  end
40
54
 
41
55
  def self.blank(length) = Array.new(length, 0)
42
56
 
43
- def self.with_cartridge
57
+ def self.with_cartridge(type)
44
58
  chips = [0x8000, 0xa000].map do |address|
45
59
  Storage::CRTFile::Chip.new(chip_type: 2, bank: 0, address:, data: blank(0x2000))
46
60
  end
47
- image = Storage::CRTFile::Image.new(hardware_type: 32, subtype: 0, exrom: 1, game: 0, name: "DOC", chips:)
61
+ image = Storage::CRTFile::Image.new(hardware_type: type, subtype: 0, exrom: 1, game: 0, name: "DOC", chips:)
48
62
  C64.new.tap do |machine|
49
63
  machine.attach_cartridge(C64::Cartridge.from_crt(Storage::CRTFile.new(bytes: Storage::CRTFile.encode(image))))
50
64
  end
51
65
  end
52
66
 
67
+ # The SID player's machine for a tune on two SIDs.
68
+ def self.with_extra_sids
69
+ C64.new.tap { |machine| machine.address_bus.add_sid(Chips::SID.new(at: 0xd420)) }
70
+ end
71
+
53
72
  def self.with_trap_dos
54
73
  C64.new.tap { |machine| machine.mount(Storage::D64Image.new("", read_only: true, bytes: blank(174_848))) }
55
74
  end
@@ -78,6 +97,28 @@ module Badline
78
97
  end
79
98
  end
80
99
 
100
+ # A Plus/4 with a cartridge in each of cartridge 1's slots.
101
+ def self.plus4_with_cartridge
102
+ chips = [0x8000, 0xc000].map do |address|
103
+ Storage::CRTFile::Chip.new(chip_type: 0, bank: 0, address:, data: blank(0x4000))
104
+ end
105
+ Plus4.new.tap { |machine| machine.attach_cartridge(chips) }
106
+ end
107
+
108
+ # A C16 with a version 2 tape in its datasette, which times the read
109
+ # line's level.
110
+ def self.plus4_with_tape
111
+ bytes = "C16-TAPE-RAW".bytes + [2, 2, 0, 0, 4, 0, 0, 0, 0x30, 0x30, 0x30, 0x30]
112
+ Plus4.new(model: "c16").tap { |machine| machine.datasette.insert(Storage::TAP.new("", bytes:)) }
113
+ end
114
+
115
+ def self.plus4_with_drive(model = "1541")
116
+ Plus4.new(true_drive_model: model).tap do |machine|
117
+ image = Storage::D64Image.new("", bytes: blank(174_848))
118
+ machine.plug_true_drive.insert(Drives::Model1541::Disk.decode(image))
119
+ end
120
+ end
121
+
81
122
  def self.with_rom
82
123
  Vic20.new.tap { |machine| machine.bus.map_rom(0xa000, blank(0x2000)) }
83
124
  end
@@ -34,7 +34,7 @@ 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" }.freeze
37
+ FAMILIES = { "c64" => "C64", "vic20" => "VIC-20", "c128" => "C128", "plus4" => "Plus/4" }.freeze
38
38
 
39
39
  attr_reader :container
40
40
 
@@ -54,8 +54,11 @@ module Badline
54
54
  end
55
55
 
56
56
  # The snapshot with VICE's modules ahead of the BL-* ones for a C64,
57
- # so x64sc opens it too.
57
+ # so x64sc opens it too. Raises FormatError for a Plus/4, whose
58
+ # snapshots badline doesn't write for xplus4.
58
59
  def self.exported(computer)
60
+ raise FormatError, "badline doesn't export a Plus/4's snapshot for VICE" if computer.family == :plus4
61
+
59
62
  computer.settle_for_snapshot!
60
63
  sections = computer.save_modules
61
64
  sections = Vice.export(computer) + sections if computer.family == :c64
@@ -67,6 +70,7 @@ module Badline
67
70
  case computer.family
68
71
  when :vic20 then Container::VIC20
69
72
  when :c128 then Container::C128
73
+ when :plus4 then Container::PLUS4
70
74
  else Container::MACHINE
71
75
  end
72
76
  end
@@ -77,6 +81,7 @@ module Badline
77
81
  case machine.choice("family", FAMILIES.keys, "c64")
78
82
  when "vic20" then Vic20Setup.read_fields(machine)
79
83
  when "c128" then C128Setup.read_fields(machine)
84
+ when "plus4" then Plus4Setup.read_fields(machine)
80
85
  else C64Setup.read_fields(machine)
81
86
  end
82
87
  end
@@ -92,7 +97,7 @@ module Badline
92
97
 
93
98
  # A new machine, built as the snapshot's was and restored from it. A
94
99
  # badline snapshot restores from its BL-* modules alone, and a VICE
95
- # one through the modules badline reads. xvic's and x128's own
100
+ # one through the modules badline reads. xvic's, x128's and xplus4's own
96
101
  # modules aren't read. Yields a line for each thing left out, or
97
102
  # warns it without a block. `detached` restores it without the host's
98
103
  # files (Keyed::Modules).
@@ -100,6 +105,7 @@ module Badline
100
105
  refuse_old_layout
101
106
  raise FormatError, "badline reads only its own VIC-20 snapshots, not xvic's" if vic20? && !badline?
102
107
  raise FormatError, "badline reads only its own C128 snapshots, not x128's" if c128? && !badline?
108
+ raise FormatError, "badline reads only its own Plus/4 snapshots, not xplus4's" if plus4? && !badline?
103
109
  return load_vice(&) unless badline?
104
110
 
105
111
  modules = Keyed::Modules.new(sections, detached:)
@@ -114,6 +120,9 @@ module Badline
114
120
  # Whether the machine is a C128, as the container names it.
115
121
  def c128? = @container.machine == Container::C128
116
122
 
123
+ # Whether the machine is of the 264 series, as the container names it.
124
+ def plus4? = @container.machine == Container::PLUS4
125
+
117
126
  # Restores `computer` from the snapshot, and returns the Report. The
118
127
  # snapshot goes into a new machine first, so one that fails, or is of
119
128
  # a machine built another way, leaves `computer` as it was.
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Snapshot
5
+ # How a 264 series machine was built: its model's name, one of
6
+ # Plus4::Model::ALL, which also says whether the Plus/4's function ROMs
7
+ # are fitted, its RAM in K, whether a 1551's TCBM interface is plugged
8
+ # in, the SID card's SID model, or nil without one, and its address,
9
+ # and the RAM expansion's kind, or nil without one, the arguments
10
+ # Plus4.new takes.
11
+ Plus4Setup = Data.define(:model, :ram_kb, :tcbm, :sid_card, :sid_card_address, :ram_expansion)
12
+
13
+ class Plus4Setup
14
+ MODELS = Plus4::Model::ALL.map(&:name).freeze
15
+
16
+ SID_CARDS = %w[none mos6581 mos8580].freeze
17
+
18
+ RAM_EXPANSIONS = ["none", *Plus4::RAMExpansion::KINDS.keys.map(&:to_s)].freeze
19
+
20
+ # The setup of `machine`.
21
+ def self.of(machine)
22
+ card = machine.sid_card
23
+ new(model: machine.model.name, ram_kb: machine.ram_kb, tcbm: !machine.tias.empty?, sid_card: card&.model,
24
+ sid_card_address: card ? card.address : Plus4::SIDCard::ADDRESSES.first,
25
+ ram_expansion: machine.ram_expansion&.kind)
26
+ end
27
+
28
+ # The model by name, the RAM, the TCBM interface, the SID card's
29
+ # model and address and the RAM expansion's kind, "none" without
30
+ # them, in a keyed snapshot module.
31
+ def write_fields(out)
32
+ out.text("model", model).int("ram_kb", ram_kb).bool("tcbm", tcbm).text("sid_card", (sid_card || :none).to_s)
33
+ out.int("sid_card_address", sid_card_address).text("ram_expansion", (ram_expansion || :none).to_s)
34
+ end
35
+
36
+ def self.read_fields(input)
37
+ model = input.choice("model", MODELS, "plus4")
38
+ ram_kb = input.int("ram_kb", Plus4::Model.named(model).ram_kb)
39
+ unless Plus4::Model::RAM_SIZES.include?(ram_kb)
40
+ raise FormatError, "the snapshot is of a Plus/4 with #{ram_kb}K of RAM, which badline doesn't build"
41
+ end
42
+
43
+ sid_card = named(input.choice("sid_card", SID_CARDS, "none"))
44
+ ram_expansion = named(input.choice("ram_expansion", RAM_EXPANSIONS, "none"))
45
+ new(model:, ram_kb:, tcbm: input.bool("tcbm", false), sid_card:, sid_card_address: sid_card_address(input),
46
+ ram_expansion:)
47
+ end
48
+
49
+ # A choice's name as a Symbol, or nil for "none".
50
+ def self.named(name) = name == "none" ? nil : name.to_sym
51
+
52
+ def self.sid_card_address(input)
53
+ address = input.int("sid_card_address", Plus4::SIDCard::ADDRESSES.first)
54
+ return address if Plus4::SIDCard::ADDRESSES.include?(address)
55
+
56
+ raise FormatError, format("the snapshot is of a Plus/4 with a SID card at $%<address>04X, " \
57
+ "which badline doesn't build", address:)
58
+ end
59
+ private_class_method :named, :sid_card_address
60
+
61
+ # A machine built this way, at power-on.
62
+ def build = Plus4.new(model:, ram: ram_kb, tcbm:, sid_card:, sid_card_address:, ram_expansion:)
63
+
64
+ def to_s
65
+ parts = ["#{ram_kb}K"]
66
+ parts << "a TCBM interface" if tcbm
67
+ parts << sid_card_part if sid_card
68
+ parts << "a #{ram_expansion} RAM expansion" if ram_expansion
69
+ "#{model} with #{parts.join(' and ')}"
70
+ end
71
+
72
+ private
73
+
74
+ def sid_card_part
75
+ format("%<article>s %<model>s SID card at $%<address>04X", article: sid_card == :mos8580 ? "an" : "a",
76
+ model: sid_card.to_s.delete_prefix("mos"),
77
+ address: sid_card_address)
78
+ end
79
+ end
80
+ end
81
+ end
@@ -35,8 +35,9 @@ module Badline
35
35
  # The machine's snapshot file. Saving first runs the machine on to the
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
- # second save straight after runs none. A VIC-20's or a C128's snapshot
39
- # is in a container named as xvic or x128 names its machine.
38
+ # second save straight after runs none. A VIC-20's, a C128's or a
39
+ # Plus/4's snapshot is in a container named as xvic, x128 or xplus4
40
+ # names its machine.
40
41
  def dump(computer) = Image.of(computer).to_s
41
42
 
42
43
  def read(path) = Image.new(Container.parse(File.binread(path)), name: File.basename(path))
@@ -50,8 +51,8 @@ module Badline
50
51
  # snapshot that fails leaves the machine as it was.
51
52
  def restore(computer, path, &) = read(path).restore(computer, &)
52
53
 
53
- # A machine's snapshots, as .vsf files and in memory. C64, Vic20 and
54
- # C128 each include it.
54
+ # A machine's snapshots, as .vsf files and in memory. C64, Vic20, C128
55
+ # and Plus4 each include it.
55
56
  module Files
56
57
  def save_snapshot(path) = Snapshot.save(self, path)
57
58
 
@@ -90,4 +91,8 @@ module Badline
90
91
  class C128
91
92
  include Snapshot::Files
92
93
  end
94
+
95
+ class Plus4
96
+ include Snapshot::Files
97
+ end
93
98
  end