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,555 @@
1
+ /*
2
+ * test_mmu.cpp - Unit tests for MMU (Memory Management Unit)
3
+ *
4
+ * Tests the core memory management unit including basic read/write,
5
+ * ROM access, zero page, stack, Language Card switching, auxiliary
6
+ * memory, soft switches, callbacks, paddle input, memory tracking,
7
+ * peek(), and reset().
8
+ */
9
+
10
+ #define CATCH_CONFIG_MAIN
11
+ #include "catch.hpp"
12
+
13
+ #include "mmu/mmu.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
+ // Basic read/write
30
+ // ============================================================================
31
+
32
+ TEST_CASE("MMU basic write and read back at $0400", "[mmu][basic]") {
33
+ auto mmu = createMMU();
34
+
35
+ mmu->write(0x0400, 0x42);
36
+ REQUIRE(mmu->read(0x0400) == 0x42);
37
+ }
38
+
39
+ TEST_CASE("MMU write and read multiple locations", "[mmu][basic]") {
40
+ auto mmu = createMMU();
41
+
42
+ mmu->write(0x0400, 0xAA);
43
+ mmu->write(0x0401, 0xBB);
44
+ mmu->write(0x0402, 0xCC);
45
+
46
+ CHECK(mmu->read(0x0400) == 0xAA);
47
+ CHECK(mmu->read(0x0401) == 0xBB);
48
+ CHECK(mmu->read(0x0402) == 0xCC);
49
+ }
50
+
51
+ // ============================================================================
52
+ // ROM read
53
+ // ============================================================================
54
+
55
+ TEST_CASE("MMU reads reset vector from ROM at $FFFC/$FFFD", "[mmu][rom]") {
56
+ auto mmu = createMMU();
57
+
58
+ // The reset vector is at the end of the 16KB ROM.
59
+ // ROM offset for $FFFC = $FFFC - $C000 = $3FFC
60
+ uint8_t lo = roms::ROM_SYSTEM[0x3FFC];
61
+ uint8_t hi = roms::ROM_SYSTEM[0x3FFD];
62
+
63
+ // Default is ROM read (lcram is false after reset)
64
+ CHECK(mmu->read(0xFFFC) == lo);
65
+ CHECK(mmu->read(0xFFFD) == hi);
66
+ }
67
+
68
+ TEST_CASE("MMU reads ROM correctly at $C100 range with INTCXROM", "[mmu][rom]") {
69
+ auto mmu = createMMU();
70
+
71
+ // Enable INTCXROM so $C100-$CFFF reads internal ROM
72
+ mmu->write(0xC007, 0); // INTCXROM on
73
+
74
+ // Internal ROM byte at $C100 = systemROM_[$0100]
75
+ uint8_t expected = roms::ROM_SYSTEM[0x0100];
76
+ CHECK(mmu->read(0xC100) == expected);
77
+ }
78
+
79
+ // ============================================================================
80
+ // Zero page
81
+ // ============================================================================
82
+
83
+ TEST_CASE("MMU zero page write and read back", "[mmu][zeropage]") {
84
+ auto mmu = createMMU();
85
+
86
+ for (int i = 0; i < 256; ++i) {
87
+ mmu->write(static_cast<uint16_t>(i), static_cast<uint8_t>(i ^ 0xA5));
88
+ }
89
+
90
+ for (int i = 0; i < 256; ++i) {
91
+ INFO("Zero page address $" << std::hex << i);
92
+ REQUIRE(mmu->read(static_cast<uint16_t>(i)) == static_cast<uint8_t>(i ^ 0xA5));
93
+ }
94
+ }
95
+
96
+ // ============================================================================
97
+ // Stack page
98
+ // ============================================================================
99
+
100
+ TEST_CASE("MMU stack page write and read back", "[mmu][stack]") {
101
+ auto mmu = createMMU();
102
+
103
+ for (int i = 0; i < 256; ++i) {
104
+ uint16_t addr = 0x0100 + i;
105
+ mmu->write(addr, static_cast<uint8_t>(i));
106
+ }
107
+
108
+ for (int i = 0; i < 256; ++i) {
109
+ uint16_t addr = 0x0100 + i;
110
+ INFO("Stack page address $" << std::hex << addr);
111
+ REQUIRE(mmu->read(addr) == static_cast<uint8_t>(i));
112
+ }
113
+ }
114
+
115
+ // ============================================================================
116
+ // Language Card switches
117
+ // ============================================================================
118
+
119
+ TEST_CASE("Language Card: read $C08B twice enables LC write", "[mmu][lc]") {
120
+ auto mmu = createMMU();
121
+
122
+ // $C08B = $C080 | 0x0B => reg = 0x8B, op = 3, bank2 = false (bank 1)
123
+ // op=3 => read RAM, write enable on second read
124
+ mmu->read(0xC08B); // First read: prewrite = true, write still false
125
+ CHECK_FALSE(mmu->getSoftSwitches().lcwrite);
126
+
127
+ mmu->read(0xC08B); // Second read: write enabled
128
+ CHECK(mmu->getSoftSwitches().lcwrite);
129
+ CHECK(mmu->getSoftSwitches().lcram); // Reading RAM enabled
130
+ }
131
+
132
+ TEST_CASE("Language Card: $C080 enables LC read RAM, disables write", "[mmu][lc]") {
133
+ auto mmu = createMMU();
134
+
135
+ // First enable write via double read of $C08B
136
+ mmu->read(0xC08B);
137
+ mmu->read(0xC08B);
138
+ REQUIRE(mmu->getSoftSwitches().lcwrite);
139
+
140
+ // $C080: op=0 => read RAM, write disabled
141
+ mmu->read(0xC080);
142
+ CHECK(mmu->getSoftSwitches().lcram);
143
+ CHECK_FALSE(mmu->getSoftSwitches().lcwrite);
144
+ }
145
+
146
+ TEST_CASE("Language Card: write to LC RAM after enabling write", "[mmu][lc]") {
147
+ auto mmu = createMMU();
148
+
149
+ // Enable LC RAM read + write (bank 2): $C083 twice
150
+ mmu->read(0xC083);
151
+ mmu->read(0xC083);
152
+ REQUIRE(mmu->getSoftSwitches().lcram);
153
+ REQUIRE(mmu->getSoftSwitches().lcwrite);
154
+
155
+ // Write to $E000 and read back
156
+ mmu->write(0xE000, 0xDE);
157
+ CHECK(mmu->read(0xE000) == 0xDE);
158
+ }
159
+
160
+ // ============================================================================
161
+ // LC bank switching
162
+ // ============================================================================
163
+
164
+ TEST_CASE("Language Card: $C088 selects bank 1, $C080 selects bank 2", "[mmu][lc][bank]") {
165
+ auto mmu = createMMU();
166
+
167
+ // $C088: reg=0x88, bank2 = !(0x88 & 0x08) = !(0x08) = false => bank 1
168
+ mmu->read(0xC088);
169
+ CHECK_FALSE(mmu->getSoftSwitches().lcram2); // lcram2=false means bank 1
170
+
171
+ // $C080: reg=0x80, bank2 = !(0x80 & 0x08) = !(0x00) = true => bank 2
172
+ mmu->read(0xC080);
173
+ CHECK(mmu->getSoftSwitches().lcram2); // lcram2=true means bank 2
174
+ }
175
+
176
+ TEST_CASE("Language Card: bank 1 and bank 2 are independent", "[mmu][lc][bank]") {
177
+ auto mmu = createMMU();
178
+
179
+ // Enable LC RAM read + write bank 1: $C08B twice
180
+ mmu->read(0xC08B);
181
+ mmu->read(0xC08B);
182
+ mmu->write(0xD000, 0xAA);
183
+
184
+ // Switch to bank 2: $C083 twice
185
+ mmu->read(0xC083);
186
+ mmu->read(0xC083);
187
+ mmu->write(0xD000, 0xBB);
188
+
189
+ // Read bank 2
190
+ CHECK(mmu->read(0xD000) == 0xBB);
191
+
192
+ // Switch back to bank 1 and verify
193
+ mmu->read(0xC08B);
194
+ mmu->read(0xC08B);
195
+ CHECK(mmu->read(0xD000) == 0xAA);
196
+ }
197
+
198
+ // ============================================================================
199
+ // Auxiliary memory (RAMWRT / RAMRD)
200
+ // ============================================================================
201
+
202
+ TEST_CASE("Aux memory: RAMWRT routes writes to aux, read stays main", "[mmu][aux]") {
203
+ auto mmu = createMMU();
204
+
205
+ // Write initial value to main memory
206
+ mmu->write(0x0900, 0x11);
207
+ CHECK(mmu->read(0x0900) == 0x11);
208
+
209
+ // Enable RAMWRT (writes go to aux)
210
+ mmu->write(0xC005, 0); // RAMWRT on
211
+ mmu->write(0x0900, 0x22);
212
+
213
+ // Disable RAMWRT
214
+ mmu->write(0xC004, 0); // RAMWRT off
215
+
216
+ // Main memory should still have old value
217
+ CHECK(mmu->read(0x0900) == 0x11);
218
+
219
+ // Aux memory should have new value
220
+ CHECK(mmu->readRAM(0x0900, true) == 0x22);
221
+ }
222
+
223
+ TEST_CASE("Aux memory: RAMRD routes reads to aux", "[mmu][aux]") {
224
+ auto mmu = createMMU();
225
+
226
+ // Write different values to main and aux
227
+ mmu->writeRAM(0x0900, 0xAA, false);
228
+ mmu->writeRAM(0x0900, 0xBB, true);
229
+
230
+ // Default: read from main
231
+ CHECK(mmu->read(0x0900) == 0xAA);
232
+
233
+ // Enable RAMRD
234
+ mmu->write(0xC003, 0); // RAMRD on
235
+ CHECK(mmu->read(0x0900) == 0xBB);
236
+
237
+ // Disable RAMRD
238
+ mmu->write(0xC002, 0); // RAMRD off
239
+ CHECK(mmu->read(0x0900) == 0xAA);
240
+ }
241
+
242
+ // ============================================================================
243
+ // ALTZP
244
+ // ============================================================================
245
+
246
+ TEST_CASE("ALTZP: routes zero page to aux memory", "[mmu][altzp]") {
247
+ auto mmu = createMMU();
248
+
249
+ // Write to main zero page
250
+ mmu->write(0x0010, 0xAA);
251
+
252
+ // Write different value to aux zero page
253
+ mmu->writeRAM(0x0010, 0xBB, true);
254
+
255
+ // Default: main zero page
256
+ CHECK(mmu->read(0x0010) == 0xAA);
257
+
258
+ // Enable ALTZP
259
+ mmu->write(0xC009, 0);
260
+ CHECK(mmu->read(0x0010) == 0xBB);
261
+
262
+ // Disable ALTZP
263
+ mmu->write(0xC008, 0);
264
+ CHECK(mmu->read(0x0010) == 0xAA);
265
+ }
266
+
267
+ // ============================================================================
268
+ // 80STORE + PAGE2
269
+ // ============================================================================
270
+
271
+ TEST_CASE("80STORE+PAGE2: writes to $0400-$07FF go to aux", "[mmu][80store]") {
272
+ auto mmu = createMMU();
273
+
274
+ // Write initial value to main text page
275
+ mmu->write(0x0400, 0x11);
276
+
277
+ // Enable 80STORE
278
+ mmu->write(0xC001, 0); // store80 on
279
+
280
+ // Enable PAGE2
281
+ mmu->write(0xC055, 0); // page2 on (read or write toggles it)
282
+
283
+ // Write to $0400 - should go to aux due to 80STORE+PAGE2
284
+ mmu->write(0x0400, 0x22);
285
+
286
+ // Disable PAGE2 and 80STORE to read main
287
+ mmu->write(0xC054, 0); // page2 off
288
+ mmu->write(0xC000, 0); // store80 off
289
+
290
+ // Main should still have original value
291
+ CHECK(mmu->read(0x0400) == 0x11);
292
+
293
+ // Aux should have the new value
294
+ CHECK(mmu->readRAM(0x0400, true) == 0x22);
295
+ }
296
+
297
+ // ============================================================================
298
+ // Display soft switches
299
+ // ============================================================================
300
+
301
+ TEST_CASE("Display switches: reading $C050-$C057 toggles switches", "[mmu][switches]") {
302
+ auto mmu = createMMU();
303
+
304
+ // Default: text=true
305
+ CHECK(mmu->getSoftSwitches().text);
306
+
307
+ // $C050 = TXTCLR = graphics mode
308
+ mmu->read(0xC050);
309
+ CHECK_FALSE(mmu->getSoftSwitches().text);
310
+
311
+ // $C051 = TXTSET = text mode
312
+ mmu->read(0xC051);
313
+ CHECK(mmu->getSoftSwitches().text);
314
+
315
+ // $C052 = MIXCLR
316
+ mmu->read(0xC053); // MIXSET on
317
+ CHECK(mmu->getSoftSwitches().mixed);
318
+ mmu->read(0xC052); // MIXCLR off
319
+ CHECK_FALSE(mmu->getSoftSwitches().mixed);
320
+
321
+ // $C054/$C055 = PAGE1/PAGE2
322
+ mmu->read(0xC055);
323
+ CHECK(mmu->getSoftSwitches().page2);
324
+ mmu->read(0xC054);
325
+ CHECK_FALSE(mmu->getSoftSwitches().page2);
326
+
327
+ // $C056/$C057 = LORES/HIRES
328
+ mmu->read(0xC057);
329
+ CHECK(mmu->getSoftSwitches().hires);
330
+ mmu->read(0xC056);
331
+ CHECK_FALSE(mmu->getSoftSwitches().hires);
332
+ }
333
+
334
+ // ============================================================================
335
+ // Keyboard callback
336
+ // ============================================================================
337
+
338
+ TEST_CASE("Keyboard callback: read $C000 returns callback value", "[mmu][keyboard]") {
339
+ auto mmu = createMMU();
340
+
341
+ mmu->setKeyboardCallback([]() -> uint8_t { return 0xC1; }); // 'A' with high bit
342
+
343
+ uint8_t val = mmu->read(0xC000);
344
+ CHECK(val == 0xC1);
345
+ }
346
+
347
+ // ============================================================================
348
+ // Speaker callback
349
+ // ============================================================================
350
+
351
+ TEST_CASE("Speaker callback: read $C030 triggers speaker callback", "[mmu][speaker]") {
352
+ auto mmu = createMMU();
353
+
354
+ bool called = false;
355
+ mmu->setSpeakerCallback([&called]() { called = true; });
356
+
357
+ mmu->read(0xC030);
358
+ CHECK(called);
359
+ }
360
+
361
+ TEST_CASE("Speaker callback: write $C030 also triggers speaker callback", "[mmu][speaker]") {
362
+ auto mmu = createMMU();
363
+
364
+ bool called = false;
365
+ mmu->setSpeakerCallback([&called]() { called = true; });
366
+
367
+ mmu->write(0xC030, 0x00);
368
+ CHECK(called);
369
+ }
370
+
371
+ // ============================================================================
372
+ // Paddle values
373
+ // ============================================================================
374
+
375
+ TEST_CASE("Paddle: setPaddleValue and getPaddleValue", "[mmu][paddle]") {
376
+ auto mmu = createMMU();
377
+
378
+ mmu->setPaddleValue(0, 200);
379
+ CHECK(mmu->getPaddleValue(0) == 200);
380
+
381
+ mmu->setPaddleValue(1, 50);
382
+ CHECK(mmu->getPaddleValue(1) == 50);
383
+
384
+ // Out of range paddle returns 128 (default)
385
+ CHECK(mmu->getPaddleValue(5) == 128);
386
+ }
387
+
388
+ TEST_CASE("Paddle: read $C064 returns timer state based on cycles", "[mmu][paddle]") {
389
+ auto mmu = createMMU();
390
+
391
+ uint64_t currentCycle = 0;
392
+ mmu->setCycleCallback([&currentCycle]() -> uint64_t { return currentCycle; });
393
+
394
+ mmu->setPaddleValue(0, 100);
395
+
396
+ // Trigger paddle timers
397
+ mmu->read(0xC070);
398
+
399
+ // Immediately after trigger: timer should be running (bit 7 = 1)
400
+ uint8_t val = mmu->read(0xC064);
401
+ CHECK((val & 0x80) == 0x80);
402
+
403
+ // Advance cycles past timer duration (100 * 11 = 1100 cycles)
404
+ currentCycle = 1200;
405
+ val = mmu->read(0xC064);
406
+ CHECK((val & 0x80) == 0x00);
407
+ }
408
+
409
+ // ============================================================================
410
+ // Memory tracking
411
+ // ============================================================================
412
+
413
+ TEST_CASE("Memory tracking: enableTracking records read and write counts", "[mmu][tracking]") {
414
+ auto mmu = createMMU();
415
+
416
+ mmu->enableTracking(true);
417
+ mmu->clearTracking();
418
+
419
+ // Perform some reads and writes
420
+ mmu->write(0x0800, 0x42);
421
+ mmu->read(0x0800);
422
+ mmu->read(0x0800);
423
+
424
+ const uint8_t* readCounts = mmu->getReadCounts();
425
+ const uint8_t* writeCounts = mmu->getWriteCounts();
426
+
427
+ CHECK(readCounts[0x0800] == 2);
428
+ CHECK(writeCounts[0x0800] == 1);
429
+ }
430
+
431
+ TEST_CASE("Memory tracking: clearTracking resets all counts", "[mmu][tracking]") {
432
+ auto mmu = createMMU();
433
+
434
+ mmu->enableTracking(true);
435
+ mmu->write(0x0800, 0x42);
436
+ mmu->read(0x0800);
437
+
438
+ mmu->clearTracking();
439
+
440
+ CHECK(mmu->getReadCounts()[0x0800] == 0);
441
+ CHECK(mmu->getWriteCounts()[0x0800] == 0);
442
+ }
443
+
444
+ TEST_CASE("Memory tracking: decayTracking reduces counts", "[mmu][tracking]") {
445
+ auto mmu = createMMU();
446
+
447
+ mmu->enableTracking(true);
448
+ mmu->clearTracking();
449
+
450
+ // Read 5 times
451
+ for (int i = 0; i < 5; ++i) {
452
+ mmu->read(0x0800);
453
+ }
454
+ REQUIRE(mmu->getReadCounts()[0x0800] == 5);
455
+
456
+ mmu->decayTracking(2);
457
+ CHECK(mmu->getReadCounts()[0x0800] == 3);
458
+
459
+ // Decay past zero should clamp to 0
460
+ mmu->decayTracking(10);
461
+ CHECK(mmu->getReadCounts()[0x0800] == 0);
462
+ }
463
+
464
+ // ============================================================================
465
+ // peek() - non-side-effecting read
466
+ // ============================================================================
467
+
468
+ TEST_CASE("peek does not trigger side effects", "[mmu][peek]") {
469
+ auto mmu = createMMU();
470
+
471
+ // Write a known value to test page
472
+ mmu->write(0x0400, 0x42);
473
+
474
+ // peek should return the value
475
+ CHECK(mmu->peek(0x0400) == 0x42);
476
+
477
+ // peek of speaker address should NOT trigger speaker callback
478
+ bool speakerCalled = false;
479
+ mmu->setSpeakerCallback([&speakerCalled]() { speakerCalled = true; });
480
+ mmu->peek(0xC030);
481
+ CHECK_FALSE(speakerCalled);
482
+
483
+ // peek of keyboard address should NOT trigger keyboard callback
484
+ bool keyboardCalled = false;
485
+ mmu->setKeyboardCallback([&keyboardCalled]() -> uint8_t {
486
+ keyboardCalled = true;
487
+ return 0xC1;
488
+ });
489
+ mmu->peek(0xC000);
490
+ CHECK_FALSE(keyboardCalled);
491
+ }
492
+
493
+ TEST_CASE("peek does not update memory tracking", "[mmu][peek]") {
494
+ auto mmu = createMMU();
495
+
496
+ mmu->enableTracking(true);
497
+ mmu->clearTracking();
498
+
499
+ mmu->write(0x0800, 0x42);
500
+ mmu->peek(0x0800);
501
+
502
+ // peek should NOT increment read count
503
+ CHECK(mmu->getReadCounts()[0x0800] == 0);
504
+ // write should still have been tracked
505
+ CHECK(mmu->getWriteCounts()[0x0800] == 1);
506
+ }
507
+
508
+ // ============================================================================
509
+ // reset
510
+ // ============================================================================
511
+
512
+ TEST_CASE("reset restores defaults", "[mmu][reset]") {
513
+ auto mmu = createMMU();
514
+
515
+ // Change many switches
516
+ mmu->write(0xC005, 0); // RAMWRT on
517
+ mmu->write(0xC003, 0); // RAMRD on
518
+ mmu->write(0xC009, 0); // ALTZP on
519
+ mmu->write(0xC001, 0); // 80STORE on
520
+ mmu->read(0xC057); // HIRES on
521
+ mmu->read(0xC050); // TEXT off
522
+
523
+ // Write some data
524
+ mmu->writeRAM(0x0400, 0x42, false);
525
+
526
+ mmu->reset();
527
+
528
+ const auto& sw = mmu->getSoftSwitches();
529
+ CHECK_FALSE(sw.ramwrt);
530
+ CHECK_FALSE(sw.ramrd);
531
+ CHECK_FALSE(sw.altzp);
532
+ CHECK_FALSE(sw.store80);
533
+ CHECK_FALSE(sw.hires);
534
+ CHECK(sw.text); // text defaults to true
535
+
536
+ // RAM should be cleared
537
+ CHECK(mmu->read(0x0400) == 0x00);
538
+ }
539
+
540
+ TEST_CASE("warmReset resets switches but preserves RAM", "[mmu][reset]") {
541
+ auto mmu = createMMU();
542
+
543
+ // Write data and change switches
544
+ mmu->write(0x0400, 0x42);
545
+ mmu->write(0xC005, 0); // RAMWRT on
546
+
547
+ mmu->warmReset();
548
+
549
+ const auto& sw = mmu->getSoftSwitches();
550
+ CHECK_FALSE(sw.ramwrt);
551
+ CHECK(sw.text);
552
+
553
+ // RAM should be preserved
554
+ CHECK(mmu->read(0x0400) == 0x42);
555
+ }