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,323 @@
1
+ /*
2
+ * test_mmu_slots.cpp - Unit tests for MMU expansion slot system
3
+ *
4
+ * Tests the expansion card slot management including card insertion,
5
+ * removal, ROM routing, I/O routing, INTCXROM/SLOTC3ROM switches,
6
+ * expansion ROM activation, and multiple cards.
7
+ */
8
+
9
+ #define CATCH_CONFIG_MAIN
10
+ #include "catch.hpp"
11
+
12
+ #include "mmu/mmu.hpp"
13
+ #include "cards/thunderclock_card.hpp"
14
+ #include "roms.cpp"
15
+
16
+ #include <memory>
17
+
18
+ using namespace a2e;
19
+
20
+ // Helper: create an MMU with ROMs loaded
21
+ static std::unique_ptr<MMU> createMMU() {
22
+ auto mmu = std::make_unique<MMU>();
23
+ mmu->loadROM(roms::ROM_SYSTEM, roms::ROM_SYSTEM_SIZE,
24
+ roms::ROM_CHAR, roms::ROM_CHAR_SIZE);
25
+ return mmu;
26
+ }
27
+
28
+ // ============================================================================
29
+ // Slot empty/occupied state
30
+ // ============================================================================
31
+
32
+ TEST_CASE("Slots are initially empty", "[mmu][slots]") {
33
+ auto mmu = createMMU();
34
+
35
+ for (int slot = 1; slot <= 7; ++slot) {
36
+ INFO("Slot " << slot);
37
+ CHECK(mmu->isSlotEmpty(slot));
38
+ CHECK(mmu->getCard(slot) == nullptr);
39
+ }
40
+ }
41
+
42
+ TEST_CASE("isSlotEmpty returns true for out-of-range slots", "[mmu][slots]") {
43
+ auto mmu = createMMU();
44
+
45
+ CHECK(mmu->isSlotEmpty(0));
46
+ CHECK(mmu->isSlotEmpty(8));
47
+ }
48
+
49
+ // ============================================================================
50
+ // Card insertion
51
+ // ============================================================================
52
+
53
+ TEST_CASE("Insert card into slot: slot becomes occupied", "[mmu][slots]") {
54
+ auto mmu = createMMU();
55
+
56
+ auto card = std::make_unique<ThunderclockCard>();
57
+ auto* cardPtr = card.get();
58
+
59
+ auto removed = mmu->insertCard(5, std::move(card));
60
+
61
+ CHECK(removed == nullptr); // Slot was empty
62
+ CHECK_FALSE(mmu->isSlotEmpty(5));
63
+ CHECK(mmu->getCard(5) == cardPtr);
64
+ }
65
+
66
+ TEST_CASE("insertCard returns previously installed card", "[mmu][slots]") {
67
+ auto mmu = createMMU();
68
+
69
+ auto card1 = std::make_unique<ThunderclockCard>();
70
+ auto* card1Ptr = card1.get();
71
+ mmu->insertCard(5, std::move(card1));
72
+
73
+ auto card2 = std::make_unique<ThunderclockCard>();
74
+ auto removed = mmu->insertCard(5, std::move(card2));
75
+
76
+ // Should get back card1
77
+ REQUIRE(removed != nullptr);
78
+ CHECK(removed.get() == card1Ptr);
79
+ CHECK_FALSE(mmu->isSlotEmpty(5));
80
+ }
81
+
82
+ // ============================================================================
83
+ // Card removal
84
+ // ============================================================================
85
+
86
+ TEST_CASE("removeCard returns the card and slot becomes empty", "[mmu][slots]") {
87
+ auto mmu = createMMU();
88
+
89
+ auto card = std::make_unique<ThunderclockCard>();
90
+ auto* cardPtr = card.get();
91
+ mmu->insertCard(5, std::move(card));
92
+
93
+ auto removed = mmu->removeCard(5);
94
+ REQUIRE(removed != nullptr);
95
+ CHECK(removed.get() == cardPtr);
96
+ CHECK(mmu->isSlotEmpty(5));
97
+ }
98
+
99
+ TEST_CASE("removeCard from empty slot returns nullptr", "[mmu][slots]") {
100
+ auto mmu = createMMU();
101
+
102
+ auto removed = mmu->removeCard(5);
103
+ CHECK(removed == nullptr);
104
+ }
105
+
106
+ // ============================================================================
107
+ // Slot ROM routing
108
+ // ============================================================================
109
+
110
+ TEST_CASE("Slot ROM: read $C500-$C5FF accesses Thunderclock card ROM", "[mmu][slots][rom]") {
111
+ auto mmu = createMMU();
112
+ mmu->insertCard(5, std::make_unique<ThunderclockCard>());
113
+
114
+ // Thunderclock ROM signature bytes (ProDOS clock detection)
115
+ CHECK(mmu->read(0xC500) == 0x08); // PHP
116
+ CHECK(mmu->read(0xC502) == 0x28); // PLP
117
+ CHECK(mmu->read(0xC504) == 0x58); // CLI
118
+ CHECK(mmu->read(0xC506) == 0x70); // BVS
119
+ }
120
+
121
+ TEST_CASE("Slot ROM: empty slot returns floating bus value", "[mmu][slots][rom]") {
122
+ auto mmu = createMMU();
123
+
124
+ // No cycle callback so floating bus returns 0x00
125
+ uint8_t val = mmu->read(0xC500);
126
+ CHECK(val == 0x00);
127
+ }
128
+
129
+ // ============================================================================
130
+ // Slot I/O routing
131
+ // ============================================================================
132
+
133
+ TEST_CASE("Slot I/O: read/write $C0D0-$C0DF accesses slot 5 card", "[mmu][slots][io]") {
134
+ auto mmu = createMMU();
135
+ mmu->insertCard(5, std::make_unique<ThunderclockCard>());
136
+
137
+ // Initial I/O read from Thunderclock
138
+ uint8_t initialValue = mmu->read(0xC0D0);
139
+ CHECK(initialValue == 0x00);
140
+
141
+ // Write a command and verify I/O is being routed
142
+ mmu->write(0xC0D0, 0x00);
143
+ mmu->write(0xC0D0, 0xA4); // CMD_TIMED | FLAG_STROBE
144
+
145
+ // The fact that we can write and read without crashing confirms routing
146
+ uint8_t result = mmu->read(0xC0D0);
147
+ // Result will have bit 7 set to data bit from Thunderclock
148
+ (void)result; // Value depends on time, just verify no crash
149
+ }
150
+
151
+ TEST_CASE("Slot I/O: slot 7 I/O at $C0F0-$C0FF", "[mmu][slots][io]") {
152
+ auto mmu = createMMU();
153
+ mmu->insertCard(7, std::make_unique<ThunderclockCard>());
154
+
155
+ uint8_t val = mmu->read(0xC0F0);
156
+ CHECK(val == 0x00);
157
+
158
+ // Slot 7 ROM at $C700
159
+ CHECK(mmu->read(0xC700) == 0x08);
160
+ }
161
+
162
+ // ============================================================================
163
+ // INTCXROM switch
164
+ // ============================================================================
165
+
166
+ TEST_CASE("INTCXROM: when internal, slot ROM reads from internal ROM", "[mmu][slots][intcxrom]") {
167
+ auto mmu = createMMU();
168
+ mmu->insertCard(5, std::make_unique<ThunderclockCard>());
169
+
170
+ // Verify card ROM is accessible first
171
+ CHECK(mmu->read(0xC500) == 0x08);
172
+
173
+ // Enable INTCXROM (write to $C007)
174
+ mmu->write(0xC007, 0);
175
+ CHECK(mmu->getSoftSwitches().intcxrom);
176
+
177
+ // Now $C500 should read from internal ROM instead of card
178
+ uint8_t internalByte = roms::ROM_SYSTEM[0x0500]; // offset $C500 - $C000 = $0500
179
+ CHECK(mmu->read(0xC500) == internalByte);
180
+
181
+ // Disable INTCXROM (write to $C006)
182
+ mmu->write(0xC006, 0);
183
+ CHECK_FALSE(mmu->getSoftSwitches().intcxrom);
184
+
185
+ // Card ROM should be accessible again
186
+ CHECK(mmu->read(0xC500) == 0x08);
187
+ }
188
+
189
+ // ============================================================================
190
+ // SLOTC3ROM switch
191
+ // ============================================================================
192
+
193
+ TEST_CASE("SLOTC3ROM: controls slot 3 behavior", "[mmu][slots][slotc3rom]") {
194
+ auto mmu = createMMU();
195
+
196
+ // Default: SLOTC3ROM is off, slot 3 uses internal ROM
197
+ CHECK_FALSE(mmu->getSoftSwitches().slotc3rom);
198
+
199
+ // Read from $C300 should return internal ROM
200
+ uint8_t internalByte = roms::ROM_SYSTEM[0x0300]; // $C300 - $C000
201
+ CHECK(mmu->read(0xC300) == internalByte);
202
+
203
+ // Enable SLOTC3ROM (write to $C00B)
204
+ mmu->write(0xC00B, 0);
205
+ CHECK(mmu->getSoftSwitches().slotc3rom);
206
+
207
+ // Disable SLOTC3ROM (write to $C00A)
208
+ mmu->write(0xC00A, 0);
209
+ CHECK_FALSE(mmu->getSoftSwitches().slotc3rom);
210
+ }
211
+
212
+ // ============================================================================
213
+ // Expansion ROM
214
+ // ============================================================================
215
+
216
+ TEST_CASE("Expansion ROM: reading $Cn00 activates expansion ROM at $C800", "[mmu][slots][exprom]") {
217
+ auto mmu = createMMU();
218
+ mmu->insertCard(5, std::make_unique<ThunderclockCard>());
219
+
220
+ // Access slot 5 ROM to activate expansion ROM
221
+ mmu->read(0xC500);
222
+
223
+ // Thunderclock has expansion ROM, should be active now
224
+ CHECK(mmu->getActiveExpansionSlot() == 5);
225
+
226
+ // Read from expansion ROM area - should not be floating bus
227
+ uint8_t byte = mmu->read(0xC800);
228
+ // Thunderclock expansion ROM should have valid data (not 0xFF floating bus)
229
+ // We just verify it was activated and returns card data
230
+ (void)byte;
231
+ }
232
+
233
+ TEST_CASE("Expansion ROM: reading $CFFF deactivates expansion ROM", "[mmu][slots][exprom]") {
234
+ auto mmu = createMMU();
235
+ mmu->insertCard(5, std::make_unique<ThunderclockCard>());
236
+
237
+ // Activate expansion ROM
238
+ mmu->read(0xC500);
239
+ REQUIRE(mmu->getActiveExpansionSlot() == 5);
240
+
241
+ // Read $CFFF to deactivate
242
+ mmu->read(0xCFFF);
243
+ CHECK(mmu->getActiveExpansionSlot() == 0);
244
+ }
245
+
246
+ TEST_CASE("Expansion ROM: switching between cards updates active slot", "[mmu][slots][exprom]") {
247
+ auto mmu = createMMU();
248
+ mmu->insertCard(5, std::make_unique<ThunderclockCard>());
249
+ mmu->insertCard(7, std::make_unique<ThunderclockCard>());
250
+
251
+ // Activate slot 5 expansion ROM
252
+ mmu->read(0xC500);
253
+ CHECK(mmu->getActiveExpansionSlot() == 5);
254
+
255
+ // Activate slot 7 expansion ROM
256
+ mmu->read(0xC700);
257
+ CHECK(mmu->getActiveExpansionSlot() == 7);
258
+ }
259
+
260
+ // ============================================================================
261
+ // Multiple cards in different slots
262
+ // ============================================================================
263
+
264
+ TEST_CASE("Multiple cards: independent access in different slots", "[mmu][slots][multi]") {
265
+ auto mmu = createMMU();
266
+ mmu->insertCard(5, std::make_unique<ThunderclockCard>());
267
+ mmu->insertCard(7, std::make_unique<ThunderclockCard>());
268
+
269
+ // Both should be accessible
270
+ CHECK(mmu->read(0xC500) == 0x08);
271
+ CHECK(mmu->read(0xC700) == 0x08);
272
+
273
+ // Both slot I/O spaces should work independently
274
+ CHECK(mmu->read(0xC0D0) == 0x00); // Slot 5
275
+ CHECK(mmu->read(0xC0F0) == 0x00); // Slot 7
276
+
277
+ // Slots that don't have cards are still empty
278
+ CHECK(mmu->isSlotEmpty(1));
279
+ CHECK(mmu->isSlotEmpty(2));
280
+ }
281
+
282
+ TEST_CASE("Card in slot 1: ROM at $C100, I/O at $C090", "[mmu][slots][multi]") {
283
+ auto mmu = createMMU();
284
+ mmu->insertCard(1, std::make_unique<ThunderclockCard>());
285
+
286
+ // Slot 1 ROM at $C100
287
+ CHECK(mmu->read(0xC100) == 0x08);
288
+
289
+ // Slot 1 I/O at $C090
290
+ CHECK(mmu->read(0xC090) == 0x00);
291
+ }
292
+
293
+ // ============================================================================
294
+ // INTCXROM affects all slot ROMs
295
+ // ============================================================================
296
+
297
+ TEST_CASE("INTCXROM blocks ROM for all slots simultaneously", "[mmu][slots][intcxrom]") {
298
+ auto mmu = createMMU();
299
+ mmu->insertCard(1, std::make_unique<ThunderclockCard>());
300
+ mmu->insertCard(5, std::make_unique<ThunderclockCard>());
301
+ mmu->insertCard(7, std::make_unique<ThunderclockCard>());
302
+
303
+ // All cards read their own ROM initially
304
+ CHECK(mmu->read(0xC100) == 0x08);
305
+ CHECK(mmu->read(0xC500) == 0x08);
306
+ CHECK(mmu->read(0xC700) == 0x08);
307
+
308
+ // Enable INTCXROM
309
+ mmu->write(0xC007, 0);
310
+
311
+ // All slots now read internal ROM
312
+ CHECK(mmu->read(0xC100) == roms::ROM_SYSTEM[0x0100]);
313
+ CHECK(mmu->read(0xC500) == roms::ROM_SYSTEM[0x0500]);
314
+ CHECK(mmu->read(0xC700) == roms::ROM_SYSTEM[0x0700]);
315
+
316
+ // Disable INTCXROM
317
+ mmu->write(0xC006, 0);
318
+
319
+ // Cards are accessible again
320
+ CHECK(mmu->read(0xC100) == 0x08);
321
+ CHECK(mmu->read(0xC500) == 0x08);
322
+ CHECK(mmu->read(0xC700) == 0x08);
323
+ }
@@ -0,0 +1,352 @@
1
+ /*
2
+ * test_mockingboard.cpp - Unit tests for MockingboardCard
3
+ *
4
+ * Tests the Mockingboard sound card implementation including:
5
+ * - Construction
6
+ * - Card metadata (name, preferred slot)
7
+ * - VIA register access via ROM space
8
+ * - PSG register write sequence via VIA
9
+ * - Timer updates
10
+ * - Reset behavior
11
+ * - Enable/disable state
12
+ * - Audio sample generation
13
+ * - Serialization round-trip
14
+ * - IRQ generation from VIA timer
15
+ */
16
+
17
+ #define CATCH_CONFIG_MAIN
18
+ #include "catch.hpp"
19
+
20
+ #include "mockingboard_card.hpp"
21
+
22
+ #include <cstring>
23
+ #include <vector>
24
+
25
+ using namespace a2e;
26
+
27
+ // VIA register offsets (within the VIA's 16-register space)
28
+ static constexpr uint8_t VIA_ORB = 0x00;
29
+ static constexpr uint8_t VIA_ORA = 0x01;
30
+ static constexpr uint8_t VIA_DDRB = 0x02;
31
+ static constexpr uint8_t VIA_DDRA = 0x03;
32
+ static constexpr uint8_t VIA_T1CL = 0x04;
33
+ static constexpr uint8_t VIA_T1CH = 0x05;
34
+ static constexpr uint8_t VIA_T1LL = 0x06;
35
+ static constexpr uint8_t VIA_T1LH = 0x07;
36
+ static constexpr uint8_t VIA_ACR = 0x0B;
37
+ static constexpr uint8_t VIA_IFR = 0x0D;
38
+ static constexpr uint8_t VIA_IER = 0x0E;
39
+
40
+ // VIA1 base offset in ROM space: bit 7 = 0, so offsets 0x00-0x0F
41
+ // VIA2 base offset in ROM space: bit 7 = 1, so offsets 0x80-0x8F
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // Construction
45
+ // ---------------------------------------------------------------------------
46
+
47
+ TEST_CASE("MockingboardCard constructor creates a valid instance", "[mockingboard]") {
48
+ MockingboardCard card;
49
+ REQUIRE(card.getName() != nullptr);
50
+ }
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Card metadata
54
+ // ---------------------------------------------------------------------------
55
+
56
+ TEST_CASE("MockingboardCard getName returns Mockingboard", "[mockingboard]") {
57
+ MockingboardCard card;
58
+ REQUIRE(std::string(card.getName()) == "Mockingboard");
59
+ }
60
+
61
+ TEST_CASE("MockingboardCard getPreferredSlot returns 4", "[mockingboard]") {
62
+ MockingboardCard card;
63
+ REQUIRE(card.getPreferredSlot() == 4);
64
+ }
65
+
66
+ TEST_CASE("MockingboardCard hasROM returns true", "[mockingboard]") {
67
+ MockingboardCard card;
68
+ REQUIRE(card.hasROM());
69
+ }
70
+
71
+ TEST_CASE("MockingboardCard hasExpansionROM returns false", "[mockingboard]") {
72
+ MockingboardCard card;
73
+ REQUIRE_FALSE(card.hasExpansionROM());
74
+ }
75
+
76
+ // ---------------------------------------------------------------------------
77
+ // VIA register access via ROM space
78
+ // ---------------------------------------------------------------------------
79
+
80
+ TEST_CASE("MockingboardCard write/read VIA1 DDRA register", "[mockingboard]") {
81
+ MockingboardCard card;
82
+
83
+ // Write 0xFF to VIA1 DDRA (offset 0x03 in VIA1 space = ROM offset 0x03)
84
+ card.writeROM(VIA_DDRA, 0xFF);
85
+ uint8_t val = card.readROM(VIA_DDRA);
86
+ REQUIRE(val == 0xFF);
87
+ }
88
+
89
+ TEST_CASE("MockingboardCard write/read VIA1 DDRB register", "[mockingboard]") {
90
+ MockingboardCard card;
91
+
92
+ card.writeROM(VIA_DDRB, 0xFF);
93
+ uint8_t val = card.readROM(VIA_DDRB);
94
+ REQUIRE(val == 0xFF);
95
+ }
96
+
97
+ TEST_CASE("MockingboardCard write/read VIA2 DDRA register", "[mockingboard]") {
98
+ MockingboardCard card;
99
+
100
+ // VIA2 registers are at offset 0x80+
101
+ card.writeROM(0x80 | VIA_DDRA, 0xFF);
102
+ uint8_t val = card.readROM(0x80 | VIA_DDRA);
103
+ REQUIRE(val == 0xFF);
104
+ }
105
+
106
+ TEST_CASE("MockingboardCard write/read VIA2 DDRB register", "[mockingboard]") {
107
+ MockingboardCard card;
108
+
109
+ card.writeROM(0x80 | VIA_DDRB, 0xFF);
110
+ uint8_t val = card.readROM(0x80 | VIA_DDRB);
111
+ REQUIRE(val == 0xFF);
112
+ }
113
+
114
+ TEST_CASE("MockingboardCard VIA1 and VIA2 are independent", "[mockingboard]") {
115
+ MockingboardCard card;
116
+
117
+ card.writeROM(VIA_DDRA, 0xAA);
118
+ card.writeROM(0x80 | VIA_DDRA, 0x55);
119
+
120
+ REQUIRE(card.readROM(VIA_DDRA) == 0xAA);
121
+ REQUIRE(card.readROM(0x80 | VIA_DDRA) == 0x55);
122
+ }
123
+
124
+ // ---------------------------------------------------------------------------
125
+ // PSG register write sequence via VIA
126
+ // ---------------------------------------------------------------------------
127
+
128
+ TEST_CASE("MockingboardCard PSG register write via VIA1 protocol", "[mockingboard]") {
129
+ MockingboardCard card;
130
+
131
+ // The PSG write protocol via VIA:
132
+ // 1. Set DDRA to 0xFF (all outputs) and DDRB to 0xFF (all outputs)
133
+ // 2. Write register address to ORA
134
+ // 3. Set ORB to 0x07 (LATCH command: BC1=1, BDIR=1, /RESET=1)
135
+ // 4. Set ORB to 0x04 (INACTIVE: BC1=0, BDIR=0, /RESET=1)
136
+ // 5. Write register value to ORA
137
+ // 6. Set ORB to 0x06 (WRITE command: BC1=0, BDIR=1, /RESET=1)
138
+ // 7. Set ORB to 0x04 (INACTIVE)
139
+
140
+ // Step 1: Set data directions
141
+ card.writeROM(VIA_DDRA, 0xFF);
142
+ card.writeROM(VIA_DDRB, 0xFF);
143
+
144
+ // Step 2: Latch register address (register 7 = mixer)
145
+ card.writeROM(VIA_ORA, 0x07);
146
+ card.writeROM(VIA_ORB, 0x07); // LATCH
147
+ card.writeROM(VIA_ORB, 0x04); // INACTIVE
148
+
149
+ // Step 3: Write value to register
150
+ card.writeROM(VIA_ORA, 0x38); // All channels: tone on, noise off
151
+ card.writeROM(VIA_ORB, 0x06); // WRITE
152
+ card.writeROM(VIA_ORB, 0x04); // INACTIVE
153
+
154
+ // Verify via debug accessor
155
+ const AY8910& psg1 = card.getPSG1();
156
+ REQUIRE(psg1.getRegister(7) == 0x38);
157
+ }
158
+
159
+ // ---------------------------------------------------------------------------
160
+ // update() advances timers
161
+ // ---------------------------------------------------------------------------
162
+
163
+ TEST_CASE("MockingboardCard update does not crash", "[mockingboard]") {
164
+ MockingboardCard card;
165
+
166
+ // Simply verify update() runs without crashing
167
+ card.update(100);
168
+ card.update(1000);
169
+ card.update(10000);
170
+ REQUIRE(true); // If we get here, no crash occurred
171
+ }
172
+
173
+ // ---------------------------------------------------------------------------
174
+ // reset() clears state
175
+ // ---------------------------------------------------------------------------
176
+
177
+ TEST_CASE("MockingboardCard reset clears VIA state", "[mockingboard]") {
178
+ MockingboardCard card;
179
+
180
+ // Write something to VIA1
181
+ card.writeROM(VIA_DDRA, 0xFF);
182
+ card.writeROM(VIA_ORA, 0xAA);
183
+
184
+ card.reset();
185
+
186
+ // After reset, DDRA should be 0
187
+ const VIA6522& via1 = card.getVIA1();
188
+ REQUIRE(via1.getDDRA() == 0x00);
189
+ REQUIRE(via1.getORA() == 0x00);
190
+ }
191
+
192
+ // ---------------------------------------------------------------------------
193
+ // Enable/disable
194
+ // ---------------------------------------------------------------------------
195
+
196
+ TEST_CASE("MockingboardCard isEnabled is true by default", "[mockingboard]") {
197
+ MockingboardCard card;
198
+ REQUIRE(card.isEnabled());
199
+ }
200
+
201
+ TEST_CASE("MockingboardCard setEnabled toggles state", "[mockingboard]") {
202
+ MockingboardCard card;
203
+
204
+ card.setEnabled(false);
205
+ REQUIRE_FALSE(card.isEnabled());
206
+
207
+ card.setEnabled(true);
208
+ REQUIRE(card.isEnabled());
209
+ }
210
+
211
+ // ---------------------------------------------------------------------------
212
+ // Audio generation
213
+ // ---------------------------------------------------------------------------
214
+
215
+ TEST_CASE("MockingboardCard generateStereoSamples produces output", "[mockingboard]") {
216
+ MockingboardCard card;
217
+
218
+ // Generate a small buffer of stereo samples
219
+ const int frameCount = 128;
220
+ std::vector<float> buffer(frameCount * 2, -999.0f); // interleaved L/R
221
+
222
+ card.generateStereoSamples(buffer.data(), frameCount, 48000);
223
+
224
+ // After generation, buffer should have been written (values should not be -999)
225
+ bool anyWritten = false;
226
+ for (int i = 0; i < frameCount * 2; ++i) {
227
+ if (buffer[i] != -999.0f) {
228
+ anyWritten = true;
229
+ break;
230
+ }
231
+ }
232
+ REQUIRE(anyWritten);
233
+ }
234
+
235
+ TEST_CASE("MockingboardCard generateStereoSamples with timing does not crash", "[mockingboard]") {
236
+ MockingboardCard card;
237
+
238
+ const int frameCount = 128;
239
+ std::vector<float> buffer(frameCount * 2, 0.0f);
240
+
241
+ card.generateStereoSamples(buffer.data(), frameCount, 48000, 0, 2730);
242
+ REQUIRE(true); // No crash
243
+ }
244
+
245
+ // ---------------------------------------------------------------------------
246
+ // I/O space (unused by Mockingboard)
247
+ // ---------------------------------------------------------------------------
248
+
249
+ TEST_CASE("MockingboardCard readIO returns a value without crash", "[mockingboard]") {
250
+ MockingboardCard card;
251
+ // Mockingboard does not use I/O space, but calling it should not crash
252
+ uint8_t val = card.readIO(0x00);
253
+ (void)val;
254
+ REQUIRE(true);
255
+ }
256
+
257
+ TEST_CASE("MockingboardCard writeIO does not crash", "[mockingboard]") {
258
+ MockingboardCard card;
259
+ card.writeIO(0x00, 0x55);
260
+ REQUIRE(true);
261
+ }
262
+
263
+ // ---------------------------------------------------------------------------
264
+ // Serialization round-trip
265
+ // ---------------------------------------------------------------------------
266
+
267
+ TEST_CASE("MockingboardCard getStateSize returns expected size", "[mockingboard]") {
268
+ MockingboardCard card;
269
+ REQUIRE(card.getStateSize() == MockingboardCard::STATE_SIZE);
270
+ }
271
+
272
+ TEST_CASE("MockingboardCard serialize/deserialize round-trip", "[mockingboard]") {
273
+ MockingboardCard card1;
274
+
275
+ // Set up some state: write to PSG via VIA1
276
+ card1.writeROM(VIA_DDRA, 0xFF);
277
+ card1.writeROM(VIA_DDRB, 0xFF);
278
+ card1.writeROM(VIA_ORA, 0x07); // register address = 7
279
+ card1.writeROM(VIA_ORB, 0x07); // LATCH
280
+ card1.writeROM(VIA_ORB, 0x04); // INACTIVE
281
+ card1.writeROM(VIA_ORA, 0x38); // value
282
+ card1.writeROM(VIA_ORB, 0x06); // WRITE
283
+ card1.writeROM(VIA_ORB, 0x04); // INACTIVE
284
+
285
+ // Serialize
286
+ std::vector<uint8_t> buffer(card1.getStateSize());
287
+ size_t written = card1.serialize(buffer.data(), buffer.size());
288
+ REQUIRE(written > 0);
289
+ REQUIRE(written <= buffer.size());
290
+
291
+ // Deserialize into a new card
292
+ MockingboardCard card2;
293
+ size_t consumed = card2.deserialize(buffer.data(), written);
294
+ REQUIRE(consumed > 0);
295
+
296
+ // Verify PSG state was preserved
297
+ REQUIRE(card2.getPSG1().getRegister(7) == card1.getPSG1().getRegister(7));
298
+ }
299
+
300
+ // ---------------------------------------------------------------------------
301
+ // IRQ: set VIA timer, update until fires, check isIRQActive
302
+ // ---------------------------------------------------------------------------
303
+
304
+ TEST_CASE("MockingboardCard IRQ fires from VIA1 Timer 1", "[mockingboard]") {
305
+ MockingboardCard card;
306
+
307
+ // Track IRQ firing via callback
308
+ bool irqFired = false;
309
+ card.setIRQCallback([&irqFired]() {
310
+ irqFired = true;
311
+ });
312
+
313
+ // Enable Timer 1 interrupt in VIA1:
314
+ // Write to IER: set bit 7 (enable) and bit 6 (T1)
315
+ card.writeROM(VIA_IER, 0xC0);
316
+
317
+ // Set ACR for one-shot mode (bit 6 = 0)
318
+ card.writeROM(VIA_ACR, 0x00);
319
+
320
+ // Load a short timer value: latch low then high starts the timer
321
+ card.writeROM(VIA_T1CL, 0x05); // low byte = 5
322
+ card.writeROM(VIA_T1CH, 0x00); // high byte = 0 (starts timer)
323
+
324
+ // Update enough cycles for the timer to fire (timer counts down from 5)
325
+ for (int i = 0; i < 20; ++i) {
326
+ card.update(1);
327
+ }
328
+
329
+ // Either the IRQ callback was called or the IRQ flag is set
330
+ bool irqActive = card.isIRQActive() || irqFired;
331
+ REQUIRE(irqActive);
332
+ }
333
+
334
+ // ---------------------------------------------------------------------------
335
+ // Debug accessors
336
+ // ---------------------------------------------------------------------------
337
+
338
+ TEST_CASE("MockingboardCard debug accessors return VIA/PSG references", "[mockingboard]") {
339
+ MockingboardCard card;
340
+
341
+ const VIA6522& via1 = card.getVIA1();
342
+ const VIA6522& via2 = card.getVIA2();
343
+ const AY8910& psg1 = card.getPSG1();
344
+ const AY8910& psg2 = card.getPSG2();
345
+
346
+ // Just verify the accessors work and return references
347
+ (void)via1.getDDRA();
348
+ (void)via2.getDDRA();
349
+ (void)psg1.getRegister(0);
350
+ (void)psg2.getRegister(0);
351
+ REQUIRE(true);
352
+ }