badline 0.11.1 → 0.12.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 (125) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -0
  3. data/CLAUDE.md +8 -7
  4. data/CONTRIBUTING.md +7 -6
  5. data/README.md +12 -5
  6. data/doc/media.md +28 -1
  7. data/doc/pinned-behaviour.md +31 -1
  8. data/exe/badline-ruby +1 -1
  9. data/lib/badline/c128/bus.rb +4 -17
  10. data/lib/badline/c128/drives.rb +35 -22
  11. data/lib/badline/c128/fast_serial.rb +1 -0
  12. data/lib/badline/c128/io_pages.rb +35 -0
  13. data/lib/badline/c128/mmu.rb +9 -7
  14. data/lib/badline/c128/modes.rb +17 -4
  15. data/lib/badline/c128/saved_state.rb +22 -0
  16. data/lib/badline/c128/z80.rb +49 -0
  17. data/lib/badline/c128/z80_pages.rb +118 -0
  18. data/lib/badline/c128/z80_turns.rb +106 -0
  19. data/lib/badline/c128.rb +16 -19
  20. data/lib/badline/cartridge/action_replay.rb +2 -2
  21. data/lib/badline/cartridge/comal80.rb +1 -7
  22. data/lib/badline/cartridge/dinamic.rb +0 -8
  23. data/lib/badline/cartridge/final_cartridge3.rb +6 -6
  24. data/lib/badline/cartridge/fun_play.rb +0 -8
  25. data/lib/badline/cartridge/game_system.rb +0 -8
  26. data/lib/badline/cartridge/kcs_power.rb +10 -7
  27. data/lib/badline/cartridge/mach5.rb +2 -4
  28. data/lib/badline/cartridge/magic_desk.rb +0 -8
  29. data/lib/badline/cartridge/ocean.rb +0 -8
  30. data/lib/badline/cartridge/pagefox.rb +2 -2
  31. data/lib/badline/cartridge/retro_replay.rb +7 -6
  32. data/lib/badline/cartridge/rex_utility.rb +2 -6
  33. data/lib/badline/cartridge/rgcd.rb +2 -2
  34. data/lib/badline/cartridge/simons_basic.rb +0 -5
  35. data/lib/badline/cartridge/super_games.rb +4 -2
  36. data/lib/badline/cartridge/westermann.rb +2 -6
  37. data/lib/badline/cartridge.rb +37 -9
  38. data/lib/badline/cia/fast_forward.rb +21 -3
  39. data/lib/badline/cia.rb +8 -5
  40. data/lib/badline/computer/attachments.rb +4 -56
  41. data/lib/badline/computer/true_drives.rb +80 -0
  42. data/lib/badline/computer.rb +4 -0
  43. data/lib/badline/core.rb +2 -0
  44. data/lib/badline/drive/core.rb +8 -33
  45. data/lib/badline/drive/fast_serial.rb +73 -0
  46. data/lib/badline/drive/idle.rb +1 -11
  47. data/lib/badline/drive/orbit.rb +2 -5
  48. data/lib/badline/drive/sleep.rb +0 -2
  49. data/lib/badline/drive/vias.rb +84 -0
  50. data/lib/badline/drive.rb +2 -0
  51. data/lib/badline/drive1541.rb +1 -0
  52. data/lib/badline/drive1571/wd1770.rb +94 -8
  53. data/lib/badline/drive1571.rb +8 -58
  54. data/lib/badline/drive1581/bus.rb +137 -0
  55. data/lib/badline/drive1581/cpu.rb +11 -0
  56. data/lib/badline/drive1581/disk.rb +135 -0
  57. data/lib/badline/drive1581/disk_state.rb +87 -0
  58. data/lib/badline/drive1581/mechanism.rb +117 -0
  59. data/lib/badline/drive1581/serial_port.rb +52 -0
  60. data/lib/badline/drive1581/slot.rb +78 -0
  61. data/lib/badline/drive1581/track.rb +100 -0
  62. data/lib/badline/drive1581/track_bytes.rb +75 -0
  63. data/lib/badline/drive1581/wd1772/registers.rb +89 -0
  64. data/lib/badline/drive1581/wd1772/sector_writes.rb +73 -0
  65. data/lib/badline/drive1581/wd1772/sectors.rb +150 -0
  66. data/lib/badline/drive1581/wd1772/state.rb +66 -0
  67. data/lib/badline/drive1581/wd1772/stepping.rb +109 -0
  68. data/lib/badline/drive1581/wd1772/tracks.rb +125 -0
  69. data/lib/badline/drive1581/wd1772/turn.rb +73 -0
  70. data/lib/badline/drive1581/wd1772.rb +216 -0
  71. data/lib/badline/drive1581.rb +233 -0
  72. data/lib/badline/frontend/app.rb +1 -3
  73. data/lib/badline/frontend/audio_device.rb +67 -0
  74. data/lib/badline/frontend/audio_sink.rb +16 -50
  75. data/lib/badline/frontend/boot.rb +18 -10
  76. data/lib/badline/frontend/buttons.rb +7 -7
  77. data/lib/badline/frontend/keys.rb +33 -8
  78. data/lib/badline/frontend/media_slots.rb +42 -0
  79. data/lib/badline/frontend/{confirmation.rb → menu/confirmation.rb} +4 -4
  80. data/lib/badline/frontend/{file_browser.rb → menu/file_browser.rb} +18 -34
  81. data/lib/badline/frontend/{menu_dialogs.rb → menu/menu_dialogs.rb} +3 -7
  82. data/lib/badline/frontend/{menu_media.rb → menu/menu_media.rb} +26 -51
  83. data/lib/badline/frontend/{menu_pages.rb → menu/menu_pages.rb} +5 -8
  84. data/lib/badline/frontend/{name_field.rb → menu/name_field.rb} +11 -24
  85. data/lib/badline/frontend/{pause_menu.rb → menu/pause_menu.rb} +13 -28
  86. data/lib/badline/frontend/menu/theme.rb +18 -0
  87. data/lib/badline/frontend/{filter_view.rb → player/filter_view.rb} +4 -4
  88. data/lib/badline/frontend/{info_view.rb → player/info_view.rb} +3 -3
  89. data/lib/badline/frontend/{player_window.rb → player/player_controller.rb} +17 -25
  90. data/lib/badline/frontend/{player_footer.rb → player/player_footer.rb} +8 -8
  91. data/lib/badline/frontend/{player_header.rb → player/player_header.rb} +5 -5
  92. data/lib/badline/frontend/{player_screen.rb → player/player_screen.rb} +4 -5
  93. data/lib/badline/frontend/{scope.rb → player/scope.rb} +1 -1
  94. data/lib/badline/frontend/{sid_view.rb → player/sid_view.rb} +16 -20
  95. data/lib/badline/frontend/player/theme.rb +18 -0
  96. data/lib/badline/frontend/{visualizer_view.rb → player/visualizer_view.rb} +8 -8
  97. data/lib/badline/frontend/sound.rb +19 -38
  98. data/lib/badline/frontend/timeline.rb +3 -14
  99. data/lib/badline/frontend.rb +24 -20
  100. data/lib/badline/iec_bus.rb +14 -2
  101. data/lib/badline/machine.rb +5 -4
  102. data/lib/badline/media/boot_disk.rb +50 -0
  103. data/lib/badline/media/not_disk.rb +8 -0
  104. data/lib/badline/media/true_drive.rb +35 -15
  105. data/lib/badline/media.rb +9 -3
  106. data/lib/badline/options/event.rb +2 -2
  107. data/lib/badline/options/family.rb +3 -9
  108. data/lib/badline/options/help.rb +2 -1
  109. data/lib/badline/roms/README +9 -0
  110. data/lib/badline/roms/dos1581.rom +0 -0
  111. data/lib/badline/sdl.rb +37 -20
  112. data/lib/badline/snapshot/state.rb +5 -4
  113. data/lib/badline/storage/p00.rb +8 -0
  114. data/lib/badline/time_of_day.rb +3 -3
  115. data/lib/badline/version.rb +1 -1
  116. data/lib/badline/vic20/attachments.rb +21 -7
  117. data/lib/badline/vic20/saved_state.rb +2 -0
  118. data/lib/badline/vic20.rb +3 -0
  119. data/lib/badline/z80/core.rb +2 -0
  120. data/lib/badline/z80/saved_state.rb +69 -0
  121. metadata +54 -21
  122. /data/lib/badline/frontend/{menu_ports.rb → menu/menu_ports.rb} +0 -0
  123. /data/lib/badline/frontend/{player_commands.rb → player/player_commands.rb} +0 -0
  124. /data/lib/badline/frontend/{player_state.rb → player/player_state.rb} +0 -0
  125. /data/lib/badline/frontend/{sid_history.rb → player/sid_history.rb} +0 -0
@@ -63,11 +63,7 @@ module Badline
63
63
  end
64
64
 
65
65
  def from_crt(crt, **)
66
- mapper = HARDWARE_TYPES.fetch(crt.hardware_type) do
67
- raise UnsupportedTypeError,
68
- "Unsupported cartridge hardware type #{crt.hardware_type}"
69
- end
70
- mapper.new(crt, **)
66
+ mapper_for(crt).new(crt, **)
71
67
  end
72
68
 
73
69
  # A cartridge built afresh from what save_setup wrote, before
@@ -76,9 +72,21 @@ module Badline
76
72
  return GeoRAM.new(size: input.int) if input.int == GEO_RAM_SETUP
77
73
 
78
74
  crt = Storage::CRTFile.new(bytes: input.string)
79
- return RetroReplay.new(crt, flash_jumper: input.boolean?, bank_jumper: input.boolean?) if input.boolean?
75
+ mapper_for(crt).from_jumpers(crt, input)
76
+ end
80
77
 
81
- from_crt(crt)
78
+ # A cartridge from its image and what save_jumpers wrote: whether
79
+ # there are jumpers, then their settings on a mapper that has them.
80
+ def from_jumpers(crt, input)
81
+ input.boolean?
82
+ new(crt)
83
+ end
84
+
85
+ def mapper_for(crt)
86
+ HARDWARE_TYPES.fetch(crt.hardware_type) do
87
+ raise UnsupportedTypeError,
88
+ "Unsupported cartridge hardware type #{crt.hardware_type}"
89
+ end
82
90
  end
83
91
  end
84
92
 
@@ -95,6 +103,7 @@ module Badline
95
103
  @exrom = crt.exrom
96
104
  @game = crt.game
97
105
  @roml = @romh = nil
106
+ @banks = nil
98
107
  @on_change = nil
99
108
  @on_nmi_change = nil
100
109
  @nmi = false
@@ -193,9 +202,22 @@ module Badline
193
202
  out.boolean(false)
194
203
  end
195
204
 
196
- def save_mapper(_out); end
205
+ # The mapper's own state: where its windows point, for a mapper that
206
+ # moves them, and whatever a mapper adds after calling super.
207
+ def save_mapper(out)
208
+ save_windows(out, windows) if windows
209
+ end
197
210
 
198
- def load_mapper(_input); end
211
+ def load_mapper(input)
212
+ load_windows(input, windows) if windows
213
+ end
214
+
215
+ # The banks and views ROML and ROMH can point at, which a snapshot
216
+ # names them by: the mapper's banks, or nothing for a mapper whose
217
+ # windows stay where install_chips put them.
218
+ def windows
219
+ [*@banks, EMPTY_BANK] if @banks
220
+ end
199
221
 
200
222
  # Where the ROML and ROMH windows point, as their places in `windows`,
201
223
  # the banks and views the mapper picks them from, or -1 for none.
@@ -232,6 +254,12 @@ module Badline
232
254
  @exrom, @game = MODES.fetch(mode)
233
255
  end
234
256
 
257
+ # Sets the lines and tells the machine to map the cartridge again.
258
+ def select_mode(mode)
259
+ self.mode = mode
260
+ changed!
261
+ end
262
+
235
263
  def open_bus(addr)
236
264
  @open_bus ? @open_bus.peek(addr) : 0xff
237
265
  end
@@ -4,6 +4,9 @@ module Badline
4
4
  class CIA
5
5
  # Running a CIA a stretch of cycles at once, for Drive::Idle.
6
6
  module FastForward
7
+ # quiet_cycles for a CIA whose counters will never set a flag.
8
+ QUIET = 1 << 40
9
+
7
10
  # Whether fast_forward can run the CIA: timer B stopped with nothing
8
11
  # in its pipeline, no interrupt or acknowledge on its way, CNT where it
9
12
  # was, the TOD clock stopped, and timer A either stopped, or free
@@ -16,11 +19,24 @@ module Badline
16
19
  @ta.idle? ? @serial.idle : timer_a_free_running?
17
20
  end
18
21
 
19
- # Runs +cycles+ cycles at once while quiet?, as cycle! would: timer A's
20
- # underflows flag in the ICR, and reach the serial port's underflow
21
- # line.
22
+ # How many cycles the CIA can run with nothing but its counters
23
+ # moving: as quiet? allows, but with timer B free running too, up to
24
+ # the cycle before it underflows. The 1581's DOS leaves timer B
25
+ # interrupting every 10 ms.
26
+ def quiet_cycles
27
+ return 0 unless @icr.quiet && @tod.quiet? && cnt_steady? && timer_a_quiet?
28
+ return QUIET if @tb.idle?
29
+ return 0 unless @tb.free_running? && @control_b.value.nobits?(0x60)
30
+
31
+ @tb.counter - 1
32
+ end
33
+
34
+ # Runs +cycles+ cycles at once while quiet? or for quiet_cycles, as
35
+ # cycle! would: timer A's underflows flag in the ICR, and reach the
36
+ # serial port's underflow line.
22
37
  def fast_forward(cycles)
23
38
  @tod.fast_forward(cycles)
39
+ @tb.fast_forward(cycles) unless @tb.idle?
24
40
  return if @ta.idle?
25
41
 
26
42
  @icr.flag(:timer_a) if @ta.fast_forward(cycles).positive?
@@ -29,6 +45,8 @@ module Badline
29
45
 
30
46
  private
31
47
 
48
+ def timer_a_quiet? = @ta.idle? ? @serial.idle : timer_a_free_running?
49
+
32
50
  # Timer A free running with its interrupt masked, and the serial port
33
51
  # an idle input it only clocks.
34
52
  def timer_a_free_running?
data/lib/badline/cia.rb CHANGED
@@ -61,14 +61,16 @@ module Badline
61
61
  @tb.latch = value
62
62
  end
63
63
 
64
- def initialize(start: 0, peripheral: nil, model: :mos6526, region: Region::PAL)
64
+ # +tod_hz+ is the rate of the pulses on the TOD pin, the region's mains
65
+ # unless a machine feeds it otherwise, and 0 for a pin held still.
66
+ def initialize(start: 0, peripheral: nil, model: :mos6526, region: Region::PAL, tod_hz: nil)
65
67
  raise ArgumentError, "unknown CIA model #{model}" unless MODELS.include?(model)
66
68
 
67
69
  addressable_at(start, length: 2**8)
68
70
 
69
71
  @model = model
70
72
  @clock_hz = region.clock_hz
71
- @mains_hz = region.mains_hz
73
+ @mains_hz = tod_hz || region.mains_hz
72
74
 
73
75
  @peripheral = peripheral
74
76
  @port_b4_handler = nil
@@ -139,9 +141,10 @@ module Badline
139
141
 
140
142
  # Port A as driven by the data/direction registers alone, without
141
143
  # peripheral pulldown. Cheap path for the VIC bank lookup.
142
- def port_a_lines
143
- driven_lines(@data_port_a, @data_dir_a)
144
- end
144
+ def port_a_lines = driven_lines(@data_port_a, @data_dir_a)
145
+
146
+ # Port B as driven by the data/direction registers alone.
147
+ def port_b_lines = driven_lines(@data_port_b, @data_dir_b)
145
148
 
146
149
  def read_port_b
147
150
  lines = driven_lines(@data_port_b, @data_dir_b)
@@ -3,9 +3,11 @@
3
3
  module Badline
4
4
  class Computer
5
5
  # What plugs into the machine besides its chips, the cartridge, the
6
- # disk device 8 serves through the traps, a true 1541 and an REU, and
7
- # their state for a snapshot.
6
+ # disk device 8 serves through the traps, its true drives (TrueDrives)
7
+ # and an REU, and their state for a snapshot.
8
8
  module Attachments
9
+ include TrueDrives
10
+
9
11
  # Puts the cartridge in the expansion port and wires its clock and NMI
10
12
  # line to the machine, leaving the machine's state as it is. A
11
13
  # restored snapshot's cartridge comes back through here.
@@ -44,42 +46,8 @@ module Badline
44
46
  # it.
45
47
  attr_reader :iec_bus
46
48
 
47
- # Plugs in a Drive1541, which then runs alongside the C64 on its own
48
- # clock and talks to it over the serial bus. The serial traps stop
49
- # answering the drive's device number, so the KERNAL's TALK, LISTEN and
50
- # byte transfers reach the drive. The LOAD and SAVE traps stay, and
51
- # still serve a mounted image.
52
- def attach_drive1541(drive)
53
- @iec_bus.detach(@drive1541) if @drive1541
54
- drive.host_clock_hz = region.clock_hz
55
- @drive1541 = drive
56
- drive.connect(iec_bus)
57
- @serial_trap&.device = serial_trap_device
58
- end
59
-
60
- # The true drive on the serial bus: the Drive1541, or nil.
61
- def true_drive = @drive1541
62
-
63
- # Plugs a Drive1541 in as device 8, and returns it.
64
- def plug_true_drive = Drive1541.new.tap { |drive| attach_drive1541(drive) }
65
-
66
- # Unplugs the Drive1541, leaving the serial bus with nothing on it.
67
- def detach_drive1541
68
- return unless @drive1541
69
-
70
- @iec_bus.detach(@drive1541)
71
- @drive1541 = nil
72
- @serial_trap&.device = serial_trap_device
73
- end
74
-
75
49
  private
76
50
 
77
- # The device number the serial traps answer: device 8, unless a true
78
- # drive is on the bus as device 8.
79
- def serial_trap_device
80
- @drive1541&.device == KernalTrap::Routine::DEVICE ? nil : KernalTrap::Routine::DEVICE
81
- end
82
-
83
51
  # CIA 2's port A drives the serial bus, which reads CLK and DATA back
84
52
  # into it.
85
53
  def plug_serial_bus
@@ -132,26 +100,6 @@ module Badline
132
100
  @save_trap.load_state(input)
133
101
  end
134
102
 
135
- # A true 1541 on the serial bus. The leading flag is the bus itself,
136
- # which every machine now has, kept so older snapshots still load.
137
- def save_serial_bus(out)
138
- out.boolean(true).boolean(!@drive1541.nil?)
139
- return unless @drive1541
140
-
141
- out.int(@drive1541.device)
142
- @drive1541.save_state(out)
143
- end
144
-
145
- def load_serial_bus(input)
146
- input.boolean?
147
- return detach_drive1541 unless input.boolean?
148
-
149
- device = input.int
150
- detach_drive1541 if @drive1541 && @drive1541.device != device
151
- attach_drive1541(Drive1541.new(device:)) unless @drive1541
152
- @drive1541.load_state(input)
153
- end
154
-
155
103
  # The REU, which the setup says the machine has, and whether it has
156
104
  # asked for the bus. Its IRQ line reaches the CPU as it was.
157
105
  def save_reu(out)
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Computer
5
+ # The true drives on the C64's serial bus: a Drive1541, and a Drive1581
6
+ # (Drive1581::Slot), each in a field of its own, and their state for a
7
+ # snapshot.
8
+ module TrueDrives
9
+ include Drive1581::Slot
10
+
11
+ # Plugs in a Drive1541, which then runs alongside the C64 on its own
12
+ # clock and talks to it over the serial bus. The serial traps stop
13
+ # answering the drive's device number, so the KERNAL's TALK, LISTEN and
14
+ # byte transfers reach the drive. The LOAD and SAVE traps stay, and
15
+ # still serve a mounted image.
16
+ def attach_drive1541(drive)
17
+ @iec_bus.detach(@drive1541) if @drive1541
18
+ drive.host_clock_hz = region.clock_hz
19
+ @drive1541 = drive
20
+ drive.connect(iec_bus)
21
+ @serial_trap&.device = serial_trap_device
22
+ end
23
+
24
+ # The true drive on the serial bus: the Drive1581 ahead of the
25
+ # Drive1541, or nil.
26
+ def true_drive = @drive1581 || @drive1541
27
+
28
+ # Plugs a Drive1541 in as device 8, in place of a 1581 there, and
29
+ # returns it.
30
+ def plug_true_drive
31
+ detach_drive1581 if drive1581_on_device8?
32
+ Drive1541.new.tap { |drive| attach_drive1541(drive) }
33
+ end
34
+
35
+ # Unplugs the Drive1541, leaving the serial bus with nothing on it.
36
+ def detach_drive1541
37
+ return unless @drive1541
38
+
39
+ @iec_bus.detach(@drive1541)
40
+ @drive1541 = nil
41
+ @serial_trap&.device = serial_trap_device
42
+ end
43
+
44
+ private
45
+
46
+ # The device number the serial traps answer: device 8, unless a true
47
+ # drive is on the bus as device 8.
48
+ def serial_trap_device
49
+ device = KernalTrap::Routine::DEVICE
50
+ @drive1541&.device == device || drive1581_on_device8? ? nil : device
51
+ end
52
+
53
+ # A true 1541 on the serial bus, then a 1581. The leading flag is the
54
+ # bus itself, which every machine now has.
55
+ def save_serial_bus(out)
56
+ out.boolean(true).boolean(!@drive1541.nil?)
57
+ if @drive1541
58
+ out.int(@drive1541.device)
59
+ @drive1541.save_state(out)
60
+ end
61
+ save_drive1581(out)
62
+ end
63
+
64
+ def load_serial_bus(input)
65
+ input.boolean?
66
+ load_drive1541(input)
67
+ load_drive1581(input)
68
+ end
69
+
70
+ def load_drive1541(input)
71
+ return detach_drive1541 unless input.boolean?
72
+
73
+ device = input.int
74
+ detach_drive1541 if @drive1541 && @drive1541.device != device
75
+ attach_drive1541(Drive1541.new(device:)) unless @drive1541
76
+ @drive1541.load_state(input)
77
+ end
78
+ end
79
+ end
80
+ end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "badline/computer/true_drives"
3
4
  require "badline/computer/attachments"
4
5
  require "badline/computer/kernal_traps"
5
6
  require "badline/computer/saved_state"
@@ -77,6 +78,7 @@ module Badline
77
78
  @serial_trap = nil
78
79
  @save_trap = nil
79
80
  @drive1541 = nil
81
+ @drive1581 = nil
80
82
  plug_serial_bus
81
83
  @reu = reu ? plug_reu(reu) : nil
82
84
  end
@@ -101,6 +103,7 @@ module Badline
101
103
  watch_freeze if @freezing
102
104
  clock_cpu
103
105
  @drive1541&.host_cycle!
106
+ @drive1581&.host_cycle!
104
107
 
105
108
  @cycles += 1
106
109
  end
@@ -174,6 +177,7 @@ module Badline
174
177
  @dma = false
175
178
  @drive&.reset!
176
179
  @drive1541&.reset!
180
+ @drive1581&.reset!
177
181
  @freezing = false
178
182
  @nmi_asserted = false
179
183
  cpu.reset!
data/lib/badline/core.rb CHANGED
@@ -38,6 +38,8 @@ require "badline/vic"
38
38
  require "badline/keyboard_buffer"
39
39
  require "badline/iec_bus"
40
40
  require "badline/drive1541"
41
+ require "badline/drive1581"
42
+ require "badline/drive1581/slot"
41
43
  require "badline/snapshot/state"
42
44
  require "badline/snapshot/setup"
43
45
  require "badline/computer"
@@ -3,8 +3,8 @@
3
3
  module Badline
4
4
  module Drive
5
5
  # What every Commodore disk drive here shares: a 6502 running the DOS
6
- # ROM with two VIAs on its bus, VIA 1 facing the serial bus and VIA 2
7
- # the disk mechanism, on a clock of its own.
6
+ # ROM, with chips on its bus facing the serial bus and the disk
7
+ # mechanism, on a clock of its own.
8
8
  #
9
9
  # The drive runs on its own crystal, so it clocks against the host's
10
10
  # clock through a fractional accumulator: each host cycle adds the
@@ -14,8 +14,7 @@ module Badline
14
14
  # every 67. The host sets its clock on attaching the drive, and until
15
15
  # then a 1 MHz drive runs one cycle per host cycle.
16
16
  #
17
- # VIA 2's port B runs the Mechanism, which reads a Disk put in with
18
- # insert.
17
+ # The Mechanism reads a Disk put in with insert.
19
18
  #
20
19
  # While the DOS idles, host_cycle! skips the drive's cycles and catches
21
20
  # up on them later, exactly (see Idle). The readers of the drive's parts
@@ -24,7 +23,10 @@ module Badline
24
23
  # sleeps through them too (see Orbit).
25
24
  #
26
25
  # A model includes Core and builds its parts, sets @clock_hz and
27
- # @idle_loop, and runs one drive cycle in step.
26
+ # @idle_loop, and runs one drive cycle in step. It hears ATN on
27
+ # connecting (hear_atn), drives the serial bus (serial_output), and
28
+ # answers Idle's and Orbit's questions about its chips, as VIAs does
29
+ # for the 1541 and the 1571.
28
30
  module Core
29
31
  include Sleep
30
32
  include Idle
@@ -42,16 +44,6 @@ module Badline
42
44
  @bus
43
45
  end
44
46
 
45
- def via1
46
- settle!
47
- @via1
48
- end
49
-
50
- def via2
51
- settle!
52
- @via2
53
- end
54
-
55
47
  def mechanism
56
48
  settle!
57
49
  @mechanism
@@ -77,7 +69,7 @@ module Badline
77
69
  @serial_bus = serial_bus
78
70
  @serial_port.bus = serial_bus
79
71
  serial_bus.attach(self)
80
- @via1.ca1 = @serial_port.atn_low?
72
+ hear_atn
81
73
  end
82
74
 
83
75
  # Puts a Disk in the drive (Disk.from_d64 makes one from an image).
@@ -93,10 +85,6 @@ module Badline
93
85
  # is the same at the end of every pass the drive sleeps through.
94
86
  def led_on? = @mechanism.led_on?
95
87
 
96
- # VIA 1's port B as it drives the serial bus. It holds still while the
97
- # drive sleeps, so reading it leaves the drive asleep.
98
- def serial_output = @via1.port_b_output
99
-
100
88
  # Stores what the head wrote since the motor last stopped in the
101
89
  # disk's image, as the motor stopping does.
102
90
  def flush
@@ -104,15 +92,6 @@ module Badline
104
92
  @mechanism.flush
105
93
  end
106
94
 
107
- # The serial bus's RESET line reaches the CPU and both VIAs. RAM keeps
108
- # its contents.
109
- def reset!
110
- settle!
111
- @via1.reset!
112
- @via2.reset!
113
- @cpu.reset!
114
- end
115
-
116
95
  # Runs the drive cycles that fall in one host cycle: none, one or two.
117
96
  # The host has run this cycle already, and the drive sees what it did
118
97
  # from the next one on (see SerialPort). Asleep, the drive owes the
@@ -158,10 +137,6 @@ module Badline
158
137
 
159
138
  private
160
139
 
161
- # Runs the model's chips beyond the VIAs +cycles+ quiet cycles at once
162
- # (see quiet_cycles). The 1541 has none.
163
- def fast_forward_chips(_cycles) = nil
164
-
165
140
  # A drive cycle from host_cycle!, which may find the idle loop.
166
141
  def run_cycle
167
142
  step
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drive
5
+ # Fast serial on a drive with a CIA, the 1571's 6526 or the 1581's
6
+ # 8520: through buffers a port line turns round, the CIA's CNT drives
7
+ # SRQ and its SP drives DATA, or it hears them. The drive pushes its
8
+ # pins into the bus as they move, and hears the host's as the host
9
+ # pushes them, pin by pin, so each byte goes through both shift
10
+ # registers bit by bit. The CIA takes no part in a pass of the idle
11
+ # loop, and any access to it, or the host moving its fast serial pins,
12
+ # wakes the drive.
13
+ #
14
+ # A model includes it beside Core, and calls turn_fast_serial when its
15
+ # direction line moves and drive_fast_serial each cycle the buffers
16
+ # face out. Its serial_output carries @fast_output above its port.
17
+ module FastSerial
18
+ # Whether the buffers face out, the drive's CIA driving DATA and SRQ.
19
+ attr_reader :fast_serial_out
20
+
21
+ # The host moved its fast serial pins: the CIA hears SRQ on CNT and
22
+ # DATA on SP while the buffers face in.
23
+ def fast_lines_moved
24
+ settle!
25
+ hear_fast_serial unless @fast_serial_out
26
+ end
27
+
28
+ private
29
+
30
+ def init_fast_serial
31
+ @fast_serial_out = false
32
+ @fast_output = 0
33
+ end
34
+
35
+ # The buffers turned round: outwards the CIA's pins drive the lines,
36
+ # and inwards they let go and hear them.
37
+ def turn_fast_serial(out)
38
+ @fast_serial_out = out
39
+ return unless @serial_bus
40
+
41
+ push_fast_output(out ? fast_pins : 0)
42
+ hear_fast_serial unless out
43
+ end
44
+
45
+ def drive_fast_serial
46
+ low = fast_pins
47
+ push_fast_output(low) if low != @fast_output
48
+ end
49
+
50
+ # The lines the CIA's CNT and SP pull while its serial port drives
51
+ # them.
52
+ def fast_pins
53
+ serial = @cia.serial
54
+ return 0 unless serial.output?
55
+
56
+ low = serial.cnt ? 0 : IECBus::DRIVE_FAST_SRQ
57
+ serial.sp_out ? low : low | IECBus::DRIVE_FAST_DATA
58
+ end
59
+
60
+ def push_fast_output(low)
61
+ @fast_output = low
62
+ @serial_bus&.drives_fast_moved!
63
+ end
64
+
65
+ def hear_fast_serial
66
+ low = @serial_bus.low_lines
67
+ serial = @cia.serial
68
+ serial.cnt_in = low.nobits?(IECBus::SRQ)
69
+ serial.sp_in = low.nobits?(IECBus::DATA)
70
+ end
71
+ end
72
+ end
73
+ end
@@ -81,7 +81,7 @@ module Badline
81
81
  end
82
82
 
83
83
  def start_recording
84
- return if @mechanism.motor_on? || @via1.irq? || @via2.irq? || @cpu.trapped?
84
+ return if busy_for_pass? || @cpu.trapped?
85
85
 
86
86
  @recording = true
87
87
  @record_state = idle_state
@@ -116,20 +116,10 @@ module Badline
116
116
  anchor
117
117
  end
118
118
 
119
- def quiet_cycles
120
- [@via1.quiet_cycles, @via2.quiet_cycles].min
121
- end
122
-
123
119
  def ram_as_found?
124
120
  ram = @bus.ram
125
121
  @bus.touched.all? { |addr, value| ram.peek(addr) == value }
126
122
  end
127
-
128
- def idle_state
129
- mechanism = @mechanism
130
- [*@cpu.idle_state, *@via1.idle_state, *@via2.idle_state, @bus.data, mechanism.so_pending,
131
- *mechanism.idle_state, mechanism.read_a(0xff), mechanism.read_b(0xff)]
132
- end
133
123
  end
134
124
  end
135
125
  end
@@ -63,9 +63,7 @@ module Badline
63
63
  cycles = orbits * @orbit_cycles
64
64
  touched = @bus.touched_timers
65
65
  @cpu.fast_forward(cycles, orbits * @orbit_instructions)
66
- @via1.skip_orbits(cycles, touched & 0x03)
67
- @via2.skip_orbits(cycles, touched >> 2)
68
- fast_forward_chips(cycles)
66
+ skip_chip_orbits(cycles, touched)
69
67
  @cycles += cycles
70
68
  cycles
71
69
  end
@@ -74,8 +72,7 @@ module Badline
74
72
  # the unarmed timers it hasn't touched since it was last tainted.
75
73
  def anchor_state
76
74
  touched = @bus.touched_timers
77
- [*idle_state, *@via1.counter_state(touched & 0x03), *@via2.counter_state(touched >> 2),
78
- @bus.ram.snapshot]
75
+ [*idle_state, *counter_state(touched), @bus.ram.snapshot]
79
76
  end
80
77
  end
81
78
  end
@@ -102,8 +102,6 @@ module Badline
102
102
  def skip_passes(passes)
103
103
  cycles = passes * @pass_cycles
104
104
  @cpu.fast_forward(cycles, passes * @pass_instructions)
105
- @via1.fast_forward(cycles)
106
- @via2.fast_forward(cycles)
107
105
  fast_forward_chips(cycles)
108
106
  @cycles += cycles
109
107
  @budget -= cycles
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drive
5
+ # The two VIAs the 1541 and the 1571 share: VIA 1 facing the serial
6
+ # bus, its CA1 on ATN, and VIA 2 running the GCR Mechanism. A model
7
+ # includes it beside Core, and it answers Core's, Idle's and Orbit's
8
+ # questions about the chips: what they hold, how long their counters
9
+ # leave them quiet, and how to run them a stretch at once. A model with
10
+ # more chips on its bus runs them in fast_forward_more.
11
+ module VIAs
12
+ def via1
13
+ settle!
14
+ @via1
15
+ end
16
+
17
+ def via2
18
+ settle!
19
+ @via2
20
+ end
21
+
22
+ # Puts in a Disk from the .d64, .g64, .d71 or .g71 image at +path+
23
+ # (Drive1541::Disk.open), write-protected with `read_only`.
24
+ def insert_image(path, read_only: false)
25
+ insert(Drive1541::Disk.open(path, read_only:))
26
+ end
27
+
28
+ # VIA 1's port B as it drives the serial bus. It holds still while the
29
+ # drive sleeps, so reading it leaves the drive asleep.
30
+ def serial_output = @via1.port_b_output
31
+
32
+ # The serial bus's RESET line reaches the CPU and both VIAs. RAM keeps
33
+ # its contents.
34
+ def reset!
35
+ settle!
36
+ @via1.reset!
37
+ @via2.reset!
38
+ @cpu.reset!
39
+ end
40
+
41
+ private
42
+
43
+ # ATN reaches VIA 1's CA1 as the serial port latched it.
44
+ def hear_atn
45
+ @via1.ca1 = @serial_port.atn_low?
46
+ end
47
+
48
+ # Whether the drive is doing anything a pass of the idle loop can't
49
+ # repeat: the motor on, or a VIA pulling IRQ.
50
+ def busy_for_pass? = @mechanism.motor_on? || @via1.irq? || @via2.irq?
51
+
52
+ def quiet_cycles
53
+ [@via1.quiet_cycles, @via2.quiet_cycles].min
54
+ end
55
+
56
+ def idle_state
57
+ mechanism = @mechanism
58
+ [*@cpu.idle_state, *@via1.idle_state, *@via2.idle_state, @bus.data, mechanism.so_pending,
59
+ *mechanism.idle_state, mechanism.read_a(0xff), mechanism.read_b(0xff)]
60
+ end
61
+
62
+ # The counters an orbit has to bring back (VIA#counter_state).
63
+ def counter_state(touched)
64
+ [*@via1.counter_state(touched & 0x03), *@via2.counter_state(touched >> 2)]
65
+ end
66
+
67
+ def fast_forward_chips(cycles)
68
+ @via1.fast_forward(cycles)
69
+ @via2.fast_forward(cycles)
70
+ fast_forward_more(cycles)
71
+ end
72
+
73
+ def skip_chip_orbits(cycles, touched)
74
+ @via1.skip_orbits(cycles, touched & 0x03)
75
+ @via2.skip_orbits(cycles, touched >> 2)
76
+ fast_forward_more(cycles)
77
+ end
78
+
79
+ # Runs the model's chips beyond the VIAs +cycles+ quiet cycles at once
80
+ # (see quiet_cycles). The 1541 has none.
81
+ def fast_forward_more(_cycles) = nil
82
+ end
83
+ end
84
+ end
data/lib/badline/drive.rb CHANGED
@@ -4,6 +4,8 @@ require "badline/drive/sleep"
4
4
  require "badline/drive/idle"
5
5
  require "badline/drive/orbit"
6
6
  require "badline/drive/core"
7
+ require "badline/drive/vias"
8
+ require "badline/drive/fast_serial"
7
9
  require "badline/drive/idle_cpu"
8
10
  require "badline/drive/watch"
9
11
  require "badline/drive/serial_lines"