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,1301 @@
1
+ /*
2
+ * test_cpu6502.cpp - Comprehensive 65C02 CPU instruction tests
3
+ *
4
+ * Tests all major instruction groups: loads, stores, arithmetic,
5
+ * logic, shifts, compares, branches, jumps, stack, flags, transfers,
6
+ * and 65C02-specific instructions.
7
+ */
8
+
9
+ #define CATCH_CONFIG_MAIN
10
+ #include "catch.hpp"
11
+ #include "test_helpers.hpp"
12
+
13
+ // ============================================================================
14
+ // LDA - Load Accumulator
15
+ // ============================================================================
16
+
17
+ TEST_CASE("LDA instructions", "[cpu][load]") {
18
+
19
+ SECTION("LDA immediate loads value") {
20
+ test::CPUTestFixture f;
21
+ f.loadAndReset(0x0400, {0xA9, 0x42}); // LDA #$42
22
+ f.cpu->executeInstruction();
23
+ REQUIRE(f.cpu->getA() == 0x42);
24
+ }
25
+
26
+ SECTION("LDA immediate sets zero flag") {
27
+ test::CPUTestFixture f;
28
+ f.loadAndReset(0x0400, {0xA9, 0x00}); // LDA #$00
29
+ f.cpu->executeInstruction();
30
+ REQUIRE(f.cpu->getA() == 0x00);
31
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_Z) == true);
32
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_N) == false);
33
+ }
34
+
35
+ SECTION("LDA immediate sets negative flag") {
36
+ test::CPUTestFixture f;
37
+ f.loadAndReset(0x0400, {0xA9, 0x80}); // LDA #$80
38
+ f.cpu->executeInstruction();
39
+ REQUIRE(f.cpu->getA() == 0x80);
40
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_N) == true);
41
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_Z) == false);
42
+ }
43
+
44
+ SECTION("LDA zero page") {
45
+ test::CPUTestFixture f;
46
+ f.mem[0x10] = 0x77;
47
+ f.loadAndReset(0x0400, {0xA5, 0x10}); // LDA $10
48
+ f.cpu->executeInstruction();
49
+ REQUIRE(f.cpu->getA() == 0x77);
50
+ }
51
+
52
+ SECTION("LDA absolute") {
53
+ test::CPUTestFixture f;
54
+ f.mem[0x1234] = 0xAB;
55
+ f.loadAndReset(0x0400, {0xAD, 0x34, 0x12}); // LDA $1234
56
+ f.cpu->executeInstruction();
57
+ REQUIRE(f.cpu->getA() == 0xAB);
58
+ }
59
+
60
+ SECTION("LDA zero page,X") {
61
+ test::CPUTestFixture f;
62
+ f.mem[0x15] = 0x99;
63
+ f.loadAndReset(0x0400, {0xA2, 0x05, 0xB5, 0x10}); // LDX #$05; LDA $10,X
64
+ test::runInstructions(*f.cpu, 2);
65
+ REQUIRE(f.cpu->getA() == 0x99);
66
+ }
67
+
68
+ SECTION("LDA absolute,X") {
69
+ test::CPUTestFixture f;
70
+ f.mem[0x1237] = 0xCD;
71
+ f.loadAndReset(0x0400, {0xA2, 0x03, 0xBD, 0x34, 0x12}); // LDX #$03; LDA $1234,X
72
+ test::runInstructions(*f.cpu, 2);
73
+ REQUIRE(f.cpu->getA() == 0xCD);
74
+ }
75
+
76
+ SECTION("LDA absolute,Y") {
77
+ test::CPUTestFixture f;
78
+ f.mem[0x1236] = 0xEF;
79
+ f.loadAndReset(0x0400, {0xA0, 0x02, 0xB9, 0x34, 0x12}); // LDY #$02; LDA $1234,Y
80
+ test::runInstructions(*f.cpu, 2);
81
+ REQUIRE(f.cpu->getA() == 0xEF);
82
+ }
83
+
84
+ SECTION("LDA (indirect,X)") {
85
+ test::CPUTestFixture f;
86
+ f.mem[0x12] = 0x00;
87
+ f.mem[0x13] = 0x20;
88
+ f.mem[0x2000] = 0xBB;
89
+ f.loadAndReset(0x0400, {0xA2, 0x02, 0xA1, 0x10}); // LDX #$02; LDA ($10,X)
90
+ test::runInstructions(*f.cpu, 2);
91
+ REQUIRE(f.cpu->getA() == 0xBB);
92
+ }
93
+
94
+ SECTION("LDA (indirect),Y") {
95
+ test::CPUTestFixture f;
96
+ f.mem[0x10] = 0x00;
97
+ f.mem[0x11] = 0x20;
98
+ f.mem[0x2003] = 0xCC;
99
+ f.loadAndReset(0x0400, {0xA0, 0x03, 0xB1, 0x10}); // LDY #$03; LDA ($10),Y
100
+ test::runInstructions(*f.cpu, 2);
101
+ REQUIRE(f.cpu->getA() == 0xCC);
102
+ }
103
+ }
104
+
105
+ // ============================================================================
106
+ // LDX - Load X Register
107
+ // ============================================================================
108
+
109
+ TEST_CASE("LDX instructions", "[cpu][load]") {
110
+
111
+ SECTION("LDX immediate loads value") {
112
+ test::CPUTestFixture f;
113
+ f.loadAndReset(0x0400, {0xA2, 0x33}); // LDX #$33
114
+ f.cpu->executeInstruction();
115
+ REQUIRE(f.cpu->getX() == 0x33);
116
+ }
117
+
118
+ SECTION("LDX immediate sets zero flag") {
119
+ test::CPUTestFixture f;
120
+ f.loadAndReset(0x0400, {0xA2, 0x00}); // LDX #$00
121
+ f.cpu->executeInstruction();
122
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_Z) == true);
123
+ }
124
+
125
+ SECTION("LDX immediate sets negative flag") {
126
+ test::CPUTestFixture f;
127
+ f.loadAndReset(0x0400, {0xA2, 0xFE}); // LDX #$FE
128
+ f.cpu->executeInstruction();
129
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_N) == true);
130
+ }
131
+
132
+ SECTION("LDX zero page") {
133
+ test::CPUTestFixture f;
134
+ f.mem[0x20] = 0x44;
135
+ f.loadAndReset(0x0400, {0xA6, 0x20}); // LDX $20
136
+ f.cpu->executeInstruction();
137
+ REQUIRE(f.cpu->getX() == 0x44);
138
+ }
139
+
140
+ SECTION("LDX absolute") {
141
+ test::CPUTestFixture f;
142
+ f.mem[0x3000] = 0x55;
143
+ f.loadAndReset(0x0400, {0xAE, 0x00, 0x30}); // LDX $3000
144
+ f.cpu->executeInstruction();
145
+ REQUIRE(f.cpu->getX() == 0x55);
146
+ }
147
+ }
148
+
149
+ // ============================================================================
150
+ // LDY - Load Y Register
151
+ // ============================================================================
152
+
153
+ TEST_CASE("LDY instructions", "[cpu][load]") {
154
+
155
+ SECTION("LDY immediate loads value") {
156
+ test::CPUTestFixture f;
157
+ f.loadAndReset(0x0400, {0xA0, 0x66}); // LDY #$66
158
+ f.cpu->executeInstruction();
159
+ REQUIRE(f.cpu->getY() == 0x66);
160
+ }
161
+
162
+ SECTION("LDY immediate sets zero flag") {
163
+ test::CPUTestFixture f;
164
+ f.loadAndReset(0x0400, {0xA0, 0x00}); // LDY #$00
165
+ f.cpu->executeInstruction();
166
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_Z) == true);
167
+ }
168
+
169
+ SECTION("LDY immediate sets negative flag") {
170
+ test::CPUTestFixture f;
171
+ f.loadAndReset(0x0400, {0xA0, 0x81}); // LDY #$81
172
+ f.cpu->executeInstruction();
173
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_N) == true);
174
+ }
175
+
176
+ SECTION("LDY zero page") {
177
+ test::CPUTestFixture f;
178
+ f.mem[0x30] = 0x77;
179
+ f.loadAndReset(0x0400, {0xA4, 0x30}); // LDY $30
180
+ f.cpu->executeInstruction();
181
+ REQUIRE(f.cpu->getY() == 0x77);
182
+ }
183
+
184
+ SECTION("LDY absolute") {
185
+ test::CPUTestFixture f;
186
+ f.mem[0x4000] = 0x88;
187
+ f.loadAndReset(0x0400, {0xAC, 0x00, 0x40}); // LDY $4000
188
+ f.cpu->executeInstruction();
189
+ REQUIRE(f.cpu->getY() == 0x88);
190
+ }
191
+ }
192
+
193
+ // ============================================================================
194
+ // STA/STX/STY - Store operations
195
+ // ============================================================================
196
+
197
+ TEST_CASE("Store instructions", "[cpu][store]") {
198
+
199
+ SECTION("STA zero page") {
200
+ test::CPUTestFixture f;
201
+ f.loadAndReset(0x0400, {0xA9, 0x42, 0x85, 0x10}); // LDA #$42; STA $10
202
+ test::runInstructions(*f.cpu, 2);
203
+ REQUIRE(f.mem[0x10] == 0x42);
204
+ }
205
+
206
+ SECTION("STA absolute") {
207
+ test::CPUTestFixture f;
208
+ f.loadAndReset(0x0400, {0xA9, 0xAB, 0x8D, 0x00, 0x20}); // LDA #$AB; STA $2000
209
+ test::runInstructions(*f.cpu, 2);
210
+ REQUIRE(f.mem[0x2000] == 0xAB);
211
+ }
212
+
213
+ SECTION("STA absolute,X") {
214
+ test::CPUTestFixture f;
215
+ f.loadAndReset(0x0400, {0xA9, 0xCD, 0xA2, 0x05, 0x9D, 0x00, 0x20}); // LDA #$CD; LDX #$05; STA $2000,X
216
+ test::runInstructions(*f.cpu, 3);
217
+ REQUIRE(f.mem[0x2005] == 0xCD);
218
+ }
219
+
220
+ SECTION("STA absolute,Y") {
221
+ test::CPUTestFixture f;
222
+ f.loadAndReset(0x0400, {0xA9, 0xEF, 0xA0, 0x03, 0x99, 0x00, 0x20}); // LDA #$EF; LDY #$03; STA $2000,Y
223
+ test::runInstructions(*f.cpu, 3);
224
+ REQUIRE(f.mem[0x2003] == 0xEF);
225
+ }
226
+
227
+ SECTION("STX zero page") {
228
+ test::CPUTestFixture f;
229
+ f.loadAndReset(0x0400, {0xA2, 0x55, 0x86, 0x20}); // LDX #$55; STX $20
230
+ test::runInstructions(*f.cpu, 2);
231
+ REQUIRE(f.mem[0x20] == 0x55);
232
+ }
233
+
234
+ SECTION("STX absolute") {
235
+ test::CPUTestFixture f;
236
+ f.loadAndReset(0x0400, {0xA2, 0x77, 0x8E, 0x00, 0x30}); // LDX #$77; STX $3000
237
+ test::runInstructions(*f.cpu, 2);
238
+ REQUIRE(f.mem[0x3000] == 0x77);
239
+ }
240
+
241
+ SECTION("STY zero page") {
242
+ test::CPUTestFixture f;
243
+ f.loadAndReset(0x0400, {0xA0, 0x66, 0x84, 0x30}); // LDY #$66; STY $30
244
+ test::runInstructions(*f.cpu, 2);
245
+ REQUIRE(f.mem[0x30] == 0x66);
246
+ }
247
+
248
+ SECTION("STY absolute") {
249
+ test::CPUTestFixture f;
250
+ f.loadAndReset(0x0400, {0xA0, 0x88, 0x8C, 0x00, 0x40}); // LDY #$88; STY $4000
251
+ test::runInstructions(*f.cpu, 2);
252
+ REQUIRE(f.mem[0x4000] == 0x88);
253
+ }
254
+ }
255
+
256
+ // ============================================================================
257
+ // ADC - Add with Carry
258
+ // ============================================================================
259
+
260
+ TEST_CASE("ADC instructions", "[cpu][arithmetic]") {
261
+
262
+ SECTION("ADC immediate simple addition") {
263
+ test::CPUTestFixture f;
264
+ // CLC; LDA #$10; ADC #$20
265
+ f.loadAndReset(0x0400, {0x18, 0xA9, 0x10, 0x69, 0x20});
266
+ test::runInstructions(*f.cpu, 3);
267
+ REQUIRE(f.cpu->getA() == 0x30);
268
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_C) == false);
269
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_Z) == false);
270
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_N) == false);
271
+ }
272
+
273
+ SECTION("ADC with carry in") {
274
+ test::CPUTestFixture f;
275
+ // SEC; LDA #$10; ADC #$20
276
+ f.loadAndReset(0x0400, {0x38, 0xA9, 0x10, 0x69, 0x20});
277
+ test::runInstructions(*f.cpu, 3);
278
+ REQUIRE(f.cpu->getA() == 0x31);
279
+ }
280
+
281
+ SECTION("ADC produces carry out") {
282
+ test::CPUTestFixture f;
283
+ // CLC; LDA #$FF; ADC #$01
284
+ f.loadAndReset(0x0400, {0x18, 0xA9, 0xFF, 0x69, 0x01});
285
+ test::runInstructions(*f.cpu, 3);
286
+ REQUIRE(f.cpu->getA() == 0x00);
287
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_C) == true);
288
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_Z) == true);
289
+ }
290
+
291
+ SECTION("ADC sets overflow flag: positive + positive = negative") {
292
+ test::CPUTestFixture f;
293
+ // CLC; LDA #$50; ADC #$50
294
+ f.loadAndReset(0x0400, {0x18, 0xA9, 0x50, 0x69, 0x50});
295
+ test::runInstructions(*f.cpu, 3);
296
+ REQUIRE(f.cpu->getA() == 0xA0);
297
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_V) == true);
298
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_N) == true);
299
+ }
300
+
301
+ SECTION("ADC sets overflow flag: negative + negative = positive") {
302
+ test::CPUTestFixture f;
303
+ // CLC; LDA #$D0; ADC #$90
304
+ f.loadAndReset(0x0400, {0x18, 0xA9, 0xD0, 0x69, 0x90});
305
+ test::runInstructions(*f.cpu, 3);
306
+ // 0xD0 + 0x90 = 0x160, A = 0x60
307
+ REQUIRE(f.cpu->getA() == 0x60);
308
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_V) == true);
309
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_C) == true);
310
+ }
311
+
312
+ SECTION("ADC zero page") {
313
+ test::CPUTestFixture f;
314
+ f.mem[0x10] = 0x25;
315
+ // CLC; LDA #$10; ADC $10
316
+ f.loadAndReset(0x0400, {0x18, 0xA9, 0x10, 0x65, 0x10});
317
+ test::runInstructions(*f.cpu, 3);
318
+ REQUIRE(f.cpu->getA() == 0x35);
319
+ }
320
+ }
321
+
322
+ // ============================================================================
323
+ // SBC - Subtract with Carry (borrow)
324
+ // ============================================================================
325
+
326
+ TEST_CASE("SBC instructions", "[cpu][arithmetic]") {
327
+
328
+ SECTION("SBC immediate simple subtraction") {
329
+ test::CPUTestFixture f;
330
+ // SEC; LDA #$50; SBC #$20
331
+ f.loadAndReset(0x0400, {0x38, 0xA9, 0x50, 0xE9, 0x20});
332
+ test::runInstructions(*f.cpu, 3);
333
+ REQUIRE(f.cpu->getA() == 0x30);
334
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_C) == true); // no borrow
335
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_N) == false);
336
+ }
337
+
338
+ SECTION("SBC with borrow (carry clear)") {
339
+ test::CPUTestFixture f;
340
+ // CLC; LDA #$50; SBC #$20
341
+ f.loadAndReset(0x0400, {0x18, 0xA9, 0x50, 0xE9, 0x20});
342
+ test::runInstructions(*f.cpu, 3);
343
+ REQUIRE(f.cpu->getA() == 0x2F); // 0x50 - 0x20 - 1 = 0x2F
344
+ }
345
+
346
+ SECTION("SBC produces borrow (carry clear result)") {
347
+ test::CPUTestFixture f;
348
+ // SEC; LDA #$10; SBC #$20
349
+ f.loadAndReset(0x0400, {0x38, 0xA9, 0x10, 0xE9, 0x20});
350
+ test::runInstructions(*f.cpu, 3);
351
+ REQUIRE(f.cpu->getA() == 0xF0);
352
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_C) == false); // borrow occurred
353
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_N) == true);
354
+ }
355
+
356
+ SECTION("SBC result is zero") {
357
+ test::CPUTestFixture f;
358
+ // SEC; LDA #$30; SBC #$30
359
+ f.loadAndReset(0x0400, {0x38, 0xA9, 0x30, 0xE9, 0x30});
360
+ test::runInstructions(*f.cpu, 3);
361
+ REQUIRE(f.cpu->getA() == 0x00);
362
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_Z) == true);
363
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_C) == true);
364
+ }
365
+
366
+ SECTION("SBC sets overflow: positive - negative = negative") {
367
+ test::CPUTestFixture f;
368
+ // SEC; LDA #$50; SBC #$B0
369
+ f.loadAndReset(0x0400, {0x38, 0xA9, 0x50, 0xE9, 0xB0});
370
+ test::runInstructions(*f.cpu, 3);
371
+ // signed: 80 - (-80) = 160, wraps -> 0xA0
372
+ REQUIRE(f.cpu->getA() == 0xA0);
373
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_V) == true);
374
+ }
375
+ }
376
+
377
+ // ============================================================================
378
+ // AND / ORA / EOR - Logical Operations
379
+ // ============================================================================
380
+
381
+ TEST_CASE("Logical instructions", "[cpu][logic]") {
382
+
383
+ SECTION("AND immediate") {
384
+ test::CPUTestFixture f;
385
+ f.loadAndReset(0x0400, {0xA9, 0xFF, 0x29, 0x0F}); // LDA #$FF; AND #$0F
386
+ test::runInstructions(*f.cpu, 2);
387
+ REQUIRE(f.cpu->getA() == 0x0F);
388
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_Z) == false);
389
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_N) == false);
390
+ }
391
+
392
+ SECTION("AND resulting in zero") {
393
+ test::CPUTestFixture f;
394
+ f.loadAndReset(0x0400, {0xA9, 0xAA, 0x29, 0x55}); // LDA #$AA; AND #$55
395
+ test::runInstructions(*f.cpu, 2);
396
+ REQUIRE(f.cpu->getA() == 0x00);
397
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_Z) == true);
398
+ }
399
+
400
+ SECTION("AND zero page") {
401
+ test::CPUTestFixture f;
402
+ f.mem[0x20] = 0xF0;
403
+ f.loadAndReset(0x0400, {0xA9, 0x3F, 0x25, 0x20}); // LDA #$3F; AND $20
404
+ test::runInstructions(*f.cpu, 2);
405
+ REQUIRE(f.cpu->getA() == 0x30);
406
+ }
407
+
408
+ SECTION("ORA immediate") {
409
+ test::CPUTestFixture f;
410
+ f.loadAndReset(0x0400, {0xA9, 0x0F, 0x09, 0xF0}); // LDA #$0F; ORA #$F0
411
+ test::runInstructions(*f.cpu, 2);
412
+ REQUIRE(f.cpu->getA() == 0xFF);
413
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_N) == true);
414
+ }
415
+
416
+ SECTION("ORA resulting in zero") {
417
+ test::CPUTestFixture f;
418
+ f.loadAndReset(0x0400, {0xA9, 0x00, 0x09, 0x00}); // LDA #$00; ORA #$00
419
+ test::runInstructions(*f.cpu, 2);
420
+ REQUIRE(f.cpu->getA() == 0x00);
421
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_Z) == true);
422
+ }
423
+
424
+ SECTION("EOR immediate") {
425
+ test::CPUTestFixture f;
426
+ f.loadAndReset(0x0400, {0xA9, 0xFF, 0x49, 0x0F}); // LDA #$FF; EOR #$0F
427
+ test::runInstructions(*f.cpu, 2);
428
+ REQUIRE(f.cpu->getA() == 0xF0);
429
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_N) == true);
430
+ }
431
+
432
+ SECTION("EOR self produces zero") {
433
+ test::CPUTestFixture f;
434
+ f.loadAndReset(0x0400, {0xA9, 0xAA, 0x49, 0xAA}); // LDA #$AA; EOR #$AA
435
+ test::runInstructions(*f.cpu, 2);
436
+ REQUIRE(f.cpu->getA() == 0x00);
437
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_Z) == true);
438
+ }
439
+ }
440
+
441
+ // ============================================================================
442
+ // INC / DEC / INX / INY / DEX / DEY
443
+ // ============================================================================
444
+
445
+ TEST_CASE("Increment and decrement instructions", "[cpu][incdec]") {
446
+
447
+ SECTION("INC zero page") {
448
+ test::CPUTestFixture f;
449
+ f.mem[0x10] = 0x41;
450
+ f.loadAndReset(0x0400, {0xE6, 0x10}); // INC $10
451
+ f.cpu->executeInstruction();
452
+ REQUIRE(f.mem[0x10] == 0x42);
453
+ }
454
+
455
+ SECTION("INC zero page wraps from FF to 00") {
456
+ test::CPUTestFixture f;
457
+ f.mem[0x10] = 0xFF;
458
+ f.loadAndReset(0x0400, {0xE6, 0x10}); // INC $10
459
+ f.cpu->executeInstruction();
460
+ REQUIRE(f.mem[0x10] == 0x00);
461
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_Z) == true);
462
+ }
463
+
464
+ SECTION("INC absolute") {
465
+ test::CPUTestFixture f;
466
+ f.mem[0x2000] = 0x7F;
467
+ f.loadAndReset(0x0400, {0xEE, 0x00, 0x20}); // INC $2000
468
+ f.cpu->executeInstruction();
469
+ REQUIRE(f.mem[0x2000] == 0x80);
470
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_N) == true);
471
+ }
472
+
473
+ SECTION("DEC zero page") {
474
+ test::CPUTestFixture f;
475
+ f.mem[0x10] = 0x42;
476
+ f.loadAndReset(0x0400, {0xC6, 0x10}); // DEC $10
477
+ f.cpu->executeInstruction();
478
+ REQUIRE(f.mem[0x10] == 0x41);
479
+ }
480
+
481
+ SECTION("DEC zero page wraps from 00 to FF") {
482
+ test::CPUTestFixture f;
483
+ f.mem[0x10] = 0x00;
484
+ f.loadAndReset(0x0400, {0xC6, 0x10}); // DEC $10
485
+ f.cpu->executeInstruction();
486
+ REQUIRE(f.mem[0x10] == 0xFF);
487
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_N) == true);
488
+ }
489
+
490
+ SECTION("INX increments X register") {
491
+ test::CPUTestFixture f;
492
+ f.loadAndReset(0x0400, {0xA2, 0x05, 0xE8}); // LDX #$05; INX
493
+ test::runInstructions(*f.cpu, 2);
494
+ REQUIRE(f.cpu->getX() == 0x06);
495
+ }
496
+
497
+ SECTION("INX wraps from FF to 00") {
498
+ test::CPUTestFixture f;
499
+ f.loadAndReset(0x0400, {0xA2, 0xFF, 0xE8}); // LDX #$FF; INX
500
+ test::runInstructions(*f.cpu, 2);
501
+ REQUIRE(f.cpu->getX() == 0x00);
502
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_Z) == true);
503
+ }
504
+
505
+ SECTION("INY increments Y register") {
506
+ test::CPUTestFixture f;
507
+ f.loadAndReset(0x0400, {0xA0, 0x0A, 0xC8}); // LDY #$0A; INY
508
+ test::runInstructions(*f.cpu, 2);
509
+ REQUIRE(f.cpu->getY() == 0x0B);
510
+ }
511
+
512
+ SECTION("DEX decrements X register") {
513
+ test::CPUTestFixture f;
514
+ f.loadAndReset(0x0400, {0xA2, 0x05, 0xCA}); // LDX #$05; DEX
515
+ test::runInstructions(*f.cpu, 2);
516
+ REQUIRE(f.cpu->getX() == 0x04);
517
+ }
518
+
519
+ SECTION("DEX wraps from 00 to FF") {
520
+ test::CPUTestFixture f;
521
+ f.loadAndReset(0x0400, {0xA2, 0x00, 0xCA}); // LDX #$00; DEX
522
+ test::runInstructions(*f.cpu, 2);
523
+ REQUIRE(f.cpu->getX() == 0xFF);
524
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_N) == true);
525
+ }
526
+
527
+ SECTION("DEY decrements Y register") {
528
+ test::CPUTestFixture f;
529
+ f.loadAndReset(0x0400, {0xA0, 0x01, 0x88}); // LDY #$01; DEY
530
+ test::runInstructions(*f.cpu, 2);
531
+ REQUIRE(f.cpu->getY() == 0x00);
532
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_Z) == true);
533
+ }
534
+ }
535
+
536
+ // ============================================================================
537
+ // ASL / LSR / ROL / ROR - Shifts and Rotates
538
+ // ============================================================================
539
+
540
+ TEST_CASE("Shift and rotate instructions", "[cpu][shift]") {
541
+
542
+ SECTION("ASL accumulator shifts left") {
543
+ test::CPUTestFixture f;
544
+ f.loadAndReset(0x0400, {0xA9, 0x41, 0x0A}); // LDA #$41; ASL A
545
+ test::runInstructions(*f.cpu, 2);
546
+ REQUIRE(f.cpu->getA() == 0x82);
547
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_C) == false);
548
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_N) == true);
549
+ }
550
+
551
+ SECTION("ASL accumulator sets carry from bit 7") {
552
+ test::CPUTestFixture f;
553
+ f.loadAndReset(0x0400, {0xA9, 0x80, 0x0A}); // LDA #$80; ASL A
554
+ test::runInstructions(*f.cpu, 2);
555
+ REQUIRE(f.cpu->getA() == 0x00);
556
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_C) == true);
557
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_Z) == true);
558
+ }
559
+
560
+ SECTION("ASL zero page") {
561
+ test::CPUTestFixture f;
562
+ f.mem[0x10] = 0x55;
563
+ f.loadAndReset(0x0400, {0x06, 0x10}); // ASL $10
564
+ f.cpu->executeInstruction();
565
+ REQUIRE(f.mem[0x10] == 0xAA);
566
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_C) == false);
567
+ }
568
+
569
+ SECTION("LSR accumulator shifts right") {
570
+ test::CPUTestFixture f;
571
+ f.loadAndReset(0x0400, {0xA9, 0x82, 0x4A}); // LDA #$82; LSR A
572
+ test::runInstructions(*f.cpu, 2);
573
+ REQUIRE(f.cpu->getA() == 0x41);
574
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_C) == false);
575
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_N) == false);
576
+ }
577
+
578
+ SECTION("LSR accumulator sets carry from bit 0") {
579
+ test::CPUTestFixture f;
580
+ f.loadAndReset(0x0400, {0xA9, 0x01, 0x4A}); // LDA #$01; LSR A
581
+ test::runInstructions(*f.cpu, 2);
582
+ REQUIRE(f.cpu->getA() == 0x00);
583
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_C) == true);
584
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_Z) == true);
585
+ }
586
+
587
+ SECTION("LSR zero page") {
588
+ test::CPUTestFixture f;
589
+ f.mem[0x10] = 0xAA;
590
+ f.loadAndReset(0x0400, {0x46, 0x10}); // LSR $10
591
+ f.cpu->executeInstruction();
592
+ REQUIRE(f.mem[0x10] == 0x55);
593
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_C) == false);
594
+ }
595
+
596
+ SECTION("ROL accumulator rotates left through carry") {
597
+ test::CPUTestFixture f;
598
+ // SEC; LDA #$40; ROL A -> bit 7=0, bit 0=carry(1) -> 0x81, carry=0
599
+ f.loadAndReset(0x0400, {0x38, 0xA9, 0x40, 0x2A});
600
+ test::runInstructions(*f.cpu, 3);
601
+ REQUIRE(f.cpu->getA() == 0x81);
602
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_C) == false);
603
+ }
604
+
605
+ SECTION("ROL accumulator sets carry from bit 7") {
606
+ test::CPUTestFixture f;
607
+ // CLC; LDA #$80; ROL A -> 0x00, carry=1
608
+ f.loadAndReset(0x0400, {0x18, 0xA9, 0x80, 0x2A});
609
+ test::runInstructions(*f.cpu, 3);
610
+ REQUIRE(f.cpu->getA() == 0x00);
611
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_C) == true);
612
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_Z) == true);
613
+ }
614
+
615
+ SECTION("ROR accumulator rotates right through carry") {
616
+ test::CPUTestFixture f;
617
+ // SEC; LDA #$02; ROR A -> carry goes to bit 7, bit 0 goes to carry
618
+ f.loadAndReset(0x0400, {0x38, 0xA9, 0x02, 0x6A});
619
+ test::runInstructions(*f.cpu, 3);
620
+ REQUIRE(f.cpu->getA() == 0x81);
621
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_C) == false);
622
+ }
623
+
624
+ SECTION("ROR accumulator sets carry from bit 0") {
625
+ test::CPUTestFixture f;
626
+ // CLC; LDA #$01; ROR A -> 0x00, carry=1
627
+ f.loadAndReset(0x0400, {0x18, 0xA9, 0x01, 0x6A});
628
+ test::runInstructions(*f.cpu, 3);
629
+ REQUIRE(f.cpu->getA() == 0x00);
630
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_C) == true);
631
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_Z) == true);
632
+ }
633
+
634
+ SECTION("ROL zero page") {
635
+ test::CPUTestFixture f;
636
+ f.mem[0x10] = 0x55;
637
+ // CLC; ROL $10
638
+ f.loadAndReset(0x0400, {0x18, 0x26, 0x10});
639
+ test::runInstructions(*f.cpu, 2);
640
+ REQUIRE(f.mem[0x10] == 0xAA);
641
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_C) == false);
642
+ }
643
+
644
+ SECTION("ROR zero page") {
645
+ test::CPUTestFixture f;
646
+ f.mem[0x10] = 0xAA;
647
+ // CLC; ROR $10
648
+ f.loadAndReset(0x0400, {0x18, 0x66, 0x10});
649
+ test::runInstructions(*f.cpu, 2);
650
+ REQUIRE(f.mem[0x10] == 0x55);
651
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_C) == false);
652
+ }
653
+ }
654
+
655
+ // ============================================================================
656
+ // CMP / CPX / CPY - Compare Operations
657
+ // ============================================================================
658
+
659
+ TEST_CASE("Compare instructions", "[cpu][compare]") {
660
+
661
+ SECTION("CMP immediate equal") {
662
+ test::CPUTestFixture f;
663
+ f.loadAndReset(0x0400, {0xA9, 0x42, 0xC9, 0x42}); // LDA #$42; CMP #$42
664
+ test::runInstructions(*f.cpu, 2);
665
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_Z) == true);
666
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_C) == true);
667
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_N) == false);
668
+ }
669
+
670
+ SECTION("CMP immediate A > operand") {
671
+ test::CPUTestFixture f;
672
+ f.loadAndReset(0x0400, {0xA9, 0x50, 0xC9, 0x30}); // LDA #$50; CMP #$30
673
+ test::runInstructions(*f.cpu, 2);
674
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_Z) == false);
675
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_C) == true);
676
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_N) == false);
677
+ }
678
+
679
+ SECTION("CMP immediate A < operand") {
680
+ test::CPUTestFixture f;
681
+ f.loadAndReset(0x0400, {0xA9, 0x30, 0xC9, 0x50}); // LDA #$30; CMP #$50
682
+ test::runInstructions(*f.cpu, 2);
683
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_Z) == false);
684
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_C) == false);
685
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_N) == true);
686
+ }
687
+
688
+ SECTION("CMP zero page") {
689
+ test::CPUTestFixture f;
690
+ f.mem[0x10] = 0x42;
691
+ f.loadAndReset(0x0400, {0xA9, 0x42, 0xC5, 0x10}); // LDA #$42; CMP $10
692
+ test::runInstructions(*f.cpu, 2);
693
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_Z) == true);
694
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_C) == true);
695
+ }
696
+
697
+ SECTION("CPX immediate equal") {
698
+ test::CPUTestFixture f;
699
+ f.loadAndReset(0x0400, {0xA2, 0x10, 0xE0, 0x10}); // LDX #$10; CPX #$10
700
+ test::runInstructions(*f.cpu, 2);
701
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_Z) == true);
702
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_C) == true);
703
+ }
704
+
705
+ SECTION("CPX immediate X < operand") {
706
+ test::CPUTestFixture f;
707
+ f.loadAndReset(0x0400, {0xA2, 0x05, 0xE0, 0x10}); // LDX #$05; CPX #$10
708
+ test::runInstructions(*f.cpu, 2);
709
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_C) == false);
710
+ }
711
+
712
+ SECTION("CPY immediate equal") {
713
+ test::CPUTestFixture f;
714
+ f.loadAndReset(0x0400, {0xA0, 0x20, 0xC0, 0x20}); // LDY #$20; CPY #$20
715
+ test::runInstructions(*f.cpu, 2);
716
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_Z) == true);
717
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_C) == true);
718
+ }
719
+
720
+ SECTION("CPY immediate Y > operand") {
721
+ test::CPUTestFixture f;
722
+ f.loadAndReset(0x0400, {0xA0, 0x30, 0xC0, 0x10}); // LDY #$30; CPY #$10
723
+ test::runInstructions(*f.cpu, 2);
724
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_C) == true);
725
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_Z) == false);
726
+ }
727
+ }
728
+
729
+ // ============================================================================
730
+ // BIT - Test Bits
731
+ // ============================================================================
732
+
733
+ TEST_CASE("BIT instruction", "[cpu][bit]") {
734
+
735
+ SECTION("BIT zero page sets Z when AND result is zero") {
736
+ test::CPUTestFixture f;
737
+ f.mem[0x10] = 0xF0;
738
+ f.loadAndReset(0x0400, {0xA9, 0x0F, 0x24, 0x10}); // LDA #$0F; BIT $10
739
+ test::runInstructions(*f.cpu, 2);
740
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_Z) == true);
741
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_N) == true); // bit 7 of memory
742
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_V) == true); // bit 6 of memory
743
+ }
744
+
745
+ SECTION("BIT zero page clears Z when AND result is nonzero") {
746
+ test::CPUTestFixture f;
747
+ f.mem[0x10] = 0x3F;
748
+ f.loadAndReset(0x0400, {0xA9, 0x0F, 0x24, 0x10}); // LDA #$0F; BIT $10
749
+ test::runInstructions(*f.cpu, 2);
750
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_Z) == false);
751
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_N) == false); // bit 7 of memory is 0
752
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_V) == false); // bit 6 of memory is 0
753
+ }
754
+
755
+ SECTION("BIT absolute") {
756
+ test::CPUTestFixture f;
757
+ f.mem[0x2000] = 0xC0;
758
+ f.loadAndReset(0x0400, {0xA9, 0x40, 0x2C, 0x00, 0x20}); // LDA #$40; BIT $2000
759
+ test::runInstructions(*f.cpu, 2);
760
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_Z) == false); // $40 AND $C0 = $40 != 0
761
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_N) == true);
762
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_V) == true);
763
+ }
764
+ }
765
+
766
+ // ============================================================================
767
+ // Branch Instructions
768
+ // ============================================================================
769
+
770
+ TEST_CASE("Branch instructions", "[cpu][branch]") {
771
+
772
+ SECTION("BEQ taken when Z set") {
773
+ test::CPUTestFixture f;
774
+ // LDA #$00 (sets Z); BEQ +2; LDA #$FF; (target): LDA #$42
775
+ f.loadAndReset(0x0400, {0xA9, 0x00, 0xF0, 0x02, 0xA9, 0xFF, 0xA9, 0x42});
776
+ test::runInstructions(*f.cpu, 3); // LDA, BEQ, LDA at target
777
+ REQUIRE(f.cpu->getA() == 0x42);
778
+ }
779
+
780
+ SECTION("BEQ not taken when Z clear") {
781
+ test::CPUTestFixture f;
782
+ f.loadAndReset(0x0400, {0xA9, 0x01, 0xF0, 0x02, 0xA9, 0xFF, 0xA9, 0x42});
783
+ test::runInstructions(*f.cpu, 3);
784
+ REQUIRE(f.cpu->getA() == 0xFF);
785
+ }
786
+
787
+ SECTION("BNE taken when Z clear") {
788
+ test::CPUTestFixture f;
789
+ f.loadAndReset(0x0400, {0xA9, 0x01, 0xD0, 0x02, 0xA9, 0xFF, 0xA9, 0x42});
790
+ test::runInstructions(*f.cpu, 3);
791
+ REQUIRE(f.cpu->getA() == 0x42);
792
+ }
793
+
794
+ SECTION("BNE not taken when Z set") {
795
+ test::CPUTestFixture f;
796
+ f.loadAndReset(0x0400, {0xA9, 0x00, 0xD0, 0x02, 0xA9, 0xFF, 0xA9, 0x42});
797
+ test::runInstructions(*f.cpu, 3);
798
+ REQUIRE(f.cpu->getA() == 0xFF);
799
+ }
800
+
801
+ SECTION("BCC taken when carry clear") {
802
+ test::CPUTestFixture f;
803
+ f.loadAndReset(0x0400, {0x18, 0x90, 0x02, 0xA9, 0xFF, 0xA9, 0x42}); // CLC; BCC +2
804
+ test::runInstructions(*f.cpu, 3);
805
+ REQUIRE(f.cpu->getA() == 0x42);
806
+ }
807
+
808
+ SECTION("BCS taken when carry set") {
809
+ test::CPUTestFixture f;
810
+ f.loadAndReset(0x0400, {0x38, 0xB0, 0x02, 0xA9, 0xFF, 0xA9, 0x42}); // SEC; BCS +2
811
+ test::runInstructions(*f.cpu, 3);
812
+ REQUIRE(f.cpu->getA() == 0x42);
813
+ }
814
+
815
+ SECTION("BMI taken when N set") {
816
+ test::CPUTestFixture f;
817
+ f.loadAndReset(0x0400, {0xA9, 0x80, 0x30, 0x02, 0xA9, 0xFF, 0xA9, 0x42}); // LDA #$80; BMI +2
818
+ test::runInstructions(*f.cpu, 3);
819
+ REQUIRE(f.cpu->getA() == 0x42);
820
+ }
821
+
822
+ SECTION("BPL taken when N clear") {
823
+ test::CPUTestFixture f;
824
+ f.loadAndReset(0x0400, {0xA9, 0x01, 0x10, 0x02, 0xA9, 0xFF, 0xA9, 0x42}); // LDA #$01; BPL +2
825
+ test::runInstructions(*f.cpu, 3);
826
+ REQUIRE(f.cpu->getA() == 0x42);
827
+ }
828
+
829
+ SECTION("BVS taken when V set") {
830
+ test::CPUTestFixture f;
831
+ // CLC; LDA #$50; ADC #$50 (sets V); BVS +2; LDA #$FF; LDA #$42
832
+ f.loadAndReset(0x0400, {0x18, 0xA9, 0x50, 0x69, 0x50, 0x70, 0x02, 0xA9, 0xFF, 0xA9, 0x42});
833
+ test::runInstructions(*f.cpu, 5);
834
+ REQUIRE(f.cpu->getA() == 0x42);
835
+ }
836
+
837
+ SECTION("BVC taken when V clear") {
838
+ test::CPUTestFixture f;
839
+ // CLV; LDA #$01; BVC +2; LDA #$FF; LDA #$42
840
+ f.loadAndReset(0x0400, {0xB8, 0xA9, 0x01, 0x50, 0x02, 0xA9, 0xFF, 0xA9, 0x42});
841
+ test::runInstructions(*f.cpu, 4);
842
+ REQUIRE(f.cpu->getA() == 0x42);
843
+ }
844
+
845
+ SECTION("Branch backward") {
846
+ test::CPUTestFixture f;
847
+ // 0400: LDX #$03
848
+ // 0402: DEX <- branch target
849
+ // 0403: BNE $FD (branch back to 0402)
850
+ // 0405: (end)
851
+ f.loadAndReset(0x0400, {0xA2, 0x03, 0xCA, 0xD0, 0xFD});
852
+ // LDX #$03, then loop DEX;BNE 3 times -> X goes 3->2->1->0
853
+ test::runUntilPC(*f.cpu, 0x0405, 100);
854
+ REQUIRE(f.cpu->getX() == 0x00);
855
+ }
856
+
857
+ SECTION("BRA always branches (65C02)") {
858
+ test::CPUTestFixture f;
859
+ // BRA +2; LDA #$FF; LDA #$42
860
+ f.loadAndReset(0x0400, {0x80, 0x02, 0xA9, 0xFF, 0xA9, 0x42});
861
+ test::runInstructions(*f.cpu, 2); // BRA, LDA at target
862
+ REQUIRE(f.cpu->getA() == 0x42);
863
+ }
864
+ }
865
+
866
+ // ============================================================================
867
+ // JMP / JSR / RTS
868
+ // ============================================================================
869
+
870
+ TEST_CASE("Jump instructions", "[cpu][jump]") {
871
+
872
+ SECTION("JMP absolute") {
873
+ test::CPUTestFixture f;
874
+ f.mem[0x2000] = 0xA9; // LDA #$42 at $2000
875
+ f.mem[0x2001] = 0x42;
876
+ f.loadAndReset(0x0400, {0x4C, 0x00, 0x20}); // JMP $2000
877
+ test::runInstructions(*f.cpu, 2); // JMP, LDA
878
+ REQUIRE(f.cpu->getA() == 0x42);
879
+ REQUIRE(f.cpu->getPC() == 0x2002);
880
+ }
881
+
882
+ SECTION("JMP indirect") {
883
+ test::CPUTestFixture f;
884
+ // Indirect pointer at $3000 points to $2000
885
+ f.mem[0x3000] = 0x00;
886
+ f.mem[0x3001] = 0x20;
887
+ f.mem[0x2000] = 0xA9;
888
+ f.mem[0x2001] = 0x77;
889
+ f.loadAndReset(0x0400, {0x6C, 0x00, 0x30}); // JMP ($3000)
890
+ test::runInstructions(*f.cpu, 2); // JMP, LDA
891
+ REQUIRE(f.cpu->getA() == 0x77);
892
+ }
893
+
894
+ SECTION("JSR and RTS") {
895
+ test::CPUTestFixture f;
896
+ // 0400: JSR $0500
897
+ // 0403: LDA #$42 <- return here
898
+ // 0500: LDX #$10 <- subroutine
899
+ // 0502: RTS
900
+ f.mem.loadProgram(0x0400, {0x20, 0x00, 0x05, 0xA9, 0x42});
901
+ f.mem.loadProgram(0x0500, {0xA2, 0x10, 0x60});
902
+ f.mem.setResetVector(0x0400);
903
+ f.cpu->reset();
904
+ test::runInstructions(*f.cpu, 4); // JSR, LDX, RTS, LDA
905
+ REQUIRE(f.cpu->getX() == 0x10);
906
+ REQUIRE(f.cpu->getA() == 0x42);
907
+ REQUIRE(f.cpu->getPC() == 0x0405);
908
+ }
909
+
910
+ SECTION("JSR pushes return address minus one") {
911
+ test::CPUTestFixture f;
912
+ f.loadAndReset(0x0400, {0x20, 0x00, 0x05}); // JSR $0500
913
+ uint8_t spBefore = f.cpu->getSP();
914
+ f.cpu->executeInstruction();
915
+ // SP should have decremented by 2
916
+ REQUIRE(f.cpu->getSP() == static_cast<uint8_t>(spBefore - 2));
917
+ // Stack should contain return address - 1 = $0402
918
+ // The 6502 pushes PCH first, then PCL
919
+ // The return address on stack is PC-1 = 0x0402
920
+ // Stack at [SP+1] = low, [SP+2] = high
921
+ uint8_t retLo = f.mem[0x0100 + f.cpu->getSP() + 1];
922
+ uint8_t retHi = f.mem[0x0100 + f.cpu->getSP() + 2];
923
+ uint16_t retAddr = (retHi << 8) | retLo;
924
+ REQUIRE(retAddr == 0x0402);
925
+ }
926
+ }
927
+
928
+ // ============================================================================
929
+ // Stack Instructions
930
+ // ============================================================================
931
+
932
+ TEST_CASE("Stack instructions", "[cpu][stack]") {
933
+
934
+ SECTION("PHA pushes accumulator") {
935
+ test::CPUTestFixture f;
936
+ f.loadAndReset(0x0400, {0xA9, 0x42, 0x48}); // LDA #$42; PHA
937
+ test::runInstructions(*f.cpu, 2);
938
+ REQUIRE(f.mem[0x01FD] == 0x42); // SP starts at $FD after reset, push writes to $01FD
939
+ }
940
+
941
+ SECTION("PLA pulls into accumulator") {
942
+ test::CPUTestFixture f;
943
+ f.loadAndReset(0x0400, {0xA9, 0x42, 0x48, 0xA9, 0x00, 0x68}); // LDA #$42; PHA; LDA #$00; PLA
944
+ test::runInstructions(*f.cpu, 4);
945
+ REQUIRE(f.cpu->getA() == 0x42);
946
+ }
947
+
948
+ SECTION("PLA sets Z flag when pulling zero") {
949
+ test::CPUTestFixture f;
950
+ f.loadAndReset(0x0400, {0xA9, 0x00, 0x48, 0xA9, 0xFF, 0x68}); // LDA #$00; PHA; LDA #$FF; PLA
951
+ test::runInstructions(*f.cpu, 4);
952
+ REQUIRE(f.cpu->getA() == 0x00);
953
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_Z) == true);
954
+ }
955
+
956
+ SECTION("PLA sets N flag when pulling negative") {
957
+ test::CPUTestFixture f;
958
+ f.loadAndReset(0x0400, {0xA9, 0x80, 0x48, 0xA9, 0x00, 0x68}); // LDA #$80; PHA; LDA #$00; PLA
959
+ test::runInstructions(*f.cpu, 4);
960
+ REQUIRE(f.cpu->getA() == 0x80);
961
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_N) == true);
962
+ }
963
+
964
+ SECTION("PHP pushes status with B and U flags set") {
965
+ test::CPUTestFixture f;
966
+ f.loadAndReset(0x0400, {0x08}); // PHP
967
+ f.cpu->executeInstruction();
968
+ uint8_t pushed = f.mem[0x01FD]; // SP starts at $FD after reset, push writes to $01FD
969
+ // PHP pushes P with B (0x10) and U (0x20) always set
970
+ REQUIRE((pushed & a2e::FLAG_B) != 0);
971
+ REQUIRE((pushed & a2e::FLAG_U) != 0);
972
+ }
973
+
974
+ SECTION("PLP restores status register") {
975
+ test::CPUTestFixture f;
976
+ // SEC; SED; PHP; CLC; CLD; PLP
977
+ f.loadAndReset(0x0400, {0x38, 0xF8, 0x08, 0x18, 0xD8, 0x28});
978
+ test::runInstructions(*f.cpu, 6);
979
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_C) == true);
980
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_D) == true);
981
+ }
982
+
983
+ SECTION("PHX pushes X register (65C02)") {
984
+ test::CPUTestFixture f;
985
+ f.loadAndReset(0x0400, {0xA2, 0x99, 0xDA}); // LDX #$99; PHX
986
+ test::runInstructions(*f.cpu, 2);
987
+ REQUIRE(f.mem[0x01FD] == 0x99); // SP starts at $FD after reset, push writes to $01FD
988
+ }
989
+
990
+ SECTION("PLX pulls into X register (65C02)") {
991
+ test::CPUTestFixture f;
992
+ f.loadAndReset(0x0400, {0xA2, 0x55, 0xDA, 0xA2, 0x00, 0xFA}); // LDX #$55; PHX; LDX #$00; PLX
993
+ test::runInstructions(*f.cpu, 4);
994
+ REQUIRE(f.cpu->getX() == 0x55);
995
+ }
996
+
997
+ SECTION("PHY pushes Y register (65C02)") {
998
+ test::CPUTestFixture f;
999
+ f.loadAndReset(0x0400, {0xA0, 0xAA, 0x5A}); // LDY #$AA; PHY
1000
+ test::runInstructions(*f.cpu, 2);
1001
+ REQUIRE(f.mem[0x01FD] == 0xAA); // SP starts at $FD after reset, push writes to $01FD
1002
+ }
1003
+
1004
+ SECTION("PLY pulls into Y register (65C02)") {
1005
+ test::CPUTestFixture f;
1006
+ f.loadAndReset(0x0400, {0xA0, 0x77, 0x5A, 0xA0, 0x00, 0x7A}); // LDY #$77; PHY; LDY #$00; PLY
1007
+ test::runInstructions(*f.cpu, 4);
1008
+ REQUIRE(f.cpu->getY() == 0x77);
1009
+ }
1010
+ }
1011
+
1012
+ // ============================================================================
1013
+ // Flag Instructions
1014
+ // ============================================================================
1015
+
1016
+ TEST_CASE("Flag instructions", "[cpu][flags]") {
1017
+
1018
+ SECTION("SEC sets carry") {
1019
+ test::CPUTestFixture f;
1020
+ f.loadAndReset(0x0400, {0x38}); // SEC
1021
+ f.cpu->executeInstruction();
1022
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_C) == true);
1023
+ }
1024
+
1025
+ SECTION("CLC clears carry") {
1026
+ test::CPUTestFixture f;
1027
+ f.loadAndReset(0x0400, {0x38, 0x18}); // SEC; CLC
1028
+ test::runInstructions(*f.cpu, 2);
1029
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_C) == false);
1030
+ }
1031
+
1032
+ SECTION("SED sets decimal") {
1033
+ test::CPUTestFixture f;
1034
+ f.loadAndReset(0x0400, {0xF8}); // SED
1035
+ f.cpu->executeInstruction();
1036
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_D) == true);
1037
+ }
1038
+
1039
+ SECTION("CLD clears decimal") {
1040
+ test::CPUTestFixture f;
1041
+ f.loadAndReset(0x0400, {0xF8, 0xD8}); // SED; CLD
1042
+ test::runInstructions(*f.cpu, 2);
1043
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_D) == false);
1044
+ }
1045
+
1046
+ SECTION("SEI sets interrupt disable") {
1047
+ test::CPUTestFixture f;
1048
+ f.loadAndReset(0x0400, {0x78}); // SEI
1049
+ f.cpu->executeInstruction();
1050
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_I) == true);
1051
+ }
1052
+
1053
+ SECTION("CLI clears interrupt disable") {
1054
+ test::CPUTestFixture f;
1055
+ f.loadAndReset(0x0400, {0x78, 0x58}); // SEI; CLI
1056
+ test::runInstructions(*f.cpu, 2);
1057
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_I) == false);
1058
+ }
1059
+
1060
+ SECTION("CLV clears overflow") {
1061
+ test::CPUTestFixture f;
1062
+ // Set overflow via ADC, then clear it
1063
+ // CLC; LDA #$50; ADC #$50 (sets V); CLV
1064
+ f.loadAndReset(0x0400, {0x18, 0xA9, 0x50, 0x69, 0x50, 0xB8});
1065
+ test::runInstructions(*f.cpu, 4);
1066
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_V) == false);
1067
+ }
1068
+ }
1069
+
1070
+ // ============================================================================
1071
+ // Transfer Instructions
1072
+ // ============================================================================
1073
+
1074
+ TEST_CASE("Transfer instructions", "[cpu][transfer]") {
1075
+
1076
+ SECTION("TAX transfers A to X") {
1077
+ test::CPUTestFixture f;
1078
+ f.loadAndReset(0x0400, {0xA9, 0x42, 0xAA}); // LDA #$42; TAX
1079
+ test::runInstructions(*f.cpu, 2);
1080
+ REQUIRE(f.cpu->getX() == 0x42);
1081
+ }
1082
+
1083
+ SECTION("TAX sets zero flag") {
1084
+ test::CPUTestFixture f;
1085
+ f.loadAndReset(0x0400, {0xA9, 0x00, 0xAA}); // LDA #$00; TAX
1086
+ test::runInstructions(*f.cpu, 2);
1087
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_Z) == true);
1088
+ }
1089
+
1090
+ SECTION("TXA transfers X to A") {
1091
+ test::CPUTestFixture f;
1092
+ f.loadAndReset(0x0400, {0xA2, 0x55, 0x8A}); // LDX #$55; TXA
1093
+ test::runInstructions(*f.cpu, 2);
1094
+ REQUIRE(f.cpu->getA() == 0x55);
1095
+ }
1096
+
1097
+ SECTION("TAY transfers A to Y") {
1098
+ test::CPUTestFixture f;
1099
+ f.loadAndReset(0x0400, {0xA9, 0x33, 0xA8}); // LDA #$33; TAY
1100
+ test::runInstructions(*f.cpu, 2);
1101
+ REQUIRE(f.cpu->getY() == 0x33);
1102
+ }
1103
+
1104
+ SECTION("TYA transfers Y to A") {
1105
+ test::CPUTestFixture f;
1106
+ f.loadAndReset(0x0400, {0xA0, 0x77, 0x98}); // LDY #$77; TYA
1107
+ test::runInstructions(*f.cpu, 2);
1108
+ REQUIRE(f.cpu->getA() == 0x77);
1109
+ }
1110
+
1111
+ SECTION("TSX transfers SP to X") {
1112
+ test::CPUTestFixture f;
1113
+ f.loadAndReset(0x0400, {0xBA}); // TSX
1114
+ f.cpu->executeInstruction();
1115
+ REQUIRE(f.cpu->getX() == f.cpu->getSP());
1116
+ }
1117
+
1118
+ SECTION("TXS transfers X to SP") {
1119
+ test::CPUTestFixture f;
1120
+ f.loadAndReset(0x0400, {0xA2, 0x80, 0x9A}); // LDX #$80; TXS
1121
+ test::runInstructions(*f.cpu, 2);
1122
+ REQUIRE(f.cpu->getSP() == 0x80);
1123
+ }
1124
+
1125
+ SECTION("TXS does not affect flags") {
1126
+ test::CPUTestFixture f;
1127
+ // LDA #$01 (clears Z, clears N); LDX #$00; TXS
1128
+ f.loadAndReset(0x0400, {0xA9, 0x01, 0xA2, 0x00, 0x9A});
1129
+ test::runInstructions(*f.cpu, 3);
1130
+ // TXS does not modify N/Z flags (unlike other transfers)
1131
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_Z) == true); // Set by LDX #$00
1132
+ }
1133
+ }
1134
+
1135
+ // ============================================================================
1136
+ // NOP and BRK
1137
+ // ============================================================================
1138
+
1139
+ TEST_CASE("NOP instruction", "[cpu][misc]") {
1140
+
1141
+ SECTION("NOP does nothing") {
1142
+ test::CPUTestFixture f;
1143
+ f.loadAndReset(0x0400, {0xA9, 0x42, 0xEA, 0xEA, 0xEA}); // LDA #$42; NOP; NOP; NOP
1144
+ test::runInstructions(*f.cpu, 4);
1145
+ REQUIRE(f.cpu->getA() == 0x42);
1146
+ REQUIRE(f.cpu->getPC() == 0x0405);
1147
+ }
1148
+ }
1149
+
1150
+ TEST_CASE("BRK instruction", "[cpu][interrupt]") {
1151
+
1152
+ SECTION("BRK pushes PC+2 and P, jumps to IRQ vector") {
1153
+ test::CPUTestFixture f;
1154
+ f.mem.setIRQVector(0x1000);
1155
+ f.mem[0x1000] = 0xA9; // LDA #$77 at IRQ handler
1156
+ f.mem[0x1001] = 0x77;
1157
+ f.loadAndReset(0x0400, {0x00, 0x00}); // BRK; padding byte
1158
+ f.cpu->setFlag(a2e::FLAG_I, false); // Clear I flag to start
1159
+ f.cpu->executeInstruction(); // BRK
1160
+ REQUIRE(f.cpu->getPC() == 0x1000);
1161
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_I) == true); // I flag set by BRK
1162
+ }
1163
+ }
1164
+
1165
+ // ============================================================================
1166
+ // 65C02 Specific Instructions
1167
+ // ============================================================================
1168
+
1169
+ TEST_CASE("65C02 STZ instruction", "[cpu][65c02]") {
1170
+
1171
+ SECTION("STZ zero page stores zero") {
1172
+ test::CPUTestFixture f;
1173
+ f.mem[0x10] = 0xFF;
1174
+ f.loadAndReset(0x0400, {0x64, 0x10}); // STZ $10
1175
+ f.cpu->executeInstruction();
1176
+ REQUIRE(f.mem[0x10] == 0x00);
1177
+ }
1178
+
1179
+ SECTION("STZ absolute stores zero") {
1180
+ test::CPUTestFixture f;
1181
+ f.mem[0x2000] = 0xFF;
1182
+ f.loadAndReset(0x0400, {0x9C, 0x00, 0x20}); // STZ $2000
1183
+ f.cpu->executeInstruction();
1184
+ REQUIRE(f.mem[0x2000] == 0x00);
1185
+ }
1186
+
1187
+ SECTION("STZ zero page,X") {
1188
+ test::CPUTestFixture f;
1189
+ f.mem[0x15] = 0xFF;
1190
+ f.loadAndReset(0x0400, {0xA2, 0x05, 0x74, 0x10}); // LDX #$05; STZ $10,X
1191
+ test::runInstructions(*f.cpu, 2);
1192
+ REQUIRE(f.mem[0x15] == 0x00);
1193
+ }
1194
+
1195
+ SECTION("STZ absolute,X") {
1196
+ test::CPUTestFixture f;
1197
+ f.mem[0x2005] = 0xFF;
1198
+ f.loadAndReset(0x0400, {0xA2, 0x05, 0x9E, 0x00, 0x20}); // LDX #$05; STZ $2000,X
1199
+ test::runInstructions(*f.cpu, 2);
1200
+ REQUIRE(f.mem[0x2005] == 0x00);
1201
+ }
1202
+ }
1203
+
1204
+ TEST_CASE("65C02 TRB instruction", "[cpu][65c02]") {
1205
+
1206
+ SECTION("TRB zero page resets tested bits") {
1207
+ test::CPUTestFixture f;
1208
+ f.mem[0x10] = 0xFF;
1209
+ // LDA #$0F; TRB $10 -> mem = mem AND NOT(A) = 0xFF & 0xF0 = 0xF0
1210
+ f.loadAndReset(0x0400, {0xA9, 0x0F, 0x14, 0x10});
1211
+ test::runInstructions(*f.cpu, 2);
1212
+ REQUIRE(f.mem[0x10] == 0xF0);
1213
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_Z) == false); // A AND mem(original) = 0x0F != 0
1214
+ }
1215
+
1216
+ SECTION("TRB sets Z when no bits in common") {
1217
+ test::CPUTestFixture f;
1218
+ f.mem[0x10] = 0xF0;
1219
+ f.loadAndReset(0x0400, {0xA9, 0x0F, 0x14, 0x10}); // LDA #$0F; TRB $10
1220
+ test::runInstructions(*f.cpu, 2);
1221
+ REQUIRE(f.mem[0x10] == 0xF0); // no bits to clear
1222
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_Z) == true); // A AND mem = 0x00
1223
+ }
1224
+ }
1225
+
1226
+ TEST_CASE("65C02 TSB instruction", "[cpu][65c02]") {
1227
+
1228
+ SECTION("TSB zero page sets tested bits") {
1229
+ test::CPUTestFixture f;
1230
+ f.mem[0x10] = 0xF0;
1231
+ // LDA #$0F; TSB $10 -> mem = mem OR A = 0xF0 | 0x0F = 0xFF
1232
+ f.loadAndReset(0x0400, {0xA9, 0x0F, 0x04, 0x10});
1233
+ test::runInstructions(*f.cpu, 2);
1234
+ REQUIRE(f.mem[0x10] == 0xFF);
1235
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_Z) == true); // A AND mem(original) = 0x0F & 0xF0 = 0x00
1236
+ }
1237
+
1238
+ SECTION("TSB clears Z when bits in common") {
1239
+ test::CPUTestFixture f;
1240
+ f.mem[0x10] = 0xFF;
1241
+ f.loadAndReset(0x0400, {0xA9, 0x0F, 0x04, 0x10}); // LDA #$0F; TSB $10
1242
+ test::runInstructions(*f.cpu, 2);
1243
+ REQUIRE(f.mem[0x10] == 0xFF);
1244
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_Z) == false); // A AND mem = 0x0F != 0
1245
+ }
1246
+ }
1247
+
1248
+ TEST_CASE("65C02 INC A / DEC A", "[cpu][65c02]") {
1249
+
1250
+ SECTION("INC A increments accumulator") {
1251
+ test::CPUTestFixture f;
1252
+ f.loadAndReset(0x0400, {0xA9, 0x41, 0x1A}); // LDA #$41; INC A
1253
+ test::runInstructions(*f.cpu, 2);
1254
+ REQUIRE(f.cpu->getA() == 0x42);
1255
+ }
1256
+
1257
+ SECTION("INC A wraps from FF to 00") {
1258
+ test::CPUTestFixture f;
1259
+ f.loadAndReset(0x0400, {0xA9, 0xFF, 0x1A}); // LDA #$FF; INC A
1260
+ test::runInstructions(*f.cpu, 2);
1261
+ REQUIRE(f.cpu->getA() == 0x00);
1262
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_Z) == true);
1263
+ }
1264
+
1265
+ SECTION("DEC A decrements accumulator") {
1266
+ test::CPUTestFixture f;
1267
+ f.loadAndReset(0x0400, {0xA9, 0x42, 0x3A}); // LDA #$42; DEC A
1268
+ test::runInstructions(*f.cpu, 2);
1269
+ REQUIRE(f.cpu->getA() == 0x41);
1270
+ }
1271
+
1272
+ SECTION("DEC A wraps from 00 to FF") {
1273
+ test::CPUTestFixture f;
1274
+ f.loadAndReset(0x0400, {0xA9, 0x00, 0x3A}); // LDA #$00; DEC A
1275
+ test::runInstructions(*f.cpu, 2);
1276
+ REQUIRE(f.cpu->getA() == 0xFF);
1277
+ REQUIRE(f.cpu->getFlag(a2e::FLAG_N) == true);
1278
+ }
1279
+ }
1280
+
1281
+ TEST_CASE("65C02 (zp) indirect addressing", "[cpu][65c02]") {
1282
+
1283
+ SECTION("LDA (zp) loads from indirect zero page address") {
1284
+ test::CPUTestFixture f;
1285
+ f.mem[0x10] = 0x00;
1286
+ f.mem[0x11] = 0x20;
1287
+ f.mem[0x2000] = 0xBB;
1288
+ f.loadAndReset(0x0400, {0xB2, 0x10}); // LDA ($10)
1289
+ f.cpu->executeInstruction();
1290
+ REQUIRE(f.cpu->getA() == 0xBB);
1291
+ }
1292
+
1293
+ SECTION("STA (zp) stores to indirect zero page address") {
1294
+ test::CPUTestFixture f;
1295
+ f.mem[0x10] = 0x00;
1296
+ f.mem[0x11] = 0x20;
1297
+ f.loadAndReset(0x0400, {0xA9, 0xCC, 0x92, 0x10}); // LDA #$CC; STA ($10)
1298
+ test::runInstructions(*f.cpu, 2);
1299
+ REQUIRE(f.mem[0x2000] == 0xCC);
1300
+ }
1301
+ }