badline 0.11.1 → 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 (175) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -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/address_bus/saved_state.rb +4 -10
  10. data/lib/badline/address_bus.rb +8 -27
  11. data/lib/badline/audio/bare_player.rb +1 -1
  12. data/lib/badline/c128/bus.rb +17 -52
  13. data/lib/badline/c128/bus_state.rb +5 -5
  14. data/lib/badline/c128/drives.rb +35 -22
  15. data/lib/badline/c128/fast_serial.rb +1 -0
  16. data/lib/badline/c128/io_pages.rb +35 -0
  17. data/lib/badline/c128/mmu.rb +9 -7
  18. data/lib/badline/c128/modes.rb +17 -4
  19. data/lib/badline/c128/saved_state.rb +22 -0
  20. data/lib/badline/c128/z80.rb +49 -0
  21. data/lib/badline/c128/z80_pages.rb +118 -0
  22. data/lib/badline/c128/z80_turns.rb +106 -0
  23. data/lib/badline/c128.rb +21 -83
  24. data/lib/badline/cartridge/action_replay.rb +2 -2
  25. data/lib/badline/cartridge/comal80.rb +1 -7
  26. data/lib/badline/cartridge/dinamic.rb +0 -8
  27. data/lib/badline/cartridge/final_cartridge3.rb +6 -6
  28. data/lib/badline/cartridge/fun_play.rb +0 -8
  29. data/lib/badline/cartridge/game_system.rb +0 -8
  30. data/lib/badline/cartridge/kcs_power.rb +10 -7
  31. data/lib/badline/cartridge/mach5.rb +2 -4
  32. data/lib/badline/cartridge/magic_desk.rb +0 -8
  33. data/lib/badline/cartridge/ocean.rb +0 -8
  34. data/lib/badline/cartridge/pagefox.rb +2 -2
  35. data/lib/badline/cartridge/retro_replay.rb +7 -6
  36. data/lib/badline/cartridge/rex_utility.rb +2 -6
  37. data/lib/badline/cartridge/rgcd.rb +2 -2
  38. data/lib/badline/cartridge/simons_basic.rb +0 -5
  39. data/lib/badline/cartridge/super_games.rb +4 -2
  40. data/lib/badline/cartridge/westermann.rb +2 -6
  41. data/lib/badline/cartridge.rb +37 -9
  42. data/lib/badline/cia/fast_forward.rb +21 -3
  43. data/lib/badline/cia.rb +8 -5
  44. data/lib/badline/computer/attachments.rb +14 -95
  45. data/lib/badline/computer/common.rb +34 -0
  46. data/lib/badline/computer/kernal_traps.rb +65 -5
  47. data/lib/badline/computer/true_drives.rb +69 -0
  48. data/lib/badline/computer.rb +9 -68
  49. data/lib/badline/core.rb +4 -2
  50. data/lib/badline/cpu_port.rb +112 -0
  51. data/lib/badline/drive/core.rb +8 -33
  52. data/lib/badline/drive/fast_serial.rb +73 -0
  53. data/lib/badline/drive/idle.rb +1 -11
  54. data/lib/badline/drive/orbit.rb +2 -5
  55. data/lib/badline/drive/sleep.rb +0 -2
  56. data/lib/badline/drive/vias.rb +84 -0
  57. data/lib/badline/drive.rb +2 -0
  58. data/lib/badline/drive1541/disk.rb +14 -17
  59. data/lib/badline/drive1541/disk_state.rb +7 -15
  60. data/lib/badline/drive1541.rb +1 -0
  61. data/lib/badline/drive1571/wd1770.rb +94 -8
  62. data/lib/badline/drive1571.rb +8 -58
  63. data/lib/badline/drive1581/bus.rb +137 -0
  64. data/lib/badline/drive1581/cpu.rb +11 -0
  65. data/lib/badline/drive1581/disk.rb +135 -0
  66. data/lib/badline/drive1581/disk_state.rb +87 -0
  67. data/lib/badline/drive1581/mechanism.rb +117 -0
  68. data/lib/badline/drive1581/serial_port.rb +52 -0
  69. data/lib/badline/drive1581/slot.rb +78 -0
  70. data/lib/badline/drive1581/track.rb +100 -0
  71. data/lib/badline/drive1581/track_bytes.rb +75 -0
  72. data/lib/badline/drive1581/wd1772/registers.rb +89 -0
  73. data/lib/badline/drive1581/wd1772/sector_writes.rb +73 -0
  74. data/lib/badline/drive1581/wd1772/sectors.rb +150 -0
  75. data/lib/badline/drive1581/wd1772/state.rb +66 -0
  76. data/lib/badline/drive1581/wd1772/stepping.rb +109 -0
  77. data/lib/badline/drive1581/wd1772/tracks.rb +125 -0
  78. data/lib/badline/drive1581/wd1772/turn.rb +73 -0
  79. data/lib/badline/drive1581/wd1772.rb +216 -0
  80. data/lib/badline/drive1581.rb +233 -0
  81. data/lib/badline/frontend/app.rb +1 -3
  82. data/lib/badline/frontend/audio_device.rb +67 -0
  83. data/lib/badline/frontend/audio_sink.rb +16 -50
  84. data/lib/badline/frontend/boot.rb +24 -67
  85. data/lib/badline/frontend/buttons.rb +7 -7
  86. data/lib/badline/frontend/keys.rb +33 -8
  87. data/lib/badline/frontend/media_slots.rb +42 -0
  88. data/lib/badline/frontend/{confirmation.rb → menu/confirmation.rb} +4 -4
  89. data/lib/badline/frontend/{file_browser.rb → menu/file_browser.rb} +18 -34
  90. data/lib/badline/frontend/{menu_dialogs.rb → menu/menu_dialogs.rb} +3 -7
  91. data/lib/badline/frontend/{menu_media.rb → menu/menu_media.rb} +26 -51
  92. data/lib/badline/frontend/{menu_pages.rb → menu/menu_pages.rb} +5 -8
  93. data/lib/badline/frontend/{name_field.rb → menu/name_field.rb} +11 -24
  94. data/lib/badline/frontend/{pause_menu.rb → menu/pause_menu.rb} +13 -28
  95. data/lib/badline/frontend/menu/theme.rb +18 -0
  96. data/lib/badline/frontend/{filter_view.rb → player/filter_view.rb} +4 -4
  97. data/lib/badline/frontend/{info_view.rb → player/info_view.rb} +3 -3
  98. data/lib/badline/frontend/{player_window.rb → player/player_controller.rb} +17 -25
  99. data/lib/badline/frontend/{player_footer.rb → player/player_footer.rb} +8 -8
  100. data/lib/badline/frontend/{player_header.rb → player/player_header.rb} +5 -5
  101. data/lib/badline/frontend/{player_screen.rb → player/player_screen.rb} +4 -5
  102. data/lib/badline/frontend/{scope.rb → player/scope.rb} +1 -1
  103. data/lib/badline/frontend/{sid_view.rb → player/sid_view.rb} +16 -20
  104. data/lib/badline/frontend/player/theme.rb +18 -0
  105. data/lib/badline/frontend/{visualizer_view.rb → player/visualizer_view.rb} +8 -8
  106. data/lib/badline/frontend/sound.rb +19 -38
  107. data/lib/badline/frontend/timeline.rb +3 -14
  108. data/lib/badline/frontend.rb +24 -20
  109. data/lib/badline/iec_bus.rb +14 -2
  110. data/lib/badline/kernal_trap/{file.rb → file_routine.rb} +1 -1
  111. data/lib/badline/kernal_trap/load.rb +1 -1
  112. data/lib/badline/kernal_trap/save.rb +1 -1
  113. data/lib/badline/kernal_trap.rb +1 -1
  114. data/lib/badline/machine/clocking.rb +50 -0
  115. data/lib/badline/machine.rb +63 -6
  116. data/lib/badline/media/boot_disk.rb +50 -0
  117. data/lib/badline/media/disk_list.rb +3 -3
  118. data/lib/badline/media/disk_set.rb +1 -1
  119. data/lib/badline/media/extensions.rb +33 -0
  120. data/lib/badline/media/not_disk.rb +8 -0
  121. data/lib/badline/media/program.rb +28 -0
  122. data/lib/badline/media/true_drive.rb +38 -17
  123. data/lib/badline/media/tune.rb +35 -0
  124. data/lib/badline/media/vic20_cartridge.rb +1 -7
  125. data/lib/badline/media/vic20_media.rb +38 -80
  126. data/lib/badline/media.rb +90 -81
  127. data/lib/badline/options/event.rb +4 -2
  128. data/lib/badline/options/family.rb +3 -9
  129. data/lib/badline/options/help.rb +2 -1
  130. data/lib/badline/roms/README +9 -0
  131. data/lib/badline/roms/dos1581.rom +0 -0
  132. data/lib/badline/sdl.rb +37 -20
  133. data/lib/badline/sid.rb +1 -1
  134. data/lib/badline/snapshot/machine_state.rb +9 -1
  135. data/lib/badline/snapshot/state.rb +6 -4
  136. data/lib/badline/snapshot.rb +11 -9
  137. data/lib/badline/storage/d64_image.rb +15 -7
  138. data/lib/badline/storage/disk_image/writing.rb +2 -2
  139. data/lib/badline/storage/disk_image.rb +1 -1
  140. data/lib/badline/storage/host_directory.rb +1 -1
  141. data/lib/badline/storage/p00.rb +8 -0
  142. data/lib/badline/storage/t64.rb +1 -1
  143. data/lib/badline/time_of_day.rb +3 -3
  144. data/lib/badline/version.rb +1 -1
  145. data/lib/badline/vic/bank.rb +1 -1
  146. data/lib/badline/vic/border_mask.rb +1 -1
  147. data/lib/badline/vic/collisions.rb +1 -1
  148. data/lib/badline/vic/color_patches.rb +1 -1
  149. data/lib/badline/vic/display_state.rb +1 -1
  150. data/lib/badline/vic/graphics_mode.rb +1 -1
  151. data/lib/badline/vic/graphics_shifter.rb +1 -1
  152. data/lib/badline/vic/register_log.rb +1 -1
  153. data/lib/badline/vic/registers.rb +1 -1
  154. data/lib/badline/vic/sequencer.rb +1 -1
  155. data/lib/badline/vic/sequencer_output.rb +1 -1
  156. data/lib/badline/vic/sequencer_state.rb +1 -1
  157. data/lib/badline/vic/sprite/internal_bus.rb +1 -1
  158. data/lib/badline/vic/sprite/saved_state.rb +1 -1
  159. data/lib/badline/vic/sprite/shifter.rb +1 -1
  160. data/lib/badline/vic/sprite/timing.rb +1 -1
  161. data/lib/badline/vic/sprite.rb +1 -1
  162. data/lib/badline/vic/sprites.rb +1 -1
  163. data/lib/badline/vic/sprites_state.rb +1 -1
  164. data/lib/badline/vic.rb +6 -5
  165. data/lib/badline/vic20/attachments.rb +15 -71
  166. data/lib/badline/vic20/saved_state.rb +2 -30
  167. data/lib/badline/vic20.rb +4 -37
  168. data/lib/badline/z80/core.rb +2 -0
  169. data/lib/badline/z80/saved_state.rb +69 -0
  170. metadata +61 -23
  171. data/lib/badline/cycleable.rb +0 -38
  172. /data/lib/badline/frontend/{menu_ports.rb → menu/menu_ports.rb} +0 -0
  173. /data/lib/badline/frontend/{player_commands.rb → player/player_commands.rb} +0 -0
  174. /data/lib/badline/frontend/{player_state.rb → player/player_state.rb} +0 -0
  175. /data/lib/badline/frontend/{sid_history.rb → player/sid_history.rb} +0 -0
@@ -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"
@@ -35,20 +35,10 @@ module Badline
35
35
  # The half track entries a side of a .g71 holds.
36
36
  G71_SIDE_ENTRIES = 84
37
37
 
38
- # GCR bytes around a track in each speed zone: 200 ms of rotation at
39
- # 300 rpm, in bytes of 32, 30, 28 and 26 µs.
38
+ # GCR bytes around a track in each speed zone (Storage::D64Image.zone):
39
+ # 200 ms of rotation at 300 rpm, in bytes of 32, 30, 28 and 26 µs.
40
40
  TRACK_LENGTHS = [6250, 6666, 7142, 7692].freeze
41
41
 
42
- # The speed zone each track is written in: 3 for the 21 sectors of
43
- # tracks 1-17, 2 for 18-24, 1 for 25-30 and 0 from 31 on.
44
- def self.zone(track)
45
- if track <= 17 then 3
46
- elsif track <= 24 then 2
47
- elsif track <= 30 then 1
48
- else 0
49
- end
50
- end
51
-
52
42
  # How far round the DOS's N: starts each track from the one before,
53
43
  # in each zone, in 1/10000 of a turn: it steps in and starts writing
54
44
  # the next track that long after it started the last.
@@ -69,10 +59,17 @@ module Badline
69
59
  # a .d71 formatted on both sides, and anything else as a .d64,
70
60
  # formatted. `read_only` opens the image write-protected.
71
61
  def self.open(path, read_only: false)
72
- return from_g64(Storage::G64Image.new(path, read_only:)).opened(path, read_only) if State.g64?(path)
73
- return from_d64(Storage::D71Image.new(path, read_only:)).opened(path, read_only) if State.d71?(path)
62
+ image = image_for(path, read_only)
63
+ (State.g64?(path) ? from_g64(image) : from_d64(image)).opened(path, read_only)
64
+ end
65
+
66
+ # The image at +path+, by its name: a .g64 or .g71, a .d71, and
67
+ # anything else a .d64.
68
+ def self.image_for(path, read_only)
69
+ return Storage::G64Image.new(path, read_only:) if State.g64?(path)
70
+ return Storage::D71Image.new(path, read_only:) if File.extname(path).casecmp?(".d71")
74
71
 
75
- from_d64(Storage::D64Image.new(path, read_only:)).opened(path, read_only)
72
+ Storage::D64Image.new(path, read_only:)
76
73
  end
77
74
 
78
75
  # A disk from a G64 or G71 image, each half track as the image stores
@@ -141,7 +138,7 @@ module Badline
141
138
  angle = 0
142
139
  (1..image.track_count).each do |track|
143
140
  half_track = d64_half_track(image, track)
144
- zone = zone((half_track % SIDE) / 2)
141
+ zone = Storage::D64Image.zone((half_track % SIDE) / 2)
145
142
  angle = half_track == SIDE + 2 ? 0 : (angle + SKEWS[zone]) % SKEW_TURN if track > 1
146
143
  bytes = format_track(image, track, id, zone)
147
144
  disk.write(half_track, Track.new(bytes.rotate(-(bytes.length * angle / SKEW_TURN)), zone))
@@ -149,7 +146,7 @@ module Badline
149
146
  disk
150
147
  end
151
148
 
152
- def self.format_track(image, track, id, zone = zone(track))
149
+ def self.format_track(image, track, id, zone = Storage::D64Image.zone(track))
153
150
  sectors = image.sectors_in(track)
154
151
  length = TRACK_LENGTHS[zone]
155
152
  gap = (length - (sectors * SECTOR_LENGTH)) / sectors
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "badline/media/extensions"
4
+
3
5
  module Badline
4
6
  class Drive1541
5
7
  class Disk
@@ -12,22 +14,12 @@ module Badline
12
14
  # for a disk from Disk.open. Nil for a disk made another way.
13
15
  attr_reader :path, :read_only
14
16
 
15
- # A .g64 or .g71 image by its name, a .d71 by its name, and anything
16
- # else a .d64.
17
- def self.g64?(path) = %w[.g64 .g71].include?(File.extname(path).downcase)
18
-
19
- def self.d71?(path) = File.extname(path).casecmp?(".d71")
17
+ # Whether the image is a .g64 or .g71, by its name.
18
+ def self.g64?(path) = Media::Extensions.kind(path) == :gcr
20
19
 
21
- # A disk for the image at the path, its tracks left for the state.
22
- def self.reopen(path, read_only)
23
- if g64?(path)
24
- Disk.new(Storage::G64Image.new(path, read_only:)).opened(path, read_only)
25
- elsif d71?(path)
26
- Disk.new(Storage::D71Image.new(path, read_only:)).opened(path, read_only)
27
- else
28
- Disk.new(Storage::D64Image.new(path, read_only:)).opened(path, read_only)
29
- end
30
- end
20
+ # A disk for the image at the path (Disk.image_for), its tracks left
21
+ # for the state.
22
+ def self.reopen(path, read_only) = Disk.new(Disk.image_for(path, read_only)).opened(path, read_only)
31
23
 
32
24
  # The disk a state from save_state describes: +current+ when it was
33
25
  # opened from the same image the same way, and otherwise a disk for
@@ -16,6 +16,7 @@ module Badline
16
16
  # clocks it against the host and sleeps through the DOS's idle loop.
17
17
  class Drive1541
18
18
  include Drive::Core
19
+ include Drive::VIAs
19
20
 
20
21
  CLOCK_HZ = 1_000_000
21
22
 
@@ -2,32 +2,118 @@
2
2
 
3
3
  module Badline
4
4
  class Drive1571
5
- # The WD1770 floppy controller at $2000, which the DOS needs only for
6
- # MFM disks. Its four registers hold what's written to them and it
7
- # runs no commands: the status reads idle, with the motor off.
5
+ # The WD1770 floppy controller at $2000, which DOS 3.0 drives for MFM
6
+ # disks. The 1571 here only ever holds GCR disks, so the controller
7
+ # never finds an MFM sector: it runs each command for its time and ends
8
+ # it as the WD177x data sheet does on a disk without one.
8
9
  #
9
10
  # $2000 status (read), command (write)
10
11
  # $2001 track
11
12
  # $2002 sector
12
13
  # $2003 data
14
+ #
15
+ # BUSY sets as a command is written. A restore, seek or step (type I)
16
+ # moves the track register and ends a few milliseconds later, with
17
+ # TRACK 0 reported at track 0. A sector read or write, a read address
18
+ # or a read track ends with RECORD NOT FOUND after five turns of the
19
+ # disk, and a write track after one, with nothing written. A force
20
+ # interrupt ends the command at once. Time is the drive CPU's cycles.
13
21
  class WD1770
22
+ BUSY = 0x01
23
+ TRACK0 = 0x04
24
+ NOT_FOUND = 0x10
25
+ MOTOR_ON = 0x80
26
+
27
+ # Cycles at 2 MHz, the 1571's clock in 1571 mode: a turn of the disk
28
+ # at 300 rpm, and a step with the head settling.
29
+ TURN = 400_000
30
+ STEP = 12_000
31
+
32
+ # The CPU whose cycles time the commands.
33
+ attr_writer :cpu
34
+
14
35
  def initialize
15
36
  @registers = [0, 0, 0, 0]
37
+ @status = 0
38
+ @due = 0
39
+ @result = 0
40
+ @cpu = nil
16
41
  end
17
42
 
18
- # Register +offset+, 0 to 3.
19
- def peek(offset) = offset.zero? ? 0 : @registers[offset]
43
+ # Register +offset+, 0 to 3: the status, settled to the command's end
44
+ # once its time has run out.
45
+ def peek(offset)
46
+ return @registers[offset] unless offset.zero?
47
+
48
+ finish if @status.anybits?(BUSY) && @cpu.cycles >= @due
49
+ @status
50
+ end
20
51
 
21
52
  def poke(offset, value)
22
- @registers[offset] = value
53
+ offset.zero? ? command(value) : @registers[offset] = value
23
54
  end
24
55
 
25
56
  def save_state(out)
26
- out.ints(@registers)
57
+ out.ints([*@registers, @status, @due, @result])
27
58
  end
28
59
 
60
+ # A state from before the controller ran commands holds the four
61
+ # registers alone.
29
62
  def load_state(input)
30
- @registers = input.ints
63
+ values = input.ints
64
+ @registers = values[0, 4]
65
+ @status = values[4] || 0
66
+ @due = values[5] || 0
67
+ @result = values[6] || 0
68
+ end
69
+
70
+ private
71
+
72
+ def command(value)
73
+ return interrupt if (value & 0xf0) == 0xd0
74
+
75
+ @registers[0] = value
76
+ value < 0x80 ? step(value) : search(value)
77
+ end
78
+
79
+ # A restore ($0x), seek ($1x) or step ($2x to $7x), its track register
80
+ # moved at once.
81
+ def step(value)
82
+ track = @registers[1]
83
+ target = case value >> 4
84
+ when 0 then 0
85
+ when 1 then @registers[3]
86
+ else track + step_direction(value)
87
+ end
88
+ @registers[1] = target.clamp(0, 255) if value < 0x20 || value.anybits?(0x10)
89
+ start(STEP * [(target - track).abs, 1].max, @registers[1].zero? ? TRACK0 : 0)
90
+ end
91
+
92
+ # Step in ($4x) moves toward higher tracks, step out ($6x) toward
93
+ # track 0, and step ($2x) the way the last one went.
94
+ def step_direction(value)
95
+ @direction = 1 if (value >> 5) == 2
96
+ @direction = -1 if (value >> 5) == 3
97
+ @direction || 1
98
+ end
99
+
100
+ # A sector or track command that finds no MFM sector.
101
+ def search(value)
102
+ value >= 0xf0 ? start(TURN, 0) : start(TURN * 5, NOT_FOUND)
103
+ end
104
+
105
+ def start(cycles, result)
106
+ @status = BUSY | MOTOR_ON
107
+ @due = @cpu.cycles + cycles
108
+ @result = result
109
+ end
110
+
111
+ def finish
112
+ @status = @result
113
+ end
114
+
115
+ def interrupt
116
+ @status &= ~BUSY & 0xff
31
117
  end
32
118
  end
33
119
  end
@@ -19,15 +19,13 @@ module Badline
19
19
  # same speed either way (Drive1541::Mechanism#clock_hz=).
20
20
  #
21
21
  # The CIA sits at $4000, on the IRQ line with both VIAs. Only its serial
22
- # port is wired: through buffers PA1 turns round, its CNT drives SRQ and
23
- # its SP DATA, or it hears them. The drive pushes its pins into the bus
24
- # as they move, and hears the host's as the host pushes them, pin by
25
- # pin, so each byte goes through both shift registers bit by bit. The
26
- # CIA takes no part in a pass of the idle loop: the drive sleeps only
27
- # while the CIA is quiet (CIA#quiet?), and any access to it, or the
28
- # host moving its fast serial pins, wakes the drive.
22
+ # port is wired, for fast serial through buffers PA1 turns round
23
+ # (Drive::FastSerial). The drive sleeps only while the CIA is quiet
24
+ # (CIA#quiet?).
29
25
  class Drive1571
30
26
  include Drive::Core
27
+ include Drive::VIAs
28
+ include Drive::FastSerial
31
29
 
32
30
  CLOCK_HZ = 1_000_000
33
31
  FAST_CLOCK_HZ = 2_000_000
@@ -40,10 +38,6 @@ module Badline
40
38
  SIDE = 0x04
41
39
  FAST = 0x20
42
40
 
43
- # Whether PA1 turns the fast serial buffers outwards, the drive's CIA
44
- # driving DATA and SRQ.
45
- attr_reader :fast_serial_out
46
-
47
41
  def cia
48
42
  settle!
49
43
  @cia
@@ -62,8 +56,7 @@ module Badline
62
56
  def initialize(rom: nil, host_clock_hz: CLOCK_HZ, device: 8, debug: false)
63
57
  @device = device
64
58
  @clock_hz = CLOCK_HZ
65
- @fast_serial_out = false
66
- @fast_output = 0
59
+ init_fast_serial
67
60
  @mechanism = Drive1541::Mechanism.new(slip: 2)
68
61
  @serial_port = SerialPort.new(@mechanism, device:)
69
62
  @via1 = SerialVIA.new(start: 0x1800, peripheral: @serial_port, drive: self)
@@ -72,6 +65,7 @@ module Badline
72
65
  @fdc = WD1770.new
73
66
  @bus = Bus.new(rom: rom || ROM.load("dos1571.rom", 0x8000), via1: @via1, via2: @via2, cia: @cia, fdc: @fdc)
74
67
  @cpu = CPU.new(@bus, debug:)
68
+ @fdc.cpu = @cpu
75
69
  @idle_loop = IDLE_LOOP
76
70
  @host_clock_hz = host_clock_hz
77
71
  @phase = 0
@@ -114,13 +108,6 @@ module Badline
114
108
  # (IECBus::DRIVE_FAST_DATA and DRIVE_FAST_SRQ) above it.
115
109
  def serial_output = @via1.port_b_output | @fast_output
116
110
 
117
- # The host moved its fast serial pins: the CIA hears SRQ on CNT and
118
- # DATA on SP while PA1 turns the buffers inwards.
119
- def fast_lines_moved
120
- settle!
121
- hear_fast_serial unless @fast_serial_out
122
- end
123
-
124
111
  # The drive's whole state, as the 1541's (Drive1541#save_state), with
125
112
  # the CIA and the WD1770's registers. The clock rate, the side and the
126
113
  # fast serial direction follow from VIA 1's port A.
@@ -181,45 +168,8 @@ module Badline
181
168
  @cia.quiet? ? [@via1.quiet_cycles, @via2.quiet_cycles].min : 0
182
169
  end
183
170
 
184
- def fast_forward_chips(cycles)
171
+ def fast_forward_more(cycles)
185
172
  @cia.fast_forward(cycles)
186
173
  end
187
-
188
- # PA1 turned the buffers round: outwards the CIA's pins drive the
189
- # lines, and inwards they let go and hear them.
190
- def turn_fast_serial(out)
191
- @fast_serial_out = out
192
- return unless @serial_bus
193
-
194
- push_fast_output(out ? fast_pins : 0)
195
- hear_fast_serial unless out
196
- end
197
-
198
- def drive_fast_serial
199
- low = fast_pins
200
- push_fast_output(low) if low != @fast_output
201
- end
202
-
203
- # The lines the CIA's CNT and SP pull while its serial port drives
204
- # them.
205
- def fast_pins
206
- serial = @cia.serial
207
- return 0 unless serial.output?
208
-
209
- low = serial.cnt ? 0 : IECBus::DRIVE_FAST_SRQ
210
- serial.sp_out ? low : low | IECBus::DRIVE_FAST_DATA
211
- end
212
-
213
- def push_fast_output(low)
214
- @fast_output = low
215
- @serial_bus&.drives_fast_moved!
216
- end
217
-
218
- def hear_fast_serial
219
- low = @serial_bus.low_lines
220
- serial = @cia.serial
221
- serial.cnt_in = low.nobits?(IECBus::SRQ)
222
- serial.sp_in = low.nobits?(IECBus::DATA)
223
- end
224
174
  end
225
175
  end