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,409 @@
1
+ /*
2
+ * test_cpu_cycle_counts.cpp - 65C02 instruction cycle count tests
3
+ *
4
+ * Verifies correct cycle counts for various instructions and
5
+ * addressing modes, including page boundary crossing penalties.
6
+ */
7
+
8
+ #define CATCH_CONFIG_MAIN
9
+ #include "catch.hpp"
10
+ #include "test_helpers.hpp"
11
+
12
+ // Helper: returns cycles consumed by the next instruction
13
+ static uint64_t measureCycles(a2e::CPU6502& cpu) {
14
+ uint64_t before = cpu.getTotalCycles();
15
+ cpu.executeInstruction();
16
+ uint64_t after = cpu.getTotalCycles();
17
+ return after - before;
18
+ }
19
+
20
+ // ============================================================================
21
+ // Basic Instruction Cycle Counts
22
+ // ============================================================================
23
+
24
+ TEST_CASE("Basic instruction cycle counts", "[cpu][cycles]") {
25
+
26
+ SECTION("NOP takes 2 cycles") {
27
+ test::CPUTestFixture f;
28
+ f.loadAndReset(0x0400, {0xEA}); // NOP
29
+ REQUIRE(measureCycles(*f.cpu) == 2);
30
+ }
31
+
32
+ SECTION("LDA immediate takes 2 cycles") {
33
+ test::CPUTestFixture f;
34
+ f.loadAndReset(0x0400, {0xA9, 0x42}); // LDA #$42
35
+ REQUIRE(measureCycles(*f.cpu) == 2);
36
+ }
37
+
38
+ SECTION("LDX immediate takes 2 cycles") {
39
+ test::CPUTestFixture f;
40
+ f.loadAndReset(0x0400, {0xA2, 0x10}); // LDX #$10
41
+ REQUIRE(measureCycles(*f.cpu) == 2);
42
+ }
43
+
44
+ SECTION("LDY immediate takes 2 cycles") {
45
+ test::CPUTestFixture f;
46
+ f.loadAndReset(0x0400, {0xA0, 0x10}); // LDY #$10
47
+ REQUIRE(measureCycles(*f.cpu) == 2);
48
+ }
49
+ }
50
+
51
+ // ============================================================================
52
+ // Zero Page Cycle Counts
53
+ // ============================================================================
54
+
55
+ TEST_CASE("Zero page instruction cycle counts", "[cpu][cycles]") {
56
+
57
+ SECTION("LDA zero page takes 3 cycles") {
58
+ test::CPUTestFixture f;
59
+ f.loadAndReset(0x0400, {0xA5, 0x10}); // LDA $10
60
+ REQUIRE(measureCycles(*f.cpu) == 3);
61
+ }
62
+
63
+ SECTION("STA zero page takes 3 cycles") {
64
+ test::CPUTestFixture f;
65
+ f.loadAndReset(0x0400, {0x85, 0x10}); // STA $10
66
+ REQUIRE(measureCycles(*f.cpu) == 3);
67
+ }
68
+
69
+ SECTION("LDA zero page,X takes 4 cycles") {
70
+ test::CPUTestFixture f;
71
+ f.loadAndReset(0x0400, {0xB5, 0x10}); // LDA $10,X
72
+ REQUIRE(measureCycles(*f.cpu) == 4);
73
+ }
74
+ }
75
+
76
+ // ============================================================================
77
+ // Absolute Addressing Cycle Counts
78
+ // ============================================================================
79
+
80
+ TEST_CASE("Absolute addressing cycle counts", "[cpu][cycles]") {
81
+
82
+ SECTION("LDA absolute takes 4 cycles") {
83
+ test::CPUTestFixture f;
84
+ f.loadAndReset(0x0400, {0xAD, 0x00, 0x20}); // LDA $2000
85
+ REQUIRE(measureCycles(*f.cpu) == 4);
86
+ }
87
+
88
+ SECTION("STA absolute takes 4 cycles") {
89
+ test::CPUTestFixture f;
90
+ f.loadAndReset(0x0400, {0x8D, 0x00, 0x20}); // STA $2000
91
+ REQUIRE(measureCycles(*f.cpu) == 4);
92
+ }
93
+ }
94
+
95
+ // ============================================================================
96
+ // Absolute,X with Page Crossing
97
+ // ============================================================================
98
+
99
+ TEST_CASE("Absolute,X cycle counts with page crossing", "[cpu][cycles]") {
100
+
101
+ SECTION("LDA abs,X no page cross takes 4 cycles") {
102
+ test::CPUTestFixture f;
103
+ // X=$01, base=$2000 -> $2001, same page
104
+ f.loadAndReset(0x0400, {0xA2, 0x01, 0xBD, 0x00, 0x20}); // LDX #$01; LDA $2000,X
105
+ measureCycles(*f.cpu); // LDX
106
+ REQUIRE(measureCycles(*f.cpu) == 4);
107
+ }
108
+
109
+ SECTION("LDA abs,X with page cross takes 5 cycles") {
110
+ test::CPUTestFixture f;
111
+ // X=$01, base=$20FF -> $2100, page crossed
112
+ f.loadAndReset(0x0400, {0xA2, 0x01, 0xBD, 0xFF, 0x20}); // LDX #$01; LDA $20FF,X
113
+ measureCycles(*f.cpu); // LDX
114
+ REQUIRE(measureCycles(*f.cpu) == 5);
115
+ }
116
+
117
+ SECTION("STA abs,X always takes 5 cycles (no page cross optimization)") {
118
+ test::CPUTestFixture f;
119
+ // Stores always take the same cycles regardless of page cross
120
+ f.loadAndReset(0x0400, {0xA2, 0x01, 0x9D, 0x00, 0x20}); // LDX #$01; STA $2000,X
121
+ measureCycles(*f.cpu); // LDX
122
+ REQUIRE(measureCycles(*f.cpu) == 5);
123
+ }
124
+ }
125
+
126
+ // ============================================================================
127
+ // Absolute,Y with Page Crossing
128
+ // ============================================================================
129
+
130
+ TEST_CASE("Absolute,Y cycle counts with page crossing", "[cpu][cycles]") {
131
+
132
+ SECTION("LDA abs,Y no page cross takes 4 cycles") {
133
+ test::CPUTestFixture f;
134
+ f.loadAndReset(0x0400, {0xA0, 0x01, 0xB9, 0x00, 0x20}); // LDY #$01; LDA $2000,Y
135
+ measureCycles(*f.cpu); // LDY
136
+ REQUIRE(measureCycles(*f.cpu) == 4);
137
+ }
138
+
139
+ SECTION("LDA abs,Y with page cross takes 5 cycles") {
140
+ test::CPUTestFixture f;
141
+ f.loadAndReset(0x0400, {0xA0, 0x01, 0xB9, 0xFF, 0x20}); // LDY #$01; LDA $20FF,Y
142
+ measureCycles(*f.cpu); // LDY
143
+ REQUIRE(measureCycles(*f.cpu) == 5);
144
+ }
145
+ }
146
+
147
+ // ============================================================================
148
+ // (Indirect),Y with Page Crossing
149
+ // ============================================================================
150
+
151
+ TEST_CASE("(Indirect),Y cycle counts with page crossing", "[cpu][cycles]") {
152
+
153
+ SECTION("LDA (zp),Y no page cross takes 5 cycles") {
154
+ test::CPUTestFixture f;
155
+ f.mem[0x10] = 0x00;
156
+ f.mem[0x11] = 0x20;
157
+ // Y=$01 -> $2000+$01 = $2001, same page
158
+ f.loadAndReset(0x0400, {0xA0, 0x01, 0xB1, 0x10}); // LDY #$01; LDA ($10),Y
159
+ measureCycles(*f.cpu); // LDY
160
+ REQUIRE(measureCycles(*f.cpu) == 5);
161
+ }
162
+
163
+ SECTION("LDA (zp),Y with page cross takes 6 cycles") {
164
+ test::CPUTestFixture f;
165
+ f.mem[0x10] = 0xFF;
166
+ f.mem[0x11] = 0x20;
167
+ // Y=$01 -> $20FF+$01 = $2100, page crossed
168
+ f.loadAndReset(0x0400, {0xA0, 0x01, 0xB1, 0x10}); // LDY #$01; LDA ($10),Y
169
+ measureCycles(*f.cpu); // LDY
170
+ REQUIRE(measureCycles(*f.cpu) == 6);
171
+ }
172
+ }
173
+
174
+ // ============================================================================
175
+ // Branch Instruction Cycle Counts
176
+ // ============================================================================
177
+
178
+ TEST_CASE("Branch instruction cycle counts", "[cpu][cycles]") {
179
+
180
+ SECTION("Branch not taken takes 2 cycles") {
181
+ test::CPUTestFixture f;
182
+ // LDA #$01 sets Z=0, then BEQ (not taken)
183
+ f.loadAndReset(0x0400, {0xA9, 0x01, 0xF0, 0x02}); // LDA #$01; BEQ +2
184
+ measureCycles(*f.cpu); // LDA
185
+ REQUIRE(measureCycles(*f.cpu) == 2);
186
+ }
187
+
188
+ SECTION("Branch taken same page takes 3 cycles") {
189
+ test::CPUTestFixture f;
190
+ // LDA #$00 sets Z=1, then BEQ +2 (taken, same page)
191
+ f.loadAndReset(0x0400, {0xA9, 0x00, 0xF0, 0x02}); // LDA #$00; BEQ +2
192
+ measureCycles(*f.cpu); // LDA
193
+ REQUIRE(measureCycles(*f.cpu) == 3);
194
+ }
195
+
196
+ SECTION("Branch taken crossing page takes 4 cycles") {
197
+ test::CPUTestFixture f;
198
+ // Place code near page boundary: $04FB: LDA #$00; BEQ +$05
199
+ // BEQ at $04FD, PC after fetch = $04FF, target = $0504 (different page)
200
+ // oldPC ($04FF) is in page $0400, target ($0504) is in page $0500
201
+ f.mem.loadProgram(0x04FB, {0xA9, 0x00, 0xF0, 0x05});
202
+ f.mem.setResetVector(0x04FB);
203
+ f.cpu->reset();
204
+ measureCycles(*f.cpu); // LDA
205
+ REQUIRE(measureCycles(*f.cpu) == 4);
206
+ }
207
+ }
208
+
209
+ // ============================================================================
210
+ // JSR / RTS Cycle Counts
211
+ // ============================================================================
212
+
213
+ TEST_CASE("JSR and RTS cycle counts", "[cpu][cycles]") {
214
+
215
+ SECTION("JSR takes 6 cycles") {
216
+ test::CPUTestFixture f;
217
+ f.mem.loadProgram(0x0500, {0xEA}); // NOP at subroutine
218
+ f.loadAndReset(0x0400, {0x20, 0x00, 0x05}); // JSR $0500
219
+ REQUIRE(measureCycles(*f.cpu) == 6);
220
+ }
221
+
222
+ SECTION("RTS takes 6 cycles") {
223
+ test::CPUTestFixture f;
224
+ f.mem.loadProgram(0x0500, {0x60}); // RTS at subroutine
225
+ f.loadAndReset(0x0400, {0x20, 0x00, 0x05}); // JSR $0500
226
+ measureCycles(*f.cpu); // JSR
227
+ REQUIRE(measureCycles(*f.cpu) == 6); // RTS
228
+ }
229
+ }
230
+
231
+ // ============================================================================
232
+ // BRK Cycle Count
233
+ // ============================================================================
234
+
235
+ TEST_CASE("BRK cycle count", "[cpu][cycles]") {
236
+
237
+ SECTION("BRK takes 7 cycles") {
238
+ test::CPUTestFixture f;
239
+ f.mem.setIRQVector(0x1000);
240
+ f.mem.loadProgram(0x1000, {0xEA});
241
+ f.loadAndReset(0x0400, {0x00, 0xEA}); // BRK; padding
242
+ REQUIRE(measureCycles(*f.cpu) == 7);
243
+ }
244
+ }
245
+
246
+ // ============================================================================
247
+ // Read-Modify-Write Cycle Counts
248
+ // ============================================================================
249
+
250
+ TEST_CASE("Read-modify-write cycle counts", "[cpu][cycles]") {
251
+
252
+ SECTION("INC absolute takes 6 cycles") {
253
+ test::CPUTestFixture f;
254
+ f.loadAndReset(0x0400, {0xEE, 0x00, 0x20}); // INC $2000
255
+ REQUIRE(measureCycles(*f.cpu) == 6);
256
+ }
257
+
258
+ SECTION("DEC absolute takes 6 cycles") {
259
+ test::CPUTestFixture f;
260
+ f.loadAndReset(0x0400, {0xCE, 0x00, 0x20}); // DEC $2000
261
+ REQUIRE(measureCycles(*f.cpu) == 6);
262
+ }
263
+
264
+ SECTION("ASL absolute takes 6 cycles") {
265
+ test::CPUTestFixture f;
266
+ f.loadAndReset(0x0400, {0x0E, 0x00, 0x20}); // ASL $2000
267
+ REQUIRE(measureCycles(*f.cpu) == 6);
268
+ }
269
+
270
+ SECTION("LSR absolute takes 6 cycles") {
271
+ test::CPUTestFixture f;
272
+ f.loadAndReset(0x0400, {0x4E, 0x00, 0x20}); // LSR $2000
273
+ REQUIRE(measureCycles(*f.cpu) == 6);
274
+ }
275
+
276
+ SECTION("ROL absolute takes 6 cycles") {
277
+ test::CPUTestFixture f;
278
+ f.loadAndReset(0x0400, {0x2E, 0x00, 0x20}); // ROL $2000
279
+ REQUIRE(measureCycles(*f.cpu) == 6);
280
+ }
281
+
282
+ SECTION("ROR absolute takes 6 cycles") {
283
+ test::CPUTestFixture f;
284
+ f.loadAndReset(0x0400, {0x6E, 0x00, 0x20}); // ROR $2000
285
+ REQUIRE(measureCycles(*f.cpu) == 6);
286
+ }
287
+
288
+ SECTION("INC zero page takes 5 cycles") {
289
+ test::CPUTestFixture f;
290
+ f.loadAndReset(0x0400, {0xE6, 0x10}); // INC $10
291
+ REQUIRE(measureCycles(*f.cpu) == 5);
292
+ }
293
+
294
+ SECTION("ASL accumulator takes 2 cycles") {
295
+ test::CPUTestFixture f;
296
+ f.loadAndReset(0x0400, {0x0A}); // ASL A
297
+ REQUIRE(measureCycles(*f.cpu) == 2);
298
+ }
299
+ }
300
+
301
+ // ============================================================================
302
+ // Stack Operation Cycle Counts
303
+ // ============================================================================
304
+
305
+ TEST_CASE("Stack operation cycle counts", "[cpu][cycles]") {
306
+
307
+ SECTION("PHA takes 3 cycles") {
308
+ test::CPUTestFixture f;
309
+ f.loadAndReset(0x0400, {0x48}); // PHA
310
+ REQUIRE(measureCycles(*f.cpu) == 3);
311
+ }
312
+
313
+ SECTION("PLA takes 4 cycles") {
314
+ test::CPUTestFixture f;
315
+ f.loadAndReset(0x0400, {0x48, 0x68}); // PHA; PLA
316
+ measureCycles(*f.cpu); // PHA
317
+ REQUIRE(measureCycles(*f.cpu) == 4);
318
+ }
319
+
320
+ SECTION("PHP takes 3 cycles") {
321
+ test::CPUTestFixture f;
322
+ f.loadAndReset(0x0400, {0x08}); // PHP
323
+ REQUIRE(measureCycles(*f.cpu) == 3);
324
+ }
325
+
326
+ SECTION("PLP takes 4 cycles") {
327
+ test::CPUTestFixture f;
328
+ f.loadAndReset(0x0400, {0x08, 0x28}); // PHP; PLP
329
+ measureCycles(*f.cpu); // PHP
330
+ REQUIRE(measureCycles(*f.cpu) == 4);
331
+ }
332
+ }
333
+
334
+ // ============================================================================
335
+ // Implied Instruction Cycle Counts
336
+ // ============================================================================
337
+
338
+ TEST_CASE("Implied instruction cycle counts", "[cpu][cycles]") {
339
+
340
+ SECTION("INX takes 2 cycles") {
341
+ test::CPUTestFixture f;
342
+ f.loadAndReset(0x0400, {0xE8}); // INX
343
+ REQUIRE(measureCycles(*f.cpu) == 2);
344
+ }
345
+
346
+ SECTION("DEX takes 2 cycles") {
347
+ test::CPUTestFixture f;
348
+ f.loadAndReset(0x0400, {0xCA}); // DEX
349
+ REQUIRE(measureCycles(*f.cpu) == 2);
350
+ }
351
+
352
+ SECTION("TAX takes 2 cycles") {
353
+ test::CPUTestFixture f;
354
+ f.loadAndReset(0x0400, {0xAA}); // TAX
355
+ REQUIRE(measureCycles(*f.cpu) == 2);
356
+ }
357
+
358
+ SECTION("SEC takes 2 cycles") {
359
+ test::CPUTestFixture f;
360
+ f.loadAndReset(0x0400, {0x38}); // SEC
361
+ REQUIRE(measureCycles(*f.cpu) == 2);
362
+ }
363
+
364
+ SECTION("CLC takes 2 cycles") {
365
+ test::CPUTestFixture f;
366
+ f.loadAndReset(0x0400, {0x18}); // CLC
367
+ REQUIRE(measureCycles(*f.cpu) == 2);
368
+ }
369
+ }
370
+
371
+ // ============================================================================
372
+ // JMP Cycle Counts
373
+ // ============================================================================
374
+
375
+ TEST_CASE("JMP cycle counts", "[cpu][cycles]") {
376
+
377
+ SECTION("JMP absolute takes 3 cycles") {
378
+ test::CPUTestFixture f;
379
+ f.loadAndReset(0x0400, {0x4C, 0x00, 0x20}); // JMP $2000
380
+ REQUIRE(measureCycles(*f.cpu) == 3);
381
+ }
382
+
383
+ SECTION("JMP indirect takes 5 cycles") {
384
+ test::CPUTestFixture f;
385
+ // Indirect vector at $3000 -> $2000
386
+ f.mem[0x3000] = 0x00;
387
+ f.mem[0x3001] = 0x20;
388
+ f.loadAndReset(0x0400, {0x6C, 0x00, 0x30}); // JMP ($3000)
389
+ // 65C02 JMP (abs) = 5 cycles (not 6 which is NMOS indirect)
390
+ // Actually the cycle table shows opcode $6C = 5 for 65C02
391
+ REQUIRE(measureCycles(*f.cpu) == 5);
392
+ }
393
+ }
394
+
395
+ // ============================================================================
396
+ // (Indirect,X) Cycle Count
397
+ // ============================================================================
398
+
399
+ TEST_CASE("(Indirect,X) cycle count", "[cpu][cycles]") {
400
+
401
+ SECTION("LDA (zp,X) takes 6 cycles") {
402
+ test::CPUTestFixture f;
403
+ f.mem[0x12] = 0x00;
404
+ f.mem[0x13] = 0x20;
405
+ f.loadAndReset(0x0400, {0xA2, 0x02, 0xA1, 0x10}); // LDX #$02; LDA ($10,X)
406
+ measureCycles(*f.cpu); // LDX
407
+ REQUIRE(measureCycles(*f.cpu) == 6);
408
+ }
409
+ }
@@ -0,0 +1,166 @@
1
+ /*
2
+ * test_cpu_decimal.cpp - 65C02 BCD (decimal mode) arithmetic tests
3
+ *
4
+ * Tests ADC and SBC in decimal mode with various BCD inputs,
5
+ * including carry/borrow, edge cases, and flag behavior.
6
+ */
7
+
8
+ #define CATCH_CONFIG_MAIN
9
+ #include "catch.hpp"
10
+ #include "test_helpers.hpp"
11
+
12
+ // ============================================================================
13
+ // ADC in Decimal Mode
14
+ // ============================================================================
15
+
16
+ TEST_CASE("ADC in decimal mode", "[cpu][decimal]") {
17
+
18
+ SECTION("Simple BCD addition: 15 + 27 = 42") {
19
+ test::CPUTestFixture f;
20
+ // SED; CLC; LDA #$15; ADC #$27
21
+ f.loadAndReset(0x0400, {0xF8, 0x18, 0xA9, 0x15, 0x69, 0x27});
22
+ test::runInstructions(*f.cpu, 4);
23
+ REQUIRE(f.cpu->getA() == 0x42);
24
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_C) == false);
25
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_D) == true);
26
+ }
27
+
28
+ SECTION("BCD addition: 25 + 35 = 60") {
29
+ test::CPUTestFixture f;
30
+ f.loadAndReset(0x0400, {0xF8, 0x18, 0xA9, 0x25, 0x69, 0x35});
31
+ test::runInstructions(*f.cpu, 4);
32
+ REQUIRE(f.cpu->getA() == 0x60);
33
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_C) == false);
34
+ }
35
+
36
+ SECTION("BCD addition: 50 + 50 = 00 with carry (100 in BCD)") {
37
+ test::CPUTestFixture f;
38
+ f.loadAndReset(0x0400, {0xF8, 0x18, 0xA9, 0x50, 0x69, 0x50});
39
+ test::runInstructions(*f.cpu, 4);
40
+ REQUIRE(f.cpu->getA() == 0x00);
41
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_C) == true);
42
+ }
43
+
44
+ SECTION("BCD addition with carry in: 15 + 27 + 1 = 43") {
45
+ test::CPUTestFixture f;
46
+ // SED; SEC; LDA #$15; ADC #$27
47
+ f.loadAndReset(0x0400, {0xF8, 0x38, 0xA9, 0x15, 0x69, 0x27});
48
+ test::runInstructions(*f.cpu, 4);
49
+ REQUIRE(f.cpu->getA() == 0x43);
50
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_C) == false);
51
+ }
52
+
53
+ SECTION("BCD addition: 99 + 01 = 00 with carry") {
54
+ test::CPUTestFixture f;
55
+ f.loadAndReset(0x0400, {0xF8, 0x18, 0xA9, 0x99, 0x69, 0x01});
56
+ test::runInstructions(*f.cpu, 4);
57
+ REQUIRE(f.cpu->getA() == 0x00);
58
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_C) == true);
59
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_Z) == true);
60
+ }
61
+
62
+ SECTION("BCD addition: 99 + 99 = 98 with carry") {
63
+ test::CPUTestFixture f;
64
+ f.loadAndReset(0x0400, {0xF8, 0x18, 0xA9, 0x99, 0x69, 0x99});
65
+ test::runInstructions(*f.cpu, 4);
66
+ // 99 + 99 = 198 in decimal -> 0x98 with carry
67
+ REQUIRE(f.cpu->getA() == 0x98);
68
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_C) == true);
69
+ }
70
+
71
+ SECTION("BCD addition: 00 + 00 = 00") {
72
+ test::CPUTestFixture f;
73
+ f.loadAndReset(0x0400, {0xF8, 0x18, 0xA9, 0x00, 0x69, 0x00});
74
+ test::runInstructions(*f.cpu, 4);
75
+ REQUIRE(f.cpu->getA() == 0x00);
76
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_Z) == true);
77
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_C) == false);
78
+ }
79
+ }
80
+
81
+ // ============================================================================
82
+ // SBC in Decimal Mode
83
+ // ============================================================================
84
+
85
+ TEST_CASE("SBC in decimal mode", "[cpu][decimal]") {
86
+
87
+ SECTION("Simple BCD subtraction: 42 - 15 = 27") {
88
+ test::CPUTestFixture f;
89
+ // SED; SEC; LDA #$42; SBC #$15
90
+ f.loadAndReset(0x0400, {0xF8, 0x38, 0xA9, 0x42, 0xE9, 0x15});
91
+ test::runInstructions(*f.cpu, 4);
92
+ REQUIRE(f.cpu->getA() == 0x27);
93
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_C) == true); // no borrow
94
+ }
95
+
96
+ SECTION("BCD subtraction: 50 - 25 = 25") {
97
+ test::CPUTestFixture f;
98
+ f.loadAndReset(0x0400, {0xF8, 0x38, 0xA9, 0x50, 0xE9, 0x25});
99
+ test::runInstructions(*f.cpu, 4);
100
+ REQUIRE(f.cpu->getA() == 0x25);
101
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_C) == true);
102
+ }
103
+
104
+ SECTION("BCD subtraction with borrow: 10 - 20 = 90 (with borrow)") {
105
+ test::CPUTestFixture f;
106
+ // SED; SEC; LDA #$10; SBC #$20
107
+ f.loadAndReset(0x0400, {0xF8, 0x38, 0xA9, 0x10, 0xE9, 0x20});
108
+ test::runInstructions(*f.cpu, 4);
109
+ // 10 - 20 = -10 in decimal -> borrow -> result = 90
110
+ REQUIRE(f.cpu->getA() == 0x90);
111
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_C) == false); // borrow occurred
112
+ }
113
+
114
+ SECTION("BCD subtraction: 00 - 01 with borrow = 99") {
115
+ test::CPUTestFixture f;
116
+ // SED; SEC; LDA #$00; SBC #$01
117
+ f.loadAndReset(0x0400, {0xF8, 0x38, 0xA9, 0x00, 0xE9, 0x01});
118
+ test::runInstructions(*f.cpu, 4);
119
+ REQUIRE(f.cpu->getA() == 0x99);
120
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_C) == false);
121
+ }
122
+
123
+ SECTION("BCD subtraction: 42 - 42 = 00") {
124
+ test::CPUTestFixture f;
125
+ f.loadAndReset(0x0400, {0xF8, 0x38, 0xA9, 0x42, 0xE9, 0x42});
126
+ test::runInstructions(*f.cpu, 4);
127
+ REQUIRE(f.cpu->getA() == 0x00);
128
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_Z) == true);
129
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_C) == true);
130
+ }
131
+
132
+ SECTION("BCD subtraction with borrow in (carry clear): 42 - 15 - 1 = 26") {
133
+ test::CPUTestFixture f;
134
+ // SED; CLC; LDA #$42; SBC #$15
135
+ f.loadAndReset(0x0400, {0xF8, 0x18, 0xA9, 0x42, 0xE9, 0x15});
136
+ test::runInstructions(*f.cpu, 4);
137
+ REQUIRE(f.cpu->getA() == 0x26);
138
+ }
139
+ }
140
+
141
+ // ============================================================================
142
+ // Decimal Mode Flag Preservation
143
+ // ============================================================================
144
+
145
+ TEST_CASE("Decimal mode flag preservation", "[cpu][decimal]") {
146
+
147
+ SECTION("D flag remains set across multiple operations") {
148
+ test::CPUTestFixture f;
149
+ // SED; CLC; LDA #$10; ADC #$20; CLC; ADC #$05
150
+ f.loadAndReset(0x0400, {0xF8, 0x18, 0xA9, 0x10, 0x69, 0x20, 0x18, 0x69, 0x05});
151
+ test::runInstructions(*f.cpu, 6);
152
+ // 10 + 20 = 30, then 30 + 05 = 35
153
+ REQUIRE(f.cpu->getA() == 0x35);
154
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_D) == true);
155
+ }
156
+
157
+ SECTION("CLD clears decimal mode and subsequent ADC is binary") {
158
+ test::CPUTestFixture f;
159
+ // SED; CLD; CLC; LDA #$09; ADC #$01
160
+ f.loadAndReset(0x0400, {0xF8, 0xD8, 0x18, 0xA9, 0x09, 0x69, 0x01});
161
+ test::runInstructions(*f.cpu, 5);
162
+ // Binary: $09 + $01 = $0A (not BCD $10)
163
+ REQUIRE(f.cpu->getA() == 0x0A);
164
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_D) == false);
165
+ }
166
+ }