badline 0.13.0 → 0.15.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 (286) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +72 -1
  3. data/CLAUDE.md +42 -12
  4. data/CONTRIBUTING.md +4 -3
  5. data/README.md +76 -13
  6. data/doc/media.md +2 -1
  7. data/doc/pinned-behaviour.md +174 -0
  8. data/doc/snapshot-format.md +1197 -0
  9. data/doc/snapshots.md +90 -40
  10. data/lib/badline/audio/bare_player.rb +46 -17
  11. data/lib/badline/audio/machine_player.rb +6 -10
  12. data/lib/badline/audio/renderer.rb +32 -21
  13. data/lib/badline/c128/banks.rb +7 -6
  14. data/lib/badline/c128/bus/keyed_state.rb +45 -0
  15. data/lib/badline/c128/bus.rb +36 -4
  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 +27 -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/extra_sids.rb +80 -0
  31. data/lib/badline/c64/bus/keyed_state.rb +41 -0
  32. data/lib/badline/c64/bus.rb +3 -1
  33. data/lib/badline/c64/cartridge/action_replay.rb +6 -15
  34. data/lib/badline/c64/cartridge/comal80.rb +1 -0
  35. data/lib/badline/c64/cartridge/dinamic.rb +3 -1
  36. data/lib/badline/c64/cartridge/easy_flash.rb +0 -15
  37. data/lib/badline/c64/cartridge/epyx_fastload.rb +0 -8
  38. data/lib/badline/c64/cartridge/final_cartridge3.rb +1 -12
  39. data/lib/badline/c64/cartridge/flash.rb +3 -3
  40. data/lib/badline/c64/cartridge/fun_play.rb +2 -1
  41. data/lib/badline/c64/cartridge/g_mod2.rb +0 -12
  42. data/lib/badline/c64/cartridge/game_system.rb +2 -1
  43. data/lib/badline/c64/cartridge/geo_ram.rb +0 -16
  44. data/lib/badline/c64/cartridge/kcs_power.rb +0 -8
  45. data/lib/badline/c64/cartridge/keyed_state.rb +514 -0
  46. data/lib/badline/c64/cartridge/magic_desk.rb +2 -1
  47. data/lib/badline/c64/cartridge/ocean.rb +1 -0
  48. data/lib/badline/c64/cartridge/pagefox.rb +1 -10
  49. data/lib/badline/c64/cartridge/retro_replay.rb +0 -8
  50. data/lib/badline/c64/cartridge/rgcd.rb +2 -11
  51. data/lib/badline/c64/cartridge/super_games.rb +3 -12
  52. data/lib/badline/c64/cartridge/zaxxon.rb +1 -16
  53. data/lib/badline/c64/cartridge.rb +4 -30
  54. data/lib/badline/c64/kernal_traps.rb +28 -25
  55. data/lib/badline/c64/keyed_modules.rb +115 -0
  56. data/lib/badline/c64/keyed_state.rb +68 -0
  57. data/lib/badline/c64/reu/dma.rb +0 -4
  58. data/lib/badline/c64/reu/keyed_state.rb +140 -0
  59. data/lib/badline/c64/reu/ram.rb +0 -15
  60. data/lib/badline/c64/reu.rb +1 -3
  61. data/lib/badline/c64/true_drives.rb +56 -5
  62. data/lib/badline/c64.rb +7 -3
  63. data/lib/badline/chips/cia/interrupt_register.rb +0 -17
  64. data/lib/badline/chips/cia/keyed_parts.rb +213 -0
  65. data/lib/badline/chips/cia/serial.rb +0 -4
  66. data/lib/badline/chips/cia/time_of_day.rb +0 -22
  67. data/lib/badline/chips/cia/timer.rb +0 -20
  68. data/lib/badline/chips/cia.rb +2 -2
  69. data/lib/badline/chips/cpu/core.rb +18 -2
  70. data/lib/badline/chips/cpu/keyed_state.rb +177 -0
  71. data/lib/badline/chips/cpu/microcode.rb +16 -0
  72. data/lib/badline/chips/cpu/port.rb +20 -3
  73. data/lib/badline/chips/cpu.rb +5 -0
  74. data/lib/badline/chips/sid/decimator.rb +10 -8
  75. data/lib/badline/chips/sid/envelope.rb +1 -6
  76. data/lib/badline/chips/sid/filter.rb +0 -14
  77. data/lib/badline/chips/sid/keyed_parts.rb +286 -0
  78. data/lib/badline/chips/sid/voice.rb +0 -10
  79. data/lib/badline/chips/sid/waveform.rb +0 -2
  80. data/lib/badline/chips/sid.rb +1 -2
  81. data/lib/badline/chips/ted/beam.rb +118 -0
  82. data/lib/badline/chips/ted/fetches.rb +199 -0
  83. data/lib/badline/chips/ted/keyed_state.rb +156 -0
  84. data/lib/badline/chips/ted/modes.rb +134 -0
  85. data/lib/badline/chips/ted/palette.rb +43 -0
  86. data/lib/badline/chips/ted/picture.rb +182 -0
  87. data/lib/badline/chips/ted/sound/output.rb +86 -0
  88. data/lib/badline/chips/ted/sound.rb +284 -0
  89. data/lib/badline/chips/ted/timers.rb +66 -0
  90. data/lib/badline/chips/ted.rb +190 -0
  91. data/lib/badline/chips/tia.rb +61 -0
  92. data/lib/badline/chips/vdc/keyed_parts.rb +105 -0
  93. data/lib/badline/chips/vdc/painter.rb +0 -12
  94. data/lib/badline/chips/vdc/raster.rb +0 -16
  95. data/lib/badline/chips/vdc/window.rb +0 -9
  96. data/lib/badline/chips/vdc.rb +1 -2
  97. data/lib/badline/chips/via/control_lines.rb +0 -15
  98. data/lib/badline/chips/via/interrupt_register.rb +0 -9
  99. data/lib/badline/chips/via/keyed_parts.rb +163 -0
  100. data/lib/badline/chips/via/shift_register.rb +0 -14
  101. data/lib/badline/chips/via/timer1.rb +0 -13
  102. data/lib/badline/chips/via/timer2.rb +0 -18
  103. data/lib/badline/chips/via.rb +1 -2
  104. data/lib/badline/chips/vic/border_mask.rb +0 -10
  105. data/lib/badline/chips/vic/collisions.rb +0 -11
  106. data/lib/badline/chips/vic/color_patches.rb +0 -11
  107. data/lib/badline/chips/vic/display_state.rb +0 -19
  108. data/lib/badline/chips/vic/graphics_shifter.rb +0 -4
  109. data/lib/badline/chips/vic/keyed_parts.rb +228 -0
  110. data/lib/badline/chips/vic/keyed_sprites.rb +225 -0
  111. data/lib/badline/chips/vic/keyed_state.rb +102 -0
  112. data/lib/badline/chips/vic/register_log.rb +0 -18
  113. data/lib/badline/chips/vic/registers.rb +0 -9
  114. data/lib/badline/chips/vic/sequencer.rb +0 -2
  115. data/lib/badline/chips/vic/sprite/internal_bus.rb +0 -11
  116. data/lib/badline/chips/vic/sprite.rb +0 -2
  117. data/lib/badline/chips/vic/sprites.rb +0 -3
  118. data/lib/badline/chips/vic.rb +6 -2
  119. data/lib/badline/chips/vic_i/keyed_parts.rb +142 -0
  120. data/lib/badline/chips/vic_i/painter.rb +0 -4
  121. data/lib/badline/chips/vic_i/sound.rb +38 -46
  122. data/lib/badline/chips/vic_i.rb +2 -2
  123. data/lib/badline/chips/z80/core.rb +2 -2
  124. data/lib/badline/chips/z80/keyed_state.rb +73 -0
  125. data/lib/badline/chips.rb +10 -0
  126. data/lib/badline/core.rb +4 -21
  127. data/lib/badline/drives/datasette/keyed_state.rb +43 -0
  128. data/lib/badline/drives/datasette.rb +62 -12
  129. data/lib/badline/drives/engine/core.rb +3 -0
  130. data/lib/badline/drives/engine/serial_lines.rb +4 -7
  131. data/lib/badline/drives/model1541/bus.rb +0 -13
  132. data/lib/badline/drives/model1541/disk.rb +9 -8
  133. data/lib/badline/drives/model1541/keyed_state.rb +251 -0
  134. data/lib/badline/drives/model1541/mechanism.rb +0 -2
  135. data/lib/badline/drives/model1541.rb +1 -2
  136. data/lib/badline/drives/model1571/bus.rb +0 -13
  137. data/lib/badline/drives/model1571/keyed_state.rb +108 -0
  138. data/lib/badline/drives/model1571/wd1770.rb +0 -14
  139. data/lib/badline/drives/model1571.rb +1 -2
  140. data/lib/badline/drives/model1581/bus.rb +0 -13
  141. data/lib/badline/drives/model1581/disk.rb +0 -4
  142. data/lib/badline/drives/model1581/keyed_state.rb +280 -0
  143. data/lib/badline/drives/model1581/mechanism.rb +0 -21
  144. data/lib/badline/drives/model1581/slot.rb +0 -18
  145. data/lib/badline/drives/model1581/wd1772.rb +0 -2
  146. data/lib/badline/drives/model1581.rb +2 -2
  147. data/lib/badline/drives.rb +8 -0
  148. data/lib/badline/frontend/app.rb +3 -3
  149. data/lib/badline/frontend/boot.rb +4 -2
  150. data/lib/badline/frontend/controls.rb +25 -5
  151. data/lib/badline/frontend/drive_led.rb +44 -21
  152. data/lib/badline/frontend/keys.rb +14 -2
  153. data/lib/badline/frontend/media_slots.rb +11 -1
  154. data/lib/badline/frontend/menu/menu_dialogs.rb +4 -3
  155. data/lib/badline/frontend/menu/menu_media.rb +13 -3
  156. data/lib/badline/frontend/menu/menu_pages.rb +8 -8
  157. data/lib/badline/frontend/menu/menu_ports.rb +13 -1
  158. data/lib/badline/frontend/screen.rb +33 -12
  159. data/lib/badline/frontend/screens.rb +2 -1
  160. data/lib/badline/frontend/snapshots.rb +12 -1
  161. data/lib/badline/frontend/sound.rb +4 -4
  162. data/lib/badline/frontend/timeline.rb +43 -12
  163. data/lib/badline/input/matrix_typist.rb +98 -0
  164. data/lib/badline/input.rb +1 -0
  165. data/lib/badline/kernal_trap/chrout.rb +45 -0
  166. data/lib/badline/kernal_trap/dos/channel.rb +0 -27
  167. data/lib/badline/kernal_trap/dos/channels.rb +0 -25
  168. data/lib/badline/kernal_trap/dos/memory.rb +0 -9
  169. data/lib/badline/kernal_trap/dos/status.rb +0 -8
  170. data/lib/badline/kernal_trap/dos/write_file.rb +0 -18
  171. data/lib/badline/kernal_trap/dos.rb +0 -19
  172. data/lib/badline/kernal_trap/file_routine.rb +4 -4
  173. data/lib/badline/kernal_trap/keyed_state.rb +150 -0
  174. data/lib/badline/kernal_trap/layout.rb +75 -11
  175. data/lib/badline/kernal_trap/load.rb +14 -16
  176. data/lib/badline/kernal_trap/routine.rb +12 -0
  177. data/lib/badline/kernal_trap/save.rb +8 -18
  178. data/lib/badline/kernal_trap/serial.rb +1 -17
  179. data/lib/badline/kernal_trap.rb +2 -0
  180. data/lib/badline/machine/keyboard_buffer.rb +49 -0
  181. data/lib/badline/machine/settling.rb +29 -0
  182. data/lib/badline/machine.rb +29 -6
  183. data/lib/badline/media/boot_disk.rb +14 -2
  184. data/lib/badline/media/extensions.rb +10 -0
  185. data/lib/badline/media/plus4_media.rb +68 -0
  186. data/lib/badline/media/program.rb +20 -6
  187. data/lib/badline/media.rb +33 -12
  188. data/lib/badline/memory.rb +3 -6
  189. data/lib/badline/options/family.rb +6 -2
  190. data/lib/badline/options/help.rb +66 -16
  191. data/lib/badline/options/table.rb +17 -2
  192. data/lib/badline/options/validation.rb +26 -2
  193. data/lib/badline/options.rb +42 -9
  194. data/lib/badline/plus4/acia.rb +45 -0
  195. data/lib/badline/plus4/bus.rb +375 -0
  196. data/lib/badline/plus4/cassette_sense.rb +21 -0
  197. data/lib/badline/plus4/cpu.rb +10 -0
  198. data/lib/badline/plus4/cpu_port.rb +93 -0
  199. data/lib/badline/plus4/keyboard_port.rb +76 -0
  200. data/lib/badline/plus4/keyed_modules.rb +99 -0
  201. data/lib/badline/plus4/keyed_parts.rb +143 -0
  202. data/lib/badline/plus4/keyed_state.rb +41 -0
  203. data/lib/badline/plus4/model.rb +38 -0
  204. data/lib/badline/plus4/user_port.rb +31 -0
  205. data/lib/badline/plus4.rb +240 -0
  206. data/lib/badline/roms/README +38 -0
  207. data/lib/badline/roms/plus4/basic.rom +0 -0
  208. data/lib/badline/roms/plus4/function-high.rom +0 -0
  209. data/lib/badline/roms/plus4/function-low.rom +0 -0
  210. data/lib/badline/roms/plus4/kernal-ntsc.rom +0 -0
  211. data/lib/badline/roms/plus4/kernal-pal.rom +0 -0
  212. data/lib/badline/snapshot/c128_setup.rb +9 -20
  213. data/lib/badline/snapshot/c64_setup.rb +28 -24
  214. data/lib/badline/snapshot/container.rb +6 -0
  215. data/lib/badline/snapshot/field_doc/markdown.rb +113 -0
  216. data/lib/badline/snapshot/field_doc.rb +148 -0
  217. data/lib/badline/snapshot/image.rb +143 -28
  218. data/lib/badline/snapshot/keyed/cursor.rb +40 -0
  219. data/lib/badline/snapshot/keyed/modules.rb +66 -0
  220. data/lib/badline/snapshot/keyed/reader.rb +188 -0
  221. data/lib/badline/snapshot/keyed/recorder.rb +88 -0
  222. data/lib/badline/snapshot/keyed/report.rb +26 -0
  223. data/lib/badline/snapshot/keyed/writer.rb +55 -0
  224. data/lib/badline/snapshot/keyed.rb +64 -0
  225. data/lib/badline/snapshot/machine_state.rb +17 -64
  226. data/lib/badline/snapshot/plus4_setup.rb +39 -0
  227. data/lib/badline/snapshot/storage_setup.rb +29 -34
  228. data/lib/badline/snapshot/vic20_setup.rb +6 -15
  229. data/lib/badline/snapshot/vice/reu1764.rb +2 -2
  230. data/lib/badline/snapshot/vice.rb +7 -31
  231. data/lib/badline/snapshot.rb +48 -20
  232. data/lib/badline/storage/crt_file.rb +3 -1
  233. data/lib/badline/storage/g64_image.rb +9 -4
  234. data/lib/badline/storage/tap.rb +57 -20
  235. data/lib/badline/support/checkpoint.rb +3 -2
  236. data/lib/badline/support/plus4_checkpoint.rb +42 -0
  237. data/lib/badline/version.rb +1 -1
  238. data/lib/badline/vic20/attachments.rb +3 -0
  239. data/lib/badline/vic20/bus/keyed_state.rb +52 -0
  240. data/lib/badline/vic20/bus.rb +1 -2
  241. data/lib/badline/vic20/keyed_modules.rb +73 -0
  242. data/lib/badline/vic20/keyed_state.rb +45 -0
  243. data/lib/badline/vic20.rb +5 -5
  244. data/lib/badline.rb +1 -0
  245. metadata +75 -42
  246. data/lib/badline/c128/bus/saved_state.rb +0 -53
  247. data/lib/badline/c128/saved_state.rb +0 -117
  248. data/lib/badline/c64/bus/saved_state.rb +0 -68
  249. data/lib/badline/c64/cartridge/flash/saved_state.rb +0 -40
  250. data/lib/badline/c64/cartridge/retro_replay/saved_state.rb +0 -47
  251. data/lib/badline/c64/cartridge/saved_state.rb +0 -63
  252. data/lib/badline/c64/reu/dma/saved_state.rb +0 -52
  253. data/lib/badline/c64/reu/saved_state.rb +0 -55
  254. data/lib/badline/c64/saved_state.rb +0 -112
  255. data/lib/badline/chips/cia/saved_state.rb +0 -49
  256. data/lib/badline/chips/cia/serial/saved_state.rb +0 -45
  257. data/lib/badline/chips/cpu/saved_state.rb +0 -117
  258. data/lib/badline/chips/sid/envelope/saved_state.rb +0 -70
  259. data/lib/badline/chips/sid/filter/saved_state.rb +0 -42
  260. data/lib/badline/chips/sid/saved_state.rb +0 -52
  261. data/lib/badline/chips/sid/waveform/saved_state.rb +0 -68
  262. data/lib/badline/chips/vdc/saved_state.rb +0 -36
  263. data/lib/badline/chips/via/saved_state.rb +0 -44
  264. data/lib/badline/chips/vic/graphics_shifter/saved_state.rb +0 -40
  265. data/lib/badline/chips/vic/saved_state.rb +0 -82
  266. data/lib/badline/chips/vic/sequencer/saved_state.rb +0 -91
  267. data/lib/badline/chips/vic/sprite/saved_state.rb +0 -66
  268. data/lib/badline/chips/vic/sprites/saved_state.rb +0 -59
  269. data/lib/badline/chips/vic_i/painter/saved_state.rb +0 -40
  270. data/lib/badline/chips/vic_i/saved_state.rb +0 -55
  271. data/lib/badline/chips/z80/saved_state.rb +0 -71
  272. data/lib/badline/chrout_trap.rb +0 -43
  273. data/lib/badline/debug_register.rb +0 -28
  274. data/lib/badline/drives/datasette/saved_state.rb +0 -37
  275. data/lib/badline/drives/model1541/disk/saved_state.rb +0 -88
  276. data/lib/badline/drives/model1541/mechanism/saved_state.rb +0 -72
  277. data/lib/badline/drives/model1541/saved_state.rb +0 -44
  278. data/lib/badline/drives/model1571/saved_state.rb +0 -47
  279. data/lib/badline/drives/model1581/disk/saved_state.rb +0 -89
  280. data/lib/badline/drives/model1581/saved_state.rb +0 -42
  281. data/lib/badline/drives/model1581/wd1772/saved_state.rb +0 -68
  282. data/lib/badline/keyboard_buffer.rb +0 -33
  283. data/lib/badline/snapshot/setup.rb +0 -31
  284. data/lib/badline/snapshot/state.rb +0 -179
  285. data/lib/badline/vic20/bus/saved_state.rb +0 -35
  286. data/lib/badline/vic20/saved_state.rb +0 -110
@@ -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,22 @@ 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, a C128's or a
39
+ # Plus/4's snapshot is in a container named as xvic, x128 or xplus4
40
+ # names its machine.
41
+ def dump(computer) = Image.of(computer).to_s
42
+
43
+ def read(path) = Image.new(Container.parse(File.binread(path)), name: File.basename(path))
40
44
 
41
45
  # A new machine, built as the snapshot's was and restored from it.
42
46
  # Yields a line for each thing it leaves out.
@@ -47,12 +51,32 @@ module Badline
47
51
  # snapshot that fails leaves the machine as it was.
48
52
  def restore(computer, path, &) = read(path).restore(computer, &)
49
53
 
50
- # A machine's snapshots as .vsf files. C64, Vic20 and C128 each
51
- # include it.
54
+ # A machine's snapshots, as .vsf files and in memory. C64, Vic20, C128
55
+ # and Plus4 each include it.
52
56
  module Files
53
57
  def save_snapshot(path) = Snapshot.save(self, path)
54
58
 
59
+ # Saves the snapshot with VICE's modules too, for a C64, so x64sc
60
+ # opens it as well as badline.
61
+ def export_snapshot(path) = Snapshot.export(self, path)
62
+
55
63
  def restore_snapshot(path, &) = Snapshot.restore(self, path, &)
64
+
65
+ # The machine's snapshot in memory (Image.of), which settles it as a
66
+ # save does.
67
+ def snapshot = Image.of(self)
68
+
69
+ # Takes the machine back to an Image from #snapshot. The machine keeps
70
+ # its chips, its traps and its callbacks, and puts in the cartridge,
71
+ # drives and tape the image has, taking out any it hasn't. Raises
72
+ # Snapshot::FormatError for an image of a machine built another way.
73
+ # The image goes into a new machine first, so one that fails leaves
74
+ # this machine as it was. What it leaves out goes unsaid:
75
+ # Image#restore returns it in a Report.
76
+ def restore(image)
77
+ image.restore(self) { nil }
78
+ self
79
+ end
56
80
  end
57
81
  end
58
82
 
@@ -67,4 +91,8 @@ module Badline
67
91
  class C128
68
92
  include Snapshot::Files
69
93
  end
94
+
95
+ class Plus4
96
+ include Snapshot::Files
97
+ end
70
98
  end
@@ -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
@@ -47,7 +47,7 @@ module Badline
47
47
  # track 1 and 1 for track 1.5, laid out as VICE lays one out: every
48
48
  # block as long as the longest track.
49
49
  def self.create(path, tracks, half_tracks: HALF_TRACKS)
50
- max = [MAX_TRACK_SIZE, *tracks.values.map { |bytes, _| bytes.length }].max
50
+ max = tracks.values.map { |bytes, _| bytes.length }.push(MAX_TRACK_SIZE).max
51
51
  offsets = Array.new(half_tracks, 0)
52
52
  zones = Array.new(half_tracks, 0)
53
53
  blocks = +"".b
@@ -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,21 +5,26 @@ 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
 
16
19
  # The file it was read from, expanded, and the file's bytes.
17
20
  attr_reader :version, :platform, :path, :bytes
18
21
 
19
- # A blank version 1 tape for `platform` (0 C64, 1 VIC-20) at `path`,
20
- # written to the host file.
22
+ # A blank tape for `platform` at `path`, written to the host file: a
23
+ # version 1 tape for the C64 (0) or the VIC-20 (1), and a version 2
24
+ # C16 tape for the 264 series (2).
21
25
  def self.create(path, platform:)
22
- File.binwrite(path, (SIGNATURE + [1, platform, 0, 0, 0, 0, 0, 0]).pack("C*"))
26
+ header = platform == 2 ? C16_SIGNATURE + [2] : SIGNATURE + [1]
27
+ File.binwrite(path, (header + [platform, 0, 0, 0, 0, 0, 0]).pack("C*"))
23
28
  new(path)
24
29
  end
25
30
 
@@ -28,10 +33,11 @@ module Badline
28
33
  def initialize(path, bytes: nil)
29
34
  @path = File.expand_path(path)
30
35
  @bytes = bytes || File.binread(path).bytes
31
- raise FormatError, "Missing TAP signature" unless @bytes[0, SIGNATURE.length] == SIGNATURE
36
+ signature = @bytes[0, SIGNATURE.length]
37
+ raise FormatError, "Missing TAP signature" unless [SIGNATURE, C16_SIGNATURE].include?(signature)
32
38
 
33
39
  @version = @bytes[0x0c].to_i
34
- raise FormatError, "Unsupported TAP version #{@version}" if @version > 1
40
+ raise FormatError, "Unsupported TAP version #{@version}" if @version > 2
35
41
 
36
42
  @platform = @bytes[0x0d].to_i
37
43
 
@@ -55,18 +61,18 @@ module Badline
55
61
  # Records a pulse `cycles` long at the position, in place of the rest
56
62
  # of the tape: a byte of units of 8 cycles, or a zero and the 24-bit
57
63
  # cycle count for a long one (a lone zero, an overflow, on a version 0
58
- # tape).
64
+ # tape). A version 2 tape records it as two equal halves.
59
65
  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
- @bytes.slice!(@pos..)
68
- @bytes.concat(encoded)
69
- @pos = @end = @bytes.length
66
+ return record(encode(cycles)) unless version == 2
67
+
68
+ half = cycles / 2
69
+ record(encode(cycles - half) + encode(half))
70
+ end
71
+
72
+ # Records a half wave `cycles` long at the position, in place of the
73
+ # rest of the tape, as a version 2 tape times them.
74
+ def record_half_wave(cycles)
75
+ record(encode(cycles))
70
76
  end
71
77
 
72
78
  # Writes the tape back to its host file, with the data size the
@@ -77,20 +83,51 @@ module Badline
77
83
  File.binwrite(@path, @bytes.pack("C*"))
78
84
  end
79
85
 
86
+ # Whether each byte times a half wave, as on a version 2 tape.
87
+ def half_waves? = version == 2
88
+
80
89
  # Cycles until the next falling edge, or nil once the tape has run out.
90
+ # On a version 2 tape that is two half waves, or one left at the end.
81
91
  def next_pulse
82
92
  return if end?
93
+ return next_time unless half_waves?
94
+
95
+ first = next_time
96
+ end? ? first : first + next_time
97
+ end
98
+
99
+ # Cycles until the read line's next edge on a version 2 tape, or nil
100
+ # once the tape has run out.
101
+ def next_half_wave
102
+ next_time unless end?
103
+ end
83
104
 
105
+ private
106
+
107
+ def next_time
84
108
  byte = @bytes[@pos]
85
109
  @pos += 1
86
110
  return byte * 8 unless byte.zero?
87
111
 
88
- # $00 is an overflow in version 0 and a 24-bit cycle count in
89
- # version 1.
112
+ # $00 is an overflow in version 0 and a 24-bit cycle count from
113
+ # version 1 on.
90
114
  version.zero? ? OVERFLOW : long_pulse
91
115
  end
92
116
 
93
- private
117
+ def record(times)
118
+ @bytes.slice!(@pos..)
119
+ @bytes.concat(times)
120
+ @pos = @end = @bytes.length
121
+ end
122
+
123
+ def encode(cycles)
124
+ units = (cycles + 4) / 8
125
+ return [units] if units.between?(1, 0xff)
126
+ return [0] if version.zero?
127
+
128
+ count = [cycles, 0xffffff].min
129
+ [0, count & 0xff, (count >> 8) & 0xff, count >> 16]
130
+ end
94
131
 
95
132
  # The stated size disagrees with the file in the wild, and a few dumps
96
133
  # leave it zeroed altogether.
@@ -9,8 +9,9 @@ module Badline
9
9
  # interface, so taking a checkpoint leaves the machine as it was.
10
10
  #
11
11
  # Checkpoint.take digests a C64's components, COMPONENTS.
12
- # Vic20Checkpoint and C128Checkpoint digest the other families', each
13
- # with a list of its own, which a checkpoint carries as +components+.
12
+ # Vic20Checkpoint, C128Checkpoint and Plus4Checkpoint digest the other
13
+ # families', each with a list of its own, which a checkpoint carries as
14
+ # +components+.
14
15
  #
15
16
  # Colour RAM is digested as the four bits each cell stores: the upper
16
17
  # nibble a CPU read sees comes off the open bus.
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Support
5
+ # A Checkpoint of a 264 series machine: its CPU, its RAM, the TED's
6
+ # picture, the TED's registers, timers and beam, its sound, and the
7
+ # CPU's port with the ROM banks. The sound's voices are run up to the
8
+ # cycle to read them, which changes nothing they do.
9
+ module Plus4Checkpoint
10
+ COMPONENTS = %w[cpu ram display ted sound port].freeze
11
+
12
+ def self.take(machine)
13
+ ram = machine.ram
14
+ Checkpoint.new(machine.cycles, [
15
+ Checkpoint.fnv1a(Checkpoint.cpu_state(machine.cpu)),
16
+ Checkpoint.fnv1a(Checkpoint.ram(ram, ram.length)),
17
+ Checkpoint.fnv1a(machine.ted.display),
18
+ Checkpoint.fnv1a(ted_state(machine.ted)),
19
+ Checkpoint.fnv1a(sound_state(machine.sound)),
20
+ Checkpoint.fnv1a(port_state(machine))
21
+ ], COMPONENTS)
22
+ end
23
+
24
+ def self.parse(line) = Checkpoint.parse(line, COMPONENTS)
25
+
26
+ def self.ted_state(ted)
27
+ Array.new(0x20) { |register| ted.peek(0xff00 + register) } +
28
+ ted.timer_counts + [ted.line, ted.column, ted.interrupt_flags, ted.cpu_slots]
29
+ end
30
+
31
+ def self.sound_state(sound)
32
+ [sound.noise, sound.phase(0), sound.phase(1), sound.level, sound.volume, sound.control]
33
+ end
34
+
35
+ def self.port_state(machine)
36
+ port = machine.port
37
+ bus = machine.bus
38
+ [port.ddr, port.data, bus.low_bank, bus.high_bank, bus.rom_selected ? 1 : 0]
39
+ end
40
+ end
41
+ end
42
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Badline
4
- VERSION = "0.13.0"
4
+ VERSION = "0.15.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
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Vic20
5
+ class Bus
6
+ class ColorRAM
7
+ # The cells in a keyed snapshot module, under `key`.
8
+ def save_fields(out, key) = out.bytes(key, @cells)
9
+
10
+ def load_fields(input, key) = input.bytes_into(key, @cells)
11
+ end
12
+
13
+ # The bytes each side's data lines hold, under `prefix`, and a
14
+ # cartridge's ROM chips as rom<N>.start and rom<N>.bytes, in a keyed
15
+ # snapshot module (Snapshot::Keyed). The pages are mapped again once
16
+ # they are loaded.
17
+ module KeyedState
18
+ def save_fields(out, prefix)
19
+ out.int("#{prefix}data", @data).int("#{prefix}video_data", @video_data)
20
+ end
21
+
22
+ def load_fields(input, prefix)
23
+ @data = input.byte("#{prefix}data", @data)
24
+ @video_data = input.byte("#{prefix}video_data", @video_data)
25
+ end
26
+
27
+ # Whether a cartridge's ROM chips are in the expansion blocks.
28
+ def cartridge? = !@roms.empty?
29
+
30
+ def save_rom_fields(out)
31
+ out.int("roms", @roms.length)
32
+ @roms.each_with_index do |rom, number|
33
+ out.int("rom#{number}.start", rom.start)
34
+ rom.save_fields(out, "rom#{number}.bytes")
35
+ end
36
+ end
37
+
38
+ def load_rom_fields(input)
39
+ @roms = []
40
+ input.int("roms", 0).times do |number|
41
+ bytes = input.bytes("rom#{number}.bytes", "".b).bytes
42
+ @roms << ROM.new(bytes, length: bytes.length, start: input.word("rom#{number}.start", 0))
43
+ end
44
+ end
45
+
46
+ def restored! = map_pages
47
+ end
48
+
49
+ include KeyedState
50
+ end
51
+ end
52
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "badline/vic20/bus/saved_state"
3
+ require "badline/vic20/bus/keyed_state"
4
4
 
5
5
  module Badline
6
6
  class Vic20
@@ -29,7 +29,6 @@ module Badline
29
29
  # Writes to ROM and to empty spots go nowhere.
30
30
  class Bus
31
31
  include Addressable
32
- include SavedState
33
32
 
34
33
  # The pages each expansion block covers, as [first page, count].
35
34
  BLOCKS = {