badline 0.16.1 → 0.18.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 (256) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +213 -0
  3. data/CLAUDE.md +11 -7
  4. data/CONTRIBUTING.md +7 -1
  5. data/README.md +265 -26
  6. data/doc/media.md +5 -2
  7. data/doc/pinned-behaviour.md +189 -0
  8. data/doc/snapshot-format.md +616 -74
  9. data/doc/snapshots.md +192 -24
  10. data/exe/badline-ruby +5 -0
  11. data/lib/badline/audio/terminal.rb +34 -2
  12. data/lib/badline/c128/keyed_state.rb +2 -0
  13. data/lib/badline/c128.rb +1 -0
  14. data/lib/badline/c64/keyed_state.rb +2 -0
  15. data/lib/badline/c64.rb +1 -0
  16. data/lib/badline/cbm2/acia.rb +10 -0
  17. data/lib/badline/cbm2/bus.rb +263 -0
  18. data/lib/badline/cbm2/cpu.rb +58 -0
  19. data/lib/badline/cbm2/crtc_video.rb +133 -0
  20. data/lib/badline/cbm2/ieee_drive.rb +18 -0
  21. data/lib/badline/cbm2/ieee_layout.rb +69 -0
  22. data/lib/badline/cbm2/ieee_ports.rb +104 -0
  23. data/lib/badline/cbm2/ieee_traps.rb +81 -0
  24. data/lib/badline/cbm2/keyboard_ports.rb +60 -0
  25. data/lib/badline/cbm2/keyed_modules.rb +67 -0
  26. data/lib/badline/cbm2/keyed_state.rb +95 -0
  27. data/lib/badline/cbm2/model.rb +44 -0
  28. data/lib/badline/cbm2/p500/control_ports.rb +80 -0
  29. data/lib/badline/cbm2/p500/ieee_layout.rb +41 -0
  30. data/lib/badline/cbm2/p500/keyed_state.rb +85 -0
  31. data/lib/badline/cbm2/p500/model.rb +39 -0
  32. data/lib/badline/cbm2/p500/pages.rb +57 -0
  33. data/lib/badline/cbm2/p500.rb +264 -0
  34. data/lib/badline/cbm2/sid_page.rb +19 -0
  35. data/lib/badline/cbm2.rb +270 -0
  36. data/lib/badline/{plus4 → chips}/acia/frames.rb +1 -1
  37. data/lib/badline/chips/acia/keyed_state.rb +47 -0
  38. data/lib/badline/chips/acia.rb +307 -0
  39. data/lib/badline/chips/cia/timer.rb +2 -2
  40. data/lib/badline/chips/cpu/instruction_set.rb +1 -1
  41. data/lib/badline/chips/crtc.rb +3 -2
  42. data/lib/badline/chips/m6809/arithmetic.rb +140 -0
  43. data/lib/badline/chips/m6809/bus_steps.rb +223 -0
  44. data/lib/badline/chips/m6809/core.rb +149 -0
  45. data/lib/badline/chips/m6809/indexed.rb +82 -0
  46. data/lib/badline/chips/m6809/interrupts.rb +159 -0
  47. data/lib/badline/chips/m6809/keyed_state.rb +147 -0
  48. data/lib/badline/chips/m6809/opcodes.rb +159 -0
  49. data/lib/badline/chips/m6809/operations.rb +258 -0
  50. data/lib/badline/chips/m6809/plans.rb +143 -0
  51. data/lib/badline/chips/m6809/registers.rb +82 -0
  52. data/lib/badline/chips/m6809/stack.rb +140 -0
  53. data/lib/badline/chips/m6809.rb +13 -0
  54. data/lib/badline/chips/riot/fast_forward.rb +72 -0
  55. data/lib/badline/chips/riot/keyed_parts.rb +69 -0
  56. data/lib/badline/chips/riot/timer.rb +91 -0
  57. data/lib/badline/chips/riot.rb +162 -0
  58. data/lib/badline/chips/rriot/keyed_state.rb +30 -0
  59. data/lib/badline/chips/rriot.rb +134 -0
  60. data/lib/badline/chips/sid/filter.rb +4 -4
  61. data/lib/badline/chips/sid/waveform.rb +6 -6
  62. data/lib/badline/chips/tpi/keyed_state.rb +38 -0
  63. data/lib/badline/chips/tpi.rb +233 -0
  64. data/lib/badline/chips/vic/bank.rb +60 -5
  65. data/lib/badline/chips/vic/sprite.rb +4 -3
  66. data/lib/badline/chips/vic_i/keyed_parts.rb +7 -2
  67. data/lib/badline/chips/vic_i.rb +84 -21
  68. data/lib/badline/data_path.rb +3 -3
  69. data/lib/badline/drives/datasette/keyed_state.rb +9 -1
  70. data/lib/badline/drives/datasette.rb +27 -3
  71. data/lib/badline/drives/ieee_models.rb +54 -0
  72. data/lib/badline/drives/model1001.rb +37 -0
  73. data/lib/badline/drives/model2031.rb +6 -0
  74. data/lib/badline/drives/model4040/disk.rb +62 -0
  75. data/lib/badline/drives/model4040/ip_bus.rb +46 -0
  76. data/lib/badline/drives/model4040/keyed_state.rb +57 -0
  77. data/lib/badline/drives/model4040/mechanism.rb +192 -0
  78. data/lib/badline/drives/model4040/parts.rb +20 -0
  79. data/lib/badline/drives/model4040.rb +179 -0
  80. data/lib/badline/drives/model8050/board.rb +77 -0
  81. data/lib/badline/drives/model8050/disk.rb +163 -0
  82. data/lib/badline/drives/model8050/dos_board.rb +91 -0
  83. data/lib/badline/drives/model8050/fdc_bus.rb +102 -0
  84. data/lib/badline/drives/model8050/idle.rb +181 -0
  85. data/lib/badline/drives/model8050/ieee_port.rb +147 -0
  86. data/lib/badline/drives/model8050/ip_bus.rb +93 -0
  87. data/lib/badline/drives/model8050/keyed_state.rb +163 -0
  88. data/lib/badline/drives/model8050/mechanism.rb +230 -0
  89. data/lib/badline/drives/model8050/parts.rb +37 -0
  90. data/lib/badline/drives/model8050.rb +131 -0
  91. data/lib/badline/drives/model9060/idle.rb +149 -0
  92. data/lib/badline/drives/model9060/keyed_state.rb +123 -0
  93. data/lib/badline/drives/model9060/parts.rb +98 -0
  94. data/lib/badline/drives/model9060/sasi_bus.rb +88 -0
  95. data/lib/badline/drives/model9060/sasi_commands.rb +168 -0
  96. data/lib/badline/drives/model9060/sasi_target.rb +161 -0
  97. data/lib/badline/drives/model9060.rb +195 -0
  98. data/lib/badline/frontend/app.rb +18 -3
  99. data/lib/badline/frontend/boot.rb +4 -3
  100. data/lib/badline/frontend/controls.rb +107 -15
  101. data/lib/badline/frontend/gamepads.rb +3 -3
  102. data/lib/badline/frontend/keys.rb +97 -2
  103. data/lib/badline/frontend/kim1_panel.rb +208 -0
  104. data/lib/badline/frontend/media_slots.rb +8 -1
  105. data/lib/badline/frontend/menu/drive_types.rb +56 -0
  106. data/lib/badline/frontend/menu/menu_dialogs.rb +61 -14
  107. data/lib/badline/frontend/menu/menu_media.rb +47 -14
  108. data/lib/badline/frontend/menu/menu_pages.rb +113 -4
  109. data/lib/badline/frontend/menu/menu_ports.rb +77 -4
  110. data/lib/badline/frontend/menu/pause_menu.rb +6 -2
  111. data/lib/badline/frontend/pet_keyboard.rb +45 -7
  112. data/lib/badline/frontend/screen.rb +1 -1
  113. data/lib/badline/frontend/screens.rb +40 -2
  114. data/lib/badline/frontend/teletype_pane.rb +152 -0
  115. data/lib/badline/frontend/timeline.rb +49 -11
  116. data/lib/badline/frontend.rb +3 -0
  117. data/lib/badline/kernal_trap/dos/disk_commands.rb +80 -0
  118. data/lib/badline/kernal_trap/dos/status.rb +3 -0
  119. data/lib/badline/kernal_trap/dos.rb +18 -5
  120. data/lib/badline/kernal_trap/ieee.rb +4 -2
  121. data/lib/badline/kernal_trap/ieee_layout.rb +95 -43
  122. data/lib/badline/kim1/bus.rb +117 -0
  123. data/lib/badline/kim1/cpu.rb +25 -0
  124. data/lib/badline/kim1/host_terminal.rb +74 -0
  125. data/lib/badline/kim1/key_entry.rb +74 -0
  126. data/lib/badline/kim1/keyed_modules.rb +53 -0
  127. data/lib/badline/kim1/keyed_state.rb +133 -0
  128. data/lib/badline/kim1/panel.rb +176 -0
  129. data/lib/badline/kim1/rriots.rb +28 -0
  130. data/lib/badline/kim1/sound.rb +29 -0
  131. data/lib/badline/kim1/tape.rb +116 -0
  132. data/lib/badline/kim1/teletype.rb +193 -0
  133. data/lib/badline/kim1.rb +190 -0
  134. data/lib/badline/machine/ieee_drives.rb +171 -0
  135. data/lib/badline/machine/keyboard_buffer.rb +20 -3
  136. data/lib/badline/machine/line_sound.rb +122 -0
  137. data/lib/badline/machine/settling.rb +4 -0
  138. data/lib/badline/machine.rb +62 -14
  139. data/lib/badline/media/cbm2_media.rb +29 -0
  140. data/lib/badline/media/extensions.rb +8 -4
  141. data/lib/badline/media/kim1_media.rb +150 -0
  142. data/lib/badline/media/pet_media.rb +3 -1
  143. data/lib/badline/media/program.rb +6 -3
  144. data/lib/badline/media/true_drive.rb +51 -25
  145. data/lib/badline/media.rb +23 -8
  146. data/lib/badline/options/family.rb +108 -6
  147. data/lib/badline/options/help.rb +58 -12
  148. data/lib/badline/options/table.rb +60 -14
  149. data/lib/badline/options/validation.rb +71 -11
  150. data/lib/badline/options.rb +46 -13
  151. data/lib/badline/pet/bus.rb +16 -2
  152. data/lib/badline/pet/cassettes.rb +22 -1
  153. data/lib/badline/pet/ieee_drive.rb +5 -64
  154. data/lib/badline/pet/ieee_ports.rb +7 -0
  155. data/lib/badline/pet/keyboard_ports.rb +9 -2
  156. data/lib/badline/pet/keyed_state.rb +4 -0
  157. data/lib/badline/pet/model.rb +11 -2
  158. data/lib/badline/pet/rom_sets.rb +1 -0
  159. data/lib/badline/pet/sound.rb +5 -105
  160. data/lib/badline/pet/superpet/acia.rb +12 -0
  161. data/lib/badline/pet/superpet/board.rb +221 -0
  162. data/lib/badline/pet/superpet/dongle.rb +81 -0
  163. data/lib/badline/pet/superpet/keyed_modules.rb +63 -0
  164. data/lib/badline/pet/superpet/keyed_parts.rb +76 -0
  165. data/lib/badline/pet/superpet/m6809.rb +32 -0
  166. data/lib/badline/pet/superpet/waterloo_traps.rb +199 -0
  167. data/lib/badline/pet/superpet.rb +216 -0
  168. data/lib/badline/pet.rb +33 -6
  169. data/lib/badline/plus4/acia.rb +4 -298
  170. data/lib/badline/plus4/keyed_parts.rb +0 -44
  171. data/lib/badline/plus4/keyed_state.rb +2 -0
  172. data/lib/badline/plus4.rb +2 -0
  173. data/lib/badline/roms/1001/fdc-251257-02a.rom +0 -0
  174. data/lib/badline/roms/4040/dos-1.2.rom +0 -0
  175. data/lib/badline/roms/4040/dos-2.1-rev.rom +0 -0
  176. data/lib/badline/roms/4040/dos-2.1.rom +0 -0
  177. data/lib/badline/roms/4040/fdc-901466-02.rom +0 -0
  178. data/lib/badline/roms/4040/fdc-901466-04.rom +0 -0
  179. data/lib/badline/roms/8050/dos-2.7.rom +0 -0
  180. data/lib/badline/roms/8050/fdc-901869-01.rom +0 -0
  181. data/lib/badline/roms/8050/fdc-901885-04.rom +0 -0
  182. data/lib/badline/roms/9060/dos-3.0.rom +0 -0
  183. data/lib/badline/roms/9060/sasi-300515-revb.rom +0 -0
  184. data/lib/badline/roms/README +277 -0
  185. data/lib/badline/roms/cbm2/basic128-02b.rom +0 -0
  186. data/lib/badline/roms/cbm2/basic128.rom +0 -0
  187. data/lib/badline/roms/cbm2/basic256.rom +0 -0
  188. data/lib/badline/roms/cbm2/characters-600.rom +0 -0
  189. data/lib/badline/roms/cbm2/characters-700.rom +0 -0
  190. data/lib/badline/roms/cbm2/kernal-03b.rom +0 -0
  191. data/lib/badline/roms/cbm2/kernal-04a.rom +0 -0
  192. data/lib/badline/roms/cbm2/p500-basic.rom +0 -0
  193. data/lib/badline/roms/cbm2/p500-kernal.rom +0 -0
  194. data/lib/badline/roms/kim1/6530-002.rom +0 -0
  195. data/lib/badline/roms/kim1/6530-003.rom +0 -0
  196. data/lib/badline/roms/pet/characters-superpet.rom +0 -0
  197. data/lib/badline/roms/pet/superpet-a000.rom +0 -0
  198. data/lib/badline/roms/pet/superpet-c000.rom +0 -0
  199. data/lib/badline/roms/pet/superpet-e000-50hz.rom +0 -0
  200. data/lib/badline/roms/pet/superpet-e000.rom +0 -0
  201. data/lib/badline/snapshot/cbm2_setup.rb +53 -0
  202. data/lib/badline/snapshot/container.rb +9 -2
  203. data/lib/badline/snapshot/field_doc/markdown.rb +4 -2
  204. data/lib/badline/snapshot/field_doc.rb +59 -3
  205. data/lib/badline/snapshot/image.rb +65 -9
  206. data/lib/badline/snapshot/kim1_setup.rb +28 -0
  207. data/lib/badline/snapshot/pet_setup.rb +27 -11
  208. data/lib/badline/snapshot/storage_setup.rb +4 -1
  209. data/lib/badline/snapshot/vic20_setup.rb +11 -7
  210. data/lib/badline/snapshot/vice/cbm2.rb +93 -0
  211. data/lib/badline/snapshot/vice/cbm2_acia.rb +75 -0
  212. data/lib/badline/snapshot/vice/cbm2_crtc.rb +127 -0
  213. data/lib/badline/snapshot/vice/cbm2_mains.rb +49 -0
  214. data/lib/badline/snapshot/vice/cbm2_mem.rb +75 -0
  215. data/lib/badline/snapshot/vice/cbm2_restore.rb +204 -0
  216. data/lib/badline/snapshot/vice/cbm2_tpi.rb +58 -0
  217. data/lib/badline/snapshot/vice/cias.rb +13 -9
  218. data/lib/badline/snapshot/vice/p500.rb +96 -0
  219. data/lib/badline/snapshot/vice/p500_restore.rb +130 -0
  220. data/lib/badline/snapshot/vice/p500_vicii.rb +166 -0
  221. data/lib/badline/snapshot/vice/pet.rb +20 -10
  222. data/lib/badline/snapshot/vice/pet_cpu.rb +42 -5
  223. data/lib/badline/snapshot/vice/pet_crtc.rb +25 -17
  224. data/lib/badline/snapshot/vice/pet_drive.rb +180 -0
  225. data/lib/badline/snapshot/vice/pet_drive8050.rb +336 -0
  226. data/lib/badline/snapshot/vice/pet_mem.rb +49 -5
  227. data/lib/badline/snapshot/vice/pet_restore.rb +100 -7
  228. data/lib/badline/snapshot/vice/pet_via.rb +6 -5
  229. data/lib/badline/snapshot/vice/sid_registers.rb +2 -1
  230. data/lib/badline/snapshot/vice/vicii_import.rb +10 -6
  231. data/lib/badline/snapshot/vice.rb +3 -0
  232. data/lib/badline/snapshot.rb +15 -3
  233. data/lib/badline/status.rb +30 -30
  234. data/lib/badline/storage/crt_file.rb +7 -1
  235. data/lib/badline/storage/d64_image.rb +7 -0
  236. data/lib/badline/storage/d67_image.rb +55 -0
  237. data/lib/badline/storage/d80_image.rb +17 -1
  238. data/lib/badline/storage/d81_image.rb +14 -0
  239. data/lib/badline/storage/d90_image.rb +180 -0
  240. data/lib/badline/storage/disk_image/bam.rb +18 -3
  241. data/lib/badline/storage/disk_image/formatting.rb +69 -0
  242. data/lib/badline/storage/disk_image/writing.rb +37 -2
  243. data/lib/badline/storage/disk_image.rb +2 -0
  244. data/lib/badline/storage/kim_tape.rb +254 -0
  245. data/lib/badline/storage/papertape.rb +90 -0
  246. data/lib/badline/storage.rb +5 -0
  247. data/lib/badline/support/cbm2_checkpoint.rb +48 -0
  248. data/lib/badline/support/kim1_checkpoint.rb +41 -0
  249. data/lib/badline/support/p500_checkpoint.rb +35 -0
  250. data/lib/badline/support/superpet_checkpoint.rb +35 -0
  251. data/lib/badline/version.rb +1 -1
  252. data/lib/badline/vic20/keyed_modules.rb +6 -3
  253. data/lib/badline/vic20/keyed_state.rb +6 -4
  254. data/lib/badline/vic20.rb +26 -12
  255. data/lib/badline.rb +2 -0
  256. metadata +148 -2
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "badline/kim1/cpu"
4
+ require "badline/kim1/rriots"
5
+ require "badline/kim1/tape"
6
+ require "badline/kim1/panel"
7
+ require "badline/kim1/key_entry"
8
+ require "badline/kim1/bus"
9
+ require "badline/kim1/sound"
10
+ require "badline/kim1/teletype"
11
+ require "badline/kim1/host_terminal"
12
+
13
+ module Badline
14
+ # The MOS KIM-1: a 6502 at 1 MHz, 1K of RAM and two 6530 RRIOTs, U2
15
+ # with the monitor in its ROM and U3 with the tape routines (KIM1::Bus).
16
+ # U2's ports drive the keypad and the six LED digits (Panel), and its PB7
17
+ # the audio output (Sound) and the cassette (Tape). Nothing pulls IRQ
18
+ # unless U3's PB7 is wired to it, as the User Manual's appendix H has the
19
+ # user do for U3's timer.
20
+ #
21
+ # RS resets the CPU and both 6530s, and ST pulls NMI, which the monitor
22
+ # leaves to the vector at $17FA: $1C00 there saves the registers and
23
+ # shows the PC. With the SST switch on, every instruction outside the
24
+ # monitor ends in an NMI (CPU#single_step). With the TTY/KB jumper
25
+ # fitted, the monitor talks to a teletype (Teletype) in place of the
26
+ # keypad and display. It runs headless.
27
+ class KIM1
28
+ include Machine::Clocking
29
+ include Machine::Settling
30
+ include KeyEntry
31
+
32
+ # The cycle by which the monitor is scanning the display, with a
33
+ # margin.
34
+ BOOT_CYCLES = 10_000
35
+
36
+ attr_reader :bus, :cpu, :u2, :u3, :panel, :teletype, :cycles, :init_threshold
37
+
38
+ # Whether the SST switch is on.
39
+ attr_accessor :sst
40
+
41
+ # Whether U3's PB7, its timer's interrupt, is wired to IRQ.
42
+ attr_accessor :irq_wired
43
+
44
+ # The 1 MHz clock. With no video, a frame is nominal: 64 by 260
45
+ # cycles, some 60 a second.
46
+ TIMING = Timing.new(clock_hz: 1_000_000, cycles_per_line: 64, lines_per_frame: 260, crop: [0, 0, 0, 0],
47
+ pixel_width: 1, dots: 1)
48
+
49
+ def family = :kim1
50
+
51
+ def ram = @bus.ram
52
+
53
+ # Reads and writes memory as the CPU sees it, through the bus.
54
+ def peek(addr) = @bus.peek(addr)
55
+
56
+ def poke(addr, value) = @bus.poke(addr, value)
57
+
58
+ # With +tty+ the TTY/KB jumper is fitted, and the monitor talks to a
59
+ # teletype at +baud+ (Teletype::BAUDS) in place of the keypad and
60
+ # display. +ram+ names the RAM, one of Bus::RAM_CONFIGURATIONS, or
61
+ # nil for the stock 1K.
62
+ def initialize(tty: false, baud: 9600, ram: nil)
63
+ @cycles = 0
64
+ @panel = Panel.new(self)
65
+ @panel.tty = tty
66
+ @u2 = MonitorRRIOT.new(rom: ROM.read("kim1/6530-002.rom"), peripheral: @panel)
67
+ @u3 = UserRRIOT.new(rom: ROM.read("kim1/6530-003.rom"))
68
+ @panel.connect(@u2)
69
+ @bus = Bus.new(@u2, @u3, ram: ram || :"1k")
70
+ @cpu = CPU.new(@bus)
71
+ @sst = false
72
+ @irq_wired = false
73
+ @sound = Sound.new(@u2, @u3)
74
+ @teletype = Teletype.new(@panel, baud:)
75
+ @teletype.reset!(0) if tty
76
+ @init_handlers = []
77
+ @init_threshold = BOOT_CYCLES
78
+ @pending_keys = nil
79
+ @key_down = false
80
+ @next_key_at = 0
81
+ end
82
+
83
+ # Whether the TTY/KB jumper is fitted.
84
+ def tty? = @panel.tty
85
+
86
+ # Fits or takes out the TTY/KB jumper.
87
+ def tty=(fitted)
88
+ @panel.tty = fitted
89
+ end
90
+
91
+ def timing = TIMING
92
+
93
+ # Types `text` on the teletype with the jumper fitted, and otherwise
94
+ # presses the keys it names (KeyEntry#type_keys).
95
+ def type_text(text)
96
+ return type_keys(text) unless @panel.tty
97
+
98
+ @teletype.type(text)
99
+ end
100
+
101
+ # What the teletype has printed: #output holds it all.
102
+ def capture_output = @teletype
103
+
104
+ # What a front end plays: U2's PB7 or the speaker (Sound), which
105
+ # answers #record and #drain_samples.
106
+ def sound_source = @sound
107
+
108
+ # Whether the speaker on U3's PA0 is wired, and plays in place of PB7.
109
+ def speaker? = @sound.speaker
110
+
111
+ def speaker=(wired)
112
+ @sound.speaker = wired
113
+ end
114
+
115
+ # The machine as the pause menu names it.
116
+ def model_label = "KIM-1, #{ram_kb}K RAM"
117
+
118
+ # The RAM in K (Bus#ram_kb).
119
+ def ram_kb = @bus.ram_kb
120
+
121
+ # No drive plugs into the KIM-1, and nothing is mounted.
122
+ def true_drive = nil
123
+
124
+ def mounted? = false
125
+
126
+ def mounted_path = ""
127
+
128
+ # The 6530s count ahead of the CPU. With SST on, the board pulls NMI
129
+ # ahead of an opcode fetch outside the monitor.
130
+ def cycle!
131
+ handle_init if @cycles == @init_threshold
132
+ feed_keys unless @pending_keys.nil?
133
+
134
+ @u2.cycle!
135
+ @u3.cycle!
136
+ @sound.cycle! if @sound.recording?
137
+ @teletype.cycle!(@cycles, @u2.port_b_output.anybits?(0x01) && @teletype.line_high?) if @panel.tty
138
+ @cpu.irq = @irq_wired && @u3.irq?
139
+ @cpu.single_step if @sst
140
+ @cpu.cycle!
141
+
142
+ @cycles += 1
143
+ end
144
+
145
+ # RS: the RST line resets the CPU and both 6530s. RAM keeps what it
146
+ # holds. A teletype then sends RUBOUT for the monitor to time.
147
+ def reset!
148
+ @u2.reset!
149
+ @u3.reset!
150
+ @cpu.reset!
151
+ @teletype.reset!(@cycles) if @panel.tty
152
+ end
153
+
154
+ alias press_reset reset!
155
+
156
+ # Switches the machine off and on: RAM starts cleared, and RS resets
157
+ # everything else.
158
+ def power_cycle!
159
+ @bus.power_on!
160
+ reset!
161
+ end
162
+
163
+ # ST: an edge on NMI.
164
+ def press_stop
165
+ @cpu.nmi = true
166
+ end
167
+
168
+ # Holds down the key Panel::KEYS names `name`.
169
+ def press_key(name)
170
+ @panel.press(name)
171
+ end
172
+
173
+ def release_key
174
+ @panel.release
175
+ end
176
+
177
+ # The segments each LED digit shows (Panel#display).
178
+ def display = @panel.display
179
+
180
+ # The display as text, such as "0200 A9" (Panel#display_text).
181
+ def display_text = @panel.display_text
182
+
183
+ # The cassette deck on U2's PB7.
184
+ def tape = @panel.tape
185
+ end
186
+ end
187
+
188
+ require "badline/snapshot/kim1_setup"
189
+ require "badline/kim1/keyed_state"
190
+ require "badline/kim1/keyed_modules"
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "badline/drives/ieee_models"
4
+ require "badline/drives/model2031"
5
+ require "badline/drives/model8050"
6
+ require "badline/drives/model9060"
7
+ require "badline/drives/model1001"
8
+ require "badline/drives/model4040"
9
+
10
+ module Badline
11
+ module Machine
12
+ # A machine's true drive on its IEEE-488 bus, one at a time, of a model
13
+ # Drives::IEEEModels names: a Drives::Model2031, Model8050, Model8250,
14
+ # Model1001, Model4040, Model3040, Model2040, Model9060 or Model9090. It
15
+ # is clocked after the CPU each cycle at the machine's clock
16
+ # (ieee_host_clock_hz) and reset with it, as the machine's RES line
17
+ # reaches IFC. The traps on the KERNAL's primitives leave device 8 to
18
+ # the drive while it is device 8 (trap_device). The PET (PET::IEEEDrive) and the CBM-II
19
+ # (CBM2::IEEEDrive) include it.
20
+ #
21
+ # The machine's @ieee is its side of the bus: #bus, the
22
+ # Drives::IEEEBus, #push_lines, which shows the drives the lines the
23
+ # machine pulls now, and #cycle_lines, which does so once a cycle if
24
+ # the machine doesn't push them as its chips are written.
25
+ module IEEEDrives
26
+ # The true drive on the bus, or nil.
27
+ attr_reader :true_drive
28
+
29
+ # The model plug_true_drive plugs in, one Drives::IEEEModels names.
30
+ attr_reader :true_drive_model
31
+
32
+ # Plugs in the drive, in place of the drive there.
33
+ def attach_ieee_drive(drive)
34
+ detach_ieee_drive
35
+ drive.host_clock_hz = ieee_host_clock_hz
36
+ drive.connect(@ieee.bus)
37
+ @ieee.push_lines
38
+ @true_drive = drive
39
+ @ieee_trap&.device = trap_device
40
+ @init_threshold = [@init_threshold, drive.boot_cycles].max if @cycles < @init_threshold
41
+ end
42
+
43
+ # Unplugs the true drive, leaving it on a bus of its own.
44
+ def detach_ieee_drive
45
+ drive = @true_drive
46
+ return unless drive
47
+
48
+ drive.connect(Badline::Drives::IEEEBus.new)
49
+ @true_drive = nil
50
+ @ieee_trap&.device = trap_device
51
+ end
52
+
53
+ # Plugs in the model as +device+, in place of the drive there, and
54
+ # returns it.
55
+ def plug_ieee_drive(model, device: 8)
56
+ build_ieee_drive(model, device).tap { |drive| attach_ieee_drive(drive) }
57
+ end
58
+
59
+ # The drive of a dual unit a disk image given on its own goes in
60
+ # (Media::TrueDrive): drive 0.
61
+ def disk_drive_number = 0
62
+
63
+ # Plugs in the model true_drive_model names as device 8, in place of
64
+ # the drive there, and returns it.
65
+ def plug_true_drive = plug_ieee_drive(@true_drive_model)
66
+
67
+ # Settles for a snapshot, then runs on until the true drive is where
68
+ # VICE saves it (the drive's settled_for_vice?) between instructions
69
+ # with the machine's, or for so many cycles, then on to the machine's
70
+ # next instruction. It stands in for Machine::Settling's, which the
71
+ # machines include first.
72
+ def settle_for_vice!
73
+ ran = settle_for_snapshot!
74
+ drive = @true_drive
75
+ return ran if drive.nil?
76
+
77
+ until ran >= Machine::Settling::LIMIT || (drive.settled_for_vice? && settled_for_snapshot?)
78
+ cycle!
79
+ ran += 1
80
+ end
81
+ ran + settle_for_snapshot!
82
+ end
83
+
84
+ private
85
+
86
+ def init_ieee_drives(true_drive_model)
87
+ unless Badline::Drives::IEEEModels::NAMES.include?(true_drive_model)
88
+ raise ArgumentError, "no true drive model #{true_drive_model} for the IEEE-488 bus"
89
+ end
90
+
91
+ @true_drive_model = true_drive_model
92
+ @true_drive = nil
93
+ end
94
+
95
+ # A new drive of the model as +device+.
96
+ def build_ieee_drive(model, device)
97
+ case model
98
+ when "8050" then Badline::Drives::Model8050.new(device:)
99
+ when "8250" then Badline::Drives::Model8250.new(device:)
100
+ when "1001" then Badline::Drives::Model1001.new(device:)
101
+ when "4040" then Badline::Drives::Model4040.new(device:)
102
+ when "3040" then Badline::Drives::Model3040.new(device:)
103
+ when "2040" then Badline::Drives::Model2040.new(device:)
104
+ when "9060" then Badline::Drives::Model9060.new(device:)
105
+ when "9090" then Badline::Drives::Model9090.new(device:)
106
+ else Badline::Drives::Model2031.new(device:)
107
+ end
108
+ end
109
+
110
+ # The true drive's cycles for one of the machine's, after the lines
111
+ # it pulls.
112
+ def clock_ieee_drives
113
+ drive = @true_drive
114
+ return unless drive
115
+
116
+ @ieee.cycle_lines
117
+ drive.host_cycle!
118
+ end
119
+
120
+ # The RES line reaches the true drive as IFC.
121
+ def reset_ieee_drives
122
+ @true_drive&.reset!
123
+ end
124
+
125
+ def save_ieee_drive_fields(out)
126
+ out.text("host.ieee_drive_model", @true_drive_model)
127
+ end
128
+
129
+ def load_ieee_drive_fields(input)
130
+ @true_drive_model = input.choice("host.ieee_drive_model", Badline::Drives::IEEEModels::NAMES,
131
+ @true_drive_model)
132
+ end
133
+
134
+ # The true drive as keyed snapshot modules named from its device
135
+ # number: BL-D8, BL-D8-CPU and so on for device 8.
136
+ def save_drive_modules(sections)
137
+ drive = @true_drive
138
+ drive&.save_modules(sections, "BL-D#{drive.device}")
139
+ sections
140
+ end
141
+
142
+ # The true drive a snapshot's modules hold, of the model they name,
143
+ # built afresh unless the same model is on the bus as the same
144
+ # device, or none when they hold no drive. The bus then carries the
145
+ # machine's lines as its chips left them.
146
+ def load_drive_modules(modules)
147
+ device = 8
148
+ input = modules.optional("BL-D8")
149
+ while input.nil? && device < 15
150
+ device += 1
151
+ input = modules.optional("BL-D#{device}")
152
+ end
153
+ if input
154
+ load_ieee_drive_modules(modules, input, device)
155
+ else
156
+ detach_ieee_drive
157
+ end
158
+ @ieee.push_lines
159
+ end
160
+
161
+ def load_ieee_drive_modules(modules, input, device)
162
+ model = input.text("model", Badline::Drives::IEEEModels::DEFAULT)
163
+ model = Badline::Drives::IEEEModels::DEFAULT unless Badline::Drives::IEEEModels::NAMES.include?(model)
164
+ drive = @true_drive
165
+ detach_ieee_drive if drive && (drive.device != device || drive.model_name != model)
166
+ drive = @true_drive || plug_ieee_drive(model, device:)
167
+ drive.load_modules(modules, input, "BL-D#{device}")
168
+ end
169
+ end
170
+ end
171
+ end
@@ -28,13 +28,30 @@ module Badline
28
28
  # The PET's under BASIC 1, outside the zero page.
29
29
  PET_BASIC1_LAYOUT = Layout.new(address: 0x020f, count: 0x020d, capacity: 10)
30
30
 
31
- def type_text(text)
32
- codes = text.bytes.map { |b| ascii_to_petscii(b) }
33
- on_init { @pending_keys = codes }
31
+ def type_text(text) = type_on_init(text.bytes.map { |b| ascii_to_petscii(b) })
32
+
33
+ # The keys type_text left for the KERNAL to boot first, under
34
+ # host.init_keys, so that a machine restored before then types them.
35
+ def save_init_keys(out)
36
+ keys = @init_keys
37
+ out.ints("host.init_keys", keys) if keys
38
+ end
39
+
40
+ def load_init_keys(input)
41
+ keys = input.ints("host.init_keys", [])
42
+ type_on_init(keys.dup) unless keys.empty?
34
43
  end
35
44
 
36
45
  private
37
46
 
47
+ def type_on_init(codes)
48
+ @init_keys = codes if booting?
49
+ on_init do
50
+ @init_keys = nil
51
+ @pending_keys = codes
52
+ end
53
+ end
54
+
38
55
  def feed_keyboard
39
56
  buffer = keyboard_buffer
40
57
  return unless ram.peek(buffer.count).zero?
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Machine
5
+ # The sound of one logic line a machine drives into its speaker or
6
+ # audio output, such as the PET's CB2 or the KIM-1's PB7. The class
7
+ # that includes it answers #line_high?, the line's level this cycle.
8
+ #
9
+ # The machine calls #cycle! once a cycle while #recording?, from #record
10
+ # on, so a sound nobody records costs only that check. Each sample is
11
+ # the share of its cycles the line spent high, through the output
12
+ # stage: a one-pole low-pass at 5 kHz and a one-pole high-pass at
13
+ # 100 Hz, so a line left high or low settles to silence.
14
+ module LineSound
15
+ LOW_PASS_HZ = 5_000
16
+ HIGH_PASS_HZ = 100
17
+
18
+ # A swing from the line low to the line high, as a signed 16-bit
19
+ # sample.
20
+ SCALE = 24_000.0
21
+
22
+ attr_reader :recording
23
+ alias recording? recording
24
+
25
+ # Starts collecting the output, averaged down to `rate` samples a
26
+ # second at `clock_hz` cycles a second, for #drain_samples. The
27
+ # output stage takes the line's level now as settled.
28
+ def record(rate:, clock_hz: 1_000_000)
29
+ @rate = rate
30
+ @clock_hz = clock_hz
31
+ @remainder = 0
32
+ @window = next_window
33
+ @left = @window
34
+ @sum = 0
35
+ @high = line_high?
36
+ @run = 0
37
+ @low = @drift = @high ? 1.0 : 0.0
38
+ @low_pass = 1.0 - Math.exp(-2.0 * Math::PI * LOW_PASS_HZ / rate)
39
+ @high_pass = 1.0 - Math.exp(-2.0 * Math::PI * HIGH_PASS_HZ / rate)
40
+ @samples = []
41
+ @recording = true
42
+ end
43
+
44
+ # Takes the line's level for this cycle, while recording.
45
+ def cycle!
46
+ high = line_high?
47
+ if high == @high
48
+ @run += 1
49
+ else
50
+ hold(@high, @run)
51
+ @high = high
52
+ @run = 1
53
+ end
54
+ end
55
+
56
+ # The samples closed since the last drain.
57
+ def drain_samples
58
+ return [] unless @recording
59
+
60
+ hold(@high, @run)
61
+ @run = 0
62
+ samples = @samples
63
+ @samples = []
64
+ samples
65
+ end
66
+
67
+ private
68
+
69
+ def init_line_sound
70
+ @recording = false
71
+ @high = true
72
+ @run = 0
73
+ @rate = 0
74
+ @clock_hz = 0
75
+ @remainder = 0
76
+ @window = 0
77
+ @left = 0
78
+ @sum = 0
79
+ @low = 0.0
80
+ @drift = 0.0
81
+ @low_pass = 0.0
82
+ @high_pass = 0.0
83
+ @samples = []
84
+ end
85
+
86
+ # Adds `cycles` cycles with the line high or low, closing every
87
+ # sample they finish.
88
+ def hold(high, cycles)
89
+ left = @left
90
+ while cycles >= left
91
+ @sum += left if high
92
+ cycles -= left
93
+ close
94
+ left = @left
95
+ end
96
+ @sum += cycles if high
97
+ @left = left - cycles
98
+ end
99
+
100
+ def close
101
+ @low += @low_pass * (@sum.fdiv(@window) - @low)
102
+ @drift += @high_pass * (@low - @drift)
103
+ sample = ((@low - @drift) * SCALE).round
104
+ sample = 32_767 if sample > 32_767
105
+ sample = -32_768 if sample < -32_768
106
+ @samples << sample
107
+ @sum = 0
108
+ @window = next_window
109
+ @left = @window
110
+ end
111
+
112
+ # Each window is a whole number of cycles, and they add up to the
113
+ # clock's cycles over a second.
114
+ def next_window
115
+ @remainder += @clock_hz
116
+ cycles = @remainder / @rate
117
+ @remainder -= cycles * @rate
118
+ cycles
119
+ end
120
+ end
121
+ end
122
+ end
@@ -24,6 +24,10 @@ module Badline
24
24
  settle_chips_for_snapshot
25
25
  ran
26
26
  end
27
+
28
+ # Settles as #settle_for_snapshot! does, for a snapshot VICE opens,
29
+ # which a machine with a drive VICE runs settles further.
30
+ def settle_for_vice! = settle_for_snapshot!
27
31
  end
28
32
  end
29
33
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "badline/machine/clocking"
4
4
  require "badline/machine/keyboard_buffer"
5
+ require "badline/machine/line_sound"
5
6
  require "badline/machine/settling"
6
7
 
7
8
  module Badline
@@ -58,6 +59,11 @@ module Badline
58
59
  # and +capture_output+, and besides +model_label+, +cartridge_slots+ and
59
60
  # +detach_cartridges+ for its two cartridges.
60
61
  #
62
+ # The KIM-1 answers the identity, clocking, lifecycle and sound parts,
63
+ # with no video chip: a front end draws its panel (KIM1::Panel), and
64
+ # #true_drive is nil. Its keys are the panel's, through +press_key+,
65
+ # +release_key+, +type_keys+, +press_stop+ and +press_reset+.
66
+ #
61
67
  # The PET answers the identity, clocking, lifecycle, video, sound and
62
68
  # keyboard parts, with no joysticks, RESTORE or control ports, and of
63
69
  # the rest +ram+, +load_prg+, +type_text+, +mount+, +unmount+,
@@ -69,20 +75,28 @@ module Badline
69
75
  # Media the machine named doesn't take.
70
76
  class Unsupported < ArgumentError; end
71
77
 
72
- # A new machine of `family`, :c64, :vic20, :c128, :plus4 or :pet, the
73
- # model of it `model` names. A C64's model is one of Model::ALL, with its SID
74
- # `sid_model` when not nil and an REU of `reu` K when not nil. A
75
- # VIC-20's is "pal", with the RAM expansion `ram` names
78
+ # A new machine of `family`, :c64, :vic20, :c128, :plus4, :pet, :cbm2 or :kim1, the
79
+ # model of it `model` names. A C64's model is one of Model::ALL, with
80
+ # its SID `sid_model` when not nil and an REU of `reu` K when not nil.
81
+ # A VIC-20's is "pal" or "ntsc", with the RAM expansion `ram` names
76
82
  # (Vic20::Bus::RAM_CONFIGURATIONS), unexpanded when nil. A C128's is
77
83
  # one of C128::Model::ALL, with its SID `sid_model` when not nil, built
78
84
  # for C64 mode. A Plus/4's is one of Plus4::Model::ALL or c116, with
79
85
  # the model's own RAM. A PET's is one of PET::Model::ALL, with its own
80
- # RAM and ROMs, its CRTC's editor the 50 Hz one.
86
+ # RAM and ROMs, its CRTC's editor the 50 Hz one, the superpet a
87
+ # PET::SuperPET with its CPU switch at 6502. A CBM-II's is one of
88
+ # CBM2::Model::ALL, with VICE's KERNAL, 901244-04a, at 50 Hz, or a 510,
89
+ # one of CBM2::P500::Model::ALL, with its region's mains. A KIM-1 is
90
+ # the one model, with the RAM `ram` names
91
+ # (KIM1::Bus::RAM_CONFIGURATIONS), 1K when nil.
81
92
  def self.build(family, model:, sid_model: nil, reu: nil, ram: nil)
93
+ return KIM1.new(ram:) if family == :kim1
82
94
  return vic20(model, ram) if family == :vic20
83
95
  return c128(model, sid_model, :c64) if family == :c128
84
96
  return Plus4.new(model:) if family == :plus4
85
- return PET.new(model:) if family == :pet
97
+ return model == "superpet" ? PET::SuperPET.new : PET.new(model:) if family == :pet
98
+ return CBM2::P500.new(model:) if family == :cbm2 && model.start_with?("510")
99
+ return CBM2.new(model:) if family == :cbm2
86
100
  raise ArgumentError, "no machine family named #{family}" unless family == :c64
87
101
 
88
102
  profile = Model.named(model)
@@ -98,12 +112,15 @@ module Badline
98
112
  # only C64 mode runs: a .sid tune or a program that loads at the C64's
99
113
  # BASIC start. A VIC-20 without RAM named gets the expansion the media
100
114
  # needs (Media::Vic20Media.ram_for) and yields a line saying so. Raises
101
- # Unsupported for media a VIC-20, a Plus/4 or a PET doesn't take.
115
+ # Unsupported for media a VIC-20, a Plus/4, a PET or a CBM-II doesn't
116
+ # take.
102
117
  def self.for_options(options, media, &)
103
118
  family = options.family
104
119
  return vic20_for(media, options.model, options.ram, &) if family == :vic20
105
120
  return plus4_for(media, options) if family == :plus4
106
121
  return pet_for(media, options) if family == :pet
122
+ return cbm2_for(media, options) if family == :cbm2
123
+ return kim1_for(media, options) if family == :kim1
107
124
 
108
125
  sid_model = options.sid_model || Media.sid_model(media, otherwise: nil)
109
126
  return build(:c64, model: options.model, sid_model:, reu: options.reu) unless family == :c128
@@ -113,11 +130,7 @@ module Badline
113
130
  machine
114
131
  end
115
132
 
116
- def self.vic20(model, ram)
117
- raise ArgumentError, "no VIC-20 model named #{model}" unless model == "pal"
118
-
119
- Vic20.new(ram: ram || :unexpanded)
120
- end
133
+ def self.vic20(model, ram) = Vic20.new(ram: ram || :unexpanded, model:)
121
134
  private_class_method :vic20
122
135
 
123
136
  def self.c128(model, sid_model, mode) = C128.new(model:, sid_model:, mode:)
@@ -154,16 +167,51 @@ module Badline
154
167
  # A PET with the RAM --ram names (PET::Model::RAM_SIZES), or with the
155
168
  # model's own without it, the editor at the frame rate --60hz or
156
169
  # --editor asks for, the ROMs --basic, --editor and --characters fit it
157
- # with, and the 8296 board's jumpers --jumpers names.
170
+ # with, the 8296 board's jumpers --jumpers names, and the true drive
171
+ # --drive-type names. The SuperPET takes the frame rate, the 8032's
172
+ # BASIC and editors, the true drive, the CPU --cpu names and the bank
173
+ # RAM switch --bank-ram sets, and with the 6809 disks alone.
158
174
  def self.pet_for(media, options)
159
175
  unsupported = !media.nil? && Media::PETMedia::UNSUPPORTED.include?(File.extname(media).downcase)
160
176
  raise Unsupported, "doesn't go in a PET" if unsupported
161
177
 
178
+ if options.model == "superpet"
179
+ disk = media.nil? || File.directory?(media) || %i[disk ieee_disk].include?(Media::Extensions.kind(media))
180
+ raise Unsupported, "doesn't go in the SuperPET's 6809 side" unless disk || options.cpu == "6502"
181
+
182
+ return PET::SuperPET.new(refresh_hz: options.refresh_hz, basic: options.basic, editor: options.editor,
183
+ cpu: options.cpu, bank_ram: options.bank_ram, true_drive_model: options.drive_type)
184
+ end
185
+
162
186
  PET.new(model: options.model, ram_kb: options.ram_kb, refresh_hz: options.refresh_hz, basic: options.basic,
163
- editor: options.editor, characters: options.characters, jumpers: options.jumpers)
187
+ editor: options.editor, characters: options.characters, jumpers: options.jumpers,
188
+ true_drive_model: options.drive_type)
164
189
  end
165
190
  private_class_method :pet_for
166
191
 
192
+ # A CBM-II with the KERNAL --kernal names, on the mains --60hz asks
193
+ # for: a B series' 50 Hz without it, and a P500's its region's, and
194
+ # the true drive --drive-type names.
195
+ def self.cbm2_for(media, options)
196
+ unsupported = !media.nil? && Media::CBM2Media::UNSUPPORTED.include?(File.extname(media).downcase)
197
+ raise Unsupported, "doesn't go in a CBM-II" if unsupported
198
+
199
+ drive = options.drive_type
200
+ return CBM2::P500.new(model: options.model, mains_hz: options.mains_hz, true_drive_model: drive) if options.p500?
201
+
202
+ CBM2.new(model: options.model, kernal: options.kernal, mains_hz: options.mains_hz || 50, true_drive_model: drive)
203
+ end
204
+ private_class_method :cbm2_for
205
+
206
+ # A KIM-1 with the RAM --ram names, on a teletype at --baud with
207
+ # --tty, for media it takes (Media::KIM1Media).
208
+ def self.kim1_for(media, options)
209
+ raise Unsupported, "doesn't go in a KIM-1" unless media.nil? || Media::KIM1Media.takes?(media)
210
+
211
+ KIM1.new(tty: options.tty?, baud: options.baud, ram: options.ram)
212
+ end
213
+ private_class_method :kim1_for
214
+
167
215
  def self.c64_media?(media)
168
216
  return false if media.nil? || !File.file?(media)
169
217
 
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Media
5
+ # What a CBM-II does with media of its own (Media.attach). Disks of
6
+ # every kind, .d80 and .d82 among them, directories and .t64 archives
7
+ # mount on device 8 through the traps on the KERNAL's IEEE-488
8
+ # primitives (CBM2::IEEETraps), programs load into BASIC's text segment
9
+ # (Program), and a CBM2 .crt's ROMs go in at their load addresses in
10
+ # segment 15. There is no datasette, the SID plays no .sid tunes, and
11
+ # the true drives are serial ones.
12
+ module CBM2Media
13
+ # What a CBM-II doesn't take.
14
+ UNSUPPORTED = Extensions.of(%i[tune tape gcr])
15
+
16
+ # What loads and runs the first program on the disk in device 8.
17
+ AUTOSTART = %(load"*",8\rrun\r)
18
+
19
+ # Where BASIC's text starts in segment 1, and the pointers to its end
20
+ # and to the KERNAL's end address (EAL) in segment 15.
21
+ BASIC_START = 0x0003
22
+ TEXT_END = 0x2f
23
+ EAL = 0x96
24
+
25
+ # The ROM chips of a CBM2 .crt.
26
+ def self.cartridge_chips(path) = Storage::CRTFile.new(path, machine: :cbm2).chips
27
+ end
28
+ end
29
+ end