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
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zlib"
4
+ require "badline/snapshot/container"
5
+
6
+ module Badline
7
+ module Snapshot
8
+ # A snapshot module as keyed, typed records, which a later badline
9
+ # reads by name: one it doesn't know is skipped, and one that isn't
10
+ # there leaves the value a freshly built machine has.
11
+ #
12
+ # The payload is deflated. Inside, each record is the key's length and
13
+ # the key (ASCII, lower snake case with dots between sub-units), a
14
+ # one-byte type tag, then the value's length and the value. Lengths are
15
+ # BER-compressed (Ruby's pack "w"), and so are integers, each folded to
16
+ # a natural number (0, -1, 1, -2 ... as 0, 1, 2, 3 ...). The value's
17
+ # length lets a reader skip a tag it doesn't know.
18
+ #
19
+ # A module's header version is MAJOR.MINOR. The major number is this
20
+ # record encoding, and a reader refuses another one. Fields carry no
21
+ # version: a changed meaning gets a new key.
22
+ module Keyed
23
+ MAJOR = 1
24
+ MINOR = 0
25
+
26
+ INT = 0x01
27
+ BOOL = 0x02
28
+ BYTES = 0x03
29
+ INTS = 0x04
30
+ TEXT = 0x05
31
+
32
+ KEY = /\A[a-z0-9_]+(\.[a-z0-9_]+)*\z/
33
+
34
+ # The values an int record holds: what a 64-bit integer holds once
35
+ # zigzag doubles it, which a native build's integers can't exceed.
36
+ VALUES = (-(1 << 62))..((1 << 62) - 1)
37
+
38
+ module_function
39
+
40
+ def ber(number) = [number].pack("w")
41
+
42
+ # Folds an Integer in VALUES to a natural number, as a
43
+ # native build's 64-bit integers hold it.
44
+ def zigzag(value) = value.negative? ? ((-value - 1) << 1) | 1 : value << 1
45
+
46
+ def unzigzag(folded) = folded.odd? ? -(folded >> 1) - 1 : folded >> 1
47
+
48
+ # The module `name`, with the records the block writes to the Writer
49
+ # it yields.
50
+ def section(name)
51
+ out = Writer.new
52
+ yield out
53
+ out.section(name)
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ require "badline/snapshot/keyed/cursor"
60
+ require "badline/snapshot/keyed/writer"
61
+ require "badline/snapshot/keyed/report"
62
+ require "badline/snapshot/keyed/reader"
63
+ require "badline/snapshot/keyed/recorder"
64
+ require "badline/snapshot/keyed/modules"
@@ -2,83 +2,36 @@
2
2
 
3
3
  module Badline
4
4
  module Snapshot
5
- # The BADLINE module: a State from C64#snapshot, which restores the
6
- # machine exactly where it was saved. VICE skips a module it doesn't
7
- # know.
5
+ # The BADLINE module badline 0.13 and earlier saved the machine in, a
6
+ # positional layout only the version that wrote it reads. All that is
7
+ # left of it is reading which version that was, for the refusal
8
+ # (Image#load).
8
9
  #
9
10
  # The payload is deflated. Inside, a little-endian 32-bit length leads
10
11
  # a run of BER-compressed integers (Ruby's pack "w"): the number of
11
- # values and of strings, each string's length, then the values, each
12
- # folded to a natural number (0, -1, 1, -2 ... as 0, 1, 2, 3 ...). The
13
- # strings follow, one after another.
14
- #
15
- # A State only restores in the badline version that wrote it, since
16
- # each chip writes its fields as it holds them.
12
+ # values and of strings, each string's length, then the values. The
13
+ # strings follow, one after another: the family's marker first, then
14
+ # the badline version.
17
15
  module MachineState
18
16
  NAME = "BADLINE"
19
- MAJOR = 2
20
- MINOR = 0
21
-
22
- # The values a 64-bit integer holds once encode doubles them, which a
23
- # native build's integers can't exceed.
24
- VALUES = (-(1 << 62))..((1 << 62) - 1)
25
17
 
26
18
  module_function
27
19
 
28
- def section(state)
29
- Section.new(name: NAME, major: MAJOR, minor: MINOR, data: Zlib.deflate(encode(state)))
30
- end
31
-
32
- def state(section)
33
- unless section.major == MAJOR
34
- raise FormatError, "#{NAME} #{section.version} was written by another badline version, " \
35
- "and this one reads #{MAJOR}.#{MINOR}"
36
- end
37
-
38
- decode(Zlib.inflate(section.data))
39
- rescue Zlib::Error => e
40
- raise FormatError, "#{NAME} is damaged: #{e.message}"
41
- end
42
-
43
- def encode(state)
44
- values = state.values
45
- strings = state.strings
46
- numbers = [values.length, strings.length]
47
- strings.each { |string| numbers << string.bytesize }
48
- values.each do |value|
49
- raise RangeError, "#{value} is too large for a snapshot" unless VALUES.cover?(value)
50
-
51
- numbers << (value.negative? ? (-2 * value) - 1 : 2 * value)
52
- end
53
- index = numbers.pack("w*")
54
- [index.bytesize].pack("V") + index + strings.join
55
- end
56
-
57
- def decode(bytes)
58
- raise FormatError, "#{NAME} ends early" if bytes.bytesize < 4
20
+ # The badline version that saved the module, or an empty string when
21
+ # it is too damaged to say.
22
+ def written_by(section)
23
+ bytes = Zlib.inflate(section.data)
24
+ return "" if bytes.bytesize < 4
59
25
 
60
26
  length = bytes.byteslice(0, 4).unpack1("V").to_i
61
27
  numbers = bytes.byteslice(4, length).to_s.unpack("w*").map(&:to_i)
62
- value_count = numbers.fetch(0, 0)
63
28
  string_count = numbers.fetch(1, 0)
64
- raise FormatError, "#{NAME} ends early" if numbers.length != 2 + string_count + value_count
65
-
66
- State.new(values(numbers, 2 + string_count), strings(bytes, 4 + length, numbers.slice(2, string_count)))
67
- end
68
-
69
- def values(numbers, from)
70
- Array.new(numbers.length - from) do |i|
71
- folded = numbers[from + i]
72
- folded.odd? ? -((folded + 1) / 2) : folded / 2
73
- end
74
- end
75
-
76
- def strings(bytes, pos, lengths)
77
- lengths.map do |length|
78
- raise FormatError, "#{NAME} ends early" if pos + length > bytes.bytesize
29
+ return "" if string_count < 2 || numbers.length < 4
79
30
 
80
- bytes.byteslice(pos, length).to_s.tap { pos += length }
81
- end
31
+ pos = 4 + length + numbers[2]
32
+ bytes.byteslice(pos, numbers[3]).to_s
33
+ rescue Zlib::Error
34
+ ""
82
35
  end
83
36
  end
84
37
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Badline
4
4
  module Snapshot
5
- # The storage device 8 serves through the traps, as a State holds it:
5
+ # The storage device 8 serves through the traps, as a snapshot holds it:
6
6
  # its kind, its path and, for an archive or a disk image, its bytes, so
7
7
  # it opens again without the host file.
8
8
  module StorageSetup
@@ -14,23 +14,6 @@ module Badline
14
14
 
15
15
  module_function
16
16
 
17
- # The same directory by its path, an archive with the bytes it held,
18
- # or a disk image with its path, whether it is write-protected, and
19
- # its bytes with the error table after them.
20
- def write(storage, out)
21
- if storage.is_a?(Storage::HostDirectory)
22
- out.int(HOST_DIRECTORY).string(File.expand_path(storage.path))
23
- elsif storage.is_a?(Storage::T64)
24
- out.int(T64_ARCHIVE).string(File.expand_path(storage.path)).blob(storage.bytes)
25
- else
26
- write_image(storage, out)
27
- end
28
- end
29
-
30
- def write_image(image, out)
31
- out.int(image_kind(image)).string(File.expand_path(image.path)).boolean(image.read_only?).blob(image.contents)
32
- end
33
-
34
17
  def image_kind(image)
35
18
  return D71 if image.is_a?(Storage::D71Image)
36
19
  return D64 if image.is_a?(Storage::D64Image)
@@ -39,28 +22,40 @@ module Badline
39
22
  raise ArgumentError, "#{image.class} can't go in a snapshot"
40
23
  end
41
24
 
42
- # Opens the storage `write` wrote: the same directory by its path, an
43
- # archive with the bytes it held, or a disk image with the bytes it
44
- # held, writes and all. A detached reader gets the image
45
- # write-protected, and Storage::Unavailable for a directory.
46
- def read(input)
47
- kind = input.int
48
- path = input.string
49
- case kind
50
- when HOST_DIRECTORY then input.detached? ? Storage::Unavailable.new : Storage::HostDirectory.new(path)
51
- when T64_ARCHIVE then Storage::T64.new(path, bytes: input.blob)
52
- else read_image(kind, path, input)
25
+ # The storage's kind by name, in a keyed snapshot module, under
26
+ # storage.*: its path, and for an archive or a disk image whether it
27
+ # is write-protected and its bytes.
28
+ def write_fields(storage, out)
29
+ out.text("storage.path", File.expand_path(storage.path))
30
+ if storage.is_a?(Storage::HostDirectory)
31
+ out.text("storage.kind", "directory")
32
+ elsif storage.is_a?(Storage::T64)
33
+ out.text("storage.kind", "t64").bytes("storage.bytes", storage.bytes)
34
+ else
35
+ out.text("storage.kind", KIND_NAMES.fetch(image_kind(storage))).bool("storage.read_only", storage.read_only?)
36
+ out.bytes("storage.bytes", storage.contents)
53
37
  end
54
38
  end
55
39
 
56
- def read_image(kind, path, input)
57
- read_only = input.boolean? || input.detached?
58
- bytes = input.blob
40
+ KIND_NAMES = %w[directory d64 d71 d81 t64].freeze
41
+
42
+ # Opens the storage write_fields wrote: the same directory by its
43
+ # path, an archive with the bytes it held, or a disk image with the
44
+ # bytes it held, writes and all. A detached reader gets the image
45
+ # write-protected, and Storage::Unavailable for a directory.
46
+ def read_fields(input, detached: false)
47
+ kind = KIND_NAMES.index(input.choice("storage.kind", KIND_NAMES, "directory")).to_i
48
+ path = input.text("storage.path", "")
49
+ return (detached ? Storage::Unavailable.new : Storage::HostDirectory.new(path)) if kind == HOST_DIRECTORY
50
+
51
+ bytes = input.bytes("storage.bytes", "".b).bytes
52
+ return Storage::T64.new(path, bytes:) if kind == T64_ARCHIVE
53
+
54
+ read_only = input.bool("storage.read_only", false) || detached
59
55
  case kind
60
56
  when D64 then Storage::D64Image.new(path, read_only:, bytes:)
61
57
  when D71 then Storage::D71Image.new(path, read_only:, bytes:)
62
- when D81 then Storage::D81Image.new(path, read_only:, bytes:)
63
- else raise FormatError, "unknown storage #{kind} in the state"
58
+ else Storage::D81Image.new(path, read_only:, bytes:)
64
59
  end
65
60
  end
66
61
  end
@@ -7,25 +7,16 @@ module Badline
7
7
  Vic20Setup = Data.define(:ram)
8
8
 
9
9
  class Vic20Setup
10
- MARKER = "VIC20"
11
10
  RAM_CONFIGURATIONS = Vic20::Bus::RAM_CONFIGURATIONS.keys.freeze
12
11
 
13
- # The setup a VIC-20's State starts with.
14
- def self.from(state)
15
- input = StateReader.new(state)
16
- input.marker(MARKER)
17
- input.check_stamp
18
- read(input)
12
+ # The RAM expansion by name, in a keyed snapshot module.
13
+ def write_fields(out)
14
+ out.text("ram", ram.to_s)
19
15
  end
20
16
 
21
- def self.read(input)
22
- new(ram: RAM_CONFIGURATIONS.fetch(input.int))
23
- rescue IndexError
24
- raise FormatError, "the state names a VIC-20 RAM expansion badline doesn't know"
25
- end
26
-
27
- def write(out)
28
- out.int(RAM_CONFIGURATIONS.index(ram))
17
+ def self.read_fields(input)
18
+ text = input.choice("ram", RAM_CONFIGURATIONS.map(&:to_s), "unexpanded")
19
+ new(ram: RAM_CONFIGURATIONS.find { |name| name.to_s == text } || :unexpanded)
29
20
  end
30
21
 
31
22
  # A machine built this way, at power-on.
@@ -9,8 +9,8 @@ module Badline
9
9
  # from $DF00, and the RAM.
10
10
  #
11
11
  # Written: the registers and the RAM. VICE saves between transfers,
12
- # so badline's copy of the machine runs a transfer in flight to its
13
- # end first (Vice.settled).
12
+ # so a save runs a transfer in flight to its end first
13
+ # (Machine::Settling).
14
14
  #
15
15
  # Read: the registers, as the values the counters run from and
16
16
  # start from, and the RAM. The status register's interrupt, end of
@@ -22,43 +22,19 @@ module Badline
22
22
  # machine at power-on with nothing attached, and applies the modules it
23
23
  # has, in the order x64sc writes them.
24
24
  module Vice
25
- # Cycles a copy of the machine may run to reach an instruction
26
- # boundary: the longest instruction, stretched by the VIC's longest
27
- # hold on the bus.
28
- BOUNDARY_CYCLES = 100
29
- # Cycles more it may run for an REU transfer in flight to end: a
30
- # swap of 64K, two cycles a byte, stretched by the VIC's holds on
31
- # the bus.
32
- TRANSFER_CYCLES = 0x40000
33
-
34
- # The modules a badline snapshot carries for VICE, in x64sc's order.
35
- # They describe a copy of the machine restored from `state`, so saving
36
- # leaves the machine itself alone. VICE saves between instructions,
37
- # and resumes by fetching an opcode at the program counter, so when
38
- # badline's CPU is inside an instruction, or an REU transfer holds
39
- # the bus, the copy runs on to the end of both. The copy's SID
40
- # catches up, so its registers and voices stand as of that cycle.
41
- def self.export(state)
42
- machine = settled(state)
25
+ # The modules a badline snapshot carries for VICE, in x64sc's order,
26
+ # describing `machine`, which Machine::Settling has brought between
27
+ # instructions with no REU transfer under way: VICE saves between
28
+ # instructions, and resumes by fetching an opcode at the program
29
+ # counter. Its SID has caught up, so the registers and voices stand
30
+ # as of that cycle.
31
+ def self.export(machine)
43
32
  sections = [MainCPU.export(machine), C64Mem.export(machine), Peripherals.cartridge_port(machine)]
44
33
  sections << REU1764.export(machine) if machine.reu
45
34
  sections + [*CIAs.export(machine), SIDRegisters.export(machine), SIDExtended.export(machine),
46
35
  Peripherals.drives, VICII.export(machine), Peripherals.glue(machine), *Peripherals.rest]
47
36
  end
48
37
 
49
- # The copy is detached from the host's files (Snapshot::StateReader),
50
- # so it reads none and its traps write none.
51
- def self.settled(state)
52
- copy = C64.restored(state, detached: true)
53
- (BOUNDARY_CYCLES + TRANSFER_CYCLES).times do
54
- break if copy.cpu.boundary? && !copy.reu&.dma?
55
-
56
- copy.cycle!
57
- end
58
- copy.sid.catch_up!
59
- copy
60
- end
61
-
62
38
  # How the machine the snapshot was saved from was built: the VIC
63
39
  # model and the region from the VIC-II's model, and an REU from
64
40
  # REU1764. VICE doesn't save the CIA model; the 8565 and 8562 go
@@ -1,23 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "zlib"
4
- require "badline/snapshot/state"
5
- require "badline/snapshot/setup"
6
4
  require "badline/snapshot/container"
5
+ require "badline/snapshot/c64_setup"
7
6
  require "badline/snapshot/machine_state"
7
+ require "badline/snapshot/keyed"
8
8
  require "badline/snapshot/vice"
9
9
  require "badline/snapshot/image"
10
10
 
11
11
  module Badline
12
12
  # Saves and restores the whole machine in VICE's .vsf snapshot format.
13
13
  #
14
- # A snapshot badline writes carries VICE's modules for what both
15
- # emulators model, and a BADLINE module holding the State
16
- # C64#snapshot takes, with everything badline needs to carry on
17
- # exactly where it stopped. VICE skips modules it doesn't know. Restoring
18
- # a badline snapshot uses the BADLINE module alone; a snapshot from VICE
19
- # restores through its modules, as far as they map, and reports the
20
- # modules badline doesn't take.
14
+ # A snapshot badline writes holds the machine as BL-* modules, one per
15
+ # unit, each a list of keyed records (Snapshot::Keyed), which later
16
+ # badlines read: doc/snapshot-format.md lists them. VICE skips modules it
17
+ # doesn't know. An export for VICE puts VICE's own modules for the C64
18
+ # ahead of them. A snapshot from VICE restores through its modules, as
19
+ # far as they map, and reports the modules badline doesn't take.
21
20
  module Snapshot
22
21
  module_function
23
22
 
@@ -26,17 +25,21 @@ module Badline
26
25
  path
27
26
  end
28
27
 
29
- # A VIC-20's or a C128's snapshot holds the BADLINE module alone, in a
30
- # container named as xvic or x128 names its machine.
31
- def dump(computer)
32
- state = computer.snapshot
33
- return Container.new([MachineState.section(state)], machine: Container::VIC20).to_s if computer.family == :vic20
34
- return Container.new([MachineState.section(state)], machine: Container::C128).to_s if computer.family == :c128
35
-
36
- Container.new(Vice.export(state) + [MachineState.section(state)]).to_s
28
+ # Saves the machine's snapshot with VICE's modules ahead of its own for
29
+ # a C64, so x64sc opens it too.
30
+ def export(computer, path)
31
+ File.binwrite(path, Image.exported(computer).to_s)
32
+ path
37
33
  end
38
34
 
39
- def read(path) = Image.new(Container.parse(File.binread(path)))
35
+ # The machine's snapshot file. Saving first runs the machine on to the
36
+ # end of the instruction its CPU is in, and of an REU transfer under
37
+ # way (Machine::Settling), so a save advances it by a few cycles, and a
38
+ # second save straight after runs none. A VIC-20's or a C128's snapshot
39
+ # is in a container named as xvic or x128 names its machine.
40
+ def dump(computer) = Image.of(computer).to_s
41
+
42
+ def read(path) = Image.new(Container.parse(File.binread(path)), name: File.basename(path))
40
43
 
41
44
  # A new machine, built as the snapshot's was and restored from it.
42
45
  # Yields a line for each thing it leaves out.
@@ -47,12 +50,32 @@ module Badline
47
50
  # snapshot that fails leaves the machine as it was.
48
51
  def restore(computer, path, &) = read(path).restore(computer, &)
49
52
 
50
- # A machine's snapshots as .vsf files. C64, Vic20 and C128 each
51
- # include it.
53
+ # A machine's snapshots, as .vsf files and in memory. C64, Vic20 and
54
+ # C128 each include it.
52
55
  module Files
53
56
  def save_snapshot(path) = Snapshot.save(self, path)
54
57
 
58
+ # Saves the snapshot with VICE's modules too, for a C64, so x64sc
59
+ # opens it as well as badline.
60
+ def export_snapshot(path) = Snapshot.export(self, path)
61
+
55
62
  def restore_snapshot(path, &) = Snapshot.restore(self, path, &)
63
+
64
+ # The machine's snapshot in memory (Image.of), which settles it as a
65
+ # save does.
66
+ def snapshot = Image.of(self)
67
+
68
+ # Takes the machine back to an Image from #snapshot. The machine keeps
69
+ # its chips, its traps and its callbacks, and puts in the cartridge,
70
+ # drives and tape the image has, taking out any it hasn't. Raises
71
+ # Snapshot::FormatError for an image of a machine built another way.
72
+ # The image goes into a new machine first, so one that fails leaves
73
+ # this machine as it was. What it leaves out goes unsaid:
74
+ # Image#restore returns it in a Report.
75
+ def restore(image)
76
+ image.restore(self) { nil }
77
+ self
78
+ end
56
79
  end
57
80
  end
58
81
 
@@ -7,6 +7,7 @@ module Badline
7
7
 
8
8
  SIGNATURE = "C64 CARTRIDGE ".b
9
9
  VIC20_SIGNATURE = "VIC20 CARTRIDGE ".b
10
+ PLUS4_SIGNATURE = "PLUS4 CARTRIDGE ".b
10
11
  CHIP_SIGNATURE = "CHIP".b
11
12
  CHIP_HEADER_SIZE = 0x10
12
13
  RAM_CHIP = 1
@@ -35,7 +36,7 @@ module Badline
35
36
  end
36
37
 
37
38
  # `bytes` stands in for the file's contents. `machine` is the one the
38
- # image has to be for, :c64 or :vic20, or nil for either.
39
+ # image has to be for, :c64, :vic20 or :plus4, or nil for any.
39
40
  def initialize(path = nil, bytes: nil, machine: :c64)
40
41
  parse(bytes || File.binread(path))
41
42
  return if machine.nil? || @machine == machine
@@ -58,6 +59,7 @@ module Badline
58
59
  def signed_machine(bytes)
59
60
  return :c64 if bytes.start_with?(SIGNATURE)
60
61
  return :vic20 if bytes.start_with?(VIC20_SIGNATURE)
62
+ return :plus4 if bytes.start_with?(PLUS4_SIGNATURE)
61
63
 
62
64
  raise FormatError, "Missing CRT signature"
63
65
  end
@@ -66,15 +66,20 @@ module Badline
66
66
  [bytes.length].pack("v") + bytes.pack("C*").ljust(size, "\x00".b)
67
67
  end
68
68
 
69
- attr_reader :half_tracks, :max_track_size
69
+ attr_reader :half_tracks, :max_track_size, :path
70
70
 
71
- def initialize(path, read_only: false)
71
+ # `bytes` stands in for the host file's contents, as a snapshot
72
+ # restores the image as it was.
73
+ def initialize(path, read_only: false, bytes: nil)
72
74
  @path = path
73
75
  @read_only = read_only
74
- @data = File.binread(path)
76
+ @data = bytes ? bytes.pack("C*") : File.binread(path)
75
77
  parse
76
78
  end
77
79
 
80
+ # The image as its file holds it.
81
+ def contents = @data.bytes
82
+
78
83
  # Whether the image was opened write-protected.
79
84
  def read_only? = @read_only
80
85
 
@@ -5,11 +5,14 @@ module Badline
5
5
  # Raw datasette pulse stream. The header holds the signature, the
6
6
  # version at $0c, the platform at $0d (0 C64, 1 VIC-20, 2 C16) and the
7
7
  # data size at $10; the data runs from $14, every byte the time to the
8
- # next falling edge on the tape read line, in units of 8 cycles.
8
+ # next falling edge on the tape read line, in units of 8 cycles. A
9
+ # version 2 tape, the C16's and the Plus/4's, times half waves instead,
10
+ # so it takes two bytes to the next falling edge.
9
11
  class TAP
10
12
  class FormatError < StandardError; end
11
13
 
12
14
  SIGNATURE = "C64-TAPE-RAW".bytes.freeze
15
+ C16_SIGNATURE = "C16-TAPE-RAW".bytes.freeze
13
16
  HEADER_SIZE = 0x14
14
17
  OVERFLOW = 0x100 * 8
15
18
 
@@ -28,10 +31,11 @@ module Badline
28
31
  def initialize(path, bytes: nil)
29
32
  @path = File.expand_path(path)
30
33
  @bytes = bytes || File.binread(path).bytes
31
- raise FormatError, "Missing TAP signature" unless @bytes[0, SIGNATURE.length] == SIGNATURE
34
+ signature = @bytes[0, SIGNATURE.length]
35
+ raise FormatError, "Missing TAP signature" unless [SIGNATURE, C16_SIGNATURE].include?(signature)
32
36
 
33
37
  @version = @bytes[0x0c].to_i
34
- raise FormatError, "Unsupported TAP version #{@version}" if @version > 1
38
+ raise FormatError, "Unsupported TAP version #{@version}" if @version > 2
35
39
 
36
40
  @platform = @bytes[0x0d].to_i
37
41
 
@@ -55,17 +59,16 @@ module Badline
55
59
  # Records a pulse `cycles` long at the position, in place of the rest
56
60
  # of the tape: a byte of units of 8 cycles, or a zero and the 24-bit
57
61
  # cycle count for a long one (a lone zero, an overflow, on a version 0
58
- # tape).
62
+ # tape). A version 2 tape records it as two equal halves.
59
63
  def record_pulse(cycles)
60
- units = (cycles + 4) / 8
61
- encoded = if units.between?(1, 0xff) then [units]
62
- elsif version.zero? then [0]
63
- else
64
- count = [cycles, 0xffffff].min
65
- [0, count & 0xff, (count >> 8) & 0xff, count >> 16]
66
- end
67
64
  @bytes.slice!(@pos..)
68
- @bytes.concat(encoded)
65
+ if version == 2
66
+ half = cycles / 2
67
+ @bytes.concat(encode(cycles - half))
68
+ @bytes.concat(encode(half))
69
+ else
70
+ @bytes.concat(encode(cycles))
71
+ end
69
72
  @pos = @end = @bytes.length
70
73
  end
71
74
 
@@ -77,20 +80,45 @@ module Badline
77
80
  File.binwrite(@path, @bytes.pack("C*"))
78
81
  end
79
82
 
83
+ # Whether each byte times a half wave, as on a version 2 tape.
84
+ def half_waves? = version == 2
85
+
80
86
  # Cycles until the next falling edge, or nil once the tape has run out.
87
+ # On a version 2 tape that is two half waves, or one left at the end.
81
88
  def next_pulse
82
89
  return if end?
90
+ return next_time unless half_waves?
91
+
92
+ first = next_time
93
+ end? ? first : first + next_time
94
+ end
95
+
96
+ # Cycles until the read line's next edge on a version 2 tape, or nil
97
+ # once the tape has run out.
98
+ def next_half_wave
99
+ next_time unless end?
100
+ end
83
101
 
102
+ private
103
+
104
+ def next_time
84
105
  byte = @bytes[@pos]
85
106
  @pos += 1
86
107
  return byte * 8 unless byte.zero?
87
108
 
88
- # $00 is an overflow in version 0 and a 24-bit cycle count in
89
- # version 1.
109
+ # $00 is an overflow in version 0 and a 24-bit cycle count from
110
+ # version 1 on.
90
111
  version.zero? ? OVERFLOW : long_pulse
91
112
  end
92
113
 
93
- private
114
+ def encode(cycles)
115
+ units = (cycles + 4) / 8
116
+ return [units] if units.between?(1, 0xff)
117
+ return [0] if version.zero?
118
+
119
+ count = [cycles, 0xffffff].min
120
+ [0, count & 0xff, (count >> 8) & 0xff, count >> 16]
121
+ end
94
122
 
95
123
  # The stated size disagrees with the file in the wild, and a few dumps
96
124
  # leave it zeroed altogether.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Badline
4
- VERSION = "0.13.0"
4
+ VERSION = "0.14.0"
5
5
  end
@@ -34,6 +34,9 @@ module Badline
34
34
  # The VIC-20's KERNAL, which the traps stand in for.
35
35
  def trap_layout = KernalTrap::VIC20_LAYOUT
36
36
 
37
+ # The VIC-20's KERNAL keeps its keyboard buffer where the C64's does.
38
+ def keyboard_buffer = Machine::KeyboardBuffer::C64_LAYOUT
39
+
37
40
  def trap_bus = @bus
38
41
 
39
42
  # A drive that powers on with the machine takes longer to boot than