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,2396 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* cpu-debugger-window.js - CPU debugger with registers, disassembly, and breakpoints
|
|
3
|
+
*
|
|
4
|
+
* Written by
|
|
5
|
+
* Mike Daley <michael_daley@icloud.com>
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { BaseWindow } from "../windows/base-window.js";
|
|
9
|
+
import { getSymbolInfo, getCategoryClass, ALL_SYMBOLS } from "./symbols.js";
|
|
10
|
+
import { BreakpointManager } from "./breakpoint-manager.js";
|
|
11
|
+
import { LabelManager } from "./label-manager.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* CPUDebuggerWindow - CPU registers, disassembly, and breakpoints
|
|
15
|
+
*/
|
|
16
|
+
export class CPUDebuggerWindow extends BaseWindow {
|
|
17
|
+
constructor(wasmModule, isRunningCallback) {
|
|
18
|
+
super({
|
|
19
|
+
id: "cpu-debugger",
|
|
20
|
+
title: "CPU Debugger",
|
|
21
|
+
minWidth: 420,
|
|
22
|
+
minHeight: 400,
|
|
23
|
+
defaultWidth: 500,
|
|
24
|
+
defaultHeight: 660,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
this.wasmModule = wasmModule;
|
|
28
|
+
this.isRunningCallback = isRunningCallback || (() => true);
|
|
29
|
+
this.bpManager = new BreakpointManager(wasmModule);
|
|
30
|
+
this.labelManager = new LabelManager();
|
|
31
|
+
this.lastPC = null;
|
|
32
|
+
this.disasmCache = []; // Cache of {addr, disasm, len} for current view
|
|
33
|
+
this.disasmStartAddr = 0;
|
|
34
|
+
this.disasmViewAddress = null; // When set, overrides PC-centered view
|
|
35
|
+
this.previousRegisters = {}; // For change highlighting
|
|
36
|
+
this.previousWatchValues = {}; // For watch change highlighting
|
|
37
|
+
this.profileEnabled = false; // When true, shows heat overlay in disassembly
|
|
38
|
+
this.watchExpressions = []; // Array of expression strings
|
|
39
|
+
this.loadWatchExpressions();
|
|
40
|
+
this.bookmarks = []; // Array of addresses
|
|
41
|
+
this.loadBookmarks();
|
|
42
|
+
this.beamBreakpoints = []; // Array of { id, scanline, hPos, enabled, mode }
|
|
43
|
+
this.loadBeamBreakpoints();
|
|
44
|
+
this.activeTab = "breakpoints"; // Active tab panel (breakpoints, watch, beam)
|
|
45
|
+
this._hitBpAddr = -1; // Address of the breakpoint/watchpoint that triggered a pause
|
|
46
|
+
this._lastHitBpAddr = -2; // Previous value for change detection
|
|
47
|
+
|
|
48
|
+
// Re-render breakpoint list when breakpoints change
|
|
49
|
+
this.bpManager.onChange(() => {
|
|
50
|
+
this.updateBreakpointList();
|
|
51
|
+
this.updateDisassembly();
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
renderContent() {
|
|
56
|
+
return `
|
|
57
|
+
<div class="cpu-dbg">
|
|
58
|
+
<div class="cpu-dbg-toolbar">
|
|
59
|
+
<div class="cpu-dbg-btn-group">
|
|
60
|
+
<button class="cpu-dbg-btn cpu-btn-run" id="dbg-run" title="Continue (F5)">▶ Run</button>
|
|
61
|
+
<button class="cpu-dbg-btn cpu-btn-pause" id="dbg-pause" title="Pause">⏸ Pause</button>
|
|
62
|
+
</div>
|
|
63
|
+
<span class="cpu-dbg-sep"></span>
|
|
64
|
+
<div class="cpu-dbg-btn-group">
|
|
65
|
+
<button class="cpu-dbg-btn" id="dbg-step" title="Step Into (F11)">Step</button>
|
|
66
|
+
<button class="cpu-dbg-btn" id="dbg-step-over" title="Step Over (F10)">Over</button>
|
|
67
|
+
<button class="cpu-dbg-btn" id="dbg-step-out" title="Step Out (Shift+F11)">Out</button>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
<div class="cpu-dbg-status-bar">
|
|
71
|
+
<div class="cpu-dbg-status-bar-left">
|
|
72
|
+
<span class="cpu-dbg-status-dot"></span>
|
|
73
|
+
<span class="cpu-dbg-status" id="dbg-status">PAUSED</span>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div class="cpu-dbg-section">
|
|
78
|
+
<span class="cpu-dbg-section-label">REGS</span>
|
|
79
|
+
<div class="cpu-dbg-regs">
|
|
80
|
+
<div class="cpu-dbg-reg"><span class="reg-label">A</span><span class="reg-value" id="reg-a">00</span></div>
|
|
81
|
+
<div class="cpu-dbg-reg"><span class="reg-label">X</span><span class="reg-value" id="reg-x">00</span></div>
|
|
82
|
+
<div class="cpu-dbg-reg"><span class="reg-label">Y</span><span class="reg-value" id="reg-y">00</span></div>
|
|
83
|
+
<div class="cpu-dbg-reg"><span class="reg-label">SP</span><span class="reg-value" id="reg-sp">FF</span></div>
|
|
84
|
+
<div class="cpu-dbg-reg reg-wide"><span class="reg-label">PC</span><span class="reg-value" id="reg-pc">0000</span></div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<div class="cpu-dbg-section">
|
|
89
|
+
<span class="cpu-dbg-section-label">FLAGS</span>
|
|
90
|
+
<div class="cpu-flags" id="flags">
|
|
91
|
+
<span class="flag" id="flag-n" title="Negative">N</span>
|
|
92
|
+
<span class="flag" id="flag-v" title="Overflow">V</span>
|
|
93
|
+
<span class="flag separator">-</span>
|
|
94
|
+
<span class="flag" id="flag-b" title="Break">B</span>
|
|
95
|
+
<span class="flag" id="flag-d" title="Decimal">D</span>
|
|
96
|
+
<span class="flag" id="flag-i" title="Interrupt Disable">I</span>
|
|
97
|
+
<span class="flag" id="flag-z" title="Zero">Z</span>
|
|
98
|
+
<span class="flag" id="flag-c" title="Carry">C</span>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<div class="cpu-dbg-section">
|
|
103
|
+
<span class="cpu-dbg-section-label">TIMING</span>
|
|
104
|
+
<div class="cpu-dbg-timing-row">
|
|
105
|
+
<span class="cpu-dbg-cycles" title="Total CPU cycles since reset"><span class="meta-dim">CYC</span> <span id="cycle-count">0</span></span>
|
|
106
|
+
<span class="irq-indicator" id="irq-pending" title="IRQ Pending">IRQ</span>
|
|
107
|
+
<span class="irq-indicator" id="nmi-pending" title="NMI Pending">NMI</span>
|
|
108
|
+
<span class="irq-indicator" id="nmi-edge" title="NMI Edge Detected">EDGE</span>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<div class="cpu-dbg-section">
|
|
113
|
+
<span class="cpu-dbg-section-label">BEAM</span>
|
|
114
|
+
<div class="cpu-dbg-scanline-row">
|
|
115
|
+
<span class="scanline-item" title="Current scanline (0-261)"><span class="scanline-label">SCAN</span> <span class="scanline-value" id="scan-line">--</span></span>
|
|
116
|
+
<span class="scanline-item" title="Horizontal position within scanline (0-64), includes visible and blanking portions"><span class="scanline-label">H</span> <span class="scanline-value" id="scan-hpos">--</span></span>
|
|
117
|
+
<span class="scanline-item" title="Visible screen column (0-39), only valid during the visible portion of the scanline"><span class="scanline-label">COL</span> <span class="scanline-value" id="scan-col">--</span></span>
|
|
118
|
+
<span class="scanline-item" title="CPU cycle count within the current frame"><span class="scanline-label">FCYC</span> <span class="scanline-value" id="scan-fcyc">--</span></span>
|
|
119
|
+
<span class="scanline-badge scanline-badge-idle" id="scan-badge">--</span>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<div class="cpu-dbg-disasm">
|
|
124
|
+
<div class="cpu-dbg-disasm-bar">
|
|
125
|
+
<input type="text" id="disasm-goto-input" placeholder="Address / Symbol" spellcheck="false">
|
|
126
|
+
<button class="cpu-dbg-bar-btn" id="disasm-goto-btn" title="Go to address">Go</button>
|
|
127
|
+
<button class="cpu-dbg-bar-btn" id="disasm-goto-pc" title="Follow PC">PC</button>
|
|
128
|
+
<button class="cpu-dbg-bar-btn" id="disasm-import-sym" title="Import symbol file">Sym</button>
|
|
129
|
+
</div>
|
|
130
|
+
<div class="cpu-disasm-view" id="disasm-view"></div>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
<div class="cpu-dbg-tabs">
|
|
134
|
+
<div class="cpu-dbg-tab-bar">
|
|
135
|
+
<button class="cpu-dbg-tab active" data-tab="breakpoints">Breakpoints <span class="cpu-dbg-tab-count" id="bp-tab-count">0</span></button>
|
|
136
|
+
<button class="cpu-dbg-tab" data-tab="watch">Watch <span class="cpu-dbg-tab-count" id="watch-tab-count">0</span></button>
|
|
137
|
+
<button class="cpu-dbg-tab" data-tab="beam">Beam <span class="cpu-dbg-tab-count" id="beam-tab-count">0</span></button>
|
|
138
|
+
</div>
|
|
139
|
+
<div class="cpu-dbg-tab-content active" data-tab="breakpoints">
|
|
140
|
+
<div class="cpu-dbg-tab-toolbar">
|
|
141
|
+
<select id="bp-source-select" title="Breakpoint source">
|
|
142
|
+
<option value="addr">Addr</option>
|
|
143
|
+
<option value="switch">Switch</option>
|
|
144
|
+
</select>
|
|
145
|
+
<select id="bp-type-select" title="Breakpoint type">
|
|
146
|
+
<option value="exec">Exec</option>
|
|
147
|
+
<option value="read">Read</option>
|
|
148
|
+
<option value="write">Write</option>
|
|
149
|
+
<option value="readwrite">R/W</option>
|
|
150
|
+
</select>
|
|
151
|
+
<input type="text" id="breakpoint-input" placeholder="$XXXX" spellcheck="false">
|
|
152
|
+
<select id="bp-switch-select" title="Soft switch" style="display:none"></select>
|
|
153
|
+
<button class="cpu-dbg-add-btn" id="breakpoint-add-btn" title="Add breakpoint">+</button>
|
|
154
|
+
</div>
|
|
155
|
+
<div class="cpu-bp-list" id="breakpoint-list"></div>
|
|
156
|
+
</div>
|
|
157
|
+
<div class="cpu-dbg-tab-content" data-tab="watch">
|
|
158
|
+
<div class="cpu-dbg-tab-toolbar">
|
|
159
|
+
<select id="watch-source-select" title="Watch source type">
|
|
160
|
+
<option value="reg">Register</option>
|
|
161
|
+
<option value="flag">Flag</option>
|
|
162
|
+
<option value="byte">Byte</option>
|
|
163
|
+
<option value="word">Word</option>
|
|
164
|
+
</select>
|
|
165
|
+
<select id="watch-detail-reg" class="watch-detail-control active" title="Register">
|
|
166
|
+
<option value="A">A</option>
|
|
167
|
+
<option value="X">X</option>
|
|
168
|
+
<option value="Y">Y</option>
|
|
169
|
+
<option value="SP">SP</option>
|
|
170
|
+
<option value="PC">PC</option>
|
|
171
|
+
<option value="P">P</option>
|
|
172
|
+
</select>
|
|
173
|
+
<select id="watch-detail-flag" class="watch-detail-control" title="Flag">
|
|
174
|
+
<option value="N">N</option>
|
|
175
|
+
<option value="V">V</option>
|
|
176
|
+
<option value="B">B</option>
|
|
177
|
+
<option value="D">D</option>
|
|
178
|
+
<option value="I">I</option>
|
|
179
|
+
<option value="Z">Z</option>
|
|
180
|
+
<option value="C">C</option>
|
|
181
|
+
</select>
|
|
182
|
+
<input type="text" id="watch-detail-addr" class="watch-detail-control" placeholder="$0000" spellcheck="false">
|
|
183
|
+
<button class="cpu-dbg-add-btn" id="watch-add-btn" title="Add watch">+</button>
|
|
184
|
+
</div>
|
|
185
|
+
<div class="cpu-watch-list" id="watch-list"></div>
|
|
186
|
+
</div>
|
|
187
|
+
<div class="cpu-dbg-tab-content" data-tab="beam">
|
|
188
|
+
<div class="cpu-dbg-tab-toolbar">
|
|
189
|
+
<select id="beam-mode-select" title="Beam breakpoint type">
|
|
190
|
+
<option value="vbl">VBL Start</option>
|
|
191
|
+
<option value="hblank">HBLANK</option>
|
|
192
|
+
<option value="scanline">Scanline</option>
|
|
193
|
+
<option value="column">Column</option>
|
|
194
|
+
<option value="scancol">Scan + Col</option>
|
|
195
|
+
</select>
|
|
196
|
+
<input type="text" id="beam-scan-input" class="beam-input" placeholder="Row" maxlength="3" style="display:none">
|
|
197
|
+
<input type="text" id="beam-col-input" class="beam-input" placeholder="Col" maxlength="2" style="display:none">
|
|
198
|
+
<button class="cpu-dbg-add-btn" id="beam-add-btn" title="Add beam breakpoint">+</button>
|
|
199
|
+
</div>
|
|
200
|
+
<div class="cpu-beam-list" id="beam-list"></div>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Set up event listeners after content is rendered
|
|
209
|
+
*/
|
|
210
|
+
setupContentEventListeners() {
|
|
211
|
+
// Disassembly click handler using event delegation with mousedown
|
|
212
|
+
const disasmView = this.contentElement.querySelector("#disasm-view");
|
|
213
|
+
if (disasmView) {
|
|
214
|
+
disasmView.addEventListener("mousedown", (e) => {
|
|
215
|
+
const line = e.target.closest(".cpu-disasm-line");
|
|
216
|
+
if (line && line.dataset.addr) {
|
|
217
|
+
const addr = parseInt(line.dataset.addr, 16);
|
|
218
|
+
e.preventDefault();
|
|
219
|
+
e.stopPropagation();
|
|
220
|
+
if (e.ctrlKey || e.metaKey) {
|
|
221
|
+
this.toggleBookmark(addr);
|
|
222
|
+
} else {
|
|
223
|
+
this.bpManager.toggle(addr);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Context menu for run to cursor
|
|
229
|
+
disasmView.addEventListener("contextmenu", (e) => {
|
|
230
|
+
e.preventDefault();
|
|
231
|
+
const line = e.target.closest(".cpu-disasm-line");
|
|
232
|
+
if (line && line.dataset.addr) {
|
|
233
|
+
const addr = parseInt(line.dataset.addr, 16);
|
|
234
|
+
this.showDisasmContextMenu(e.clientX, e.clientY, addr);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Debug control buttons
|
|
240
|
+
const runBtn = this.contentElement.querySelector("#dbg-run");
|
|
241
|
+
const pauseBtn = this.contentElement.querySelector("#dbg-pause");
|
|
242
|
+
const stepBtn = this.contentElement.querySelector("#dbg-step");
|
|
243
|
+
|
|
244
|
+
if (runBtn) {
|
|
245
|
+
runBtn.addEventListener("click", () => {
|
|
246
|
+
this.wasmModule._setPaused(false);
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (pauseBtn) {
|
|
251
|
+
pauseBtn.addEventListener("click", () => {
|
|
252
|
+
this.bpManager.clearTemp();
|
|
253
|
+
this.wasmModule._setPaused(true);
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (stepBtn) {
|
|
258
|
+
stepBtn.addEventListener("click", () => {
|
|
259
|
+
this.bpManager.clearTemp();
|
|
260
|
+
this.wasmModule._stepInstruction();
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Step Over - step over JSR instructions
|
|
265
|
+
const stepOverBtn = this.contentElement.querySelector("#dbg-step-over");
|
|
266
|
+
if (stepOverBtn) {
|
|
267
|
+
stepOverBtn.addEventListener("click", () => this.stepOver());
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Step Out - run until RTS returns
|
|
271
|
+
const stepOutBtn = this.contentElement.querySelector("#dbg-step-out");
|
|
272
|
+
if (stepOutBtn) {
|
|
273
|
+
stepOutBtn.addEventListener("click", () => this.stepOut());
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Goto address in disassembly
|
|
277
|
+
const gotoInput = this.contentElement.querySelector("#disasm-goto-input");
|
|
278
|
+
const gotoBtn = this.contentElement.querySelector("#disasm-goto-btn");
|
|
279
|
+
const gotoPcBtn = this.contentElement.querySelector("#disasm-goto-pc");
|
|
280
|
+
|
|
281
|
+
if (gotoBtn && gotoInput) {
|
|
282
|
+
const doGoto = () => {
|
|
283
|
+
const text = gotoInput.value.trim();
|
|
284
|
+
if (!text) return;
|
|
285
|
+
const addr = this.resolveAddress(text);
|
|
286
|
+
if (addr !== null) {
|
|
287
|
+
this.disasmViewAddress = addr;
|
|
288
|
+
this.updateDisassembly();
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
gotoBtn.addEventListener("click", doGoto);
|
|
292
|
+
gotoInput.addEventListener("keypress", (e) => {
|
|
293
|
+
if (e.key === "Enter") doGoto();
|
|
294
|
+
});
|
|
295
|
+
gotoInput.addEventListener("keydown", (e) => e.stopPropagation());
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (gotoPcBtn) {
|
|
299
|
+
gotoPcBtn.addEventListener("click", () => {
|
|
300
|
+
this.disasmViewAddress = null;
|
|
301
|
+
this.updateDisassembly();
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Import symbols button
|
|
306
|
+
const importSymBtn =
|
|
307
|
+
this.contentElement.querySelector("#disasm-import-sym");
|
|
308
|
+
if (importSymBtn) {
|
|
309
|
+
importSymBtn.addEventListener("click", () => this.importSymbolFile());
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Double-click disassembly line to add/edit comment
|
|
313
|
+
if (disasmView) {
|
|
314
|
+
disasmView.addEventListener("dblclick", (e) => {
|
|
315
|
+
const line = e.target.closest(".cpu-disasm-line");
|
|
316
|
+
if (line && line.dataset.addr) {
|
|
317
|
+
const addr = parseInt(line.dataset.addr, 16);
|
|
318
|
+
this.editInlineComment(addr);
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Breakpoint source mode switching (addr/switch)
|
|
324
|
+
const bpSourceSelect =
|
|
325
|
+
this.contentElement.querySelector("#bp-source-select");
|
|
326
|
+
const bpTypeSelect = this.contentElement.querySelector("#bp-type-select");
|
|
327
|
+
const bpSwitchSelect =
|
|
328
|
+
this.contentElement.querySelector("#bp-switch-select");
|
|
329
|
+
|
|
330
|
+
if (bpSourceSelect && bpSwitchSelect) {
|
|
331
|
+
// Populate soft switch dropdown with optgroups
|
|
332
|
+
for (const group of CPUDebuggerWindow.SOFT_SWITCH_GROUPS) {
|
|
333
|
+
const optgroup = document.createElement("optgroup");
|
|
334
|
+
optgroup.label = group.category;
|
|
335
|
+
for (const sw of group.switches) {
|
|
336
|
+
const option = document.createElement("option");
|
|
337
|
+
option.value = `${sw.start}:${sw.end}:${sw.name}`;
|
|
338
|
+
const startHex = sw.start.toString(16).toUpperCase();
|
|
339
|
+
const endHex = sw.end.toString(16).toUpperCase();
|
|
340
|
+
const range =
|
|
341
|
+
sw.start === sw.end ? `$${startHex}` : `$${startHex}-${endHex}`;
|
|
342
|
+
option.textContent = `${sw.name} (${range})`;
|
|
343
|
+
option.title = sw.desc;
|
|
344
|
+
optgroup.appendChild(option);
|
|
345
|
+
}
|
|
346
|
+
bpSwitchSelect.appendChild(optgroup);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
bpSourceSelect.addEventListener("change", () => {
|
|
350
|
+
const isSwitch = bpSourceSelect.value === "switch";
|
|
351
|
+
const bpInput = this.contentElement.querySelector("#breakpoint-input");
|
|
352
|
+
if (bpInput) bpInput.style.display = isSwitch ? "none" : "";
|
|
353
|
+
bpSwitchSelect.style.display = isSwitch ? "" : "none";
|
|
354
|
+
|
|
355
|
+
if (isSwitch) {
|
|
356
|
+
// Hide Exec option, auto-select R/W
|
|
357
|
+
if (bpTypeSelect) {
|
|
358
|
+
const execOption = bpTypeSelect.querySelector(
|
|
359
|
+
'option[value="exec"]',
|
|
360
|
+
);
|
|
361
|
+
if (execOption) execOption.style.display = "none";
|
|
362
|
+
if (bpTypeSelect.value === "exec") bpTypeSelect.value = "readwrite";
|
|
363
|
+
}
|
|
364
|
+
} else {
|
|
365
|
+
// Show Exec option again
|
|
366
|
+
if (bpTypeSelect) {
|
|
367
|
+
const execOption = bpTypeSelect.querySelector(
|
|
368
|
+
'option[value="exec"]',
|
|
369
|
+
);
|
|
370
|
+
if (execOption) execOption.style.display = "";
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Breakpoint add
|
|
377
|
+
const bpInput = this.contentElement.querySelector("#breakpoint-input");
|
|
378
|
+
const bpAddBtn = this.contentElement.querySelector("#breakpoint-add-btn");
|
|
379
|
+
if (bpAddBtn && bpInput) {
|
|
380
|
+
bpAddBtn.addEventListener("click", () => this.addBreakpointFromInput());
|
|
381
|
+
bpInput.addEventListener("keypress", (e) => {
|
|
382
|
+
if (e.key === "Enter") this.addBreakpointFromInput();
|
|
383
|
+
});
|
|
384
|
+
bpInput.addEventListener("keydown", (e) => e.stopPropagation());
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Breakpoint list event delegation (survives DOM rebuilds)
|
|
388
|
+
const bpList = this.contentElement.querySelector("#breakpoint-list");
|
|
389
|
+
if (bpList) {
|
|
390
|
+
bpList.addEventListener("mousedown", (e) => {
|
|
391
|
+
const removeBtn = e.target.closest(".bp-remove");
|
|
392
|
+
if (removeBtn) {
|
|
393
|
+
e.stopPropagation();
|
|
394
|
+
e.preventDefault();
|
|
395
|
+
const item = removeBtn.closest(".cpu-bp-item");
|
|
396
|
+
if (item && item.dataset.addr) {
|
|
397
|
+
this.bpManager.remove(parseInt(item.dataset.addr, 10));
|
|
398
|
+
}
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
const editBtn = e.target.closest(".bp-edit");
|
|
402
|
+
if (editBtn) {
|
|
403
|
+
e.stopPropagation();
|
|
404
|
+
e.preventDefault();
|
|
405
|
+
const item = editBtn.closest(".cpu-bp-item");
|
|
406
|
+
if (item && item.dataset.addr) {
|
|
407
|
+
this.editBreakpointCondition(parseInt(item.dataset.addr, 10));
|
|
408
|
+
}
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
const checkbox = e.target.closest(".bp-enable input");
|
|
412
|
+
if (checkbox) {
|
|
413
|
+
// Let the checkbox handle its own change event
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
bpList.addEventListener("change", (e) => {
|
|
418
|
+
const checkbox = e.target.closest(".bp-enable input");
|
|
419
|
+
if (checkbox) {
|
|
420
|
+
const item = checkbox.closest(".cpu-bp-item");
|
|
421
|
+
if (item && item.dataset.addr) {
|
|
422
|
+
this.bpManager.setEnabled(
|
|
423
|
+
parseInt(item.dataset.addr, 10),
|
|
424
|
+
checkbox.checked,
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
bpList.addEventListener("dblclick", (e) => {
|
|
430
|
+
const item = e.target.closest(".cpu-bp-item");
|
|
431
|
+
if (item && item.dataset.addr) {
|
|
432
|
+
e.stopPropagation();
|
|
433
|
+
this.editBreakpointCondition(parseInt(item.dataset.addr, 10));
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Watch source/detail switching
|
|
439
|
+
const watchSourceSelect = this.contentElement.querySelector(
|
|
440
|
+
"#watch-source-select",
|
|
441
|
+
);
|
|
442
|
+
const watchDetailReg =
|
|
443
|
+
this.contentElement.querySelector("#watch-detail-reg");
|
|
444
|
+
const watchDetailFlag =
|
|
445
|
+
this.contentElement.querySelector("#watch-detail-flag");
|
|
446
|
+
const watchDetailAddr =
|
|
447
|
+
this.contentElement.querySelector("#watch-detail-addr");
|
|
448
|
+
const watchAddBtn = this.contentElement.querySelector("#watch-add-btn");
|
|
449
|
+
|
|
450
|
+
if (watchSourceSelect) {
|
|
451
|
+
watchSourceSelect.addEventListener("change", () => {
|
|
452
|
+
const source = watchSourceSelect.value;
|
|
453
|
+
if (watchDetailReg)
|
|
454
|
+
watchDetailReg.classList.toggle("active", source === "reg");
|
|
455
|
+
if (watchDetailFlag)
|
|
456
|
+
watchDetailFlag.classList.toggle("active", source === "flag");
|
|
457
|
+
if (watchDetailAddr)
|
|
458
|
+
watchDetailAddr.classList.toggle(
|
|
459
|
+
"active",
|
|
460
|
+
source === "byte" || source === "word",
|
|
461
|
+
);
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (watchAddBtn) {
|
|
466
|
+
watchAddBtn.addEventListener("click", () => this.addWatchFromForm());
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (watchDetailAddr) {
|
|
470
|
+
watchDetailAddr.addEventListener("keypress", (e) => {
|
|
471
|
+
if (e.key === "Enter") this.addWatchFromForm();
|
|
472
|
+
});
|
|
473
|
+
watchDetailAddr.addEventListener("keydown", (e) => e.stopPropagation());
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Tab switching
|
|
477
|
+
const tabBar = this.contentElement.querySelector(".cpu-dbg-tab-bar");
|
|
478
|
+
if (tabBar) {
|
|
479
|
+
tabBar.addEventListener("click", (e) => {
|
|
480
|
+
const tab = e.target.closest(".cpu-dbg-tab");
|
|
481
|
+
if (!tab) return;
|
|
482
|
+
const tabName = tab.dataset.tab;
|
|
483
|
+
tabBar
|
|
484
|
+
.querySelectorAll(".cpu-dbg-tab")
|
|
485
|
+
.forEach((t) => t.classList.remove("active"));
|
|
486
|
+
tab.classList.add("active");
|
|
487
|
+
tab.classList.remove("hit-alert");
|
|
488
|
+
this.contentElement
|
|
489
|
+
.querySelectorAll(".cpu-dbg-tab-content")
|
|
490
|
+
.forEach((c) => {
|
|
491
|
+
c.classList.toggle("active", c.dataset.tab === tabName);
|
|
492
|
+
});
|
|
493
|
+
this.activeTab = tabName;
|
|
494
|
+
if (this.onStateChange) this.onStateChange();
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Watch list event delegation (survives DOM rebuilds from updateWatchList)
|
|
499
|
+
const watchList = this.contentElement.querySelector("#watch-list");
|
|
500
|
+
if (watchList) {
|
|
501
|
+
watchList.addEventListener("mousedown", (e) => {
|
|
502
|
+
const removeBtn = e.target.closest(".watch-remove");
|
|
503
|
+
if (removeBtn) {
|
|
504
|
+
e.stopPropagation();
|
|
505
|
+
e.preventDefault();
|
|
506
|
+
const item = removeBtn.closest(".cpu-watch-item");
|
|
507
|
+
if (item && item.dataset.index !== undefined) {
|
|
508
|
+
const idx = parseInt(item.dataset.index, 10);
|
|
509
|
+
this.watchExpressions.splice(idx, 1);
|
|
510
|
+
this.saveWatchExpressions();
|
|
511
|
+
this.updateWatchList();
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Override create to set up content event listeners
|
|
520
|
+
*/
|
|
521
|
+
create() {
|
|
522
|
+
super.create();
|
|
523
|
+
this.setupContentEventListeners();
|
|
524
|
+
this.setupKeyboardShortcuts();
|
|
525
|
+
this.setupRegisterEditing();
|
|
526
|
+
this.setupBeamBreakTabEvents();
|
|
527
|
+
this.updateBreakpointList();
|
|
528
|
+
this.updateBeamList();
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
destroy() {
|
|
532
|
+
this.removeKeyboardShortcuts();
|
|
533
|
+
super.destroy();
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
setupKeyboardShortcuts() {
|
|
537
|
+
this._keyHandler = (e) => {
|
|
538
|
+
if (!this.isVisible) return;
|
|
539
|
+
|
|
540
|
+
switch (e.key) {
|
|
541
|
+
case "F5":
|
|
542
|
+
e.preventDefault();
|
|
543
|
+
e.stopPropagation();
|
|
544
|
+
this.wasmModule._setPaused(false);
|
|
545
|
+
break;
|
|
546
|
+
case "F10":
|
|
547
|
+
e.preventDefault();
|
|
548
|
+
e.stopPropagation();
|
|
549
|
+
this.stepOver();
|
|
550
|
+
break;
|
|
551
|
+
case "F11":
|
|
552
|
+
e.preventDefault();
|
|
553
|
+
e.stopPropagation();
|
|
554
|
+
if (e.shiftKey) {
|
|
555
|
+
this.stepOut();
|
|
556
|
+
} else {
|
|
557
|
+
this.bpManager.clearTemp();
|
|
558
|
+
this.wasmModule._stepInstruction();
|
|
559
|
+
}
|
|
560
|
+
break;
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
document.addEventListener("keydown", this._keyHandler, { capture: true });
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
removeKeyboardShortcuts() {
|
|
567
|
+
if (this._keyHandler) {
|
|
568
|
+
document.removeEventListener("keydown", this._keyHandler, {
|
|
569
|
+
capture: true,
|
|
570
|
+
});
|
|
571
|
+
this._keyHandler = null;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Add breakpoint from input field
|
|
577
|
+
*/
|
|
578
|
+
addBreakpointFromInput() {
|
|
579
|
+
const sourceSelect = this.contentElement.querySelector("#bp-source-select");
|
|
580
|
+
const typeSelect = this.contentElement.querySelector("#bp-type-select");
|
|
581
|
+
const type = typeSelect ? typeSelect.value : "exec";
|
|
582
|
+
|
|
583
|
+
if (sourceSelect && sourceSelect.value === "switch") {
|
|
584
|
+
// Switch mode: parse the switch dropdown value
|
|
585
|
+
const switchSelect =
|
|
586
|
+
this.contentElement.querySelector("#bp-switch-select");
|
|
587
|
+
if (!switchSelect || !switchSelect.value) return;
|
|
588
|
+
|
|
589
|
+
const parts = switchSelect.value.split(":");
|
|
590
|
+
const startAddr = parseInt(parts[0], 10);
|
|
591
|
+
const endAddr = parseInt(parts[1], 10);
|
|
592
|
+
const name = parts.slice(2).join(":");
|
|
593
|
+
|
|
594
|
+
if (!isNaN(startAddr) && !isNaN(endAddr)) {
|
|
595
|
+
this.bpManager.add(startAddr, {
|
|
596
|
+
type,
|
|
597
|
+
endAddress: endAddr,
|
|
598
|
+
name,
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
} else {
|
|
602
|
+
// Address mode: parse the text input
|
|
603
|
+
const input = this.contentElement.querySelector("#breakpoint-input");
|
|
604
|
+
if (!input) return;
|
|
605
|
+
|
|
606
|
+
const text = input.value.trim();
|
|
607
|
+
const addr = this.resolveAddress(text) ?? parseInt(text, 16);
|
|
608
|
+
|
|
609
|
+
if (!isNaN(addr) && addr >= 0 && addr <= 0xffff) {
|
|
610
|
+
this.bpManager.add(addr, { type });
|
|
611
|
+
input.value = "";
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Build expression string from the watch form and add it
|
|
618
|
+
*/
|
|
619
|
+
addWatchFromForm() {
|
|
620
|
+
const source = this.contentElement.querySelector(
|
|
621
|
+
"#watch-source-select",
|
|
622
|
+
)?.value;
|
|
623
|
+
if (!source) return;
|
|
624
|
+
|
|
625
|
+
let expr = null;
|
|
626
|
+
if (source === "reg") {
|
|
627
|
+
expr = this.contentElement.querySelector("#watch-detail-reg")?.value;
|
|
628
|
+
} else if (source === "flag") {
|
|
629
|
+
expr = this.contentElement.querySelector("#watch-detail-flag")?.value;
|
|
630
|
+
} else if (source === "byte" || source === "word") {
|
|
631
|
+
const addrInput = this.contentElement.querySelector("#watch-detail-addr");
|
|
632
|
+
if (!addrInput) return;
|
|
633
|
+
const text = addrInput.value.trim();
|
|
634
|
+
if (!text) return;
|
|
635
|
+
const addr = this.resolveAddress(text);
|
|
636
|
+
if (addr === null) return;
|
|
637
|
+
const hexAddr = "$" + this.formatHex(addr, 4);
|
|
638
|
+
expr = source === "byte" ? `PEEK(${hexAddr})` : `DEEK(${hexAddr})`;
|
|
639
|
+
addrInput.value = "";
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if (expr) {
|
|
643
|
+
this.watchExpressions.push(expr);
|
|
644
|
+
this.saveWatchExpressions();
|
|
645
|
+
this.updateWatchList();
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Resolve an address from hex string or symbol name
|
|
651
|
+
* @returns {number|null} Address or null if invalid
|
|
652
|
+
*/
|
|
653
|
+
resolveAddress(text) {
|
|
654
|
+
// Try hex: $XXXX, 0xXXXX, or plain hex
|
|
655
|
+
const hexMatch = text.match(/^\$?(?:0x)?([0-9A-Fa-f]{1,4})$/);
|
|
656
|
+
if (hexMatch) {
|
|
657
|
+
return parseInt(hexMatch[1], 16);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Try label manager first (user labels + imported symbols)
|
|
661
|
+
const labelAddr = this.labelManager.resolveByName(text);
|
|
662
|
+
if (labelAddr !== null) return labelAddr;
|
|
663
|
+
|
|
664
|
+
// Try built-in symbol name lookup (case-insensitive)
|
|
665
|
+
const upper = text.toUpperCase();
|
|
666
|
+
for (const [addr, info] of Object.entries(ALL_SYMBOLS)) {
|
|
667
|
+
if (info.name.toUpperCase() === upper) {
|
|
668
|
+
return parseInt(addr);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
return null;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Breakpoint management is delegated to this.bpManager (BreakpointManager)
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Step Over - if current instruction is JSR, run until it returns
|
|
679
|
+
* Otherwise, just do a single step
|
|
680
|
+
*/
|
|
681
|
+
stepOver() {
|
|
682
|
+
this.bpManager.clearTemp();
|
|
683
|
+
const tempAddr = this.wasmModule._stepOver();
|
|
684
|
+
if (tempAddr) {
|
|
685
|
+
this.bpManager.syncTemp(tempAddr);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Step Out - run until the current subroutine returns
|
|
691
|
+
* Reads return address from stack and sets breakpoint there
|
|
692
|
+
*/
|
|
693
|
+
stepOut() {
|
|
694
|
+
this.bpManager.clearTemp();
|
|
695
|
+
const tempAddr = this.wasmModule._stepOut();
|
|
696
|
+
if (tempAddr) {
|
|
697
|
+
this.bpManager.syncTemp(tempAddr);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Show context menu on disassembly line
|
|
703
|
+
*/
|
|
704
|
+
showDisasmContextMenu(x, y, addr) {
|
|
705
|
+
// Remove existing menu
|
|
706
|
+
this.hideDisasmContextMenu();
|
|
707
|
+
|
|
708
|
+
const menu = document.createElement("div");
|
|
709
|
+
menu.className = "cpu-disasm-context-menu";
|
|
710
|
+
menu.innerHTML = `
|
|
711
|
+
<div class="ctx-item" data-action="run-to">Run to $${this.formatHex(addr, 4)}</div>
|
|
712
|
+
<div class="ctx-item" data-action="goto">Go to $${this.formatHex(addr, 4)}</div>
|
|
713
|
+
<div class="ctx-item" data-action="toggle-bp">${this.bpManager.has(addr) ? "Remove" : "Set"} Breakpoint</div>
|
|
714
|
+
`;
|
|
715
|
+
menu.style.cssText = `
|
|
716
|
+
position: fixed;
|
|
717
|
+
left: ${x}px;
|
|
718
|
+
top: ${y}px;
|
|
719
|
+
z-index: 10000;
|
|
720
|
+
background: var(--glass-bg-solid);
|
|
721
|
+
border: 1px solid var(--glass-border);
|
|
722
|
+
border-radius: 4px;
|
|
723
|
+
padding: 4px 0;
|
|
724
|
+
font-family: var(--font-mono);
|
|
725
|
+
font-size: 11px;
|
|
726
|
+
box-shadow: var(--shadow-md);
|
|
727
|
+
min-width: 160px;
|
|
728
|
+
`;
|
|
729
|
+
|
|
730
|
+
menu.querySelectorAll(".ctx-item").forEach((item) => {
|
|
731
|
+
item.style.cssText = `
|
|
732
|
+
padding: 6px 12px;
|
|
733
|
+
color: var(--text-secondary);
|
|
734
|
+
cursor: pointer;
|
|
735
|
+
`;
|
|
736
|
+
item.addEventListener("mouseenter", () => {
|
|
737
|
+
item.style.background = "var(--accent-blue-bg-strong)";
|
|
738
|
+
item.style.color = "var(--text-primary)";
|
|
739
|
+
});
|
|
740
|
+
item.addEventListener("mouseleave", () => {
|
|
741
|
+
item.style.background = "transparent";
|
|
742
|
+
item.style.color = "var(--text-secondary)";
|
|
743
|
+
});
|
|
744
|
+
item.addEventListener("click", () => {
|
|
745
|
+
const action = item.dataset.action;
|
|
746
|
+
if (action === "run-to") {
|
|
747
|
+
this.runToCursor(addr);
|
|
748
|
+
} else if (action === "goto") {
|
|
749
|
+
this.disasmViewAddress = addr;
|
|
750
|
+
this.updateDisassembly();
|
|
751
|
+
} else if (action === "toggle-bp") {
|
|
752
|
+
this.bpManager.toggle(addr);
|
|
753
|
+
}
|
|
754
|
+
this.hideDisasmContextMenu();
|
|
755
|
+
});
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
document.body.appendChild(menu);
|
|
759
|
+
this._contextMenu = menu;
|
|
760
|
+
|
|
761
|
+
// Close on click anywhere else
|
|
762
|
+
this._contextMenuClose = () => this.hideDisasmContextMenu();
|
|
763
|
+
setTimeout(() => {
|
|
764
|
+
document.addEventListener("mousedown", this._contextMenuClose);
|
|
765
|
+
}, 0);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
hideDisasmContextMenu() {
|
|
769
|
+
if (this._contextMenu) {
|
|
770
|
+
this._contextMenu.remove();
|
|
771
|
+
this._contextMenu = null;
|
|
772
|
+
}
|
|
773
|
+
if (this._contextMenuClose) {
|
|
774
|
+
document.removeEventListener("mousedown", this._contextMenuClose);
|
|
775
|
+
this._contextMenuClose = null;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Run to cursor - set temp breakpoint and resume
|
|
781
|
+
*/
|
|
782
|
+
runToCursor(addr) {
|
|
783
|
+
this.bpManager.setTemp(addr);
|
|
784
|
+
this.wasmModule._setPaused(false);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* Update all window content
|
|
789
|
+
*/
|
|
790
|
+
update(wasmModule) {
|
|
791
|
+
this.wasmModule = wasmModule;
|
|
792
|
+
|
|
793
|
+
const pc = this.wasmModule._getPC();
|
|
794
|
+
const isPaused = this.wasmModule._isPaused();
|
|
795
|
+
|
|
796
|
+
// Update status indicator
|
|
797
|
+
const statusEl = this.contentElement.querySelector("#dbg-status");
|
|
798
|
+
const statusBar = this.contentElement.querySelector(".cpu-dbg-status-bar");
|
|
799
|
+
if (statusEl) {
|
|
800
|
+
const emulatorOn = this.isRunningCallback();
|
|
801
|
+
if (!emulatorOn) {
|
|
802
|
+
statusEl.textContent = "EMULATOR OFF";
|
|
803
|
+
statusEl.classList.remove("running");
|
|
804
|
+
if (statusBar) statusBar.dataset.state = "off";
|
|
805
|
+
} else if (isPaused) {
|
|
806
|
+
statusEl.textContent = "PAUSED";
|
|
807
|
+
statusEl.classList.remove("running");
|
|
808
|
+
if (statusBar) statusBar.dataset.state = "paused";
|
|
809
|
+
} else {
|
|
810
|
+
statusEl.textContent = "RUNNING";
|
|
811
|
+
statusEl.classList.add("running");
|
|
812
|
+
if (statusBar) statusBar.dataset.state = "running";
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// Check temp breakpoint
|
|
817
|
+
this.bpManager.checkTemp(pc);
|
|
818
|
+
|
|
819
|
+
// Check if a watchpoint was hit - evaluate conditions/hit counts (range-aware)
|
|
820
|
+
if (
|
|
821
|
+
isPaused &&
|
|
822
|
+
this.wasmModule._isWatchpointHit &&
|
|
823
|
+
this.wasmModule._isWatchpointHit()
|
|
824
|
+
) {
|
|
825
|
+
const wpAddr = this.wasmModule._getWatchpointAddress();
|
|
826
|
+
const entry = this.bpManager.findByAddress(wpAddr);
|
|
827
|
+
if (entry) {
|
|
828
|
+
if (!this.bpManager.shouldBreakEntry(entry)) {
|
|
829
|
+
// Condition not met or hit target not reached - resume
|
|
830
|
+
this.wasmModule._setPaused(false);
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
this._hitBpAddr = entry.address;
|
|
834
|
+
} else if (!this.bpManager.shouldBreak(wpAddr)) {
|
|
835
|
+
// Fallback for direct-address match
|
|
836
|
+
this.wasmModule._setPaused(false);
|
|
837
|
+
return;
|
|
838
|
+
} else {
|
|
839
|
+
this._hitBpAddr = wpAddr;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// Check conditional breakpoint evaluation
|
|
844
|
+
if (
|
|
845
|
+
isPaused &&
|
|
846
|
+
this.wasmModule._isBreakpointHit &&
|
|
847
|
+
this.wasmModule._isBreakpointHit()
|
|
848
|
+
) {
|
|
849
|
+
const bpAddr = this.wasmModule._getBreakpointAddress();
|
|
850
|
+
if (!this.bpManager.shouldBreak(bpAddr)) {
|
|
851
|
+
// Condition not met - resume execution
|
|
852
|
+
this.wasmModule._setPaused(false);
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
this._hitBpAddr = bpAddr;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// Clear hit address when running
|
|
859
|
+
if (!isPaused) {
|
|
860
|
+
this._hitBpAddr = -1;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// If PC changed, snap disassembly back to follow PC
|
|
864
|
+
if (this.lastPC !== null && pc !== this.lastPC) {
|
|
865
|
+
this.disasmViewAddress = null;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
this.updateRegisters();
|
|
869
|
+
this.updateFlags();
|
|
870
|
+
this.updateIRQState();
|
|
871
|
+
if (isPaused) {
|
|
872
|
+
this.updateScanline();
|
|
873
|
+
} else {
|
|
874
|
+
this.clearScanline();
|
|
875
|
+
}
|
|
876
|
+
this.updateDisassembly();
|
|
877
|
+
this.updateWatchList();
|
|
878
|
+
this.updateBreakpointHitHighlight();
|
|
879
|
+
this.updateBeamHitHighlight();
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* Register definitions for display and editing
|
|
884
|
+
*/
|
|
885
|
+
static REGISTER_DEFS = [
|
|
886
|
+
{ id: "reg-a", fn: "_getA", setFn: "_setRegA", digits: 2 },
|
|
887
|
+
{ id: "reg-x", fn: "_getX", setFn: "_setRegX", digits: 2 },
|
|
888
|
+
{ id: "reg-y", fn: "_getY", setFn: "_setRegY", digits: 2 },
|
|
889
|
+
{ id: "reg-sp", fn: "_getSP", setFn: "_setRegSP", digits: 2 },
|
|
890
|
+
{ id: "reg-pc", fn: "_getPC", setFn: "_setRegPC", digits: 4 },
|
|
891
|
+
{ id: "cycle-count", fn: "_getTotalCycles", setFn: null, digits: 0 },
|
|
892
|
+
];
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* Update CPU register display
|
|
896
|
+
*/
|
|
897
|
+
updateRegisters() {
|
|
898
|
+
CPUDebuggerWindow.REGISTER_DEFS.forEach(({ id, fn, digits }) => {
|
|
899
|
+
const elem = this.contentElement.querySelector(`#${id}`);
|
|
900
|
+
if (elem && this.wasmModule[fn]) {
|
|
901
|
+
// Don't update if we're currently editing this register
|
|
902
|
+
if (elem.dataset.editing === "true") return;
|
|
903
|
+
const value = this.wasmModule[fn]();
|
|
904
|
+
const text =
|
|
905
|
+
digits > 0 ? this.formatHex(value, digits) : value.toString();
|
|
906
|
+
|
|
907
|
+
// Highlight changes
|
|
908
|
+
const prevVal = this.previousRegisters[id];
|
|
909
|
+
if (prevVal !== undefined && prevVal !== text) {
|
|
910
|
+
elem.classList.remove("changed");
|
|
911
|
+
// Force reflow to restart animation
|
|
912
|
+
void elem.offsetWidth;
|
|
913
|
+
elem.classList.add("changed");
|
|
914
|
+
}
|
|
915
|
+
this.previousRegisters[id] = text;
|
|
916
|
+
|
|
917
|
+
elem.textContent = text;
|
|
918
|
+
}
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/**
|
|
923
|
+
* Set up register editing - click a register value to edit it
|
|
924
|
+
*/
|
|
925
|
+
setupRegisterEditing() {
|
|
926
|
+
CPUDebuggerWindow.REGISTER_DEFS.forEach(({ id, setFn, digits }) => {
|
|
927
|
+
if (!setFn) return; // Skip non-editable registers like cycle count
|
|
928
|
+
const elem = this.contentElement.querySelector(`#${id}`);
|
|
929
|
+
if (!elem) return;
|
|
930
|
+
|
|
931
|
+
elem.style.cursor = "pointer";
|
|
932
|
+
elem.title = "Click to edit";
|
|
933
|
+
|
|
934
|
+
elem.addEventListener("dblclick", (e) => {
|
|
935
|
+
e.stopPropagation();
|
|
936
|
+
if (!this.wasmModule._isPaused()) return;
|
|
937
|
+
if (elem.dataset.editing === "true") return;
|
|
938
|
+
|
|
939
|
+
const currentValue = elem.textContent;
|
|
940
|
+
elem.dataset.editing = "true";
|
|
941
|
+
|
|
942
|
+
const input = document.createElement("input");
|
|
943
|
+
input.type = "text";
|
|
944
|
+
input.value = currentValue;
|
|
945
|
+
input.maxLength = digits;
|
|
946
|
+
input.className = "cpu-reg-edit-input";
|
|
947
|
+
input.style.cssText = `
|
|
948
|
+
width: ${digits * 8 + 8}px;
|
|
949
|
+
font-family: var(--font-mono);
|
|
950
|
+
font-size: 12px;
|
|
951
|
+
font-weight: 600;
|
|
952
|
+
color: var(--accent-green);
|
|
953
|
+
background: var(--input-bg-deeper);
|
|
954
|
+
border: 1px solid var(--accent-blue);
|
|
955
|
+
border-radius: 2px;
|
|
956
|
+
padding: 0 2px;
|
|
957
|
+
text-align: center;
|
|
958
|
+
outline: none;
|
|
959
|
+
`;
|
|
960
|
+
|
|
961
|
+
elem.textContent = "";
|
|
962
|
+
elem.appendChild(input);
|
|
963
|
+
input.focus();
|
|
964
|
+
input.select();
|
|
965
|
+
|
|
966
|
+
const commit = () => {
|
|
967
|
+
const val = parseInt(input.value, 16);
|
|
968
|
+
const maxVal = digits === 4 ? 0xffff : 0xff;
|
|
969
|
+
if (!isNaN(val) && val >= 0 && val <= maxVal) {
|
|
970
|
+
this.wasmModule[setFn](val);
|
|
971
|
+
}
|
|
972
|
+
elem.dataset.editing = "false";
|
|
973
|
+
input.remove();
|
|
974
|
+
this.updateRegisters();
|
|
975
|
+
this.updateDisassembly();
|
|
976
|
+
};
|
|
977
|
+
|
|
978
|
+
const cancel = () => {
|
|
979
|
+
elem.dataset.editing = "false";
|
|
980
|
+
input.remove();
|
|
981
|
+
this.updateRegisters();
|
|
982
|
+
};
|
|
983
|
+
|
|
984
|
+
input.addEventListener("keydown", (ev) => {
|
|
985
|
+
if (ev.key === "Enter") {
|
|
986
|
+
ev.preventDefault();
|
|
987
|
+
commit();
|
|
988
|
+
} else if (ev.key === "Escape") {
|
|
989
|
+
ev.preventDefault();
|
|
990
|
+
cancel();
|
|
991
|
+
}
|
|
992
|
+
ev.stopPropagation();
|
|
993
|
+
});
|
|
994
|
+
input.addEventListener("blur", commit);
|
|
995
|
+
});
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/**
|
|
1000
|
+
* Set up beam breakpoint tab events
|
|
1001
|
+
*/
|
|
1002
|
+
setupBeamBreakTabEvents() {
|
|
1003
|
+
const modeSelect = this.contentElement.querySelector("#beam-mode-select");
|
|
1004
|
+
const scanInput = this.contentElement.querySelector("#beam-scan-input");
|
|
1005
|
+
const colInput = this.contentElement.querySelector("#beam-col-input");
|
|
1006
|
+
const addBtn = this.contentElement.querySelector("#beam-add-btn");
|
|
1007
|
+
if (!modeSelect) return;
|
|
1008
|
+
|
|
1009
|
+
const updateInputVisibility = () => {
|
|
1010
|
+
const mode = modeSelect.value;
|
|
1011
|
+
if (scanInput)
|
|
1012
|
+
scanInput.style.display =
|
|
1013
|
+
mode === "scanline" || mode === "scancol" ? "" : "none";
|
|
1014
|
+
if (colInput)
|
|
1015
|
+
colInput.style.display =
|
|
1016
|
+
mode === "column" || mode === "scancol" ? "" : "none";
|
|
1017
|
+
};
|
|
1018
|
+
|
|
1019
|
+
modeSelect.addEventListener("change", updateInputVisibility);
|
|
1020
|
+
updateInputVisibility();
|
|
1021
|
+
|
|
1022
|
+
if (addBtn) {
|
|
1023
|
+
addBtn.addEventListener("click", () => this.addBeamBreakpointFromForm());
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
const onInputKey = (e) => {
|
|
1027
|
+
if (e.key === "Enter") this.addBeamBreakpointFromForm();
|
|
1028
|
+
e.stopPropagation();
|
|
1029
|
+
};
|
|
1030
|
+
if (scanInput) scanInput.addEventListener("keydown", onInputKey);
|
|
1031
|
+
if (colInput) colInput.addEventListener("keydown", onInputKey);
|
|
1032
|
+
|
|
1033
|
+
// Event delegation on beam list for checkboxes and remove buttons
|
|
1034
|
+
const beamList = this.contentElement.querySelector("#beam-list");
|
|
1035
|
+
if (beamList) {
|
|
1036
|
+
beamList.addEventListener("mousedown", (e) => {
|
|
1037
|
+
const removeBtn = e.target.closest(".beam-remove");
|
|
1038
|
+
if (removeBtn) {
|
|
1039
|
+
e.stopPropagation();
|
|
1040
|
+
e.preventDefault();
|
|
1041
|
+
const item = removeBtn.closest(".cpu-beam-item");
|
|
1042
|
+
if (item && item.dataset.id) {
|
|
1043
|
+
this.removeBeamBreakpoint(parseInt(item.dataset.id, 10));
|
|
1044
|
+
}
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
});
|
|
1048
|
+
beamList.addEventListener("change", (e) => {
|
|
1049
|
+
const checkbox = e.target.closest(".beam-enable input");
|
|
1050
|
+
if (checkbox) {
|
|
1051
|
+
const item = checkbox.closest(".cpu-beam-item");
|
|
1052
|
+
if (item && item.dataset.id) {
|
|
1053
|
+
this.enableBeamBreakpoint(
|
|
1054
|
+
parseInt(item.dataset.id, 10),
|
|
1055
|
+
checkbox.checked,
|
|
1056
|
+
);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
/**
|
|
1064
|
+
* Add a beam breakpoint from the toolbar form
|
|
1065
|
+
*/
|
|
1066
|
+
addBeamBreakpointFromForm() {
|
|
1067
|
+
const modeSelect = this.contentElement.querySelector("#beam-mode-select");
|
|
1068
|
+
const scanInput = this.contentElement.querySelector("#beam-scan-input");
|
|
1069
|
+
const colInput = this.contentElement.querySelector("#beam-col-input");
|
|
1070
|
+
if (!modeSelect) return;
|
|
1071
|
+
|
|
1072
|
+
const mode = modeSelect.value;
|
|
1073
|
+
let scanline = -1;
|
|
1074
|
+
let hPos = -1;
|
|
1075
|
+
|
|
1076
|
+
switch (mode) {
|
|
1077
|
+
case "vbl":
|
|
1078
|
+
scanline = 192;
|
|
1079
|
+
hPos = 0;
|
|
1080
|
+
break;
|
|
1081
|
+
case "hblank":
|
|
1082
|
+
scanline = -1;
|
|
1083
|
+
hPos = 0;
|
|
1084
|
+
break;
|
|
1085
|
+
case "scanline": {
|
|
1086
|
+
const n = parseInt(scanInput?.value, 10);
|
|
1087
|
+
if (isNaN(n) || n < 0 || n > 261) return;
|
|
1088
|
+
scanline = n;
|
|
1089
|
+
hPos = -1;
|
|
1090
|
+
break;
|
|
1091
|
+
}
|
|
1092
|
+
case "column": {
|
|
1093
|
+
const c = parseInt(colInput?.value, 10);
|
|
1094
|
+
if (isNaN(c) || c < 0 || c > 39) return;
|
|
1095
|
+
scanline = -1;
|
|
1096
|
+
hPos = c + 25;
|
|
1097
|
+
break;
|
|
1098
|
+
}
|
|
1099
|
+
case "scancol": {
|
|
1100
|
+
const n = parseInt(scanInput?.value, 10);
|
|
1101
|
+
const c = parseInt(colInput?.value, 10);
|
|
1102
|
+
if (isNaN(n) || n < 0 || n > 261 || isNaN(c) || c < 0 || c > 39) return;
|
|
1103
|
+
scanline = n;
|
|
1104
|
+
hPos = c + 25;
|
|
1105
|
+
break;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
const id = this.wasmModule._addBeamBreakpoint(scanline, hPos);
|
|
1110
|
+
if (id < 0) return; // full
|
|
1111
|
+
|
|
1112
|
+
this.beamBreakpoints.push({ id, scanline, hPos, enabled: true, mode });
|
|
1113
|
+
this.saveBeamBreakpoints();
|
|
1114
|
+
this.updateBeamList();
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
/**
|
|
1118
|
+
* Remove a beam breakpoint by ID
|
|
1119
|
+
*/
|
|
1120
|
+
removeBeamBreakpoint(id) {
|
|
1121
|
+
this.wasmModule._removeBeamBreakpoint(id);
|
|
1122
|
+
this.beamBreakpoints = this.beamBreakpoints.filter((bp) => bp.id !== id);
|
|
1123
|
+
this.saveBeamBreakpoints();
|
|
1124
|
+
this.updateBeamList();
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
/**
|
|
1128
|
+
* Enable/disable a beam breakpoint by ID
|
|
1129
|
+
*/
|
|
1130
|
+
enableBeamBreakpoint(id, enabled) {
|
|
1131
|
+
this.wasmModule._enableBeamBreakpoint(id, enabled);
|
|
1132
|
+
const bp = this.beamBreakpoints.find((b) => b.id === id);
|
|
1133
|
+
if (bp) bp.enabled = enabled;
|
|
1134
|
+
this.saveBeamBreakpoints();
|
|
1135
|
+
this.updateBeamList();
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
/**
|
|
1139
|
+
* Render the beam breakpoint list and update the tab badge
|
|
1140
|
+
*/
|
|
1141
|
+
updateBeamList() {
|
|
1142
|
+
const list = this.contentElement.querySelector("#beam-list");
|
|
1143
|
+
if (!list) return;
|
|
1144
|
+
|
|
1145
|
+
list.innerHTML = "";
|
|
1146
|
+
const isPaused = this.wasmModule._isPaused();
|
|
1147
|
+
const hitId =
|
|
1148
|
+
isPaused &&
|
|
1149
|
+
this.wasmModule._isBeamBreakpointHit &&
|
|
1150
|
+
this.wasmModule._isBeamBreakpointHit()
|
|
1151
|
+
? this.wasmModule._getBeamBreakpointHitId()
|
|
1152
|
+
: -1;
|
|
1153
|
+
|
|
1154
|
+
for (const bp of this.beamBreakpoints) {
|
|
1155
|
+
const item = document.createElement("div");
|
|
1156
|
+
item.className = "cpu-beam-item";
|
|
1157
|
+
item.dataset.id = bp.id;
|
|
1158
|
+
if (!bp.enabled) item.classList.add("disabled");
|
|
1159
|
+
if (bp.id === hitId) item.classList.add("hit");
|
|
1160
|
+
|
|
1161
|
+
const { typeLabel, typeClass, detail } =
|
|
1162
|
+
this.getBeamBreakpointDisplay(bp);
|
|
1163
|
+
|
|
1164
|
+
item.innerHTML = `
|
|
1165
|
+
<span class="beam-enable"><input type="checkbox" ${bp.enabled ? "checked" : ""}></span>
|
|
1166
|
+
<span class="beam-type ${typeClass}">${typeLabel}</span>
|
|
1167
|
+
<span class="beam-detail">${detail}</span>
|
|
1168
|
+
<button class="beam-remove" title="Remove">×</button>
|
|
1169
|
+
`;
|
|
1170
|
+
list.appendChild(item);
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
if (this.beamBreakpoints.length === 0) {
|
|
1174
|
+
const empty = document.createElement("div");
|
|
1175
|
+
empty.className = "cpu-dbg-empty-state";
|
|
1176
|
+
empty.textContent =
|
|
1177
|
+
"Add beam breakpoints to pause at specific raster positions.";
|
|
1178
|
+
list.appendChild(empty);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
// Update tab badge count
|
|
1182
|
+
const badge = this.contentElement.querySelector("#beam-tab-count");
|
|
1183
|
+
if (badge) {
|
|
1184
|
+
badge.textContent = this.beamBreakpoints.length;
|
|
1185
|
+
badge.classList.toggle("has-items", this.beamBreakpoints.length > 0);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
/**
|
|
1190
|
+
* Highlight the breakpoint/watchpoint that triggered a pause
|
|
1191
|
+
*/
|
|
1192
|
+
updateBreakpointHitHighlight() {
|
|
1193
|
+
const addr = this._hitBpAddr;
|
|
1194
|
+
if (addr === this._lastHitBpAddr) return;
|
|
1195
|
+
this._lastHitBpAddr = addr;
|
|
1196
|
+
|
|
1197
|
+
const list = this.contentElement.querySelector("#breakpoint-list");
|
|
1198
|
+
if (list) {
|
|
1199
|
+
let hitItem = null;
|
|
1200
|
+
const items = list.querySelectorAll(".cpu-bp-item");
|
|
1201
|
+
for (const item of items) {
|
|
1202
|
+
const isHit = parseInt(item.dataset.addr, 10) === addr;
|
|
1203
|
+
if (isHit && !item.classList.contains("hit")) {
|
|
1204
|
+
item.classList.remove("hit");
|
|
1205
|
+
void item.offsetWidth; // force reflow to restart animation
|
|
1206
|
+
hitItem = item;
|
|
1207
|
+
}
|
|
1208
|
+
item.classList.toggle("hit", isHit);
|
|
1209
|
+
}
|
|
1210
|
+
if (hitItem) {
|
|
1211
|
+
hitItem.scrollIntoView({ block: "nearest", behavior: "smooth" });
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
// Pulse tab header if breakpoints panel is not active
|
|
1216
|
+
this._pulseTabHeader("breakpoints", addr >= 0);
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
/**
|
|
1220
|
+
* Lightweight hit highlight update — toggles .hit class without rebuilding DOM
|
|
1221
|
+
*/
|
|
1222
|
+
updateBeamHitHighlight() {
|
|
1223
|
+
const list = this.contentElement.querySelector("#beam-list");
|
|
1224
|
+
if (!list) return;
|
|
1225
|
+
|
|
1226
|
+
const isPaused = this.wasmModule._isPaused();
|
|
1227
|
+
const hitId =
|
|
1228
|
+
isPaused &&
|
|
1229
|
+
this.wasmModule._isBeamBreakpointHit &&
|
|
1230
|
+
this.wasmModule._isBeamBreakpointHit()
|
|
1231
|
+
? this.wasmModule._getBeamBreakpointHitId()
|
|
1232
|
+
: -1;
|
|
1233
|
+
|
|
1234
|
+
if (hitId === this._lastBeamHitId) return;
|
|
1235
|
+
this._lastBeamHitId = hitId;
|
|
1236
|
+
|
|
1237
|
+
const items = list.querySelectorAll(".cpu-beam-item");
|
|
1238
|
+
for (const item of items) {
|
|
1239
|
+
const id = parseInt(item.dataset.id, 10);
|
|
1240
|
+
item.classList.toggle("hit", id === hitId);
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
// Pulse tab header if beam panel is not active
|
|
1244
|
+
this._pulseTabHeader("beam", hitId >= 0);
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
/**
|
|
1248
|
+
* Pulse a tab header to draw attention when its panel is not active
|
|
1249
|
+
*/
|
|
1250
|
+
_pulseTabHeader(tabName, isHit) {
|
|
1251
|
+
const tab = this.contentElement.querySelector(
|
|
1252
|
+
`.cpu-dbg-tab[data-tab="${tabName}"]`,
|
|
1253
|
+
);
|
|
1254
|
+
if (!tab) return;
|
|
1255
|
+
|
|
1256
|
+
if (isHit && this.activeTab !== tabName) {
|
|
1257
|
+
if (!tab.classList.contains("hit-alert")) {
|
|
1258
|
+
tab.classList.add("hit-alert");
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
// hit-alert is cleared only when the user clicks the tab
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
/**
|
|
1265
|
+
* Get display info for a beam breakpoint
|
|
1266
|
+
*/
|
|
1267
|
+
getBeamBreakpointDisplay(bp) {
|
|
1268
|
+
const modeMap = {
|
|
1269
|
+
vbl: {
|
|
1270
|
+
typeLabel: "VBL",
|
|
1271
|
+
typeClass: "beam-type-vbl",
|
|
1272
|
+
detail: "Scanline 192",
|
|
1273
|
+
},
|
|
1274
|
+
hblank: {
|
|
1275
|
+
typeLabel: "HBL",
|
|
1276
|
+
typeClass: "beam-type-hbl",
|
|
1277
|
+
detail: "HPos 0",
|
|
1278
|
+
},
|
|
1279
|
+
scanline: {
|
|
1280
|
+
typeLabel: "SCAN",
|
|
1281
|
+
typeClass: "beam-type-scan",
|
|
1282
|
+
detail: `Row ${bp.scanline}`,
|
|
1283
|
+
},
|
|
1284
|
+
column: {
|
|
1285
|
+
typeLabel: "COL",
|
|
1286
|
+
typeClass: "beam-type-col",
|
|
1287
|
+
detail: `Col ${bp.hPos - 25}`,
|
|
1288
|
+
},
|
|
1289
|
+
scancol: {
|
|
1290
|
+
typeLabel: "S+C",
|
|
1291
|
+
typeClass: "beam-type-sc",
|
|
1292
|
+
detail: `Row ${bp.scanline}, Col ${bp.hPos - 25}`,
|
|
1293
|
+
},
|
|
1294
|
+
};
|
|
1295
|
+
return (
|
|
1296
|
+
modeMap[bp.mode] || {
|
|
1297
|
+
typeLabel: "?",
|
|
1298
|
+
typeClass: "",
|
|
1299
|
+
detail: `Scan ${bp.scanline}, HPos ${bp.hPos}`,
|
|
1300
|
+
}
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
/**
|
|
1305
|
+
* Update CPU flags display
|
|
1306
|
+
*/
|
|
1307
|
+
updateFlags() {
|
|
1308
|
+
const p = this.wasmModule._getP();
|
|
1309
|
+
const flags = [
|
|
1310
|
+
{ id: "flag-n", bit: 0x80 },
|
|
1311
|
+
{ id: "flag-v", bit: 0x40 },
|
|
1312
|
+
{ id: "flag-b", bit: 0x10 },
|
|
1313
|
+
{ id: "flag-d", bit: 0x08 },
|
|
1314
|
+
{ id: "flag-i", bit: 0x04 },
|
|
1315
|
+
{ id: "flag-z", bit: 0x02 },
|
|
1316
|
+
{ id: "flag-c", bit: 0x01 },
|
|
1317
|
+
];
|
|
1318
|
+
|
|
1319
|
+
flags.forEach(({ id, bit }) => {
|
|
1320
|
+
const elem = this.contentElement.querySelector(`#${id}`);
|
|
1321
|
+
if (elem) {
|
|
1322
|
+
elem.classList.toggle("active", (p & bit) !== 0);
|
|
1323
|
+
}
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
/**
|
|
1328
|
+
* Update IRQ/NMI state indicators
|
|
1329
|
+
*/
|
|
1330
|
+
updateIRQState() {
|
|
1331
|
+
const indicators = [
|
|
1332
|
+
{ id: "irq-pending", fn: "_isIRQPending" },
|
|
1333
|
+
{ id: "nmi-pending", fn: "_isNMIPending" },
|
|
1334
|
+
{ id: "nmi-edge", fn: "_isNMIEdge" },
|
|
1335
|
+
];
|
|
1336
|
+
|
|
1337
|
+
indicators.forEach(({ id, fn }) => {
|
|
1338
|
+
const elem = this.contentElement.querySelector(`#${id}`);
|
|
1339
|
+
if (elem && this.wasmModule[fn]) {
|
|
1340
|
+
elem.classList.toggle("active", this.wasmModule[fn]());
|
|
1341
|
+
}
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
/**
|
|
1346
|
+
* Cache beam element references to avoid querySelector every frame
|
|
1347
|
+
*/
|
|
1348
|
+
getBeamElements() {
|
|
1349
|
+
if (!this._beamEls) {
|
|
1350
|
+
this._beamEls = {
|
|
1351
|
+
scan: this.contentElement.querySelector("#scan-line"),
|
|
1352
|
+
hPos: this.contentElement.querySelector("#scan-hpos"),
|
|
1353
|
+
col: this.contentElement.querySelector("#scan-col"),
|
|
1354
|
+
fcyc: this.contentElement.querySelector("#scan-fcyc"),
|
|
1355
|
+
badge: this.contentElement.querySelector("#scan-badge"),
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
return this._beamEls;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
/**
|
|
1362
|
+
* Update scanline / beam position display.
|
|
1363
|
+
* Skips redundant DOM writes so browser title tooltips aren't interrupted.
|
|
1364
|
+
*/
|
|
1365
|
+
updateScanline() {
|
|
1366
|
+
const frameCycle = this.wasmModule._getFrameCycle();
|
|
1367
|
+
const scanline = this.wasmModule._getBeamScanline();
|
|
1368
|
+
const hPos = this.wasmModule._getBeamHPos();
|
|
1369
|
+
const col = this.wasmModule._getBeamColumn();
|
|
1370
|
+
const inVBL = this.wasmModule._isInVBL();
|
|
1371
|
+
const inHBLANK = this.wasmModule._isInHBLANK();
|
|
1372
|
+
|
|
1373
|
+
const els = this.getBeamElements();
|
|
1374
|
+
|
|
1375
|
+
const scanText = String(scanline);
|
|
1376
|
+
const hPosText = String(hPos);
|
|
1377
|
+
const colText = col >= 0 ? col.toString().padStart(2, "0") : "--";
|
|
1378
|
+
const fcycText = String(frameCycle);
|
|
1379
|
+
|
|
1380
|
+
if (els.scan && els.scan.textContent !== scanText) els.scan.textContent = scanText;
|
|
1381
|
+
if (els.hPos && els.hPos.textContent !== hPosText) els.hPos.textContent = hPosText;
|
|
1382
|
+
if (els.col && els.col.textContent !== colText) els.col.textContent = colText;
|
|
1383
|
+
if (els.fcyc && els.fcyc.textContent !== fcycText) els.fcyc.textContent = fcycText;
|
|
1384
|
+
|
|
1385
|
+
if (els.badge) {
|
|
1386
|
+
let text, cls;
|
|
1387
|
+
if (inVBL) {
|
|
1388
|
+
text = "VBL";
|
|
1389
|
+
cls = "scanline-badge scanline-badge-vbl";
|
|
1390
|
+
} else if (inHBLANK) {
|
|
1391
|
+
text = "HBLANK";
|
|
1392
|
+
cls = "scanline-badge scanline-badge-hblank";
|
|
1393
|
+
} else {
|
|
1394
|
+
text = "VISIBLE";
|
|
1395
|
+
cls = "scanline-badge scanline-badge-visible";
|
|
1396
|
+
}
|
|
1397
|
+
if (els.badge.textContent !== text) els.badge.textContent = text;
|
|
1398
|
+
if (els.badge.className !== cls) els.badge.className = cls;
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
/**
|
|
1403
|
+
* Clear scanline / beam position display to "--" while running.
|
|
1404
|
+
* Skips redundant DOM writes so browser title tooltips aren't interrupted.
|
|
1405
|
+
*/
|
|
1406
|
+
clearScanline() {
|
|
1407
|
+
const els = this.getBeamElements();
|
|
1408
|
+
|
|
1409
|
+
if (els.scan && els.scan.textContent !== "--") els.scan.textContent = "--";
|
|
1410
|
+
if (els.hPos && els.hPos.textContent !== "--") els.hPos.textContent = "--";
|
|
1411
|
+
if (els.col && els.col.textContent !== "--") els.col.textContent = "--";
|
|
1412
|
+
if (els.fcyc && els.fcyc.textContent !== "--") els.fcyc.textContent = "--";
|
|
1413
|
+
if (els.badge && els.badge.textContent !== "--") {
|
|
1414
|
+
els.badge.textContent = "--";
|
|
1415
|
+
els.badge.className = "scanline-badge scanline-badge-idle";
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
/**
|
|
1420
|
+
* Find a good starting address for disassembly that aligns with instruction boundaries.
|
|
1421
|
+
* Scans forward from a safe distance back to find valid instruction starts.
|
|
1422
|
+
*/
|
|
1423
|
+
findDisasmStartAddress(pc, instructionsBefore) {
|
|
1424
|
+
// Start from further back and scan forward to find instruction boundaries
|
|
1425
|
+
const maxLookback = instructionsBefore * 3 + 10; // Max bytes to look back
|
|
1426
|
+
let startAddr = Math.max(0, pc - maxLookback);
|
|
1427
|
+
|
|
1428
|
+
// Build a list of instruction addresses by scanning forward
|
|
1429
|
+
const addresses = [];
|
|
1430
|
+
let addr = startAddr;
|
|
1431
|
+
while (addr <= pc + 100 && addr <= 0xffff) {
|
|
1432
|
+
addresses.push(addr);
|
|
1433
|
+
const opcode = this.wasmModule._peekMemory(addr);
|
|
1434
|
+
addr += this.getInstructionLength(opcode);
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
// Find where PC falls in our list
|
|
1438
|
+
const pcIndex = addresses.indexOf(pc);
|
|
1439
|
+
if (pcIndex === -1) {
|
|
1440
|
+
// PC not aligned - find closest address before PC
|
|
1441
|
+
for (let i = addresses.length - 1; i >= 0; i--) {
|
|
1442
|
+
if (addresses[i] <= pc) {
|
|
1443
|
+
const startIndex = Math.max(0, i - instructionsBefore);
|
|
1444
|
+
return addresses[startIndex];
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
return Math.max(0, pc - 20);
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
// Return address that gives us instructionsBefore lines before PC
|
|
1451
|
+
const startIndex = Math.max(0, pcIndex - instructionsBefore);
|
|
1452
|
+
return addresses[startIndex];
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
/**
|
|
1456
|
+
* Update disassembly view
|
|
1457
|
+
*/
|
|
1458
|
+
updateDisassembly() {
|
|
1459
|
+
const view = this.contentElement.querySelector("#disasm-view");
|
|
1460
|
+
if (!view) return;
|
|
1461
|
+
|
|
1462
|
+
const pc = this.wasmModule._getPC();
|
|
1463
|
+
const totalLines = 24; // Total instructions to show
|
|
1464
|
+
const linesBefore = 6; // Instructions to show before center
|
|
1465
|
+
|
|
1466
|
+
// Cache profiling data if enabled
|
|
1467
|
+
this._profileMax = 0;
|
|
1468
|
+
this._profilePtr = 0;
|
|
1469
|
+
if (this.profileEnabled && this.wasmModule._getProfileCycles) {
|
|
1470
|
+
this._profilePtr = this.wasmModule._getProfileCycles();
|
|
1471
|
+
if (this._profilePtr) {
|
|
1472
|
+
// Find max for normalization by scanning a quick sample
|
|
1473
|
+
const heap32 = new Uint32Array(this.wasmModule.HEAPU8.buffer);
|
|
1474
|
+
const baseIdx = this._profilePtr >> 2;
|
|
1475
|
+
let max = 0;
|
|
1476
|
+
// Sample the profile to find max
|
|
1477
|
+
for (let i = 0; i < 65536; i += 64) {
|
|
1478
|
+
const v = heap32[baseIdx + i];
|
|
1479
|
+
if (v > max) max = v;
|
|
1480
|
+
}
|
|
1481
|
+
this._profileMax = max;
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
// Use custom view address or PC
|
|
1486
|
+
const centerAddr =
|
|
1487
|
+
this.disasmViewAddress !== null ? this.disasmViewAddress : pc;
|
|
1488
|
+
|
|
1489
|
+
// Find aligned start address
|
|
1490
|
+
const startAddr = this.findDisasmStartAddress(centerAddr, linesBefore);
|
|
1491
|
+
|
|
1492
|
+
view.innerHTML = "";
|
|
1493
|
+
let addr = startAddr;
|
|
1494
|
+
let pcLineElement = null;
|
|
1495
|
+
|
|
1496
|
+
// Disassemble instructions
|
|
1497
|
+
for (let i = 0; i < totalLines && addr <= 0xffff; i++) {
|
|
1498
|
+
// Check for label at this address - show on its own line
|
|
1499
|
+
const labelInfo = this.labelManager.getLabel(addr);
|
|
1500
|
+
if (labelInfo && labelInfo.name) {
|
|
1501
|
+
const labelLine = document.createElement("div");
|
|
1502
|
+
labelLine.className = "cpu-disasm-label-line";
|
|
1503
|
+
labelLine.textContent = labelInfo.name + ":";
|
|
1504
|
+
view.appendChild(labelLine);
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
const line = document.createElement("div");
|
|
1508
|
+
line.className = "cpu-disasm-line";
|
|
1509
|
+
line.dataset.addr = addr.toString(16);
|
|
1510
|
+
|
|
1511
|
+
const isCurrent = addr === pc;
|
|
1512
|
+
if (isCurrent) {
|
|
1513
|
+
line.classList.add("current");
|
|
1514
|
+
pcLineElement = line;
|
|
1515
|
+
}
|
|
1516
|
+
if (this.bpManager.has(addr)) {
|
|
1517
|
+
line.classList.add("breakpoint");
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
// Heat overlay from profiling
|
|
1521
|
+
if (this.profileEnabled && this._profileMax > 0) {
|
|
1522
|
+
const heat = this.getHeatLevel(addr);
|
|
1523
|
+
if (heat > 0) line.classList.add("heat-" + heat);
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
// Breakpoint gutter
|
|
1527
|
+
const gutterSpan = document.createElement("span");
|
|
1528
|
+
gutterSpan.className = "cpu-disasm-gutter";
|
|
1529
|
+
const isBookmarked = this.bookmarks.includes(addr);
|
|
1530
|
+
if (isBookmarked) {
|
|
1531
|
+
line.classList.add("bookmarked");
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
if (this.bpManager.has(addr)) {
|
|
1535
|
+
gutterSpan.innerHTML = '<span class="bp-dot"></span>';
|
|
1536
|
+
} else if (isCurrent) {
|
|
1537
|
+
gutterSpan.innerHTML = '<span class="pc-arrow">▶</span>';
|
|
1538
|
+
} else if (isBookmarked) {
|
|
1539
|
+
gutterSpan.innerHTML = '<span class="bm-star">★</span>';
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
// Get disassembly from WASM
|
|
1543
|
+
const disasm = this.wasmModule.UTF8ToString(
|
|
1544
|
+
this.wasmModule._disassembleAt(addr),
|
|
1545
|
+
);
|
|
1546
|
+
|
|
1547
|
+
// Parse: "AAAA: BB BB BB MMM OPERAND"
|
|
1548
|
+
const addrPart = disasm.substring(0, 4);
|
|
1549
|
+
const bytesPart = disasm.substring(6, 14).trim();
|
|
1550
|
+
const instrPart = disasm.substring(16);
|
|
1551
|
+
|
|
1552
|
+
const addrSpan = document.createElement("span");
|
|
1553
|
+
addrSpan.className = "cpu-disasm-addr";
|
|
1554
|
+
addrSpan.textContent = addrPart;
|
|
1555
|
+
|
|
1556
|
+
const bytesSpan = document.createElement("span");
|
|
1557
|
+
bytesSpan.className = "cpu-disasm-bytes";
|
|
1558
|
+
bytesSpan.textContent = bytesPart;
|
|
1559
|
+
|
|
1560
|
+
const instrSpan = document.createElement("span");
|
|
1561
|
+
instrSpan.className = "cpu-disasm-instr";
|
|
1562
|
+
|
|
1563
|
+
// Split mnemonic from operand for proper column alignment and color coding
|
|
1564
|
+
const spaceIdx = instrPart.indexOf(" ");
|
|
1565
|
+
const mnemonic =
|
|
1566
|
+
spaceIdx >= 0 ? instrPart.substring(0, spaceIdx) : instrPart;
|
|
1567
|
+
const operandStr = spaceIdx >= 0 ? instrPart.substring(spaceIdx + 1) : "";
|
|
1568
|
+
|
|
1569
|
+
const mnemonicSpan = document.createElement("span");
|
|
1570
|
+
mnemonicSpan.className = "cpu-disasm-mnemonic";
|
|
1571
|
+
if (CPUDebuggerWindow.FLOW_MNEMONICS.has(mnemonic)) {
|
|
1572
|
+
mnemonicSpan.classList.add("flow");
|
|
1573
|
+
}
|
|
1574
|
+
mnemonicSpan.textContent = mnemonic;
|
|
1575
|
+
instrSpan.appendChild(mnemonicSpan);
|
|
1576
|
+
|
|
1577
|
+
if (operandStr) {
|
|
1578
|
+
const operandSpan = document.createElement("span");
|
|
1579
|
+
operandSpan.className = "cpu-disasm-operand";
|
|
1580
|
+
operandSpan.innerHTML = this.symbolizeInstruction(operandStr);
|
|
1581
|
+
instrSpan.appendChild(operandSpan);
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
line.appendChild(gutterSpan);
|
|
1585
|
+
line.appendChild(addrSpan);
|
|
1586
|
+
line.appendChild(bytesSpan);
|
|
1587
|
+
line.appendChild(instrSpan);
|
|
1588
|
+
|
|
1589
|
+
// Inline comment
|
|
1590
|
+
if (labelInfo && labelInfo.comment) {
|
|
1591
|
+
const commentSpan = document.createElement("span");
|
|
1592
|
+
commentSpan.className = "cpu-disasm-comment";
|
|
1593
|
+
commentSpan.textContent = "; " + labelInfo.comment;
|
|
1594
|
+
line.appendChild(commentSpan);
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
view.appendChild(line);
|
|
1598
|
+
|
|
1599
|
+
// Advance to next instruction
|
|
1600
|
+
const opcode = this.wasmModule._peekMemory(addr);
|
|
1601
|
+
addr += this.getInstructionLength(opcode);
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
// Scroll to keep PC visible only while running — when paused, let user scroll freely
|
|
1605
|
+
if (pcLineElement && !this.wasmModule._isPaused()) {
|
|
1606
|
+
const viewRect = view.getBoundingClientRect();
|
|
1607
|
+
const lineRect = pcLineElement.getBoundingClientRect();
|
|
1608
|
+
|
|
1609
|
+
// Check if line is outside visible area
|
|
1610
|
+
if (lineRect.top < viewRect.top || lineRect.bottom > viewRect.bottom) {
|
|
1611
|
+
pcLineElement.scrollIntoView({ block: "center", behavior: "auto" });
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
this.lastPC = pc;
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
/**
|
|
1619
|
+
* Type icon map for breakpoint list display
|
|
1620
|
+
*/
|
|
1621
|
+
/**
|
|
1622
|
+
* Control flow mnemonics - colored differently in disassembly
|
|
1623
|
+
* to help developers track execution flow at a glance.
|
|
1624
|
+
*/
|
|
1625
|
+
static FLOW_MNEMONICS = new Set([
|
|
1626
|
+
"JMP",
|
|
1627
|
+
"JSR",
|
|
1628
|
+
"RTS",
|
|
1629
|
+
"RTI",
|
|
1630
|
+
"BRK",
|
|
1631
|
+
"BPL",
|
|
1632
|
+
"BMI",
|
|
1633
|
+
"BVC",
|
|
1634
|
+
"BVS",
|
|
1635
|
+
"BCC",
|
|
1636
|
+
"BCS",
|
|
1637
|
+
"BNE",
|
|
1638
|
+
"BEQ",
|
|
1639
|
+
"BRA",
|
|
1640
|
+
]);
|
|
1641
|
+
|
|
1642
|
+
static BP_TYPE_ICONS = {
|
|
1643
|
+
exec: "●",
|
|
1644
|
+
read: "R",
|
|
1645
|
+
write: "W",
|
|
1646
|
+
readwrite: "RW",
|
|
1647
|
+
};
|
|
1648
|
+
|
|
1649
|
+
static BP_TYPE_TITLES = {
|
|
1650
|
+
exec: "Execution breakpoint",
|
|
1651
|
+
read: "Read watchpoint",
|
|
1652
|
+
write: "Write watchpoint",
|
|
1653
|
+
readwrite: "Read/Write watchpoint",
|
|
1654
|
+
};
|
|
1655
|
+
|
|
1656
|
+
static SOFT_SWITCH_GROUPS = [
|
|
1657
|
+
{
|
|
1658
|
+
category: "Display",
|
|
1659
|
+
switches: [
|
|
1660
|
+
{
|
|
1661
|
+
name: "TEXT",
|
|
1662
|
+
start: 0xc050,
|
|
1663
|
+
end: 0xc051,
|
|
1664
|
+
desc: "Text/Graphics mode",
|
|
1665
|
+
},
|
|
1666
|
+
{
|
|
1667
|
+
name: "MIXED",
|
|
1668
|
+
start: 0xc052,
|
|
1669
|
+
end: 0xc053,
|
|
1670
|
+
desc: "Mixed text+graphics",
|
|
1671
|
+
},
|
|
1672
|
+
{ name: "PAGE2", start: 0xc054, end: 0xc055, desc: "Display page 2" },
|
|
1673
|
+
{ name: "HIRES", start: 0xc056, end: 0xc057, desc: "Hi-res graphics" },
|
|
1674
|
+
{
|
|
1675
|
+
name: "80COL",
|
|
1676
|
+
start: 0xc00c,
|
|
1677
|
+
end: 0xc00d,
|
|
1678
|
+
desc: "80-column display",
|
|
1679
|
+
},
|
|
1680
|
+
{
|
|
1681
|
+
name: "ALTCHAR",
|
|
1682
|
+
start: 0xc00e,
|
|
1683
|
+
end: 0xc00f,
|
|
1684
|
+
desc: "Alt charset (MouseText)",
|
|
1685
|
+
},
|
|
1686
|
+
],
|
|
1687
|
+
},
|
|
1688
|
+
{
|
|
1689
|
+
category: "Memory Banking",
|
|
1690
|
+
switches: [
|
|
1691
|
+
{
|
|
1692
|
+
name: "80STORE",
|
|
1693
|
+
start: 0xc000,
|
|
1694
|
+
end: 0xc001,
|
|
1695
|
+
desc: "PAGE2 selects aux memory",
|
|
1696
|
+
},
|
|
1697
|
+
{
|
|
1698
|
+
name: "RAMRD",
|
|
1699
|
+
start: 0xc002,
|
|
1700
|
+
end: 0xc003,
|
|
1701
|
+
desc: "Read from aux RAM",
|
|
1702
|
+
},
|
|
1703
|
+
{
|
|
1704
|
+
name: "RAMWRT",
|
|
1705
|
+
start: 0xc004,
|
|
1706
|
+
end: 0xc005,
|
|
1707
|
+
desc: "Write to aux RAM",
|
|
1708
|
+
},
|
|
1709
|
+
{
|
|
1710
|
+
name: "INTCXROM",
|
|
1711
|
+
start: 0xc006,
|
|
1712
|
+
end: 0xc007,
|
|
1713
|
+
desc: "$Cxxx ROM source",
|
|
1714
|
+
},
|
|
1715
|
+
{
|
|
1716
|
+
name: "ALTZP",
|
|
1717
|
+
start: 0xc008,
|
|
1718
|
+
end: 0xc009,
|
|
1719
|
+
desc: "Aux zero page/stack",
|
|
1720
|
+
},
|
|
1721
|
+
{ name: "SLOTC3ROM", start: 0xc00a, end: 0xc00b, desc: "Slot 3 ROM" },
|
|
1722
|
+
],
|
|
1723
|
+
},
|
|
1724
|
+
{
|
|
1725
|
+
category: "Language Card",
|
|
1726
|
+
switches: [
|
|
1727
|
+
{
|
|
1728
|
+
name: "LANGCARD",
|
|
1729
|
+
start: 0xc080,
|
|
1730
|
+
end: 0xc08f,
|
|
1731
|
+
desc: "Language card control",
|
|
1732
|
+
},
|
|
1733
|
+
],
|
|
1734
|
+
},
|
|
1735
|
+
{
|
|
1736
|
+
category: "Annunciators",
|
|
1737
|
+
switches: [
|
|
1738
|
+
{ name: "AN0", start: 0xc058, end: 0xc059, desc: "Annunciator 0" },
|
|
1739
|
+
{ name: "AN1", start: 0xc05a, end: 0xc05b, desc: "Annunciator 1" },
|
|
1740
|
+
{ name: "AN2", start: 0xc05c, end: 0xc05d, desc: "Annunciator 2" },
|
|
1741
|
+
{
|
|
1742
|
+
name: "AN3",
|
|
1743
|
+
start: 0xc05e,
|
|
1744
|
+
end: 0xc05f,
|
|
1745
|
+
desc: "Annunciator 3 / DHIRES",
|
|
1746
|
+
},
|
|
1747
|
+
],
|
|
1748
|
+
},
|
|
1749
|
+
{
|
|
1750
|
+
category: "I/O",
|
|
1751
|
+
switches: [
|
|
1752
|
+
{ name: "KBD", start: 0xc000, end: 0xc000, desc: "Keyboard data" },
|
|
1753
|
+
{
|
|
1754
|
+
name: "KBDSTRB",
|
|
1755
|
+
start: 0xc010,
|
|
1756
|
+
end: 0xc010,
|
|
1757
|
+
desc: "Clear keyboard strobe",
|
|
1758
|
+
},
|
|
1759
|
+
{ name: "SPKR", start: 0xc030, end: 0xc030, desc: "Speaker toggle" },
|
|
1760
|
+
{ name: "PTRIG", start: 0xc070, end: 0xc070, desc: "Paddle trigger" },
|
|
1761
|
+
],
|
|
1762
|
+
},
|
|
1763
|
+
{
|
|
1764
|
+
category: "Status Registers",
|
|
1765
|
+
switches: [
|
|
1766
|
+
{
|
|
1767
|
+
name: "RDLCBNK2",
|
|
1768
|
+
start: 0xc011,
|
|
1769
|
+
end: 0xc011,
|
|
1770
|
+
desc: "LC bank 2 status",
|
|
1771
|
+
},
|
|
1772
|
+
{
|
|
1773
|
+
name: "RDLCRAM",
|
|
1774
|
+
start: 0xc012,
|
|
1775
|
+
end: 0xc012,
|
|
1776
|
+
desc: "LC RAM read status",
|
|
1777
|
+
},
|
|
1778
|
+
{ name: "RDRAMRD", start: 0xc013, end: 0xc013, desc: "RAMRD status" },
|
|
1779
|
+
{ name: "RDRAMWRT", start: 0xc014, end: 0xc014, desc: "RAMWRT status" },
|
|
1780
|
+
{
|
|
1781
|
+
name: "RDCXROM",
|
|
1782
|
+
start: 0xc015,
|
|
1783
|
+
end: 0xc015,
|
|
1784
|
+
desc: "INTCXROM status",
|
|
1785
|
+
},
|
|
1786
|
+
{ name: "RDALTZP", start: 0xc016, end: 0xc016, desc: "ALTZP status" },
|
|
1787
|
+
{
|
|
1788
|
+
name: "RDC3ROM",
|
|
1789
|
+
start: 0xc017,
|
|
1790
|
+
end: 0xc017,
|
|
1791
|
+
desc: "SLOTC3ROM status",
|
|
1792
|
+
},
|
|
1793
|
+
{
|
|
1794
|
+
name: "RD80STORE",
|
|
1795
|
+
start: 0xc018,
|
|
1796
|
+
end: 0xc018,
|
|
1797
|
+
desc: "80STORE status",
|
|
1798
|
+
},
|
|
1799
|
+
{
|
|
1800
|
+
name: "RDVBL",
|
|
1801
|
+
start: 0xc019,
|
|
1802
|
+
end: 0xc019,
|
|
1803
|
+
desc: "Vertical blank status",
|
|
1804
|
+
},
|
|
1805
|
+
{
|
|
1806
|
+
name: "RDTEXT",
|
|
1807
|
+
start: 0xc01a,
|
|
1808
|
+
end: 0xc01a,
|
|
1809
|
+
desc: "TEXT mode status",
|
|
1810
|
+
},
|
|
1811
|
+
{
|
|
1812
|
+
name: "RDMIXED",
|
|
1813
|
+
start: 0xc01b,
|
|
1814
|
+
end: 0xc01b,
|
|
1815
|
+
desc: "MIXED mode status",
|
|
1816
|
+
},
|
|
1817
|
+
{ name: "RDPAGE2", start: 0xc01c, end: 0xc01c, desc: "PAGE2 status" },
|
|
1818
|
+
{
|
|
1819
|
+
name: "RDHIRES",
|
|
1820
|
+
start: 0xc01d,
|
|
1821
|
+
end: 0xc01d,
|
|
1822
|
+
desc: "HIRES mode status",
|
|
1823
|
+
},
|
|
1824
|
+
{
|
|
1825
|
+
name: "RDALTCHAR",
|
|
1826
|
+
start: 0xc01e,
|
|
1827
|
+
end: 0xc01e,
|
|
1828
|
+
desc: "ALTCHAR status",
|
|
1829
|
+
},
|
|
1830
|
+
{ name: "RD80COL", start: 0xc01f, end: 0xc01f, desc: "80COL status" },
|
|
1831
|
+
],
|
|
1832
|
+
},
|
|
1833
|
+
];
|
|
1834
|
+
|
|
1835
|
+
/**
|
|
1836
|
+
* Update breakpoint list with rich UI
|
|
1837
|
+
*/
|
|
1838
|
+
updateBreakpointList() {
|
|
1839
|
+
const list = this.contentElement.querySelector("#breakpoint-list");
|
|
1840
|
+
if (!list) return;
|
|
1841
|
+
|
|
1842
|
+
list.innerHTML = "";
|
|
1843
|
+
const allBps = this.bpManager.getAll();
|
|
1844
|
+
let count = 0;
|
|
1845
|
+
|
|
1846
|
+
for (const [addr, entry] of allBps) {
|
|
1847
|
+
if (entry.isTemp) continue;
|
|
1848
|
+
count++;
|
|
1849
|
+
|
|
1850
|
+
const item = document.createElement("div");
|
|
1851
|
+
item.className = "cpu-bp-item";
|
|
1852
|
+
item.dataset.addr = addr;
|
|
1853
|
+
if (!entry.enabled) item.classList.add("disabled");
|
|
1854
|
+
if (addr === this._hitBpAddr) item.classList.add("hit");
|
|
1855
|
+
|
|
1856
|
+
const typeIcon = CPUDebuggerWindow.BP_TYPE_ICONS[entry.type] || "●";
|
|
1857
|
+
const typeTitle = CPUDebuggerWindow.BP_TYPE_TITLES[entry.type] || "";
|
|
1858
|
+
const typeClass =
|
|
1859
|
+
entry.type === "exec" ? "bp-type-exec" : "bp-type-watch";
|
|
1860
|
+
|
|
1861
|
+
// Check if this is a named soft switch breakpoint with a range
|
|
1862
|
+
const isRange =
|
|
1863
|
+
entry.endAddress != null && entry.endAddress !== entry.address;
|
|
1864
|
+
const hasName = !!entry.name;
|
|
1865
|
+
|
|
1866
|
+
let html = `
|
|
1867
|
+
<span class="bp-enable" title="Toggle enable">
|
|
1868
|
+
<input type="checkbox" ${entry.enabled ? "checked" : ""}>
|
|
1869
|
+
</span>
|
|
1870
|
+
<span class="bp-type ${typeClass}" title="${typeTitle}">${typeIcon}</span>
|
|
1871
|
+
`;
|
|
1872
|
+
|
|
1873
|
+
if (hasName) {
|
|
1874
|
+
const startHex = this.formatHex(entry.address, 4);
|
|
1875
|
+
const endHex = this.formatHex(entry.endAddress, 4);
|
|
1876
|
+
const rangeStr = isRange ? `$${startHex}-${endHex}` : `$${startHex}`;
|
|
1877
|
+
html += `<span class="bp-name" title="${rangeStr}">${entry.name}</span>`;
|
|
1878
|
+
html += `<span class="bp-range">${rangeStr}</span>`;
|
|
1879
|
+
} else {
|
|
1880
|
+
html += `<span class="bp-addr">${this.formatAddr(addr)}</span>`;
|
|
1881
|
+
// Symbol name for address
|
|
1882
|
+
const symbolInfo = getSymbolInfo(addr);
|
|
1883
|
+
const label = symbolInfo ? symbolInfo.name : "";
|
|
1884
|
+
if (label) {
|
|
1885
|
+
html += `<span class="bp-label" title="${label}">${label}</span>`;
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
if (entry.condition) {
|
|
1890
|
+
html += `<span class="bp-cond" title="Condition: ${entry.condition}">if</span>`;
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
if (entry.hitCount > 0 || entry.hitTarget > 0) {
|
|
1894
|
+
const hitText =
|
|
1895
|
+
entry.hitTarget > 0
|
|
1896
|
+
? `${entry.hitCount}/${entry.hitTarget}`
|
|
1897
|
+
: `${entry.hitCount}`;
|
|
1898
|
+
html += `<span class="bp-hits" title="Hit count">${hitText}</span>`;
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
html += `<button class="bp-edit" title="Edit condition">if…</button>`;
|
|
1902
|
+
html += `<button class="bp-remove" title="Remove">×</button>`;
|
|
1903
|
+
|
|
1904
|
+
item.innerHTML = html;
|
|
1905
|
+
list.appendChild(item);
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
if (count === 0) {
|
|
1909
|
+
const empty = document.createElement("div");
|
|
1910
|
+
empty.className = "cpu-dbg-empty-state";
|
|
1911
|
+
empty.textContent =
|
|
1912
|
+
"Click a disassembly line to toggle a breakpoint, or add one above.";
|
|
1913
|
+
list.appendChild(empty);
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
// Update tab badge count
|
|
1917
|
+
const badge = this.contentElement.querySelector("#bp-tab-count");
|
|
1918
|
+
if (badge) {
|
|
1919
|
+
badge.textContent = count;
|
|
1920
|
+
badge.classList.toggle("has-items", count > 0);
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
/**
|
|
1925
|
+
* Set the Rule Builder window reference
|
|
1926
|
+
*/
|
|
1927
|
+
setRuleBuilder(ruleBuilderWindow) {
|
|
1928
|
+
this.ruleBuilder = ruleBuilderWindow;
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
/**
|
|
1932
|
+
* Edit condition on a breakpoint via Rule Builder or prompt fallback
|
|
1933
|
+
*/
|
|
1934
|
+
editBreakpointCondition(addr) {
|
|
1935
|
+
const entry = this.bpManager.get(addr);
|
|
1936
|
+
if (!entry) return;
|
|
1937
|
+
|
|
1938
|
+
if (this.ruleBuilder) {
|
|
1939
|
+
this.ruleBuilder.editBreakpoint(addr, entry);
|
|
1940
|
+
} else {
|
|
1941
|
+
// Fallback to prompt if Rule Builder not wired
|
|
1942
|
+
const condition = prompt(
|
|
1943
|
+
`Condition for breakpoint at $${this.formatHex(addr, 4)}:\n` +
|
|
1944
|
+
`Examples: A==#$FF, PEEK($00)==#$42, C==1 && X>=#$10`,
|
|
1945
|
+
entry.condition || "",
|
|
1946
|
+
);
|
|
1947
|
+
if (condition !== null) {
|
|
1948
|
+
this.bpManager.setCondition(addr, condition);
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
/**
|
|
1954
|
+
* Import a symbol file via file picker
|
|
1955
|
+
*/
|
|
1956
|
+
importSymbolFile() {
|
|
1957
|
+
const input = document.createElement("input");
|
|
1958
|
+
input.type = "file";
|
|
1959
|
+
input.accept = ".dbg,.sym,.txt,.labels,.map";
|
|
1960
|
+
input.addEventListener("change", () => {
|
|
1961
|
+
const file = input.files[0];
|
|
1962
|
+
if (!file) return;
|
|
1963
|
+
const reader = new FileReader();
|
|
1964
|
+
reader.onload = () => {
|
|
1965
|
+
const count = this.labelManager.importSymbolFile(
|
|
1966
|
+
reader.result,
|
|
1967
|
+
file.name,
|
|
1968
|
+
);
|
|
1969
|
+
this.updateDisassembly();
|
|
1970
|
+
console.log(`Imported ${count} symbols from ${file.name}`);
|
|
1971
|
+
};
|
|
1972
|
+
reader.readAsText(file);
|
|
1973
|
+
});
|
|
1974
|
+
input.click();
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
/**
|
|
1978
|
+
* Edit inline comment at an address
|
|
1979
|
+
*/
|
|
1980
|
+
editInlineComment(addr) {
|
|
1981
|
+
const labelInfo = this.labelManager.getLabel(addr);
|
|
1982
|
+
const currentComment = labelInfo ? labelInfo.comment : "";
|
|
1983
|
+
|
|
1984
|
+
const comment = prompt(
|
|
1985
|
+
`Comment for $${this.formatHex(addr, 4)}:`,
|
|
1986
|
+
currentComment,
|
|
1987
|
+
);
|
|
1988
|
+
|
|
1989
|
+
if (comment !== null) {
|
|
1990
|
+
if (comment === "" && labelInfo && !labelInfo.name) {
|
|
1991
|
+
// No comment and no label - remove the entry
|
|
1992
|
+
this.labelManager.removeLabel(addr);
|
|
1993
|
+
} else {
|
|
1994
|
+
this.labelManager.setComment(addr, comment);
|
|
1995
|
+
}
|
|
1996
|
+
this.updateDisassembly();
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
// ---- Watch Expressions ----
|
|
2001
|
+
|
|
2002
|
+
static WATCH_STORAGE_KEY = "a2e-watch-expressions";
|
|
2003
|
+
|
|
2004
|
+
loadWatchExpressions() {
|
|
2005
|
+
try {
|
|
2006
|
+
const saved = localStorage.getItem(CPUDebuggerWindow.WATCH_STORAGE_KEY);
|
|
2007
|
+
if (saved) this.watchExpressions = JSON.parse(saved);
|
|
2008
|
+
} catch (e) {
|
|
2009
|
+
/* ignore */
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
saveWatchExpressions() {
|
|
2014
|
+
try {
|
|
2015
|
+
localStorage.setItem(
|
|
2016
|
+
CPUDebuggerWindow.WATCH_STORAGE_KEY,
|
|
2017
|
+
JSON.stringify(this.watchExpressions),
|
|
2018
|
+
);
|
|
2019
|
+
} catch (e) {
|
|
2020
|
+
/* ignore */
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
static REGISTER_NAMES = new Set(["A", "X", "Y", "SP", "PC", "P"]);
|
|
2025
|
+
|
|
2026
|
+
static FLAG_LABELS = {
|
|
2027
|
+
N: "Negative",
|
|
2028
|
+
V: "Overflow",
|
|
2029
|
+
B: "Break",
|
|
2030
|
+
D: "Decimal",
|
|
2031
|
+
I: "Interrupt",
|
|
2032
|
+
Z: "Zero",
|
|
2033
|
+
C: "Carry",
|
|
2034
|
+
};
|
|
2035
|
+
|
|
2036
|
+
/**
|
|
2037
|
+
* Build a friendly display label and type icon for a watch expression
|
|
2038
|
+
*/
|
|
2039
|
+
getWatchLabel(expr) {
|
|
2040
|
+
// Register: single token like "A", "X", "PC"
|
|
2041
|
+
if (CPUDebuggerWindow.REGISTER_NAMES.has(expr)) {
|
|
2042
|
+
return { icon: "R", iconClass: "watch-icon-reg", label: expr };
|
|
2043
|
+
}
|
|
2044
|
+
// Flag: single letter N/V/B/D/I/Z/C
|
|
2045
|
+
if (CPUDebuggerWindow.FLAG_LABELS[expr]) {
|
|
2046
|
+
return {
|
|
2047
|
+
icon: "F",
|
|
2048
|
+
iconClass: "watch-icon-flag",
|
|
2049
|
+
label: CPUDebuggerWindow.FLAG_LABELS[expr],
|
|
2050
|
+
};
|
|
2051
|
+
}
|
|
2052
|
+
// PEEK($XXXX) → byte
|
|
2053
|
+
const peekMatch = expr.match(/^PEEK\(\$([0-9A-Fa-f]{1,4})\)$/);
|
|
2054
|
+
if (peekMatch) {
|
|
2055
|
+
return {
|
|
2056
|
+
icon: "B",
|
|
2057
|
+
iconClass: "watch-icon-byte",
|
|
2058
|
+
label: `$${peekMatch[1].toUpperCase()} byte`,
|
|
2059
|
+
};
|
|
2060
|
+
}
|
|
2061
|
+
// DEEK($XXXX) → word
|
|
2062
|
+
const deekMatch = expr.match(/^DEEK\(\$([0-9A-Fa-f]{1,4})\)$/);
|
|
2063
|
+
if (deekMatch) {
|
|
2064
|
+
return {
|
|
2065
|
+
icon: "W",
|
|
2066
|
+
iconClass: "watch-icon-word",
|
|
2067
|
+
label: `$${deekMatch[1].toUpperCase()} word`,
|
|
2068
|
+
};
|
|
2069
|
+
}
|
|
2070
|
+
// Legacy / custom expression
|
|
2071
|
+
return { icon: "E", iconClass: "watch-icon-expr", label: expr };
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
/**
|
|
2075
|
+
* Format a watch value for display — always shows hex + decimal
|
|
2076
|
+
*/
|
|
2077
|
+
formatWatchValue(expr, value) {
|
|
2078
|
+
// Flags: show 0/1 plus set/clear label
|
|
2079
|
+
if (CPUDebuggerWindow.FLAG_LABELS[expr]) {
|
|
2080
|
+
return value ? "1 (set)" : "0 (clear)";
|
|
2081
|
+
}
|
|
2082
|
+
// Word values (DEEK)
|
|
2083
|
+
if (/^DEEK\(/.test(expr)) {
|
|
2084
|
+
return `$${this.formatHex(value & 0xffff, 4)} (${value & 0xffff})`;
|
|
2085
|
+
}
|
|
2086
|
+
// Byte values and registers
|
|
2087
|
+
if (value >= 0 && value <= 0xff) {
|
|
2088
|
+
return `$${this.formatHex(value & 0xff, 2)} (${value & 0xff})`;
|
|
2089
|
+
}
|
|
2090
|
+
return `$${this.formatHex(value & 0xffff, 4)} (${value & 0xffff})`;
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
updateWatchList() {
|
|
2094
|
+
const list = this.contentElement.querySelector("#watch-list");
|
|
2095
|
+
if (!list) return;
|
|
2096
|
+
|
|
2097
|
+
// Check if DOM structure matches expressions (needs full rebuild if not)
|
|
2098
|
+
const existingItems = list.querySelectorAll(".cpu-watch-item");
|
|
2099
|
+
const needsRebuild = existingItems.length !== this.watchExpressions.length;
|
|
2100
|
+
|
|
2101
|
+
if (needsRebuild) {
|
|
2102
|
+
list.innerHTML = "";
|
|
2103
|
+
this.previousWatchValues = {};
|
|
2104
|
+
|
|
2105
|
+
for (let i = 0; i < this.watchExpressions.length; i++) {
|
|
2106
|
+
const expr = this.watchExpressions[i];
|
|
2107
|
+
let valueStr;
|
|
2108
|
+
try {
|
|
2109
|
+
const value = this.bpManager.evaluateValue(expr);
|
|
2110
|
+
valueStr = this.formatWatchValue(expr, value);
|
|
2111
|
+
this.previousWatchValues[i] = valueStr;
|
|
2112
|
+
} catch (e) {
|
|
2113
|
+
valueStr = `err: ${e.message}`;
|
|
2114
|
+
this.previousWatchValues[i] = valueStr;
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
const { icon, iconClass, label } = this.getWatchLabel(expr);
|
|
2118
|
+
const item = document.createElement("div");
|
|
2119
|
+
item.className = "cpu-watch-item";
|
|
2120
|
+
item.dataset.index = i;
|
|
2121
|
+
item.innerHTML = `
|
|
2122
|
+
<span class="watch-type-icon ${iconClass}" title="${expr}">${icon}</span>
|
|
2123
|
+
<span class="watch-expr" title="${expr}">${label}</span>
|
|
2124
|
+
<span class="watch-value">${valueStr}</span>
|
|
2125
|
+
<button class="watch-remove" title="Remove">×</button>
|
|
2126
|
+
`;
|
|
2127
|
+
list.appendChild(item);
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
if (this.watchExpressions.length === 0) {
|
|
2131
|
+
const empty = document.createElement("div");
|
|
2132
|
+
empty.className = "cpu-dbg-empty-state";
|
|
2133
|
+
empty.textContent =
|
|
2134
|
+
"Add watch expressions to monitor values during execution.";
|
|
2135
|
+
list.appendChild(empty);
|
|
2136
|
+
}
|
|
2137
|
+
} else {
|
|
2138
|
+
// In-place value update with change highlighting
|
|
2139
|
+
for (let i = 0; i < this.watchExpressions.length; i++) {
|
|
2140
|
+
const expr = this.watchExpressions[i];
|
|
2141
|
+
const item = existingItems[i];
|
|
2142
|
+
const valueEl = item.querySelector(".watch-value");
|
|
2143
|
+
if (!valueEl) continue;
|
|
2144
|
+
|
|
2145
|
+
let valueStr;
|
|
2146
|
+
try {
|
|
2147
|
+
const value = this.bpManager.evaluateValue(expr);
|
|
2148
|
+
valueStr = this.formatWatchValue(expr, value);
|
|
2149
|
+
} catch (e) {
|
|
2150
|
+
valueStr = `err: ${e.message}`;
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
const prevVal = this.previousWatchValues[i];
|
|
2154
|
+
if (prevVal !== undefined && prevVal !== valueStr) {
|
|
2155
|
+
valueEl.classList.remove("changed");
|
|
2156
|
+
void valueEl.offsetWidth; // force reflow to restart animation
|
|
2157
|
+
valueEl.classList.add("changed");
|
|
2158
|
+
}
|
|
2159
|
+
this.previousWatchValues[i] = valueStr;
|
|
2160
|
+
valueEl.textContent = valueStr;
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
// Update tab badge count
|
|
2165
|
+
const badge = this.contentElement.querySelector("#watch-tab-count");
|
|
2166
|
+
if (badge) {
|
|
2167
|
+
badge.textContent = this.watchExpressions.length;
|
|
2168
|
+
badge.classList.toggle("has-items", this.watchExpressions.length > 0);
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
// ---- Address Bookmarks ----
|
|
2173
|
+
|
|
2174
|
+
static BOOKMARK_STORAGE_KEY = "a2e-bookmarks";
|
|
2175
|
+
|
|
2176
|
+
loadBookmarks() {
|
|
2177
|
+
try {
|
|
2178
|
+
const saved = localStorage.getItem(
|
|
2179
|
+
CPUDebuggerWindow.BOOKMARK_STORAGE_KEY,
|
|
2180
|
+
);
|
|
2181
|
+
if (saved) this.bookmarks = JSON.parse(saved);
|
|
2182
|
+
} catch (e) {
|
|
2183
|
+
/* ignore */
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
saveBookmarks() {
|
|
2188
|
+
try {
|
|
2189
|
+
localStorage.setItem(
|
|
2190
|
+
CPUDebuggerWindow.BOOKMARK_STORAGE_KEY,
|
|
2191
|
+
JSON.stringify(this.bookmarks),
|
|
2192
|
+
);
|
|
2193
|
+
} catch (e) {
|
|
2194
|
+
/* ignore */
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
toggleBookmark(addr) {
|
|
2199
|
+
const idx = this.bookmarks.indexOf(addr);
|
|
2200
|
+
if (idx >= 0) {
|
|
2201
|
+
this.bookmarks.splice(idx, 1);
|
|
2202
|
+
} else {
|
|
2203
|
+
this.bookmarks.push(addr);
|
|
2204
|
+
}
|
|
2205
|
+
this.saveBookmarks();
|
|
2206
|
+
this.updateDisassembly();
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
// ---- Beam Breakpoint Persistence ----
|
|
2210
|
+
|
|
2211
|
+
static BEAM_STORAGE_KEY = "a2e-beam-breakpoints";
|
|
2212
|
+
|
|
2213
|
+
loadBeamBreakpoints() {
|
|
2214
|
+
try {
|
|
2215
|
+
const saved = localStorage.getItem(CPUDebuggerWindow.BEAM_STORAGE_KEY);
|
|
2216
|
+
if (!saved) return;
|
|
2217
|
+
const data = JSON.parse(saved);
|
|
2218
|
+
for (const bp of data) {
|
|
2219
|
+
const id = this.wasmModule._addBeamBreakpoint(bp.scanline, bp.hPos);
|
|
2220
|
+
if (id < 0) continue;
|
|
2221
|
+
if (!bp.enabled) {
|
|
2222
|
+
this.wasmModule._enableBeamBreakpoint(id, false);
|
|
2223
|
+
}
|
|
2224
|
+
this.beamBreakpoints.push({
|
|
2225
|
+
id,
|
|
2226
|
+
scanline: bp.scanline,
|
|
2227
|
+
hPos: bp.hPos,
|
|
2228
|
+
enabled: bp.enabled,
|
|
2229
|
+
mode: bp.mode,
|
|
2230
|
+
});
|
|
2231
|
+
}
|
|
2232
|
+
} catch (e) {
|
|
2233
|
+
/* ignore */
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
saveBeamBreakpoints() {
|
|
2238
|
+
try {
|
|
2239
|
+
const data = this.beamBreakpoints.map((bp) => ({
|
|
2240
|
+
scanline: bp.scanline,
|
|
2241
|
+
hPos: bp.hPos,
|
|
2242
|
+
enabled: bp.enabled,
|
|
2243
|
+
mode: bp.mode,
|
|
2244
|
+
}));
|
|
2245
|
+
localStorage.setItem(
|
|
2246
|
+
CPUDebuggerWindow.BEAM_STORAGE_KEY,
|
|
2247
|
+
JSON.stringify(data),
|
|
2248
|
+
);
|
|
2249
|
+
} catch (e) {
|
|
2250
|
+
/* ignore */
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
/**
|
|
2255
|
+
* Re-push all beam breakpoints from JS state to C++.
|
|
2256
|
+
* Called after state import since importState() calls reset() which
|
|
2257
|
+
* clears all WASM-side beam breakpoints.
|
|
2258
|
+
*/
|
|
2259
|
+
resyncBeamToWasm() {
|
|
2260
|
+
if (this.wasmModule._clearAllBeamBreakpoints) {
|
|
2261
|
+
this.wasmModule._clearAllBeamBreakpoints();
|
|
2262
|
+
}
|
|
2263
|
+
for (const bp of this.beamBreakpoints) {
|
|
2264
|
+
const newId = this.wasmModule._addBeamBreakpoint(bp.scanline, bp.hPos);
|
|
2265
|
+
if (newId >= 0) {
|
|
2266
|
+
bp.id = newId;
|
|
2267
|
+
if (!bp.enabled) {
|
|
2268
|
+
this.wasmModule._enableBeamBreakpoint(newId, false);
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
this.updateBeamList();
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
/**
|
|
2276
|
+
* Look up a symbol name for an address. User labels take priority,
|
|
2277
|
+
* then imported labels, then built-in symbols.
|
|
2278
|
+
*/
|
|
2279
|
+
lookupSymbol(addr) {
|
|
2280
|
+
const label = this.labelManager.getLabel(addr);
|
|
2281
|
+
if (label && label.name) {
|
|
2282
|
+
return {
|
|
2283
|
+
name: label.name,
|
|
2284
|
+
desc: label.comment || label.name,
|
|
2285
|
+
category: "user",
|
|
2286
|
+
};
|
|
2287
|
+
}
|
|
2288
|
+
return getSymbolInfo(addr);
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
symbolizeInstruction(instrText) {
|
|
2292
|
+
// First, wrap immediate constants (#$XX or #$XXXX) in spans
|
|
2293
|
+
let result = instrText.replace(/#\$([0-9A-Fa-f]{2,4})/g, (match) => {
|
|
2294
|
+
return `<span class="cpu-disasm-const">${match}</span>`;
|
|
2295
|
+
});
|
|
2296
|
+
|
|
2297
|
+
// Then replace $XXXX patterns (4-digit hex addresses) with symbols
|
|
2298
|
+
result = result.replace(
|
|
2299
|
+
/\$([0-9A-Fa-f]{4})(?![0-9A-Fa-f])/g,
|
|
2300
|
+
(match, hexAddr) => {
|
|
2301
|
+
const addr = parseInt(hexAddr, 16);
|
|
2302
|
+
const info = this.lookupSymbol(addr);
|
|
2303
|
+
if (info) {
|
|
2304
|
+
const cssClass =
|
|
2305
|
+
info.category === "user"
|
|
2306
|
+
? "cpu-disasm-user-label"
|
|
2307
|
+
: getCategoryClass(info.category);
|
|
2308
|
+
return `<span class="cpu-disasm-symbol ${cssClass}" data-tooltip="${info.desc}">${info.name}</span>`;
|
|
2309
|
+
}
|
|
2310
|
+
return match;
|
|
2311
|
+
},
|
|
2312
|
+
);
|
|
2313
|
+
|
|
2314
|
+
// Also handle 2-digit zero page addresses that have symbols
|
|
2315
|
+
result = result.replace(
|
|
2316
|
+
/\$([0-9A-Fa-f]{2})(?![0-9A-Fa-f])/g,
|
|
2317
|
+
(match, hexAddr) => {
|
|
2318
|
+
const addr = parseInt(hexAddr, 16);
|
|
2319
|
+
const info = this.lookupSymbol(addr);
|
|
2320
|
+
if (info) {
|
|
2321
|
+
const cssClass =
|
|
2322
|
+
info.category === "user"
|
|
2323
|
+
? "cpu-disasm-user-label"
|
|
2324
|
+
: getCategoryClass(info.category);
|
|
2325
|
+
return `<span class="cpu-disasm-symbol ${cssClass}" data-tooltip="${info.desc}">${info.name}</span>`;
|
|
2326
|
+
}
|
|
2327
|
+
return match;
|
|
2328
|
+
},
|
|
2329
|
+
);
|
|
2330
|
+
|
|
2331
|
+
return result;
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
/**
|
|
2335
|
+
* Get heat level (1-5) for an address from profiling data
|
|
2336
|
+
*/
|
|
2337
|
+
getHeatLevel(addr) {
|
|
2338
|
+
if (!this._profilePtr || this._profileMax === 0) return 0;
|
|
2339
|
+
const heap32 = new Uint32Array(this.wasmModule.HEAPU8.buffer);
|
|
2340
|
+
const value = heap32[(this._profilePtr >> 2) + addr];
|
|
2341
|
+
if (value === 0) return 0;
|
|
2342
|
+
const ratio = value / this._profileMax;
|
|
2343
|
+
if (ratio > 0.6) return 5;
|
|
2344
|
+
if (ratio > 0.3) return 4;
|
|
2345
|
+
if (ratio > 0.1) return 3;
|
|
2346
|
+
if (ratio > 0.02) return 2;
|
|
2347
|
+
return 1;
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
getState() {
|
|
2351
|
+
const base = super.getState();
|
|
2352
|
+
base.activeTab = this.activeTab;
|
|
2353
|
+
return base;
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
restoreState(state) {
|
|
2357
|
+
if (state.activeTab) {
|
|
2358
|
+
this.activeTab = state.activeTab;
|
|
2359
|
+
}
|
|
2360
|
+
super.restoreState(state);
|
|
2361
|
+
// Apply tab selection to DOM after restoreState calls show()
|
|
2362
|
+
if (this.contentElement && this.activeTab) {
|
|
2363
|
+
const tabBar = this.contentElement.querySelector(".cpu-dbg-tab-bar");
|
|
2364
|
+
if (tabBar) {
|
|
2365
|
+
tabBar.querySelectorAll(".cpu-dbg-tab").forEach((t) => {
|
|
2366
|
+
t.classList.toggle("active", t.dataset.tab === this.activeTab);
|
|
2367
|
+
});
|
|
2368
|
+
this.contentElement
|
|
2369
|
+
.querySelectorAll(".cpu-dbg-tab-content")
|
|
2370
|
+
.forEach((c) => {
|
|
2371
|
+
c.classList.toggle("active", c.dataset.tab === this.activeTab);
|
|
2372
|
+
});
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
/**
|
|
2378
|
+
* Get instruction length for a given opcode
|
|
2379
|
+
*/
|
|
2380
|
+
getInstructionLength(opcode) {
|
|
2381
|
+
const lengths = [
|
|
2382
|
+
1, 2, 1, 1, 2, 2, 2, 2, 1, 2, 1, 1, 3, 3, 3, 3, 2, 2, 2, 1, 2, 2, 2, 2, 1,
|
|
2383
|
+
3, 1, 1, 3, 3, 3, 3, 3, 2, 1, 1, 2, 2, 2, 2, 1, 2, 1, 1, 3, 3, 3, 3, 2, 2,
|
|
2384
|
+
2, 1, 2, 2, 2, 2, 1, 3, 1, 1, 3, 3, 3, 3, 1, 2, 1, 1, 1, 2, 2, 2, 1, 2, 1,
|
|
2385
|
+
1, 3, 3, 3, 3, 2, 2, 2, 1, 1, 2, 2, 2, 1, 3, 1, 1, 1, 3, 3, 3, 1, 2, 1, 1,
|
|
2386
|
+
2, 2, 2, 2, 1, 2, 1, 1, 3, 3, 3, 3, 2, 2, 2, 1, 2, 2, 2, 2, 1, 3, 1, 1, 3,
|
|
2387
|
+
3, 3, 3, 2, 2, 1, 1, 2, 2, 2, 2, 1, 2, 1, 1, 3, 3, 3, 3, 2, 2, 2, 1, 2, 2,
|
|
2388
|
+
2, 2, 1, 3, 1, 1, 3, 3, 3, 3, 2, 2, 2, 1, 2, 2, 2, 2, 1, 2, 1, 1, 3, 3, 3,
|
|
2389
|
+
3, 2, 2, 2, 1, 2, 2, 2, 2, 1, 3, 1, 1, 3, 3, 3, 3, 2, 2, 1, 1, 2, 2, 2, 2,
|
|
2390
|
+
1, 2, 1, 1, 3, 3, 3, 3, 2, 2, 2, 1, 1, 2, 2, 2, 1, 3, 1, 1, 1, 3, 3, 3, 2,
|
|
2391
|
+
2, 1, 1, 2, 2, 2, 2, 1, 2, 1, 1, 3, 3, 3, 3, 2, 2, 2, 1, 1, 2, 2, 2, 1, 3,
|
|
2392
|
+
1, 1, 1, 3, 3, 3,
|
|
2393
|
+
];
|
|
2394
|
+
return lengths[opcode] || 1;
|
|
2395
|
+
}
|
|
2396
|
+
}
|