badline 0.4.1 → 0.5.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 (70) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +35 -0
  3. data/CLAUDE.md +10 -5
  4. data/README.md +59 -24
  5. data/doc/pinned-behaviour.md +221 -0
  6. data/exe/badline-ruby +8 -4
  7. data/lib/badline/address_bus/ultimax_pages.rb +38 -0
  8. data/lib/badline/address_bus.rb +19 -29
  9. data/lib/badline/audio/cli.rb +2 -1
  10. data/lib/badline/audio/jukebox.rb +6 -4
  11. data/lib/badline/audio/machine_player.rb +10 -6
  12. data/lib/badline/cartridge/kcs_power.rb +3 -3
  13. data/lib/badline/cia.rb +5 -1
  14. data/lib/badline/computer.rb +86 -7
  15. data/lib/badline/cpu.rb +39 -0
  16. data/lib/badline/drive1541/bus.rb +134 -0
  17. data/lib/badline/drive1541/disk.rb +256 -0
  18. data/lib/badline/drive1541/disk_via.rb +33 -0
  19. data/lib/badline/drive1541/gcr.rb +57 -0
  20. data/lib/badline/drive1541/idle.rb +184 -0
  21. data/lib/badline/drive1541/mechanism.rb +277 -0
  22. data/lib/badline/drive1541/rotation.rb +111 -0
  23. data/lib/badline/drive1541/sector_reader.rb +76 -0
  24. data/lib/badline/drive1541/serial_port.rb +67 -0
  25. data/lib/badline/drive1541/track.rb +98 -0
  26. data/lib/badline/drive1541.rb +218 -0
  27. data/lib/badline/gui/application.rb +24 -7
  28. data/lib/badline/gui/drive_led_pane.rb +29 -0
  29. data/lib/badline/gui.rb +1 -0
  30. data/lib/badline/iec_bus.rb +93 -0
  31. data/lib/badline/kernal_trap/channel.rb +9 -1
  32. data/lib/badline/kernal_trap/serial.rb +9 -3
  33. data/lib/badline/media/true_drive.rb +54 -0
  34. data/lib/badline/media.rb +22 -1
  35. data/lib/badline/options.rb +30 -4
  36. data/lib/badline/region.rb +35 -10
  37. data/lib/badline/reu/dma.rb +228 -0
  38. data/lib/badline/reu/ram.rb +65 -0
  39. data/lib/badline/reu/trigger.rb +32 -0
  40. data/lib/badline/reu.rb +276 -0
  41. data/lib/badline/roms/README +10 -0
  42. data/lib/badline/roms/dos1541.rom +0 -0
  43. data/lib/badline/sdl/functions.rb +1 -0
  44. data/lib/badline/storage/d64_image.rb +18 -9
  45. data/lib/badline/storage/d81_image.rb +1 -0
  46. data/lib/badline/storage/disk_image/catalog.rb +52 -0
  47. data/lib/badline/storage/disk_image/writing.rb +16 -0
  48. data/lib/badline/storage/disk_image.rb +2 -0
  49. data/lib/badline/storage/g64_image.rb +222 -0
  50. data/lib/badline/storage/host_directory.rb +21 -1
  51. data/lib/badline/storage/listing.rb +118 -0
  52. data/lib/badline/storage/t64.rb +14 -0
  53. data/lib/badline/storage.rb +2 -0
  54. data/lib/badline/traps.rb +4 -0
  55. data/lib/badline/version.rb +1 -1
  56. data/lib/badline/via/control_lines.rb +3 -0
  57. data/lib/badline/via/fast_forward.rb +38 -0
  58. data/lib/badline/via/shift_register.rb +8 -3
  59. data/lib/badline/via/timer1.rb +30 -0
  60. data/lib/badline/via/timer2.rb +25 -0
  61. data/lib/badline/via.rb +2 -0
  62. data/lib/badline/vic/display_state.rb +7 -7
  63. data/lib/badline/vic/sprite/internal_bus.rb +11 -7
  64. data/lib/badline/vic/sprite/shifter.rb +10 -15
  65. data/lib/badline/vic/sprite/timing.rb +60 -0
  66. data/lib/badline/vic/sprite.rb +14 -12
  67. data/lib/badline/vic/sprites.rb +2 -2
  68. data/lib/badline/vic.rb +86 -39
  69. data/lib/badline.rb +3 -0
  70. metadata +26 -1
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "badline/address_bus/ultimax_pages"
4
+
3
5
  module Badline
4
6
  # Memory layout:
5
7
  #
@@ -28,6 +30,7 @@ module Badline
28
30
  # 0xE000-0xFFFF - KERNAL ROM / Cartridge ROM (high) - 8kb
29
31
  class AddressBus
30
32
  include Addressable
33
+ include UltimaxPages
31
34
 
32
35
  # I/O 1 and 2, and the Ultimax holes, with nothing on the bus. A read
33
36
  # picks up the byte the VIC fetched in the preceding phi1 half-cycle,
@@ -45,11 +48,15 @@ module Badline
45
48
  PORT_FLOATING = 0b1100_1000
46
49
  TAPE_SENSE = 0b0001_0000
47
50
 
48
- RAM_POWER_ON = [0xff, 0x07].freeze
51
+ # RAM powers on in runs of $00 $00 $FF $FF $FF $FF $00 $00, inverted in
52
+ # the second and fourth 16K: a C64C (ASSY 250469 R4) from
53
+ # C64/raminitpattern/readme.txt, without its occasional random bytes.
54
+ # See doc/pinned-behaviour.md.
55
+ RAM_POWER_ON = Array.new(2**16) { |addr| (((addr + 2) / 4) ^ (addr / 0x4000)).odd? ? 0xff : 0x00 }.freeze
49
56
 
50
57
  attr_reader :io_port, :ram, :basic_rom, :character_rom, :kernal_rom,
51
58
  :vic, :sid, :color_ram, :cia1, :cia2, :keyboard, :joystick1, :joystick2,
52
- :control_ports, :cartridge, :ultimax, :phi1_ultimax, :datasette, :region, :video_ram
59
+ :control_ports, :cartridge, :ultimax, :phi1_ultimax, :datasette, :region, :video_ram, :reu
53
60
 
54
61
  # ram_expansion fits a +60K (:plus60k) or +256K (:plus256k).
55
62
  def initialize(sid_model: :mos6581, cia_model: :mos6526, vic_model: :mos6569, region: Region::PAL,
@@ -57,7 +64,7 @@ module Badline
57
64
  @region = region
58
65
  @ram = Memory.new(RAM_POWER_ON, length: 2**16, start: 0)
59
66
  @ram_expansion = RAMExpansion.build(ram_expansion, @ram) { update_overlays! }
60
- @cartridge = nil
67
+ @cartridge = @reu = nil
61
68
  @debug_register = nil
62
69
 
63
70
  @basic_rom = ROM.load("basic.rom", 0xa000)
@@ -99,6 +106,13 @@ module Badline
99
106
  update_overlays!
100
107
  end
101
108
 
109
+ # An REU takes I/O 2 unless a cartridge claims it, and watches writes
110
+ # to $FF00 for the one that starts an armed transfer.
111
+ def attach_reu(reu)
112
+ @reu = reu
113
+ update_overlays!
114
+ end
115
+
102
116
  def power_on!
103
117
  @ram.clear!(RAM_POWER_ON)
104
118
  @ram_expansion.power_on!
@@ -177,6 +191,7 @@ module Badline
177
191
  @video_ram = @ram_expansion.video_ram
178
192
 
179
193
  @ultimax ? map_ultimax_pages : map_banked_pages
194
+ @write_pages[0xff] = @reu.trigger.wrap(@write_pages[0xff]) if @reu
180
195
  end
181
196
 
182
197
  def map_banked_pages
@@ -216,32 +231,6 @@ module Badline
216
231
  @write_pages.fill(bank, first_page, 0x20) if bank.is_a?(Cartridge::RAMBank)
217
232
  end
218
233
 
219
- # Ultimax cartridges ignore the $01 lines: 4K of RAM, ROML/ROMH windows,
220
- # I/O always visible and open address space everywhere else. The ROML
221
- # and ROMH selects fire on writes as well, so cartridge RAM or flash in
222
- # either window takes the writes there.
223
- def map_ultimax_pages
224
- @read_pages.fill(@open_bus, 0x10, 0xf0)
225
- @write_pages.fill(@open_bus, 0x10, 0xf0)
226
- @read_pages.fill(@cartridge.roml, 0x80, 0x20) if @cartridge.roml
227
- map_ultimax_writes(@cartridge.roml, 0x80)
228
- @read_pages.fill(@cartridge.romh, 0xe0, 0x20) if @cartridge.romh
229
- map_ultimax_writes(@cartridge.romh, 0xe0)
230
- map_ultimax_a000
231
- map_io_pages
232
- end
233
-
234
- def map_ultimax_writes(bank, first_page)
235
- @write_pages.fill(bank, first_page, 0x20) if bank.respond_to?(:poke)
236
- end
237
-
238
- def map_ultimax_a000
239
- return unless (window = @cartridge.ultimax_a000)
240
-
241
- @read_pages.fill(window, 0xa0, 0x20)
242
- @write_pages.fill(window, 0xa0, 0x20)
243
- end
244
-
245
234
  def map_io_pages
246
235
  {
247
236
  vic => 0xd0..0xd3, sid => 0xd4..0xd7, color_ram => 0xd8..0xdb,
@@ -251,6 +240,7 @@ module Badline
251
240
  end
252
241
  @ram_expansion.map_io(@read_pages, @write_pages)
253
242
  @read_pages[0xd7] = @write_pages[0xd7] = @debug_register if @debug_register
243
+ @read_pages[0xdf] = @write_pages[0xdf] = @reu if @reu
254
244
  map_cartridge_io if @cartridge
255
245
  end
256
246
 
@@ -111,8 +111,9 @@ module Badline
111
111
  def songlength(song) = songlengths&.at(song - 1)
112
112
 
113
113
  def songlengths
114
- return @songlengths if defined?(@songlengths)
114
+ return @songlengths if @songlengths_read
115
115
 
116
+ @songlengths_read = true
116
117
  path = @options.songlengths || Storage::SongLengths.locate(@options.tune_path)
117
118
  @songlengths = path && Storage::SongLengths.new(path).lengths(tune.md5)
118
119
  end
@@ -3,8 +3,9 @@
3
3
  module Badline
4
4
  module Audio
5
5
  # Plays a tune's songs interactively: the console's keys step between
6
- # songs, pause and quit, and the status line follows along. It stops
7
- # when a song ends, on q, or on Ctrl-C.
6
+ # songs, pause and quit, and the status line follows along. A song that
7
+ # plays to its end moves on to the next one. It stops after the last
8
+ # song, on q, or on Ctrl-C.
8
9
  #
9
10
  # `renderer` builds the renderer for a song at the sink's rate, and
10
11
  # `length` gives a song's length in seconds.
@@ -23,7 +24,7 @@ module Badline
23
24
  loop do
24
25
  @action = nil
25
26
  result = play(song)
26
- song = following(song)
27
+ song = following(song, result)
27
28
  return result unless song
28
29
  end
29
30
  end
@@ -43,10 +44,11 @@ module Badline
43
44
  end
44
45
  end
45
46
 
46
- def following(song)
47
+ def following(song, result)
47
48
  case @action
48
49
  when :next then song + 1
49
50
  when :previous then song - 1
51
+ else song + 1 if result == :finished && song < @songs
50
52
  end
51
53
  end
52
54
 
@@ -5,7 +5,9 @@ module Badline
5
5
  # Runs a tune on the whole machine, for the RSID tunes that install their
6
6
  # own interrupts and expect a booted C64 underneath. The tune goes in
7
7
  # the way `Media` puts it in: BASIC SYSes the driver stub, or RUNs a
8
- # BASIC tune. Rendering starts when the CPU gets there.
8
+ # BASIC tune. Rendering starts when the CPU gets there. The machine is
9
+ # PAL whatever the tune asks for, and the frames and the clock follow
10
+ # its region.
9
11
  class MachinePlayer
10
12
  FRAME_CYCLES = BarePlayer::FRAME_CYCLES
11
13
 
@@ -16,6 +18,8 @@ module Badline
16
18
  @tune = tune
17
19
  @song = song || tune.start_song
18
20
  @computer = Computer.new(sid_model:)
21
+ region = @computer.region
22
+ @frame_cycles = region.cycles_per_line * region.lines_per_frame
19
23
  @injected = false
20
24
  @started = false
21
25
  end
@@ -28,13 +32,13 @@ module Badline
28
32
 
29
33
  def sid = @computer.sid
30
34
 
31
- # The machine is PAL whatever the tune asks for.
32
- def clock_hz = TimeOfDay::CLOCK_HZ
35
+ def clock_hz = @computer.region.clock_hz
33
36
 
34
- # Advances one PAL frame, or `budget` cycles if that is shorter, then
35
- # yields whatever the SID recorded over it. Returns the cycles advanced.
37
+ # Advances one frame of the machine, or `budget` cycles if that is
38
+ # shorter, then yields whatever the SID recorded over it. Returns the
39
+ # cycles advanced.
36
40
  def frame(budget, &)
37
- cycles = [budget, FRAME_CYCLES].min
41
+ cycles = [budget, @frame_cycles].min
38
42
  cycles.times { @computer.cycle! }
39
43
  sid.drain_samples.each(&)
40
44
  cycles
@@ -31,7 +31,7 @@ module Badline
31
31
  elsif addr.anybits?(0x80)
32
32
  (@exrom << 7) | (@game << 6) | (open_bus(addr) & 0x3f)
33
33
  else
34
- @ram[addr & 0x7f]
34
+ @io_ram[addr & 0x7f]
35
35
  end
36
36
  end
37
37
 
@@ -39,7 +39,7 @@ module Badline
39
39
  if addr < 0xdf00
40
40
  switch(addr.anybits?(0x02) ? :ultimax : :rom16k)
41
41
  elsif addr.nobits?(0x80)
42
- @ram[addr & 0x7f] = value
42
+ @io_ram[addr & 0x7f] = value
43
43
  end
44
44
  end
45
45
 
@@ -64,7 +64,7 @@ module Badline
64
64
  roml, romh = banks_from(chips)
65
65
  @roml = roml.first || EMPTY_BANK
66
66
  @romh = romh.first || EMPTY_BANK
67
- @ram = Array.new(0x80, 0xff)
67
+ @io_ram = Array.new(0x80, 0xff)
68
68
  reset
69
69
  end
70
70
  end
data/lib/badline/cia.rb CHANGED
@@ -13,7 +13,11 @@ module Badline
13
13
  # C64C, which differs from it only in its interrupt control register.
14
14
  MODELS = %i[mos6526 mos6526a].freeze
15
15
 
16
- attr_reader :start, :control_a, :control_b, :peripheral, :serial, :model
16
+ attr_reader :start, :control_a, :control_b, :serial, :model
17
+
18
+ # What's on the ports outside the chip. It pulls port lines low through
19
+ # read_a and read_b, and on CIA 1 drives PB4 through port_b4_high?.
20
+ attr_accessor :peripheral
17
21
 
18
22
  def interrupt_status = @icr.status
19
23
 
@@ -5,7 +5,7 @@ module Badline
5
5
  include IntegerHelper
6
6
  include KeyboardBuffer
7
7
 
8
- attr_reader :address_bus, :cpu, :cycles
8
+ attr_reader :address_bus, :cpu, :cycles, :drive1541
9
9
 
10
10
  def region = address_bus.region
11
11
 
@@ -33,9 +33,12 @@ module Badline
33
33
 
34
34
  # The machine options (sid_model:, cia_model:, vic_model:, region: and
35
35
  # ram_expansion:) configure the AddressBus. The region sets the clock,
36
- # the VIC's raster and the mains frequency the CIAs' TOD clocks count.
37
- # Only PAL runs as yet.
38
- def initialize(debug: false, **machine)
36
+ # the VIC's raster and sprite timing, and the mains frequency the CIAs'
37
+ # TOD clocks count. The KERNAL tells PAL from NTSC by the raster, so
38
+ # every region boots the same ROMs. reu plugs an REU of that many K
39
+ # into the expansion port, where it drives the IRQ line and takes the
40
+ # bus for its transfers.
41
+ def initialize(debug: false, reu: nil, **machine)
39
42
  @address_bus = AddressBus.new(**machine)
40
43
  @cpu = CPU.new(@address_bus, debug:)
41
44
  @vic = @address_bus.vic
@@ -48,11 +51,17 @@ module Badline
48
51
  @nmi_asserted = false
49
52
  @cartridge_nmi = false
50
53
  @restore_pulse = false
54
+ @reu_irq = false
55
+ @dma = false
51
56
  @freezing = false
52
57
  @freeze_writes = 0
53
58
  @init_handlers = []
54
59
  @pending_keys = nil
55
60
  @drive = nil
61
+ @serial_trap = nil
62
+ @drive1541 = nil
63
+ @iec_bus = nil
64
+ @reu = reu ? plug_reu(reu) : nil
56
65
  end
57
66
 
58
67
  INIT_THRESHOLD = 2_500_000
@@ -69,11 +78,12 @@ module Badline
69
78
  @sid.cycle!
70
79
  @datasette.cycle!
71
80
 
72
- @cpu.irq = @cia1.interrupted? || @vic.interrupted?
81
+ @cpu.irq = @cia1.interrupted? || @vic.interrupted? || @reu_irq
73
82
 
74
83
  drive_nmi
75
84
  watch_freeze if @freezing
76
- @cpu.pending_write? || !@vic.ba_low? ? @cpu.cycle! : @cpu.stall!
85
+ clock_cpu
86
+ @drive1541&.host_cycle!
77
87
 
78
88
  @cycles += 1
79
89
  end
@@ -91,6 +101,8 @@ module Badline
91
101
  power_cycle!
92
102
  end
93
103
 
104
+ def reu = address_bus.reu
105
+
94
106
  # A cartridge goes in with the power off, so attaching one switches the
95
107
  # machine off and on: the VIC and RAM start from their power-on state,
96
108
  # and the RES line resets everything else.
@@ -109,7 +121,10 @@ module Badline
109
121
  @cia2.reset!
110
122
  @sid.reset!
111
123
  address_bus.cartridge&.reset
124
+ @reu&.reset!
125
+ @dma = false
112
126
  @drive&.reset!
127
+ @drive1541&.reset!
113
128
  @freezing = false
114
129
  @nmi_asserted = false
115
130
  cpu.reset!
@@ -145,11 +160,44 @@ module Badline
145
160
  @drive = KernalTrap::Drive.new(storage)
146
161
  load_trap = KernalTrap::Load.new(cpu:, bus: address_bus, drive: @drive)
147
162
  cpu.install_trap(KernalTrap::Load::ADDRESS) { load_trap.call }
148
- KernalTrap::Serial.new(cpu:, bus: address_bus, drive: @drive).install
163
+ @serial_trap = KernalTrap::Serial.new(cpu:, bus: address_bus, drive: @drive, device: serial_trap_device).install
149
164
  save_trap = KernalTrap::Save.new(cpu:, bus: address_bus, drive: @drive)
150
165
  cpu.install_trap(KernalTrap::Save::ADDRESS) { save_trap.call }
151
166
  end
152
167
 
168
+ # Takes device 8's mounted storage out, and with it the LOAD, SAVE and
169
+ # serial traps, so the KERNAL's routines go out over the serial bus.
170
+ # Mounting again starts a new drive, with its RAM cleared.
171
+ def unmount
172
+ return unless @drive
173
+
174
+ cpu.remove_trap(KernalTrap::Load::ADDRESS)
175
+ cpu.remove_trap(KernalTrap::Save::ADDRESS)
176
+ @serial_trap.device = nil
177
+ @serial_trap = nil
178
+ @drive = nil
179
+ end
180
+
181
+ # Plugs in a Drive1541, which then runs alongside the C64 on its own
182
+ # clock and talks to it over the serial bus. The serial traps stop
183
+ # answering the drive's device number, so the KERNAL's TALK, LISTEN and
184
+ # byte transfers reach the drive. The LOAD and SAVE traps stay, and
185
+ # still serve a mounted image.
186
+ def attach_drive1541(drive)
187
+ @iec_bus.detach(@drive1541) if @iec_bus && @drive1541
188
+ drive.host_clock_hz = region.clock_hz
189
+ @drive1541 = drive
190
+ drive.connect(iec_bus)
191
+ @serial_trap&.device = serial_trap_device
192
+ end
193
+
194
+ # The serial bus. CIA 2's port A joins it the first time it's asked
195
+ # for, which plugging in a drive does. Until then port A's serial
196
+ # inputs float high.
197
+ def iec_bus
198
+ @iec_bus ||= IECBus.new(host: @cia2).tap { |bus| @cia2.peripheral = bus }
199
+ end
200
+
153
201
  def capture_output
154
202
  @capture_output ||= ChroutTrap.new(cpu:, bus: address_bus).tap do |trap|
155
203
  cpu.install_trap(ChroutTrap::ADDRESS) { trap.call }
@@ -170,6 +218,12 @@ module Badline
170
218
 
171
219
  private
172
220
 
221
+ # The device number the serial traps answer: device 8, unless a true
222
+ # drive is on the bus as device 8.
223
+ def serial_trap_device
224
+ @drive1541&.device == KernalTrap::Routine::DEVICE ? nil : KernalTrap::Routine::DEVICE
225
+ end
226
+
173
227
  # The NMI line is wired-OR between CIA 2, the cartridge and the RESTORE
174
228
  # key, and the CPU takes an interrupt on its falling edge.
175
229
  def drive_nmi
@@ -179,6 +233,31 @@ module Badline
179
233
  @nmi_asserted = nmi
180
234
  end
181
235
 
236
+ def plug_reu(size_kb)
237
+ reu = REU.new(size_kb, bus: @address_bus, vic: @vic)
238
+ reu.on_irq_change { |level| @reu_irq = level }
239
+ reu.on_dma { @dma = true }
240
+ @address_bus.attach_reu(reu)
241
+ reu
242
+ end
243
+
244
+ # BA halts the CPU on a read cycle, and so does an REU holding the bus
245
+ # for a transfer.
246
+ def clock_cpu
247
+ return dma_cycle! if @dma
248
+
249
+ @cpu.pending_write? || !@vic.ba_low? ? @cpu.cycle! : @cpu.stall!
250
+ end
251
+
252
+ def dma_cycle!
253
+ writing = @cpu.pending_write?
254
+ @reu.dma_cycle!(@vic.reu_ba_low?, writing)
255
+ return @cpu.stall! if @reu.holds_bus?
256
+
257
+ @dma = @reu.dma?
258
+ writing || !@vic.ba_low? ? @cpu.cycle! : @cpu.stall!
259
+ end
260
+
182
261
  # A freezer counts the CPU's write cycles once it pulls NMI and switches
183
262
  # to Ultimax on the third in a row, the last push of the interrupt
184
263
  # sequence, so the vector fetch that follows reads the cartridge.
data/lib/badline/cpu.rb CHANGED
@@ -40,6 +40,7 @@ module Badline
40
40
  @brk = false
41
41
  @pending_write = false
42
42
  @stalled_at = nil
43
+ @so_high = true
43
44
 
44
45
  @cycles = 0
45
46
  @instructions = 0
@@ -88,6 +89,20 @@ module Badline
88
89
  sample_while_stalled
89
90
  end
90
91
 
92
+ # The 6502's SO (set overflow) pin, which the 6510 leaves out, so the
93
+ # C64 never drives it. A falling edge sets V and a held level does
94
+ # nothing more.
95
+ def so=(high)
96
+ @status.overflow = true if @so_high && !high
97
+ @so_high = high
98
+ end
99
+
100
+ # Pulses SO: pulls it low, which sets V, and releases it.
101
+ def so!
102
+ @status.overflow = true
103
+ @so_high = true
104
+ end
105
+
91
106
  def jammed?
92
107
  @plan.equal?(JAMMED_PLAN)
93
108
  end
@@ -99,6 +114,30 @@ module Badline
99
114
  nil
100
115
  end
101
116
 
117
+ # Whether the next cycle fetches an opcode.
118
+ def boundary? = @index.zero? && @plan.equal?(FETCH_PLAN)
119
+
120
+ def trapped? = !@traps.nil?
121
+
122
+ # Everything the CPU holds but its cycle and instruction counts: the
123
+ # registers, the interrupt lines and pipeline, and what the last
124
+ # instruction left in its working registers. Drive1541::Idle compares
125
+ # it at two instruction boundaries.
126
+ def idle_state
127
+ [@program_counter, @stack_pointer, @a, @x, @y, @status.value,
128
+ @irq, @nmi, @irq_sample, @irq_pending, @nmi_sample, @nmi_pending, @skip_poll,
129
+ @boundary_irq, @boundary_nmi, @so_high, @interrupt, @brk, @pending_write, @stalled_at, @traps,
130
+ @operation, @optional_dummy, @boundary_crossed, @branch_taken,
131
+ @address, @pointer, @dummy_address, @value, @rmw_result]
132
+ end
133
+
134
+ # Counts +cycles+ cycles and +instructions+ instructions that ran
135
+ # without changing anything idle_state holds.
136
+ def fast_forward(cycles, instructions)
137
+ @cycles += cycles
138
+ @instructions += instructions
139
+ end
140
+
102
141
  def inspect
103
142
  "Cycles: #{@cycles}, PC: #{format16(program_counter)}, " \
104
143
  "SP: #{format8(stack_pointer)}, A: #{format8(a)}, X: #{format8(x)}, " \
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Drive1541
5
+ # The drive CPU's bus. The decoder looks at A15, A12, A11 and A10 only:
6
+ #
7
+ # $0000-$07FF - 2 KB of RAM
8
+ # $0800-$17FF - nothing: open bus
9
+ # $1800-$1BFF - VIA 1, its sixteen registers mirrored
10
+ # $1C00-$1FFF - VIA 2, its sixteen registers mirrored
11
+ # $2000-$7FFF - $0000-$1FFF again, since A13 and A14 aren't decoded
12
+ # $8000-$BFFF - the ROM again, since A14 isn't decoded
13
+ # $C000-$FFFF - the DOS ROM
14
+ #
15
+ # Writes to the ROM and to open bus go nowhere. A read from open bus
16
+ # finds the last byte on the data bus, which for LDA abs or LDA (zp),Y
17
+ # is the address's high byte, and for an indexed read that crosses a
18
+ # page is what its dummy read found. VICE-testprogs drive/openbus pins
19
+ # both on a real 1541.
20
+ #
21
+ # While Idle records a pass of the DOS's idle loop, the bus watches it
22
+ # (see watch!).
23
+ class Bus
24
+ include Addressable
25
+
26
+ # VIA registers, as bits by offset, whose access makes a watched
27
+ # stretch volatile: reads of either VIA's counters and shift register
28
+ # and of VIA 1's port B, and writes to VIA 2's timers, shift register
29
+ # and ACR. Any write to VIA 1 does too (watch_write).
30
+ VOLATILE_VIA1_READS = 0x0731
31
+ VOLATILE_VIA2_READS = 0x0730
32
+ VOLATILE_VIA2_WRITES = 0x0ff0
33
+
34
+ attr_reader :ram, :rom, :via1, :via2, :data
35
+
36
+ # Whether a watched stretch did something that makes it unrepeatable.
37
+ attr_reader :volatile
38
+
39
+ # The RAM a watched stretch read or wrote, as each address's value
40
+ # from before the first access.
41
+ attr_reader :touched
42
+
43
+ def initialize(rom:, via1:, via2:)
44
+ addressable_at(0, length: 2**16)
45
+ @ram = Memory.new([], length: 0x0800, start: 0)
46
+ @rom = rom
47
+ @via1 = via1
48
+ @via2 = via2
49
+ @data = 0
50
+ @watching = false
51
+ @volatile = false
52
+ @touched = {}
53
+ end
54
+
55
+ # Starts watching: see volatile and touched. The bus stops watching
56
+ # once the stretch turns volatile.
57
+ def watch!
58
+ @watching = true
59
+ @volatile = false
60
+ @touched = {}
61
+ end
62
+
63
+ def unwatch!
64
+ @watching = false
65
+ end
66
+
67
+ def peek(addr)
68
+ return @data = @rom.peek(addr | 0x4000) if addr >= 0x8000
69
+
70
+ addr &= 0x1fff
71
+ watch_read(addr) if @watching
72
+ @data = if addr < 0x0800
73
+ @ram.peek(addr)
74
+ elsif addr < 0x1800
75
+ @data
76
+ elsif addr < 0x1c00
77
+ @via1.peek(addr)
78
+ else
79
+ @via2.peek(addr)
80
+ end
81
+ end
82
+
83
+ def poke(addr, value)
84
+ @data = value
85
+ return if addr >= 0x8000
86
+
87
+ addr &= 0x1fff
88
+ if addr < 0x0800
89
+ touch(addr) if @watching
90
+ @ram.poke(addr, value)
91
+ elsif addr >= 0x1c00
92
+ @via2.poke(addr, value)
93
+ elsif addr >= 0x1800
94
+ @via1.poke(addr, value)
95
+ end
96
+ watch_write(addr) if @watching
97
+ end
98
+
99
+ private
100
+
101
+ def touch(addr)
102
+ @touched[addr] = @ram.peek(addr) unless @touched.key?(addr)
103
+ end
104
+
105
+ def watch_read(addr)
106
+ if addr < 0x0800
107
+ touch(addr)
108
+ elsif addr >= 0x1800
109
+ watch(addr < 0x1c00 ? VOLATILE_VIA1_READS : VOLATILE_VIA2_READS, addr)
110
+ end
111
+ end
112
+
113
+ # Any write to VIA 1 makes the stretch volatile, and so does a write
114
+ # to VIA 2 that leaves the motor on.
115
+ def watch_write(addr)
116
+ if addr >= 0x1c00
117
+ watch(VOLATILE_VIA2_WRITES, addr)
118
+ volatile! if @watching && @via2.port_b_output.anybits?(Mechanism::MOTOR)
119
+ elsif addr >= 0x1800
120
+ volatile!
121
+ end
122
+ end
123
+
124
+ def watch(volatile, addr)
125
+ volatile! if volatile.anybits?(1 << (addr & 0x0f))
126
+ end
127
+
128
+ def volatile!
129
+ @volatile = true
130
+ @watching = false
131
+ end
132
+ end
133
+ end
134
+ end