badline 0.15.0 → 0.16.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 (155) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +102 -0
  3. data/CLAUDE.md +15 -7
  4. data/README.md +142 -16
  5. data/doc/media.md +19 -0
  6. data/doc/pinned-behaviour.md +67 -0
  7. data/doc/snapshot-format.md +274 -31
  8. data/doc/snapshots.md +14 -0
  9. data/exe/badline-ruby +1 -1
  10. data/lib/badline/audio/machine_player.rb +2 -14
  11. data/lib/badline/c64/common.rb +1 -1
  12. data/lib/badline/c64/true_drives.rb +13 -1
  13. data/lib/badline/c64.rb +6 -0
  14. data/lib/badline/chips/crtc/keyed_state.rb +46 -0
  15. data/lib/badline/chips/crtc.rb +163 -0
  16. data/lib/badline/chips/pia/control_lines.rb +138 -0
  17. data/lib/badline/chips/pia/keyed_parts.rb +56 -0
  18. data/lib/badline/chips/pia.rb +138 -0
  19. data/lib/badline/chips/tia.rb +16 -0
  20. data/lib/badline/core.rb +3 -0
  21. data/lib/badline/drives/datasette/keyed_state.rb +8 -3
  22. data/lib/badline/drives/engine/core.rb +6 -14
  23. data/lib/badline/drives/engine/idle.rb +42 -4
  24. data/lib/badline/drives/engine/idle_cpu.rb +18 -0
  25. data/lib/badline/drives/engine/serial_connection.rb +25 -0
  26. data/lib/badline/drives/engine/sleep.rb +1 -0
  27. data/lib/badline/drives/engine.rb +1 -0
  28. data/lib/badline/drives/ieee_bus.rb +28 -0
  29. data/lib/badline/drives/model1541/byte_ready.rb +19 -3
  30. data/lib/badline/drives/model1541/mechanism.rb +10 -1
  31. data/lib/badline/drives/model1541.rb +1 -0
  32. data/lib/badline/drives/model1551/bus.rb +138 -0
  33. data/lib/badline/drives/model1551/cable.rb +107 -0
  34. data/lib/badline/drives/model1551/cpu.rb +14 -0
  35. data/lib/badline/drives/model1551/keyed_state.rb +65 -0
  36. data/lib/badline/drives/model1551/port.rb +74 -0
  37. data/lib/badline/drives/model1551/tia.rb +71 -0
  38. data/lib/badline/drives/model1551.rb +192 -0
  39. data/lib/badline/drives/model1571.rb +1 -0
  40. data/lib/badline/drives/model1581.rb +1 -0
  41. data/lib/badline/drives.rb +1 -0
  42. data/lib/badline/frontend/app.rb +1 -1
  43. data/lib/badline/frontend/boot.rb +20 -1
  44. data/lib/badline/frontend/controls.rb +23 -3
  45. data/lib/badline/frontend/gamepads.rb +10 -3
  46. data/lib/badline/frontend/keys.rb +88 -0
  47. data/lib/badline/frontend/media_slots.rb +1 -0
  48. data/lib/badline/frontend/menu/menu_dialogs.rb +15 -6
  49. data/lib/badline/frontend/menu/menu_media.rb +96 -19
  50. data/lib/badline/frontend/menu/menu_pages.rb +25 -7
  51. data/lib/badline/frontend/menu/menu_ports.rb +22 -1
  52. data/lib/badline/frontend/menu/pause_menu.rb +1 -0
  53. data/lib/badline/frontend/pet_keyboard.rb +124 -0
  54. data/lib/badline/frontend/player/player_controller.rb +11 -4
  55. data/lib/badline/frontend/screens.rb +2 -1
  56. data/lib/badline/frontend/timeline.rb +16 -9
  57. data/lib/badline/frontend.rb +1 -0
  58. data/lib/badline/kernal_trap/dos/writes.rb +3 -1
  59. data/lib/badline/kernal_trap/dos.rb +6 -1
  60. data/lib/badline/kernal_trap/frames.rb +117 -0
  61. data/lib/badline/kernal_trap/ieee.rb +148 -0
  62. data/lib/badline/kernal_trap/ieee_layout.rb +113 -0
  63. data/lib/badline/kernal_trap/keyed_state.rb +4 -4
  64. data/lib/badline/kernal_trap/routine.rb +3 -53
  65. data/lib/badline/kernal_trap/serial.rb +4 -102
  66. data/lib/badline/kernal_trap/steps.rb +64 -0
  67. data/lib/badline/kernal_trap.rb +4 -0
  68. data/lib/badline/machine/keyboard_buffer.rb +8 -0
  69. data/lib/badline/machine/settling.rb +1 -1
  70. data/lib/badline/machine.rb +37 -11
  71. data/lib/badline/media/extensions.rb +5 -2
  72. data/lib/badline/media/pet_media.rb +40 -0
  73. data/lib/badline/media/program.rb +14 -2
  74. data/lib/badline/media/true_drive.rb +7 -5
  75. data/lib/badline/media/vic20_media.rb +2 -2
  76. data/lib/badline/media.rb +36 -16
  77. data/lib/badline/options/family.rb +17 -6
  78. data/lib/badline/options/help.rb +23 -5
  79. data/lib/badline/options/option.rb +2 -1
  80. data/lib/badline/options/table.rb +28 -4
  81. data/lib/badline/options/validation.rb +117 -14
  82. data/lib/badline/options.rb +65 -21
  83. data/lib/badline/pet/bus/expansion.rb +106 -0
  84. data/lib/badline/pet/bus.rb +204 -0
  85. data/lib/badline/pet/canvas.rb +97 -0
  86. data/lib/badline/pet/cassettes.rb +63 -0
  87. data/lib/badline/pet/characters.rb +26 -0
  88. data/lib/badline/pet/cpu.rb +10 -0
  89. data/lib/badline/pet/crtc_video.rb +142 -0
  90. data/lib/badline/pet/discrete_video.rb +83 -0
  91. data/lib/badline/pet/ieee_ports.rb +66 -0
  92. data/lib/badline/pet/ieee_traps.rb +75 -0
  93. data/lib/badline/pet/keyboard_ports.rb +74 -0
  94. data/lib/badline/pet/keyed_modules.rb +85 -0
  95. data/lib/badline/pet/keyed_parts.rb +123 -0
  96. data/lib/badline/pet/keyed_state.rb +42 -0
  97. data/lib/badline/pet/model.rb +113 -0
  98. data/lib/badline/pet/rom_sets.rb +65 -0
  99. data/lib/badline/pet/sound.rb +128 -0
  100. data/lib/badline/pet/via.rb +10 -0
  101. data/lib/badline/pet/via_ports.rb +19 -0
  102. data/lib/badline/pet.rb +226 -0
  103. data/lib/badline/plus4/acia/frames.rb +55 -0
  104. data/lib/badline/plus4/acia.rb +276 -16
  105. data/lib/badline/plus4/bus.rb +181 -45
  106. data/lib/badline/plus4/keyed_modules.rb +22 -5
  107. data/lib/badline/plus4/keyed_parts.rb +33 -4
  108. data/lib/badline/plus4/keyed_state.rb +10 -5
  109. data/lib/badline/plus4/ram_expansion.rb +145 -0
  110. data/lib/badline/plus4/sid_card.rb +95 -0
  111. data/lib/badline/plus4/sound_mix.rb +51 -0
  112. data/lib/badline/plus4/tcbm_drive.rb +134 -0
  113. data/lib/badline/plus4/user_port.rb +11 -2
  114. data/lib/badline/plus4.rb +104 -22
  115. data/lib/badline/roms/README +146 -0
  116. data/lib/badline/roms/dos1551.rom +0 -0
  117. data/lib/badline/roms/pet/basic1.rom +0 -0
  118. data/lib/badline/roms/pet/basic2.rom +0 -0
  119. data/lib/badline/roms/pet/basic4.rom +0 -0
  120. data/lib/badline/roms/pet/characters1.rom +0 -0
  121. data/lib/badline/roms/pet/characters2.rom +0 -0
  122. data/lib/badline/roms/pet/editor1.rom +0 -0
  123. data/lib/badline/roms/pet/editor2-b.rom +0 -0
  124. data/lib/badline/roms/pet/editor2-n.rom +0 -0
  125. data/lib/badline/roms/pet/editor4-40-n-50hz.rom +0 -0
  126. data/lib/badline/roms/pet/editor4-40-n-60hz.rom +0 -0
  127. data/lib/badline/roms/pet/editor4-80-b-50hz-0283.rom +0 -0
  128. data/lib/badline/roms/pet/editor4-80-b-50hz.rom +0 -0
  129. data/lib/badline/roms/pet/editor4-80-b-60hz.rom +0 -0
  130. data/lib/badline/roms/pet/editor4-b.rom +0 -0
  131. data/lib/badline/roms/pet/editor4-n.rom +0 -0
  132. data/lib/badline/roms/pet/kernal1.rom +0 -0
  133. data/lib/badline/roms/pet/kernal2.rom +0 -0
  134. data/lib/badline/roms/pet/kernal4.rom +0 -0
  135. data/lib/badline/snapshot/container.rb +2 -0
  136. data/lib/badline/snapshot/field_doc/markdown.rb +1 -1
  137. data/lib/badline/snapshot/field_doc.rb +21 -6
  138. data/lib/badline/snapshot/image.rb +14 -7
  139. data/lib/badline/snapshot/pet_setup.rb +71 -0
  140. data/lib/badline/snapshot/plus4_setup.rb +52 -10
  141. data/lib/badline/snapshot/storage_setup.rb +7 -1
  142. data/lib/badline/snapshot.rb +9 -5
  143. data/lib/badline/storage/d80_image.rb +48 -0
  144. data/lib/badline/storage/d82_image.rb +21 -0
  145. data/lib/badline/storage/g64_image.rb +1 -1
  146. data/lib/badline/storage/tap.rb +16 -3
  147. data/lib/badline/storage.rb +2 -0
  148. data/lib/badline/support/pet_checkpoint.rb +38 -0
  149. data/lib/badline/timing.rb +5 -4
  150. data/lib/badline/version.rb +1 -1
  151. data/lib/badline/vic20/attachments.rb +7 -2
  152. data/lib/badline/vic20/bus/keyed_state.rb +5 -4
  153. data/lib/badline/vic20/bus.rb +5 -0
  154. data/lib/badline/vic20.rb +1 -1
  155. metadata +69 -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,7 +32,8 @@ 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.
31
37
  #
32
38
  # The TED fetches through the same banks: the video matrix and the
33
39
  # attributes from ROM or RAM at $8000-$FFFF as the CPU sees it, with no
@@ -40,17 +46,18 @@ module Badline
40
46
 
41
47
  RAM_POWER_ON = 0x00
42
48
 
43
- # RAM through a size it repeats at.
49
+ # RAM through a size it repeats at, from +offset+ on in +ram+.
44
50
  class MirroredRAM
45
- def initialize(ram, mask)
51
+ def initialize(ram, mask, offset = 0)
46
52
  @ram = ram
47
53
  @mask = mask
54
+ @offset = offset
48
55
  end
49
56
 
50
- def peek(addr) = @ram.peek(addr & @mask)
57
+ def peek(addr) = @ram.peek((addr & @mask) + @offset)
51
58
 
52
59
  def poke(addr, value)
53
- @ram.poke(addr & @mask, value)
60
+ @ram.poke((addr & @mask) + @offset, value)
54
61
  end
55
62
  end
56
63
 
@@ -80,14 +87,20 @@ module Badline
80
87
  end
81
88
  end
82
89
 
83
- # Page $FD: the ACIA, the user port, the keyboard port and the ROM
84
- # bank latch, by the address's bits 4-7. +acia+ and +user_port+ are
85
- # nil where the machine has none, and +cassette_sense+ answers in the
86
- # user port's place on a machine without one (CassetteSense).
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.
87
98
  class IOPage
88
- def initialize(keyboard_port:, acia: nil, user_port: nil, cassette_sense: nil)
99
+ def initialize(keyboard_port:, acia: nil, user_port: nil, cassette_sense: nil, sid_card: nil)
89
100
  @bus = nil
101
+ @ram_expansion = nil
90
102
  @acia = acia
103
+ @sid_card = sid_card
91
104
  @user_port = user_port
92
105
  @cassette_sense = cassette_sense
93
106
  @keyboard_port = keyboard_port
@@ -97,13 +110,15 @@ module Badline
97
110
  # a write to $FDD0-$FDDF sets.
98
111
  def connect(bus)
99
112
  @bus = bus
113
+ @ram_expansion = bus.ram_expansion
100
114
  end
101
115
 
102
116
  def peek(addr)
103
117
  case addr & 0xf0
104
118
  when 0x00 then @acia ? @acia.peek(addr) : @bus.data
105
- when 0x10 then user_port_peek(addr)
119
+ when 0x10 then @ram_expansion ? expansion_peek(addr) : user_port_peek(addr)
106
120
  when 0x30 then @keyboard_port.peek(addr)
121
+ when 0x40, 0x50 then @sid_card ? @sid_card.peek(addr) : @bus.data
107
122
  else @bus.data
108
123
  end
109
124
  end
@@ -111,14 +126,31 @@ module Badline
111
126
  def poke(addr, value)
112
127
  case addr & 0xf0
113
128
  when 0x00 then @acia&.poke(addr, value)
114
- when 0x10 then @user_port&.poke(addr, value)
129
+ when 0x10 then @ram_expansion ? expansion_poke(addr, value) : @user_port&.poke(addr, value)
115
130
  when 0x30 then @keyboard_port.poke(addr, value)
131
+ when 0x40, 0x50 then @sid_card&.poke(addr, value)
116
132
  when 0xd0 then @bus.select_banks(addr)
117
133
  end
118
134
  end
119
135
 
120
136
  private
121
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
+
122
154
  def user_port_peek(addr)
123
155
  return @user_port.peek(addr) if @user_port
124
156
  return @cassette_sense.read(@bus.data) if @cassette_sense
@@ -148,26 +180,50 @@ module Badline
148
180
  end
149
181
  end
150
182
 
151
- # Page $FE with a 1551's TCBM interface plugged in: the 6523 TIA
152
- # that talks to drive 9 at $FEC0-$FEDF and drive 8's at $FEE0-$FEFF.
153
- # 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.
154
188
  class TCBMPage
155
- 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)
156
196
  @bus = bus
157
- @drive8 = drive8
158
- @drive9 = drive9
197
+ @drive8 = nil
198
+ @drive9 = nil
199
+ @sid_card = nil
159
200
  end
160
201
 
161
202
  def peek(addr)
162
- return @bus.data if addr < 0xfec0
203
+ return low_peek(addr) if addr < 0xfec0
163
204
 
164
- addr < 0xfee0 ? @drive9.peek(addr) : @drive8.peek(addr)
205
+ tia = addr < 0xfee0 ? @drive9 : @drive8
206
+ tia ? tia.peek(addr) : @bus.data
165
207
  end
166
208
 
167
209
  def poke(addr, value)
168
- 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
219
+
220
+ private
221
+
222
+ def sid_card?(addr) = (addr & 0xe0) == 0x80
169
223
 
170
- addr < 0xfee0 ? @drive9.poke(addr, value) : @drive8.poke(addr, value)
224
+ def low_peek(addr)
225
+ sid_card = @sid_card
226
+ sid_card && sid_card?(addr) ? sid_card.peek(addr) : @bus.data
171
227
  end
172
228
  end
173
229
 
@@ -186,7 +242,7 @@ module Badline
186
242
  end
187
243
 
188
244
  def poke(addr, value)
189
- return @bus.ram_view.poke(addr, value) if addr >= 0xff40
245
+ return @bus.top_ram.poke(addr, value) if addr >= 0xff40
190
246
 
191
247
  case addr
192
248
  when 0xff3e then @bus.rom_selected = true
@@ -201,17 +257,29 @@ module Badline
201
257
  # What the top page reads above $FF3F: the high ROM bank or RAM.
202
258
  attr_reader :top_memory
203
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
+
204
267
  # The CPU sees ROM at $8000-$FFFF.
205
268
  attr_reader :rom_selected
206
269
 
207
270
  # +io+ is the IOPage, +model+ a Model profile, whose KERNAL it maps
208
271
  # and whose function ROMs it maps where the model has them, and
209
- # +ram_kb+ the RAM fitted.
210
- 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)
211
275
  addressable_at(0, length: 2**16)
212
276
  size = ram_kb * 1024
213
277
  @ram = Memory.new(Array.new(size, RAM_POWER_ON), length: size, start: 0)
214
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
215
283
  @ted = ted
216
284
  @open_bus = OpenBus.new(self)
217
285
  @low_roms = [ROM.load("plus4/basic.rom", 0x8000), nil, nil, nil]
@@ -220,11 +288,12 @@ module Badline
220
288
  @low_roms[1] = ROM.load("plus4/function-low.rom", 0x8000)
221
289
  @high_roms[1] = ROM.load("plus4/function-high.rom", 0xc000)
222
290
  end
291
+ @port = port
223
292
  @zero_page = ZeroPage.new(port, @ram_view)
224
293
  @io_page = io
225
294
  io.connect(self)
226
295
  @top_page = TopPage.new(self, ted)
227
- @tcbm_page = @open_bus
296
+ @tcbm_page = TCBMPage.new(self)
228
297
  @debug_register = nil
229
298
  @data = 0
230
299
  @read_pages = Array.new(256, @open_bus)
@@ -234,19 +303,29 @@ module Badline
234
303
  @low_bank = @high_bank = 0
235
304
  @rom_selected = true
236
305
  @top_memory = @ram_view
237
- map_writes
238
- map_reads
306
+ map_ram
239
307
  end
240
308
 
241
309
  def power_on!
242
310
  @ram.clear!(Array.new(@ram.length, RAM_POWER_ON))
311
+ @ram_expansion&.power_on!
243
312
  reset!
244
313
  end
245
314
 
246
- # 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.
247
317
  def reset!
248
318
  @low_bank = @high_bank = 0
249
- 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
250
329
  end
251
330
 
252
331
  def rom_selected=(selected)
@@ -274,9 +353,23 @@ module Badline
274
353
 
275
354
  # Plugs in a 1551's TCBM interface, the TIAs at $FEC0-$FEFF.
276
355
  def plug_tcbm(drive8:, drive9:)
277
- @tcbm_page = TCBMPage.new(self, drive8:, drive9:)
278
- map_writes
279
- map_reads
356
+ @tcbm_page.drive8 = drive8
357
+ @tcbm_page.drive9 = drive9
358
+ end
359
+
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
280
373
  end
281
374
 
282
375
  # Puts a cartridge's ROM chips in their slots, as a PLUS4 CARTRIDGE
@@ -334,12 +427,35 @@ module Badline
334
427
 
335
428
  # A TED fetch of character or bitmap data, from ROM when +rom+.
336
429
  def ted_pattern_fetch(addr, rom)
337
- @data = rom ? @ted_rom_pages[addr >> 8].peek(addr) : @ram_view.peek(addr)
430
+ @data = rom ? @ted_rom_pages[addr >> 8].peek(addr) : @ted_ram[addr >> 8].peek(addr)
338
431
  end
339
432
 
340
433
  private
341
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
+
342
457
  def map_writes
458
+ copy_ram(@write_pages) if @ram_expansion
343
459
  @write_pages[0x00] = @zero_page
344
460
  @write_pages[0xfd] = @debug_register || @io_page
345
461
  @write_pages[0xfe] = @tcbm_page
@@ -347,10 +463,14 @@ module Badline
347
463
  end
348
464
 
349
465
  def map_reads
350
- @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
351
471
  @read_pages[0x00] = @zero_page
352
- map_roms(@read_pages) if @rom_selected
353
- @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
354
474
  @read_pages[0xfd] = @io_page
355
475
  @read_pages[0xfe] = @tcbm_page
356
476
  @read_pages[0xff] = @top_page
@@ -358,16 +478,32 @@ module Badline
358
478
  end
359
479
 
360
480
  def map_ted_reads
361
- @ted_pages.fill(@ram_view, 0x80, 0x80)
362
- map_roms(@ted_pages) if @rom_selected
363
- map_roms(@ted_rom_pages)
364
- @ted_pages.fill(@high_roms[@high_bank] || @open_bus, 0xfd, 3) if @rom_selected
365
- @ted_rom_pages.fill(@high_roms[@high_bank] || @open_bus, 0xfd, 3)
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
366
502
  end
367
503
 
368
- def map_roms(pages)
504
+ def map_roms(pages, high_bank)
369
505
  pages.fill(@low_roms[@low_bank] || @open_bus, 0x80, 0x40)
370
- pages.fill(@high_roms[@high_bank] || @open_bus, 0xc0, 0x3c)
506
+ pages.fill(@high_roms[high_bank] || @open_bus, 0xc0, 0x3c)
371
507
  pages[0xfc] = @high_roms[0]
372
508
  end
373
509
  end
@@ -4,11 +4,12 @@ module Badline
4
4
  class Plus4
5
5
  # The whole machine as keyed snapshot modules (Snapshot::Keyed): BL-MACHINE
6
6
  # with the setup, the cartridges' ROMs by slot, the CPU with its port,
7
- # the TED, its sound, the keyboard's 6529, the user port's 6529 and the
8
- # ACIA where the model has them, the TCBM interface's TIAs where one is
9
- # plugged in, the RAM with the ROM bank latch and the ROM/RAM switch
10
- # under bus.*, the datasette, device 8 through the traps and the true
11
- # drives.
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.
12
13
  #
13
14
  # A load maps the banks before the chips load, and works out what each
14
15
  # unit derives from its fields once every unit is loaded.
@@ -22,6 +23,8 @@ module Badline
22
23
  bus.ram.save_fields(out, "ram")
23
24
  bus.save_fields(out, "bus.")
24
25
  end
26
+ expansion = bus.ram_expansion
27
+ sections << Snapshot::Keyed.section("BL-RAMEXP") { |out| expansion.save_fields(out) } if expansion
25
28
  sections << Snapshot::Keyed.section("BL-TAPE") { |out| @datasette.save_fields(out) }
26
29
  sections << Snapshot::Keyed.section("BL-TRAPDOS") { |out| save_trap_dos_fields(out) } if mounted?
27
30
  save_drive_modules(sections)
@@ -38,6 +41,7 @@ module Badline
38
41
  bus.ram.load_fields(memory, "ram")
39
42
  bus.load_fields(memory, "bus.")
40
43
  bus.load_slot_fields(modules.optional("BL-CART") || Snapshot::Keyed::Reader.new("BL-CART"))
44
+ load_ram_expansion(modules)
41
45
  bus.restored!
42
46
  load_chip_modules(modules)
43
47
  @datasette.load_fields(modules.required("BL-TAPE"))
@@ -50,6 +54,13 @@ module Badline
50
54
 
51
55
  private
52
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
+
53
64
  def save_chip_modules(sections)
54
65
  sections << Snapshot::Keyed.section("BL-CPU") do |out|
55
66
  @cpu.save_fields(out)
@@ -57,6 +68,8 @@ module Badline
57
68
  end
58
69
  sections << Snapshot::Keyed.section("BL-TED") { |out| @ted.save_fields(out) }
59
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
60
73
  sections << Snapshot::Keyed.section("BL-KEYPORT") { |out| @keyboard_port.save_fields(out) }
61
74
  user_port = @user_port
62
75
  sections << Snapshot::Keyed.section("BL-USERPORT") { |out| user_port.save_fields(out) } if user_port
@@ -72,6 +85,7 @@ module Badline
72
85
  @port.load_fields(cpu, "port.")
73
86
  @ted.load_fields(modules.required("BL-TED"))
74
87
  @sound.load_fields(modules.required("BL-TEDSOUND"))
88
+ @sid_card&.load_fields(modules.required("BL-SID"))
75
89
  @keyboard_port.load_fields(modules.required("BL-KEYPORT"))
76
90
  @user_port&.load_fields(modules.required("BL-USERPORT"))
77
91
  @acia&.load_fields(modules.required("BL-ACIA"))
@@ -83,7 +97,10 @@ module Badline
83
97
  @cpu.restored!
84
98
  @ted.restored!
85
99
  @sound.restored!(@ted)
100
+ @sid_card&.restored!
86
101
  @port.restored!
102
+ @acia&.restored!
103
+ schedule
87
104
  end
88
105
 
89
106
  def check_snapshot_setup(setup)
@@ -53,14 +53,43 @@ module Badline
53
53
  end
54
54
 
55
55
  class ACIA
56
- # The command and control registers in a keyed snapshot module
57
- # (BL-ACIA).
56
+ # The registers, the status, the shift register and when the
57
+ # transmitter's character ends, in a keyed snapshot module (BL-ACIA).
58
+ # What the device on the user port has still to send stays with the
59
+ # host, and the receiver starts on it again once loaded (#restored!).
60
+ # A snapshot from before the transmitter's timing was saved starts
61
+ # its character clock on load.
58
62
  module KeyedState
59
- def save_fields(out) = out.int("command", @command).int("control", @control)
63
+ def save_fields(out)
64
+ out.int("command", @command).int("control", @control).int("errors", @errors)
65
+ out.bool("receiver_full", @receiver_full).bool("transmitter_empty", @transmitter_empty).bool("irq", @irq)
66
+ out.int("receive_data", @receive_data).int("transmit_data", @transmit_data).int("shifting", @shifting)
67
+ out.int("transmit_at", @transmit_at).int("transmit_remainder", @transmit_remainder)
68
+ end
60
69
 
61
70
  def load_fields(input)
62
71
  @command = input.byte("command", @command)
63
72
  @control = input.byte("control", @control)
73
+ @errors = input.masked("errors", 0, PARITY_ERROR | FRAMING_ERROR | OVERRUN)
74
+ @receiver_full = input.bool("receiver_full", false)
75
+ @transmitter_empty = input.bool("transmitter_empty", true)
76
+ @irq = input.bool("irq", false)
77
+ @receive_data = input.byte("receive_data", 0)
78
+ @transmit_data = input.byte("transmit_data", 0)
79
+ @shifting = input.int("shifting", IDLE)
80
+ @transmit_remainder = input.int("transmit_remainder", 0)
81
+ transmit_at = input.optional_int("transmit_at")
82
+ @transmit_at = transmit_at || NEVER
83
+ @clock_lost = transmit_at.nil?
84
+ end
85
+
86
+ def restored!
87
+ @receive_at = NEVER
88
+ @lines = modem_lines
89
+ start_transmitting if @clock_lost && transmitter_on?
90
+ @clock_lost = false
91
+ start_receiving
92
+ changed
64
93
  end
65
94
  end
66
95
 
@@ -126,7 +155,7 @@ module Badline
126
155
 
127
156
  def restored!
128
157
  @ted.rom_selected = @rom_selected
129
- map_reads
158
+ map_ram
130
159
  end
131
160
 
132
161
  private
@@ -4,9 +4,9 @@ module Badline
4
4
  class Plus4
5
5
  # The machine's own fields in a keyed snapshot module (Snapshot::Keyed,
6
6
  # BL-MACHINE): the family, its setup by name (Snapshot::Plus4Setup), the
7
- # cycle count, and the host's typing still to feed and on_init handlers
8
- # still to run under host.*. The badline version that wrote it is
9
- # written for reports and never read.
7
+ # cycle count, and the host's typing still to feed, on_init handlers
8
+ # still to run and the model plug_true_drive plugs in under host.*. The
9
+ # badline version that wrote it is written for reports and never read.
10
10
  module KeyedState
11
11
  # How many more on_init handlers the machine a snapshot was taken of had
12
12
  # yet to run than this one has. Nil until the machine is restored.
@@ -16,8 +16,11 @@ module Badline
16
16
  # instructions or jammed.
17
17
  def settled_for_snapshot? = @cpu.boundary? || @cpu.jammed?
18
18
 
19
- # The sound's voices, run up to this cycle.
20
- def settle_chips_for_snapshot = @sound.catch_up!
19
+ # The sound's voices and the SID card's, run up to this cycle.
20
+ def settle_chips_for_snapshot
21
+ @sound.catch_up!
22
+ @sid_card&.catch_up!
23
+ end
21
24
 
22
25
  def save_machine_fields(out)
23
26
  out.text("family", "plus4").text("written_by", VERSION)
@@ -26,6 +29,7 @@ module Badline
26
29
  pending = @pending_keys
27
30
  out.ints("host.pending_keys", pending) if pending
28
31
  out.int("host.init_pending", booting? ? @init_handlers.length : 0)
32
+ out.text("host.true_drive_model", @true_drive_model)
29
33
  end
30
34
 
31
35
  def load_machine_fields(input)
@@ -33,6 +37,7 @@ module Badline
33
37
  pending = input.ints("host.pending_keys", [])
34
38
  @pending_keys = pending.empty? ? nil : pending.dup
35
39
  @init_handlers_lost = [input.int("host.init_pending", 0) - @init_handlers.length, 0].max
40
+ @true_drive_model = input.choice("host.true_drive_model", TCBMDrive::TRUE_DRIVE_MODELS, @true_drive_model)
36
41
  end
37
42
  end
38
43