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,354 @@
1
+ /*
2
+ * test_thunderclock.cpp - Unit tests for ThunderclockCard (Catch2)
3
+ *
4
+ * Tests the Thunderclock Plus real-time clock card implementation including:
5
+ * - Construction and ROM loading
6
+ * - Card metadata (name, preferred slot)
7
+ * - ROM presence and expansion ROM
8
+ * - ProDOS ROM signature bytes
9
+ * - I/O read/write for time data
10
+ * - Reset behavior
11
+ * - Serialization round-trip
12
+ */
13
+
14
+ #define CATCH_CONFIG_MAIN
15
+ #include "catch.hpp"
16
+
17
+ #include "thunderclock_card.hpp"
18
+ #include "roms.cpp"
19
+
20
+ #include <cstring>
21
+ #include <ctime>
22
+ #include <vector>
23
+
24
+ using namespace a2e;
25
+
26
+ // Control register flags (matching the existing tests)
27
+ static constexpr uint8_t FLAG_CLOCK = 0x02;
28
+ static constexpr uint8_t FLAG_STROBE = 0x04;
29
+
30
+ // Time read command: bits 3-5 encode the uPD1990C command.
31
+ // CMD_TIMEREAD = 0x03 (binary 011), so shifted into bits 3-5: 0x03 << 3 = 0x18
32
+ static constexpr uint8_t CMD_TIMED = 0x18;
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Construction
36
+ // ---------------------------------------------------------------------------
37
+
38
+ TEST_CASE("ThunderclockCard constructor creates valid instance", "[thunderclock]") {
39
+ ThunderclockCard card;
40
+ REQUIRE(card.getName() != nullptr);
41
+ }
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // Card metadata
45
+ // ---------------------------------------------------------------------------
46
+
47
+ TEST_CASE("ThunderclockCard getName returns Thunderclock", "[thunderclock]") {
48
+ ThunderclockCard card;
49
+ REQUIRE(std::string(card.getName()) == "Thunderclock");
50
+ }
51
+
52
+ TEST_CASE("ThunderclockCard getPreferredSlot returns 5", "[thunderclock]") {
53
+ ThunderclockCard card;
54
+ REQUIRE(card.getPreferredSlot() == 5);
55
+ }
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // ROM presence
59
+ // ---------------------------------------------------------------------------
60
+
61
+ TEST_CASE("ThunderclockCard hasROM returns true", "[thunderclock]") {
62
+ ThunderclockCard card;
63
+ REQUIRE(card.hasROM());
64
+ }
65
+
66
+ TEST_CASE("ThunderclockCard hasExpansionROM returns true", "[thunderclock]") {
67
+ ThunderclockCard card;
68
+ REQUIRE(card.hasExpansionROM());
69
+ }
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // ProDOS ROM signature bytes
73
+ // ---------------------------------------------------------------------------
74
+
75
+ TEST_CASE("ThunderclockCard ROM signature byte at offset 0x00 is 0x08", "[thunderclock]") {
76
+ ThunderclockCard card;
77
+ REQUIRE(card.readROM(0x00) == 0x08);
78
+ }
79
+
80
+ TEST_CASE("ThunderclockCard ROM signature byte at offset 0x02 is 0x28", "[thunderclock]") {
81
+ ThunderclockCard card;
82
+ REQUIRE(card.readROM(0x02) == 0x28);
83
+ }
84
+
85
+ TEST_CASE("ThunderclockCard ROM signature byte at offset 0x04 is 0x58", "[thunderclock]") {
86
+ ThunderclockCard card;
87
+ REQUIRE(card.readROM(0x04) == 0x58);
88
+ }
89
+
90
+ TEST_CASE("ThunderclockCard ROM signature byte at offset 0x06 is 0x70", "[thunderclock]") {
91
+ ThunderclockCard card;
92
+ REQUIRE(card.readROM(0x06) == 0x70);
93
+ }
94
+
95
+ TEST_CASE("ThunderclockCard all four ProDOS signature bytes match", "[thunderclock]") {
96
+ ThunderclockCard card;
97
+ REQUIRE(card.readROM(0x00) == 0x08);
98
+ REQUIRE(card.readROM(0x02) == 0x28);
99
+ REQUIRE(card.readROM(0x04) == 0x58);
100
+ REQUIRE(card.readROM(0x06) == 0x70);
101
+ }
102
+
103
+ // ---------------------------------------------------------------------------
104
+ // I/O read initial state
105
+ // ---------------------------------------------------------------------------
106
+
107
+ TEST_CASE("ThunderclockCard initial I/O read returns 0x00", "[thunderclock]") {
108
+ ThunderclockCard card;
109
+ REQUIRE(card.readIO(0x00) == 0x00);
110
+ }
111
+
112
+ TEST_CASE("ThunderclockCard all I/O offsets return same register", "[thunderclock]") {
113
+ ThunderclockCard card;
114
+
115
+ // Issue a command so there is non-trivial state
116
+ card.writeIO(0x00, CMD_TIMED | FLAG_STROBE);
117
+ card.writeIO(0x00, CMD_TIMED | FLAG_STROBE | FLAG_CLOCK);
118
+
119
+ uint8_t expected = card.readIO(0x00);
120
+ for (int offset = 1; offset < 16; ++offset) {
121
+ REQUIRE(card.readIO(static_cast<uint8_t>(offset)) == expected);
122
+ }
123
+ }
124
+
125
+ TEST_CASE("ThunderclockCard peekIO matches readIO", "[thunderclock]") {
126
+ ThunderclockCard card;
127
+ REQUIRE(card.readIO(0x00) == card.peekIO(0x00));
128
+ }
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // I/O read/write for time data
132
+ // ---------------------------------------------------------------------------
133
+
134
+ TEST_CASE("ThunderclockCard time command loads data via clock bits", "[thunderclock]") {
135
+ ThunderclockCard card;
136
+
137
+ // Issue CMD_TIMED with strobe rising edge
138
+ card.writeIO(0x00, 0x00);
139
+ card.writeIO(0x00, CMD_TIMED | FLAG_STROBE);
140
+
141
+ // Clock out the first bit
142
+ card.writeIO(0x00, CMD_TIMED | FLAG_STROBE);
143
+ card.writeIO(0x00, CMD_TIMED | FLAG_STROBE | FLAG_CLOCK);
144
+
145
+ // Read should now have data in bit 7
146
+ uint8_t value = card.readIO(0x00);
147
+ // The value in bit 7 is the first time data bit (0 or 1)
148
+ // Just verify no crash and the register is accessible
149
+ REQUIRE((value == (value & 0xFF))); // always true, just ensuring no UB
150
+ }
151
+
152
+ TEST_CASE("ThunderclockCard clock out 40 bits produces valid time", "[thunderclock]") {
153
+ ThunderclockCard card;
154
+
155
+ // Issue time read command with strobe rising edge
156
+ card.writeIO(0x00, 0x00);
157
+ card.writeIO(0x00, CMD_TIMED | FLAG_STROBE);
158
+
159
+ // Bit 0 is immediately available after strobe (no clock needed)
160
+ std::vector<int> bits;
161
+ bits.reserve(40);
162
+
163
+ // Read bit 0 (already loaded by strobe)
164
+ uint8_t value = card.readIO(0x00);
165
+ bits.push_back((value & 0x80) ? 1 : 0);
166
+
167
+ // Clock out remaining 39 bits (each clock rising edge advances to next bit)
168
+ for (int i = 1; i < 40; ++i) {
169
+ card.writeIO(0x00, CMD_TIMED | FLAG_STROBE); // Clock low
170
+ card.writeIO(0x00, CMD_TIMED | FLAG_STROBE | FLAG_CLOCK); // Clock rising edge
171
+ value = card.readIO(0x00);
172
+ bits.push_back((value & 0x80) ? 1 : 0);
173
+ }
174
+
175
+ REQUIRE(bits.size() == 40);
176
+
177
+ // Thunderclock bit format: 10 BCD nibbles, LSB-first within each nibble
178
+ // Order: sec_ones(0-3), sec_tens(4-7), min_ones(8-11), min_tens(12-15),
179
+ // hr_ones(16-19), hr_tens(20-23), day_ones(24-27), day_tens(28-31),
180
+ // dow(32-35), month(36-39)
181
+ auto decodeLSBNibble = [&](int startBit) -> int {
182
+ return bits[startBit] | (bits[startBit+1] << 1) | (bits[startBit+2] << 2) | (bits[startBit+3] << 3);
183
+ };
184
+
185
+ int secOnes = decodeLSBNibble(0);
186
+ int secTens = decodeLSBNibble(4);
187
+ int minOnes = decodeLSBNibble(8);
188
+ int minTens = decodeLSBNibble(12);
189
+ int hourOnes = decodeLSBNibble(16);
190
+ int hourTens = decodeLSBNibble(20);
191
+ int dayOnes = decodeLSBNibble(24);
192
+ int dayTens = decodeLSBNibble(28);
193
+ int weekday = decodeLSBNibble(32);
194
+ int month = decodeLSBNibble(36);
195
+
196
+ int second = secTens * 10 + secOnes;
197
+ int minute = minTens * 10 + minOnes;
198
+ int hour = hourTens * 10 + hourOnes;
199
+ int day = dayTens * 10 + dayOnes;
200
+
201
+ // Validate ranges
202
+ REQUIRE(month >= 1);
203
+ REQUIRE(month <= 12);
204
+ REQUIRE(weekday >= 0);
205
+ REQUIRE(weekday <= 6);
206
+ REQUIRE(day >= 1);
207
+ REQUIRE(day <= 31);
208
+ REQUIRE(hour >= 0);
209
+ REQUIRE(hour <= 23);
210
+ REQUIRE(minute >= 0);
211
+ REQUIRE(minute <= 59);
212
+ REQUIRE(second >= 0);
213
+ REQUIRE(second <= 59);
214
+ }
215
+
216
+ TEST_CASE("ThunderclockCard strobe edge detection - no re-trigger on hold", "[thunderclock]") {
217
+ ThunderclockCard card;
218
+
219
+ card.writeIO(0x00, 0x00); // Low
220
+ card.writeIO(0x00, FLAG_STROBE); // Rising edge
221
+ card.writeIO(0x00, FLAG_STROBE); // Still high - should NOT re-trigger
222
+ card.writeIO(0x00, 0x00); // Falling edge
223
+ card.writeIO(0x00, FLAG_STROBE); // Rising edge again
224
+
225
+ // No crash = pass (edge detection is internal)
226
+ REQUIRE(true);
227
+ }
228
+
229
+ TEST_CASE("ThunderclockCard clock edge detection - no shift on hold", "[thunderclock]") {
230
+ ThunderclockCard card;
231
+
232
+ // Issue time command
233
+ card.writeIO(0x00, 0x00);
234
+ card.writeIO(0x00, CMD_TIMED | FLAG_STROBE);
235
+
236
+ // Clock rising edge
237
+ card.writeIO(0x00, CMD_TIMED | FLAG_STROBE | FLAG_CLOCK);
238
+ uint8_t val1 = card.readIO(0x00);
239
+
240
+ // Hold clock high - should not shift again
241
+ card.writeIO(0x00, CMD_TIMED | FLAG_STROBE | FLAG_CLOCK);
242
+ uint8_t val2 = card.readIO(0x00);
243
+
244
+ REQUIRE(val1 == val2);
245
+ }
246
+
247
+ // ---------------------------------------------------------------------------
248
+ // Reset
249
+ // ---------------------------------------------------------------------------
250
+
251
+ TEST_CASE("ThunderclockCard reset clears state", "[thunderclock]") {
252
+ ThunderclockCard card;
253
+
254
+ // Set up some state
255
+ card.writeIO(0x00, CMD_TIMED | FLAG_STROBE);
256
+ card.writeIO(0x00, CMD_TIMED | FLAG_STROBE | FLAG_CLOCK);
257
+
258
+ card.reset();
259
+
260
+ REQUIRE(card.readIO(0x00) == 0x00);
261
+ }
262
+
263
+ // ---------------------------------------------------------------------------
264
+ // Expansion ROM
265
+ // ---------------------------------------------------------------------------
266
+
267
+ TEST_CASE("ThunderclockCard expansion ROM is not all zeros", "[thunderclock]") {
268
+ ThunderclockCard card;
269
+
270
+ bool allZero = true;
271
+ for (int i = 0; i < 256; ++i) {
272
+ if (card.readExpansionROM(static_cast<uint16_t>(i)) != 0x00) {
273
+ allZero = false;
274
+ break;
275
+ }
276
+ }
277
+ REQUIRE_FALSE(allZero);
278
+ }
279
+
280
+ TEST_CASE("ThunderclockCard expansion ROM is not all 0xFF", "[thunderclock]") {
281
+ ThunderclockCard card;
282
+
283
+ bool allFF = true;
284
+ for (int i = 0; i < 256; ++i) {
285
+ if (card.readExpansionROM(static_cast<uint16_t>(i)) != 0xFF) {
286
+ allFF = false;
287
+ break;
288
+ }
289
+ }
290
+ REQUIRE_FALSE(allFF);
291
+ }
292
+
293
+ // ---------------------------------------------------------------------------
294
+ // Serialization round-trip
295
+ // ---------------------------------------------------------------------------
296
+
297
+ TEST_CASE("ThunderclockCard getStateSize is correct", "[thunderclock]") {
298
+ ThunderclockCard card;
299
+ REQUIRE(card.getStateSize() == ThunderclockCard::STATE_SIZE);
300
+ }
301
+
302
+ TEST_CASE("ThunderclockCard serialize/deserialize round-trip", "[thunderclock]") {
303
+ ThunderclockCard card1;
304
+
305
+ // Set up some state
306
+ card1.writeIO(0x00, 0x00);
307
+ card1.writeIO(0x00, CMD_TIMED | FLAG_STROBE);
308
+ card1.writeIO(0x00, CMD_TIMED | FLAG_STROBE | FLAG_CLOCK);
309
+
310
+ uint8_t regBefore = card1.readIO(0x00);
311
+
312
+ // Serialize
313
+ std::vector<uint8_t> buffer(card1.getStateSize());
314
+ size_t written = card1.serialize(buffer.data(), buffer.size());
315
+ REQUIRE(written > 0);
316
+ REQUIRE(written <= buffer.size());
317
+
318
+ // Deserialize into new card
319
+ ThunderclockCard card2;
320
+ size_t consumed = card2.deserialize(buffer.data(), written);
321
+ REQUIRE(consumed > 0);
322
+
323
+ // The register value should be preserved
324
+ uint8_t regAfter = card2.readIO(0x00);
325
+ REQUIRE(regAfter == regBefore);
326
+ }
327
+
328
+ // ---------------------------------------------------------------------------
329
+ // Multiple time reads without reset
330
+ // ---------------------------------------------------------------------------
331
+
332
+ TEST_CASE("ThunderclockCard multiple time reads work correctly", "[thunderclock]") {
333
+ ThunderclockCard card;
334
+
335
+ for (int iter = 0; iter < 3; ++iter) {
336
+ // Issue time read command
337
+ card.writeIO(0x00, 0x00);
338
+ card.writeIO(0x00, CMD_TIMED | FLAG_STROBE);
339
+
340
+ // Read 8 bits
341
+ uint8_t byte = 0;
342
+ for (int bit = 0; bit < 8; ++bit) {
343
+ card.writeIO(0x00, CMD_TIMED | FLAG_STROBE);
344
+ card.writeIO(0x00, CMD_TIMED | FLAG_STROBE | FLAG_CLOCK);
345
+ uint8_t value = card.readIO(0x00);
346
+ byte = (byte << 1) | ((value & 0x80) ? 1 : 0);
347
+ }
348
+
349
+ // The byte should be some valid time data
350
+ // (month nibble + weekday nibble for first 8 bits)
351
+ // Just verify no crash
352
+ REQUIRE((byte == (byte & 0xFF))); // always true
353
+ }
354
+ }
@@ -0,0 +1,323 @@
1
+ /*
2
+ * test_via6522.cpp - Unit tests for VIA 6522 timer chip emulation
3
+ */
4
+
5
+ #define CATCH_CONFIG_MAIN
6
+ #include "catch.hpp"
7
+
8
+ #include "via6522.hpp"
9
+ #include "ay8910.hpp"
10
+
11
+ using namespace a2e;
12
+
13
+ // VIA register addresses (matching the private constants)
14
+ static constexpr uint8_t REG_ORB = 0x00;
15
+ static constexpr uint8_t REG_ORA = 0x01;
16
+ static constexpr uint8_t REG_DDRB = 0x02;
17
+ static constexpr uint8_t REG_DDRA = 0x03;
18
+ static constexpr uint8_t REG_T1CL = 0x04;
19
+ static constexpr uint8_t REG_T1CH = 0x05;
20
+ static constexpr uint8_t REG_T1LL = 0x06;
21
+ static constexpr uint8_t REG_T1LH = 0x07;
22
+ static constexpr uint8_t REG_T2CL = 0x08;
23
+ static constexpr uint8_t REG_T2CH = 0x09;
24
+ static constexpr uint8_t REG_ACR = 0x0B;
25
+ static constexpr uint8_t REG_IFR = 0x0D;
26
+ static constexpr uint8_t REG_IER = 0x0E;
27
+
28
+ // ============================================================================
29
+ // Register read/write: ORA, ORB, DDRA, DDRB
30
+ // ============================================================================
31
+
32
+ TEST_CASE("VIA6522 ORA register write and read back", "[via][port]") {
33
+ VIA6522 via;
34
+ // Set DDRA to all output
35
+ via.write(REG_DDRA, 0xFF);
36
+ via.write(REG_ORA, 0xA5);
37
+ CHECK(via.getORA() == 0xA5);
38
+ }
39
+
40
+ TEST_CASE("VIA6522 ORB register write and read back", "[via][port]") {
41
+ VIA6522 via;
42
+ // Set DDRB to all output
43
+ via.write(REG_DDRB, 0xFF);
44
+ via.write(REG_ORB, 0x5A);
45
+ CHECK(via.getORB() == 0x5A);
46
+ }
47
+
48
+ TEST_CASE("VIA6522 DDRA register write and read back", "[via][port]") {
49
+ VIA6522 via;
50
+ via.write(REG_DDRA, 0xF0);
51
+ CHECK(via.getDDRA() == 0xF0);
52
+ }
53
+
54
+ TEST_CASE("VIA6522 DDRB register write and read back", "[via][port]") {
55
+ VIA6522 via;
56
+ via.write(REG_DDRB, 0x0F);
57
+ CHECK(via.getDDRB() == 0x0F);
58
+ }
59
+
60
+ // ============================================================================
61
+ // Timer 1: write T1CL/T1CH, read back counter
62
+ // ============================================================================
63
+
64
+ TEST_CASE("VIA6522 Timer 1 latch write via T1CL/T1CH", "[via][timer1]") {
65
+ VIA6522 via;
66
+
67
+ // Writing T1CL sets the low latch (doesn't start timer)
68
+ via.write(REG_T1CL, 0x34);
69
+
70
+ // Writing T1CH sets the high latch AND loads counter AND starts timer
71
+ via.write(REG_T1CH, 0x12);
72
+
73
+ // Read back the latch values
74
+ CHECK(via.getT1Latch() == 0x1234);
75
+ CHECK(via.isT1Running() == true);
76
+ }
77
+
78
+ // ============================================================================
79
+ // Timer 1 latch: T1LL/T1LH
80
+ // ============================================================================
81
+
82
+ TEST_CASE("VIA6522 Timer 1 latch only (T1LL/T1LH)", "[via][timer1_latch]") {
83
+ VIA6522 via;
84
+
85
+ // Writing T1LL/T1LH only sets the latch, does not start timer
86
+ via.write(REG_T1LL, 0xCD);
87
+ via.write(REG_T1LH, 0xAB);
88
+
89
+ CHECK(via.getT1Latch() == 0xABCD);
90
+ }
91
+
92
+ // ============================================================================
93
+ // Timer 2: T2CL/T2CH
94
+ // ============================================================================
95
+
96
+ TEST_CASE("VIA6522 Timer 2 write and start", "[via][timer2]") {
97
+ VIA6522 via;
98
+
99
+ // Writing T2CL sets the low latch
100
+ via.write(REG_T2CL, 0x10);
101
+ // Writing T2CH loads counter and starts timer
102
+ via.write(REG_T2CH, 0x00);
103
+
104
+ // Timer 2 should be loaded with 0x0010
105
+ // We can verify it's running by updating and checking the counter
106
+ // decrements
107
+ }
108
+
109
+ // ============================================================================
110
+ // IFR (0x0D): read interrupt flags
111
+ // ============================================================================
112
+
113
+ TEST_CASE("VIA6522 IFR initially zero", "[via][ifr]") {
114
+ VIA6522 via;
115
+ CHECK(via.getIFR() == 0x00);
116
+ }
117
+
118
+ // ============================================================================
119
+ // IER (0x0E): write enable/disable interrupt bits
120
+ // ============================================================================
121
+
122
+ TEST_CASE("VIA6522 IER enable and disable bits", "[via][ier]") {
123
+ VIA6522 via;
124
+
125
+ // Enable Timer 1 interrupt (bit 6): write with bit 7 set = enable
126
+ via.write(REG_IER, 0x80 | 0x40); // Set bit 7 (enable mode) + bit 6 (T1)
127
+ CHECK((via.getIER() & 0x40) != 0);
128
+
129
+ // Disable Timer 1 interrupt: write without bit 7 = disable
130
+ via.write(REG_IER, 0x40); // Clear mode (bit 7=0) + bit 6 (T1)
131
+ CHECK((via.getIER() & 0x40) == 0);
132
+ }
133
+
134
+ TEST_CASE("VIA6522 IER multiple interrupt sources", "[via][ier]") {
135
+ VIA6522 via;
136
+
137
+ // Enable T1 (bit 6) and T2 (bit 5)
138
+ via.write(REG_IER, 0x80 | 0x60);
139
+ CHECK((via.getIER() & 0x60) == 0x60);
140
+
141
+ // Disable only T2 (bit 5)
142
+ via.write(REG_IER, 0x20); // Clear mode, bit 5
143
+ CHECK((via.getIER() & 0x40) != 0); // T1 still enabled
144
+ CHECK((via.getIER() & 0x20) == 0); // T2 disabled
145
+ }
146
+
147
+ // ============================================================================
148
+ // Timer fires IRQ
149
+ // ============================================================================
150
+
151
+ TEST_CASE("VIA6522 Timer 1 fires IRQ after countdown", "[via][timer1_irq]") {
152
+ VIA6522 via;
153
+
154
+ // Enable Timer 1 interrupt
155
+ via.write(REG_IER, 0x80 | 0x40);
156
+
157
+ // Set a short timer value
158
+ via.write(REG_T1CL, 0x05);
159
+ via.write(REG_T1CH, 0x00); // Timer = 5 cycles, starts running
160
+
161
+ CHECK(via.isIRQActive() == false);
162
+
163
+ // Run enough cycles for the timer to fire
164
+ via.update(10);
165
+
166
+ // Timer 1 should have fired, setting IFR bit 6
167
+ CHECK((via.getIFR() & 0x40) != 0);
168
+ CHECK(via.isIRQActive() == true);
169
+ }
170
+
171
+ TEST_CASE("VIA6522 Timer 1 IRQ not active when not enabled", "[via][timer1_irq]") {
172
+ VIA6522 via;
173
+
174
+ // Don't enable any interrupts in IER
175
+
176
+ via.write(REG_T1CL, 0x05);
177
+ via.write(REG_T1CH, 0x00);
178
+
179
+ via.update(10);
180
+
181
+ // IFR bit should be set, but IRQ should not be active (IER doesn't enable it)
182
+ CHECK((via.getIFR() & 0x40) != 0); // T1 flag set
183
+ CHECK(via.isIRQActive() == false); // But not enabled
184
+ }
185
+
186
+ // ============================================================================
187
+ // IRQ callback invoked on timer fire
188
+ // ============================================================================
189
+
190
+ TEST_CASE("VIA6522 IRQ callback invoked when timer fires", "[via][irq_callback]") {
191
+ VIA6522 via;
192
+ bool callbackFired = false;
193
+
194
+ via.setIRQCallback([&callbackFired]() {
195
+ callbackFired = true;
196
+ });
197
+
198
+ // Enable Timer 1 interrupt
199
+ via.write(REG_IER, 0x80 | 0x40);
200
+
201
+ // Start timer with short value
202
+ via.write(REG_T1CL, 0x02);
203
+ via.write(REG_T1CH, 0x00);
204
+
205
+ CHECK(callbackFired == false);
206
+
207
+ // Run enough cycles
208
+ via.update(10);
209
+
210
+ CHECK(callbackFired == true);
211
+ }
212
+
213
+ // ============================================================================
214
+ // ACR controls timer mode
215
+ // ============================================================================
216
+
217
+ TEST_CASE("VIA6522 ACR register write and read", "[via][acr]") {
218
+ VIA6522 via;
219
+
220
+ via.write(REG_ACR, 0x40); // T1 free-running mode (bit 6)
221
+ CHECK(via.getACR() == 0x40);
222
+
223
+ via.write(REG_ACR, 0x00); // T1 one-shot mode
224
+ CHECK(via.getACR() == 0x00);
225
+ }
226
+
227
+ TEST_CASE("VIA6522 Timer 1 free-running mode re-fires", "[via][acr_freerun]") {
228
+ VIA6522 via;
229
+ int callbackCount = 0;
230
+
231
+ via.setIRQCallback([&callbackCount]() {
232
+ callbackCount++;
233
+ });
234
+
235
+ // Enable T1 interrupt
236
+ via.write(REG_IER, 0x80 | 0x40);
237
+
238
+ // Set ACR for T1 free-running mode (bit 6 set)
239
+ via.write(REG_ACR, 0x40);
240
+
241
+ // Start timer with a short period
242
+ via.write(REG_T1CL, 0x03);
243
+ via.write(REG_T1CH, 0x00);
244
+
245
+ // Run many cycles - should fire multiple times in free-running mode
246
+ // Clear IFR between iterations to allow re-fire
247
+ for (int i = 0; i < 50; i++) {
248
+ via.update(1);
249
+ }
250
+
251
+ // In free-running mode, the timer should have fired at least once
252
+ CHECK(callbackCount >= 1);
253
+ }
254
+
255
+ // ============================================================================
256
+ // reset
257
+ // ============================================================================
258
+
259
+ TEST_CASE("VIA6522 reset clears all state", "[via][reset]") {
260
+ VIA6522 via;
261
+
262
+ // Set up some state
263
+ via.write(REG_DDRA, 0xFF);
264
+ via.write(REG_ORA, 0xAA);
265
+ via.write(REG_IER, 0x80 | 0x40);
266
+ via.write(REG_T1CL, 0x10);
267
+ via.write(REG_T1CH, 0x20);
268
+
269
+ via.reset();
270
+
271
+ CHECK(via.getORA() == 0x00);
272
+ CHECK(via.getORB() == 0x00);
273
+ CHECK(via.getDDRA() == 0x00);
274
+ CHECK(via.getDDRB() == 0x00);
275
+ CHECK(via.getIFR() == 0x00);
276
+ CHECK(via.getACR() == 0x00);
277
+ CHECK(via.isIRQActive() == false);
278
+ }
279
+
280
+ // ============================================================================
281
+ // State serialization
282
+ // ============================================================================
283
+
284
+ TEST_CASE("VIA6522 state serialization round-trip", "[via][state]") {
285
+ VIA6522 via1;
286
+ via1.write(REG_DDRA, 0xFF);
287
+ via1.write(REG_ORA, 0xBB);
288
+ via1.write(REG_DDRB, 0x0F);
289
+
290
+ uint8_t stateBuffer[VIA6522::STATE_SIZE];
291
+ size_t written = via1.exportState(stateBuffer);
292
+ REQUIRE(written == VIA6522::STATE_SIZE);
293
+
294
+ VIA6522 via2;
295
+ via2.importState(stateBuffer);
296
+
297
+ CHECK(via2.getORA() == 0xBB);
298
+ CHECK(via2.getDDRA() == 0xFF);
299
+ CHECK(via2.getDDRB() == 0x0F);
300
+ }
301
+
302
+ // ============================================================================
303
+ // Reading T1CL clears T1 interrupt flag
304
+ // ============================================================================
305
+
306
+ TEST_CASE("VIA6522 reading T1CL clears T1 interrupt flag", "[via][timer1_clear]") {
307
+ VIA6522 via;
308
+
309
+ // Enable T1 interrupt
310
+ via.write(REG_IER, 0x80 | 0x40);
311
+
312
+ // Start short timer
313
+ via.write(REG_T1CL, 0x02);
314
+ via.write(REG_T1CH, 0x00);
315
+
316
+ // Let timer fire
317
+ via.update(10);
318
+ CHECK((via.getIFR() & 0x40) != 0);
319
+
320
+ // Reading T1CL should clear the T1 interrupt flag
321
+ via.read(REG_T1CL);
322
+ CHECK((via.getIFR() & 0x40) == 0);
323
+ }