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,574 @@
1
+ /*
2
+ * disassembler.js - 65C02 disassembler for file viewer
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ /**
9
+ * 6502 Disassembler with Virtual Scrolling
10
+ * Uses the C++ disassembler from the emulation core for structured data
11
+ * JavaScript handles all rendering/formatting
12
+ */
13
+
14
+ // Reference to the WASM module
15
+ let wasmModule = null;
16
+
17
+ // Virtual scroll configuration
18
+ const LINE_HEIGHT = 18;
19
+ const BUFFER_LINES = 20;
20
+
21
+ // Addressing modes (must match C++ enum)
22
+ const AddrMode = {
23
+ IMP: 0, ACC: 1, IMM: 2, ZP: 3, ZPX: 4, ZPY: 5,
24
+ ABS: 6, ABX: 7, ABY: 8, IND: 9, IZX: 10, IZY: 11,
25
+ REL: 12, ZPI: 13, AIX: 14, ZPR: 15
26
+ };
27
+
28
+ // Instruction categories (must match C++ enum)
29
+ const Category = {
30
+ BRANCH: 0, LOAD: 1, MATH: 2, STACK: 3, FLAG: 4, UNKNOWN: 5
31
+ };
32
+
33
+ // Category to CSS class mapping
34
+ const CATEGORY_CLASSES = {
35
+ [Category.BRANCH]: 'dis-branch',
36
+ [Category.LOAD]: 'dis-load',
37
+ [Category.MATH]: 'dis-math',
38
+ [Category.STACK]: 'dis-stack',
39
+ [Category.FLAG]: 'dis-flag',
40
+ [Category.UNKNOWN]: 'dis-unknown'
41
+ };
42
+
43
+ /**
44
+ * Set the WASM module
45
+ */
46
+ export function setWasmModule(module) {
47
+ wasmModule = module;
48
+ }
49
+
50
+ /**
51
+ * Format a byte as 2-digit hex
52
+ */
53
+ function hexByte(b) {
54
+ return b.toString(16).toUpperCase().padStart(2, '0');
55
+ }
56
+
57
+ /**
58
+ * Format a word as 4-digit hex
59
+ */
60
+ function hexWord(w) {
61
+ return w.toString(16).toUpperCase().padStart(4, '0');
62
+ }
63
+
64
+ /**
65
+ * Format a clickable target address for branches/jumps
66
+ */
67
+ function formatClickableTarget(target) {
68
+ return `<span class="dis-target dis-clickable" data-target="${target}">$${hexWord(target)}</span>`;
69
+ }
70
+
71
+ /**
72
+ * Format operand based on addressing mode
73
+ * isBranch: if true, make targets clickable for navigation
74
+ */
75
+ function formatOperand(mode, operand1, operand2, target, isBranch = false) {
76
+ switch (mode) {
77
+ case AddrMode.IMP:
78
+ return '';
79
+ case AddrMode.ACC:
80
+ return '<span class="dis-register">A</span>';
81
+ case AddrMode.IMM:
82
+ return `<span class="dis-punct">#$</span><span class="dis-immediate">${hexByte(operand1)}</span>`;
83
+ case AddrMode.ZP:
84
+ return `<span class="dis-punct">$</span><span class="dis-address">${hexByte(operand1)}</span>`;
85
+ case AddrMode.ZPX:
86
+ return `<span class="dis-punct">$</span><span class="dis-address">${hexByte(operand1)}</span><span class="dis-punct">,</span><span class="dis-register">X</span>`;
87
+ case AddrMode.ZPY:
88
+ return `<span class="dis-punct">$</span><span class="dis-address">${hexByte(operand1)}</span><span class="dis-punct">,</span><span class="dis-register">Y</span>`;
89
+ case AddrMode.ABS:
90
+ if (isBranch) {
91
+ return formatClickableTarget(target);
92
+ }
93
+ return `<span class="dis-punct">$</span><span class="dis-address">${hexWord(target)}</span>`;
94
+ case AddrMode.ABX:
95
+ return `<span class="dis-punct">$</span><span class="dis-address">${hexWord(target)}</span><span class="dis-punct">,</span><span class="dis-register">X</span>`;
96
+ case AddrMode.ABY:
97
+ return `<span class="dis-punct">$</span><span class="dis-address">${hexWord(target)}</span><span class="dis-punct">,</span><span class="dis-register">Y</span>`;
98
+ case AddrMode.IND:
99
+ return `<span class="dis-punct">($</span><span class="dis-address">${hexWord(target)}</span><span class="dis-punct">)</span>`;
100
+ case AddrMode.IZX:
101
+ return `<span class="dis-punct">($</span><span class="dis-address">${hexByte(operand1)}</span><span class="dis-punct">,</span><span class="dis-register">X</span><span class="dis-punct">)</span>`;
102
+ case AddrMode.IZY:
103
+ return `<span class="dis-punct">($</span><span class="dis-address">${hexByte(operand1)}</span><span class="dis-punct">),</span><span class="dis-register">Y</span>`;
104
+ case AddrMode.REL:
105
+ return formatClickableTarget(target);
106
+ case AddrMode.ZPI:
107
+ return `<span class="dis-punct">($</span><span class="dis-address">${hexByte(operand1)}</span><span class="dis-punct">)</span>`;
108
+ case AddrMode.AIX:
109
+ return `<span class="dis-punct">($</span><span class="dis-address">${hexWord(target)}</span><span class="dis-punct">,</span><span class="dis-register">X</span><span class="dis-punct">)</span>`;
110
+ case AddrMode.ZPR:
111
+ return `<span class="dis-punct">$</span><span class="dis-address">${hexByte(operand1)}</span><span class="dis-punct">,</span>${formatClickableTarget(target)}`;
112
+ default:
113
+ return '';
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Format a single instruction to HTML
119
+ */
120
+ function formatInstruction(instr) {
121
+ // Handle ORG directive
122
+ if (instr.isOrg) {
123
+ return formatOrg(instr.address);
124
+ }
125
+
126
+ // Handle data bytes (non-code)
127
+ if (instr.isData) {
128
+ return formatDataLine(instr);
129
+ }
130
+
131
+ const catClass = CATEGORY_CLASSES[instr.category] || 'dis-mnemonic';
132
+
133
+ // Address
134
+ let html = `<span class="dis-addr">${hexWord(instr.address)}:</span> `;
135
+
136
+ // Bytes
137
+ html += `<span class="dis-bytes">${hexByte(instr.opcode)}`;
138
+ if (instr.length >= 2) {
139
+ html += ` ${hexByte(instr.operand1)}`;
140
+ } else {
141
+ html += ' ';
142
+ }
143
+ if (instr.length >= 3) {
144
+ html += ` ${hexByte(instr.operand2)}`;
145
+ } else {
146
+ html += ' ';
147
+ }
148
+ html += '</span>';
149
+
150
+ // Mnemonic
151
+ html += ` <span class="${catClass}">${instr.mnemonic}</span>`;
152
+
153
+ // Operand - make branch targets clickable
154
+ const isBranch = instr.category === Category.BRANCH;
155
+ const operand = formatOperand(instr.mode, instr.operand1, instr.operand2, instr.target, isBranch);
156
+ if (operand) {
157
+ html += ` ${operand}`;
158
+ }
159
+
160
+ return html;
161
+ }
162
+
163
+ /**
164
+ * Format a data line (unvisited bytes)
165
+ */
166
+ function formatDataLine(entry) {
167
+ let html = `<span class="dis-addr">${hexWord(entry.address)}:</span> `;
168
+
169
+ // Show bytes (up to 8 per line)
170
+ const byteStrs = entry.bytes.map(b => hexByte(b));
171
+ html += `<span class="dis-bytes">${byteStrs.join(' ').padEnd(23)}</span>`;
172
+
173
+ // .BYTE directive
174
+ html += ` <span class="dis-directive">.BYTE</span> `;
175
+ html += `<span class="dis-data">${byteStrs.map(b => '$' + b).join(',')}</span>`;
176
+
177
+ return html;
178
+ }
179
+
180
+ /**
181
+ * Format ORG directive
182
+ */
183
+ function formatOrg(address) {
184
+ return `<span class="dis-directive"> ORG</span> <span class="dis-punct">$</span><span class="dis-address">${hexWord(address)}</span>`;
185
+ }
186
+
187
+ /**
188
+ * Build complete listing with code and data
189
+ * Fills gaps between visited instructions with .BYTE data
190
+ */
191
+ function buildCompleteListing(codeInstructions, rawData, baseAddress) {
192
+ const lines = [];
193
+ const dataLength = rawData.length;
194
+ const endAddress = baseAddress + dataLength;
195
+
196
+ // Build a map of address -> instruction for visited code
197
+ const codeMap = new Map();
198
+ const coveredAddresses = new Set();
199
+
200
+ for (const instr of codeInstructions) {
201
+ codeMap.set(instr.address, instr);
202
+ // Mark all bytes covered by this instruction
203
+ for (let i = 0; i < instr.length; i++) {
204
+ coveredAddresses.add(instr.address + i);
205
+ }
206
+ }
207
+
208
+ // Iterate through all addresses
209
+ let addr = baseAddress;
210
+ while (addr < endAddress) {
211
+ if (codeMap.has(addr)) {
212
+ // This is a visited code instruction
213
+ const instr = codeMap.get(addr);
214
+ lines.push(instr);
215
+ addr += instr.length;
216
+ } else if (coveredAddresses.has(addr)) {
217
+ // This byte is part of a multi-byte instruction, skip it
218
+ addr++;
219
+ } else {
220
+ // This is unvisited data - collect consecutive data bytes
221
+ const dataBytes = [];
222
+ const dataStart = addr;
223
+
224
+ while (addr < endAddress &&
225
+ !codeMap.has(addr) &&
226
+ !coveredAddresses.has(addr) &&
227
+ dataBytes.length < 8) {
228
+ const offset = addr - baseAddress;
229
+ dataBytes.push(rawData[offset]);
230
+ addr++;
231
+ }
232
+
233
+ lines.push({
234
+ isData: true,
235
+ address: dataStart,
236
+ bytes: dataBytes
237
+ });
238
+ }
239
+ }
240
+
241
+ return lines;
242
+ }
243
+
244
+ /**
245
+ * Virtual scroll renderer
246
+ */
247
+ class VirtualScrollRenderer {
248
+ constructor(container, instructions) {
249
+ this.container = container;
250
+ this.instructions = instructions;
251
+ this.lineHeight = LINE_HEIGHT;
252
+ this.totalHeight = instructions.length * this.lineHeight;
253
+
254
+ // Build address-to-line-index map for navigation
255
+ this.addressToLine = new Map();
256
+ for (let i = 0; i < instructions.length; i++) {
257
+ const instr = instructions[i];
258
+ if (instr.address !== undefined) {
259
+ this.addressToLine.set(instr.address, i);
260
+ }
261
+ }
262
+
263
+ this.scrollContainer = document.createElement('div');
264
+ this.scrollContainer.className = 'virtual-scroll-container';
265
+ this.scrollContainer.style.cssText = `
266
+ height: 100%;
267
+ overflow-y: auto;
268
+ position: relative;
269
+ `;
270
+
271
+ this.spacer = document.createElement('div');
272
+ this.spacer.style.cssText = `
273
+ height: ${this.totalHeight}px;
274
+ width: 1px;
275
+ position: absolute;
276
+ top: 0;
277
+ left: 0;
278
+ pointer-events: none;
279
+ `;
280
+
281
+ this.content = document.createElement('pre');
282
+ this.content.className = 'virtual-scroll-content';
283
+ this.content.style.cssText = `
284
+ position: absolute;
285
+ top: 0;
286
+ left: 0;
287
+ right: 0;
288
+ margin: 0;
289
+ padding: 0 8px;
290
+ font-family: var(--font-mono);
291
+ font-size: 11px;
292
+ line-height: 1.5;
293
+ white-space: pre;
294
+ `;
295
+
296
+ this.scrollContainer.appendChild(this.spacer);
297
+ this.scrollContainer.appendChild(this.content);
298
+ this.container.appendChild(this.scrollContainer);
299
+
300
+ this.visibleStart = -1;
301
+ this.visibleEnd = -1;
302
+ this.resizeTimeout = null;
303
+ this.highlightedLine = -1;
304
+
305
+ this.handleScroll = this.handleScroll.bind(this);
306
+ this.handleResize = this.handleResize.bind(this);
307
+ this.handleClick = this.handleClick.bind(this);
308
+
309
+ this.scrollContainer.addEventListener('scroll', this.handleScroll, { passive: true });
310
+ this.content.addEventListener('click', this.handleClick);
311
+ this.resizeObserver = new ResizeObserver(this.handleResize);
312
+ this.resizeObserver.observe(this.scrollContainer);
313
+
314
+ this.render();
315
+ }
316
+
317
+ handleScroll() {
318
+ requestAnimationFrame(() => this.render());
319
+ }
320
+
321
+ handleResize() {
322
+ if (this.resizeTimeout) clearTimeout(this.resizeTimeout);
323
+ this.resizeTimeout = setTimeout(() => {
324
+ this.visibleStart = -1;
325
+ this.visibleEnd = -1;
326
+ this.render();
327
+ }, 16);
328
+ }
329
+
330
+ handleClick(event) {
331
+ const target = event.target.closest('.dis-clickable');
332
+ if (target) {
333
+ const address = parseInt(target.dataset.target, 10);
334
+ if (!isNaN(address)) {
335
+ this.scrollToAddress(address);
336
+ }
337
+ }
338
+ }
339
+
340
+ scrollToAddress(address) {
341
+ const lineIndex = this.addressToLine.get(address);
342
+ if (lineIndex !== undefined) {
343
+ // Highlight the target line
344
+ this.highlightedLine = lineIndex;
345
+
346
+ const scrollTop = lineIndex * this.lineHeight;
347
+ // Center the target line in the viewport
348
+ const viewportHeight = this.scrollContainer.clientHeight;
349
+ const centeredScrollTop = Math.max(0, scrollTop - viewportHeight / 2 + this.lineHeight / 2);
350
+ this.scrollContainer.scrollTop = centeredScrollTop;
351
+
352
+ // Force re-render to show highlight
353
+ this.visibleStart = -1;
354
+ this.visibleEnd = -1;
355
+ this.render();
356
+ }
357
+ }
358
+
359
+ render() {
360
+ const scrollTop = this.scrollContainer.scrollTop;
361
+ const viewportHeight = this.scrollContainer.clientHeight;
362
+
363
+ const startLine = Math.max(0, Math.floor(scrollTop / this.lineHeight) - BUFFER_LINES);
364
+ const endLine = Math.min(
365
+ this.instructions.length,
366
+ Math.ceil((scrollTop + viewportHeight) / this.lineHeight) + BUFFER_LINES
367
+ );
368
+
369
+ if (startLine === this.visibleStart && endLine === this.visibleEnd) {
370
+ return;
371
+ }
372
+
373
+ this.visibleStart = startLine;
374
+ this.visibleEnd = endLine;
375
+
376
+ const offsetTop = startLine * this.lineHeight;
377
+ this.content.style.top = `${offsetTop}px`;
378
+
379
+ // Render visible instructions
380
+ const lines = [];
381
+ for (let i = startLine; i < endLine; i++) {
382
+ const line = formatInstruction(this.instructions[i]);
383
+ if (i === this.highlightedLine) {
384
+ lines.push(`<span class="dis-highlight">${line}</span>`);
385
+ } else {
386
+ lines.push(line);
387
+ }
388
+ }
389
+ this.content.innerHTML = lines.join('\n');
390
+ }
391
+
392
+ destroy() {
393
+ this.scrollContainer.removeEventListener('scroll', this.handleScroll);
394
+ this.content.removeEventListener('click', this.handleClick);
395
+ if (this.resizeObserver) this.resizeObserver.disconnect();
396
+ if (this.resizeTimeout) clearTimeout(this.resizeTimeout);
397
+ }
398
+ }
399
+
400
+ // Store address map for small output click handling
401
+ let currentAddressMap = null;
402
+ let currentScrollContainer = null;
403
+ let currentPreElement = null;
404
+
405
+ /**
406
+ * Handle clicks on branch targets in small output mode
407
+ */
408
+ function handleSmallOutputClick(event) {
409
+ const target = event.target.closest('.dis-clickable');
410
+ if (target && currentAddressMap && currentScrollContainer && currentPreElement) {
411
+ const address = parseInt(target.dataset.target, 10);
412
+ if (!isNaN(address)) {
413
+ const lineIndex = currentAddressMap.get(address);
414
+ if (lineIndex !== undefined) {
415
+ const lineHeight = LINE_HEIGHT;
416
+ const scrollTop = lineIndex * lineHeight;
417
+ const viewportHeight = currentScrollContainer.clientHeight;
418
+ const centeredScrollTop = Math.max(0, scrollTop - viewportHeight / 2 + lineHeight / 2);
419
+ currentScrollContainer.scrollTop = centeredScrollTop;
420
+
421
+ // Highlight the target line
422
+ highlightSmallOutputLine(lineIndex);
423
+ }
424
+ }
425
+ }
426
+ }
427
+
428
+ /**
429
+ * Highlight a line in small output mode
430
+ */
431
+ function highlightSmallOutputLine(lineIndex) {
432
+ if (!currentPreElement) return;
433
+
434
+ // Remove any existing highlight
435
+ const existing = currentPreElement.querySelector('.dis-highlight');
436
+ if (existing) {
437
+ existing.outerHTML = existing.innerHTML;
438
+ }
439
+
440
+ // Find and wrap the target line
441
+ const html = currentPreElement.innerHTML;
442
+ const lines = html.split('\n');
443
+ if (lineIndex >= 0 && lineIndex < lines.length) {
444
+ lines[lineIndex] = `<span class="dis-highlight">${lines[lineIndex]}</span>`;
445
+ currentPreElement.innerHTML = lines.join('\n');
446
+ }
447
+ }
448
+
449
+ let currentRenderer = null;
450
+
451
+ /**
452
+ * Parse instruction data from WASM memory
453
+ * Structure: 16 bytes per instruction
454
+ * Layout: [address:2][target:2][length:1][opcode:1][op1:1][op2:1][mode:1][cat:1][mnem:4][pad:2]
455
+ */
456
+ function parseInstructions(ptr, count) {
457
+ const instructions = [];
458
+ const heap = wasmModule.HEAPU8;
459
+
460
+ for (let i = 0; i < count; i++) {
461
+ const offset = ptr + i * 16;
462
+
463
+ // Read mnemonic as string (4 chars, null-terminated)
464
+ let mnemonic = '';
465
+ for (let j = 0; j < 4; j++) {
466
+ const c = heap[offset + 10 + j];
467
+ if (c === 0) break;
468
+ mnemonic += String.fromCharCode(c);
469
+ }
470
+
471
+ instructions.push({
472
+ address: heap[offset] | (heap[offset + 1] << 8),
473
+ target: heap[offset + 2] | (heap[offset + 3] << 8),
474
+ length: heap[offset + 4],
475
+ opcode: heap[offset + 5],
476
+ operand1: heap[offset + 6],
477
+ operand2: heap[offset + 7],
478
+ mode: heap[offset + 8],
479
+ category: heap[offset + 9],
480
+ mnemonic: mnemonic
481
+ });
482
+ }
483
+
484
+ return instructions;
485
+ }
486
+
487
+ /**
488
+ * Disassemble binary data
489
+ */
490
+ export async function disassemble(data, targetElement) {
491
+ if (!targetElement) {
492
+ throw new Error('Target element required');
493
+ }
494
+ if (!wasmModule) {
495
+ throw new Error('WASM module not initialized');
496
+ }
497
+
498
+ if (currentRenderer) {
499
+ currentRenderer.destroy();
500
+ currentRenderer = null;
501
+ }
502
+
503
+ targetElement.innerHTML = '<pre><span class="dis-comment">; Disassembling...</span></pre>';
504
+
505
+ // Parse DOS 3.3 header
506
+ let baseAddress = 0;
507
+ let codeData = data;
508
+
509
+ if (data.length >= 4) {
510
+ const headerAddr = data[0] | (data[1] << 8);
511
+ const headerLen = data[2] | (data[3] << 8);
512
+ if (headerLen > 0 && headerLen <= data.length - 4 && headerAddr >= 0x0800) {
513
+ baseAddress = headerAddr;
514
+ codeData = data.slice(4);
515
+ }
516
+ }
517
+
518
+ // Copy data to WASM memory
519
+ const dataPtr = wasmModule._malloc(codeData.length);
520
+ wasmModule.HEAPU8.set(codeData, dataPtr);
521
+
522
+ // Call C++ disassembler with flow analysis (recursive descent)
523
+ const count = wasmModule._disassembleWithFlowAnalysis(dataPtr, codeData.length, baseAddress);
524
+ wasmModule._free(dataPtr);
525
+
526
+ if (count === 0) {
527
+ targetElement.innerHTML = '<pre><span class="dis-comment">; No instructions</span></pre>';
528
+ return;
529
+ }
530
+
531
+ // Get pointer to instruction array
532
+ const instrPtr = wasmModule._getDisasmInstructions();
533
+ const codeInstructions = parseInstructions(instrPtr, count);
534
+
535
+ // Build complete listing with code and data gaps filled
536
+ const listing = buildCompleteListing(codeInstructions, codeData, baseAddress);
537
+
538
+ targetElement.innerHTML = '';
539
+
540
+ // Build output lines with ORG at top
541
+ const outputLines = [formatOrg(baseAddress)];
542
+
543
+ // Build address map for navigation (used by both small and large outputs)
544
+ const listingWithOrg = [{ isOrg: true, address: baseAddress }, ...listing];
545
+
546
+ // Small output: render directly
547
+ if (listing.length < 500) {
548
+ const pre = document.createElement('pre');
549
+ for (const entry of listing) {
550
+ outputLines.push(formatInstruction(entry));
551
+ }
552
+ pre.innerHTML = outputLines.join('\n');
553
+ targetElement.appendChild(pre);
554
+
555
+ // Set up click handling for small output
556
+ currentAddressMap = new Map();
557
+ for (let i = 0; i < listingWithOrg.length; i++) {
558
+ const entry = listingWithOrg[i];
559
+ if (entry.address !== undefined) {
560
+ currentAddressMap.set(entry.address, i);
561
+ }
562
+ }
563
+ currentScrollContainer = targetElement;
564
+ currentPreElement = pre;
565
+ pre.addEventListener('click', handleSmallOutputClick);
566
+ return;
567
+ }
568
+
569
+ // Large output: virtual scrolling
570
+ currentAddressMap = null;
571
+ currentScrollContainer = null;
572
+ currentPreElement = null;
573
+ currentRenderer = new VirtualScrollRenderer(targetElement, listingWithOrg);
574
+ }