debugger.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567
  1. /* global EVENT_NEW_FRAME, EVENT_AUDIO_BUFFER_FULL, EVENT_UNTIL_TICKS, vm, emulator, API */
  2. let debug;
  3. // Consts
  4. const EVENT_BREAKPOINT = 8;
  5. const EXECUTING_CTX_SYMBOL = "_executing_ctx";
  6. const FIRST_CTX_SYMBOL = "_first_ctx";
  7. const SCRIPT_MEMORY_SYMBOL = "_script_memory";
  8. const CURRENT_SCENE_SYMBOL = "_current_scene";
  9. const MAX_GLOBAL_VARS = "MAX_GLOBAL_VARS";
  10. // Helpers
  11. const toAddrHex = (value) =>
  12. ("0000" + value.toString(16).toUpperCase()).slice(-4);
  13. const parseDebuggerSymbol = (input) => {
  14. const match = input.match(
  15. /GBVM\$([^$]+)\$([^$]+)\$([^$]+)\$([^$]+)\$([^$]+)\$([^$]+)/
  16. );
  17. if (!match) {
  18. return undefined;
  19. }
  20. return {
  21. scriptSymbol: match[1],
  22. scriptEventId: match[2].replace(/_/g, "-"),
  23. sceneId: match[3].replace(/_/g, "-"),
  24. entityType: match[4],
  25. entityId: match[5].replace(/_/g, "-"),
  26. scriptKey: match[6],
  27. };
  28. };
  29. const parseDebuggerEndSymbol = (input) => {
  30. const match = input.match(/GBVM_END\$([^$]+)\$([^$]+)/);
  31. if (!match) {
  32. return undefined;
  33. }
  34. return {
  35. scriptSymbol: match[1],
  36. };
  37. };
  38. // Debugger
  39. class Debug {
  40. constructor(emulator) {
  41. this.emulator = emulator;
  42. this.module = emulator.module;
  43. this.e = emulator.e;
  44. this.vramCanvas = document.createElement("canvas");
  45. this.vramCanvas.width = 256;
  46. this.vramCanvas.height = 256;
  47. this.memoryMap = {};
  48. this.globalVariables = {};
  49. this.variableMap = {};
  50. this.memoryDict = new Map();
  51. this.breakpoints = [];
  52. this.pauseOnScriptChanged = false;
  53. this.pauseOnWatchedVariableChanged = true;
  54. this.pauseOnVMStep = false;
  55. this.currentScriptSymbol = "";
  56. this.scriptContexts = [];
  57. this.pausedUI = null;
  58. this.prevGlobals = [];
  59. this.watchedVariables = [];
  60. this.debugRunUntil = (ticks) => {
  61. while (true) {
  62. const event = this.module._emulator_run_until_f64(this.e, ticks);
  63. if (event & EVENT_NEW_FRAME) {
  64. this.emulator.rewind.pushBuffer();
  65. this.emulator.video.uploadTexture();
  66. }
  67. if (event & EVENT_BREAKPOINT) {
  68. // Breakpoint hit
  69. const firstCtxAddr = this.memoryMap[FIRST_CTX_SYMBOL];
  70. const executingCtxAddr = this.memoryMap[EXECUTING_CTX_SYMBOL];
  71. const currentCtx = this.readMemInt16(executingCtxAddr);
  72. let firstCtx = debug.readMemInt16(firstCtxAddr);
  73. let scriptContexts = [];
  74. let currentCtxData = undefined;
  75. const prevCtxs = this.scriptContexts;
  76. while (firstCtx !== 0) {
  77. const ctxAddr = debug.readMemInt16(firstCtx);
  78. const ctxBank = debug.readMem(firstCtx + 2);
  79. const ctxStackPtrAddr = debug.readMemInt16(firstCtx + 8);
  80. const ctxStackBaseAddr = debug.readMemInt16(firstCtx + 10);
  81. const closestAddr = debug.getClosestAddress(ctxBank, ctxAddr);
  82. const closestSymbol = debug.getSymbol(ctxBank, closestAddr);
  83. const closestGBVMSymbol = parseDebuggerSymbol(closestSymbol);
  84. const prevCtx = prevCtxs[scriptContexts.length];
  85. let stackString = "";
  86. for (var i = ctxStackBaseAddr; i < ctxStackPtrAddr + 4; i += 2) {
  87. stackString += `${i === ctxStackPtrAddr ? "->" : " "}${toAddrHex(
  88. i
  89. )}: ${debug.readMemInt16(i)}\n`;
  90. }
  91. const ctxData = {
  92. address: ctxAddr,
  93. bank: ctxBank,
  94. current: currentCtx === firstCtx,
  95. closestAddr,
  96. closestSymbol,
  97. closestGBVMSymbol,
  98. prevClosestSymbol: prevCtx?.closestSymbol,
  99. prevClosestGBVMSymbol: prevCtx?.closestGBVMSymbol,
  100. stackString,
  101. };
  102. scriptContexts.push(ctxData);
  103. if (ctxData.current) {
  104. currentCtxData = ctxData;
  105. }
  106. firstCtx = debug.readMemInt16(firstCtx + 3);
  107. }
  108. this.scriptContexts = scriptContexts;
  109. if (currentCtxData) {
  110. // If pausing on VM Step and current script block changed
  111. if (
  112. this.pauseOnVMStep &&
  113. currentCtxData.closestGBVMSymbol &&
  114. currentCtxData.closestGBVMSymbol.scriptEventId !== "end" &&
  115. currentCtxData.closestSymbol !== currentCtxData.prevClosestSymbol
  116. ) {
  117. emulator.pause();
  118. this.pauseOnVMStep = false;
  119. break;
  120. }
  121. // If manual breakpoint is hit
  122. if (
  123. currentCtxData.closestGBVMSymbol &&
  124. currentCtxData.address === currentCtxData.closestAddr &&
  125. currentCtxData.closestSymbol !==
  126. currentCtxData.prevClosestSymbol &&
  127. this.breakpoints.includes(
  128. currentCtxData.closestGBVMSymbol.scriptEventId
  129. )
  130. ) {
  131. this.pauseOnVMStep = true;
  132. emulator.pause();
  133. break;
  134. }
  135. if (
  136. this.pauseOnScriptChanged &&
  137. // Found matching GBVM event
  138. currentCtxData.closestGBVMSymbol &&
  139. // GBVM event has changed since last pause
  140. (!currentCtxData.prevClosestGBVMSymbol ||
  141. currentCtxData.closestGBVMSymbol.scriptSymbol !==
  142. currentCtxData.prevClosestGBVMSymbol.scriptSymbol)
  143. ) {
  144. this.pauseOnVMStep = true;
  145. emulator.pause();
  146. break;
  147. }
  148. if (this.pauseOnWatchedVariableChanged) {
  149. const globals = this.getGlobals();
  150. if (this.prevGlobals.length > 0) {
  151. // Check if watched has change
  152. const modified = !this.prevGlobals.every(
  153. (v, i) => v === globals[i]
  154. );
  155. if (modified) {
  156. const changedVariable = this.watchedVariables.find(
  157. (variableId) => {
  158. const variableData = this.variableMap[variableId];
  159. const symbol = variableData?.symbol;
  160. const variableIndex = this.globalVariables[symbol];
  161. if (variableIndex !== undefined) {
  162. return (
  163. this.prevGlobals[variableIndex] !== undefined &&
  164. globals[variableIndex] !==
  165. this.prevGlobals[variableIndex]
  166. );
  167. }
  168. return false;
  169. }
  170. );
  171. if (changedVariable) {
  172. this.pauseOnVMStep = true;
  173. emulator.pause();
  174. }
  175. }
  176. }
  177. this.prevGlobals = globals;
  178. }
  179. }
  180. }
  181. if (event & EVENT_AUDIO_BUFFER_FULL && !this.emulator.isRewinding) {
  182. this.emulator.audio.pushBuffer();
  183. }
  184. if (event & EVENT_UNTIL_TICKS) {
  185. break;
  186. }
  187. }
  188. if (this.module._emulator_was_ext_ram_updated(this.e)) {
  189. vm.extRamUpdated = true;
  190. }
  191. };
  192. // replace the emulator run method with the debug one
  193. this.emulator.runUntil = this.debugRunUntil;
  194. }
  195. initialize(
  196. memoryMap,
  197. globalVariables,
  198. variableMap,
  199. pauseOnScriptChanged,
  200. pauseOnWatchedVarChanged,
  201. breakpoints,
  202. watchedVariables
  203. ) {
  204. this.memoryMap = memoryMap;
  205. this.globalVariables = globalVariables;
  206. this.variableMap = variableMap;
  207. this.pauseOnScriptChanged = pauseOnScriptChanged;
  208. this.pauseOnWatchedVariableChanged = pauseOnWatchedVarChanged;
  209. this.breakpoints = breakpoints;
  210. this.watchedVariables = watchedVariables;
  211. const memoryDict = new Map();
  212. Object.keys(memoryMap).forEach((k) => {
  213. // Banked resources
  214. const match = k.match(/___bank_(.*)/);
  215. if (match) {
  216. const label = `_${match[1]}`;
  217. const bank = memoryMap[k];
  218. if (memoryMap[label]) {
  219. const n = memoryDict.get(bank) ?? new Map();
  220. const ptr = memoryMap[label] & 0x0ffff;
  221. n.set(ptr, label);
  222. memoryDict.set(bank, n);
  223. }
  224. }
  225. // Script debug symbols
  226. // const matchGBVM = k.match(/GBVM\$([^$]*)\$([^$]*)/);
  227. const matchGBVM = parseDebuggerSymbol(k);
  228. if (matchGBVM) {
  229. const bankLabel = `___bank_${matchGBVM.scriptSymbol}`;
  230. const label = k;
  231. const bank = memoryMap[bankLabel];
  232. if (memoryMap[label]) {
  233. const n = memoryDict.get(bank) ?? new Map();
  234. const ptr = memoryMap[label] & 0x0ffff;
  235. n.set(ptr, label);
  236. memoryDict.set(bank, n);
  237. }
  238. }
  239. const matchEnd = parseDebuggerEndSymbol(k);
  240. if (matchEnd) {
  241. const bankLabel = `___bank_${matchEnd.scriptSymbol}`;
  242. const label = k;
  243. const bank = memoryMap[bankLabel];
  244. if (memoryMap[label]) {
  245. const n = memoryDict.get(bank) ?? new Map();
  246. const ptr = memoryMap[label] & 0x0ffff;
  247. if (!n.get(ptr)) {
  248. n.set(ptr, label);
  249. memoryDict.set(bank, n);
  250. }
  251. }
  252. }
  253. });
  254. this.memoryDict = memoryDict;
  255. // Break on VM_STEP
  256. this.module._emulator_set_breakpoint(this.e, memoryMap["_VM_STEP"]);
  257. // Add paused UI
  258. this.initializeUI();
  259. this.initializeKeyboardShortcuts();
  260. }
  261. initializeUI() {
  262. const pausedUI = document.createElement("div");
  263. const pausedUIContainer = document.createElement("div");
  264. const pausedUILabel = document.createElement("span");
  265. const pausedUIResumeBtn = document.createElement("button");
  266. const pausedUIStepBtn = document.createElement("button");
  267. const pausedUIStepFrameBtn = document.createElement("button");
  268. document.body.appendChild(pausedUI);
  269. pausedUI.appendChild(pausedUIContainer);
  270. pausedUIContainer.appendChild(pausedUILabel);
  271. pausedUIContainer.appendChild(pausedUIResumeBtn);
  272. pausedUIContainer.appendChild(pausedUIStepBtn);
  273. pausedUIContainer.appendChild(pausedUIStepFrameBtn);
  274. pausedUI.id = "debug";
  275. pausedUILabel.innerHTML = "Paused in debugger";
  276. pausedUIResumeBtn.innerHTML = `<svg width="15" height="15" viewBox="0 0 24 24"><path d="M2 3H6V21H2V3Z" /><path d="M22 12L7 21L7 3L22 12Z" /></svg>`;
  277. pausedUIResumeBtn.title = "Resume execution - F8";
  278. pausedUIResumeBtn.addEventListener("click", this.resume.bind(this));
  279. pausedUIStepBtn.innerHTML = `<svg width="15" height="15" viewBox="0 0 24 24"><path d="M16 8v-4l8 8-8 8v-4h-5v-8h5zm-7 0h-2v8h2v-8zm-4.014 0h-1.986v8h1.986v-8zm-3.986 0h-1v8h1v-8z" /></svg>`;
  280. pausedUIStepBtn.title = "Step - F9";
  281. pausedUIStepBtn.addEventListener("click", this.step.bind(this));
  282. pausedUIStepFrameBtn.innerHTML = `<svg width="15" height="15" viewBox="0 0 24 24"><path d="M19 12l-18 12v-24l18 12zm4-11h-4v22h4v-22z" /></svg>`;
  283. pausedUIStepFrameBtn.title = "Step Frame - F10";
  284. pausedUIStepFrameBtn.addEventListener("click", this.stepFrame.bind(this));
  285. this.pausedUI = pausedUI;
  286. }
  287. initializeKeyboardShortcuts() {
  288. window.addEventListener("keydown", (e) => {
  289. if (e.key === "F8") {
  290. this.togglePlayPause();
  291. } else if (e.key === "F9") {
  292. this.step();
  293. } else if (e.key === "F10") {
  294. this.stepFrame();
  295. }
  296. });
  297. }
  298. getClosestAddress(bank, address) {
  299. const bankScripts = this.memoryDict.get(bank);
  300. const currentAddress = address;
  301. let closestAddress = -1;
  302. if (bankScripts) {
  303. const addresses = Array.from(bankScripts.keys()).sort();
  304. for (let i = 0; i < addresses.length; i++) {
  305. if (addresses[i] > currentAddress) {
  306. break;
  307. } else {
  308. closestAddress = addresses[i];
  309. }
  310. }
  311. }
  312. return closestAddress;
  313. }
  314. getSymbol(bank, address) {
  315. const symbol = this.memoryDict.get(bank)?.get(address) ?? "";
  316. return symbol.replace(/^_/, "");
  317. }
  318. readMem(addr) {
  319. return this.module._emulator_read_mem(this.e, addr);
  320. }
  321. readMemInt16(addr) {
  322. return (
  323. (this.module._emulator_read_mem(this.e, addr + 1) << 8) |
  324. this.module._emulator_read_mem(this.e, addr)
  325. );
  326. }
  327. writeMem(addr, value) {
  328. this.module._emulator_write_mem(this.e, addr, value & 0xff);
  329. }
  330. writeMemInt16(addr, value) {
  331. this.module._emulator_write_mem(this.e, addr, value & 0xff);
  332. this.module._emulator_write_mem(this.e, addr + 1, value >> 8);
  333. }
  334. readVariables(addr, size) {
  335. const ptr = this.module._emulator_get_wram_ptr(this.e) - 0xc000;
  336. return new Int16Array(
  337. this.module.HEAP8.buffer.slice(ptr + addr, ptr + addr + size * 2)
  338. );
  339. }
  340. renderVRam() {
  341. var ctx = this.vramCanvas.getContext("2d");
  342. var imgData = ctx.createImageData(256, 256);
  343. var ptr = this.module._malloc(4 * 256 * 256);
  344. this.module._emulator_render_vram(this.e, ptr);
  345. var buffer = new Uint8Array(this.module.HEAP8.buffer, ptr, 4 * 256 * 256);
  346. imgData.data.set(buffer);
  347. ctx.putImageData(imgData, 0, 0);
  348. this.module._free(ptr);
  349. return this.vramCanvas.toDataURL("image/png");
  350. }
  351. setBreakPoints(breakpoints) {
  352. this.breakpoints = breakpoints;
  353. }
  354. setWatchedVariables(watchedVariables) {
  355. this.watchedVariables = watchedVariables;
  356. }
  357. pause() {
  358. this.pauseOnVMStep = true;
  359. this.emulator.pause();
  360. }
  361. resume() {
  362. this.pauseOnVMStep = false;
  363. this.emulator.resume();
  364. }
  365. togglePlayPause() {
  366. if (this.isPaused()) {
  367. this.resume();
  368. } else {
  369. this.pause();
  370. }
  371. }
  372. step() {
  373. if (this.isPaused()) {
  374. this.resume();
  375. this.pauseOnVMStep = true;
  376. }
  377. }
  378. stepFrame() {
  379. if (this.isPaused()) {
  380. const ticks = this.module._emulator_get_ticks_f64(this.e) + 70224;
  381. this.emulator.runUntil(ticks);
  382. this.emulator.video.renderTexture();
  383. }
  384. }
  385. isPaused() {
  386. return this.emulator.isPaused || this.pauseOnVMStep;
  387. }
  388. getGlobals() {
  389. const variablesStartAddr = this.memoryMap[SCRIPT_MEMORY_SYMBOL];
  390. const variablesLength = this.globalVariables[MAX_GLOBAL_VARS];
  391. return this.readVariables(variablesStartAddr, variablesLength);
  392. }
  393. setGlobal(symbol, value) {
  394. const offset = (this.globalVariables[symbol] ?? 0) * 2;
  395. const variablesStartAddr = this.memoryMap[SCRIPT_MEMORY_SYMBOL];
  396. this.writeMemInt16(variablesStartAddr + offset, value);
  397. this.prevGlobals = this.getGlobals();
  398. }
  399. getCurrentSceneSymbol() {
  400. const currentSceneAddr = this.memoryMap[CURRENT_SCENE_SYMBOL];
  401. return this.getSymbol(
  402. this.readMem(currentSceneAddr),
  403. this.readMemInt16(currentSceneAddr + 1)
  404. );
  405. }
  406. getNumScriptCtxs() {
  407. const firstCtxAddr = this.memoryMap[FIRST_CTX_SYMBOL];
  408. let firstCtx = debug.readMemInt16(firstCtxAddr);
  409. let numCtxs = 0;
  410. while (firstCtx !== 0) {
  411. numCtxs++;
  412. firstCtx = debug.readMemInt16(firstCtx + 3);
  413. }
  414. return numCtxs;
  415. }
  416. }
  417. // Debugger Initialisation
  418. let ready = setInterval(() => {
  419. const debugEnabled = window.location.href.includes("debug=true");
  420. if (!debugEnabled) {
  421. // Debugging not enabled
  422. clearInterval(ready);
  423. return;
  424. }
  425. console.log("Waiting for emulator...", emulator);
  426. if (emulator !== null) {
  427. debug = new Debug(emulator);
  428. clearInterval(ready);
  429. API.debugger.sendToProjectWindow({
  430. action: "initialized",
  431. });
  432. API.events.debugger.data.subscribe((_, packet) => {
  433. const { action, data } = packet;
  434. switch (action) {
  435. case "listener-ready":
  436. debug.initialize(
  437. data.memoryMap,
  438. data.globalVariables,
  439. data.variableMap,
  440. data.pauseOnScriptChanged,
  441. data.pauseOnWatchedVariableChanged,
  442. data.breakpoints,
  443. data.watchedVariables
  444. );
  445. setInterval(() => {
  446. if (debug.pausedUI) {
  447. debug.pausedUI.style.visibility = debug.isPaused()
  448. ? "visible"
  449. : "hidden";
  450. }
  451. const scriptContexts =
  452. debug.getNumScriptCtxs() > 0 ? debug.scriptContexts : [];
  453. if (scriptContexts.length === 0) {
  454. debug.pauseOnVMStep = false;
  455. }
  456. API.debugger.sendToProjectWindow({
  457. action: "update-globals",
  458. data: debug.getGlobals(),
  459. vram: debug.renderVRam(),
  460. isPaused: debug.isPaused(),
  461. scriptContexts,
  462. currentSceneSymbol: debug.getCurrentSceneSymbol(),
  463. });
  464. }, 100);
  465. break;
  466. case "set-breakpoints":
  467. debug.setBreakPoints(data);
  468. break;
  469. case "pause":
  470. debug.pause();
  471. break;
  472. case "resume":
  473. debug.resume();
  474. break;
  475. case "step":
  476. debug.step();
  477. break;
  478. case "step-frame":
  479. debug.stepFrame();
  480. break;
  481. case "pause-on-script":
  482. debug.pauseOnScriptChanged = data;
  483. break;
  484. case "pause-on-var":
  485. debug.pauseOnWatchedVariableChanged = data;
  486. break;
  487. case "set-global":
  488. debug.setGlobal(data.symbol, data.value);
  489. break;
  490. case "set-watched":
  491. debug.setWatchedVariables(data);
  492. break;
  493. default:
  494. // console.warn(event);
  495. }
  496. });
  497. }
  498. }, 200);