badline 0.12.0 → 0.12.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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -0
  3. data/lib/badline/address_bus/saved_state.rb +4 -10
  4. data/lib/badline/address_bus.rb +8 -27
  5. data/lib/badline/audio/bare_player.rb +1 -1
  6. data/lib/badline/c128/bus.rb +13 -35
  7. data/lib/badline/c128/bus_state.rb +5 -5
  8. data/lib/badline/c128.rb +5 -64
  9. data/lib/badline/computer/attachments.rb +16 -45
  10. data/lib/badline/computer/common.rb +34 -0
  11. data/lib/badline/computer/kernal_traps.rb +65 -5
  12. data/lib/badline/computer/true_drives.rb +12 -23
  13. data/lib/badline/computer.rb +5 -68
  14. data/lib/badline/core.rb +2 -2
  15. data/lib/badline/cpu_port.rb +112 -0
  16. data/lib/badline/drive1541/disk.rb +14 -17
  17. data/lib/badline/drive1541/disk_state.rb +7 -15
  18. data/lib/badline/drive1581/wd1772.rb +1 -1
  19. data/lib/badline/frontend/boot.rb +8 -59
  20. data/lib/badline/kernal_trap/{file.rb → file_routine.rb} +1 -1
  21. data/lib/badline/kernal_trap/load.rb +1 -1
  22. data/lib/badline/kernal_trap/save.rb +1 -1
  23. data/lib/badline/kernal_trap.rb +1 -1
  24. data/lib/badline/machine/clocking.rb +50 -0
  25. data/lib/badline/machine.rb +58 -2
  26. data/lib/badline/media/boot_disk.rb +2 -2
  27. data/lib/badline/media/disk_list.rb +3 -3
  28. data/lib/badline/media/disk_set.rb +1 -1
  29. data/lib/badline/media/extensions.rb +33 -0
  30. data/lib/badline/media/program.rb +28 -0
  31. data/lib/badline/media/true_drive.rb +3 -2
  32. data/lib/badline/media/tune.rb +35 -0
  33. data/lib/badline/media/vic20_cartridge.rb +1 -7
  34. data/lib/badline/media/vic20_media.rb +38 -80
  35. data/lib/badline/media.rb +82 -79
  36. data/lib/badline/options/event.rb +3 -1
  37. data/lib/badline/sid.rb +1 -1
  38. data/lib/badline/snapshot/machine_state.rb +9 -1
  39. data/lib/badline/snapshot/state.rb +5 -4
  40. data/lib/badline/snapshot.rb +11 -9
  41. data/lib/badline/storage/d64_image.rb +15 -7
  42. data/lib/badline/storage/disk_image/writing.rb +2 -2
  43. data/lib/badline/storage/disk_image.rb +1 -1
  44. data/lib/badline/storage/host_directory.rb +1 -1
  45. data/lib/badline/storage/t64.rb +1 -1
  46. data/lib/badline/version.rb +1 -1
  47. data/lib/badline/vic/bank.rb +1 -1
  48. data/lib/badline/vic/border_mask.rb +1 -1
  49. data/lib/badline/vic/collisions.rb +1 -1
  50. data/lib/badline/vic/color_patches.rb +1 -1
  51. data/lib/badline/vic/display_state.rb +1 -1
  52. data/lib/badline/vic/graphics_mode.rb +1 -1
  53. data/lib/badline/vic/graphics_shifter.rb +1 -1
  54. data/lib/badline/vic/register_log.rb +1 -1
  55. data/lib/badline/vic/registers.rb +1 -1
  56. data/lib/badline/vic/sequencer.rb +1 -1
  57. data/lib/badline/vic/sequencer_output.rb +1 -1
  58. data/lib/badline/vic/sequencer_state.rb +1 -1
  59. data/lib/badline/vic/sprite/internal_bus.rb +1 -1
  60. data/lib/badline/vic/sprite/saved_state.rb +1 -1
  61. data/lib/badline/vic/sprite/shifter.rb +1 -1
  62. data/lib/badline/vic/sprite/timing.rb +1 -1
  63. data/lib/badline/vic/sprite.rb +1 -1
  64. data/lib/badline/vic/sprites.rb +1 -1
  65. data/lib/badline/vic/sprites_state.rb +1 -1
  66. data/lib/badline/vic.rb +6 -5
  67. data/lib/badline/vic20/attachments.rb +10 -80
  68. data/lib/badline/vic20/saved_state.rb +0 -30
  69. data/lib/badline/vic20.rb +1 -37
  70. metadata +8 -3
  71. data/lib/badline/cycleable.rb +0 -38
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7e5111f801416999c9748d88475f3bdb54e1a1b4b25b3a4246a64c4b3b59bdd0
4
- data.tar.gz: c924f57fce55ffd0e9c6134c22618106828e831feed277411e8e6d56bec7c3aa
3
+ metadata.gz: 8872c5d5117a691aa38d054acd5dd7e84f7abf737c977a2dc309c6dc3072381d
4
+ data.tar.gz: fa4f4e5b3729989c78443f73bd655c2083555d62a8d3ee45437d147bfe0e5fb8
5
5
  SHA512:
6
- metadata.gz: 6a7f251456552573fab6b3041c4cc0c2ce83550c81547953ba9bfb6550e331458bf307f58a3e47ffd2bfaa98cb5c7f8938adc7c68299e1ffd6adb604460f0241
7
- data.tar.gz: 66632ccc5e1851a028837833e792a4ce2ff71cdd1e66113bed8b652618e73d0c90990359b00812c6368f949db6532b46a43684bf2d9c3334ab9876e7ea6836b6
6
+ metadata.gz: d278ba0e0341242af3e1782a8c003bc4cbdb1594be434021b5bbccae126ddf12dd89ffa0520dd615075a77f89b1285595d2101868db0e6679b69130586ed210c
7
+ data.tar.gz: 5dabb1d53a29dc43b619f90abd67495ab3847d3ba8d8f89275d45f276aac4fbe3ca37a4963f048e300e88cd50228b35bd39da7efbd431f46527b893b1c4bd95c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.12.1](https://github.com/elektronaut/badline/compare/v0.12.0...v0.12.1) (2026-10-10)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * keep the 1581's idle deadline within a native build's integers ([a98848f](https://github.com/elektronaut/badline/commit/a98848f0889ca7ad4c12007976b63d54bbbf122b))
9
+ * refuse snapshot values a native build can't hold, on CRuby too ([5ec51e8](https://github.com/elektronaut/badline/commit/5ec51e81cfabfdb585367bb2f8183516ac64d2b0))
10
+
3
11
  ## [0.12.0](https://github.com/elektronaut/badline/compare/v0.11.1...v0.12.0) (2026-10-10)
4
12
 
5
13
 
@@ -17,15 +17,12 @@ module Badline
17
17
 
18
18
  # The 6510 port's direction and output registers, and the charge left
19
19
  # on its floating bits.
20
- def port_state = [@port_ddr, @port_out, @port_floating]
20
+ def port_state = [@cpu_port.ddr, @cpu_port.data, @cpu_port.floating]
21
21
 
22
22
  # Sets the port as port_state reads it, as a snapshot restores it,
23
23
  # leaving the RAM under $00 and $01 alone.
24
24
  def restore_port(ddr, out, floating)
25
- @port_ddr = ddr
26
- @port_out = out
27
- @port_floating = floating
28
- update_port!
25
+ @cpu_port.restore(ddr, out, floating)
29
26
  end
30
27
 
31
28
  # The machine's memory and everything on the bus but the keyboard, the
@@ -33,7 +30,7 @@ module Badline
33
30
  # already in the port, built from its setup (Computer#load_state).
34
31
  def save_state(out)
35
32
  out.marker("BUS")
36
- out.int(@port_ddr).int(@port_out).int(@port_floating)
33
+ out.int(@cpu_port.ddr).int(@cpu_port.data).int(@cpu_port.floating)
37
34
  @ram.save_state(out)
38
35
  @color_ram.save_state(out)
39
36
  @ram_expansion.save_state(out)
@@ -55,10 +52,7 @@ module Badline
55
52
  @ram_expansion.load_state(input)
56
53
  @cartridge&.load_state(input)
57
54
  @datasette.load_state(input)
58
- @port_ddr = ddr
59
- @port_out = port_out
60
- @port_floating = floating
61
- @io_port.value = port_value
55
+ @cpu_port.load(ddr, port_out, floating)
62
56
  update_overlays!
63
57
  @vic.load_state(input)
64
58
  @cia1.load_state(input)
@@ -52,9 +52,9 @@ module Badline
52
52
  def poke(_addr, _value); end
53
53
  end
54
54
 
55
+ # The 6510's port pins with pull-ups, and the floating ones (CPUPort).
55
56
  PORT_PULLUPS = 0b0001_0111
56
57
  PORT_FLOATING = 0b1100_1000
57
- TAPE_SENSE = 0b0001_0000
58
58
 
59
59
  # RAM powers on in runs of $00 $00 $FF $FF $FF $FF $00 $00, inverted in
60
60
  # the second and fourth 16K: a C64C (ASSY 250469 R4) from
@@ -92,7 +92,7 @@ module Badline
92
92
 
93
93
  @datasette = Datasette.new
94
94
  @datasette.on_flag { @cia1.flag! }
95
- @datasette.on_sense_change { @io_port.value = port_value }
95
+ @datasette.on_sense_change { @cpu_port.refresh }
96
96
 
97
97
  @color_ram = ColorMemory.new(@vic)
98
98
  @vic.vic_bank.connect(cia2: @cia2, color_ram: @color_ram)
@@ -100,10 +100,9 @@ module Badline
100
100
  @open_bus = OpenBus.new(@vic)
101
101
  @cpu_off_bus = false
102
102
 
103
- @port_ddr = 0x00
104
- @port_out = 0x00
105
- @port_floating = 0x00
106
- @io_port = PortStatus.new(%i[basic kernal io tape_out tape_switch tape_motor], value: port_value)
103
+ @cpu_port = CPUPort.new(@datasette, pullups: PORT_PULLUPS, floating: PORT_FLOATING)
104
+ @cpu_port.on_change { update_overlays! }
105
+ @io_port = @cpu_port.status
107
106
 
108
107
  @read_pages = Array.new(256)
109
108
  @write_pages = Array.new(256)
@@ -134,9 +133,7 @@ module Badline
134
133
  # their charge.
135
134
  def reset!
136
135
  @ram_expansion.reset!
137
- @port_ddr = 0x00
138
- @port_out = 0x00
139
- update_port!
136
+ @cpu_port.reset!
140
137
  end
141
138
 
142
139
  def disable_overlays!
@@ -150,7 +147,7 @@ module Badline
150
147
  end
151
148
 
152
149
  def peek(addr)
153
- return @port_ddr if addr.zero?
150
+ return @cpu_port.ddr if addr.zero?
154
151
  return @io_port.value if addr == 0x01
155
152
 
156
153
  @read_pages[addr >> 8].peek(addr)
@@ -162,8 +159,7 @@ module Badline
162
159
  def poke(addr, value)
163
160
  if addr < 0x02
164
161
  @ram.poke(addr, @vic.phi1_data)
165
- addr.zero? ? @port_ddr = value : @port_out = value
166
- update_port!
162
+ addr.zero? ? @cpu_port.write_ddr(value) : @cpu_port.write_data(value)
167
163
  elsif !@cpu_off_bus
168
164
  @write_pages[addr >> 8].poke(addr, value)
169
165
  end
@@ -185,21 +181,6 @@ module Badline
185
181
 
186
182
  private
187
183
 
188
- def update_port!
189
- driven = @port_ddr & PORT_FLOATING
190
- @port_floating = (@port_floating & ~driven) | (@port_out & driven)
191
- @io_port.value = port_value
192
- # $01 bit 5 drives the motor through an inverter: low runs it.
193
- @datasette.motor = !@io_port.tape_motor?
194
- update_overlays!
195
- end
196
-
197
- def port_value
198
- input = PORT_PULLUPS | (@port_floating & PORT_FLOATING)
199
- input &= ~TAPE_SENSE if @datasette.sense_low?
200
- (@port_out & @port_ddr) | (input & ~@port_ddr & 0xff)
201
- end
202
-
203
184
  # Banking changes only on $01 writes and cartridge line/bank changes,
204
185
  # so reads and writes dispatch through per-page handler tables instead
205
186
  # of range checks.
@@ -116,7 +116,7 @@ module Badline
116
116
  cycles
117
117
  end
118
118
 
119
- def clock_hz = @tune.ntsc? ? NTSC_CLOCK_HZ : TimeOfDay::CLOCK_HZ
119
+ def clock_hz = @tune.ntsc? ? NTSC_CLOCK_HZ : Region::PAL.clock_hz
120
120
 
121
121
  private
122
122
 
@@ -47,7 +47,6 @@ module Badline
47
47
  PORT_PULLUPS = 0b0001_0111
48
48
  PORT_FLOATING = 0b1000_1000
49
49
  CAPS_LOCK = 0b0100_0000
50
- TAPE_SENSE = 0b0001_0000
51
50
 
52
51
  # The C64's power-on pattern (AddressBus::RAM_POWER_ON) in both 64K
53
52
  # banks. The C128's own DRAMs' pattern is unmeasured.
@@ -126,9 +125,6 @@ module Badline
126
125
  # KERNAL and the character ROM's C128 set.
127
126
  attr_reader :basic_low_rom, :basic_high_rom, :editor_rom, :c128_kernal_rom, :c128_character_rom
128
127
 
129
- # Whether CAPS LOCK is down, holding P6 low.
130
- attr_reader :caps_lock
131
-
132
128
  # The chips +model+, a Model::Profile, names, with the SID
133
129
  # +sid_model+. +mode+ is the mode the MMU resets into.
134
130
  def initialize(model, sid_model: model.sid_model, mode: :c64)
@@ -139,7 +135,6 @@ module Badline
139
135
  @mmu = MMU.new(mode)
140
136
  @cartridge = nil
141
137
  @debug_page = nil
142
- @caps_lock = false
143
138
 
144
139
  load_roms
145
140
  load_c128_roms
@@ -150,7 +145,7 @@ module Badline
150
145
 
151
146
  @datasette = Datasette.new
152
147
  @datasette.on_flag { @cia1.flag! }
153
- @datasette.on_sense_change { @io_port.value = port_value }
148
+ @datasette.on_sense_change { @cpu_port.refresh }
154
149
 
155
150
  @color_lines = ColorLines.new(@vic, @cia2, character_rom, @c128_character_rom)
156
151
  @color_ram = @color_lines.color_ram(1)
@@ -160,10 +155,8 @@ module Badline
160
155
  @configuration_reads = ConfigurationPage.new(@mmu, @open_bus)
161
156
  @configuration_writes = ConfigurationPage.new(@mmu, @open_bus)
162
157
 
163
- @port_ddr = 0x00
164
- @port_out = 0x00
165
- @port_floating = 0x00
166
- @io_port = PortStatus.new(%i[basic kernal io tape_out tape_switch tape_motor], value: port_value)
158
+ @cpu_port = CPUPort.new(@datasette, pullups: PORT_PULLUPS, floating: PORT_FLOATING, caps_lock: CAPS_LOCK)
159
+ @io_port = @cpu_port.status
167
160
 
168
161
  @read_pages = Array.new(256)
169
162
  @write_pages = Array.new(256)
@@ -171,6 +164,7 @@ module Badline
171
164
  @data = 0
172
165
  @io_mapped = false
173
166
  @mmu.on_change { update_overlays! }
167
+ @cpu_port.on_change { update_overlays! }
174
168
  update_overlays!
175
169
  end
176
170
 
@@ -191,19 +185,19 @@ module Badline
191
185
  @ram.clear!(RAM_POWER_ON)
192
186
  end
193
187
 
194
- # The RES line clears the port's direction and output registers and
195
- # resets the MMU. The port's floating bit keeps its charge.
188
+ # The RES line resets the MMU and clears the port's direction and
189
+ # output registers. The port's floating bit keeps its charge.
196
190
  def reset!
197
- @port_ddr = 0x00
198
- @port_out = 0x00
199
191
  @mmu.reset!
200
- update_port!
192
+ @cpu_port.reset!
201
193
  end
202
194
 
195
+ # Whether CAPS LOCK is down, holding P6 low.
196
+ def caps_lock = @cpu_port.caps_lock
197
+
203
198
  # Holds CAPS LOCK down, or lets it up.
204
199
  def caps_lock=(down)
205
- @caps_lock = down
206
- @io_port.value = port_value
200
+ @cpu_port.caps_lock = down
207
201
  end
208
202
 
209
203
  # Calls the block whenever MCR bit 6 takes the machine into C64 mode or
@@ -236,7 +230,7 @@ module Badline
236
230
  @data = if addr > 0x01
237
231
  @read_pages[addr >> 8].peek(addr)
238
232
  else
239
- addr.zero? ? @port_ddr : @io_port.value
233
+ addr.zero? ? @cpu_port.ddr : @io_port.value
240
234
  end
241
235
  end
242
236
 
@@ -254,8 +248,7 @@ module Badline
254
248
  @data = value
255
249
  if addr < 0x02
256
250
  @write_pages[0].poke(addr, @vic.phi1_data)
257
- addr.zero? ? @port_ddr = value : @port_out = value
258
- update_port!
251
+ addr.zero? ? @cpu_port.write_ddr(value) : @cpu_port.write_data(value)
259
252
  else
260
253
  @write_pages[addr >> 8].poke(addr, value)
261
254
  end
@@ -294,21 +287,6 @@ module Badline
294
287
  @vdc = VDC.new(model: model.vdc_model, ram_kb: model.vdc_ram_kb, clock_hz: @region.clock_hz)
295
288
  end
296
289
 
297
- def update_port!
298
- driven = @port_ddr & PORT_FLOATING
299
- @port_floating = (@port_floating & ~driven) | (@port_out & driven)
300
- @io_port.value = port_value
301
- @datasette.motor = !@io_port.tape_motor?
302
- update_overlays!
303
- end
304
-
305
- def port_value
306
- input = PORT_PULLUPS | (@port_floating & PORT_FLOATING)
307
- input |= CAPS_LOCK unless @caps_lock
308
- input &= ~TAPE_SENSE if @datasette.sense_low?
309
- (@port_out & @port_ddr) | (input & ~@port_ddr & 0xff)
310
- end
311
-
312
290
  def update_overlays!
313
291
  @ultimax = @cartridge ? @cartridge.ultimax? : false
314
292
  @phi1_ultimax = @cartridge ? @cartridge.phi1_ultimax? : false
@@ -11,7 +11,7 @@ module Badline
11
11
  # host holds. A cartridge is already in the port, built from its setup.
12
12
  def save_state(out)
13
13
  out.marker("C128 BUS")
14
- out.int(@port_ddr).int(@port_out).int(@port_floating).int(@address).int(@data)
14
+ out.int(@cpu_port.ddr).int(@cpu_port.data).int(@cpu_port.floating).int(@address).int(@data)
15
15
  @mmu.save_state(out)
16
16
  @ram.save_state(out)
17
17
  @color_lines.color_ram(0).save_state(out)
@@ -27,9 +27,9 @@ module Badline
27
27
 
28
28
  def load_state(input)
29
29
  input.marker("C128 BUS")
30
- @port_ddr = input.int
31
- @port_out = input.int
32
- @port_floating = input.int
30
+ ddr = input.int
31
+ data = input.int
32
+ floating = input.int
33
33
  @address = input.int
34
34
  @data = input.int
35
35
  @mmu.load_state(input)
@@ -38,7 +38,7 @@ module Badline
38
38
  @color_lines.color_ram(1).load_state(input)
39
39
  @cartridge&.load_state(input)
40
40
  @datasette.load_state(input)
41
- @io_port.value = port_value
41
+ @cpu_port.load(ddr, data, floating)
42
42
  update_overlays!
43
43
  @vic.load_state(input)
44
44
  @control_ports.extra_rows = 0xf8 | @vic.extra_keyboard_lines
data/lib/badline/c128.rb CHANGED
@@ -44,6 +44,8 @@ module Badline
44
44
  class C128
45
45
  include IntegerHelper
46
46
  include KeyboardBuffer
47
+ include Machine::Clocking
48
+ include Computer::Common
47
49
  include Computer::Attachments
48
50
  include Computer::KernalTraps
49
51
  include Keys
@@ -71,10 +73,6 @@ module Badline
71
73
 
72
74
  attr_reader :cpu, :cycles, :drive1541, :model, :init_threshold
73
75
 
74
- # The path of the disk or directory device 8 serves through the traps
75
- # (Computer::Attachments), or an empty one.
76
- def mounted_path = @drive.nil? ? "" : @drive.path
77
-
78
76
  def family = :c128
79
77
 
80
78
  def address_bus = @bus
@@ -173,36 +171,6 @@ module Badline
173
171
  @cycles += 1
174
172
  end
175
173
 
176
- # Runs `count` cycles, one #cycle! after another.
177
- def run_cycles(count)
178
- i = 0
179
- while i < count
180
- cycle!
181
- i += 1
182
- end
183
- end
184
-
185
- # Runs cycles until the block returns true or the cycle count passes
186
- # `limit`, checking before each cycle.
187
- def run_until(limit)
188
- cycle! until yield || @cycles > limit
189
- end
190
-
191
- def on_init(&block)
192
- if @cycles < init_threshold
193
- @init_handlers << block
194
- else
195
- block.call
196
- end
197
- end
198
-
199
- # The clock, the raster and the crop of the VIC-IIe's region.
200
- def timing
201
- region = @bus.region
202
- Timing.new(clock_hz: region.clock_hz, cycles_per_line: region.cycles_per_line,
203
- lines_per_frame: region.lines_per_frame, crop: region.crop, pixel_width: 1)
204
- end
205
-
206
174
  # The chip whose #display a front end shows: the VIC-IIe, or with
207
175
  # :vdc the VDC, whose display paints only while its #render is on.
208
176
  def video(chip = :vic) = chip == :vdc ? @vdc : @vic
@@ -217,12 +185,6 @@ module Badline
217
185
  # The chip a front end records the machine's sound from.
218
186
  def sound_source = @sid
219
187
 
220
- def load_prg(data)
221
- uint16(data[0], data[1]).tap do |load_addr|
222
- ram.write(load_addr, data[2..])
223
- end
224
- end
225
-
226
188
  # A cartridge goes in with the power off.
227
189
  def attach_cartridge(cartridge)
228
190
  connect_cartridge(cartridge)
@@ -255,19 +217,11 @@ module Badline
255
217
  reset_z80
256
218
  end
257
219
 
258
- # CHROUT is at $FFD2 in both KERNALs, and the trap follows the mode.
259
- def capture_output
260
- @capture_output ||= ChroutTrap.new(cpu:, bus: @bus, layout: trap_layout).tap do |trap|
261
- cpu.install_trap(ChroutTrap::ADDRESS) { trap.call }
262
- end
263
- end
264
-
265
- def inspect
266
- "#<#{self.class.name} cycles=#{@cycles} cpu=(#{@cpu.inspect})>"
267
- end
268
-
269
220
  private
270
221
 
222
+ # The bus the KERNAL traps read and write through.
223
+ def trap_bus = @bus
224
+
271
225
  # The C128 KERNAL leaves $D02F at $FF, every extra keyboard row
272
226
  # deselected, before it jumps to the C64's reset.
273
227
  def enter_c64_mode
@@ -275,15 +229,6 @@ module Badline
275
229
  @bus.control_ports.extra_rows = 0xff
276
230
  end
277
231
 
278
- # The NMI line is wired-OR between CIA 2, the cartridge and RESTORE,
279
- # and the CPU takes an interrupt on its falling edge.
280
- def drive_nmi
281
- nmi = @cia2.interrupted? || @cartridge_nmi || @restore_pulse
282
- @restore_pulse = false
283
- @cpu.nmi = true if nmi && !@nmi_asserted
284
- @nmi_asserted = nmi
285
- end
286
-
287
232
  # BA halts the CPU on a read cycle.
288
233
  def clock_cpu
289
234
  @cpu.pending_write? || !@vic.ba_low? ? @cpu.cycle! : @cpu.stall!
@@ -311,9 +256,5 @@ module Badline
311
256
  @cpu.cycle!
312
257
  @vic.take_cpu_bus(phi1, @bus.data, @bus.vic_access?)
313
258
  end
314
-
315
- def handle_init
316
- @init_handlers.each(&:call)
317
- end
318
259
  end
319
260
  end
@@ -2,9 +2,9 @@
2
2
 
3
3
  module Badline
4
4
  class Computer
5
- # What plugs into the machine besides its chips, the cartridge, the
6
- # disk device 8 serves through the traps, its true drives (TrueDrives)
7
- # and an REU, and their state for a snapshot.
5
+ # What plugs into the machine besides its chips, the cartridge, its
6
+ # true drives (TrueDrives) and an REU, and their state for a snapshot.
7
+ # The disk device 8 serves through the traps is KernalTraps'.
8
8
  module Attachments
9
9
  include TrueDrives
10
10
 
@@ -17,30 +17,6 @@ module Badline
17
17
  address_bus.attach_cartridge(cartridge)
18
18
  end
19
19
 
20
- # Puts the storage in device 8. Mounting again swaps the disk at any
21
- # point while the machine runs: the drive keeps its RAM, which only a
22
- # drive reset clears, and its status. The traps go on the KERNAL the
23
- # machine runs (#trap_layout).
24
- def mount(storage)
25
- return @drive.insert(storage) if @drive
26
-
27
- @drive = KernalTrap::Drive.new(storage)
28
- install_kernal_traps(trap_layout)
29
- end
30
-
31
- # Takes device 8's mounted storage out, and with it the LOAD, SAVE and
32
- # serial traps, so the KERNAL's routines go out over the serial bus.
33
- # Mounting again starts a new drive, with its RAM cleared.
34
- def unmount
35
- return unless @drive
36
-
37
- remove_kernal_traps
38
- @drive = nil
39
- end
40
-
41
- # Whether device 8 serves a disk or directory through the traps.
42
- def mounted? = !@drive.nil?
43
-
44
20
  # The serial bus, which CIA 2's port A drives through the C64's own
45
21
  # inverters and reads back on PA6 and PA7, with or without a drive on
46
22
  # it.
@@ -78,26 +54,21 @@ module Badline
78
54
  end
79
55
  end
80
56
 
81
- # The disk device 8 serves through the traps, with the traps' own
82
- # state: open channels, the drive's status and RAM, a SAVE under way.
83
- def save_trap_drive(out)
84
- out.boolean(!@drive.nil?)
85
- return unless @drive
86
-
87
- @drive.save_state(out)
88
- @serial_trap.save_state(out)
89
- @save_trap.save_state(out)
57
+ # A true 1541 on the serial bus, then a 1581. The leading flag is the
58
+ # bus itself, which every machine now has.
59
+ def save_serial_bus(out)
60
+ out.boolean(true).boolean(!@drive1541.nil?)
61
+ if @drive1541
62
+ out.int(@drive1541.device)
63
+ @drive1541.save_state(out)
64
+ end
65
+ save_drive1581(out)
90
66
  end
91
67
 
92
- def load_trap_drive(input)
93
- return unmount unless input.boolean?
94
-
95
- storage = Storage.reopen(input)
96
- unmount
97
- mount(storage)
98
- @drive.load_state(input)
99
- @serial_trap.load_state(input)
100
- @save_trap.load_state(input)
68
+ def load_serial_bus(input)
69
+ input.boolean?
70
+ load_drive1541(input)
71
+ load_drive1581(input)
101
72
  end
102
73
 
103
74
  # The REU, which the setup says the machine has, and whether it has
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Computer
5
+ # What the C128 does as the C64 does: the NMI line, a PRG put into RAM
6
+ # and the region's timing. Computer and C128 each include it.
7
+ module Common
8
+ # The clock, the raster and the crop of the machine's region, a pixel
9
+ # to a window pixel.
10
+ def timing
11
+ region = address_bus.region
12
+ Timing.new(clock_hz: region.clock_hz, cycles_per_line: region.cycles_per_line,
13
+ lines_per_frame: region.lines_per_frame, crop: region.crop, pixel_width: 1)
14
+ end
15
+
16
+ def load_prg(data)
17
+ uint16(data[0], data[1]).tap do |load_addr|
18
+ ram.write(load_addr, data[2..])
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ # The NMI line is wired-OR between CIA 2, the cartridge and the RESTORE
25
+ # key, and the CPU takes an interrupt on its falling edge.
26
+ def drive_nmi
27
+ nmi = @cia2.interrupted? || @cartridge_nmi || @restore_pulse
28
+ @restore_pulse = false
29
+ @cpu.nmi = true if nmi && !@nmi_asserted
30
+ @nmi_asserted = nmi
31
+ end
32
+ end
33
+ end
34
+ end
@@ -2,18 +2,56 @@
2
2
 
3
3
  module Badline
4
4
  class Computer
5
- # The LOAD, SAVE and serial traps on the KERNAL a KernalTrap::Layout
6
- # describes, which serve the disk in device 8 (Attachments#mount).
5
+ # The disk device 8 serves through the LOAD, SAVE and serial traps, and
6
+ # CHROUT capture, on the KERNAL the machine runs. Computer, Vic20 and
7
+ # C128 each include it and answer two private methods: #trap_layout,
8
+ # the KernalTrap::Layout of that KERNAL, and #trap_bus, the bus the
9
+ # traps read and write through.
7
10
  module KernalTraps
11
+ # Puts the storage in device 8. Mounting again swaps the disk at any
12
+ # point while the machine runs: the drive keeps its RAM, which only a
13
+ # drive reset clears, and its status. The traps go on the KERNAL the
14
+ # machine runs (#trap_layout).
15
+ def mount(storage)
16
+ return @drive.insert(storage) if @drive
17
+
18
+ @drive = KernalTrap::Drive.new(storage)
19
+ install_kernal_traps(trap_layout)
20
+ end
21
+
22
+ # Takes device 8's mounted storage out, and with it the LOAD, SAVE and
23
+ # serial traps, so the KERNAL's routines go out over the serial bus.
24
+ # Mounting again starts a new drive, with its RAM cleared.
25
+ def unmount
26
+ return unless @drive
27
+
28
+ remove_kernal_traps
29
+ @drive = nil
30
+ end
31
+
32
+ # Whether device 8 serves a disk or directory through the traps.
33
+ def mounted? = !@drive.nil?
34
+
35
+ # The path of the disk or directory device 8 serves through the
36
+ # traps, or an empty one.
37
+ def mounted_path = @drive.nil? ? "" : @drive.path
38
+
39
+ # Records what the KERNAL prints through CHROUT (ChroutTrap).
40
+ def capture_output
41
+ @capture_output ||= ChroutTrap.new(cpu:, bus: trap_bus, layout: trap_layout).tap do |trap|
42
+ cpu.install_trap(ChroutTrap::ADDRESS) { trap.call }
43
+ end
44
+ end
45
+
8
46
  private
9
47
 
10
48
  def install_kernal_traps(layout)
11
49
  @trap_layout = layout
12
- load_trap = KernalTrap::Load.new(cpu:, bus: address_bus, layout:, drive: @drive)
50
+ load_trap = KernalTrap::Load.new(cpu:, bus: trap_bus, layout:, drive: @drive)
13
51
  cpu.install_trap(layout.load) { load_trap.call }
14
- @serial_trap = KernalTrap::Serial.new(cpu:, bus: address_bus, layout:, drive: @drive,
52
+ @serial_trap = KernalTrap::Serial.new(cpu:, bus: trap_bus, layout:, drive: @drive,
15
53
  device: serial_trap_device).install
16
- save_trap = @save_trap = KernalTrap::Save.new(cpu:, bus: address_bus, layout:, drive: @drive)
54
+ save_trap = @save_trap = KernalTrap::Save.new(cpu:, bus: trap_bus, layout:, drive: @drive)
17
55
  cpu.install_trap(layout.save) { save_trap.call }
18
56
  end
19
57
 
@@ -24,6 +62,28 @@ module Badline
24
62
  @serial_trap = nil
25
63
  @save_trap = nil
26
64
  end
65
+
66
+ # The disk device 8 serves through the traps, with the traps' own
67
+ # state: open channels, the drive's status and RAM, a SAVE under way.
68
+ def save_trap_drive(out)
69
+ out.boolean(!@drive.nil?)
70
+ return unless @drive
71
+
72
+ @drive.save_state(out)
73
+ @serial_trap.save_state(out)
74
+ @save_trap.save_state(out)
75
+ end
76
+
77
+ def load_trap_drive(input)
78
+ return unmount unless input.boolean?
79
+
80
+ storage = Storage.reopen(input)
81
+ unmount
82
+ mount(storage)
83
+ @drive.load_state(input)
84
+ @serial_trap.load_state(input)
85
+ @save_trap.load_state(input)
86
+ end
27
87
  end
28
88
  end
29
89
  end