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,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "badline/chips/ted"
4
+ require "badline/chips/tia"
5
+ require "badline/plus4/model"
6
+ require "badline/plus4/cpu"
7
+ require "badline/plus4/cpu_port"
8
+ require "badline/plus4/keyboard_port"
9
+ require "badline/plus4/user_port"
10
+ require "badline/plus4/acia"
11
+ require "badline/plus4/bus"
12
+
13
+ module Badline
14
+ # The Commodore 264 series: the Plus/4, the C16 and the C116. A 7501 on
15
+ # the TED's clock, which runs it once or twice a TED cycle (Chips::TED's
16
+ # Beam), the TED and, on the Plus/4, a 6551 ACIA and the user port's
17
+ # 6529. Only the TED pulls IRQ, and nothing pulls NMI, which the 7501
18
+ # doesn't have.
19
+ #
20
+ # It runs headless only: the TED paints no picture yet. Device 8
21
+ # serves disks through the KERNAL traps (C64::KernalTraps), and
22
+ # cartridges go in cartridge 1's slots.
23
+ class Plus4
24
+ include IntegerHelper
25
+ include Machine::KeyboardBuffer
26
+ include Machine::Clocking
27
+ include C64::KernalTraps
28
+
29
+ # The cycle by which the KERNAL has booted and BASIC printed READY,
30
+ # with a margin, in TED cycles. READY is up by about 116,000 on a PAL
31
+ # Plus/4, 126,000 on an NTSC one and sooner on a C16.
32
+ INIT_THRESHOLD = 200_000
33
+
34
+ attr_reader :model, :bus, :cpu, :ted, :port, :keyboard, :keyboard_port, :acia, :user_port, :datasette, :tias,
35
+ :iec_bus, :cycles, :ram_kb
36
+
37
+ def family = :plus4
38
+
39
+ def init_threshold = INIT_THRESHOLD
40
+
41
+ def ram = @bus.ram
42
+
43
+ # Reads and writes memory as the CPU sees it, through the bus.
44
+ def peek(addr) = @bus.peek(addr)
45
+
46
+ def poke(addr, value) = @bus.poke(addr, value)
47
+
48
+ # +model+ names one of Model::ALL, or c116 for the C16, and +ram+ the
49
+ # RAM in K, one of Model::RAM_SIZES, when not the model's own. +tcbm+
50
+ # plugs in a 1551's TCBM interface, the TIAs at $FEC0-$FEFF, with no
51
+ # drive answering on the other side.
52
+ def initialize(model: "plus4", ram: nil, tcbm: false, debug: false)
53
+ @model = Model.named(model)
54
+ @ram_kb = ram || @model.ram_kb
55
+ raise ArgumentError, "no Plus/4 RAM size of #{@ram_kb}K" unless Model::RAM_SIZES.include?(@ram_kb)
56
+
57
+ @ted = Chips::TED.new(@model.ted)
58
+ @keyboard = Input::Keyboard.new(matrix: KeyboardPort::MATRIX)
59
+ @keyboard_port = KeyboardPort.new(@keyboard)
60
+ @ted.keyboard_port = @keyboard_port
61
+ @iec_bus = Drives::SerialBus.new
62
+ @datasette = Drives::Datasette.new(level: true)
63
+ @port = CPUPort.new(serial_bus: @iec_bus, datasette: @datasette)
64
+ plus4_parts = @model.plus4_parts
65
+ @acia = plus4_parts ? ACIA.new : nil
66
+ @user_port = plus4_parts ? UserPort.new(@datasette) : nil
67
+ io = Bus::IOPage.new(keyboard_port: @keyboard_port, acia: @acia, user_port: @user_port)
68
+ @bus = Bus.new(ted: @ted, port: @port, io:, model: @model, ram_kb: @ram_kb)
69
+ @tias = tcbm ? [Chips::TIA.new, Chips::TIA.new] : []
70
+ @bus.plug_tcbm(drive8: @tias[0], drive9: @tias[1]) if tcbm
71
+ @cpu = CPU.new(@bus, debug:)
72
+ @cycles = 0
73
+ @init_handlers = []
74
+ @pending_keys = nil
75
+ @dos = nil
76
+ @serial_trap = nil
77
+ @save_trap = nil
78
+ @capture_output = nil
79
+ end
80
+
81
+ # One TED cycle: the TED moves on and decides the CPU's clock, then the
82
+ # CPU runs its cycles in it, none while the TED holds the bus.
83
+ def cycle!
84
+ handle_init if @cycles == INIT_THRESHOLD
85
+ feed_keyboard if @pending_keys
86
+
87
+ ted = @ted
88
+ ted.cycle!
89
+ @datasette.cycle!
90
+ @cpu.irq = ted.irq?
91
+ slots = ted.cpu_slots
92
+ if slots.zero?
93
+ @cpu.stall!
94
+ else
95
+ clock_cpu(ted)
96
+ clock_cpu(ted) if slots == 2
97
+ end
98
+
99
+ @cycles += 1
100
+ end
101
+
102
+ # The RESET line: the CPU, the port, the 6529s, the ACIA, the bank
103
+ # latch and ROM selection. The TED has no reset pin.
104
+ def reset!
105
+ @port.reset!
106
+ @keyboard_port.reset!
107
+ @user_port&.reset!
108
+ @dos&.reset!
109
+ @acia&.reset!
110
+ @tias.each(&:reset!)
111
+ @bus.reset!
112
+ @cpu.reset!
113
+ end
114
+
115
+ # Switches the machine off and on: RAM and the TED start from their
116
+ # power-on state, and the RES line resets everything else.
117
+ def power_cycle!
118
+ @bus.power_on!
119
+ @ted.power_on!
120
+ reset!
121
+ end
122
+
123
+ # Calls the block with the exit code a VICE testprog writes to $FDCF.
124
+ def install_debug_register(&) = @bus.install_debug_register(&)
125
+
126
+ # Puts a cartridge's ROM chips in cartridge 1's slots (Bus#attach_cartridge)
127
+ # and switches the machine off and on, as a cartridge goes in with the
128
+ # power off. The KERNAL starts it at reset when it signs itself with CBM
129
+ # at $8007.
130
+ def attach_cartridge(chips)
131
+ @bus.attach_cartridge(chips)
132
+ power_cycle!
133
+ end
134
+
135
+ # Stores a PRG's bytes from its load address on, into RAM, and returns
136
+ # that address.
137
+ def load_prg(data)
138
+ load_addr = uint16(data[0], data[1])
139
+ bytes = data[2..] || []
140
+ bytes = bytes[0, 0x10000 - load_addr] if load_addr + bytes.length > 0x10000
141
+ bytes.each_with_index { |byte, i| @bus.ram_view.poke(load_addr + i, byte) }
142
+ load_addr
143
+ end
144
+
145
+ private
146
+
147
+ # The 264 KERNAL's keyboard buffer.
148
+ def keyboard_buffer = Machine::KeyboardBuffer::PLUS4_LAYOUT
149
+
150
+ # The 264 KERNAL, which the traps stand in for.
151
+ def trap_layout = KernalTrap::PLUS4_LAYOUT
152
+
153
+ def trap_bus = @bus
154
+
155
+ # Device 8, which the serial traps answer for, with no true drive on
156
+ # the bus to take it.
157
+ def serial_trap_device = KernalTrap::Routine::DEVICE
158
+
159
+ # BA stops the CPU at a read, and a write goes ahead.
160
+ def clock_cpu(ted)
161
+ ted.ba_low? && !@cpu.pending_write? ? @cpu.stall! : @cpu.cycle!
162
+ end
163
+ end
164
+ end
@@ -132,3 +132,41 @@ c128/character.rom
132
132
  The character generator ROM, 390059-01 (characters.390059-01.bin),
133
133
  8 KB: the C64's set, the same as character.rom, followed by the C128's.
134
134
  SHA-1 29ed066d513f2d5c09ff26d9166ba23c2afb2b3f.
135
+
136
+ Commodore 264 series ROMs
137
+ =========================
138
+
139
+ Downloaded from
140
+ https://www.zimmers.net/anonftp/pub/cbm/firmware/computers/plus4/
141
+ for the Plus/4, the C16 and the C116, which share BASIC and the KERNAL.
142
+ Nothing maps them yet.
143
+
144
+ plus4/basic.rom
145
+ ---------------
146
+ BASIC 3.5, 318006-01 (basic.318006-01.bin), for $8000-$BFFF in ROM
147
+ bank 0.
148
+ SHA-1 161c96b4ad20f3a4f2321808e37a5ded26a135dd.
149
+
150
+ plus4/kernal-pal.rom
151
+ --------------------
152
+ The PAL KERNAL, 318004-05 (kernal.318004-05.bin), for $C000-$FFFF in
153
+ ROM bank 0, with the character set from $D000.
154
+ SHA-1 7c7e07f016391174a557e790c4ef1cbe33512cdb.
155
+
156
+ plus4/kernal-ntsc.rom
157
+ ---------------------
158
+ The NTSC KERNAL, 318005-05 (kernal.318005-05.bin), for $C000-$FFFF in
159
+ ROM bank 0.
160
+ SHA-1 a3d9e5be091b98de39a046ab167fb7632d053682.
161
+
162
+ plus4/function-low.rom
163
+ ----------------------
164
+ The Plus/4's 3-plus-1 function ROM, low half, 317053-01
165
+ (3-plus-1.317053-01.bin), for $8000-$BFFF in ROM bank 1.
166
+ SHA-1 3b69f6e7cb4c18bb08e203fb18b7dabfa853390f.
167
+
168
+ plus4/function-high.rom
169
+ -----------------------
170
+ The Plus/4's 3-plus-1 function ROM, high half, 317054-01
171
+ (3-plus-1.317054-01.bin), for $C000-$FFFF in ROM bank 1.
172
+ SHA-1 0ad7ac2db7da692d972e586ca0dfd747d82c7693.
Binary file
@@ -7,31 +7,20 @@ module Badline
7
7
  C128Setup = Data.define(:model, :sid_model, :mode)
8
8
 
9
9
  class C128Setup
10
- MARKER = "C128"
11
10
  SID_MODELS = %i[mos6581 mos8580].freeze
12
11
  MODES = %i[c64 c128].freeze
13
12
 
14
- # The setup a C128's State starts with.
15
- def self.from(state)
16
- input = StateReader.new(state)
17
- input.marker(MARKER)
18
- input.check_stamp
19
- read(input)
13
+ # The model, the SID and the mode by name, in a keyed snapshot module.
14
+ def write_fields(out)
15
+ out.text("model", model).text("sid_model", sid_model.to_s).text("mode", mode.to_s)
20
16
  end
21
17
 
22
- def self.read(input)
23
- model = input.string
24
- known = C128::Model::ALL.any? { |candidate| candidate.name == model }
25
- raise FormatError, "the state names a C128 model badline doesn't know" unless known
26
-
27
- sid_model = SID_MODELS.fetch(input.int)
28
- new(model:, sid_model:, mode: MODES.fetch(input.int))
29
- rescue IndexError
30
- raise FormatError, "the state names a SID or a mode badline doesn't know"
31
- end
32
-
33
- def write(out)
34
- out.string(model).int(SID_MODELS.index(sid_model)).int(MODES.index(mode))
18
+ def self.read_fields(input)
19
+ model = input.choice("model", C128::Model::ALL.map(&:name), "c128")
20
+ sid = input.choice("sid_model", SID_MODELS.map(&:to_s), "mos6581")
21
+ mode = input.choice("mode", MODES.map(&:to_s), "c64")
22
+ new(model:, sid_model: SID_MODELS.find { |name| name.to_s == sid } || :mos6581,
23
+ mode: MODES.find { |name| name.to_s == mode } || :c64)
35
24
  end
36
25
 
37
26
  # A machine built this way, at power-on.
@@ -9,7 +9,6 @@ module Badline
9
9
  :board)
10
10
 
11
11
  class C64Setup
12
- MARKER = "COMPUTER"
13
12
  SID_MODELS = %i[mos6581 mos8580].freeze
14
13
  REGIONS = [Region::PAL, Region::NTSC, Region::NTSC_OLD, Region::DREAN].freeze
15
14
  RAM_EXPANSIONS = [:none, *C64::BankedRAM::TYPES.keys].freeze
@@ -22,23 +21,6 @@ module Badline
22
21
  kernal: address_bus.kernal, datasette: address_bus.datasette.connected?, board: address_bus.board)
23
22
  end
24
23
 
25
- # The setup a C64's State starts with.
26
- def self.from(state)
27
- input = StateReader.new(state)
28
- input.marker(MARKER)
29
- input.check_stamp
30
- read(input)
31
- end
32
-
33
- def self.read(input)
34
- new(vic_model: Chips::VIC::MODELS.fetch(input.int), cia_model: Chips::CIA::MODELS.fetch(input.int),
35
- sid_model: SID_MODELS.fetch(input.int), region: REGIONS.fetch(input.int),
36
- ram_expansion: ram_expansion(RAM_EXPANSIONS.fetch(input.int)), reu: reu(input.int),
37
- kernal: KERNALS.fetch(input.int), datasette: input.boolean?, board: BOARDS.fetch(input.int))
38
- rescue IndexError
39
- raise FormatError, "the state names a chip model, region, RAM expansion, KERNAL or board badline doesn't know"
40
- end
41
-
42
24
  def self.ram_expansion(name) = name == :none ? nil : name
43
25
 
44
26
  # An REU's size in K, written as 0 for none.
@@ -46,14 +28,36 @@ module Badline
46
28
  return nil if size_kb.zero?
47
29
  return size_kb if C64::REU::SIZES_KB.include?(size_kb)
48
30
 
49
- raise FormatError, "the state names a #{size_kb}K REU, which badline doesn't build"
31
+ raise FormatError, "the snapshot names a #{size_kb}K REU, which badline doesn't build"
32
+ end
33
+
34
+ # The setup in a keyed snapshot module (Snapshot::Keyed), with each
35
+ # model, the region, the RAM expansion, the KERNAL and the board by
36
+ # name.
37
+ def write_fields(out)
38
+ out.text("vic_model", vic_model.to_s).text("cia_model", cia_model.to_s).text("sid_model", sid_model.to_s)
39
+ out.text("region", region.name.to_s).text("ram_expansion", (ram_expansion || :none).to_s)
40
+ out.int("reu_kb", reu || 0).text("kernal", kernal.to_s).bool("datasette", datasette)
41
+ out.text("board", board.to_s)
42
+ end
43
+
44
+ # The setup a keyed snapshot module names, a C64's at power-on for
45
+ # anything it leaves out or names that badline doesn't build.
46
+ def self.read_fields(input)
47
+ region = named(input, "region", REGIONS.map(&:name), :pal)
48
+ new(vic_model: named(input, "vic_model", Chips::VIC::MODELS, :mos6569),
49
+ cia_model: named(input, "cia_model", Chips::CIA::MODELS, :mos6526),
50
+ sid_model: named(input, "sid_model", SID_MODELS, :mos6581),
51
+ region: REGIONS.find { |each| each.name == region } || Region::PAL,
52
+ ram_expansion: ram_expansion(named(input, "ram_expansion", RAM_EXPANSIONS, :none)),
53
+ reu: reu(input.int("reu_kb", 0)), kernal: named(input, "kernal", KERNALS, :c64),
54
+ datasette: input.bool("datasette", true), board: named(input, "board", BOARDS, :c64))
50
55
  end
51
56
 
52
- def write(out)
53
- out.int(Chips::VIC::MODELS.index(vic_model)).int(Chips::CIA::MODELS.index(cia_model))
54
- out.int(SID_MODELS.index(sid_model)).int(REGIONS.index(region))
55
- out.int(RAM_EXPANSIONS.index(ram_expansion || :none)).int(reu || 0)
56
- out.int(KERNALS.index(kernal)).boolean(datasette).int(BOARDS.index(board))
57
+ # The Symbol of `names` the text at `key` names.
58
+ def self.named(input, key, names, default)
59
+ text = input.choice(key, names.map(&:to_s), default.to_s)
60
+ names.find { |name| name.to_s == text } || default
57
61
  end
58
62
 
59
63
  # A machine built this way, at power-on.
@@ -4,6 +4,10 @@ require "zlib"
4
4
 
5
5
  module Badline
6
6
  module Snapshot
7
+ # A file that isn't a snapshot badline reads, is cut short, or is of a
8
+ # machine built another way.
9
+ class FormatError < StandardError; end
10
+
7
11
  # One module of a snapshot: its name, its version and its payload,
8
12
  # without the 22-byte module header.
9
13
  Section = Data.define(:name, :major, :minor, :data) do
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Snapshot
5
+ module FieldDoc
6
+ # The doc's markdown: one table per module, of each key, its type and
7
+ # its default, with keys that differ only in a unit's number, such as
8
+ # voice1 to voice3, on one row.
9
+ class Markdown
10
+ NAMES = { "c64" => "C64", "vic20" => "VIC-20", "c128" => "C128" }.freeze
11
+ TAG_TYPES = { Keyed::INT => "int", Keyed::BOOL => "bool", Keyed::BYTES => "bytes", Keyed::INTS => "ints",
12
+ Keyed::TEXT => "text" }.freeze
13
+
14
+ # A row: the key, the type, and the defaults each family whose
15
+ # machines read or write it reads it with.
16
+ Row = Struct.new(:key, :type, :defaults)
17
+
18
+ INTRO = <<~MARKDOWN
19
+ # Snapshot format
20
+
21
+ This file is generated by `rake snapshot:fields` from the code that
22
+ loads a snapshot. Don't edit it by hand. doc/snapshots.md covers
23
+ what a snapshot holds and which versions load.
24
+
25
+ A snapshot is a VICE `.vsf` container. The machine is in modules
26
+ named `BL-` plus a unit, each a deflated list of records: a key, a
27
+ type and a value. The types are `int` (a zigzag BER integer), `bool`,
28
+ `bytes`, `ints` (a list of integers) and `text`. A load skips a key
29
+ it doesn't know, and a key that isn't there leaves the value a
30
+ machine built afresh has: the default below. `word`, `byte` and
31
+ `int & 0x..` are ints cut to the latch's width. A text that lists
32
+ its values takes one of them. An optional int is absent when the
33
+ latch holds nothing. A true drive's modules are named from its
34
+ device number, `BL-D8` to `BL-D11`. Keys with `<N>` stand for each
35
+ numbered unit. A default of "written only" is a key the load
36
+ doesn't read, or reads only when the snapshot has it. Where the
37
+ machines differ, the default is given for each, and a key only
38
+ some of them have names them.
39
+ MARKDOWN
40
+
41
+ def initialize(loads)
42
+ @modules = {}
43
+ loads.each { |load| add(load) }
44
+ end
45
+
46
+ def to_s
47
+ sections = @modules.map do |name, rows|
48
+ families = rows.values.flat_map { |row| row.defaults.keys }.uniq
49
+ ["## #{name}", "", "Written by the #{families.map { |family| NAMES.fetch(family) }.join(', ')}.", "",
50
+ "| Key | Type | Default |", "| --- | --- | --- |",
51
+ *rows.values.map { |row| line(row, families) }].join("\n")
52
+ end
53
+ "#{INTRO}\n#{sections.join("\n\n")}\n"
54
+ end
55
+
56
+ private
57
+
58
+ def add(load)
59
+ load.fields.each do |name, fields|
60
+ numbered = numbered_keys(load.written[name].keys + fields.map(&:key))
61
+ fields.each { |field| note(name, numbered.fetch(field.key, field.key), field.type, load.family, field) }
62
+ read = fields.to_set(&:key)
63
+ load.written[name].each do |key, tag|
64
+ next if read.include?(key)
65
+
66
+ note(name, numbered.fetch(key, key), TAG_TYPES.fetch(tag, "?"), load.family, nil)
67
+ end
68
+ end
69
+ end
70
+
71
+ def note(name, key, type, family, field)
72
+ row = (@modules[name] ||= {})[key] ||= Row.new(key, type, {})
73
+ (row.defaults[family] ||= []) << (field ? brief(field.default, field.type) : "written only")
74
+ end
75
+
76
+ # Keys that differ only in a number, as the same key with <N>: each
77
+ # key whose number another key in the module shares the rest of.
78
+ def numbered_keys(keys)
79
+ groups = keys.uniq.group_by { |key| key.gsub(/(?<=[a-z])\d+(?=\.|\z)/, "<N>") }
80
+ groups.each_with_object({}) do |(general, members), numbered|
81
+ members.each { |key| numbered[key] = general } if members.length > 1
82
+ end
83
+ end
84
+
85
+ def line(row, families)
86
+ each = row.defaults.transform_values { |defaults| summary(defaults.uniq) }
87
+ default = if each.values.uniq.length == 1 && each.length == families.length then each.values.first
88
+ else each.map { |family, value| "#{NAMES.fetch(family)}: #{value}" }.join("; ")
89
+ end
90
+ "| `#{row.key}` | #{row.type} | #{default} |"
91
+ end
92
+
93
+ def summary(defaults) = defaults.length > 3 ? "varies" : defaults.join(" or ")
94
+
95
+ def brief(value, type)
96
+ case value
97
+ when nil then "absent"
98
+ when Array then list(value)
99
+ when String then type.start_with?("bytes") ? list(value.bytes) : value.inspect
100
+ else value.to_s
101
+ end
102
+ end
103
+
104
+ def list(values)
105
+ return values.inspect if values.length <= 8
106
+
107
+ values.uniq.length == 1 ? "#{values.length} × #{values.first}" : "#{values.length} values"
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "badline"
4
+ require "badline/c128"
5
+
6
+ module Badline
7
+ module Snapshot
8
+ # doc/snapshot-format.md, written by running the code: each family's
9
+ # machine is saved with its attachments, and its modules loaded into a
10
+ # machine built the same way at power-on through Keyed::Recorders,
11
+ # which note every key read with its type and the value the new machine
12
+ # had, its default. `rake snapshot:fields` writes it, and a spec checks
13
+ # that the file matches.
14
+ module FieldDoc
15
+ PATH = File.expand_path("../../../doc/snapshot-format.md", __dir__)
16
+
17
+ # Modules read through Recorders.
18
+ class RecordingModules < Keyed::Modules
19
+ private
20
+
21
+ def parse(section) = Keyed::Recorder.parse(section)
22
+
23
+ def missing(name) = Keyed::Recorder.missing(name)
24
+ end
25
+
26
+ # One machine saved and loaded: the family it belongs to, and for
27
+ # each module the keys its records hold, with their tags, and the fields the load read.
28
+ Load = Data.define(:family, :written, :fields)
29
+
30
+ # The machines the doc is made from: each family at power-on, and the
31
+ # C64 with each kind of expansion, cartridge and media.
32
+ def self.machines
33
+ {
34
+ "c64" => [C64.new, C64.new(ram_expansion: :plus256k), C64.new(reu: 512), with_cartridge, with_trap_dos,
35
+ with_tape, with_drive(:d64), with_drive(:d81)],
36
+ "vic20" => [Vic20.new, with_rom],
37
+ "c128" => [C128.new(mode: :c128), with_drive(:d71)]
38
+ }
39
+ end
40
+
41
+ def self.blank(length) = Array.new(length, 0)
42
+
43
+ def self.with_cartridge
44
+ chips = [0x8000, 0xa000].map do |address|
45
+ Storage::CRTFile::Chip.new(chip_type: 2, bank: 0, address:, data: blank(0x2000))
46
+ end
47
+ image = Storage::CRTFile::Image.new(hardware_type: 32, subtype: 0, exrom: 1, game: 0, name: "DOC", chips:)
48
+ C64.new.tap do |machine|
49
+ machine.attach_cartridge(C64::Cartridge.from_crt(Storage::CRTFile.new(bytes: Storage::CRTFile.encode(image))))
50
+ end
51
+ end
52
+
53
+ def self.with_trap_dos
54
+ C64.new.tap { |machine| machine.mount(Storage::D64Image.new("", read_only: true, bytes: blank(174_848))) }
55
+ end
56
+
57
+ def self.with_tape
58
+ bytes = "C64-TAPE-RAW".bytes + [1, 0, 0, 0, 4, 0, 0, 0, 0x30, 0x30, 0x30, 0x30]
59
+ C64.new.tap { |machine| machine.datasette.insert(Storage::TAP.new("", bytes:)) }
60
+ end
61
+
62
+ def self.with_drive(kind)
63
+ case kind
64
+ when :d81
65
+ C64.new.tap do |machine|
66
+ machine.plug_drive1581.insert(Drives::Model1581::Disk.new(Storage::D81Image.new("", bytes: blank(819_200))))
67
+ end
68
+ when :d71
69
+ C128.new(mode: :c128).tap do |machine|
70
+ image = Storage::D71Image.new("", bytes: blank(349_696))
71
+ machine.plug_true_drive.insert(Drives::Model1541::Disk.decode(image))
72
+ end
73
+ else
74
+ C64.new.tap do |machine|
75
+ image = Storage::D64Image.new("", bytes: blank(174_848))
76
+ machine.plug_true_drive.insert(Drives::Model1541::Disk.decode(image))
77
+ end
78
+ end
79
+ end
80
+
81
+ def self.with_rom
82
+ Vic20.new.tap { |machine| machine.bus.map_rom(0xa000, blank(0x2000)) }
83
+ end
84
+
85
+ # Saves `machine` and loads it into a new one through Recorders.
86
+ def self.load(family, machine)
87
+ modules = RecordingModules.new(Image.of(machine).sections)
88
+ Image.setup(modules.machine).build.load_modules(modules)
89
+ modules.machine.text("written_by", "")
90
+ written = {}
91
+ fields = {}
92
+ modules.readers.each do |reader|
93
+ written[reader.name] = reader.tags
94
+ fields[reader.name] = reader.fields
95
+ end
96
+ Load.new(family:, written:, fields:)
97
+ end
98
+
99
+ def self.loads = machines.flat_map { |family, list| list.map { |machine| load(family, machine) } }
100
+
101
+ # The doc, as markdown.
102
+ def self.markdown(loads = self.loads) = Markdown.new(loads).to_s
103
+
104
+ def self.write(path = PATH) = File.write(path, markdown)
105
+ end
106
+ end
107
+ end
108
+
109
+ require "badline/snapshot/field_doc/markdown"