badline 0.4.2 → 0.5.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 (73) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +37 -0
  3. data/CLAUDE.md +32 -27
  4. data/CONTRIBUTING.md +5 -0
  5. data/README.md +55 -24
  6. data/doc/pinned-behaviour.md +223 -0
  7. data/exe/badline-ruby +7 -4
  8. data/lib/badline/address_bus/ultimax_pages.rb +38 -0
  9. data/lib/badline/address_bus.rb +19 -29
  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 +21 -3
  14. data/lib/badline/computer.rb +86 -7
  15. data/lib/badline/cpu.rb +39 -0
  16. data/lib/badline/drive1541/bus.rb +197 -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 +136 -0
  21. data/lib/badline/drive1541/mechanism.rb +277 -0
  22. data/lib/badline/drive1541/orbit.rb +81 -0
  23. data/lib/badline/drive1541/rotation.rb +111 -0
  24. data/lib/badline/drive1541/sector_reader.rb +76 -0
  25. data/lib/badline/drive1541/serial_port.rb +67 -0
  26. data/lib/badline/drive1541/sleep.rb +114 -0
  27. data/lib/badline/drive1541/track.rb +98 -0
  28. data/lib/badline/drive1541.rb +234 -0
  29. data/lib/badline/gui/application.rb +15 -3
  30. data/lib/badline/gui/drive_led_pane.rb +29 -0
  31. data/lib/badline/gui.rb +1 -0
  32. data/lib/badline/iec_bus.rb +109 -0
  33. data/lib/badline/kernal_trap/channel.rb +9 -1
  34. data/lib/badline/kernal_trap/serial.rb +9 -3
  35. data/lib/badline/media/true_drive.rb +54 -0
  36. data/lib/badline/media.rb +22 -1
  37. data/lib/badline/memory.rb +3 -0
  38. data/lib/badline/options.rb +24 -4
  39. data/lib/badline/region.rb +35 -10
  40. data/lib/badline/reu/dma.rb +228 -0
  41. data/lib/badline/reu/ram.rb +65 -0
  42. data/lib/badline/reu/trigger.rb +32 -0
  43. data/lib/badline/reu.rb +276 -0
  44. data/lib/badline/roms/README +10 -0
  45. data/lib/badline/roms/dos1541.rom +0 -0
  46. data/lib/badline/sdl/functions.rb +1 -0
  47. data/lib/badline/storage/d64_image.rb +18 -9
  48. data/lib/badline/storage/d81_image.rb +1 -0
  49. data/lib/badline/storage/disk_image/catalog.rb +52 -0
  50. data/lib/badline/storage/disk_image/writing.rb +16 -0
  51. data/lib/badline/storage/disk_image.rb +2 -0
  52. data/lib/badline/storage/g64_image.rb +222 -0
  53. data/lib/badline/storage/host_directory.rb +21 -1
  54. data/lib/badline/storage/listing.rb +118 -0
  55. data/lib/badline/storage/t64.rb +14 -0
  56. data/lib/badline/storage.rb +2 -0
  57. data/lib/badline/traps.rb +4 -0
  58. data/lib/badline/version.rb +1 -1
  59. data/lib/badline/via/control_lines.rb +3 -0
  60. data/lib/badline/via/fast_forward.rb +53 -0
  61. data/lib/badline/via/shift_register.rb +8 -3
  62. data/lib/badline/via/timer1.rb +36 -0
  63. data/lib/badline/via/timer2.rb +31 -0
  64. data/lib/badline/via.rb +2 -0
  65. data/lib/badline/vic/display_state.rb +7 -7
  66. data/lib/badline/vic/sprite/internal_bus.rb +11 -7
  67. data/lib/badline/vic/sprite/shifter.rb +10 -15
  68. data/lib/badline/vic/sprite/timing.rb +60 -0
  69. data/lib/badline/vic/sprite.rb +14 -12
  70. data/lib/badline/vic/sprites.rb +2 -2
  71. data/lib/badline/vic.rb +86 -39
  72. data/lib/badline.rb +3 -0
  73. metadata +28 -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
 
@@ -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
 
@@ -58,6 +62,7 @@ module Badline
58
62
 
59
63
  @peripheral = peripheral
60
64
  @port_b4_handler = nil
65
+ @port_a_handler = nil
61
66
  @port_b4_high = true
62
67
  reset!
63
68
  end
@@ -83,6 +88,7 @@ module Badline
83
88
  @ta = Timer.new(@control_a)
84
89
  @tb = Timer.new(@control_b)
85
90
  @serial = Serial.new(@control_a)
91
+ @port_a_handler&.call
86
92
  end
87
93
 
88
94
  # Register a change handler on the PB4 line. On CIA 1, this feeds the
@@ -91,6 +97,13 @@ module Badline
91
97
  @port_b4_handler = handler
92
98
  end
93
99
 
100
+ # Register a handler for writes that can move port A's lines: to the
101
+ # data register or DDR, and the reset. On CIA 2, this tells the drives
102
+ # on the serial bus that the C64 may have moved a line.
103
+ def on_port_a_write(&handler)
104
+ @port_a_handler = handler
105
+ end
106
+
94
107
  # A falling edge on the FLAG pin. On CIA 1 the datasette's tape read
95
108
  # line drives it, on CIA 2 the serial bus SRQ.
96
109
  def flag! = raise_interrupt(:flag)
@@ -149,9 +162,9 @@ module Badline
149
162
 
150
163
  def poke(addr, value)
151
164
  case offset_of(addr) & 0x0f
152
- when 0x00 then @data_port_a = value
165
+ when 0x00 then update_port_a { @data_port_a = value }
153
166
  when 0x01 then update_port_b { @data_port_b = value }
154
- when 0x02 then @data_dir_a = value
167
+ when 0x02 then update_port_a { @data_dir_a = value }
155
168
  when 0x03 then update_port_b { @data_dir_b = value }
156
169
  when 0x04 then @ta.write_latch_low(value)
157
170
  when 0x05 then @ta.write_latch_high(value)
@@ -170,6 +183,11 @@ module Badline
170
183
 
171
184
  private
172
185
 
186
+ def update_port_a
187
+ yield
188
+ @port_a_handler&.call
189
+ end
190
+
173
191
  def update_port_b
174
192
  yield
175
193
  @port_b4_driven_high = driven_lines(@data_port_b, @data_dir_b).anybits?(0x10)
@@ -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(&:plug_in!)
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,197 @@
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!). While Idle looks for an orbit, the bus guards the drive
23
+ # (see guard!).
24
+ class Bus
25
+ include Addressable
26
+
27
+ # VIA registers, as bits by offset, whose access makes a watched
28
+ # stretch volatile: reads of either VIA's counters and shift register
29
+ # and of VIA 1's port B, and writes to VIA 2's timers, shift register
30
+ # and ACR. Any write to VIA 1 does too (watch_write).
31
+ VOLATILE_VIA1_READS = 0x0731
32
+ VOLATILE_VIA2_READS = 0x0730
33
+ VOLATILE_VIA2_WRITES = 0x0ff0
34
+
35
+ # The timer each VIA register reaches, by offset, as bits: 1 for
36
+ # timer 1 and 2 for timer 2.
37
+ TIMER_REGISTERS = [0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 0, 0, 0, 0, 0, 0].freeze
38
+
39
+ attr_reader :ram, :rom, :via1, :via2, :data
40
+
41
+ # Whether a watched stretch did something that makes it unrepeatable.
42
+ attr_reader :volatile
43
+
44
+ # The RAM a watched stretch read or wrote, as each address's value
45
+ # from before the first access.
46
+ attr_reader :touched
47
+
48
+ # Whether the drive did something since guard! that reached outside
49
+ # it, or took something in from outside.
50
+ attr_reader :tainted
51
+
52
+ # The timers the drive read or wrote since guard!, as bits: VIA 1's
53
+ # timer 1 and timer 2 in bits 0 and 1, and VIA 2's in bits 2 and 3.
54
+ attr_reader :touched_timers
55
+
56
+ def initialize(rom:, via1:, via2:)
57
+ addressable_at(0, length: 2**16)
58
+ @ram = Memory.new([], length: 0x0800, start: 0)
59
+ @rom = rom
60
+ @via1 = via1
61
+ @via2 = via2
62
+ @data = 0
63
+ @recording = @guarding = @watching = false
64
+ @volatile = @tainted = false
65
+ @touched = {}
66
+ @touched_timers = 0
67
+ @led = 0
68
+ end
69
+
70
+ # Starts watching: see volatile and touched. The bus stops watching
71
+ # once the stretch turns volatile.
72
+ def watch!
73
+ @recording = true
74
+ @volatile = false
75
+ @touched = {}
76
+ watching!
77
+ end
78
+
79
+ def unwatch!
80
+ @recording = false
81
+ watching!
82
+ end
83
+
84
+ # Starts guarding: see tainted and touched_timers. Reading VIA 1's
85
+ # port B, which reads the serial bus, taints the drive, and so do
86
+ # any write to VIA 1, whose port B drives the serial bus, a write to
87
+ # VIA 2's shift register or ACR, and VIA 2 turning the motor on or
88
+ # changing the LED. The bus stops guarding once the drive is tainted.
89
+ def guard!
90
+ @guarding = true
91
+ @tainted = false
92
+ @touched_timers = 0
93
+ @led = @via2.port_b_output & Mechanism::LED
94
+ watching!
95
+ end
96
+
97
+ def peek(addr)
98
+ return @data = @rom.peek(addr | 0x4000) if addr >= 0x8000
99
+
100
+ addr &= 0x1fff
101
+ watch_read(addr) if @watching
102
+ @data = if addr < 0x0800
103
+ @ram.peek(addr)
104
+ elsif addr < 0x1800
105
+ @data
106
+ elsif addr < 0x1c00
107
+ @via1.peek(addr)
108
+ else
109
+ @via2.peek(addr)
110
+ end
111
+ end
112
+
113
+ def poke(addr, value)
114
+ @data = value
115
+ return if addr >= 0x8000
116
+
117
+ addr &= 0x1fff
118
+ if addr < 0x0800
119
+ touch(addr) if @recording
120
+ @ram.poke(addr, value)
121
+ elsif addr >= 0x1c00
122
+ @via2.poke(addr, value)
123
+ elsif addr >= 0x1800
124
+ @via1.poke(addr, value)
125
+ end
126
+ watch_write(addr) if @watching
127
+ end
128
+
129
+ private
130
+
131
+ def watching!
132
+ @watching = @recording || @guarding
133
+ end
134
+
135
+ def touch(addr)
136
+ @touched[addr] = @ram.peek(addr) unless @touched.key?(addr)
137
+ end
138
+
139
+ def watch_read(addr)
140
+ if addr < 0x0800
141
+ touch(addr) if @recording
142
+ elsif addr >= 0x1800
143
+ via1 = addr < 0x1c00
144
+ watch(via1 ? VOLATILE_VIA1_READS : VOLATILE_VIA2_READS, addr) if @recording
145
+ guard_read(via1, addr & 0x0f) if @guarding
146
+ end
147
+ end
148
+
149
+ def watch_write(addr)
150
+ if addr >= 0x1c00
151
+ watch_via2_write(addr) if @recording
152
+ guard_via2_write(addr & 0x0f) if @guarding
153
+ elsif addr >= 0x1800
154
+ volatile! if @recording
155
+ taint! if @guarding
156
+ end
157
+ end
158
+
159
+ # Any write to VIA 1 makes the stretch volatile (watch_write), and so
160
+ # does a write to VIA 2 that leaves the motor on.
161
+ def watch_via2_write(addr)
162
+ watch(VOLATILE_VIA2_WRITES, addr)
163
+ volatile! if @recording && @via2.port_b_output.anybits?(Mechanism::MOTOR)
164
+ end
165
+
166
+ def watch(volatile, addr)
167
+ volatile! if volatile.anybits?(1 << (addr & 0x0f))
168
+ end
169
+
170
+ def volatile!
171
+ @volatile = true
172
+ @recording = false
173
+ watching!
174
+ end
175
+
176
+ def guard_read(via1, offset)
177
+ return taint! if via1 && offset.zero?
178
+
179
+ @touched_timers |= via1 ? TIMER_REGISTERS[offset] : TIMER_REGISTERS[offset] << 2
180
+ end
181
+
182
+ def guard_via2_write(offset)
183
+ return taint! if offset.between?(0x0a, 0x0b)
184
+
185
+ @touched_timers |= TIMER_REGISTERS[offset] << 2
186
+ lines = @via2.port_b_output
187
+ taint! if lines.anybits?(Mechanism::MOTOR) || (lines & Mechanism::LED) != @led
188
+ end
189
+
190
+ def taint!
191
+ @tainted = true
192
+ @guarding = false
193
+ watching!
194
+ end
195
+ end
196
+ end
197
+ end