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.
- package/.clangd +5 -0
- package/.mcp.json +12 -0
- package/CLAUDE.md +362 -0
- package/CMakeLists.txt +774 -0
- package/LICENSE +21 -0
- package/README.md +392 -0
- package/build-wasm/generated/roms.cpp +2447 -0
- package/docker-compose.staging.yml +9 -0
- package/docs/basic-rom-disassembly.md +6663 -0
- package/docs/softswitch-comparison.md +273 -0
- package/docs/thunderclock-debug.md +89 -0
- package/examples/cube.bas +72 -0
- package/examples/hello.s +55 -0
- package/examples/scroll.s +140 -0
- package/package.json +18 -0
- package/public/assets/apple-logo-old.png +0 -0
- package/public/assets/apple-logo.png +0 -0
- package/public/assets/drive-closed-light-on.png +0 -0
- package/public/assets/drive-closed.png +0 -0
- package/public/assets/drive-open-light-on.png +0 -0
- package/public/assets/drive-open.png +0 -0
- package/public/audio-worklet.js +82 -0
- package/public/disks/Apple DOS 3.3 January 1983.dsk +0 -0
- package/public/disks/ProDOS 2.4.3.po +0 -0
- package/public/disks/h32mb.2mg +0 -0
- package/public/disks/library.json +26 -0
- package/public/docs/llms/llm-assembler.txt +90 -0
- package/public/docs/llms/llm-basic-program.txt +256 -0
- package/public/docs/llms/llm-disk-drives.txt +72 -0
- package/public/docs/llms/llm-file-explorer.txt +50 -0
- package/public/docs/llms/llm-hard-drives.txt +80 -0
- package/public/docs/llms/llm-main.txt +51 -0
- package/public/docs/llms/llm-slot-configuration.txt +66 -0
- package/public/icons/icon-192.svg +4 -0
- package/public/icons/icon-512.svg +4 -0
- package/public/index.html +661 -0
- package/public/llms.txt +49 -0
- package/public/manifest.json +29 -0
- package/public/shaders/burnin.glsl +22 -0
- package/public/shaders/crt.glsl +706 -0
- package/public/shaders/edge.glsl +109 -0
- package/public/shaders/vertex.glsl +8 -0
- package/public/sw.js +186 -0
- package/roms/341-0027.bin +0 -0
- package/roms/341-0160-A-US-UK.bin +0 -0
- package/roms/341-0160-A.bin +0 -0
- package/roms/342-0273-A-US-UK.bin +0 -0
- package/roms/342-0349-B-C0-FF.bin +0 -0
- package/roms/Apple Mouse Interface Card ROM - 342-0270-C.bin +0 -0
- package/roms/Thunderclock Plus ROM.bin +0 -0
- package/scripts/generate_roms.sh +69 -0
- package/src/bindings/wasm_interface.cpp +1940 -0
- package/src/core/assembler/assembler.cpp +1239 -0
- package/src/core/assembler/assembler.hpp +115 -0
- package/src/core/audio/audio.cpp +160 -0
- package/src/core/audio/audio.hpp +81 -0
- package/src/core/basic/basic_detokenizer.cpp +436 -0
- package/src/core/basic/basic_detokenizer.hpp +41 -0
- package/src/core/basic/basic_tokenizer.cpp +286 -0
- package/src/core/basic/basic_tokenizer.hpp +26 -0
- package/src/core/basic/basic_tokens.hpp +295 -0
- package/src/core/cards/disk2_card.cpp +568 -0
- package/src/core/cards/disk2_card.hpp +316 -0
- package/src/core/cards/expansion_card.hpp +185 -0
- package/src/core/cards/mockingboard/ay8910.cpp +616 -0
- package/src/core/cards/mockingboard/ay8910.hpp +159 -0
- package/src/core/cards/mockingboard/via6522.cpp +530 -0
- package/src/core/cards/mockingboard/via6522.hpp +163 -0
- package/src/core/cards/mockingboard_card.cpp +312 -0
- package/src/core/cards/mockingboard_card.hpp +159 -0
- package/src/core/cards/mouse_card.cpp +654 -0
- package/src/core/cards/mouse_card.hpp +190 -0
- package/src/core/cards/smartport/block_device.cpp +202 -0
- package/src/core/cards/smartport/block_device.hpp +60 -0
- package/src/core/cards/smartport/smartport_card.cpp +603 -0
- package/src/core/cards/smartport/smartport_card.hpp +120 -0
- package/src/core/cards/thunderclock_card.cpp +237 -0
- package/src/core/cards/thunderclock_card.hpp +122 -0
- package/src/core/cpu/cpu6502.cpp +1609 -0
- package/src/core/cpu/cpu6502.hpp +203 -0
- package/src/core/debug/condition_evaluator.cpp +470 -0
- package/src/core/debug/condition_evaluator.hpp +87 -0
- package/src/core/disassembler/disassembler.cpp +552 -0
- package/src/core/disassembler/disassembler.hpp +171 -0
- package/src/core/disk-image/disk_image.hpp +267 -0
- package/src/core/disk-image/dsk_disk_image.cpp +827 -0
- package/src/core/disk-image/dsk_disk_image.hpp +204 -0
- package/src/core/disk-image/gcr_encoding.cpp +147 -0
- package/src/core/disk-image/gcr_encoding.hpp +78 -0
- package/src/core/disk-image/woz_disk_image.cpp +1049 -0
- package/src/core/disk-image/woz_disk_image.hpp +343 -0
- package/src/core/emulator.cpp +2126 -0
- package/src/core/emulator.hpp +434 -0
- package/src/core/filesystem/dos33.cpp +178 -0
- package/src/core/filesystem/dos33.hpp +66 -0
- package/src/core/filesystem/pascal.cpp +262 -0
- package/src/core/filesystem/pascal.hpp +87 -0
- package/src/core/filesystem/prodos.cpp +369 -0
- package/src/core/filesystem/prodos.hpp +119 -0
- package/src/core/input/keyboard.cpp +227 -0
- package/src/core/input/keyboard.hpp +111 -0
- package/src/core/mmu/mmu.cpp +1387 -0
- package/src/core/mmu/mmu.hpp +236 -0
- package/src/core/types.hpp +196 -0
- package/src/core/video/video.cpp +680 -0
- package/src/core/video/video.hpp +156 -0
- package/src/css/assembler-editor.css +1617 -0
- package/src/css/base.css +470 -0
- package/src/css/basic-debugger.css +791 -0
- package/src/css/basic-editor.css +792 -0
- package/src/css/controls.css +783 -0
- package/src/css/cpu-debugger.css +1413 -0
- package/src/css/debug-base.css +160 -0
- package/src/css/debug-windows.css +6455 -0
- package/src/css/disk-drives.css +406 -0
- package/src/css/documentation.css +392 -0
- package/src/css/file-explorer.css +867 -0
- package/src/css/hard-drive.css +180 -0
- package/src/css/layout.css +217 -0
- package/src/css/memory-windows.css +798 -0
- package/src/css/modals.css +510 -0
- package/src/css/monitor.css +425 -0
- package/src/css/release-notes.css +101 -0
- package/src/css/responsive.css +400 -0
- package/src/css/rule-builder.css +340 -0
- package/src/css/save-states.css +201 -0
- package/src/css/settings-windows.css +1231 -0
- package/src/css/window-switcher.css +150 -0
- package/src/js/agent/agent-manager.js +643 -0
- package/src/js/agent/agent-tools.js +293 -0
- package/src/js/agent/agent-version-tools.js +131 -0
- package/src/js/agent/assembler-tools.js +357 -0
- package/src/js/agent/basic-program-tools.js +894 -0
- package/src/js/agent/disk-tools.js +417 -0
- package/src/js/agent/file-explorer-tools.js +269 -0
- package/src/js/agent/index.js +13 -0
- package/src/js/agent/main-tools.js +222 -0
- package/src/js/agent/slot-tools.js +303 -0
- package/src/js/agent/smartport-tools.js +257 -0
- package/src/js/agent/window-tools.js +80 -0
- package/src/js/audio/audio-driver.js +417 -0
- package/src/js/audio/audio-worklet.js +85 -0
- package/src/js/audio/index.js +8 -0
- package/src/js/config/default-layout.js +34 -0
- package/src/js/config/version.js +8 -0
- package/src/js/data/apple2-rom-routines.js +577 -0
- package/src/js/debug/assembler-editor-window.js +2993 -0
- package/src/js/debug/basic-breakpoint-manager.js +529 -0
- package/src/js/debug/basic-program-parser.js +436 -0
- package/src/js/debug/basic-program-window.js +2594 -0
- package/src/js/debug/basic-variable-inspector.js +447 -0
- package/src/js/debug/breakpoint-manager.js +472 -0
- package/src/js/debug/cpu-debugger-window.js +2396 -0
- package/src/js/debug/index.js +22 -0
- package/src/js/debug/label-manager.js +238 -0
- package/src/js/debug/memory-browser-window.js +416 -0
- package/src/js/debug/memory-heat-map-window.js +481 -0
- package/src/js/debug/memory-map-window.js +206 -0
- package/src/js/debug/mockingboard-window.js +882 -0
- package/src/js/debug/mouse-card-window.js +355 -0
- package/src/js/debug/rule-builder-window.js +648 -0
- package/src/js/debug/soft-switch-window.js +458 -0
- package/src/js/debug/stack-viewer-window.js +221 -0
- package/src/js/debug/symbols.js +416 -0
- package/src/js/debug/trace-panel.js +291 -0
- package/src/js/debug/zero-page-watch-window.js +297 -0
- package/src/js/disk-manager/disk-drives-window.js +212 -0
- package/src/js/disk-manager/disk-operations.js +284 -0
- package/src/js/disk-manager/disk-persistence.js +301 -0
- package/src/js/disk-manager/disk-surface-renderer.js +388 -0
- package/src/js/disk-manager/drive-sounds.js +139 -0
- package/src/js/disk-manager/hard-drive-manager.js +481 -0
- package/src/js/disk-manager/hard-drive-persistence.js +187 -0
- package/src/js/disk-manager/hard-drive-window.js +57 -0
- package/src/js/disk-manager/index.js +890 -0
- package/src/js/display/display-settings-window.js +383 -0
- package/src/js/display/index.js +10 -0
- package/src/js/display/screen-window.js +342 -0
- package/src/js/display/webgl-renderer.js +705 -0
- package/src/js/file-explorer/disassembler.js +574 -0
- package/src/js/file-explorer/dos33.js +266 -0
- package/src/js/file-explorer/file-viewer.js +359 -0
- package/src/js/file-explorer/index.js +1261 -0
- package/src/js/file-explorer/prodos.js +549 -0
- package/src/js/file-explorer/utils.js +67 -0
- package/src/js/help/documentation-window.js +1096 -0
- package/src/js/help/index.js +10 -0
- package/src/js/help/release-notes-window.js +85 -0
- package/src/js/help/release-notes.js +612 -0
- package/src/js/input/gamepad-handler.js +176 -0
- package/src/js/input/index.js +12 -0
- package/src/js/input/input-handler.js +396 -0
- package/src/js/input/joystick-window.js +404 -0
- package/src/js/input/mouse-handler.js +99 -0
- package/src/js/input/text-selection.js +462 -0
- package/src/js/main.js +653 -0
- package/src/js/state/index.js +15 -0
- package/src/js/state/save-states-window.js +393 -0
- package/src/js/state/state-manager.js +409 -0
- package/src/js/state/state-persistence.js +218 -0
- package/src/js/ui/confirm.js +43 -0
- package/src/js/ui/disk-drive-positioner.js +347 -0
- package/src/js/ui/reminder-controller.js +129 -0
- package/src/js/ui/slot-configuration-window.js +560 -0
- package/src/js/ui/theme-manager.js +61 -0
- package/src/js/ui/toast.js +44 -0
- package/src/js/ui/ui-controller.js +897 -0
- package/src/js/ui/window-switcher.js +275 -0
- package/src/js/utils/basic-autocomplete.js +832 -0
- package/src/js/utils/basic-highlighting.js +473 -0
- package/src/js/utils/basic-tokenizer.js +153 -0
- package/src/js/utils/basic-tokens.js +117 -0
- package/src/js/utils/constants.js +28 -0
- package/src/js/utils/indexeddb-helper.js +225 -0
- package/src/js/utils/merlin-editor-support.js +905 -0
- package/src/js/utils/merlin-highlighting.js +551 -0
- package/src/js/utils/storage.js +125 -0
- package/src/js/utils/string-utils.js +19 -0
- package/src/js/utils/wasm-memory.js +54 -0
- package/src/js/windows/base-window.js +690 -0
- package/src/js/windows/index.js +9 -0
- package/src/js/windows/window-manager.js +375 -0
- package/tests/catch2/catch.hpp +17976 -0
- package/tests/common/basic_program_builder.cpp +119 -0
- package/tests/common/basic_program_builder.hpp +209 -0
- package/tests/common/disk_image_builder.cpp +444 -0
- package/tests/common/disk_image_builder.hpp +141 -0
- package/tests/common/test_helpers.hpp +118 -0
- package/tests/gcr/gcr-test.cpp +142 -0
- package/tests/integration/check-rom.js +70 -0
- package/tests/integration/compare-boot.js +239 -0
- package/tests/integration/crash-trace.js +102 -0
- package/tests/integration/disk-boot-test.js +264 -0
- package/tests/integration/memory-crash.js +108 -0
- package/tests/integration/nibble-read-test.js +249 -0
- package/tests/integration/phase-test.js +159 -0
- package/tests/integration/test_emulator.cpp +291 -0
- package/tests/integration/test_emulator_basic.cpp +91 -0
- package/tests/integration/test_emulator_debug.cpp +344 -0
- package/tests/integration/test_emulator_disk.cpp +153 -0
- package/tests/integration/test_emulator_state.cpp +163 -0
- package/tests/klaus/6502_functional_test.bin +0 -0
- package/tests/klaus/65C02_extended_opcodes_test.bin +0 -0
- package/tests/klaus/klaus_6502_test.cpp +184 -0
- package/tests/klaus/klaus_65c02_test.cpp +197 -0
- package/tests/thunderclock/thunderclock_mmu_test.cpp +304 -0
- package/tests/thunderclock/thunderclock_test.cpp +550 -0
- package/tests/unit/test_assembler.cpp +521 -0
- package/tests/unit/test_audio.cpp +196 -0
- package/tests/unit/test_ay8910.cpp +311 -0
- package/tests/unit/test_basic_detokenizer.cpp +265 -0
- package/tests/unit/test_basic_tokenizer.cpp +382 -0
- package/tests/unit/test_block_device.cpp +259 -0
- package/tests/unit/test_condition_evaluator.cpp +219 -0
- package/tests/unit/test_cpu6502.cpp +1301 -0
- package/tests/unit/test_cpu_addressing.cpp +361 -0
- package/tests/unit/test_cpu_cycle_counts.cpp +409 -0
- package/tests/unit/test_cpu_decimal.cpp +166 -0
- package/tests/unit/test_cpu_interrupts.cpp +285 -0
- package/tests/unit/test_disassembler.cpp +323 -0
- package/tests/unit/test_disk2_card.cpp +330 -0
- package/tests/unit/test_dos33.cpp +273 -0
- package/tests/unit/test_dsk_disk_image.cpp +315 -0
- package/tests/unit/test_expansion_card.cpp +178 -0
- package/tests/unit/test_gcr_encoding.cpp +232 -0
- package/tests/unit/test_keyboard.cpp +262 -0
- package/tests/unit/test_mmu.cpp +555 -0
- package/tests/unit/test_mmu_slots.cpp +323 -0
- package/tests/unit/test_mockingboard.cpp +352 -0
- package/tests/unit/test_mouse_card.cpp +386 -0
- package/tests/unit/test_pascal.cpp +248 -0
- package/tests/unit/test_prodos.cpp +259 -0
- package/tests/unit/test_smartport_card.cpp +321 -0
- package/tests/unit/test_thunderclock.cpp +354 -0
- package/tests/unit/test_via6522.cpp +323 -0
- package/tests/unit/test_video.cpp +319 -0
- package/tests/unit/test_woz_disk_image.cpp +257 -0
- package/vite.config.js +96 -0
- package/wiki/AI-Agent.md +372 -0
- package/wiki/Architecture-Overview.md +303 -0
- package/wiki/Audio-System.md +449 -0
- package/wiki/CPU-Emulation.md +477 -0
- package/wiki/Debugger.md +516 -0
- package/wiki/Disk-Drives.md +161 -0
- package/wiki/Disk-System-Internals.md +547 -0
- package/wiki/Display-Settings.md +88 -0
- package/wiki/Expansion-Slots.md +187 -0
- package/wiki/File-Explorer.md +259 -0
- package/wiki/Getting-Started.md +156 -0
- package/wiki/Home.md +69 -0
- package/wiki/Input-Devices.md +183 -0
- package/wiki/Keyboard-Shortcuts.md +158 -0
- package/wiki/Memory-System.md +364 -0
- package/wiki/Save-States.md +172 -0
- 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
|
+
}
|