web-a2e 1.0.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 (295) hide show
  1. package/.clangd +5 -0
  2. package/.mcp.json +12 -0
  3. package/CLAUDE.md +362 -0
  4. package/CMakeLists.txt +774 -0
  5. package/LICENSE +21 -0
  6. package/README.md +392 -0
  7. package/build-wasm/generated/roms.cpp +2447 -0
  8. package/docker-compose.staging.yml +9 -0
  9. package/docs/basic-rom-disassembly.md +6663 -0
  10. package/docs/softswitch-comparison.md +273 -0
  11. package/docs/thunderclock-debug.md +89 -0
  12. package/examples/cube.bas +72 -0
  13. package/examples/hello.s +55 -0
  14. package/examples/scroll.s +140 -0
  15. package/package.json +18 -0
  16. package/public/assets/apple-logo-old.png +0 -0
  17. package/public/assets/apple-logo.png +0 -0
  18. package/public/assets/drive-closed-light-on.png +0 -0
  19. package/public/assets/drive-closed.png +0 -0
  20. package/public/assets/drive-open-light-on.png +0 -0
  21. package/public/assets/drive-open.png +0 -0
  22. package/public/audio-worklet.js +82 -0
  23. package/public/disks/Apple DOS 3.3 January 1983.dsk +0 -0
  24. package/public/disks/ProDOS 2.4.3.po +0 -0
  25. package/public/disks/h32mb.2mg +0 -0
  26. package/public/disks/library.json +26 -0
  27. package/public/docs/llms/llm-assembler.txt +90 -0
  28. package/public/docs/llms/llm-basic-program.txt +256 -0
  29. package/public/docs/llms/llm-disk-drives.txt +72 -0
  30. package/public/docs/llms/llm-file-explorer.txt +50 -0
  31. package/public/docs/llms/llm-hard-drives.txt +80 -0
  32. package/public/docs/llms/llm-main.txt +51 -0
  33. package/public/docs/llms/llm-slot-configuration.txt +66 -0
  34. package/public/icons/icon-192.svg +4 -0
  35. package/public/icons/icon-512.svg +4 -0
  36. package/public/index.html +661 -0
  37. package/public/llms.txt +49 -0
  38. package/public/manifest.json +29 -0
  39. package/public/shaders/burnin.glsl +22 -0
  40. package/public/shaders/crt.glsl +706 -0
  41. package/public/shaders/edge.glsl +109 -0
  42. package/public/shaders/vertex.glsl +8 -0
  43. package/public/sw.js +186 -0
  44. package/roms/341-0027.bin +0 -0
  45. package/roms/341-0160-A-US-UK.bin +0 -0
  46. package/roms/341-0160-A.bin +0 -0
  47. package/roms/342-0273-A-US-UK.bin +0 -0
  48. package/roms/342-0349-B-C0-FF.bin +0 -0
  49. package/roms/Apple Mouse Interface Card ROM - 342-0270-C.bin +0 -0
  50. package/roms/Thunderclock Plus ROM.bin +0 -0
  51. package/scripts/generate_roms.sh +69 -0
  52. package/src/bindings/wasm_interface.cpp +1940 -0
  53. package/src/core/assembler/assembler.cpp +1239 -0
  54. package/src/core/assembler/assembler.hpp +115 -0
  55. package/src/core/audio/audio.cpp +160 -0
  56. package/src/core/audio/audio.hpp +81 -0
  57. package/src/core/basic/basic_detokenizer.cpp +436 -0
  58. package/src/core/basic/basic_detokenizer.hpp +41 -0
  59. package/src/core/basic/basic_tokenizer.cpp +286 -0
  60. package/src/core/basic/basic_tokenizer.hpp +26 -0
  61. package/src/core/basic/basic_tokens.hpp +295 -0
  62. package/src/core/cards/disk2_card.cpp +568 -0
  63. package/src/core/cards/disk2_card.hpp +316 -0
  64. package/src/core/cards/expansion_card.hpp +185 -0
  65. package/src/core/cards/mockingboard/ay8910.cpp +616 -0
  66. package/src/core/cards/mockingboard/ay8910.hpp +159 -0
  67. package/src/core/cards/mockingboard/via6522.cpp +530 -0
  68. package/src/core/cards/mockingboard/via6522.hpp +163 -0
  69. package/src/core/cards/mockingboard_card.cpp +312 -0
  70. package/src/core/cards/mockingboard_card.hpp +159 -0
  71. package/src/core/cards/mouse_card.cpp +654 -0
  72. package/src/core/cards/mouse_card.hpp +190 -0
  73. package/src/core/cards/smartport/block_device.cpp +202 -0
  74. package/src/core/cards/smartport/block_device.hpp +60 -0
  75. package/src/core/cards/smartport/smartport_card.cpp +603 -0
  76. package/src/core/cards/smartport/smartport_card.hpp +120 -0
  77. package/src/core/cards/thunderclock_card.cpp +237 -0
  78. package/src/core/cards/thunderclock_card.hpp +122 -0
  79. package/src/core/cpu/cpu6502.cpp +1609 -0
  80. package/src/core/cpu/cpu6502.hpp +203 -0
  81. package/src/core/debug/condition_evaluator.cpp +470 -0
  82. package/src/core/debug/condition_evaluator.hpp +87 -0
  83. package/src/core/disassembler/disassembler.cpp +552 -0
  84. package/src/core/disassembler/disassembler.hpp +171 -0
  85. package/src/core/disk-image/disk_image.hpp +267 -0
  86. package/src/core/disk-image/dsk_disk_image.cpp +827 -0
  87. package/src/core/disk-image/dsk_disk_image.hpp +204 -0
  88. package/src/core/disk-image/gcr_encoding.cpp +147 -0
  89. package/src/core/disk-image/gcr_encoding.hpp +78 -0
  90. package/src/core/disk-image/woz_disk_image.cpp +1049 -0
  91. package/src/core/disk-image/woz_disk_image.hpp +343 -0
  92. package/src/core/emulator.cpp +2126 -0
  93. package/src/core/emulator.hpp +434 -0
  94. package/src/core/filesystem/dos33.cpp +178 -0
  95. package/src/core/filesystem/dos33.hpp +66 -0
  96. package/src/core/filesystem/pascal.cpp +262 -0
  97. package/src/core/filesystem/pascal.hpp +87 -0
  98. package/src/core/filesystem/prodos.cpp +369 -0
  99. package/src/core/filesystem/prodos.hpp +119 -0
  100. package/src/core/input/keyboard.cpp +227 -0
  101. package/src/core/input/keyboard.hpp +111 -0
  102. package/src/core/mmu/mmu.cpp +1387 -0
  103. package/src/core/mmu/mmu.hpp +236 -0
  104. package/src/core/types.hpp +196 -0
  105. package/src/core/video/video.cpp +680 -0
  106. package/src/core/video/video.hpp +156 -0
  107. package/src/css/assembler-editor.css +1617 -0
  108. package/src/css/base.css +470 -0
  109. package/src/css/basic-debugger.css +791 -0
  110. package/src/css/basic-editor.css +792 -0
  111. package/src/css/controls.css +783 -0
  112. package/src/css/cpu-debugger.css +1413 -0
  113. package/src/css/debug-base.css +160 -0
  114. package/src/css/debug-windows.css +6455 -0
  115. package/src/css/disk-drives.css +406 -0
  116. package/src/css/documentation.css +392 -0
  117. package/src/css/file-explorer.css +867 -0
  118. package/src/css/hard-drive.css +180 -0
  119. package/src/css/layout.css +217 -0
  120. package/src/css/memory-windows.css +798 -0
  121. package/src/css/modals.css +510 -0
  122. package/src/css/monitor.css +425 -0
  123. package/src/css/release-notes.css +101 -0
  124. package/src/css/responsive.css +400 -0
  125. package/src/css/rule-builder.css +340 -0
  126. package/src/css/save-states.css +201 -0
  127. package/src/css/settings-windows.css +1231 -0
  128. package/src/css/window-switcher.css +150 -0
  129. package/src/js/agent/agent-manager.js +643 -0
  130. package/src/js/agent/agent-tools.js +293 -0
  131. package/src/js/agent/agent-version-tools.js +131 -0
  132. package/src/js/agent/assembler-tools.js +357 -0
  133. package/src/js/agent/basic-program-tools.js +894 -0
  134. package/src/js/agent/disk-tools.js +417 -0
  135. package/src/js/agent/file-explorer-tools.js +269 -0
  136. package/src/js/agent/index.js +13 -0
  137. package/src/js/agent/main-tools.js +222 -0
  138. package/src/js/agent/slot-tools.js +303 -0
  139. package/src/js/agent/smartport-tools.js +257 -0
  140. package/src/js/agent/window-tools.js +80 -0
  141. package/src/js/audio/audio-driver.js +417 -0
  142. package/src/js/audio/audio-worklet.js +85 -0
  143. package/src/js/audio/index.js +8 -0
  144. package/src/js/config/default-layout.js +34 -0
  145. package/src/js/config/version.js +8 -0
  146. package/src/js/data/apple2-rom-routines.js +577 -0
  147. package/src/js/debug/assembler-editor-window.js +2993 -0
  148. package/src/js/debug/basic-breakpoint-manager.js +529 -0
  149. package/src/js/debug/basic-program-parser.js +436 -0
  150. package/src/js/debug/basic-program-window.js +2594 -0
  151. package/src/js/debug/basic-variable-inspector.js +447 -0
  152. package/src/js/debug/breakpoint-manager.js +472 -0
  153. package/src/js/debug/cpu-debugger-window.js +2396 -0
  154. package/src/js/debug/index.js +22 -0
  155. package/src/js/debug/label-manager.js +238 -0
  156. package/src/js/debug/memory-browser-window.js +416 -0
  157. package/src/js/debug/memory-heat-map-window.js +481 -0
  158. package/src/js/debug/memory-map-window.js +206 -0
  159. package/src/js/debug/mockingboard-window.js +882 -0
  160. package/src/js/debug/mouse-card-window.js +355 -0
  161. package/src/js/debug/rule-builder-window.js +648 -0
  162. package/src/js/debug/soft-switch-window.js +458 -0
  163. package/src/js/debug/stack-viewer-window.js +221 -0
  164. package/src/js/debug/symbols.js +416 -0
  165. package/src/js/debug/trace-panel.js +291 -0
  166. package/src/js/debug/zero-page-watch-window.js +297 -0
  167. package/src/js/disk-manager/disk-drives-window.js +212 -0
  168. package/src/js/disk-manager/disk-operations.js +284 -0
  169. package/src/js/disk-manager/disk-persistence.js +301 -0
  170. package/src/js/disk-manager/disk-surface-renderer.js +388 -0
  171. package/src/js/disk-manager/drive-sounds.js +139 -0
  172. package/src/js/disk-manager/hard-drive-manager.js +481 -0
  173. package/src/js/disk-manager/hard-drive-persistence.js +187 -0
  174. package/src/js/disk-manager/hard-drive-window.js +57 -0
  175. package/src/js/disk-manager/index.js +890 -0
  176. package/src/js/display/display-settings-window.js +383 -0
  177. package/src/js/display/index.js +10 -0
  178. package/src/js/display/screen-window.js +342 -0
  179. package/src/js/display/webgl-renderer.js +705 -0
  180. package/src/js/file-explorer/disassembler.js +574 -0
  181. package/src/js/file-explorer/dos33.js +266 -0
  182. package/src/js/file-explorer/file-viewer.js +359 -0
  183. package/src/js/file-explorer/index.js +1261 -0
  184. package/src/js/file-explorer/prodos.js +549 -0
  185. package/src/js/file-explorer/utils.js +67 -0
  186. package/src/js/help/documentation-window.js +1096 -0
  187. package/src/js/help/index.js +10 -0
  188. package/src/js/help/release-notes-window.js +85 -0
  189. package/src/js/help/release-notes.js +612 -0
  190. package/src/js/input/gamepad-handler.js +176 -0
  191. package/src/js/input/index.js +12 -0
  192. package/src/js/input/input-handler.js +396 -0
  193. package/src/js/input/joystick-window.js +404 -0
  194. package/src/js/input/mouse-handler.js +99 -0
  195. package/src/js/input/text-selection.js +462 -0
  196. package/src/js/main.js +653 -0
  197. package/src/js/state/index.js +15 -0
  198. package/src/js/state/save-states-window.js +393 -0
  199. package/src/js/state/state-manager.js +409 -0
  200. package/src/js/state/state-persistence.js +218 -0
  201. package/src/js/ui/confirm.js +43 -0
  202. package/src/js/ui/disk-drive-positioner.js +347 -0
  203. package/src/js/ui/reminder-controller.js +129 -0
  204. package/src/js/ui/slot-configuration-window.js +560 -0
  205. package/src/js/ui/theme-manager.js +61 -0
  206. package/src/js/ui/toast.js +44 -0
  207. package/src/js/ui/ui-controller.js +897 -0
  208. package/src/js/ui/window-switcher.js +275 -0
  209. package/src/js/utils/basic-autocomplete.js +832 -0
  210. package/src/js/utils/basic-highlighting.js +473 -0
  211. package/src/js/utils/basic-tokenizer.js +153 -0
  212. package/src/js/utils/basic-tokens.js +117 -0
  213. package/src/js/utils/constants.js +28 -0
  214. package/src/js/utils/indexeddb-helper.js +225 -0
  215. package/src/js/utils/merlin-editor-support.js +905 -0
  216. package/src/js/utils/merlin-highlighting.js +551 -0
  217. package/src/js/utils/storage.js +125 -0
  218. package/src/js/utils/string-utils.js +19 -0
  219. package/src/js/utils/wasm-memory.js +54 -0
  220. package/src/js/windows/base-window.js +690 -0
  221. package/src/js/windows/index.js +9 -0
  222. package/src/js/windows/window-manager.js +375 -0
  223. package/tests/catch2/catch.hpp +17976 -0
  224. package/tests/common/basic_program_builder.cpp +119 -0
  225. package/tests/common/basic_program_builder.hpp +209 -0
  226. package/tests/common/disk_image_builder.cpp +444 -0
  227. package/tests/common/disk_image_builder.hpp +141 -0
  228. package/tests/common/test_helpers.hpp +118 -0
  229. package/tests/gcr/gcr-test.cpp +142 -0
  230. package/tests/integration/check-rom.js +70 -0
  231. package/tests/integration/compare-boot.js +239 -0
  232. package/tests/integration/crash-trace.js +102 -0
  233. package/tests/integration/disk-boot-test.js +264 -0
  234. package/tests/integration/memory-crash.js +108 -0
  235. package/tests/integration/nibble-read-test.js +249 -0
  236. package/tests/integration/phase-test.js +159 -0
  237. package/tests/integration/test_emulator.cpp +291 -0
  238. package/tests/integration/test_emulator_basic.cpp +91 -0
  239. package/tests/integration/test_emulator_debug.cpp +344 -0
  240. package/tests/integration/test_emulator_disk.cpp +153 -0
  241. package/tests/integration/test_emulator_state.cpp +163 -0
  242. package/tests/klaus/6502_functional_test.bin +0 -0
  243. package/tests/klaus/65C02_extended_opcodes_test.bin +0 -0
  244. package/tests/klaus/klaus_6502_test.cpp +184 -0
  245. package/tests/klaus/klaus_65c02_test.cpp +197 -0
  246. package/tests/thunderclock/thunderclock_mmu_test.cpp +304 -0
  247. package/tests/thunderclock/thunderclock_test.cpp +550 -0
  248. package/tests/unit/test_assembler.cpp +521 -0
  249. package/tests/unit/test_audio.cpp +196 -0
  250. package/tests/unit/test_ay8910.cpp +311 -0
  251. package/tests/unit/test_basic_detokenizer.cpp +265 -0
  252. package/tests/unit/test_basic_tokenizer.cpp +382 -0
  253. package/tests/unit/test_block_device.cpp +259 -0
  254. package/tests/unit/test_condition_evaluator.cpp +219 -0
  255. package/tests/unit/test_cpu6502.cpp +1301 -0
  256. package/tests/unit/test_cpu_addressing.cpp +361 -0
  257. package/tests/unit/test_cpu_cycle_counts.cpp +409 -0
  258. package/tests/unit/test_cpu_decimal.cpp +166 -0
  259. package/tests/unit/test_cpu_interrupts.cpp +285 -0
  260. package/tests/unit/test_disassembler.cpp +323 -0
  261. package/tests/unit/test_disk2_card.cpp +330 -0
  262. package/tests/unit/test_dos33.cpp +273 -0
  263. package/tests/unit/test_dsk_disk_image.cpp +315 -0
  264. package/tests/unit/test_expansion_card.cpp +178 -0
  265. package/tests/unit/test_gcr_encoding.cpp +232 -0
  266. package/tests/unit/test_keyboard.cpp +262 -0
  267. package/tests/unit/test_mmu.cpp +555 -0
  268. package/tests/unit/test_mmu_slots.cpp +323 -0
  269. package/tests/unit/test_mockingboard.cpp +352 -0
  270. package/tests/unit/test_mouse_card.cpp +386 -0
  271. package/tests/unit/test_pascal.cpp +248 -0
  272. package/tests/unit/test_prodos.cpp +259 -0
  273. package/tests/unit/test_smartport_card.cpp +321 -0
  274. package/tests/unit/test_thunderclock.cpp +354 -0
  275. package/tests/unit/test_via6522.cpp +323 -0
  276. package/tests/unit/test_video.cpp +319 -0
  277. package/tests/unit/test_woz_disk_image.cpp +257 -0
  278. package/vite.config.js +96 -0
  279. package/wiki/AI-Agent.md +372 -0
  280. package/wiki/Architecture-Overview.md +303 -0
  281. package/wiki/Audio-System.md +449 -0
  282. package/wiki/CPU-Emulation.md +477 -0
  283. package/wiki/Debugger.md +516 -0
  284. package/wiki/Disk-Drives.md +161 -0
  285. package/wiki/Disk-System-Internals.md +547 -0
  286. package/wiki/Display-Settings.md +88 -0
  287. package/wiki/Expansion-Slots.md +187 -0
  288. package/wiki/File-Explorer.md +259 -0
  289. package/wiki/Getting-Started.md +156 -0
  290. package/wiki/Home.md +69 -0
  291. package/wiki/Input-Devices.md +183 -0
  292. package/wiki/Keyboard-Shortcuts.md +158 -0
  293. package/wiki/Memory-System.md +364 -0
  294. package/wiki/Save-States.md +172 -0
  295. package/wiki/Video-Rendering.md +658 -0
@@ -0,0 +1,163 @@
1
+ /*
2
+ * via6522.hpp - VIA 6522 timer chip emulation
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ #pragma once
9
+
10
+ #include <cstdint>
11
+ #include <functional>
12
+
13
+ namespace a2e {
14
+
15
+ // Forward declaration
16
+ class AY8910;
17
+
18
+ // 6522 VIA (Versatile Interface Adapter) emulation
19
+ // Two of these are used in the Mockingboard, each controlling one AY-3-8910
20
+ class VIA6522 {
21
+ public:
22
+ using IRQCallback = std::function<void()>;
23
+
24
+ VIA6522();
25
+
26
+ // Set VIA ID for debug logging (1 or 2)
27
+ void setViaId(int id);
28
+
29
+ // Enable/disable console debug logging
30
+ static void setDebugLogging(bool enabled);
31
+
32
+ // Register access
33
+ uint8_t read(uint8_t reg);
34
+ void write(uint8_t reg, uint8_t value);
35
+
36
+ // Timer update (call with CPU cycles elapsed)
37
+ void update(int cycles);
38
+
39
+ // Connect to PSG chip
40
+ void connectPSG(AY8910* psg) { psg_ = psg; }
41
+
42
+ // IRQ callback
43
+ void setIRQCallback(IRQCallback cb) { irqCallback_ = std::move(cb); }
44
+
45
+ // Reset
46
+ void reset();
47
+
48
+ // Check if IRQ is active
49
+ bool isIRQActive() const { return (ifr_ & ier_ & 0x7F) != 0; }
50
+
51
+ // Debug accessors
52
+ uint8_t getORA() const { return ora_; }
53
+ uint8_t getORB() const { return orb_; }
54
+ uint8_t getDDRA() const { return ddra_; }
55
+ uint8_t getDDRB() const { return ddrb_; }
56
+
57
+ // State serialization
58
+ size_t exportState(uint8_t* buffer) const;
59
+ void importState(const uint8_t* buffer);
60
+ static constexpr size_t STATE_SIZE = 32;
61
+
62
+ // Timer debug accessors
63
+ uint16_t getT1Counter() const { return t1Counter_; }
64
+ uint16_t getT1Latch() const { return t1Latch_; }
65
+ bool isT1Running() const { return t1Running_; }
66
+ bool hasT1Fired() const { return t1Fired_; }
67
+ uint8_t getACR() const { return acr_; }
68
+ uint8_t getIFR() const { return ifr_; }
69
+ uint8_t getIER() const { return ier_; }
70
+
71
+ private:
72
+ // Register addresses
73
+ static constexpr int REG_ORB = 0x00; // Output Register B / Input Register B
74
+ static constexpr int REG_ORA = 0x01; // Output Register A / Input Register A
75
+ static constexpr int REG_DDRB = 0x02; // Data Direction Register B
76
+ static constexpr int REG_DDRA = 0x03; // Data Direction Register A
77
+ static constexpr int REG_T1CL = 0x04; // Timer 1 Counter Low
78
+ static constexpr int REG_T1CH = 0x05; // Timer 1 Counter High
79
+ static constexpr int REG_T1LL = 0x06; // Timer 1 Latch Low
80
+ static constexpr int REG_T1LH = 0x07; // Timer 1 Latch High
81
+ static constexpr int REG_T2CL = 0x08; // Timer 2 Counter Low
82
+ static constexpr int REG_T2CH = 0x09; // Timer 2 Counter High
83
+ static constexpr int REG_SR = 0x0A; // Shift Register
84
+ static constexpr int REG_ACR = 0x0B; // Auxiliary Control Register
85
+ static constexpr int REG_PCR = 0x0C; // Peripheral Control Register
86
+ static constexpr int REG_IFR = 0x0D; // Interrupt Flag Register
87
+ static constexpr int REG_IER = 0x0E; // Interrupt Enable Register
88
+ static constexpr int REG_ORA_NH = 0x0F; // Output Register A (no handshake)
89
+
90
+ // IRQ flag bits
91
+ static constexpr uint8_t IRQ_CA2 = 0x01;
92
+ static constexpr uint8_t IRQ_CA1 = 0x02;
93
+ static constexpr uint8_t IRQ_SR = 0x04;
94
+ static constexpr uint8_t IRQ_CB2 = 0x08;
95
+ static constexpr uint8_t IRQ_CB1 = 0x10;
96
+ static constexpr uint8_t IRQ_T2 = 0x20;
97
+ static constexpr uint8_t IRQ_T1 = 0x40;
98
+ static constexpr uint8_t IRQ_ANY = 0x80;
99
+
100
+ // Port registers
101
+ uint8_t ora_ = 0; // Output Register A
102
+ uint8_t orb_ = 0; // Output Register B
103
+ uint8_t ddra_ = 0; // Data Direction Register A (1 = output)
104
+ uint8_t ddrb_ = 0; // Data Direction Register B (1 = output)
105
+ uint8_t ira_ = 0; // Input Register A (external input)
106
+ uint8_t irb_ = 0; // Input Register B (external input)
107
+
108
+ // Timer 1
109
+ uint16_t t1Counter_ = 0xFFFF;
110
+ uint16_t t1Latch_ = 0xFFFF;
111
+ bool t1Running_ = false;
112
+ bool t1Fired_ = false;
113
+
114
+ // Timer 2
115
+ uint16_t t2Counter_ = 0xFFFF;
116
+ uint8_t t2LatchLow_ = 0xFF;
117
+ bool t2Running_ = false;
118
+ bool t2Fired_ = false;
119
+
120
+ // Shift register
121
+ uint8_t sr_ = 0;
122
+
123
+ // Control registers
124
+ uint8_t acr_ = 0; // Auxiliary Control Register
125
+ uint8_t pcr_ = 0; // Peripheral Control Register
126
+ uint8_t ifr_ = 0; // Interrupt Flag Register
127
+ uint8_t ier_ = 0; // Interrupt Enable Register
128
+
129
+ // Connected PSG
130
+ AY8910* psg_ = nullptr;
131
+
132
+ // IRQ callback
133
+ IRQCallback irqCallback_;
134
+
135
+ // Helper methods
136
+ void updatePSG();
137
+ void checkIRQ();
138
+
139
+ // Previous PSG control state for edge detection
140
+ uint8_t prevPsgControl_ = 0;
141
+
142
+ // Track if a valid PSG address was latched (AppleWin-style)
143
+ bool psgAddressLatched_ = false;
144
+
145
+ // PSG state machine: operations only execute from INACTIVE state
146
+ enum PsgState { PSG_INACTIVE, PSG_READ, PSG_WRITE, PSG_LATCH };
147
+ PsgState psgState_ = PSG_INACTIVE;
148
+
149
+ // Bus-driven flag: true when PSG is driving port A data (READ state)
150
+ bool busDriven_ = false;
151
+
152
+ // Previous IRQ state for edge detection (only trigger on 0->1 transition)
153
+ bool prevIrqActive_ = false;
154
+
155
+ // IRQ delay - matches AppleWin's CheckTimerUnderflow behavior
156
+ int t1IrqDelay_ = 0;
157
+ int t2IrqDelay_ = 0;
158
+
159
+ // VIA identifier for debug logging
160
+ int viaId_ = 1;
161
+ };
162
+
163
+ } // namespace a2e
@@ -0,0 +1,312 @@
1
+ /*
2
+ * mockingboard_card.cpp - Mockingboard sound card implementation
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ #include "mockingboard_card.hpp"
9
+ #include <cstring>
10
+ #include <algorithm>
11
+
12
+ namespace a2e {
13
+
14
+ MockingboardCard::MockingboardCard() {
15
+ // Set IDs for debug logging
16
+ psg1_.setPsgId(1);
17
+ psg2_.setPsgId(2);
18
+ via1_.setViaId(1);
19
+ via2_.setViaId(2);
20
+
21
+ // Connect PSGs to VIAs
22
+ via1_.connectPSG(&psg1_);
23
+ via2_.connectPSG(&psg2_);
24
+
25
+ reset();
26
+ }
27
+
28
+ void MockingboardCard::setDebugLogging(bool enabled) {
29
+ AY8910::setDebugLogging(enabled);
30
+ VIA6522::setDebugLogging(enabled);
31
+ }
32
+
33
+ uint8_t MockingboardCard::readIO(uint8_t offset) {
34
+ // Mockingboard doesn't use I/O space ($C0C0-$C0CF)
35
+ // It uses ROM space for VIA registers
36
+ (void)offset;
37
+ return 0xFF;
38
+ }
39
+
40
+ void MockingboardCard::writeIO(uint8_t offset, uint8_t value) {
41
+ // Mockingboard doesn't use I/O space
42
+ (void)offset;
43
+ (void)value;
44
+ }
45
+
46
+ uint8_t MockingboardCard::peekIO(uint8_t offset) const {
47
+ (void)offset;
48
+ return 0xFF;
49
+ }
50
+
51
+ uint8_t MockingboardCard::readROM(uint8_t offset) {
52
+ if (!enabled_) return 0xFF;
53
+
54
+ // Address decoding for slot ROM space
55
+ // VIA 1 is mirrored at $C400-$C47F (active when bit 7 = 0)
56
+ // VIA 2 is mirrored at $C480-$C4FF (active when bit 7 = 1)
57
+ // Register is determined by bits 0-3
58
+
59
+ uint8_t reg = offset & 0x0F;
60
+
61
+ if ((offset & 0x80) == 0) {
62
+ // VIA 1 ($C400-$C47F)
63
+ return via1_.read(reg);
64
+ } else {
65
+ // VIA 2 ($C480-$C4FF)
66
+ return via2_.read(reg);
67
+ }
68
+ }
69
+
70
+ void MockingboardCard::writeROM(uint8_t offset, uint8_t value) {
71
+ if (!enabled_) return;
72
+
73
+ uint8_t reg = offset & 0x0F;
74
+
75
+ if ((offset & 0x80) == 0) {
76
+ // VIA 1 ($C400-$C47F)
77
+ via1_.write(reg, value);
78
+ } else {
79
+ // VIA 2 ($C480-$C4FF)
80
+ via2_.write(reg, value);
81
+ }
82
+ }
83
+
84
+ void MockingboardCard::reset() {
85
+ via1_.reset();
86
+ via2_.reset();
87
+ psg1_.reset();
88
+ psg2_.reset();
89
+ cycleAccum_ = 0.0;
90
+ sampleAccum_.clear();
91
+ sampleReadPos_ = 0;
92
+ dcStateL_ = 0.0f;
93
+ dcStateR_ = 0.0f;
94
+ }
95
+
96
+ bool MockingboardCard::arePsgsIdentical() const {
97
+ // Compare sound registers 0-13 (skip I/O ports 14-15)
98
+ for (int i = 0; i < 14; i++) {
99
+ if (psg1_.getRegister(i) != psg2_.getRegister(i)) return false;
100
+ }
101
+ return true;
102
+ }
103
+
104
+ void MockingboardCard::update(int cycles) {
105
+ if (!enabled_) return;
106
+
107
+ via1_.update(cycles);
108
+ via2_.update(cycles);
109
+
110
+ // Incremental audio generation: accumulate CPU cycles and generate
111
+ // audio samples at 48kHz rate. This ensures PSG register changes
112
+ // from VIA timer IRQ handlers are immediately reflected in output.
113
+ cycleAccum_ += cycles;
114
+ while (cycleAccum_ >= CYCLES_PER_SAMPLE) {
115
+ cycleAccum_ -= CYCLES_PER_SAMPLE;
116
+
117
+ float left = psg1_.generateSingleSample();
118
+ float right = psg2_.generateSingleSample();
119
+
120
+ // When both PSGs are programmed identically, use PSG1's output
121
+ // for both channels to eliminate phase cancellation from
122
+ // independent tone counters
123
+ if (arePsgsIdentical()) {
124
+ right = left;
125
+ }
126
+
127
+ sampleAccum_.push_back(left);
128
+ sampleAccum_.push_back(right);
129
+ }
130
+ }
131
+
132
+ void MockingboardCard::setIRQCallback(IRQCallback callback) {
133
+ // Both VIAs can trigger IRQ
134
+ via1_.setIRQCallback(callback);
135
+ via2_.setIRQCallback(callback);
136
+ }
137
+
138
+ bool MockingboardCard::isIRQActive() const {
139
+ return enabled_ && (via1_.isIRQActive() || via2_.isIRQActive());
140
+ }
141
+
142
+ size_t MockingboardCard::getStateSize() const {
143
+ return STATE_SIZE;
144
+ }
145
+
146
+ size_t MockingboardCard::serialize(uint8_t* buffer, size_t maxSize) const {
147
+ if (maxSize < STATE_SIZE) return 0;
148
+
149
+ size_t offset = 0;
150
+
151
+ // Enabled flag
152
+ buffer[offset++] = enabled_ ? 1 : 0;
153
+
154
+ // VIA1 and PSG1
155
+ offset += via1_.exportState(buffer + offset);
156
+ offset += psg1_.exportState(buffer + offset);
157
+
158
+ // VIA2 and PSG2
159
+ offset += via2_.exportState(buffer + offset);
160
+ offset += psg2_.exportState(buffer + offset);
161
+
162
+ return offset;
163
+ }
164
+
165
+ size_t MockingboardCard::deserialize(const uint8_t* buffer, size_t size) {
166
+ if (size < STATE_SIZE) return 0;
167
+
168
+ size_t offset = 0;
169
+
170
+ // Enabled flag
171
+ enabled_ = buffer[offset++] != 0;
172
+
173
+ // VIA1 and PSG1
174
+ via1_.importState(buffer + offset);
175
+ offset += VIA6522::STATE_SIZE;
176
+ psg1_.importState(buffer + offset);
177
+ offset += AY8910::STATE_SIZE;
178
+
179
+ // VIA2 and PSG2
180
+ via2_.importState(buffer + offset);
181
+ offset += VIA6522::STATE_SIZE;
182
+ psg2_.importState(buffer + offset);
183
+ offset += AY8910::STATE_SIZE;
184
+
185
+ // Reset DC filter state for clean audio restart
186
+ dcStateL_ = 0.0f;
187
+ dcStateR_ = 0.0f;
188
+
189
+ return offset;
190
+ }
191
+
192
+ void MockingboardCard::generateStereoSamples(float* buffer, int count, int sampleRate) {
193
+ if (!enabled_ || count <= 0) {
194
+ for (int i = 0; i < count * 2; i++) {
195
+ buffer[i] = 0.0f;
196
+ }
197
+ return;
198
+ }
199
+
200
+ if (static_cast<int>(audioBuffer1_.size()) < count) {
201
+ audioBuffer1_.resize(count);
202
+ }
203
+ if (static_cast<int>(audioBuffer2_.size()) < count) {
204
+ audioBuffer2_.resize(count);
205
+ }
206
+
207
+ psg1_.generateSamples(audioBuffer1_.data(), count, sampleRate);
208
+ psg2_.generateSamples(audioBuffer2_.data(), count, sampleRate);
209
+
210
+ bool identical = arePsgsIdentical();
211
+ for (int i = 0; i < count; i++) {
212
+ float left = audioBuffer1_[i];
213
+ float right = identical ? left : audioBuffer2_[i];
214
+
215
+ // DC offset removal
216
+ dcStateL_ = DC_ALPHA * dcStateL_ + (1.0f - DC_ALPHA) * left;
217
+ dcStateR_ = DC_ALPHA * dcStateR_ + (1.0f - DC_ALPHA) * right;
218
+ buffer[i * 2] = left - dcStateL_;
219
+ buffer[i * 2 + 1] = right - dcStateR_;
220
+ }
221
+ }
222
+
223
+ void MockingboardCard::generateStereoSamples(float* buffer, int count, int sampleRate, uint64_t startCycle, uint64_t endCycle) {
224
+ if (!enabled_ || count <= 0) {
225
+ for (int i = 0; i < count * 2; i++) {
226
+ buffer[i] = 0.0f;
227
+ }
228
+ return;
229
+ }
230
+
231
+ if (static_cast<int>(audioBuffer1_.size()) < count) {
232
+ audioBuffer1_.resize(count);
233
+ }
234
+ if (static_cast<int>(audioBuffer2_.size()) < count) {
235
+ audioBuffer2_.resize(count);
236
+ }
237
+
238
+ // Generate with proper timing
239
+ psg1_.generateSamples(audioBuffer1_.data(), count, sampleRate, startCycle, endCycle);
240
+ psg2_.generateSamples(audioBuffer2_.data(), count, sampleRate, startCycle, endCycle);
241
+
242
+ bool identical = arePsgsIdentical();
243
+ for (int i = 0; i < count; i++) {
244
+ float left = audioBuffer1_[i];
245
+ float right = identical ? left : audioBuffer2_[i];
246
+
247
+ // DC offset removal
248
+ dcStateL_ = DC_ALPHA * dcStateL_ + (1.0f - DC_ALPHA) * left;
249
+ dcStateR_ = DC_ALPHA * dcStateR_ + (1.0f - DC_ALPHA) * right;
250
+ buffer[i * 2] = left - dcStateL_;
251
+ buffer[i * 2 + 1] = right - dcStateR_;
252
+ }
253
+ }
254
+
255
+ int MockingboardCard::consumeStereoSamples(float* buffer, int frameCount) {
256
+ if (!enabled_ || frameCount <= 0) {
257
+ for (int i = 0; i < frameCount * 2; i++) {
258
+ buffer[i] = 0.0f;
259
+ }
260
+ return frameCount;
261
+ }
262
+
263
+ int availableFrames = static_cast<int>((sampleAccum_.size() - sampleReadPos_) / 2);
264
+ int framesToCopy = std::min(frameCount, availableFrames);
265
+
266
+ // Copy available accumulated samples
267
+ if (framesToCopy > 0) {
268
+ std::memcpy(buffer, sampleAccum_.data() + sampleReadPos_,
269
+ framesToCopy * 2 * sizeof(float));
270
+ sampleReadPos_ += framesToCopy * 2;
271
+ }
272
+
273
+ // If we need more samples than accumulated, generate the remainder on the spot
274
+ // (handles slight timing drift between CPU execution and audio requests)
275
+ if (framesToCopy < frameCount) {
276
+ bool identical = arePsgsIdentical();
277
+ for (int i = framesToCopy; i < frameCount; i++) {
278
+ float left = psg1_.generateSingleSample();
279
+ float right = identical ? left : psg2_.generateSingleSample();
280
+ buffer[i * 2] = left;
281
+ buffer[i * 2 + 1] = right;
282
+ }
283
+ }
284
+
285
+ // DC offset removal converts unipolar PSG output to bipolar for audio playback.
286
+ // Identity check was already applied during accumulation in update() and
287
+ // in the overflow path above.
288
+ for (int i = 0; i < frameCount; i++) {
289
+ float left = buffer[i * 2];
290
+ float right = buffer[i * 2 + 1];
291
+
292
+ dcStateL_ = DC_ALPHA * dcStateL_ + (1.0f - DC_ALPHA) * left;
293
+ dcStateR_ = DC_ALPHA * dcStateR_ + (1.0f - DC_ALPHA) * right;
294
+ buffer[i * 2] = left - dcStateL_;
295
+ buffer[i * 2 + 1] = right - dcStateR_;
296
+ }
297
+
298
+ // Compact the buffer: remove consumed samples
299
+ if (sampleReadPos_ > 0) {
300
+ size_t remaining = sampleAccum_.size() - sampleReadPos_;
301
+ if (remaining > 0) {
302
+ std::memmove(sampleAccum_.data(), sampleAccum_.data() + sampleReadPos_,
303
+ remaining * sizeof(float));
304
+ }
305
+ sampleAccum_.resize(remaining);
306
+ sampleReadPos_ = 0;
307
+ }
308
+
309
+ return frameCount;
310
+ }
311
+
312
+ } // namespace a2e
@@ -0,0 +1,159 @@
1
+ /*
2
+ * mockingboard_card.hpp - Mockingboard sound card
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ #pragma once
9
+
10
+ #include "expansion_card.hpp"
11
+ #include "mockingboard/via6522.hpp"
12
+ #include "mockingboard/ay8910.hpp"
13
+ #include <vector>
14
+
15
+ namespace a2e {
16
+
17
+ /**
18
+ * MockingboardCard - Mockingboard Sound Card
19
+ *
20
+ * Implements the ExpansionCard interface with direct ownership of VIA/PSG chips.
21
+ * The Mockingboard typically occupies slot 4, providing:
22
+ * - I/O space: $C0C0-$C0CF (unused - Mockingboard uses ROM space for VIA access)
23
+ *
24
+ * Note: The Mockingboard is unusual in that it uses the slot ROM space
25
+ * ($C400-$C4FF) for its VIA registers rather than the I/O space ($C0C0-$C0CF).
26
+ * This is because it needs more than 16 bytes of address space.
27
+ *
28
+ * VIA 1: $C400-$C47F (bit 7 = 0) - left channel PSG
29
+ * VIA 2: $C480-$C4FF (bit 7 = 1) - right channel PSG
30
+ */
31
+ class MockingboardCard : public ExpansionCard {
32
+ public:
33
+ using CycleCallback = std::function<uint64_t()>;
34
+
35
+ // State size for serialization: enabled(1) + VIA1(32) + PSG1(48) + VIA2(32) + PSG2(48) = 161
36
+ static constexpr size_t STATE_SIZE = 161;
37
+
38
+ MockingboardCard();
39
+ ~MockingboardCard() override = default;
40
+
41
+ // Delete copy
42
+ MockingboardCard(const MockingboardCard&) = delete;
43
+ MockingboardCard& operator=(const MockingboardCard&) = delete;
44
+
45
+ // Allow move
46
+ MockingboardCard(MockingboardCard&&) = default;
47
+ MockingboardCard& operator=(MockingboardCard&&) = default;
48
+
49
+ // ===== ExpansionCard Interface =====
50
+
51
+ // I/O space ($C0C0-$C0CF) - Mockingboard doesn't use this
52
+ uint8_t readIO(uint8_t offset) override;
53
+ void writeIO(uint8_t offset, uint8_t value) override;
54
+ uint8_t peekIO(uint8_t offset) const override;
55
+
56
+ // ROM space ($C400-$C4FF) - Contains VIA registers
57
+ uint8_t readROM(uint8_t offset) override;
58
+ void writeROM(uint8_t offset, uint8_t value) override;
59
+ bool hasROM() const override { return true; }
60
+
61
+ bool hasExpansionROM() const override { return false; }
62
+
63
+ void reset() override;
64
+ void update(int cycles) override;
65
+
66
+ void setIRQCallback(IRQCallback callback) override;
67
+ void setCycleCallback(CycleCallback callback) override {
68
+ cycleCallback_ = callback;
69
+ // Pass to PSGs for timestamped register writes
70
+ psg1_.setCycleCallback(callback);
71
+ psg2_.setCycleCallback(callback);
72
+ }
73
+
74
+ bool isIRQActive() const override;
75
+
76
+ size_t getStateSize() const override;
77
+ size_t serialize(uint8_t* buffer, size_t maxSize) const override;
78
+ size_t deserialize(const uint8_t* buffer, size_t size) override;
79
+
80
+ const char* getName() const override { return "Mockingboard"; }
81
+ uint8_t getPreferredSlot() const override { return 4; }
82
+
83
+ bool isEnabled() const override { return enabled_; }
84
+ void setEnabled(bool enabled) override { enabled_ = enabled; }
85
+
86
+ // ===== Audio Generation =====
87
+
88
+ /**
89
+ * Generate stereo audio samples (legacy, no timing)
90
+ */
91
+ void generateStereoSamples(float* buffer, int count, int sampleRate);
92
+
93
+ /**
94
+ * Generate stereo audio samples with proper timing
95
+ */
96
+ void generateStereoSamples(float* buffer, int count, int sampleRate, uint64_t startCycle, uint64_t endCycle);
97
+
98
+ /**
99
+ * Consume accumulated stereo samples (from incremental generation).
100
+ * Returns actual number of sample frames consumed.
101
+ * If fewer samples available than requested, generates remaining on the spot.
102
+ */
103
+ int consumeStereoSamples(float* buffer, int frameCount);
104
+
105
+ /**
106
+ * Enable/disable debug logging
107
+ * @param enabled true to enable
108
+ */
109
+ void setDebugLogging(bool enabled);
110
+
111
+ // ===== Debug Access =====
112
+ const VIA6522& getVIA1() const { return via1_; }
113
+ const VIA6522& getVIA2() const { return via2_; }
114
+ const AY8910& getPSG1() const { return psg1_; }
115
+ const AY8910& getPSG2() const { return psg2_; }
116
+ AY8910& getPSG1() { return psg1_; }
117
+ AY8910& getPSG2() { return psg2_; }
118
+
119
+ private:
120
+ // Two VIA chips
121
+ VIA6522 via1_; // $C400-$C47F (bit 7 = 0)
122
+ VIA6522 via2_; // $C480-$C4FF (bit 7 = 1)
123
+
124
+ // Two PSG chips
125
+ AY8910 psg1_; // Connected to VIA1 (left channel)
126
+ AY8910 psg2_; // Connected to VIA2 (right channel)
127
+
128
+ // Enabled state
129
+ bool enabled_ = true;
130
+
131
+ // Callbacks
132
+ CycleCallback cycleCallback_;
133
+
134
+ // Preallocated audio buffers to avoid heap allocations in audio hot path
135
+ mutable std::vector<float> audioBuffer1_;
136
+ mutable std::vector<float> audioBuffer2_;
137
+
138
+ // Phase coherence: when both PSGs have identical sound registers (0-13),
139
+ // use PSG1's output for both channels. This eliminates phase cancellation
140
+ // caused by independent tone counters producing anti-phase waveforms
141
+ // when Mockingboard music mirrors content to both PSGs.
142
+ bool arePsgsIdentical() const;
143
+
144
+ // Per-channel DC offset removal (high-pass filter)
145
+ // Converts unipolar PSG output to bipolar for audio playback.
146
+ // Slow time constant (~200ms at 48kHz) avoids tracking musical content.
147
+ static constexpr float DC_ALPHA = 0.9999f;
148
+ float dcStateL_ = 0.0f;
149
+ float dcStateR_ = 0.0f;
150
+
151
+ // Incremental audio generation state
152
+ // CPU cycles per audio sample at 48kHz: 1,023,000 / 48,000 ≈ 21.3125
153
+ static constexpr double CYCLES_PER_SAMPLE = 1023000.0 / 48000.0;
154
+ double cycleAccum_ = 0.0; // Fractional CPU cycle accumulator
155
+ std::vector<float> sampleAccum_; // Accumulated stereo samples (interleaved L/R)
156
+ size_t sampleReadPos_ = 0; // Read position in accumulated buffer
157
+ };
158
+
159
+ } // namespace a2e