web-a2e 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (295) hide show
  1. package/.clangd +5 -0
  2. package/.mcp.json +12 -0
  3. package/CLAUDE.md +362 -0
  4. package/CMakeLists.txt +774 -0
  5. package/LICENSE +21 -0
  6. package/README.md +392 -0
  7. package/build-wasm/generated/roms.cpp +2447 -0
  8. package/docker-compose.staging.yml +9 -0
  9. package/docs/basic-rom-disassembly.md +6663 -0
  10. package/docs/softswitch-comparison.md +273 -0
  11. package/docs/thunderclock-debug.md +89 -0
  12. package/examples/cube.bas +72 -0
  13. package/examples/hello.s +55 -0
  14. package/examples/scroll.s +140 -0
  15. package/package.json +18 -0
  16. package/public/assets/apple-logo-old.png +0 -0
  17. package/public/assets/apple-logo.png +0 -0
  18. package/public/assets/drive-closed-light-on.png +0 -0
  19. package/public/assets/drive-closed.png +0 -0
  20. package/public/assets/drive-open-light-on.png +0 -0
  21. package/public/assets/drive-open.png +0 -0
  22. package/public/audio-worklet.js +82 -0
  23. package/public/disks/Apple DOS 3.3 January 1983.dsk +0 -0
  24. package/public/disks/ProDOS 2.4.3.po +0 -0
  25. package/public/disks/h32mb.2mg +0 -0
  26. package/public/disks/library.json +26 -0
  27. package/public/docs/llms/llm-assembler.txt +90 -0
  28. package/public/docs/llms/llm-basic-program.txt +256 -0
  29. package/public/docs/llms/llm-disk-drives.txt +72 -0
  30. package/public/docs/llms/llm-file-explorer.txt +50 -0
  31. package/public/docs/llms/llm-hard-drives.txt +80 -0
  32. package/public/docs/llms/llm-main.txt +51 -0
  33. package/public/docs/llms/llm-slot-configuration.txt +66 -0
  34. package/public/icons/icon-192.svg +4 -0
  35. package/public/icons/icon-512.svg +4 -0
  36. package/public/index.html +661 -0
  37. package/public/llms.txt +49 -0
  38. package/public/manifest.json +29 -0
  39. package/public/shaders/burnin.glsl +22 -0
  40. package/public/shaders/crt.glsl +706 -0
  41. package/public/shaders/edge.glsl +109 -0
  42. package/public/shaders/vertex.glsl +8 -0
  43. package/public/sw.js +186 -0
  44. package/roms/341-0027.bin +0 -0
  45. package/roms/341-0160-A-US-UK.bin +0 -0
  46. package/roms/341-0160-A.bin +0 -0
  47. package/roms/342-0273-A-US-UK.bin +0 -0
  48. package/roms/342-0349-B-C0-FF.bin +0 -0
  49. package/roms/Apple Mouse Interface Card ROM - 342-0270-C.bin +0 -0
  50. package/roms/Thunderclock Plus ROM.bin +0 -0
  51. package/scripts/generate_roms.sh +69 -0
  52. package/src/bindings/wasm_interface.cpp +1940 -0
  53. package/src/core/assembler/assembler.cpp +1239 -0
  54. package/src/core/assembler/assembler.hpp +115 -0
  55. package/src/core/audio/audio.cpp +160 -0
  56. package/src/core/audio/audio.hpp +81 -0
  57. package/src/core/basic/basic_detokenizer.cpp +436 -0
  58. package/src/core/basic/basic_detokenizer.hpp +41 -0
  59. package/src/core/basic/basic_tokenizer.cpp +286 -0
  60. package/src/core/basic/basic_tokenizer.hpp +26 -0
  61. package/src/core/basic/basic_tokens.hpp +295 -0
  62. package/src/core/cards/disk2_card.cpp +568 -0
  63. package/src/core/cards/disk2_card.hpp +316 -0
  64. package/src/core/cards/expansion_card.hpp +185 -0
  65. package/src/core/cards/mockingboard/ay8910.cpp +616 -0
  66. package/src/core/cards/mockingboard/ay8910.hpp +159 -0
  67. package/src/core/cards/mockingboard/via6522.cpp +530 -0
  68. package/src/core/cards/mockingboard/via6522.hpp +163 -0
  69. package/src/core/cards/mockingboard_card.cpp +312 -0
  70. package/src/core/cards/mockingboard_card.hpp +159 -0
  71. package/src/core/cards/mouse_card.cpp +654 -0
  72. package/src/core/cards/mouse_card.hpp +190 -0
  73. package/src/core/cards/smartport/block_device.cpp +202 -0
  74. package/src/core/cards/smartport/block_device.hpp +60 -0
  75. package/src/core/cards/smartport/smartport_card.cpp +603 -0
  76. package/src/core/cards/smartport/smartport_card.hpp +120 -0
  77. package/src/core/cards/thunderclock_card.cpp +237 -0
  78. package/src/core/cards/thunderclock_card.hpp +122 -0
  79. package/src/core/cpu/cpu6502.cpp +1609 -0
  80. package/src/core/cpu/cpu6502.hpp +203 -0
  81. package/src/core/debug/condition_evaluator.cpp +470 -0
  82. package/src/core/debug/condition_evaluator.hpp +87 -0
  83. package/src/core/disassembler/disassembler.cpp +552 -0
  84. package/src/core/disassembler/disassembler.hpp +171 -0
  85. package/src/core/disk-image/disk_image.hpp +267 -0
  86. package/src/core/disk-image/dsk_disk_image.cpp +827 -0
  87. package/src/core/disk-image/dsk_disk_image.hpp +204 -0
  88. package/src/core/disk-image/gcr_encoding.cpp +147 -0
  89. package/src/core/disk-image/gcr_encoding.hpp +78 -0
  90. package/src/core/disk-image/woz_disk_image.cpp +1049 -0
  91. package/src/core/disk-image/woz_disk_image.hpp +343 -0
  92. package/src/core/emulator.cpp +2126 -0
  93. package/src/core/emulator.hpp +434 -0
  94. package/src/core/filesystem/dos33.cpp +178 -0
  95. package/src/core/filesystem/dos33.hpp +66 -0
  96. package/src/core/filesystem/pascal.cpp +262 -0
  97. package/src/core/filesystem/pascal.hpp +87 -0
  98. package/src/core/filesystem/prodos.cpp +369 -0
  99. package/src/core/filesystem/prodos.hpp +119 -0
  100. package/src/core/input/keyboard.cpp +227 -0
  101. package/src/core/input/keyboard.hpp +111 -0
  102. package/src/core/mmu/mmu.cpp +1387 -0
  103. package/src/core/mmu/mmu.hpp +236 -0
  104. package/src/core/types.hpp +196 -0
  105. package/src/core/video/video.cpp +680 -0
  106. package/src/core/video/video.hpp +156 -0
  107. package/src/css/assembler-editor.css +1617 -0
  108. package/src/css/base.css +470 -0
  109. package/src/css/basic-debugger.css +791 -0
  110. package/src/css/basic-editor.css +792 -0
  111. package/src/css/controls.css +783 -0
  112. package/src/css/cpu-debugger.css +1413 -0
  113. package/src/css/debug-base.css +160 -0
  114. package/src/css/debug-windows.css +6455 -0
  115. package/src/css/disk-drives.css +406 -0
  116. package/src/css/documentation.css +392 -0
  117. package/src/css/file-explorer.css +867 -0
  118. package/src/css/hard-drive.css +180 -0
  119. package/src/css/layout.css +217 -0
  120. package/src/css/memory-windows.css +798 -0
  121. package/src/css/modals.css +510 -0
  122. package/src/css/monitor.css +425 -0
  123. package/src/css/release-notes.css +101 -0
  124. package/src/css/responsive.css +400 -0
  125. package/src/css/rule-builder.css +340 -0
  126. package/src/css/save-states.css +201 -0
  127. package/src/css/settings-windows.css +1231 -0
  128. package/src/css/window-switcher.css +150 -0
  129. package/src/js/agent/agent-manager.js +643 -0
  130. package/src/js/agent/agent-tools.js +293 -0
  131. package/src/js/agent/agent-version-tools.js +131 -0
  132. package/src/js/agent/assembler-tools.js +357 -0
  133. package/src/js/agent/basic-program-tools.js +894 -0
  134. package/src/js/agent/disk-tools.js +417 -0
  135. package/src/js/agent/file-explorer-tools.js +269 -0
  136. package/src/js/agent/index.js +13 -0
  137. package/src/js/agent/main-tools.js +222 -0
  138. package/src/js/agent/slot-tools.js +303 -0
  139. package/src/js/agent/smartport-tools.js +257 -0
  140. package/src/js/agent/window-tools.js +80 -0
  141. package/src/js/audio/audio-driver.js +417 -0
  142. package/src/js/audio/audio-worklet.js +85 -0
  143. package/src/js/audio/index.js +8 -0
  144. package/src/js/config/default-layout.js +34 -0
  145. package/src/js/config/version.js +8 -0
  146. package/src/js/data/apple2-rom-routines.js +577 -0
  147. package/src/js/debug/assembler-editor-window.js +2993 -0
  148. package/src/js/debug/basic-breakpoint-manager.js +529 -0
  149. package/src/js/debug/basic-program-parser.js +436 -0
  150. package/src/js/debug/basic-program-window.js +2594 -0
  151. package/src/js/debug/basic-variable-inspector.js +447 -0
  152. package/src/js/debug/breakpoint-manager.js +472 -0
  153. package/src/js/debug/cpu-debugger-window.js +2396 -0
  154. package/src/js/debug/index.js +22 -0
  155. package/src/js/debug/label-manager.js +238 -0
  156. package/src/js/debug/memory-browser-window.js +416 -0
  157. package/src/js/debug/memory-heat-map-window.js +481 -0
  158. package/src/js/debug/memory-map-window.js +206 -0
  159. package/src/js/debug/mockingboard-window.js +882 -0
  160. package/src/js/debug/mouse-card-window.js +355 -0
  161. package/src/js/debug/rule-builder-window.js +648 -0
  162. package/src/js/debug/soft-switch-window.js +458 -0
  163. package/src/js/debug/stack-viewer-window.js +221 -0
  164. package/src/js/debug/symbols.js +416 -0
  165. package/src/js/debug/trace-panel.js +291 -0
  166. package/src/js/debug/zero-page-watch-window.js +297 -0
  167. package/src/js/disk-manager/disk-drives-window.js +212 -0
  168. package/src/js/disk-manager/disk-operations.js +284 -0
  169. package/src/js/disk-manager/disk-persistence.js +301 -0
  170. package/src/js/disk-manager/disk-surface-renderer.js +388 -0
  171. package/src/js/disk-manager/drive-sounds.js +139 -0
  172. package/src/js/disk-manager/hard-drive-manager.js +481 -0
  173. package/src/js/disk-manager/hard-drive-persistence.js +187 -0
  174. package/src/js/disk-manager/hard-drive-window.js +57 -0
  175. package/src/js/disk-manager/index.js +890 -0
  176. package/src/js/display/display-settings-window.js +383 -0
  177. package/src/js/display/index.js +10 -0
  178. package/src/js/display/screen-window.js +342 -0
  179. package/src/js/display/webgl-renderer.js +705 -0
  180. package/src/js/file-explorer/disassembler.js +574 -0
  181. package/src/js/file-explorer/dos33.js +266 -0
  182. package/src/js/file-explorer/file-viewer.js +359 -0
  183. package/src/js/file-explorer/index.js +1261 -0
  184. package/src/js/file-explorer/prodos.js +549 -0
  185. package/src/js/file-explorer/utils.js +67 -0
  186. package/src/js/help/documentation-window.js +1096 -0
  187. package/src/js/help/index.js +10 -0
  188. package/src/js/help/release-notes-window.js +85 -0
  189. package/src/js/help/release-notes.js +612 -0
  190. package/src/js/input/gamepad-handler.js +176 -0
  191. package/src/js/input/index.js +12 -0
  192. package/src/js/input/input-handler.js +396 -0
  193. package/src/js/input/joystick-window.js +404 -0
  194. package/src/js/input/mouse-handler.js +99 -0
  195. package/src/js/input/text-selection.js +462 -0
  196. package/src/js/main.js +653 -0
  197. package/src/js/state/index.js +15 -0
  198. package/src/js/state/save-states-window.js +393 -0
  199. package/src/js/state/state-manager.js +409 -0
  200. package/src/js/state/state-persistence.js +218 -0
  201. package/src/js/ui/confirm.js +43 -0
  202. package/src/js/ui/disk-drive-positioner.js +347 -0
  203. package/src/js/ui/reminder-controller.js +129 -0
  204. package/src/js/ui/slot-configuration-window.js +560 -0
  205. package/src/js/ui/theme-manager.js +61 -0
  206. package/src/js/ui/toast.js +44 -0
  207. package/src/js/ui/ui-controller.js +897 -0
  208. package/src/js/ui/window-switcher.js +275 -0
  209. package/src/js/utils/basic-autocomplete.js +832 -0
  210. package/src/js/utils/basic-highlighting.js +473 -0
  211. package/src/js/utils/basic-tokenizer.js +153 -0
  212. package/src/js/utils/basic-tokens.js +117 -0
  213. package/src/js/utils/constants.js +28 -0
  214. package/src/js/utils/indexeddb-helper.js +225 -0
  215. package/src/js/utils/merlin-editor-support.js +905 -0
  216. package/src/js/utils/merlin-highlighting.js +551 -0
  217. package/src/js/utils/storage.js +125 -0
  218. package/src/js/utils/string-utils.js +19 -0
  219. package/src/js/utils/wasm-memory.js +54 -0
  220. package/src/js/windows/base-window.js +690 -0
  221. package/src/js/windows/index.js +9 -0
  222. package/src/js/windows/window-manager.js +375 -0
  223. package/tests/catch2/catch.hpp +17976 -0
  224. package/tests/common/basic_program_builder.cpp +119 -0
  225. package/tests/common/basic_program_builder.hpp +209 -0
  226. package/tests/common/disk_image_builder.cpp +444 -0
  227. package/tests/common/disk_image_builder.hpp +141 -0
  228. package/tests/common/test_helpers.hpp +118 -0
  229. package/tests/gcr/gcr-test.cpp +142 -0
  230. package/tests/integration/check-rom.js +70 -0
  231. package/tests/integration/compare-boot.js +239 -0
  232. package/tests/integration/crash-trace.js +102 -0
  233. package/tests/integration/disk-boot-test.js +264 -0
  234. package/tests/integration/memory-crash.js +108 -0
  235. package/tests/integration/nibble-read-test.js +249 -0
  236. package/tests/integration/phase-test.js +159 -0
  237. package/tests/integration/test_emulator.cpp +291 -0
  238. package/tests/integration/test_emulator_basic.cpp +91 -0
  239. package/tests/integration/test_emulator_debug.cpp +344 -0
  240. package/tests/integration/test_emulator_disk.cpp +153 -0
  241. package/tests/integration/test_emulator_state.cpp +163 -0
  242. package/tests/klaus/6502_functional_test.bin +0 -0
  243. package/tests/klaus/65C02_extended_opcodes_test.bin +0 -0
  244. package/tests/klaus/klaus_6502_test.cpp +184 -0
  245. package/tests/klaus/klaus_65c02_test.cpp +197 -0
  246. package/tests/thunderclock/thunderclock_mmu_test.cpp +304 -0
  247. package/tests/thunderclock/thunderclock_test.cpp +550 -0
  248. package/tests/unit/test_assembler.cpp +521 -0
  249. package/tests/unit/test_audio.cpp +196 -0
  250. package/tests/unit/test_ay8910.cpp +311 -0
  251. package/tests/unit/test_basic_detokenizer.cpp +265 -0
  252. package/tests/unit/test_basic_tokenizer.cpp +382 -0
  253. package/tests/unit/test_block_device.cpp +259 -0
  254. package/tests/unit/test_condition_evaluator.cpp +219 -0
  255. package/tests/unit/test_cpu6502.cpp +1301 -0
  256. package/tests/unit/test_cpu_addressing.cpp +361 -0
  257. package/tests/unit/test_cpu_cycle_counts.cpp +409 -0
  258. package/tests/unit/test_cpu_decimal.cpp +166 -0
  259. package/tests/unit/test_cpu_interrupts.cpp +285 -0
  260. package/tests/unit/test_disassembler.cpp +323 -0
  261. package/tests/unit/test_disk2_card.cpp +330 -0
  262. package/tests/unit/test_dos33.cpp +273 -0
  263. package/tests/unit/test_dsk_disk_image.cpp +315 -0
  264. package/tests/unit/test_expansion_card.cpp +178 -0
  265. package/tests/unit/test_gcr_encoding.cpp +232 -0
  266. package/tests/unit/test_keyboard.cpp +262 -0
  267. package/tests/unit/test_mmu.cpp +555 -0
  268. package/tests/unit/test_mmu_slots.cpp +323 -0
  269. package/tests/unit/test_mockingboard.cpp +352 -0
  270. package/tests/unit/test_mouse_card.cpp +386 -0
  271. package/tests/unit/test_pascal.cpp +248 -0
  272. package/tests/unit/test_prodos.cpp +259 -0
  273. package/tests/unit/test_smartport_card.cpp +321 -0
  274. package/tests/unit/test_thunderclock.cpp +354 -0
  275. package/tests/unit/test_via6522.cpp +323 -0
  276. package/tests/unit/test_video.cpp +319 -0
  277. package/tests/unit/test_woz_disk_image.cpp +257 -0
  278. package/vite.config.js +96 -0
  279. package/wiki/AI-Agent.md +372 -0
  280. package/wiki/Architecture-Overview.md +303 -0
  281. package/wiki/Audio-System.md +449 -0
  282. package/wiki/CPU-Emulation.md +477 -0
  283. package/wiki/Debugger.md +516 -0
  284. package/wiki/Disk-Drives.md +161 -0
  285. package/wiki/Disk-System-Internals.md +547 -0
  286. package/wiki/Display-Settings.md +88 -0
  287. package/wiki/Expansion-Slots.md +187 -0
  288. package/wiki/File-Explorer.md +259 -0
  289. package/wiki/Getting-Started.md +156 -0
  290. package/wiki/Home.md +69 -0
  291. package/wiki/Input-Devices.md +183 -0
  292. package/wiki/Keyboard-Shortcuts.md +158 -0
  293. package/wiki/Memory-System.md +364 -0
  294. package/wiki/Save-States.md +172 -0
  295. package/wiki/Video-Rendering.md +658 -0
@@ -0,0 +1,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&#8230;</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
+ }