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,319 @@
1
+ /*
2
+ * test_video.cpp - Unit tests for Video output generation
3
+ *
4
+ * Tests the video subsystem including mode detection, framebuffer
5
+ * management, display options, page switching, text rendering,
6
+ * and dirty flag management.
7
+ */
8
+
9
+ #define CATCH_CONFIG_MAIN
10
+ #include "catch.hpp"
11
+
12
+ #include "video/video.hpp"
13
+ #include "mmu/mmu.hpp"
14
+ #include "roms.cpp"
15
+
16
+ using namespace a2e;
17
+
18
+ // Helper: create an MMU with ROMs loaded and a Video instance
19
+ struct VideoTestFixture {
20
+ MMU mmu;
21
+ std::unique_ptr<Video> video;
22
+
23
+ VideoTestFixture() {
24
+ mmu.loadROM(roms::ROM_SYSTEM, roms::ROM_SYSTEM_SIZE,
25
+ roms::ROM_CHAR, roms::ROM_CHAR_SIZE);
26
+ video = std::make_unique<Video>(mmu);
27
+ }
28
+ };
29
+
30
+ // ============================================================================
31
+ // Default state
32
+ // ============================================================================
33
+
34
+ TEST_CASE("Video default mode is TEXT_40", "[video][mode]") {
35
+ VideoTestFixture f;
36
+
37
+ // Default soft switches: text=true, col80=false
38
+ CHECK(f.video->getCurrentMode() == VideoMode::TEXT_40);
39
+ }
40
+
41
+ // ============================================================================
42
+ // Framebuffer
43
+ // ============================================================================
44
+
45
+ TEST_CASE("Video getFramebufferSize returns 860160", "[video][framebuffer]") {
46
+ // 560 * 384 * 4 (RGBA) = 860160
47
+ CHECK(Video::getFramebufferSize() == 860160);
48
+ }
49
+
50
+ TEST_CASE("Video getFramebuffer returns non-null pointer", "[video][framebuffer]") {
51
+ VideoTestFixture f;
52
+
53
+ CHECK(f.video->getFramebuffer() != nullptr);
54
+ }
55
+
56
+ TEST_CASE("Video const getFramebuffer returns non-null pointer", "[video][framebuffer]") {
57
+ VideoTestFixture f;
58
+
59
+ const Video& constVideo = *f.video;
60
+ CHECK(constVideo.getFramebuffer() != nullptr);
61
+ }
62
+
63
+ // ============================================================================
64
+ // forceRenderFrame
65
+ // ============================================================================
66
+
67
+ TEST_CASE("Video forceRenderFrame does not crash", "[video][render]") {
68
+ VideoTestFixture f;
69
+
70
+ // Should complete without error
71
+ f.video->forceRenderFrame();
72
+ }
73
+
74
+ TEST_CASE("Video forceRenderFrame sets frame dirty", "[video][render]") {
75
+ VideoTestFixture f;
76
+
77
+ f.video->clearFrameDirty();
78
+ CHECK_FALSE(f.video->isFrameDirty());
79
+
80
+ f.video->forceRenderFrame();
81
+ // After rendering, the dirty flag should reflect the render occurred
82
+ // (forceRenderFrame renders regardless of dirty state)
83
+ }
84
+
85
+ // ============================================================================
86
+ // Mode detection via soft switch toggling
87
+ // ============================================================================
88
+
89
+ TEST_CASE("Video mode: text=true, col80=false -> TEXT_40", "[video][mode]") {
90
+ VideoTestFixture f;
91
+
92
+ // Ensure text mode
93
+ f.mmu.write(0xC051, 0); // TEXT on (write also toggles)
94
+ f.mmu.write(0xC00C, 0); // 80COL off
95
+
96
+ CHECK(f.video->getCurrentMode() == VideoMode::TEXT_40);
97
+ }
98
+
99
+ TEST_CASE("Video mode: text=true, col80=true -> TEXT_80", "[video][mode]") {
100
+ VideoTestFixture f;
101
+
102
+ f.mmu.write(0xC051, 0); // TEXT on
103
+ f.mmu.write(0xC00D, 0); // 80COL on
104
+
105
+ CHECK(f.video->getCurrentMode() == VideoMode::TEXT_80);
106
+ }
107
+
108
+ TEST_CASE("Video mode: text=false, hires=false -> LORES", "[video][mode]") {
109
+ VideoTestFixture f;
110
+
111
+ f.mmu.read(0xC050); // TEXT off (graphics mode)
112
+ f.mmu.read(0xC056); // HIRES off
113
+
114
+ CHECK(f.video->getCurrentMode() == VideoMode::LORES);
115
+ }
116
+
117
+ TEST_CASE("Video mode: text=false, hires=true -> HIRES", "[video][mode]") {
118
+ VideoTestFixture f;
119
+
120
+ f.mmu.read(0xC050); // TEXT off (graphics mode)
121
+ f.mmu.read(0xC057); // HIRES on
122
+
123
+ CHECK(f.video->getCurrentMode() == VideoMode::HIRES);
124
+ }
125
+
126
+ TEST_CASE("Video mode: DOUBLE_LORES requires AN3 off + 80COL", "[video][mode]") {
127
+ VideoTestFixture f;
128
+
129
+ f.mmu.read(0xC050); // TEXT off (graphics)
130
+ f.mmu.read(0xC056); // HIRES off
131
+ f.mmu.write(0xC00D, 0); // 80COL on
132
+ f.mmu.read(0xC05E); // AN3 off
133
+
134
+ CHECK(f.video->getCurrentMode() == VideoMode::DOUBLE_LORES);
135
+ }
136
+
137
+ TEST_CASE("Video mode: DOUBLE_HIRES requires AN3 off + 80COL + HIRES", "[video][mode]") {
138
+ VideoTestFixture f;
139
+
140
+ f.mmu.read(0xC050); // TEXT off (graphics)
141
+ f.mmu.read(0xC057); // HIRES on
142
+ f.mmu.write(0xC00D, 0); // 80COL on
143
+ f.mmu.read(0xC05E); // AN3 off
144
+
145
+ CHECK(f.video->getCurrentMode() == VideoMode::DOUBLE_HIRES);
146
+ }
147
+
148
+ // ============================================================================
149
+ // Display options: monochrome
150
+ // ============================================================================
151
+
152
+ TEST_CASE("Video setMonochrome/isMonochrome toggle", "[video][options]") {
153
+ VideoTestFixture f;
154
+
155
+ CHECK_FALSE(f.video->isMonochrome());
156
+
157
+ f.video->setMonochrome(true);
158
+ CHECK(f.video->isMonochrome());
159
+
160
+ f.video->setMonochrome(false);
161
+ CHECK_FALSE(f.video->isMonochrome());
162
+ }
163
+
164
+ // ============================================================================
165
+ // Display options: green phosphor
166
+ // ============================================================================
167
+
168
+ TEST_CASE("Video setGreenPhosphor/isGreenPhosphor toggle", "[video][options]") {
169
+ VideoTestFixture f;
170
+
171
+ CHECK_FALSE(f.video->isGreenPhosphor());
172
+
173
+ f.video->setGreenPhosphor(true);
174
+ CHECK(f.video->isGreenPhosphor());
175
+
176
+ f.video->setGreenPhosphor(false);
177
+ CHECK_FALSE(f.video->isGreenPhosphor());
178
+ }
179
+
180
+ // ============================================================================
181
+ // Display options: UK character set
182
+ // ============================================================================
183
+
184
+ TEST_CASE("Video setUKCharacterSet/isUKCharacterSet toggle", "[video][options]") {
185
+ VideoTestFixture f;
186
+
187
+ CHECK_FALSE(f.video->isUKCharacterSet());
188
+
189
+ f.video->setUKCharacterSet(true);
190
+ CHECK(f.video->isUKCharacterSet());
191
+
192
+ f.video->setUKCharacterSet(false);
193
+ CHECK_FALSE(f.video->isUKCharacterSet());
194
+ }
195
+
196
+ // ============================================================================
197
+ // Page switching
198
+ // ============================================================================
199
+
200
+ TEST_CASE("Video page switching via $C055/$C054", "[video][page]") {
201
+ VideoTestFixture f;
202
+
203
+ // Default: page1
204
+ CHECK_FALSE(f.mmu.getSoftSwitches().page2);
205
+
206
+ // Switch to page 2
207
+ f.mmu.write(0xC055, 0);
208
+ CHECK(f.mmu.getSoftSwitches().page2);
209
+
210
+ // Switch back to page 1
211
+ f.mmu.write(0xC054, 0);
212
+ CHECK_FALSE(f.mmu.getSoftSwitches().page2);
213
+ }
214
+
215
+ // ============================================================================
216
+ // Text rendering
217
+ // ============================================================================
218
+
219
+ TEST_CASE("Video text rendering: writing ASCII to $0400 produces non-zero pixels", "[video][render]") {
220
+ VideoTestFixture f;
221
+
222
+ // Fill text page 1 with a visible character (inverse '@' = 0x00, normal 'A' = 0xC1)
223
+ // Use normal 'A' character (0xC1 in Apple II encoding)
224
+ for (int i = 0; i < 40; ++i) {
225
+ f.mmu.write(0x0400 + i, 0xC1); // 'A' in normal video
226
+ }
227
+
228
+ // Render the frame
229
+ f.video->forceRenderFrame();
230
+
231
+ // Check that framebuffer has some non-zero pixels in the first row area
232
+ const uint8_t* fb = f.video->getFramebuffer();
233
+ bool hasNonZero = false;
234
+ // Check the first few scanlines (each text row is 16 pixels tall in 560x384)
235
+ // 384 / 24 = 16 pixels per text row
236
+ for (size_t i = 0; i < 560 * 16 * 4; ++i) {
237
+ if (fb[i] != 0) {
238
+ hasNonZero = true;
239
+ break;
240
+ }
241
+ }
242
+
243
+ CHECK(hasNonZero);
244
+ }
245
+
246
+ TEST_CASE("Video text rendering: blank screen has predictable output", "[video][render]") {
247
+ VideoTestFixture f;
248
+
249
+ // Text page is all zeros (inverse '@' on Apple IIe)
250
+ // Even with all zeros, the character ROM should produce some pixel output
251
+ f.video->forceRenderFrame();
252
+
253
+ const uint8_t* fb = f.video->getFramebuffer();
254
+ // Framebuffer should exist and have been written to
255
+ CHECK(fb != nullptr);
256
+ }
257
+
258
+ // ============================================================================
259
+ // Frame dirty flag
260
+ // ============================================================================
261
+
262
+ TEST_CASE("Video isFrameDirty/clearFrameDirty/setFrameDirty", "[video][dirty]") {
263
+ VideoTestFixture f;
264
+
265
+ // Initially dirty
266
+ CHECK(f.video->isFrameDirty());
267
+
268
+ f.video->clearFrameDirty();
269
+ CHECK_FALSE(f.video->isFrameDirty());
270
+
271
+ f.video->setFrameDirty();
272
+ CHECK(f.video->isFrameDirty());
273
+ }
274
+
275
+ // ============================================================================
276
+ // Mixed mode
277
+ // ============================================================================
278
+
279
+ TEST_CASE("Video mixed mode: $C053 sets mixed, $C052 clears", "[video][mode]") {
280
+ VideoTestFixture f;
281
+
282
+ // Default: mixed off
283
+ CHECK_FALSE(f.mmu.getSoftSwitches().mixed);
284
+
285
+ f.mmu.read(0xC053); // MIXSET
286
+ CHECK(f.mmu.getSoftSwitches().mixed);
287
+
288
+ f.mmu.read(0xC052); // MIXCLR
289
+ CHECK_FALSE(f.mmu.getSoftSwitches().mixed);
290
+ }
291
+
292
+ // ============================================================================
293
+ // Mode transitions
294
+ // ============================================================================
295
+
296
+ TEST_CASE("Video mode transitions work correctly", "[video][mode]") {
297
+ VideoTestFixture f;
298
+
299
+ // Start in TEXT_40
300
+ CHECK(f.video->getCurrentMode() == VideoMode::TEXT_40);
301
+
302
+ // Switch to HIRES
303
+ f.mmu.read(0xC050); // TEXT off
304
+ f.mmu.read(0xC057); // HIRES on
305
+ CHECK(f.video->getCurrentMode() == VideoMode::HIRES);
306
+
307
+ // Switch to LORES
308
+ f.mmu.read(0xC056); // HIRES off
309
+ CHECK(f.video->getCurrentMode() == VideoMode::LORES);
310
+
311
+ // Switch to TEXT_80
312
+ f.mmu.read(0xC051); // TEXT on
313
+ f.mmu.write(0xC00D, 0); // 80COL on
314
+ CHECK(f.video->getCurrentMode() == VideoMode::TEXT_80);
315
+
316
+ // Back to TEXT_40
317
+ f.mmu.write(0xC00C, 0); // 80COL off
318
+ CHECK(f.video->getCurrentMode() == VideoMode::TEXT_40);
319
+ }
@@ -0,0 +1,257 @@
1
+ /*
2
+ * test_woz_disk_image.cpp - Unit tests for WozDiskImage
3
+ *
4
+ * Tests the WOZ 1.0/2.0 bit-accurate disk image format including:
5
+ * - Loading with invalid data
6
+ * - Creating blank WOZ2 images
7
+ * - Bit read/write operations
8
+ * - Track count and head positioning
9
+ * - Format and metadata queries
10
+ */
11
+
12
+ #define CATCH_CONFIG_MAIN
13
+ #include "catch.hpp"
14
+
15
+ #include "woz_disk_image.hpp"
16
+
17
+ #include <cstring>
18
+ #include <vector>
19
+
20
+ using namespace a2e;
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Loading with invalid data
24
+ // ---------------------------------------------------------------------------
25
+
26
+ TEST_CASE("WozDiskImage load with empty data returns false", "[woz]") {
27
+ WozDiskImage img;
28
+ bool ok = img.load(nullptr, 0, "empty.woz");
29
+ REQUIRE_FALSE(ok);
30
+ REQUIRE_FALSE(img.isLoaded());
31
+ }
32
+
33
+ TEST_CASE("WozDiskImage load with too-small data returns false", "[woz]") {
34
+ WozDiskImage img;
35
+ std::vector<uint8_t> tiny(10, 0x00);
36
+ bool ok = img.load(tiny.data(), tiny.size(), "tiny.woz");
37
+ REQUIRE_FALSE(ok);
38
+ REQUIRE_FALSE(img.isLoaded());
39
+ }
40
+
41
+ TEST_CASE("WozDiskImage load with random garbage returns false", "[woz]") {
42
+ WozDiskImage img;
43
+ std::vector<uint8_t> garbage(8192, 0xAA);
44
+ bool ok = img.load(garbage.data(), garbage.size(), "garbage.woz");
45
+ REQUIRE_FALSE(ok);
46
+ REQUIRE_FALSE(img.isLoaded());
47
+ }
48
+
49
+ TEST_CASE("WozDiskImage load with wrong signature returns false", "[woz]") {
50
+ WozDiskImage img;
51
+ // Build a buffer that looks like a file header but has wrong signature
52
+ std::vector<uint8_t> badSig(256, 0x00);
53
+ badSig[0] = 'W'; badSig[1] = 'O'; badSig[2] = 'Z'; badSig[3] = '9';
54
+ badSig[4] = 0xFF;
55
+ badSig[5] = 0x0A; badSig[6] = 0x0D; badSig[7] = 0x0A;
56
+
57
+ bool ok = img.load(badSig.data(), badSig.size(), "badsig.woz");
58
+ REQUIRE_FALSE(ok);
59
+ }
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // Creating blank images
63
+ // ---------------------------------------------------------------------------
64
+
65
+ TEST_CASE("WozDiskImage createBlank creates a valid image", "[woz]") {
66
+ WozDiskImage img;
67
+ img.createBlank();
68
+
69
+ REQUIRE(img.isLoaded());
70
+ REQUIRE(img.getFormat() == DiskImage::Format::WOZ2);
71
+ }
72
+
73
+ TEST_CASE("WozDiskImage createBlank has 35 tracks", "[woz]") {
74
+ WozDiskImage img;
75
+ img.createBlank();
76
+
77
+ REQUIRE(img.getTrackCount() == 35);
78
+ }
79
+
80
+ TEST_CASE("WozDiskImage createBlank starts at track 0", "[woz]") {
81
+ WozDiskImage img;
82
+ img.createBlank();
83
+
84
+ REQUIRE(img.getTrack() == 0);
85
+ REQUIRE(img.getQuarterTrack() == 0);
86
+ }
87
+
88
+ TEST_CASE("WozDiskImage createBlank reports 5.25 inch disk type", "[woz]") {
89
+ WozDiskImage img;
90
+ img.createBlank();
91
+
92
+ REQUIRE(img.getDiskType() == 1); // 1 = 5.25"
93
+ }
94
+
95
+ TEST_CASE("WozDiskImage createBlank has valid disk type string", "[woz]") {
96
+ WozDiskImage img;
97
+ img.createBlank();
98
+
99
+ std::string typeStr = img.getDiskTypeString();
100
+ REQUIRE_FALSE(typeStr.empty());
101
+ }
102
+
103
+ TEST_CASE("WozDiskImage createBlank has default bit timing", "[woz]") {
104
+ WozDiskImage img;
105
+ img.createBlank();
106
+
107
+ // Default optimal bit timing is 32 (= 4 microseconds per bit)
108
+ REQUIRE(img.getOptimalBitTiming() == 32);
109
+ }
110
+
111
+ TEST_CASE("WozDiskImage createBlank is not write-protected", "[woz]") {
112
+ WozDiskImage img;
113
+ img.createBlank();
114
+
115
+ REQUIRE_FALSE(img.isWriteProtected());
116
+ }
117
+
118
+ TEST_CASE("WozDiskImage createBlank is marked as modified", "[woz]") {
119
+ WozDiskImage img;
120
+ img.createBlank();
121
+
122
+ // createBlank marks the image as modified so it will be saved
123
+ REQUIRE(img.isModified());
124
+ }
125
+
126
+ TEST_CASE("WozDiskImage createBlank hasData returns true", "[woz]") {
127
+ WozDiskImage img;
128
+ img.createBlank();
129
+
130
+ REQUIRE(img.hasData());
131
+ }
132
+
133
+ // ---------------------------------------------------------------------------
134
+ // Bit read/write operations
135
+ // ---------------------------------------------------------------------------
136
+
137
+ TEST_CASE("WozDiskImage readBit on blank image returns 0 or 1", "[woz]") {
138
+ WozDiskImage img;
139
+ img.createBlank();
140
+
141
+ // Blank disk is filled with sync bytes, so bits should be valid
142
+ uint8_t bit = img.readBit();
143
+ REQUIRE((bit == 0 || bit == 1));
144
+ }
145
+
146
+ TEST_CASE("WozDiskImage readBit advances position", "[woz]") {
147
+ WozDiskImage img;
148
+ img.createBlank();
149
+
150
+ size_t pos1 = img.getCurrentNibblePosition();
151
+ // Read enough bits to shift nibble position
152
+ for (int i = 0; i < 8; ++i) {
153
+ img.readBit();
154
+ }
155
+ size_t pos2 = img.getCurrentNibblePosition();
156
+ REQUIRE(pos2 > pos1);
157
+ }
158
+
159
+ TEST_CASE("WozDiskImage writeBit modifies the image", "[woz]") {
160
+ WozDiskImage img;
161
+ img.createBlank();
162
+
163
+ // Write some bits
164
+ img.writeBit(1);
165
+ img.writeBit(0);
166
+ img.writeBit(1);
167
+ img.writeBit(1);
168
+
169
+ REQUIRE(img.isModified());
170
+ }
171
+
172
+ TEST_CASE("WozDiskImage readNibble on blank image returns sync-like nibble", "[woz]") {
173
+ WozDiskImage img;
174
+ img.createBlank();
175
+
176
+ // Blank disk is filled with sync bytes (0xFF typically)
177
+ uint8_t nibble = img.readNibble();
178
+ // Valid nibbles have bit 7 set
179
+ REQUIRE((nibble & 0x80) != 0);
180
+ }
181
+
182
+ // ---------------------------------------------------------------------------
183
+ // Head positioning
184
+ // ---------------------------------------------------------------------------
185
+
186
+ TEST_CASE("WozDiskImage setQuarterTrack changes position", "[woz]") {
187
+ WozDiskImage img;
188
+ img.createBlank();
189
+
190
+ img.setQuarterTrack(12);
191
+ REQUIRE(img.getQuarterTrack() == 12);
192
+ REQUIRE(img.getTrack() == 3);
193
+ }
194
+
195
+ TEST_CASE("WozDiskImage setPhase stepping moves head", "[woz]") {
196
+ WozDiskImage img;
197
+ img.createBlank();
198
+
199
+ REQUIRE(img.getQuarterTrack() == 0);
200
+
201
+ // Step outward: phase 0 on, phase 1 on, phase 0 off
202
+ img.setPhase(0, true);
203
+ img.setPhase(1, true);
204
+ img.setPhase(0, false);
205
+
206
+ REQUIRE(img.getQuarterTrack() > 0);
207
+ }
208
+
209
+ // ---------------------------------------------------------------------------
210
+ // Reset state
211
+ // ---------------------------------------------------------------------------
212
+
213
+ TEST_CASE("WozDiskImage resetState resets head position", "[woz]") {
214
+ WozDiskImage img;
215
+ img.createBlank();
216
+
217
+ img.setQuarterTrack(20);
218
+ REQUIRE(img.getQuarterTrack() == 20);
219
+
220
+ img.resetState();
221
+ REQUIRE(img.getQuarterTrack() == 0);
222
+ }
223
+
224
+ // ---------------------------------------------------------------------------
225
+ // Format name
226
+ // ---------------------------------------------------------------------------
227
+
228
+ TEST_CASE("WozDiskImage getFormatName returns non-empty string", "[woz]") {
229
+ WozDiskImage img;
230
+ img.createBlank();
231
+
232
+ std::string name = img.getFormatName();
233
+ REQUIRE_FALSE(name.empty());
234
+ }
235
+
236
+ // ---------------------------------------------------------------------------
237
+ // Move semantics
238
+ // ---------------------------------------------------------------------------
239
+
240
+ TEST_CASE("WozDiskImage is move-constructible", "[woz]") {
241
+ WozDiskImage img1;
242
+ img1.createBlank();
243
+ REQUIRE(img1.isLoaded());
244
+
245
+ WozDiskImage img2(std::move(img1));
246
+ REQUIRE(img2.isLoaded());
247
+ REQUIRE(img2.getTrackCount() == 35);
248
+ }
249
+
250
+ TEST_CASE("WozDiskImage is move-assignable", "[woz]") {
251
+ WozDiskImage img1;
252
+ img1.createBlank();
253
+
254
+ WozDiskImage img2;
255
+ img2 = std::move(img1);
256
+ REQUIRE(img2.isLoaded());
257
+ }
package/vite.config.js ADDED
@@ -0,0 +1,96 @@
1
+ import { defineConfig } from "vite";
2
+ import { resolve } from "path";
3
+ import { copyFileSync, mkdirSync } from "fs";
4
+
5
+ // Plugin to copy audio worklet file (can't be bundled)
6
+ const copyAudioWorklet = () => ({
7
+ name: "copy-audio-worklet",
8
+ writeBundle() {
9
+ mkdirSync(resolve(__dirname, "dist"), { recursive: true });
10
+ copyFileSync(
11
+ resolve(__dirname, "src/js/audio/audio-worklet.js"),
12
+ resolve(__dirname, "dist/audio-worklet.js"),
13
+ );
14
+ },
15
+ });
16
+
17
+ export default defineConfig({
18
+ root: "public",
19
+ publicDir: "../public",
20
+
21
+ server: {
22
+ port: 3000,
23
+ open: true,
24
+ headers: {
25
+ // Required for SharedArrayBuffer (if needed for AudioWorklet)
26
+ "Cross-Origin-Opener-Policy": "same-origin",
27
+ "Cross-Origin-Embedder-Policy": "require-corp",
28
+ "Cache-Control": "no-store",
29
+ },
30
+ },
31
+
32
+ build: {
33
+ outDir: "../dist",
34
+ emptyOutDir: true,
35
+ rollupOptions: {
36
+ input: {
37
+ main: resolve(__dirname, "public/index.html"),
38
+ },
39
+ output: {
40
+ manualChunks: {
41
+ debug: [
42
+ "/src/js/debug/cpu-debugger-window.js",
43
+ "/src/js/debug/memory-browser-window.js",
44
+ "/src/js/debug/memory-heat-map-window.js",
45
+ "/src/js/debug/memory-map-window.js",
46
+ "/src/js/debug/stack-viewer-window.js",
47
+ "/src/js/debug/zero-page-watch-window.js",
48
+ "/src/js/debug/soft-switch-window.js",
49
+ "/src/js/debug/mockingboard-window.js",
50
+ "/src/js/debug/mouse-card-window.js",
51
+ "/src/js/debug/basic-program-window.js",
52
+ "/src/js/debug/rule-builder-window.js",
53
+ "/src/js/debug/assembler-editor-window.js",
54
+ ],
55
+ display: [
56
+ "/src/js/display/index.js",
57
+ "/src/js/display/webgl-renderer.js",
58
+ "/src/js/display/display-settings-window.js",
59
+ "/src/js/display/screen-window.js",
60
+ ],
61
+ "disk-manager": [
62
+ "/src/js/disk-manager/index.js",
63
+ "/src/js/disk-manager/disk-operations.js",
64
+ "/src/js/disk-manager/disk-persistence.js",
65
+ "/src/js/disk-manager/disk-surface-renderer.js",
66
+ "/src/js/disk-manager/disk-drives-window.js",
67
+ "/src/js/disk-manager/drive-sounds.js",
68
+ ],
69
+ "file-explorer": [
70
+ "/src/js/file-explorer/index.js",
71
+ "/src/js/file-explorer/dos33.js",
72
+ "/src/js/file-explorer/prodos.js",
73
+ "/src/js/file-explorer/disassembler.js",
74
+ "/src/js/file-explorer/file-viewer.js",
75
+ "/src/js/file-explorer/utils.js",
76
+ ],
77
+ },
78
+ },
79
+ },
80
+ },
81
+
82
+ resolve: {
83
+ alias: {
84
+ "/src": resolve(__dirname, "src"),
85
+ },
86
+ },
87
+
88
+ // Handle WASM files
89
+ assetsInclude: ["**/*.wasm"],
90
+
91
+ optimizeDeps: {
92
+ exclude: ["a2e.js"],
93
+ },
94
+
95
+ plugins: [copyAudioWorklet()],
96
+ });