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
@@ -10,9 +10,14 @@ module Badline
10
10
  # $C000-$FCFF - the high ROM bank, or RAM; $FC00-$FCFF is always the
11
11
  # KERNAL's while ROM is selected
12
12
  # $FD00-$FD0F - the 6551 ACIA (Plus/4 only)
13
- # $FD10-$FD1F - the user port's 6529 (Plus/4 only)
13
+ # $FD10-$FD1F - the user port's 6529 (Plus/4 only), or with a RAM
14
+ # expansion, the 6529 at $FD10 and the expansion's
15
+ # registers at $FD15 and $FD16
14
16
  # $FD30-$FD3F - the keyboard's 6529 (KeyboardPort)
17
+ # $FD40-$FD5F - a SID card's SID, where one is fitted there
15
18
  # $FDD0-$FDDF - the ROM bank latch
19
+ # $FE80-$FE9F - a SID card's SID, where one is fitted there
20
+ # $FEC0-$FEFF - a 1551's TCBM interface, where one is plugged in
16
21
  # $FF00-$FF1F - the TED
17
22
  # $FF3E/$FF3F - a write selects ROM or RAM at $8000-$FFFF
18
23
  # $FF40-$FFFF - the high ROM bank, or RAM
@@ -27,23 +32,32 @@ module Badline
27
32
  #
28
33
  # RAM smaller than 64K repeats through the 64K, as the C16's 16K does
29
34
  # without A14 and A15. Where nothing answers, a read finds the last byte
30
- # on the bus.
35
+ # on the bus. A RAM expansion (RAMExpansion) picks the RAM each page
36
+ # reads and writes, the CPU's and the TED's.
37
+ #
38
+ # The TED fetches through the same banks: the video matrix and the
39
+ # attributes from ROM or RAM at $8000-$FFFF as the CPU sees it, with no
40
+ # I/O in the way (#ted_fetch), and character and bitmap data from RAM,
41
+ # or from ROM with $FF12 bit 2 set, where below $8000 and in an empty
42
+ # bank it finds the last byte on the bus (#ted_pattern_fetch, Plus/4
43
+ # openmem). Each byte it fetches is left on the bus.
31
44
  class Bus
32
45
  include Addressable
33
46
 
34
47
  RAM_POWER_ON = 0x00
35
48
 
36
- # RAM through a size it repeats at.
49
+ # RAM through a size it repeats at, from +offset+ on in +ram+.
37
50
  class MirroredRAM
38
- def initialize(ram, mask)
51
+ def initialize(ram, mask, offset = 0)
39
52
  @ram = ram
40
53
  @mask = mask
54
+ @offset = offset
41
55
  end
42
56
 
43
- def peek(addr) = @ram.peek(addr & @mask)
57
+ def peek(addr) = @ram.peek((addr & @mask) + @offset)
44
58
 
45
59
  def poke(addr, value)
46
- @ram.poke(addr & @mask, value)
60
+ @ram.poke((addr & @mask) + @offset, value)
47
61
  end
48
62
  end
49
63
 
@@ -73,14 +87,22 @@ module Badline
73
87
  end
74
88
  end
75
89
 
76
- # Page $FD: the ACIA, the user port, the keyboard port and the ROM
77
- # bank latch, by the address's bits 4-7. +acia+ and +user_port+ are
78
- # nil where the machine has none.
90
+ # Page $FD: the ACIA, the user port, the keyboard port, a SID card and
91
+ # the ROM bank latch, by the address's bits 4-7. +acia+, +user_port+
92
+ # and +sid_card+ are nil where the machine has none, and
93
+ # +cassette_sense+ answers in the user port's place on a machine
94
+ # without one (CassetteSense). The SID card decodes its 32 bytes
95
+ # alone, with no mirrors, as xplus4's does. A RAM expansion
96
+ # (RAMExpansion) leaves the user port only $FD10 of $FD10-$FD1F, and
97
+ # has its register at $FD16.
79
98
  class IOPage
80
- def initialize(keyboard_port:, acia: nil, user_port: nil)
99
+ def initialize(keyboard_port:, acia: nil, user_port: nil, cassette_sense: nil, sid_card: nil)
81
100
  @bus = nil
101
+ @ram_expansion = nil
82
102
  @acia = acia
103
+ @sid_card = sid_card
83
104
  @user_port = user_port
105
+ @cassette_sense = cassette_sense
84
106
  @keyboard_port = keyboard_port
85
107
  end
86
108
 
@@ -88,13 +110,15 @@ module Badline
88
110
  # a write to $FDD0-$FDDF sets.
89
111
  def connect(bus)
90
112
  @bus = bus
113
+ @ram_expansion = bus.ram_expansion
91
114
  end
92
115
 
93
116
  def peek(addr)
94
117
  case addr & 0xf0
95
118
  when 0x00 then @acia ? @acia.peek(addr) : @bus.data
96
- when 0x10 then @user_port ? @user_port.peek(addr) : @bus.data
119
+ when 0x10 then @ram_expansion ? expansion_peek(addr) : user_port_peek(addr)
97
120
  when 0x30 then @keyboard_port.peek(addr)
121
+ when 0x40, 0x50 then @sid_card ? @sid_card.peek(addr) : @bus.data
98
122
  else @bus.data
99
123
  end
100
124
  end
@@ -102,11 +126,37 @@ module Badline
102
126
  def poke(addr, value)
103
127
  case addr & 0xf0
104
128
  when 0x00 then @acia&.poke(addr, value)
105
- when 0x10 then @user_port&.poke(addr, value)
129
+ when 0x10 then @ram_expansion ? expansion_poke(addr, value) : @user_port&.poke(addr, value)
106
130
  when 0x30 then @keyboard_port.poke(addr, value)
131
+ when 0x40, 0x50 then @sid_card&.poke(addr, value)
107
132
  when 0xd0 then @bus.select_banks(addr)
108
133
  end
109
134
  end
135
+
136
+ private
137
+
138
+ def expansion_peek(addr)
139
+ case addr & 0x0f
140
+ when 0x00 then user_port_peek(addr)
141
+ when RAMExpansion::CONTROL then @ram_expansion.control
142
+ else @bus.data
143
+ end
144
+ end
145
+
146
+ def expansion_poke(addr, value)
147
+ if addr.nobits?(0x0f)
148
+ @user_port&.poke(addr, value)
149
+ else
150
+ @bus.ram_expansion_written(addr, value)
151
+ end
152
+ end
153
+
154
+ def user_port_peek(addr)
155
+ return @user_port.peek(addr) if @user_port
156
+ return @cassette_sense.read(@bus.data) if @cassette_sense
157
+
158
+ @bus.data
159
+ end
110
160
  end
111
161
 
112
162
  # VICE's debug cartridge register at $FDCF, a spot in the I/O page
@@ -130,26 +180,50 @@ module Badline
130
180
  end
131
181
  end
132
182
 
133
- # Page $FE with a 1551's TCBM interface plugged in: the 6523 TIA
134
- # that talks to drive 9 at $FEC0-$FEDF and drive 8's at $FEE0-$FEFF.
135
- # The rest of the page is open.
183
+ # Page $FE, where a 1551's TCBM interface puts its 6523 TIA: the one
184
+ # that talks to drive 9 at $FEC0-$FEDF and drive 8's at $FEE0-$FEFF,
185
+ # and where a SID card can put its SID, at $FE80-$FE9F, decoding its
186
+ # 32 bytes alone as xplus4's does. The rest of the page, and a place
187
+ # with nothing in it, is open.
136
188
  class TCBMPage
137
- def initialize(bus, drive8:, drive9:)
189
+ # The TIA at $FEE0-$FEFF and the one at $FEC0-$FEDF, or nil.
190
+ attr_accessor :drive8, :drive9
191
+
192
+ # The SIDCard at $FE80-$FE9F, or nil.
193
+ attr_accessor :sid_card
194
+
195
+ def initialize(bus)
138
196
  @bus = bus
139
- @drive8 = drive8
140
- @drive9 = drive9
197
+ @drive8 = nil
198
+ @drive9 = nil
199
+ @sid_card = nil
141
200
  end
142
201
 
143
202
  def peek(addr)
144
- return @bus.data if addr < 0xfec0
203
+ return low_peek(addr) if addr < 0xfec0
145
204
 
146
- addr < 0xfee0 ? @drive9.peek(addr) : @drive8.peek(addr)
205
+ tia = addr < 0xfee0 ? @drive9 : @drive8
206
+ tia ? tia.peek(addr) : @bus.data
147
207
  end
148
208
 
149
209
  def poke(addr, value)
150
- return if addr < 0xfec0
210
+ if addr < 0xfec0
211
+ sid_card = @sid_card
212
+ sid_card.poke(addr, value) if sid_card && sid_card?(addr)
213
+ return
214
+ end
215
+
216
+ tia = addr < 0xfee0 ? @drive9 : @drive8
217
+ tia&.poke(addr, value)
218
+ end
151
219
 
152
- addr < 0xfee0 ? @drive9.poke(addr, value) : @drive8.poke(addr, value)
220
+ private
221
+
222
+ def sid_card?(addr) = (addr & 0xe0) == 0x80
223
+
224
+ def low_peek(addr)
225
+ sid_card = @sid_card
226
+ sid_card && sid_card?(addr) ? sid_card.peek(addr) : @bus.data
153
227
  end
154
228
  end
155
229
 
@@ -168,7 +242,7 @@ module Badline
168
242
  end
169
243
 
170
244
  def poke(addr, value)
171
- return @bus.ram_view.poke(addr, value) if addr >= 0xff40
245
+ return @bus.top_ram.poke(addr, value) if addr >= 0xff40
172
246
 
173
247
  case addr
174
248
  when 0xff3e then @bus.rom_selected = true
@@ -183,17 +257,29 @@ module Badline
183
257
  # What the top page reads above $FF3F: the high ROM bank or RAM.
184
258
  attr_reader :top_memory
185
259
 
260
+ # The RAM the CPU sees at $FF00-$FFFF, which a write above $FF3F
261
+ # reaches.
262
+ attr_reader :top_ram
263
+
264
+ # The RAMExpansion fitted, or nil.
265
+ attr_reader :ram_expansion
266
+
186
267
  # The CPU sees ROM at $8000-$FFFF.
187
268
  attr_reader :rom_selected
188
269
 
189
270
  # +io+ is the IOPage, +model+ a Model profile, whose KERNAL it maps
190
271
  # and whose function ROMs it maps where the model has them, and
191
- # +ram_kb+ the RAM fitted.
192
- def initialize(ted:, port:, io:, model: Model::PLUS4, ram_kb: model.ram_kb)
272
+ # +ram_kb+ the RAM fitted. +ram_expansion+ names the kind of
273
+ # RAMExpansion to fit over 64K of it.
274
+ def initialize(ted:, port:, io:, model: Model::PLUS4, ram_kb: model.ram_kb, ram_expansion: nil)
193
275
  addressable_at(0, length: 2**16)
194
276
  size = ram_kb * 1024
195
277
  @ram = Memory.new(Array.new(size, RAM_POWER_ON), length: size, start: 0)
196
278
  @ram_view = MirroredRAM.new(@ram, size - 1)
279
+ @ram_expansion = ram_expansion ? RAMExpansion.new(ram_expansion, @ram, @ram_view) : nil
280
+ @cpu_ram = Array.new(256, @ram_view)
281
+ @ted_ram = Array.new(256, @ram_view)
282
+ @top_ram = @ram_view
197
283
  @ted = ted
198
284
  @open_bus = OpenBus.new(self)
199
285
  @low_roms = [ROM.load("plus4/basic.rom", 0x8000), nil, nil, nil]
@@ -202,31 +288,44 @@ module Badline
202
288
  @low_roms[1] = ROM.load("plus4/function-low.rom", 0x8000)
203
289
  @high_roms[1] = ROM.load("plus4/function-high.rom", 0xc000)
204
290
  end
291
+ @port = port
205
292
  @zero_page = ZeroPage.new(port, @ram_view)
206
293
  @io_page = io
207
294
  io.connect(self)
208
295
  @top_page = TopPage.new(self, ted)
209
- @tcbm_page = @open_bus
296
+ @tcbm_page = TCBMPage.new(self)
210
297
  @debug_register = nil
211
298
  @data = 0
212
299
  @read_pages = Array.new(256, @open_bus)
213
300
  @write_pages = Array.new(256, @ram_view)
301
+ @ted_pages = Array.new(256, @ram_view)
302
+ @ted_rom_pages = Array.new(256, @open_bus)
214
303
  @low_bank = @high_bank = 0
215
304
  @rom_selected = true
216
305
  @top_memory = @ram_view
217
- map_writes
218
- map_reads
306
+ map_ram
219
307
  end
220
308
 
221
309
  def power_on!
222
310
  @ram.clear!(Array.new(@ram.length, RAM_POWER_ON))
311
+ @ram_expansion&.power_on!
223
312
  reset!
224
313
  end
225
314
 
226
- # The RESET line selects ROM and the first bank of each.
315
+ # The RESET line selects ROM and the first bank of each, and sets a
316
+ # RAM expansion's register.
227
317
  def reset!
228
318
  @low_bank = @high_bank = 0
229
- self.rom_selected = true
319
+ @ram_expansion&.reset!
320
+ @rom_selected = true
321
+ @ted.rom_selected = true
322
+ map_ram
323
+ end
324
+
325
+ # A write to a RAM expansion's registers in $FD11-$FD1F.
326
+ def ram_expansion_written(addr, value)
327
+ @ram_expansion.poke(addr, value)
328
+ map_ram
230
329
  end
231
330
 
232
331
  def rom_selected=(selected)
@@ -254,26 +353,64 @@ module Badline
254
353
 
255
354
  # Plugs in a 1551's TCBM interface, the TIAs at $FEC0-$FEFF.
256
355
  def plug_tcbm(drive8:, drive9:)
257
- @tcbm_page = TCBMPage.new(self, drive8:, drive9:)
258
- map_writes
259
- map_reads
356
+ @tcbm_page.drive8 = drive8
357
+ @tcbm_page.drive9 = drive9
260
358
  end
261
359
 
262
- # Puts a cartridge's ROM chips in the slots of cartridge 1: a chip at
263
- # $8000 in low bank 2 and one at $C000 in high bank 2, each repeated
264
- # through the bank's 16K.
360
+ # Puts a SIDCard at $FE80-$FE9F.
361
+ def plug_sid_card(sid_card)
362
+ @tcbm_page.sid_card = sid_card
363
+ end
364
+
365
+ # Puts +tia+ where the TCBM interface for +device+, 8 or 9, answers,
366
+ # or takes the one there out with nil.
367
+ def plug_tia(device, tia)
368
+ if device == 9
369
+ @tcbm_page.drive9 = tia
370
+ else
371
+ @tcbm_page.drive8 = tia
372
+ end
373
+ end
374
+
375
+ # Puts a cartridge's ROM chips in their slots, as a PLUS4 CARTRIDGE
376
+ # .crt numbers them: a chip's bank picks cartridge 1 (0) or 2 (1), in
377
+ # ROM banks 2 and 3, and its address the low ($8000) or the high
378
+ # ($C000) slot. A chip shorter than 16K repeats through the slot.
379
+ # Slots the chips don't name keep what they held.
265
380
  def attach_cartridge(chips)
266
381
  chips.each do |chip|
267
- data = chip.data * (0x4000 / chip.data.length)
382
+ raise ArgumentError, "no Plus/4 cartridge slot in bank #{chip.bank}" unless chip.bank.between?(0, 1)
383
+
384
+ rom = chip.data
385
+ length = rom.length
386
+ data = Array.new(0x4000) { |i| length.zero? ? 0xff : rom[i % length] }
268
387
  if chip.address < 0xc000
269
- @low_roms[2] = ROM.new(data, length: 0x4000, start: 0x8000)
388
+ @low_roms[2 + chip.bank] = ROM.new(data, length: 0x4000, start: 0x8000)
270
389
  else
271
- @high_roms[2] = ROM.new(data, length: 0x4000, start: 0xc000)
390
+ @high_roms[2 + chip.bank] = ROM.new(data, length: 0x4000, start: 0xc000)
272
391
  end
273
392
  end
274
393
  map_reads
275
394
  end
276
395
 
396
+ # Empties both cartridges' slots, ROM banks 2 and 3, which then read
397
+ # as the open bus.
398
+ def detach_cartridges
399
+ @low_roms[2] = @low_roms[3] = @high_roms[2] = @high_roms[3] = nil
400
+ map_reads
401
+ end
402
+
403
+ # The slots that hold a cartridge's ROM, by xplus4's names: c1lo,
404
+ # c1hi, c2lo and c2hi.
405
+ def cartridge_slots
406
+ slots = []
407
+ slots << "c1lo" unless @low_roms[2].nil?
408
+ slots << "c1hi" unless @high_roms[2].nil?
409
+ slots << "c2lo" unless @low_roms[3].nil?
410
+ slots << "c2hi" unless @high_roms[3].nil?
411
+ slots
412
+ end
413
+
277
414
  def peek(addr)
278
415
  @data = @read_pages[addr >> 8].peek(addr)
279
416
  end
@@ -283,9 +420,42 @@ module Badline
283
420
  @write_pages[addr >> 8].poke(addr, value)
284
421
  end
285
422
 
423
+ # A TED fetch of the video matrix or the attributes.
424
+ def ted_fetch(addr)
425
+ @data = @ted_pages[addr >> 8].peek(addr)
426
+ end
427
+
428
+ # A TED fetch of character or bitmap data, from ROM when +rom+.
429
+ def ted_pattern_fetch(addr, rom)
430
+ @data = rom ? @ted_rom_pages[addr >> 8].peek(addr) : @ted_ram[addr >> 8].peek(addr)
431
+ end
432
+
286
433
  private
287
434
 
435
+ # Lays out the RAM a RAM expansion banks in, then every page.
436
+ def map_ram
437
+ expansion = @ram_expansion
438
+ if expansion
439
+ expansion.map(@cpu_ram, @ted_ram)
440
+ @zero_page = ZeroPage.new(@port, @cpu_ram[0])
441
+ @top_ram = @cpu_ram[0xff]
442
+ end
443
+ map_writes
444
+ map_reads
445
+ end
446
+
447
+ # Copies the RAM the CPU sees into +pages+, up to the I/O pages.
448
+ def copy_ram(pages)
449
+ cpu_ram = @cpu_ram
450
+ page = 1
451
+ while page < 0xfd
452
+ pages[page] = cpu_ram[page]
453
+ page += 1
454
+ end
455
+ end
456
+
288
457
  def map_writes
458
+ copy_ram(@write_pages) if @ram_expansion
289
459
  @write_pages[0x00] = @zero_page
290
460
  @write_pages[0xfd] = @debug_register || @io_page
291
461
  @write_pages[0xfe] = @tcbm_page
@@ -293,19 +463,48 @@ module Badline
293
463
  end
294
464
 
295
465
  def map_reads
296
- @read_pages.fill(@ram_view, 0x00, 0xfd)
466
+ if @ram_expansion
467
+ copy_ram(@read_pages)
468
+ else
469
+ @read_pages.fill(@ram_view, 0x00, 0xfd)
470
+ end
297
471
  @read_pages[0x00] = @zero_page
298
- map_roms if @rom_selected
299
- @top_memory = @rom_selected ? @high_roms[@high_bank] || @open_bus : @ram_view
472
+ map_roms(@read_pages, @high_bank) if @rom_selected
473
+ @top_memory = @rom_selected ? @high_roms[@high_bank] || @open_bus : @top_ram
300
474
  @read_pages[0xfd] = @io_page
301
475
  @read_pages[0xfe] = @tcbm_page
302
476
  @read_pages[0xff] = @top_page
477
+ map_ted_reads
478
+ end
479
+
480
+ def map_ted_reads
481
+ expansion = @ram_expansion
482
+ if expansion
483
+ copy_ted_ram
484
+ else
485
+ @ted_pages.fill(@ram_view, 0x80, 0x80)
486
+ end
487
+ high_bank = expansion&.kernal_for_ted? ? 0 : @high_bank
488
+ high_rom = @high_roms[high_bank] || @open_bus
489
+ map_roms(@ted_pages, high_bank) if @rom_selected
490
+ map_roms(@ted_rom_pages, high_bank)
491
+ @ted_pages.fill(high_rom, 0xfd, 3) if @rom_selected
492
+ @ted_rom_pages.fill(high_rom, 0xfd, 3)
493
+ end
494
+
495
+ def copy_ted_ram
496
+ ted_ram = @ted_ram
497
+ page = 0
498
+ while page < 0x100
499
+ @ted_pages[page] = ted_ram[page]
500
+ page += 1
501
+ end
303
502
  end
304
503
 
305
- def map_roms
306
- @read_pages.fill(@low_roms[@low_bank] || @open_bus, 0x80, 0x40)
307
- @read_pages.fill(@high_roms[@high_bank] || @open_bus, 0xc0, 0x3c)
308
- @read_pages[0xfc] = @high_roms[0]
504
+ def map_roms(pages, high_bank)
505
+ pages.fill(@low_roms[@low_bank] || @open_bus, 0x80, 0x40)
506
+ pages.fill(@high_roms[high_bank] || @open_bus, 0xc0, 0x3c)
507
+ pages[0xfc] = @high_roms[0]
309
508
  end
310
509
  end
311
510
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Plus4
5
+ # The C16's and C116's cassette sense, which has no 6529 to go through:
6
+ # a read of $FD10-$FD1F enables a 74LS125 that drives the sense line
7
+ # onto data bit 2, low while a key is down on the deck (C16 schematic
8
+ # 251788, sheet 1, U11). Nothing drives the other bits, which keep the
9
+ # last byte on the bus, and a write goes nowhere.
10
+ class CassetteSense
11
+ TAPE_SENSE = 0x04
12
+
13
+ def initialize(datasette)
14
+ @datasette = datasette
15
+ end
16
+
17
+ # The byte a read finds, from +data+, the last byte on the bus.
18
+ def read(data) = @datasette.sense_low? ? data & ~TAPE_SENSE : data | TAPE_SENSE
19
+ end
20
+ end
21
+ end
@@ -2,12 +2,19 @@
2
2
 
3
3
  module Badline
4
4
  class Plus4
5
- # The keyboard: the 6529 at $FD30-$FD3F, whose eight outputs drive the
6
- # key matrix's lines, and the TED's K0-K7 inputs, which $FF08 latches
7
- # (Chips::TED#keyboard_port=). The KERNAL's scan ($DB11) writes each
8
- # line's mask to $FD30 and $FF08 and reads the keys back from $FF08,
9
- # and its key table at $E026 puts each key at line times 8 plus input
10
- # bit.
5
+ # The keyboard and the joysticks: the 6529 at $FD30-$FD3F, whose eight
6
+ # outputs drive the key matrix's lines, and the TED's K0-K7 inputs,
7
+ # which $FF08 latches (Chips::TED#keyboard_port=). The KERNAL's scan
8
+ # ($DB11) writes each line's mask to $FD30 and $FF08 and reads the keys
9
+ # back from $FF08, and its key table at $E026 puts each key at line
10
+ # times 8 plus input bit.
11
+ #
12
+ # The joysticks share K0-K7. The byte written to $FF08 selects them,
13
+ # bit 2 low joystick 1 and bit 1 low joystick 2, as BASIC's JOY does
14
+ # ($FA and $FD). A selected joystick pulls up, down, left and right
15
+ # onto K0-K3 and its fire button onto K6 (joystick 1) or K7
16
+ # (joystick 2), and those lines settle against the matrix with the
17
+ # keys'.
11
18
  #
12
19
  # The 6529 is a quasi-bidirectional latch: a read returns what was
13
20
  # written to it.
@@ -28,8 +35,10 @@ module Badline
28
35
 
29
36
  attr_reader :keyboard, :output
30
37
 
31
- def initialize(keyboard)
38
+ def initialize(keyboard, joystick1, joystick2)
32
39
  @keyboard = keyboard
40
+ @joystick1 = joystick1
41
+ @joystick2 = joystick2
33
42
  @output = 0xff
34
43
  end
35
44
 
@@ -43,9 +52,25 @@ module Badline
43
52
  @output = value
44
53
  end
45
54
 
46
- # The K0-K7 inputs the TED latches on a write of +_value+ to $FF08:
47
- # high but where a held key joins a line the 6529 pulls low.
48
- def latch(_value) = @keyboard.scan(@output, 0xff).last
55
+ # The K0-K7 inputs the TED latches on a write of +value+ to $FF08:
56
+ # high but where a held key joins a line the 6529 pulls low, or a
57
+ # joystick +value+ selects holds a switch closed.
58
+ def latch(value) = @keyboard.scan(@output, joystick_lines(value)).last
59
+
60
+ private
61
+
62
+ def joystick_lines(value)
63
+ lines = 0xff
64
+ lines &= joystick_inputs(@joystick1.port_bits, 0x40) if value.nobits?(0x04)
65
+ lines &= joystick_inputs(@joystick2.port_bits, 0x80) if value.nobits?(0x02)
66
+ lines
67
+ end
68
+
69
+ # A joystick's switches, Input::Joystick's bits 0-4, on K0-K7: the
70
+ # directions on K0-K3 and the fire button on +fire+.
71
+ def joystick_inputs(bits, fire)
72
+ bits.anybits?(0x10) ? bits | 0xf0 : (bits | 0xf0) & ~fire & 0xff
73
+ end
49
74
  end
50
75
  end
51
76
  end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Plus4
5
+ # The whole machine as keyed snapshot modules (Snapshot::Keyed): BL-MACHINE
6
+ # with the setup, the cartridges' ROMs by slot, the CPU with its port,
7
+ # the TED, its sound, the SID card's SID where one is fitted, the
8
+ # keyboard's 6529, the user port's 6529 and the ACIA where the model
9
+ # has them, the TCBM interface's TIAs where one is plugged in, the RAM
10
+ # with the ROM bank latch and the ROM/RAM switch under bus.*, a RAM
11
+ # expansion's registers and banks, the datasette, device 8 through the
12
+ # traps and the true drives.
13
+ #
14
+ # A load maps the banks before the chips load, and works out what each
15
+ # unit derives from its fields once every unit is loaded.
16
+ module KeyedModules
17
+ def save_modules
18
+ bus = @bus
19
+ sections = [Snapshot::Keyed.section("BL-MACHINE") { |out| save_machine_fields(out) }]
20
+ sections << Snapshot::Keyed.section("BL-CART") { |out| bus.save_slot_fields(out) } if bus.cartridge?
21
+ save_chip_modules(sections)
22
+ sections << Snapshot::Keyed.section("BL-RAM") do |out|
23
+ bus.ram.save_fields(out, "ram")
24
+ bus.save_fields(out, "bus.")
25
+ end
26
+ expansion = bus.ram_expansion
27
+ sections << Snapshot::Keyed.section("BL-RAMEXP") { |out| expansion.save_fields(out) } if expansion
28
+ sections << Snapshot::Keyed.section("BL-TAPE") { |out| @datasette.save_fields(out) }
29
+ sections << Snapshot::Keyed.section("BL-TRAPDOS") { |out| save_trap_dos_fields(out) } if mounted?
30
+ save_drive_modules(sections)
31
+ end
32
+
33
+ # Raises Snapshot::FormatError for the modules of a machine built
34
+ # another way.
35
+ def load_modules(modules)
36
+ machine = modules.machine
37
+ check_snapshot_setup(Snapshot::Plus4Setup.read_fields(machine))
38
+ load_machine_fields(machine)
39
+ bus = @bus
40
+ memory = modules.required("BL-RAM")
41
+ bus.ram.load_fields(memory, "ram")
42
+ bus.load_fields(memory, "bus.")
43
+ bus.load_slot_fields(modules.optional("BL-CART") || Snapshot::Keyed::Reader.new("BL-CART"))
44
+ load_ram_expansion(modules)
45
+ bus.restored!
46
+ load_chip_modules(modules)
47
+ @datasette.load_fields(modules.required("BL-TAPE"))
48
+ trap_dos = modules.optional("BL-TRAPDOS")
49
+ trap_dos ? load_trap_dos_fields(trap_dos, detached: modules.detached?) : unmount
50
+ load_drive_modules(modules)
51
+ modules_restored!
52
+ self
53
+ end
54
+
55
+ private
56
+
57
+ def load_ram_expansion(modules)
58
+ expansion = @bus.ram_expansion
59
+ return if expansion.nil?
60
+
61
+ expansion.load_fields(modules.required("BL-RAMEXP"))
62
+ end
63
+
64
+ def save_chip_modules(sections)
65
+ sections << Snapshot::Keyed.section("BL-CPU") do |out|
66
+ @cpu.save_fields(out)
67
+ @port.save_fields(out, "port.")
68
+ end
69
+ sections << Snapshot::Keyed.section("BL-TED") { |out| @ted.save_fields(out) }
70
+ sections << Snapshot::Keyed.section("BL-TEDSOUND") { |out| @sound.save_fields(out) }
71
+ sid_card = @sid_card
72
+ sections << Snapshot::Keyed.section("BL-SID") { |out| sid_card.save_fields(out) } if sid_card
73
+ sections << Snapshot::Keyed.section("BL-KEYPORT") { |out| @keyboard_port.save_fields(out) }
74
+ user_port = @user_port
75
+ sections << Snapshot::Keyed.section("BL-USERPORT") { |out| user_port.save_fields(out) } if user_port
76
+ acia = @acia
77
+ sections << Snapshot::Keyed.section("BL-ACIA") { |out| acia.save_fields(out) } if acia
78
+ @tias.each_with_index do |tia, index|
79
+ sections << Snapshot::Keyed.section("BL-TIA#{index + 1}") { |out| tia.save_fields(out) }
80
+ end
81
+ end
82
+
83
+ def load_chip_modules(modules)
84
+ cpu = modules.required("BL-CPU")
85
+ @port.load_fields(cpu, "port.")
86
+ @ted.load_fields(modules.required("BL-TED"))
87
+ @sound.load_fields(modules.required("BL-TEDSOUND"))
88
+ @sid_card&.load_fields(modules.required("BL-SID"))
89
+ @keyboard_port.load_fields(modules.required("BL-KEYPORT"))
90
+ @user_port&.load_fields(modules.required("BL-USERPORT"))
91
+ @acia&.load_fields(modules.required("BL-ACIA"))
92
+ @tias.each_with_index { |tia, index| tia.load_fields(modules.required("BL-TIA#{index + 1}")) }
93
+ @cpu.load_fields(cpu)
94
+ end
95
+
96
+ def modules_restored!
97
+ @cpu.restored!
98
+ @ted.restored!
99
+ @sound.restored!(@ted)
100
+ @sid_card&.restored!
101
+ @port.restored!
102
+ @acia&.restored!
103
+ schedule
104
+ end
105
+
106
+ def check_snapshot_setup(setup)
107
+ ours = Snapshot::Plus4Setup.of(self)
108
+ return if setup == ours
109
+
110
+ raise Snapshot::FormatError, "the snapshot is of a #{setup}, not a #{ours}"
111
+ end
112
+ end
113
+
114
+ include KeyedModules
115
+ end
116
+ end