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
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Chips
5
+ class TED
6
+ class Sound
7
+ # The sound's fields in a keyed snapshot module (Snapshot::Keyed,
8
+ # BL-TEDSOUND): each voice's output and when it next toggles, in
9
+ # cycles from the machine's clock, absent while it never will, the
10
+ # noise's shift register, and how far behind the clock the voices
11
+ # last ran. The frequencies, the volume and the reload bit come from
12
+ # the TED's registers on load (#restored!), and the output's level
13
+ # from those.
14
+ module KeyedState
15
+ def save_fields(out)
16
+ now = @clock.cycles
17
+ run_to(now)
18
+ VOICES.times do |voice|
19
+ due = @next_toggles[voice]
20
+ out.int("voice#{voice + 1}.phase", @phases[voice])
21
+ out.optional_int("voice#{voice + 1}.toggle_in", due >= NEVER ? nil : due - now)
22
+ end
23
+ out.int("noise", @noise).int("ran_ago", now - @time)
24
+ end
25
+
26
+ def load_fields(input)
27
+ now = @clock.cycles
28
+ VOICES.times do |voice|
29
+ @phases[voice] = input.masked("voice#{voice + 1}.phase", @phases[voice], 0x01)
30
+ due = input.optional_int("voice#{voice + 1}.toggle_in")
31
+ @next_toggles[voice] = due.nil? ? NEVER : now + due
32
+ end
33
+ @noise = input.byte("noise", @noise)
34
+ @time = now - input.int("ran_ago", now - @time)
35
+ end
36
+
37
+ # The frequencies, the volume and the reload bit as `ted`'s
38
+ # registers $FF0E-$FF12 hold them, and the level they give.
39
+ def restored!(ted)
40
+ @frequencies[0] = ted.peek(0xff0e) | ((ted.peek(0xff12) & 0x03) << 8)
41
+ @frequencies[1] = ted.peek(0xff0f) | ((ted.peek(0xff10) & 0x03) << 8)
42
+ VOICES.times { |voice| @intervals[voice] = interval_for(@frequencies[voice]) }
43
+ control = ted.peek(0xff11)
44
+ @control = control
45
+ @reloading = control >= 0x80
46
+ @volume = [control & 0x0f, 8].min
47
+ @level = level_now
48
+ end
49
+ end
50
+
51
+ include KeyedState
52
+ end
53
+
54
+ # The DMA a line makes, as a snapshot names it, in Beam's order.
55
+ DMA_KINDS = %w[none attributes pointers].freeze
56
+
57
+ # The shape of the line going out, as the picture paints it. A
58
+ # snapshot whose line.layout differs repaints the line from its
59
+ # start.
60
+ LINE_LAYOUT = 1
61
+
62
+ # The TED's fields in a keyed snapshot module (Snapshot::Keyed,
63
+ # BL-TED): the registers as written, the interrupt flags, the keyboard
64
+ # latch, the blink counter, the timers, the beam and the clock it gave
65
+ # the CPU last, the fetches' buffers and counters, and the picture:
66
+ # where the scanline is, whether the window is open, how far the line
67
+ # going out is painted (line.*), and the picture itself. The interrupt
68
+ # mask and the raster compare come from $FF0A and $FF0B on load.
69
+ module KeyedState
70
+ def save_fields(out)
71
+ out.bytes("registers", @registers).int("interrupt_flags", @interrupt_flags).int("key_latch", @key_latch)
72
+ out.int("blink", @blink).bool("blink_on", @blink_on)
73
+ save_timer_fields(out)
74
+ out.int("column", @column).int("line", @line).bool("ba", @ba_low).int("cpu_slots", @cpu_slots)
75
+ save_fetch_fields(out)
76
+ out.int("scanline", @scanline).bool("window_open", @window_open)
77
+ out.int("line.layout", LINE_LAYOUT).int("line.painted", @painted).bytes("display", @display)
78
+ end
79
+
80
+ def load_fields(input)
81
+ input.bytes_into("registers", @registers)
82
+ @interrupt_flags = input.byte("interrupt_flags", @interrupt_flags)
83
+ @key_latch = input.byte("key_latch", @key_latch)
84
+ @blink = input.masked("blink", @blink, 0x0f)
85
+ @blink_on = input.bool("blink_on", @blink_on)
86
+ load_timer_fields(input)
87
+ @column = input.int("column", @column) % Beam::CYCLES_PER_LINE
88
+ @line = input.masked("line", @line, 0x1ff)
89
+ @ba_low = input.bool("ba", @ba_low)
90
+ @cpu_slots = input.int("cpu_slots", @cpu_slots).clamp(0, 2)
91
+ load_fetch_fields(input)
92
+ @scanline = input.int("scanline", @scanline)
93
+ @window_open = input.bool("window_open", @window_open)
94
+ load_line_fields(input)
95
+ input.bytes_into("display", @display)
96
+ end
97
+
98
+ # The interrupt mask and the raster compare from $FF0A and $FF0B,
99
+ # and every line of the picture changed.
100
+ def restored!
101
+ mask = @registers[0x0a]
102
+ @interrupt_mask = mask & 0x5f
103
+ @raster_compare = ((mask & 0x01) << 8) | @registers[0x0b]
104
+ @dirty_lines.fill(true)
105
+ end
106
+
107
+ private
108
+
109
+ def save_timer_fields(out)
110
+ 3.times do |index|
111
+ out.int("timer#{index + 1}.count", @timer_counts[index])
112
+ out.bool("timer#{index + 1}.running", @timer_running[index])
113
+ end
114
+ out.int("timer1.reload", @timer1_reload)
115
+ end
116
+
117
+ def load_timer_fields(input)
118
+ 3.times do |index|
119
+ @timer_counts[index] = input.word("timer#{index + 1}.count", @timer_counts[index])
120
+ @timer_running[index] = input.bool("timer#{index + 1}.running", @timer_running[index])
121
+ end
122
+ @timer1_reload = input.word("timer1.reload", @timer1_reload)
123
+ end
124
+
125
+ def save_fetch_fields(out)
126
+ out.bytes("fetch.patterns", @patterns).bytes("fetch.pointers", @pointers)
127
+ out.bytes("fetch.attributes", @attributes).bytes("fetch.next_attributes", @fetched_attributes)
128
+ out.int("fetch.char_position", @char_position).int("fetch.line_position", @line_position)
129
+ out.int("fetch.row_line", @row_line).bool("fetch.row_active", @row_active)
130
+ out.text("fetch.dma", DMA_KINDS.fetch(@dma_kind)).bool("fetch.patterns_on", @pattern_fetches)
131
+ end
132
+
133
+ def load_fetch_fields(input)
134
+ input.bytes_into("fetch.patterns", @patterns)
135
+ input.bytes_into("fetch.pointers", @pointers)
136
+ input.bytes_into("fetch.attributes", @attributes)
137
+ input.bytes_into("fetch.next_attributes", @fetched_attributes)
138
+ @char_position = input.masked("fetch.char_position", @char_position, 0x3ff)
139
+ @line_position = input.masked("fetch.line_position", @line_position, 0x3ff)
140
+ @row_line = input.masked("fetch.row_line", @row_line, 0x07)
141
+ @row_active = input.bool("fetch.row_active", @row_active)
142
+ @dma_kind = DMA_KINDS.index(input.choice("fetch.dma", DMA_KINDS, DMA_KINDS.fetch(@dma_kind))) || 0
143
+ @pattern_fetches = input.bool("fetch.patterns_on", @pattern_fetches)
144
+ end
145
+
146
+ def load_line_fields(input)
147
+ line = input.int("line.layout", 0) == LINE_LAYOUT
148
+ input.adjust("line in progress from another renderer, restarted") unless line || input.missing?
149
+ @painted = line ? input.int("line.painted", @painted).clamp(0, Picture::WIDTH) : 0
150
+ end
151
+ end
152
+
153
+ include KeyedState
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Chips
5
+ class TED
6
+ # The colours a character's pixels take in each mode, by $FF06 bits 5
7
+ # (bitmap) and 6 (extended colour) and $FF07 bit 4 (multicolour), with
8
+ # a pattern byte's set bits as 1 (the datasheet's "Character modes"
9
+ # and "Bit map mode", Härsfalvi's "Talking to TED"). Each was checked
10
+ # against xplus4 screenshots of the same registers and memory.
11
+ #
12
+ # - Text: 1 is the attribute's colour and luminance, bits 0-6, and 0
13
+ # the background, $FF15. With $FF07 bit 7 clear a pointer's bit 7
14
+ # reverses the character. The cursor ($FF0C-$FF0D, a character
15
+ # position) reverses its character, and an attribute's bit 7 makes
16
+ # its character flash to the background, each in turns of 16 frames
17
+ # of the blink counter.
18
+ # - Multicolour text: a character whose attribute has bit 3 set takes
19
+ # its bits in pairs of double-width pixels, 00 the background, 01
20
+ # $FF16, 10 $FF17 and 11 the attribute's bits 0-2 and 4-6. Other
21
+ # characters show as in text. No reverse, cursor or flash.
22
+ # - Extended colour: the pointer's bits 6-7 pick the background,
23
+ # $FF15-$FF18. No reverse, cursor or flash.
24
+ # - Bitmap: 0 takes its colour from the video matrix's low nibble and
25
+ # its luminance from the attribute's bits 4-6, 1 from the matrix's
26
+ # high nibble and the attribute's bits 0-2.
27
+ # - Multicolour bitmap: 00 the background, 01 the bitmap's 1, 10 its
28
+ # 0, 11 $FF16, as xplus4 shows them. The datasheet gives 01 the
29
+ # luminance of bits 4-6 as well.
30
+ # - Extended colour with either of the others: black.
31
+ module Modes
32
+ TEXT = 0x00
33
+ MULTICOLOR_TEXT = 0x10
34
+ BITMAP = 0x20
35
+ MULTICOLOR_BITMAP = 0x30
36
+ EXTENDED_COLOR = 0x40
37
+
38
+ private
39
+
40
+ def reset_modes
41
+ @pixel_bits = 0
42
+ @pixel_colors = Array.new(4, 0)
43
+ @multicolor_pixels = false
44
+ @blink_on = false
45
+ end
46
+
47
+ # The mode, $FF06 bits 5 and 6 with $FF07 bit 4.
48
+ def mode = (@registers[0x06] & 0x60) | (@registers[0x07] & 0x10)
49
+
50
+ # Sets the pixel bits and colours of character +index+ in +mode+.
51
+ def character_colors(index, mode)
52
+ pattern = @patterns[index]
53
+ pointer = @pointers[index]
54
+ attribute = @attributes[index]
55
+ case mode
56
+ when TEXT then text_colors(index, pattern, pointer, attribute)
57
+ when MULTICOLOR_TEXT then multicolor_text_colors(pattern, attribute)
58
+ when BITMAP then bitmap_colors(pattern, pointer, attribute)
59
+ when MULTICOLOR_BITMAP then multicolor_bitmap_colors(pattern, pointer, attribute)
60
+ when EXTENDED_COLOR then hires(pattern, @registers[0x15 + (pointer >> 6)] & 0x7f, attribute & 0x7f)
61
+ else hires(0, 0, 0)
62
+ end
63
+ end
64
+
65
+ # Paints pixels +from+ to +to+ of the line at +base+, of a character
66
+ # whose first pixel is +origin+, in the colours last set.
67
+ def paint_pixels(base, from, to, origin)
68
+ display = @display
69
+ bits = @pixel_bits
70
+ colors = @pixel_colors
71
+ multicolor = @multicolor_pixels
72
+ x = from
73
+ while x < to
74
+ offset = x - origin
75
+ color = multicolor ? colors[(bits >> (6 - (offset & 0x06))) & 0x03] : colors[(bits >> (7 - offset)) & 0x01]
76
+ display[base + x] = color
77
+ x += 1
78
+ end
79
+ end
80
+
81
+ def hires(pattern, background, foreground)
82
+ @pixel_bits = pattern
83
+ @pixel_colors[0] = background
84
+ @pixel_colors[1] = foreground
85
+ @multicolor_pixels = false
86
+ end
87
+
88
+ def multicolor(pattern, color1, color2, color3)
89
+ @pixel_bits = pattern
90
+ colors = @pixel_colors
91
+ colors[0] = @registers[0x15] & 0x7f
92
+ colors[1] = color1
93
+ colors[2] = color2
94
+ colors[3] = color3
95
+ @multicolor_pixels = true
96
+ end
97
+
98
+ def text_colors(index, pattern, pointer, attribute)
99
+ pattern ^= 0xff if pointer >= 0x80 && !@registers[0x07].anybits?(0x80)
100
+ if @blink_on
101
+ pattern ^= 0xff if ((@line_position + index) & 0x3ff) == cursor_position
102
+ elsif attribute >= 0x80
103
+ pattern = 0
104
+ end
105
+ hires(pattern, @registers[0x15] & 0x7f, attribute & 0x7f)
106
+ end
107
+
108
+ def cursor_position = ((@registers[0x0c] & 0x03) << 8) | @registers[0x0d]
109
+
110
+ def multicolor_text_colors(pattern, attribute)
111
+ if attribute.anybits?(0x08)
112
+ multicolor(pattern, @registers[0x16] & 0x7f, @registers[0x17] & 0x7f, attribute & 0x77)
113
+ else
114
+ hires(pattern, @registers[0x15] & 0x7f, attribute & 0x7f)
115
+ end
116
+ end
117
+
118
+ def bitmap_colors(pattern, pointer, attribute)
119
+ hires(pattern, (pointer & 0x0f) | (attribute & 0x70), (pointer >> 4) | ((attribute & 0x07) << 4))
120
+ end
121
+
122
+ def multicolor_bitmap_colors(pattern, pointer, attribute)
123
+ multicolor(pattern, (pointer >> 4) | ((attribute & 0x07) << 4), (pointer & 0x0f) | (attribute & 0x70),
124
+ @registers[0x16] & 0x7f)
125
+ end
126
+
127
+ # The blink counter's carry turns the cursor and flash over.
128
+ def blink_carry
129
+ @blink_on = !@blink_on
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Chips
5
+ class TED
6
+ # The TED's 128 colours.
7
+ module Palette
8
+ # The colours by index, (luminance << 4) | colour, as RGB, from the
9
+ # Plus/4 hardware manual's TED chapter. Luma comes from each
10
+ # luminance level's voltage (4.4.3): level 0, 2.0 V, is black, the
11
+ # eight luminances are levels 1-8, and 4.8 V is white. Chroma takes
12
+ # the PAL column's hue phase (4.4.4), with U along 0 degrees and V
13
+ # along 90, at an amplitude of half CHROMA OUT's 1 Vp-p over the
14
+ # 2.8 V from black to white (4.4.2). Colour 0 is black at every
15
+ # luminance and colour 1 has no chroma. BT.601 turns them into RGB,
16
+ # B-Y = U / 0.492 and R-Y = V / 0.877, clamped. NTSC machines take
17
+ # the same palette. The datasheet's NTSC column is the PAL one less
18
+ # 33 degrees, but for orange, which it gives 129 on PAL and 90 on
19
+ # NTSC.
20
+ PALETTE = [
21
+ 0x000000, 0x242424, 0x570f10, 0x003a39, 0x4e045c, 0x004400, 0x19197f, 0x303000,
22
+ 0x4d1b00, 0x402600, 0x173d00, 0x580830, 0x00401c, 0x05266e, 0x390a7a, 0x084200,
23
+ 0x000000, 0x323232, 0x651c1d, 0x004847, 0x5c126a, 0x055205, 0x26278c, 0x3e3e00,
24
+ 0x5a2900, 0x4e3300, 0x254a00, 0x66163d, 0x004e2a, 0x13347c, 0x461787, 0x165000,
25
+ 0x000000, 0x404040, 0x722a2b, 0x0d5555, 0x692077, 0x126013, 0x34349a, 0x4b4b00,
26
+ 0x683706, 0x5b4100, 0x325800, 0x73234b, 0x0c5c38, 0x21418a, 0x542595, 0x235d00,
27
+ 0x000000, 0x525252, 0x853c3d, 0x1f6867, 0x7b328a, 0x257225, 0x4646ac, 0x5e5e00,
28
+ 0x7a4918, 0x6d5303, 0x456a00, 0x85365d, 0x1e6e4a, 0x33549c, 0x6637a7, 0x366f04,
29
+ 0x000000, 0x767676, 0xa96162, 0x448c8b, 0xa056ae, 0x49964a, 0x6b6bd1, 0x82821c,
30
+ 0x9f6d3c, 0x927828, 0x698f1d, 0xaa5a82, 0x43926e, 0x5778c0, 0x8b5ccc, 0x5a9429,
31
+ 0x000000, 0x929292, 0xc47c7d, 0x5fa7a7, 0xbb72c9, 0x64b265, 0x8686ec, 0x9d9d38,
32
+ 0xba8857, 0xad9343, 0x84aa38, 0xc5759d, 0x5eae8a, 0x7293dc, 0xa677e7, 0x75af44,
33
+ 0x000000, 0xbfbfbf, 0xf2aaaa, 0x8dd5d4, 0xe99ff7, 0x92df92, 0xb4b4ff, 0xcbcb65,
34
+ 0xe8b685, 0xdbc071, 0xb2d766, 0xf3a3cb, 0x8cdbb7, 0xa0c1ff, 0xd4a4ff, 0xa3dd72,
35
+ 0x000000, 0xffffff, 0xffe9ea, 0xccffff, 0xffdfff, 0xd2ffd2, 0xf3f3ff, 0xffffa5,
36
+ 0xfff6c5, 0xffffb1, 0xf2ffa6, 0xffe3ff, 0xcbfff7, 0xe0ffff, 0xffe4ff, 0xe3ffb1
37
+ ].freeze
38
+
39
+ def palette = PALETTE
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,182 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Chips
5
+ class TED
6
+ # The picture: a palette index a pixel (Palette), 384 pixels a line
7
+ # and every line of the frame. A line's pixels start at horizontal
8
+ # count 419, so the 40-column window, counts 451-315, runs from pixel
9
+ # 32 to 351, as in xplus4's screenshots.
10
+ #
11
+ # The picture's lines are the scanlines of the frame as they go out,
12
+ # which follow the line count until a write to $FF1C-$FF1D moves it:
13
+ # the frame's scanlines start over where the line count wraps to 0 at
14
+ # the end of the frame, and step with each line otherwise, as in
15
+ # xplus4's screenshots of TED/write-scanline-coarse.
16
+ #
17
+ # A line is painted at column 44, once its last pixel is out, from
18
+ # what the line fetched (Fetches) in the colours of its mode (Modes).
19
+ # A write to a register the picture reads paints the line up to the
20
+ # pixel the write lands on first, so the old value holds to there and
21
+ # the new one from there.
22
+ #
23
+ # Outside the window the border colour ($FF19) shows: above and below
24
+ # it, from the line count reaching line 4 to its reaching line 204 with
25
+ # $FF06 bit 3 set (25 rows), or 8 and 200 (24 rows), so a line count
26
+ # moved past line 4 leaves the frame's window shut;
27
+ # beside it, the 38-column window ($FF07 bit 3 clear) is pixels 39 to
28
+ # 342, as in xplus4's screenshots, where the datasheet's counts would
29
+ # make it 40 to 343. $FF06 bit 4 clear blanks the whole picture to the
30
+ # border colour. A line of the window with no row under way shows the
31
+ # background colour, $FF15.
32
+ #
33
+ # $FF07 bits 0-2 move the characters right by as many pixels, the
34
+ # window's pixels before the first character showing the background.
35
+ module Picture
36
+ WIDTH = 384
37
+
38
+ # The horizontal count of a line's first pixel.
39
+ FIRST_COUNT = 419
40
+
41
+ # The column whose cycle paints the line, and the first whose writes
42
+ # land on the next line.
43
+ PAINT_COLUMN = 44
44
+ NEXT_LINE_COLUMN = 52
45
+
46
+ WINDOW_LEFT = 32
47
+ WINDOW_RIGHT = 352
48
+ NARROW_LEFT = 39
49
+ NARROW_RIGHT = 343
50
+
51
+ # How many dots after its cycle's first a register write takes hold:
52
+ # where xplus4 shows a raster interrupt's colour change, a cell
53
+ # boundary with no horizontal scroll.
54
+ WRITE_DELAY = 19
55
+
56
+ # The picture, a palette index a pixel.
57
+ attr_reader :display
58
+
59
+ # Whether each line has been painted since the front end last
60
+ # cleared them.
61
+ attr_reader :dirty_lines
62
+
63
+ # Whether the TED paints its picture. It fetches either way.
64
+ attr_reader :render
65
+
66
+ def width = WIDTH
67
+
68
+ def height = lines_per_frame
69
+
70
+ def clear_dirty_lines!
71
+ @dirty_lines.fill(false)
72
+ end
73
+
74
+ def render=(value)
75
+ @render = value
76
+ end
77
+
78
+ private
79
+
80
+ def reset_picture
81
+ @display = Array.new(WIDTH * 312, 0)
82
+ @dirty_lines = Array.new(312, true)
83
+ @render = true
84
+ @painted = 0
85
+ @scanline = 0
86
+ @window_open = false
87
+ reset_modes
88
+ end
89
+
90
+ # The line count steps to +line+: the next scanline, and the
91
+ # window opens or shuts on its first and last lines.
92
+ def step_scanline(line)
93
+ @scanline = (@scanline + 1) % 312
94
+ rows25 = @registers[0x06].anybits?(0x08)
95
+ if line == (rows25 ? 4 : 8)
96
+ @window_open = true
97
+ elsif line == (rows25 ? 204 : 200)
98
+ @window_open = false
99
+ end
100
+ end
101
+
102
+ # The line count wraps to 0: the frame's first scanline.
103
+ def end_picture_frame
104
+ @scanline = -1
105
+ end
106
+
107
+ # The picture's part of a cycle in +column+.
108
+ def picture_cycle(column)
109
+ return unless column == PAINT_COLUMN
110
+
111
+ paint(WIDTH) if @render
112
+ @painted = 0
113
+ end
114
+
115
+ # Paints the line up to the pixel a register write lands on.
116
+ def paint_to_write
117
+ return unless @render
118
+
119
+ column = @column
120
+ return if column >= PAINT_COLUMN && column < NEXT_LINE_COLUMN
121
+
122
+ x = ((column * 8) + WRITE_DELAY - FIRST_COUNT) % 456
123
+ x = WIDTH if x > WIDTH
124
+ paint(x)
125
+ end
126
+
127
+ # Paints the line's pixels from where it was left up to +stop+.
128
+ def paint(stop)
129
+ start = @painted
130
+ return if stop <= start
131
+
132
+ @painted = stop
133
+ scanline = @scanline
134
+ @dirty_lines[scanline] = true
135
+ base = scanline * WIDTH
136
+ border = @registers[0x19] & 0x7f
137
+ return fill_span(base, start, stop, border) unless @window_open && @registers[0x06].anybits?(0x10)
138
+
139
+ narrow = !@registers[0x07].anybits?(0x08)
140
+ left = narrow ? NARROW_LEFT : WINDOW_LEFT
141
+ right = narrow ? NARROW_RIGHT : WINDOW_RIGHT
142
+ fill_span(base, start, stop.clamp(..left), border)
143
+ paint_window(base, start.clamp(left..), stop.clamp(..right))
144
+ fill_span(base, start.clamp(right..), stop, border)
145
+ end
146
+
147
+ def fill_span(base, start, stop, color)
148
+ @display.fill(color, base + start, stop - start) if stop > start
149
+ end
150
+
151
+ def paint_window(base, start, stop)
152
+ return if stop <= start
153
+
154
+ background = @registers[0x15] & 0x7f
155
+ return fill_span(base, start, stop, background) unless @pattern_fetches
156
+
157
+ first = WINDOW_LEFT + (@registers[0x07] & 0x07)
158
+ if start < first
159
+ fill_span(base, start, stop.clamp(..first), background)
160
+ start = first
161
+ end
162
+ paint_characters(base, start, stop, first)
163
+ end
164
+
165
+ # Paints the characters' pixels +start+ to +stop+, the first
166
+ # character's first pixel at +first+.
167
+ def paint_characters(base, start, stop, first)
168
+ mode = self.mode
169
+ x = start
170
+ while x < stop
171
+ index = (x - first) >> 3
172
+ origin = first + (index << 3)
173
+ to = (origin + 8).clamp(..stop)
174
+ character_colors(index, mode)
175
+ paint_pixels(base, x, to, origin)
176
+ x = to
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Chips
5
+ class TED
6
+ class Sound
7
+ # The sound's level averaged over each sample of the audio rate,
8
+ # which integrates the SND pin as the board's RC network does, then
9
+ # through the network's coupling capacitor, a one-pole high-pass at
10
+ # 2.7 Hz run at the audio rate.
11
+ #
12
+ # The corner comes from the Plus/4 schematic (310164, sheet 2): SND
13
+ # reaches the video connector through R10 (1K) and C18 (10 uF) into
14
+ # R11 (12K) and R12 (10K) beside Q3's emitter follower. The
15
+ # network's low-pass, C19 (2,700 pF) at about 70 kHz, is far above
16
+ # any audio rate.
17
+ class Output
18
+ HIGH_PASS_HZ = 2.7
19
+
20
+ # Scales the loudest level, both voices high at volume 8, to the
21
+ # top of a signed 16-bit sample.
22
+ SCALE = 32_767.0 / 19_976
23
+
24
+ attr_reader :samples
25
+
26
+ # `level` is the sound's level as the recording starts, which the
27
+ # stage takes as settled.
28
+ def initialize(clock_hz:, rate:, level:)
29
+ @clock_hz = clock_hz
30
+ @rate = rate
31
+ @remainder = 0
32
+ @window = next_window
33
+ @left = @window
34
+ @sum = 0
35
+ @drift = level.to_f
36
+ @high_pass = 1.0 - Math.exp(-2.0 * Math::PI * HIGH_PASS_HZ / rate)
37
+ @samples = []
38
+ end
39
+
40
+ # Adds `cycles` cycles at `level`, closing every sample they finish.
41
+ def hold_level(level, cycles)
42
+ left = @left
43
+ while cycles >= left
44
+ @sum += level * left
45
+ cycles -= left
46
+ close
47
+ left = @left
48
+ end
49
+ @sum += level * cycles
50
+ @left = left - cycles
51
+ end
52
+
53
+ # The samples closed since the last drain.
54
+ def drain
55
+ samples = @samples
56
+ @samples = []
57
+ samples
58
+ end
59
+
60
+ private
61
+
62
+ def close
63
+ average = @sum.fdiv(@window)
64
+ @drift += @high_pass * (average - @drift)
65
+ sample = ((average - @drift) * SCALE).round
66
+ sample = 32_767 if sample > 32_767
67
+ sample = -32_768 if sample < -32_768
68
+ @samples << sample
69
+ @sum = 0
70
+ @window = next_window
71
+ @left = @window
72
+ end
73
+
74
+ # Each window is a whole number of cycles, and they add up to the
75
+ # clock's cycles over a second.
76
+ def next_window
77
+ @remainder += @clock_hz
78
+ cycles = @remainder / @rate
79
+ @remainder -= cycles * @rate
80
+ cycles
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end