badline 0.13.0 → 0.14.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 (254) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +49 -1
  3. data/CLAUDE.md +27 -7
  4. data/CONTRIBUTING.md +4 -3
  5. data/README.md +8 -5
  6. data/doc/media.md +2 -1
  7. data/doc/pinned-behaviour.md +29 -0
  8. data/doc/snapshot-format.md +1047 -0
  9. data/doc/snapshots.md +72 -39
  10. data/lib/badline/audio/bare_player.rb +50 -17
  11. data/lib/badline/audio/machine_player.rb +15 -9
  12. data/lib/badline/audio/renderer.rb +32 -21
  13. data/lib/badline/c128/banks.rb +6 -3
  14. data/lib/badline/c128/bus/keyed_state.rb +45 -0
  15. data/lib/badline/c128/bus.rb +17 -3
  16. data/lib/badline/c128/drives.rb +6 -25
  17. data/lib/badline/c128/keyed_modules.rb +117 -0
  18. data/lib/badline/c128/keyed_state.rb +91 -0
  19. data/lib/badline/c128/keys.rb +9 -0
  20. data/lib/badline/c128/mmu.rb +9 -8
  21. data/lib/badline/c128/mmu_pages.rb +20 -0
  22. data/lib/badline/c128/modes.rb +19 -9
  23. data/lib/badline/c128/z80_turns.rb +1 -1
  24. data/lib/badline/c128.rb +8 -8
  25. data/lib/badline/c64/attachments.rb +0 -44
  26. data/lib/badline/c64/banked_ram/plus256k.rb +18 -10
  27. data/lib/badline/c64/banked_ram/plus60k.rb +8 -6
  28. data/lib/badline/c64/banked_ram/unexpanded.rb +2 -2
  29. data/lib/badline/c64/bus/debug_register.rb +32 -0
  30. data/lib/badline/c64/bus/keyed_state.rb +41 -0
  31. data/lib/badline/c64/bus.rb +2 -1
  32. data/lib/badline/c64/cartridge/action_replay.rb +0 -13
  33. data/lib/badline/c64/cartridge/easy_flash.rb +0 -15
  34. data/lib/badline/c64/cartridge/epyx_fastload.rb +0 -8
  35. data/lib/badline/c64/cartridge/final_cartridge3.rb +0 -12
  36. data/lib/badline/c64/cartridge/flash.rb +3 -3
  37. data/lib/badline/c64/cartridge/g_mod2.rb +0 -12
  38. data/lib/badline/c64/cartridge/geo_ram.rb +0 -16
  39. data/lib/badline/c64/cartridge/kcs_power.rb +0 -8
  40. data/lib/badline/c64/cartridge/keyed_state.rb +393 -0
  41. data/lib/badline/c64/cartridge/pagefox.rb +0 -10
  42. data/lib/badline/c64/cartridge/retro_replay.rb +0 -8
  43. data/lib/badline/c64/cartridge/rgcd.rb +0 -10
  44. data/lib/badline/c64/cartridge/super_games.rb +0 -10
  45. data/lib/badline/c64/cartridge/zaxxon.rb +0 -16
  46. data/lib/badline/c64/cartridge.rb +1 -27
  47. data/lib/badline/c64/kernal_traps.rb +28 -25
  48. data/lib/badline/c64/keyed_modules.rb +111 -0
  49. data/lib/badline/c64/keyed_state.rb +68 -0
  50. data/lib/badline/c64/reu/dma.rb +0 -4
  51. data/lib/badline/c64/reu/keyed_state.rb +140 -0
  52. data/lib/badline/c64/reu/ram.rb +0 -15
  53. data/lib/badline/c64/reu.rb +1 -3
  54. data/lib/badline/c64/true_drives.rb +56 -5
  55. data/lib/badline/c64.rb +7 -3
  56. data/lib/badline/chips/cia/interrupt_register.rb +0 -17
  57. data/lib/badline/chips/cia/keyed_parts.rb +213 -0
  58. data/lib/badline/chips/cia/serial.rb +0 -4
  59. data/lib/badline/chips/cia/time_of_day.rb +0 -22
  60. data/lib/badline/chips/cia/timer.rb +0 -20
  61. data/lib/badline/chips/cia.rb +2 -2
  62. data/lib/badline/chips/cpu/core.rb +18 -2
  63. data/lib/badline/chips/cpu/keyed_state.rb +177 -0
  64. data/lib/badline/chips/cpu/microcode.rb +16 -0
  65. data/lib/badline/chips/cpu/port.rb +20 -3
  66. data/lib/badline/chips/cpu.rb +5 -0
  67. data/lib/badline/chips/sid/decimator.rb +10 -8
  68. data/lib/badline/chips/sid/envelope.rb +1 -6
  69. data/lib/badline/chips/sid/filter.rb +0 -14
  70. data/lib/badline/chips/sid/keyed_parts.rb +286 -0
  71. data/lib/badline/chips/sid/voice.rb +0 -10
  72. data/lib/badline/chips/sid/waveform.rb +0 -2
  73. data/lib/badline/chips/sid.rb +1 -2
  74. data/lib/badline/chips/ted/beam.rb +119 -0
  75. data/lib/badline/chips/ted/timers.rb +66 -0
  76. data/lib/badline/chips/ted.rb +143 -0
  77. data/lib/badline/chips/tia.rb +49 -0
  78. data/lib/badline/chips/vdc/keyed_parts.rb +105 -0
  79. data/lib/badline/chips/vdc/painter.rb +0 -12
  80. data/lib/badline/chips/vdc/raster.rb +0 -16
  81. data/lib/badline/chips/vdc/window.rb +0 -9
  82. data/lib/badline/chips/vdc.rb +1 -2
  83. data/lib/badline/chips/via/control_lines.rb +0 -15
  84. data/lib/badline/chips/via/interrupt_register.rb +0 -9
  85. data/lib/badline/chips/via/keyed_parts.rb +163 -0
  86. data/lib/badline/chips/via/shift_register.rb +0 -14
  87. data/lib/badline/chips/via/timer1.rb +0 -13
  88. data/lib/badline/chips/via/timer2.rb +0 -18
  89. data/lib/badline/chips/via.rb +1 -2
  90. data/lib/badline/chips/vic/border_mask.rb +0 -10
  91. data/lib/badline/chips/vic/collisions.rb +0 -11
  92. data/lib/badline/chips/vic/color_patches.rb +0 -11
  93. data/lib/badline/chips/vic/display_state.rb +0 -19
  94. data/lib/badline/chips/vic/graphics_shifter.rb +0 -4
  95. data/lib/badline/chips/vic/keyed_parts.rb +228 -0
  96. data/lib/badline/chips/vic/keyed_sprites.rb +225 -0
  97. data/lib/badline/chips/vic/keyed_state.rb +102 -0
  98. data/lib/badline/chips/vic/register_log.rb +0 -18
  99. data/lib/badline/chips/vic/registers.rb +0 -9
  100. data/lib/badline/chips/vic/sequencer.rb +0 -2
  101. data/lib/badline/chips/vic/sprite/internal_bus.rb +0 -11
  102. data/lib/badline/chips/vic/sprite.rb +0 -2
  103. data/lib/badline/chips/vic/sprites.rb +0 -3
  104. data/lib/badline/chips/vic.rb +6 -2
  105. data/lib/badline/chips/vic_i/keyed_parts.rb +142 -0
  106. data/lib/badline/chips/vic_i/painter.rb +0 -4
  107. data/lib/badline/chips/vic_i/sound.rb +38 -46
  108. data/lib/badline/chips/vic_i.rb +2 -2
  109. data/lib/badline/chips/z80/core.rb +2 -2
  110. data/lib/badline/chips/z80/keyed_state.rb +73 -0
  111. data/lib/badline/chips.rb +10 -0
  112. data/lib/badline/core.rb +4 -21
  113. data/lib/badline/drives/datasette/keyed_state.rb +38 -0
  114. data/lib/badline/drives/datasette.rb +40 -7
  115. data/lib/badline/drives/engine/core.rb +3 -0
  116. data/lib/badline/drives/engine/serial_lines.rb +4 -7
  117. data/lib/badline/drives/model1541/bus.rb +0 -13
  118. data/lib/badline/drives/model1541/disk.rb +9 -8
  119. data/lib/badline/drives/model1541/keyed_state.rb +251 -0
  120. data/lib/badline/drives/model1541/mechanism.rb +0 -2
  121. data/lib/badline/drives/model1541.rb +1 -2
  122. data/lib/badline/drives/model1571/bus.rb +0 -13
  123. data/lib/badline/drives/model1571/keyed_state.rb +108 -0
  124. data/lib/badline/drives/model1571/wd1770.rb +0 -14
  125. data/lib/badline/drives/model1571.rb +1 -2
  126. data/lib/badline/drives/model1581/bus.rb +0 -13
  127. data/lib/badline/drives/model1581/disk.rb +0 -4
  128. data/lib/badline/drives/model1581/keyed_state.rb +280 -0
  129. data/lib/badline/drives/model1581/mechanism.rb +0 -21
  130. data/lib/badline/drives/model1581/slot.rb +0 -18
  131. data/lib/badline/drives/model1581/wd1772.rb +0 -2
  132. data/lib/badline/drives/model1581.rb +2 -2
  133. data/lib/badline/drives.rb +8 -0
  134. data/lib/badline/frontend/app.rb +3 -3
  135. data/lib/badline/frontend/boot.rb +1 -1
  136. data/lib/badline/frontend/controls.rb +19 -3
  137. data/lib/badline/frontend/drive_led.rb +44 -21
  138. data/lib/badline/frontend/menu/menu_dialogs.rb +2 -2
  139. data/lib/badline/frontend/snapshots.rb +12 -1
  140. data/lib/badline/frontend/timeline.rb +28 -10
  141. data/lib/badline/input/matrix_typist.rb +98 -0
  142. data/lib/badline/input.rb +1 -0
  143. data/lib/badline/kernal_trap/chrout.rb +45 -0
  144. data/lib/badline/kernal_trap/dos/channel.rb +0 -27
  145. data/lib/badline/kernal_trap/dos/channels.rb +0 -25
  146. data/lib/badline/kernal_trap/dos/memory.rb +0 -9
  147. data/lib/badline/kernal_trap/dos/status.rb +0 -8
  148. data/lib/badline/kernal_trap/dos/write_file.rb +0 -18
  149. data/lib/badline/kernal_trap/dos.rb +0 -19
  150. data/lib/badline/kernal_trap/file_routine.rb +4 -4
  151. data/lib/badline/kernal_trap/keyed_state.rb +150 -0
  152. data/lib/badline/kernal_trap/layout.rb +75 -11
  153. data/lib/badline/kernal_trap/load.rb +14 -16
  154. data/lib/badline/kernal_trap/routine.rb +12 -0
  155. data/lib/badline/kernal_trap/save.rb +8 -18
  156. data/lib/badline/kernal_trap/serial.rb +1 -17
  157. data/lib/badline/kernal_trap.rb +2 -0
  158. data/lib/badline/machine/keyboard_buffer.rb +49 -0
  159. data/lib/badline/machine/settling.rb +29 -0
  160. data/lib/badline/machine.rb +5 -2
  161. data/lib/badline/media/boot_disk.rb +14 -2
  162. data/lib/badline/media/extensions.rb +10 -0
  163. data/lib/badline/memory.rb +3 -6
  164. data/lib/badline/options/family.rb +5 -2
  165. data/lib/badline/options/help.rb +42 -16
  166. data/lib/badline/options/table.rb +2 -0
  167. data/lib/badline/options/validation.rb +2 -0
  168. data/lib/badline/options.rb +6 -1
  169. data/lib/badline/plus4/acia.rb +45 -0
  170. data/lib/badline/plus4/bus.rb +312 -0
  171. data/lib/badline/plus4/cpu.rb +10 -0
  172. data/lib/badline/plus4/cpu_port.rb +93 -0
  173. data/lib/badline/plus4/keyboard_port.rb +51 -0
  174. data/lib/badline/plus4/model.rb +38 -0
  175. data/lib/badline/plus4/user_port.rb +31 -0
  176. data/lib/badline/plus4.rb +164 -0
  177. data/lib/badline/roms/README +38 -0
  178. data/lib/badline/roms/plus4/basic.rom +0 -0
  179. data/lib/badline/roms/plus4/function-high.rom +0 -0
  180. data/lib/badline/roms/plus4/function-low.rom +0 -0
  181. data/lib/badline/roms/plus4/kernal-ntsc.rom +0 -0
  182. data/lib/badline/roms/plus4/kernal-pal.rom +0 -0
  183. data/lib/badline/snapshot/c128_setup.rb +9 -20
  184. data/lib/badline/snapshot/c64_setup.rb +28 -24
  185. data/lib/badline/snapshot/container.rb +4 -0
  186. data/lib/badline/snapshot/field_doc/markdown.rb +112 -0
  187. data/lib/badline/snapshot/field_doc.rb +109 -0
  188. data/lib/badline/snapshot/image.rb +134 -28
  189. data/lib/badline/snapshot/keyed/cursor.rb +40 -0
  190. data/lib/badline/snapshot/keyed/modules.rb +66 -0
  191. data/lib/badline/snapshot/keyed/reader.rb +188 -0
  192. data/lib/badline/snapshot/keyed/recorder.rb +88 -0
  193. data/lib/badline/snapshot/keyed/report.rb +26 -0
  194. data/lib/badline/snapshot/keyed/writer.rb +55 -0
  195. data/lib/badline/snapshot/keyed.rb +64 -0
  196. data/lib/badline/snapshot/machine_state.rb +17 -64
  197. data/lib/badline/snapshot/storage_setup.rb +29 -34
  198. data/lib/badline/snapshot/vic20_setup.rb +6 -15
  199. data/lib/badline/snapshot/vice/reu1764.rb +2 -2
  200. data/lib/badline/snapshot/vice.rb +7 -31
  201. data/lib/badline/snapshot.rb +43 -20
  202. data/lib/badline/storage/crt_file.rb +3 -1
  203. data/lib/badline/storage/g64_image.rb +8 -3
  204. data/lib/badline/storage/tap.rb +43 -15
  205. data/lib/badline/version.rb +1 -1
  206. data/lib/badline/vic20/attachments.rb +3 -0
  207. data/lib/badline/vic20/bus/keyed_state.rb +52 -0
  208. data/lib/badline/vic20/bus.rb +1 -2
  209. data/lib/badline/vic20/keyed_modules.rb +73 -0
  210. data/lib/badline/vic20/keyed_state.rb +45 -0
  211. data/lib/badline/vic20.rb +5 -5
  212. data/lib/badline.rb +1 -0
  213. metadata +61 -42
  214. data/lib/badline/c128/bus/saved_state.rb +0 -53
  215. data/lib/badline/c128/saved_state.rb +0 -117
  216. data/lib/badline/c64/bus/saved_state.rb +0 -68
  217. data/lib/badline/c64/cartridge/flash/saved_state.rb +0 -40
  218. data/lib/badline/c64/cartridge/retro_replay/saved_state.rb +0 -47
  219. data/lib/badline/c64/cartridge/saved_state.rb +0 -63
  220. data/lib/badline/c64/reu/dma/saved_state.rb +0 -52
  221. data/lib/badline/c64/reu/saved_state.rb +0 -55
  222. data/lib/badline/c64/saved_state.rb +0 -112
  223. data/lib/badline/chips/cia/saved_state.rb +0 -49
  224. data/lib/badline/chips/cia/serial/saved_state.rb +0 -45
  225. data/lib/badline/chips/cpu/saved_state.rb +0 -117
  226. data/lib/badline/chips/sid/envelope/saved_state.rb +0 -70
  227. data/lib/badline/chips/sid/filter/saved_state.rb +0 -42
  228. data/lib/badline/chips/sid/saved_state.rb +0 -52
  229. data/lib/badline/chips/sid/waveform/saved_state.rb +0 -68
  230. data/lib/badline/chips/vdc/saved_state.rb +0 -36
  231. data/lib/badline/chips/via/saved_state.rb +0 -44
  232. data/lib/badline/chips/vic/graphics_shifter/saved_state.rb +0 -40
  233. data/lib/badline/chips/vic/saved_state.rb +0 -82
  234. data/lib/badline/chips/vic/sequencer/saved_state.rb +0 -91
  235. data/lib/badline/chips/vic/sprite/saved_state.rb +0 -66
  236. data/lib/badline/chips/vic/sprites/saved_state.rb +0 -59
  237. data/lib/badline/chips/vic_i/painter/saved_state.rb +0 -40
  238. data/lib/badline/chips/vic_i/saved_state.rb +0 -55
  239. data/lib/badline/chips/z80/saved_state.rb +0 -71
  240. data/lib/badline/chrout_trap.rb +0 -43
  241. data/lib/badline/debug_register.rb +0 -28
  242. data/lib/badline/drives/datasette/saved_state.rb +0 -37
  243. data/lib/badline/drives/model1541/disk/saved_state.rb +0 -88
  244. data/lib/badline/drives/model1541/mechanism/saved_state.rb +0 -72
  245. data/lib/badline/drives/model1541/saved_state.rb +0 -44
  246. data/lib/badline/drives/model1571/saved_state.rb +0 -47
  247. data/lib/badline/drives/model1581/disk/saved_state.rb +0 -89
  248. data/lib/badline/drives/model1581/saved_state.rb +0 -42
  249. data/lib/badline/drives/model1581/wd1772/saved_state.rb +0 -68
  250. data/lib/badline/keyboard_buffer.rb +0 -33
  251. data/lib/badline/snapshot/setup.rb +0 -31
  252. data/lib/badline/snapshot/state.rb +0 -179
  253. data/lib/badline/vic20/bus/saved_state.rb +0 -35
  254. data/lib/badline/vic20/saved_state.rb +0 -110
@@ -1,24 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "badline/drives/datasette/saved_state"
3
+ require "badline/drives/datasette/keyed_state"
4
4
 
5
5
  module Badline
6
6
  module Drives
7
7
  # The 1530 datasette. Plays a pulse stream into CIA 1's FLAG pin while the
8
8
  # motor runs with a key pressed, and reports the keys through the cassette
9
9
  # sense line on the $01 port. With RECORD down too, it records the
10
- # machine's write line onto the tape instead.
10
+ # machine's write line onto the tape instead. Built with +level: true+,
11
+ # it also times the read line's level, for a machine that reads that
12
+ # (#read_high?) rather than the falling edges.
11
13
  class Datasette
12
- include SavedState
13
-
14
14
  # What inserting a tape raises on a machine without a cassette port.
15
15
  class Missing < ArgumentError; end
16
-
17
16
  attr_reader :tape
18
17
  attr_writer :motor
19
18
 
20
- def initialize(tape = nil)
19
+ def initialize(tape = nil, level: false)
21
20
  @tape = tape
21
+ @level = level
22
22
  @connected = true
23
23
  @playing = false
24
24
  @recording = false
@@ -26,6 +26,7 @@ module Badline
26
26
  @since = 0
27
27
  @motor = false
28
28
  @countdown = 0
29
+ @rise_at = 0
29
30
  @flag_handler = nil
30
31
  @sense_handler = nil
31
32
  end
@@ -64,6 +65,7 @@ module Badline
64
65
  def rewind
65
66
  @tape&.rewind
66
67
  @countdown = 0
68
+ @rise_at = 0
67
69
  end
68
70
 
69
71
  def play! = press(true)
@@ -93,6 +95,12 @@ module Badline
93
95
  @since = 0
94
96
  end
95
97
 
98
+ # Sets the write line's level without recording an edge, as a
99
+ # snapshot loads it.
100
+ def write_level=(high)
101
+ @write_high = high
102
+ end
103
+
96
104
  def playing? = @playing
97
105
 
98
106
  # Sense reads low while a key is down on the deck.
@@ -102,11 +110,22 @@ module Badline
102
110
 
103
111
  def running? = @playing && @motor && !@tape.nil?
104
112
 
113
+ # The tape read line's level, on a datasette built with +level: true+.
114
+ # Each pulse holds it low for its first half and high for its second,
115
+ # up to the falling edge that ends it. A version 2 tape times each
116
+ # half, and a version 0 or 1 tape's pulse plays as two equal halves.
117
+ # It reads high while the tape isn't playing.
118
+ def read_high?
119
+ return true unless running? && !@recording
120
+
121
+ @countdown.positive? && @countdown <= @rise_at
122
+ end
123
+
105
124
  def cycle!
106
125
  return unless running?
107
126
  return @since += 1 if @recording
108
127
 
109
- @countdown = @tape.next_pulse.to_i if @countdown.zero?
128
+ next_pulse if @countdown.zero?
110
129
  return if @countdown.zero?
111
130
 
112
131
  @countdown -= 1
@@ -120,6 +139,20 @@ module Badline
120
139
 
121
140
  private
122
141
 
142
+ def next_pulse
143
+ return @countdown = @tape.next_pulse.to_i unless @level
144
+ return next_half_waves if @tape.half_waves?
145
+
146
+ @countdown = @tape.next_pulse.to_i
147
+ @rise_at = @countdown / 2
148
+ end
149
+
150
+ def next_half_waves
151
+ first = @tape.next_half_wave.to_i
152
+ @rise_at = @tape.next_half_wave.to_i
153
+ @countdown = first + @rise_at
154
+ end
155
+
123
156
  def press(down)
124
157
  return if down == @playing || !@connected
125
158
 
@@ -86,6 +86,9 @@ module Badline
86
86
  # is the same at the end of every pass the drive sleeps through.
87
87
  def led_on? = @mechanism.led_on?
88
88
 
89
+ # The power LED, which stays lit.
90
+ def power_led_on? = true
91
+
89
92
  # Stores what the head wrote since the motor last stopped in the
90
93
  # disk's image, as the motor stopping does.
91
94
  def flush
@@ -38,14 +38,11 @@ module Badline
38
38
  @host = @bus.host_lines
39
39
  end
40
40
 
41
- # The host's lines as last latched. The jumpers and the bus are the
42
- # drive's wiring.
43
- def save_state(out)
44
- out.int(@host)
45
- end
41
+ # The host's lines as the port latched them, under `prefix`.
42
+ def save_fields(out, prefix) = out.int("#{prefix}host", @host)
46
43
 
47
- def load_state(input)
48
- @host = input.int
44
+ def load_fields(input, prefix)
45
+ @host = input.int("#{prefix}host", @host)
49
46
  end
50
47
 
51
48
  # Whether the host has moved ATN since the last latch_host.
@@ -38,19 +38,6 @@ module Badline
38
38
  init_watch
39
39
  end
40
40
 
41
- # The last byte on the data bus and the RAM. What a watched stretch
42
- # touched is Idle's, and a restored bus isn't watching.
43
- def save_state(out)
44
- out.int(@data)
45
- @ram.save_state(out)
46
- end
47
-
48
- def load_state(input)
49
- @data = input.int
50
- @ram.load_state(input)
51
- @watching = false
52
- end
53
-
54
41
  def peek(addr)
55
42
  return @data = @rom.peek(addr | 0x4000) if addr >= 0x8000
56
43
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "badline/media/extensions"
3
4
  require "badline/drives/model1541/sector_reader"
4
- require "badline/drives/model1541/disk/saved_state"
5
5
 
6
6
  module Badline
7
7
  module Drives
@@ -23,7 +23,8 @@ module Badline
23
23
  # host file. The disk is write-protected when the image won't take
24
24
  # writes.
25
25
  class Disk
26
- include SavedState
26
+ # Whether the image is a .g64 or .g71, by its name.
27
+ def self.g64?(path) = Media::Extensions.kind(path) == :gcr
27
28
 
28
29
  MAX_HALF_TRACK = 84
29
30
 
@@ -61,16 +62,16 @@ module Badline
61
62
  # formatted. `read_only` opens the image write-protected.
62
63
  def self.open(path, read_only: false)
63
64
  image = image_for(path, read_only)
64
- (SavedState.g64?(path) ? from_g64(image) : from_d64(image)).opened(path, read_only)
65
+ (Disk.g64?(path) ? from_g64(image) : from_d64(image)).opened(path, read_only)
65
66
  end
66
67
 
67
68
  # The image at +path+, by its name: a .g64 or .g71, a .d71, and
68
- # anything else a .d64.
69
- def self.image_for(path, read_only)
70
- return Storage::G64Image.new(path, read_only:) if SavedState.g64?(path)
71
- return Storage::D71Image.new(path, read_only:) if File.extname(path).casecmp?(".d71")
69
+ # anything else a .d64. `bytes` stands in for the file's contents.
70
+ def self.image_for(path, read_only, bytes = nil)
71
+ return Storage::G64Image.new(path, read_only:, bytes:) if Disk.g64?(path)
72
+ return Storage::D71Image.new(path, read_only:, bytes:) if File.extname(path).casecmp?(".d71")
72
73
 
73
- Storage::D64Image.new(path, read_only:)
74
+ Storage::D64Image.new(path, read_only:, bytes:)
74
75
  end
75
76
 
76
77
  # A disk from a G64 or G71 image, each half track as the image stores
@@ -0,0 +1,251 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model1541
6
+ class Disk
7
+ # The kinds of image a disk embeds, by name.
8
+ IMAGE_KINDS = %w[none d64 d71 g64].freeze
9
+
10
+ # A disk's fields in a keyed snapshot module (BL-D8-DISK): the image
11
+ # file's path and how it was opened, the image as last flushed,
12
+ # embedded under image and image.kind, each half track that differs
13
+ # from what the image decodes to, as track<H>.zone, track<H>.speeds
14
+ # and track<H>.gcr, raw GCR as the head sees it, and the half tracks
15
+ # written since the last flush.
16
+ module KeyedState
17
+ # The image's path, expanded, and whether it was opened read-only,
18
+ # for a disk from Disk.open. Nil for a disk made another way.
19
+ attr_reader :path, :read_only
20
+
21
+ # Notes the image's path and how it was opened.
22
+ def opened(path, read_only)
23
+ @path = File.expand_path(path)
24
+ @read_only = read_only
25
+ self
26
+ end
27
+
28
+ def save_fields(out)
29
+ path = @path
30
+ out.text("path", path) if path
31
+ out.bool("read_only", @read_only == true)
32
+ image = @image
33
+ decoded = nil
34
+ if image
35
+ out.text("image.kind", Disk.image_kind(image)).text("image.path", image.path.to_s)
36
+ out.bytes("image", image.contents)
37
+ decoded = Disk.decode(image)
38
+ end
39
+ @tracks.each_with_index { |track, half| save_track_fields(out, half, track, decoded) }
40
+ out.ints("written", @written.keys)
41
+ end
42
+
43
+ def load_track_fields(input)
44
+ @tracks.each_index do |half|
45
+ zone = input.optional_int("track#{half}.zone")
46
+ next if zone.nil?
47
+
48
+ speeds = input.ints("track#{half}.speeds", [])
49
+ gcr = input.bytes("track#{half}.gcr", "".b).bytes
50
+ @tracks[half] = Track.new(gcr, zone, speeds.empty? ? nil : speeds.dup)
51
+ end
52
+ @written.clear
53
+ input.ints("written", []).each { |half| @written[half] = true }
54
+ self
55
+ end
56
+
57
+ private
58
+
59
+ def save_track_fields(out, half, track, decoded)
60
+ return if track.nil?
61
+
62
+ original = decoded&.track(half)
63
+ return if original && original.zone == track.zone && original.speeds == track.speeds &&
64
+ original.bytes == track.bytes
65
+
66
+ out.int("track#{half}.zone", track.zone)
67
+ speeds = track.speeds
68
+ out.ints("track#{half}.speeds", speeds) if speeds
69
+ out.bytes("track#{half}.gcr", track.bytes)
70
+ end
71
+ end
72
+
73
+ include KeyedState
74
+
75
+ # The kind of `image`, by name.
76
+ def self.image_kind(image)
77
+ return "g64" if image.is_a?(Storage::G64Image)
78
+ return "d71" if image.is_a?(Storage::D71Image)
79
+
80
+ "d64"
81
+ end
82
+
83
+ # The tracks `image` holds, as an insert decodes them.
84
+ def self.decode(image) = image.is_a?(Storage::G64Image) ? from_g64(image) : from_d64(image)
85
+
86
+ # The disk a keyed snapshot module describes: its embedded image
87
+ # decoded, with the tracks the module holds laid over it. The image
88
+ # takes writes back to its file only while that file is there and
89
+ # the reader isn't `detached`. With no image, the disk is the
90
+ # tracks alone, write-protected.
91
+ def self.from_fields(input, detached: false)
92
+ path = input.text("path", "")
93
+ read_only = input.bool("read_only", false)
94
+ kind = input.choice("image.kind", IMAGE_KINDS, "none")
95
+ image_path = input.text("image.path", "")
96
+ disk = kind == "none" ? Disk.new : from_image(input, kind, image_path, read_only || detached)
97
+ disk.opened(path, read_only || kind == "none") unless path.empty?
98
+ disk.load_track_fields(input)
99
+ end
100
+
101
+ def self.from_image(input, kind, path, read_only)
102
+ bytes = input.bytes("image", "".b).bytes
103
+ writable_path = path.empty? || !File.file?(path) ? "" : path
104
+ image = case kind
105
+ when "g64" then Storage::G64Image.new(writable_path, read_only:, bytes:)
106
+ when "d71" then Storage::D71Image.new(writable_path, read_only:, bytes:)
107
+ else Storage::D64Image.new(writable_path, read_only:, bytes:)
108
+ end
109
+ decode(image)
110
+ end
111
+ end
112
+
113
+ class Mechanism
114
+ # The head, the motor, the stepper and the read and write
115
+ # electronics in a keyed snapshot module (BL-D8-MECH): where the
116
+ # head is, the disk's place in its turn, the read clock's next
117
+ # clock in cycles from now, the shift registers and the flags. The
118
+ # cell under the head, its width and the next event come from the
119
+ # track and the time on load, the cell period from the zone, and
120
+ # the write protection from the disk.
121
+ module KeyedState
122
+ def save_fields(out)
123
+ out.int("half_track", @half_track).int("slip", @slip).int("side", @side_base).int("zone", @zone)
124
+ out.int("time", @time).int("read_clock_in", @clock_at - @time).int("clocks", @clocks)
125
+ out.int("shift", @shift).int("ones", @ones).int("bits", @bits).int("write_shift", @write_shift)
126
+ out.bool("motor", @motor).bool("led", @led).bool("sync", @sync).bool("byte_ready", @byte_ready)
127
+ out.bool("byte_latched", @byte_latched).bool("writing", @writing).bool("write_gate", @write_gate)
128
+ out.bool("track_written", @track_written).optional_int("write_index", @write_index)
129
+ end
130
+
131
+ # Puts `disk` under the head without flushing the one there, as a
132
+ # snapshot load replaces it.
133
+ def load_fields(input, disk)
134
+ @disk = disk
135
+ load_head_fields(input)
136
+ load_flag_fields(input)
137
+ @clock = Track.cell(@zone)
138
+ @protected = disk&.write_protected? || false
139
+ track_written = input.bool("track_written", false)
140
+ write_index = input.optional_int("write_index")
141
+ load_track(@time)
142
+ @track_written = track_written
143
+ @write_index = write_index
144
+ end
145
+
146
+ private
147
+
148
+ def load_head_fields(input)
149
+ @half_track = input.int("half_track", @half_track)
150
+ @slip = input.masked("slip", @slip, 0x03)
151
+ @side_base = input.int("side", @side_base)
152
+ @zone = input.masked("zone", @zone, 0x03)
153
+ @time = input.int("time", @time)
154
+ @clock_at = @time + input.int("read_clock_in", @clock_at - @time)
155
+ @clocks = input.int("clocks", @clocks)
156
+ @shift = input.int("shift", @shift)
157
+ @ones = input.int("ones", @ones)
158
+ @bits = input.int("bits", @bits)
159
+ @write_shift = input.int("write_shift", @write_shift)
160
+ end
161
+
162
+ def load_flag_fields(input)
163
+ @motor = input.bool("motor", @motor)
164
+ @led = input.bool("led", @led)
165
+ @sync = input.bool("sync", @sync)
166
+ @byte_ready = input.bool("byte_ready", @byte_ready)
167
+ @byte_latched = input.bool("byte_latched", @byte_latched)
168
+ @writing = input.bool("writing", @writing)
169
+ @write_gate = input.bool("write_gate", @write_gate)
170
+ end
171
+ end
172
+
173
+ include KeyedState
174
+ end
175
+
176
+ class Bus
177
+ # The drive's RAM and the byte last on its data bus, in a keyed
178
+ # snapshot module (BL-D8-RAM).
179
+ def save_fields(out)
180
+ out.int("data", @data).bytes("ram", @ram.read(0, @ram.length))
181
+ end
182
+
183
+ def load_fields(input)
184
+ @data = input.byte("data", @data)
185
+ ram = @ram.read(0, @ram.length)
186
+ @ram.write(0, input.bytes_into("ram", ram))
187
+ @watching = false
188
+ end
189
+ end
190
+
191
+ # The drive's own fields in a keyed snapshot module (BL-D8): its model,
192
+ # its clock phase and cycle count, the SO pulse due, and the host's
193
+ # lines as the serial port latched them. A save settles the drive first, so it is awake;
194
+ # a load forgets its orbits.
195
+ module KeyedState
196
+ def save_fields(out)
197
+ settle!
198
+ out.text("model", model_name).int("phase", @phase).int("cycles", @cycles)
199
+ out.bool("so_pending", @mechanism.so_pending)
200
+ @serial_port.save_fields(out, "serial.")
201
+ out
202
+ end
203
+
204
+ def load_fields(input)
205
+ settle!
206
+ input.choice("model", [model_name], model_name)
207
+ @phase = input.int("phase", @phase)
208
+ @cycles = input.int("cycles", @cycles)
209
+ @mechanism.so_pending = input.bool("so_pending", @mechanism.so_pending)
210
+ @serial_port.load_fields(input, "serial.")
211
+ end
212
+
213
+ def restored! = forget_orbits
214
+
215
+ # The drive as modules of its own, named from `name` (BL-D8 for
216
+ # device 8): its own fields, its RAM, each VIA, the CPU, the
217
+ # mechanism and the disk, when one is in.
218
+ def save_modules(sections, name)
219
+ sections << Snapshot::Keyed.section(name) { |out| save_fields(out) }
220
+ sections << Snapshot::Keyed.section("#{name}-RAM") { |out| @bus.save_fields(out) }
221
+ sections << Snapshot::Keyed.section("#{name}-VIA1") { |out| @via1.save_fields(out) }
222
+ sections << Snapshot::Keyed.section("#{name}-VIA2") { |out| @via2.save_fields(out) }
223
+ sections << Snapshot::Keyed.section("#{name}-CPU") { |out| @cpu.save_fields(out) }
224
+ sections << Snapshot::Keyed.section("#{name}-MECH") { |out| @mechanism.save_fields(out) }
225
+ disk = @mechanism.disk
226
+ sections << Snapshot::Keyed.section("#{name}-DISK") { |out| disk.save_fields(out) } if disk
227
+ sections
228
+ end
229
+
230
+ # The drive from its modules, `input` being its own. No disk module
231
+ # means no disk in the drive.
232
+ def load_modules(modules, input, name)
233
+ load_fields(input)
234
+ @bus.load_fields(modules.required("#{name}-RAM"))
235
+ @via1.load_fields(modules.required("#{name}-VIA1"))
236
+ @via2.load_fields(modules.required("#{name}-VIA2"))
237
+ @cpu.load_fields(modules.required("#{name}-CPU"))
238
+ disk = modules.optional("#{name}-DISK")
239
+ mechanism = modules.required("#{name}-MECH")
240
+ @mechanism.load_fields(mechanism, disk.nil? ? nil : Disk.from_fields(disk, detached: modules.detached?))
241
+ @via1.restored!
242
+ @via2.restored!
243
+ @cpu.restored!
244
+ restored!
245
+ end
246
+ end
247
+
248
+ include KeyedState
249
+ end
250
+ end
251
+ end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "badline/drives/model1541/rotation"
4
- require "badline/drives/model1541/mechanism/saved_state"
5
4
  require "badline/drives/model1541/byte_ready"
6
5
 
7
6
  module Badline
@@ -71,7 +70,6 @@ module Badline
71
70
  # its track 0 sensor looks for the head (Mechanism.new's +slip+).
72
71
  class Mechanism
73
72
  include Rotation
74
- include SavedState
75
73
  include ByteReady
76
74
 
77
75
  MOTOR = 0x04
@@ -9,7 +9,7 @@ require "badline/drives/model1541/track"
9
9
  require "badline/drives/model1541/disk"
10
10
  require "badline/drives/model1541/mechanism"
11
11
  require "badline/drives/model1541/disk_via"
12
- require "badline/drives/model1541/saved_state"
12
+ require "badline/drives/model1541/keyed_state"
13
13
 
14
14
  module Badline
15
15
  module Drives
@@ -19,7 +19,6 @@ module Badline
19
19
  class Model1541
20
20
  include Engine::Core
21
21
  include Engine::VIAs
22
- include SavedState
23
22
 
24
23
  CLOCK_HZ = 1_000_000
25
24
 
@@ -40,19 +40,6 @@ module Badline
40
40
  init_watch
41
41
  end
42
42
 
43
- # The last byte on the data bus and the RAM. What a watched stretch
44
- # touched is Idle's, and a restored bus isn't watching.
45
- def save_state(out)
46
- out.int(@data)
47
- @ram.save_state(out)
48
- end
49
-
50
- def load_state(input)
51
- @data = input.int
52
- @ram.load_state(input)
53
- @watching = false
54
- end
55
-
56
43
  def peek(addr)
57
44
  return @data = @rom.peek(addr) if addr >= 0x8000
58
45
  return @data = chip_peek(addr) if addr >= 0x2000
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model1571
6
+ class WD1770
7
+ # The controller's registers, status and result in a keyed snapshot
8
+ # module (BL-D8-FDC), and how many of the drive CPU's cycles until a
9
+ # command under way finishes. It loads after the CPU, which that
10
+ # counts from.
11
+ def save_fields(out)
12
+ out.ints("registers", @registers).int("status", @status).int("result", @result)
13
+ out.int("due_in", @due - @cpu.cycles)
14
+ end
15
+
16
+ def load_fields(input)
17
+ input.ints_into("registers", @registers)
18
+ @status = input.byte("status", @status)
19
+ @result = input.int("result", @result)
20
+ @due = @cpu.cycles + input.int("due_in", @due - @cpu.cycles)
21
+ end
22
+ end
23
+
24
+ class Bus
25
+ # The drive's RAM and the byte last on its data bus, in a keyed
26
+ # snapshot module (BL-D8-RAM).
27
+ def save_fields(out)
28
+ out.int("data", @data).bytes("ram", @ram.read(0, @ram.length))
29
+ end
30
+
31
+ def load_fields(input)
32
+ @data = input.byte("data", @data)
33
+ ram = @ram.read(0, @ram.length)
34
+ @ram.write(0, input.bytes_into("ram", ram))
35
+ @watching = false
36
+ end
37
+ end
38
+
39
+ # The drive's own fields in a keyed snapshot module (BL-D8), as the
40
+ # 1541's. The clock rate, the side and the fast serial direction come
41
+ # from VIA 1's port A once the VIAs are loaded.
42
+ module KeyedState
43
+ def save_fields(out)
44
+ settle!
45
+ out.text("model", model_name).int("phase", @phase).int("cycles", @cycles)
46
+ out.bool("so_pending", @mechanism.so_pending)
47
+ @serial_port.save_fields(out, "serial.")
48
+ out
49
+ end
50
+
51
+ def load_fields(input)
52
+ settle!
53
+ input.choice("model", [model_name], model_name)
54
+ @phase = input.int("phase", @phase)
55
+ @cycles = input.int("cycles", @cycles)
56
+ @mechanism.so_pending = input.bool("so_pending", @mechanism.so_pending)
57
+ @serial_port.load_fields(input, "serial.")
58
+ end
59
+
60
+ def restored!
61
+ @fast_serial_out = @via1.port_a_output.anybits?(FAST_SERIAL_OUT)
62
+ port_a_written(@via1.port_a_output)
63
+ push_fast_output(@fast_serial_out ? fast_pins : 0)
64
+ forget_orbits
65
+ end
66
+
67
+ # The drive as modules of its own, as the 1541's, with the CIA and
68
+ # the WD1770.
69
+ def save_modules(sections, name)
70
+ sections << Snapshot::Keyed.section(name) { |out| save_fields(out) }
71
+ sections << Snapshot::Keyed.section("#{name}-RAM") { |out| @bus.save_fields(out) }
72
+ sections << Snapshot::Keyed.section("#{name}-VIA1") { |out| @via1.save_fields(out) }
73
+ sections << Snapshot::Keyed.section("#{name}-VIA2") { |out| @via2.save_fields(out) }
74
+ sections << Snapshot::Keyed.section("#{name}-CIA") { |out| @cia.save_fields(out) }
75
+ sections << Snapshot::Keyed.section("#{name}-CPU") { |out| @cpu.save_fields(out) }
76
+ sections << Snapshot::Keyed.section("#{name}-FDC") { |out| @fdc.save_fields(out) }
77
+ sections << Snapshot::Keyed.section("#{name}-MECH") { |out| @mechanism.save_fields(out) }
78
+ disk = @mechanism.disk
79
+ sections << Snapshot::Keyed.section("#{name}-DISK") { |out| disk.save_fields(out) } if disk
80
+ sections
81
+ end
82
+
83
+ # The drive from its modules, `input` being its own. The WD1770
84
+ # counts from the CPU's cycles, so it loads after the CPU.
85
+ def load_modules(modules, input, name)
86
+ load_fields(input)
87
+ @bus.load_fields(modules.required("#{name}-RAM"))
88
+ @via1.load_fields(modules.required("#{name}-VIA1"))
89
+ @via2.load_fields(modules.required("#{name}-VIA2"))
90
+ @cia.load_fields(modules.required("#{name}-CIA"))
91
+ @cpu.load_fields(modules.required("#{name}-CPU"))
92
+ @fdc.load_fields(modules.required("#{name}-FDC"))
93
+ disk = modules.optional("#{name}-DISK")
94
+ mechanism = modules.required("#{name}-MECH")
95
+ @mechanism.load_fields(mechanism,
96
+ disk.nil? ? nil : Model1541::Disk.from_fields(disk, detached: modules.detached?))
97
+ @via1.restored!
98
+ @via2.restored!
99
+ @cia.restored!
100
+ @cpu.restored!
101
+ restored!
102
+ end
103
+ end
104
+
105
+ include KeyedState
106
+ end
107
+ end
108
+ end
@@ -54,20 +54,6 @@ module Badline
54
54
  offset.zero? ? command(value) : @registers[offset] = value
55
55
  end
56
56
 
57
- def save_state(out)
58
- out.ints([*@registers, @status, @due, @result])
59
- end
60
-
61
- # A state from before the controller ran commands holds the four
62
- # registers alone.
63
- def load_state(input)
64
- values = input.ints
65
- @registers = values[0, 4]
66
- @status = values[4] || 0
67
- @due = values[5] || 0
68
- @result = values[6] || 0
69
- end
70
-
71
57
  private
72
58
 
73
59
  def command(value)
@@ -5,7 +5,7 @@ require "badline/drives/model1571/wd1770"
5
5
  require "badline/drives/model1571/serial_port"
6
6
  require "badline/drives/model1571/serial_via"
7
7
  require "badline/drives/model1571/bus"
8
- require "badline/drives/model1571/saved_state"
8
+ require "badline/drives/model1571/keyed_state"
9
9
 
10
10
  module Badline
11
11
  module Drives
@@ -28,7 +28,6 @@ module Badline
28
28
  include Engine::Core
29
29
  include Engine::VIAs
30
30
  include Engine::FastSerial
31
- include SavedState
32
31
 
33
32
  CLOCK_HZ = 1_000_000
34
33
  FAST_CLOCK_HZ = 2_000_000
@@ -41,19 +41,6 @@ module Badline
41
41
  @touched_timers = 0
42
42
  end
43
43
 
44
- # The last byte on the data bus and the RAM. What a watched stretch
45
- # touched is Idle's, and a restored bus isn't watching.
46
- def save_state(out)
47
- out.int(@data)
48
- @ram.save_state(out)
49
- end
50
-
51
- def load_state(input)
52
- @data = input.int
53
- @ram.load_state(input)
54
- @watching = false
55
- end
56
-
57
44
  def peek(addr)
58
45
  return @data = @rom.peek(addr) if addr >= 0x8000
59
46
  return @data = chip_peek(addr) if addr >= 0x2000