romdevtools 0.43.0 → 0.56.1

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 (65) hide show
  1. package/CHANGELOG.md +322 -0
  2. package/examples/n64/platformer/main.c +158 -0
  3. package/examples/n64/puzzle/main.c +117 -0
  4. package/examples/n64/racing/main.c +147 -0
  5. package/examples/n64/shmup/main.c +122 -0
  6. package/examples/n64/sports/main.c +127 -0
  7. package/examples/ps1/platformer/main.c +158 -0
  8. package/examples/ps1/puzzle/main.c +125 -0
  9. package/examples/ps1/racing/main.c +147 -0
  10. package/examples/ps1/shmup/main.c +192 -0
  11. package/examples/ps1/sports/main.c +127 -0
  12. package/examples/ps1/sprite_move/main.c +38 -0
  13. package/package.json +9 -2
  14. package/src/analysis/analyze.js +169 -29
  15. package/src/analysis/decompile.js +4 -0
  16. package/src/analysis/decompiler/sleigh/mips.ldefs +25 -0
  17. package/src/analysis/decompiler/sleigh/mips32.pspec +78 -0
  18. package/src/analysis/decompiler/sleigh/mips32be.cspec +107 -0
  19. package/src/analysis/decompiler/sleigh/mips32be.sla +211162 -0
  20. package/src/analysis/decompiler/sleigh/mips32le.cspec +106 -0
  21. package/src/analysis/decompiler/sleigh/mips32le.sla +210624 -0
  22. package/src/analysis/rizin.js +22 -1
  23. package/src/cores/capabilities.js +90 -2
  24. package/src/cores/registry.js +12 -0
  25. package/src/host/LibretroGL.js +270 -0
  26. package/src/host/LibretroGLBridge.js +836 -0
  27. package/src/host/LibretroHost.js +144 -3
  28. package/src/host/callbacks.js +18 -2
  29. package/src/host/coreLoader.js +49 -2
  30. package/src/host/cpu-state.js +27 -0
  31. package/src/host/framebuffer.js +14 -1
  32. package/src/host/glOptionalDep.js +60 -0
  33. package/src/host/n64-ai-state.js +43 -0
  34. package/src/host/ps1-spu-state.js +65 -0
  35. package/src/host/retroConstants.js +14 -0
  36. package/src/mcp/tools/cheats.js +73 -4
  37. package/src/mcp/tools/disasm.js +2 -0
  38. package/src/mcp/tools/frame.js +18 -9
  39. package/src/mcp/tools/index.js +12 -2
  40. package/src/mcp/tools/lifecycle.js +1 -1
  41. package/src/mcp/tools/platform-tools.js +20 -4
  42. package/src/mcp/tools/platforms.js +38 -4
  43. package/src/mcp/tools/project.js +100 -0
  44. package/src/mcp/tools/rendering-context.js +2 -1
  45. package/src/mcp/tools/toolchain.js +1 -1
  46. package/src/mcp/tools/watch-memory.js +93 -20
  47. package/src/platforms/n64/lib/c/n64.c +196 -0
  48. package/src/platforms/n64/lib/c/n64.h +68 -0
  49. package/src/platforms/ps1/lib/c/psx.c +200 -0
  50. package/src/platforms/ps1/lib/c/psx.h +83 -0
  51. package/src/toolchains/cc65/cc65.js +11 -0
  52. package/src/toolchains/index.js +35 -0
  53. package/src/toolchains/mips-c/lib/be/libc.a +0 -0
  54. package/src/toolchains/mips-c/lib/be/libgcc.a +0 -0
  55. package/src/toolchains/mips-c/lib/be/libm.a +0 -0
  56. package/src/toolchains/mips-c/lib/el/libc.a +0 -0
  57. package/src/toolchains/mips-c/lib/el/libm.a +0 -0
  58. package/src/toolchains/mips-c/lib/n64-crt0.s +21 -0
  59. package/src/toolchains/mips-c/lib/n64-ipl3.s +30 -0
  60. package/src/toolchains/mips-c/lib/n64.ld +15 -0
  61. package/src/toolchains/mips-c/lib/ps1-crt0.s +20 -0
  62. package/src/toolchains/mips-c/lib/ps1.ld +15 -0
  63. package/src/toolchains/mips-c/lib/softint.c +37 -0
  64. package/src/toolchains/mips-c/mips-c.js +155 -0
  65. package/src/toolchains/mips-elf-gcc/gcc.js +130 -0
@@ -49,6 +49,12 @@ const PLATFORM_CORE_OPTIONS = {
49
49
  // host's port-1 input to pad slot 2 — PCE 2P works (probed 2026-06-10
50
50
  // during the ZENITH BARRAGE gold round).
51
51
  pce: { geargrafx_turbotap: "Enabled" },
52
+ // parallel_n64 defaults to a software gfx plugin that never presents to our GL
53
+ // FBO. Force glide64 (the GL renderer) so HW render actually produces frames.
54
+ n64: { "parallel-n64-gfxplugin": "glide64" },
55
+ // beetle_psx_hw defaults to the software renderer; force hardware_gl so it
56
+ // renders through the GL context the host set up (otherwise no FBO frames).
57
+ ps1: { beetle_psx_hw_renderer: "hardware_gl" },
52
58
  // VICE mounts a .d64/.tap/.crt but, with autostart off, just sits at the BASIC
53
59
  // `READY.` prompt — the agent would see a blue boot screen, not the game. Force
54
60
  // autostart so a disk/tape image runs the first program automatically (same as
@@ -155,7 +161,7 @@ export const PLATFORM_VIRTUAL_EXT = {
155
161
  pce: ".pce",
156
162
  msx: ".rom",
157
163
  };
158
- import { RETRO_DEVICE_JOYPAD } from "./retroConstants.js";
164
+ import { RETRO_DEVICE_JOYPAD, ROMDEV_PIXEL_FORMAT_RGBA8888 } from "./retroConstants.js";
159
165
 
160
166
  // C64 controller→keyboard map (the Batocera/RetroDeck model: a CONTROLLER alone
161
167
  // plays C64 — no physical keyboard needed — by mapping spare buttons/stick to
@@ -241,11 +247,45 @@ export class LibretroHost {
241
247
  * Load a libretro core module. Registers callbacks then runs retro_init.
242
248
  * @param {string} jsPath
243
249
  * @param {string} [wasmPath]
250
+ * @param {Object} [opts]
251
+ * @param {boolean} [opts.hwRender] this core HW-renders (GL) — n64/ps1. Loads the
252
+ * optional native GL stack and creates a headless context BEFORE the core boots
253
+ * (GL calls happen during init/load). The 14 software cores omit this.
244
254
  */
245
- async loadCore(jsPath, wasmPath) {
255
+ async loadCore(jsPath, wasmPath, opts = {}) {
246
256
  if (this.mod) throw new Error("core already loaded; create a new host");
247
- const mod = await loadLibretroCore({ jsPath, wasmPath });
257
+
258
+ let glCanvas = null;
259
+ if (opts.hwRender) {
260
+ // Set up the HW-render path: LibretroGL owns the FBO/readback; a webgl-node
261
+ // mock canvas backs the minified core's GLctx. Both pull the OPTIONAL native
262
+ // GL deps lazily (clear install hint if absent).
263
+ const { LibretroGL } = await import("./LibretroGL.js");
264
+ const { loadWebGl2Context } = await import("./glOptionalDep.js");
265
+ this.hwRender = new LibretroGL();
266
+ await this.hwRender.ensureGl();
267
+ const createWebGL2Context = await loadWebGl2Context();
268
+ const { canvas } = createWebGL2Context(640, 480);
269
+ // Skip Emscripten's Safari WebGL2 workaround (breaks instanceof) by
270
+ // pre-setting the flag it checks.
271
+ canvas.getContextSafariWebGL2Fixed = canvas.getContext;
272
+ glCanvas = canvas;
273
+ this._glContextCreated = true;
274
+ // The env callback routes SET_HW_RENDER to this handler.
275
+ this.state.hwRender = this.hwRender;
276
+ }
277
+
278
+ const mod = await loadLibretroCore({ jsPath, wasmPath, glCanvas });
248
279
  this.mod = mod;
280
+
281
+ // Canvas HW-render cores: initialize the Emscripten GL context so `GLctx` is
282
+ // set BEFORE any GL call (the core's context_reset / first getParameter). Must
283
+ // run before _retro_init.
284
+ if (glCanvas && mod.GL) {
285
+ const handle = mod.GL.createContext(glCanvas, { majorVersion: 2 });
286
+ if (handle > 0) mod.GL.makeContextCurrent(handle);
287
+ }
288
+
249
289
  registerCallbacks({ mod, state: this.state, log: this.log });
250
290
  mod._retro_init();
251
291
  this.status.corePath = jsPath;
@@ -430,8 +470,19 @@ export class LibretroHost {
430
470
  : this.status.fbWidth / this.status.fbHeight;
431
471
  // timing.sample_rate is at offset +32 (double).
432
472
  this.status.audioSampleRate = mod.getValue(avInfoPtr + 32, "double");
473
+ // max_width/max_height (+8/+12) bound the FBO for HW-render cores (the core may
474
+ // render larger than base, e.g. N64 hi-res or PS1 internal upscale).
475
+ const maxW = mod.getValue(avInfoPtr + 8, "i32") || this.status.fbWidth;
476
+ const maxH = mod.getValue(avInfoPtr + 12, "i32") || this.status.fbHeight;
433
477
  mod._free(avInfoPtr);
434
478
 
479
+ // HW render: now that AV info is known, create the FBO the core renders into.
480
+ // createContext() also fires the core's context_reset (via dynCall) so it
481
+ // (re)builds its GL resources. The EGL context was already created in loadCore.
482
+ if (this.hwRender?.active) {
483
+ this.hwRender.createContext(maxW, maxH, this._glContextCreated);
484
+ }
485
+
435
486
  // Configure controller port 0 as joypad (some cores default to NONE).
436
487
  mod._retro_set_controller_port_device(0, RETRO_DEVICE_JOYPAD);
437
488
  // Port 1 too — needed for 2P. The C64/VICE 2P path (two live control ports)
@@ -480,6 +531,7 @@ export class LibretroHost {
480
531
  let firstRefreshAt = -1;
481
532
  while (stepped < MAX_SETTLE) {
482
533
  mod._retro_run();
534
+ this._afterRun();
483
535
  stepped++;
484
536
  if (firstRefreshAt < 0 && this.state.lastFrame && this.state.lastFrame !== beforeRefreshSnapshot) {
485
537
  firstRefreshAt = stepped;
@@ -520,6 +572,7 @@ export class LibretroHost {
520
572
  if (this.status.paused) return 0;
521
573
  for (let i = 0; i < n; i++) {
522
574
  mod._retro_run();
575
+ this._afterRun();
523
576
  this.status.frameCount++;
524
577
  }
525
578
  if (this.state.lastFrame) {
@@ -529,6 +582,23 @@ export class LibretroHost {
529
582
  return n;
530
583
  }
531
584
 
585
+ /** Post-_retro_run hook: HW-render readback. The video callback set
586
+ * state.hwFramePending during run; now (GL state stable) read back the FBO into
587
+ * state.lastFrame as an RGBA frame. No-op for the 14 software cores. */
588
+ _afterRun() {
589
+ if (this.state.hwFramePending && this.hwRender?.active) {
590
+ this.state.hwFramePending = false;
591
+ const frame = this.hwRender.readbackFrame();
592
+ if (frame) {
593
+ this.state.lastFrame = {
594
+ width: frame.width, height: frame.height,
595
+ pitch: frame.width * 4, format: ROMDEV_PIXEL_FORMAT_RGBA8888,
596
+ pixels: frame.pixels, rgba: true,
597
+ };
598
+ }
599
+ }
600
+ }
601
+
532
602
  /** Run exactly ONE frame to refresh the framebuffer, even while paused — for a
533
603
  * deterministic "restore → screenshot" without un-pausing (so the real-time
534
604
  * playtest loop can't race). Advances the (monotonic) frame counter by 1.
@@ -537,6 +607,7 @@ export class LibretroHost {
537
607
  const mod = this._needMod();
538
608
  this._needMedia();
539
609
  mod._retro_run();
610
+ this._afterRun();
540
611
  this.status.frameCount++;
541
612
  if (this.state.lastFrame) {
542
613
  this.status.fbWidth = this.state.lastFrame.width;
@@ -821,6 +892,19 @@ export class LibretroHost {
821
892
 
822
893
  readMemory(region, offset, length) {
823
894
  const mod = this._needMod();
895
+ // PS1 GPU VRAM (1024×512×16bpp = 1MB) isn't on the RETRO_MEMORY interface; the
896
+ // rebuilt pcsx core exposes it via romdev_vram_get. Serve video_ram from there.
897
+ if (region === "video_ram" && typeof mod._romdev_vram_get === "function") {
898
+ const SIZE = 1024 * 512 * 2;
899
+ if (offset < 0 || offset + length > SIZE) {
900
+ throw new RangeError(`read out of bounds: offset=${offset} len=${length} size=${SIZE}`);
901
+ }
902
+ const p = mod._malloc(SIZE);
903
+ try {
904
+ mod._romdev_vram_get(p, SIZE / 2);
905
+ return new Uint8Array(mod.HEAPU8.buffer, p + offset, length).slice();
906
+ } finally { mod._free(p); }
907
+ }
824
908
  const id = MemoryRegionToRetro[region];
825
909
  if (id === undefined) throw new Error(this._unknownRegionError(region));
826
910
  const ptr = mod._retro_get_memory_data(id);
@@ -1380,6 +1464,63 @@ export class LibretroHost {
1380
1464
  * instruction (ARM: its pipeline PC, the same convention breakpoint
1381
1465
  * addresses use). Pass clear to reset the kind.
1382
1466
  */
1467
+ /** True when the loaded MIPS core (n64/ps1) exposes the live R3000/R4300 register
1468
+ * snapshot (the cheat+regsnap-enabled romdev build). */
1469
+ mipsRegsSupported() {
1470
+ return !!(this.mod && typeof this.mod._romdev_mips_regs_get === "function");
1471
+ }
1472
+
1473
+ /** Read the live MIPS register file: 32 GPRs + LO + HI + PC, as a Uint32Array(35).
1474
+ * null if the core doesn't expose it (older build). */
1475
+ getMipsRegs() {
1476
+ const mod = this.mod;
1477
+ if (!mod || typeof mod._romdev_mips_regs_get !== "function") return null;
1478
+ const ptr = mod._malloc(35 * 4);
1479
+ try {
1480
+ mod._romdev_mips_regs_get(ptr);
1481
+ return new Uint32Array(mod.HEAPU8.buffer, ptr, 35).slice();
1482
+ } finally {
1483
+ mod._free(ptr);
1484
+ }
1485
+ }
1486
+
1487
+ /** True when the PS1 core exposes the SPU register block (getAudioState chip:'spu'). */
1488
+ spuRegsSupported() {
1489
+ return !!(this.mod && typeof this.mod._romdev_spu_get === "function");
1490
+ }
1491
+
1492
+ /** True when the N64 core exposes the AI registers (getAudioState chip:'ai'). */
1493
+ aiRegsSupported() {
1494
+ return !!(this.mod && typeof this.mod._romdev_ai_get === "function");
1495
+ }
1496
+
1497
+ /** Read the N64 AI registers + VI clock as a Uint32Array(7). null if absent. */
1498
+ getAiRegs() {
1499
+ const mod = this.mod;
1500
+ if (!mod || typeof mod._romdev_ai_get !== "function") return null;
1501
+ const ptr = mod._malloc(7 * 4);
1502
+ try {
1503
+ mod._romdev_ai_get(ptr);
1504
+ return new Uint32Array(mod.HEAPU8.buffer, ptr, 7).slice();
1505
+ } finally {
1506
+ mod._free(ptr);
1507
+ }
1508
+ }
1509
+
1510
+ /** Read the PS1 SPU's 0x400-word register block as a Uint16Array(1024). null if
1511
+ * the core doesn't expose it. */
1512
+ getSpuRegs() {
1513
+ const mod = this.mod;
1514
+ if (!mod || typeof mod._romdev_spu_get !== "function") return null;
1515
+ const ptr = mod._malloc(0x400 * 2);
1516
+ try {
1517
+ mod._romdev_spu_get(ptr, 0x400);
1518
+ return new Uint16Array(mod.HEAPU8.buffer, ptr, 0x400).slice();
1519
+ } finally {
1520
+ mod._free(ptr);
1521
+ }
1522
+ }
1523
+
1383
1524
  getRegSnapshot(clear = false) {
1384
1525
  const mod = this.mod;
1385
1526
  if (!mod || typeof mod._romdev_regsnap_get !== "function") return null;
@@ -164,7 +164,17 @@ export function registerCallbacks(args) {
164
164
  mod._retro_set_environment(envCb);
165
165
 
166
166
  const videoCb = mod.addFunction((dataPtr, w, h, pitch) => {
167
- if (dataPtr === 0 || dataPtr === RETRO_HW_FRAME_BUFFER_VALID) return;
167
+ // HW-render path: dataPtr == RETRO_HW_FRAME_BUFFER_VALID means "the frame is in
168
+ // the GL framebuffer." The WASM passes the i32 as SIGNED -1; the constant is the
169
+ // UNSIGNED 0xFFFFFFFF — coerce with >>>0 so both match. DON'T read back here —
170
+ // GL state is only stable AFTER _retro_run returns. Flag it; stepFrames does the
171
+ // glReadPixels post-run.
172
+ const uPtr = dataPtr >>> 0;
173
+ if (uPtr === RETRO_HW_FRAME_BUFFER_VALID && state.hwRender?.active) {
174
+ state.hwFramePending = true;
175
+ return;
176
+ }
177
+ if (dataPtr === 0 || uPtr === RETRO_HW_FRAME_BUFFER_VALID) return;
168
178
  const bytes = h * pitch;
169
179
  const view = mod.HEAPU8.subarray(dataPtr, dataPtr + bytes);
170
180
  state.lastFrame = {
@@ -429,8 +439,14 @@ function handleEnv(mod, state, rawCmd, dataPtr, log) {
429
439
  case E.SET_PROC_ADDRESS_CALLBACK:
430
440
  return true;
431
441
 
442
+ // HW render (n64/ps1): if the host set up a GL handler (state.hwRender), let it
443
+ // claim the SET_HW_RENDER request + install its callbacks. Without it (the 14
444
+ // software cores, or no GL stack), reject so the core falls back to software.
445
+ case E.SET_HW_RENDER:
446
+ if (state.hwRender) return state.hwRender.setup(mod, dataPtr);
447
+ return false;
448
+
432
449
  // Things we can't or don't want to support — reject so core falls back.
433
- case E.SET_HW_RENDER: // no GL in headless mode
434
450
  case E.GET_RUMBLE_INTERFACE:
435
451
  case E.GET_SENSOR_INTERFACE:
436
452
  case E.GET_CAMERA_INTERFACE:
@@ -14,6 +14,10 @@ import { RETRO_API_VERSION } from "./retroConstants.js";
14
14
  * @typedef {Object} LoadCoreArgs
15
15
  * @property {string} jsPath absolute path to the `_libretro.js` glue
16
16
  * @property {string} [wasmPath] absolute path to the `.wasm`; if omitted, Emscripten resolves next to the .js
17
+ * @property {object} [glCanvas] HW-render cores (n64/ps1): a webgl-node mock canvas the
18
+ * minified Emscripten glue drives via `GLctx = canvas.getContext('webgl2')`.
19
+ * @property {object} [glBridge] HW-render cores with UNMINIFIED imports: a map of GL
20
+ * function name → native-gles impl, patched directly into the WASM env imports.
17
21
  */
18
22
 
19
23
  /**
@@ -22,7 +26,7 @@ import { RETRO_API_VERSION } from "./retroConstants.js";
22
26
  * @param {LoadCoreArgs} args
23
27
  */
24
28
  export async function loadLibretroCore(args) {
25
- const { jsPath, wasmPath } = args;
29
+ const { jsPath, wasmPath, glCanvas, glBridge } = args;
26
30
 
27
31
  const url = pathToFileURL(jsPath).href + "?t=" + Date.now();
28
32
  const ns = await import(url);
@@ -37,7 +41,50 @@ export async function loadLibretroCore(args) {
37
41
  print: () => {},
38
42
  printErr: () => {},
39
43
  };
40
- if (wasmPath) {
44
+
45
+ // HW-render cores: minified glue uses GLctx = canvas.getContext('webgl2'), so we
46
+ // hand it a webgl-node mock canvas + install the WebGL2 globals Emscripten probes.
47
+ if (glCanvas) {
48
+ opts.canvas = glCanvas;
49
+ if (typeof globalThis.WebGL2RenderingContext === "undefined") {
50
+ const { WebGL2RenderingContext } = await import("webgl-node");
51
+ globalThis.WebGL2RenderingContext = WebGL2RenderingContext;
52
+ // CRITICAL: WebGLRenderingContext (WebGL1) must be a DISTINCT class from
53
+ // WebGL2RenderingContext. Emscripten's getContext shim does
54
+ // return (ver=="webgl") == (gl instanceof WebGLRenderingContext) ? gl : null
55
+ // For ver="webgl2" that needs `gl instanceof WebGLRenderingContext` to be
56
+ // FALSE. If we aliased WebGLRenderingContext to the WebGL2 class, a webgl2
57
+ // context IS instanceof it → the shim returns null → GLctx never set → the
58
+ // core renders nothing. So WebGL1 gets its own empty marker class.
59
+ globalThis.WebGLRenderingContext = class WebGLRenderingContext {};
60
+ }
61
+ }
62
+
63
+ if (wasmPath && glBridge) {
64
+ // Unminified GL cores: patch GL function imports + no-op EGL (we own EGL via
65
+ // native-gles), and capture the instance memory for the bridge's marshaling.
66
+ const wasmBinary = await readFile(wasmPath);
67
+ opts.instantiateWasm = (info, receiveInstance) => {
68
+ for (const nsObj of Object.values(info)) {
69
+ if (typeof nsObj !== "object" || nsObj === null) continue;
70
+ for (const [name, fn] of Object.entries(glBridge)) {
71
+ if (name in nsObj) nsObj[name] = fn;
72
+ }
73
+ if ("eglGetDisplay" in nsObj) {
74
+ nsObj.eglGetDisplay = () => 62000;
75
+ nsObj.eglInitialize = () => 1;
76
+ nsObj.eglQueryString = () => 0;
77
+ }
78
+ }
79
+ WebAssembly.instantiate(wasmBinary, info).then((result) => {
80
+ if (glBridge._setMemory && result.instance.exports.memory) {
81
+ glBridge._setMemory(result.instance.exports.memory);
82
+ }
83
+ receiveInstance(result.instance, result.module);
84
+ });
85
+ return {};
86
+ };
87
+ } else if (wasmPath) {
41
88
  opts.wasmBinary = await readFile(wasmPath);
42
89
  }
43
90
 
@@ -139,6 +139,30 @@ function decodeSnes9xSPC(state) {
139
139
  * @param {Uint8Array} bytes 28-byte snapshot
140
140
  * @returns {ReturnType<typeof formatCpuState> | null}
141
141
  */
142
+ /** MIPS R3000 (PS1) / R4300 (N64) register names, $0..$31. */
143
+ const MIPS_GPR_NAMES = [
144
+ "zero", "at", "v0", "v1", "a0", "a1", "a2", "a3",
145
+ "t0", "t1", "t2", "t3", "t4", "t5", "t6", "t7",
146
+ "s0", "s1", "s2", "s3", "s4", "s5", "s6", "s7",
147
+ "t8", "t9", "k0", "k1", "gp", "sp", "fp", "ra",
148
+ ];
149
+
150
+ /** Decode the romdev MIPS regsnap (Uint32Array(35): 32 GPRs + LO + HI + PC) into a
151
+ * CPUState. Names follow the MIPS o32 ABI convention. */
152
+ function decodeMips(regs) {
153
+ if (!regs || regs.length < 35) return null;
154
+ const hx = (v) => "$" + (v >>> 0).toString(16).toUpperCase().padStart(8, "0");
155
+ const named = {};
156
+ for (let i = 0; i < 32; i++) named[MIPS_GPR_NAMES[i]] = hx(regs[i]);
157
+ named.lo = hx(regs[32]);
158
+ named.hi = hx(regs[33]);
159
+ return {
160
+ pc: regs[34] >>> 0,
161
+ pcHex: hx(regs[34]),
162
+ registers: named,
163
+ };
164
+ }
165
+
142
166
  function decode6502(bytes) {
143
167
  if (!bytes || bytes.length < 25) return null;
144
168
  const u16le = (o) => bytes[o] | (bytes[o + 1] << 8);
@@ -300,6 +324,9 @@ function decodeZ80(bytes) {
300
324
  * @returns {CPUState | null}
301
325
  */
302
326
  export function getCPUState(host, platform, cpu = "main") {
327
+ if (platform === "n64" || platform === "ps1") {
328
+ return decodeMips(host.getMipsRegs?.());
329
+ }
303
330
  if (platform === "nes") {
304
331
  const bytes = host.readMemory("nes_cpu_regs", 0, 28);
305
332
  return decode6502(bytes);
@@ -12,6 +12,7 @@ import {
12
12
  RETRO_PIXEL_FORMAT_0RGB1555,
13
13
  RETRO_PIXEL_FORMAT_RGB565,
14
14
  RETRO_PIXEL_FORMAT_XRGB8888,
15
+ ROMDEV_PIXEL_FORMAT_RGBA8888,
15
16
  } from "./retroConstants.js";
16
17
 
17
18
  /**
@@ -35,7 +36,19 @@ export function framebufferToRgba(width, height, src, pitch, format) {
35
36
  }
36
37
 
37
38
  function decodePixelsInto(dst, width, height, src, pitch, format) {
38
- if (format === RETRO_PIXEL_FORMAT_XRGB8888) {
39
+ if (format === ROMDEV_PIXEL_FORMAT_RGBA8888) {
40
+ // HW-render readback: already RGBA. Copy RGB row-by-row but FORCE alpha=255 —
41
+ // the N64/PS1 GL framebuffer leaves alpha at 0 (it's the render target's unused
42
+ // channel), which would make every pixel transparent → composites to white.
43
+ for (let y = 0; y < height; y++) {
44
+ const sRow = y * pitch, dRow = y * width * 4;
45
+ for (let x = 0; x < width; x++) {
46
+ const s = sRow + x * 4, d = dRow + x * 4;
47
+ dst[d] = src[s]; dst[d + 1] = src[s + 1]; dst[d + 2] = src[s + 2];
48
+ dst[d + 3] = 0xff;
49
+ }
50
+ }
51
+ } else if (format === RETRO_PIXEL_FORMAT_XRGB8888) {
39
52
  for (let y = 0; y < height; y++) {
40
53
  const srcRow = y * pitch;
41
54
  const dstRow = y * width * 4;
@@ -0,0 +1,60 @@
1
+ // glOptionalDep.js — lazy, optional loader for the native GL stack.
2
+ //
3
+ // The HW-render cores (N64 ParaLLEl, PS1 Beetle-PSX) need a real headless GL
4
+ // context. romdev provides it through `native-gles` (EGL/GLES via a prebuilt
5
+ // .node) and `webgl-node` (a WebGL2 context object for the minified Emscripten
6
+ // cores). Both are OPTIONAL dependencies: the 14 software-rendered cores never
7
+ // touch them, and a headless user who never boots N64/PS1 doesn't need them
8
+ // installed. We load them lazily and, if absent, throw ONE clear, actionable
9
+ // error instead of a cryptic module-not-found.
10
+
11
+ let _gl = null;
12
+ let _webgl = null;
13
+
14
+ const INSTALL_HINT =
15
+ "N64/PS1 emulation needs the optional native GL module. Install it with:\n" +
16
+ " npm install native-gles webgl-node\n" +
17
+ "(the other 14 platforms are software-rendered and don't require it).";
18
+
19
+ /**
20
+ * Load the `native-gles` default export (the EGL/GLES binding). Cached.
21
+ * @returns {Promise<object>} the native-gles module
22
+ * @throws {Error} a clear install hint if the module isn't available
23
+ */
24
+ export async function loadNativeGles() {
25
+ if (_gl) return _gl;
26
+ try {
27
+ _gl = (await import("native-gles")).default;
28
+ } catch (e) {
29
+ throw new Error(`${INSTALL_HINT}\n(underlying: ${e.message})`);
30
+ }
31
+ if (!_gl || typeof _gl.createContext !== "function") {
32
+ throw new Error(`native-gles loaded but has no createContext — ${INSTALL_HINT}`);
33
+ }
34
+ return _gl;
35
+ }
36
+
37
+ /**
38
+ * Load `webgl-node`'s createWebGL2Context (the canvas/context the minified
39
+ * Emscripten cores use via GLctx = canvas.getContext('webgl2')). Cached.
40
+ * @returns {Promise<Function>} createWebGL2Context(width, height) → { canvas, gl }
41
+ * @throws {Error} a clear install hint if the module isn't available
42
+ */
43
+ export async function loadWebGl2Context() {
44
+ if (_webgl) return _webgl;
45
+ try {
46
+ const m = await import("webgl-node");
47
+ _webgl = m.createWebGL2Context;
48
+ } catch (e) {
49
+ throw new Error(`${INSTALL_HINT}\n(underlying: ${e.message})`);
50
+ }
51
+ if (typeof _webgl !== "function") {
52
+ throw new Error(`webgl-node loaded but has no createWebGL2Context — ${INSTALL_HINT}`);
53
+ }
54
+ return _webgl;
55
+ }
56
+
57
+ /** Best-effort probe: is the native GL stack importable? (for capability flags) */
58
+ export async function glStackAvailable() {
59
+ try { await loadNativeGles(); return true; } catch { return false; }
60
+ }
@@ -0,0 +1,43 @@
1
+ // N64 AI (Audio Interface) decoder.
2
+ //
3
+ // Data source: the romdev_ai_get export (parallel_n64) — the 6 AI registers + the
4
+ // VI clock. The N64 has no per-voice sound chip like the SNES DSP; audio is mixed
5
+ // by the RSP (microcode-dependent) and streamed to the AI's DAC via DMA. So the
6
+ // AI tells us the OUTPUT state: sample rate, whether audio is playing (a buffer is
7
+ // queued), and the DMA source address — the actionable "is sound on, at what rate"
8
+ // answer, not a per-channel breakdown (which lives in game-specific RSP audio lists).
9
+ //
10
+ // out layout: [DRAM_ADDR, LEN, CONTROL, STATUS, DACRATE, BITRATE, VI_CLOCK]
11
+
12
+ /**
13
+ * @param {Uint32Array} a the 7-word AI snapshot (6 regs + VI clock)
14
+ * @returns {{ chip:"ai", playing:boolean, sampleRate:number, dmaAddress:number,
15
+ * queuedBytes:number, dacRate:number, bitRate:number, control:number,
16
+ * status:number, note:string }}
17
+ */
18
+ export function decodeN64Ai(a) {
19
+ if (!a || a.length < 7) return { chip: "ai", playing: false, note: "no AI data" };
20
+ const dramAddr = a[0] >>> 0;
21
+ const len = a[1] >>> 0;
22
+ const control = a[2] >>> 0;
23
+ const status = a[3] >>> 0;
24
+ const dacRate = a[4] >>> 0;
25
+ const bitRate = a[5] >>> 0;
26
+ const viClock = a[6] >>> 0;
27
+ // DAC sample rate = VI clock / (dacrate + 1). dacrate 0 / no clock → unknown.
28
+ const sampleRate = dacRate && viClock ? Math.round(viClock / (dacRate + 1)) : 0;
29
+ // AI_LEN nonzero (a buffer is queued) AND DMA enabled (control bit0) = playing.
30
+ const playing = (len & 0x3ffff) > 0 && (control & 1) !== 0;
31
+ return {
32
+ chip: "ai",
33
+ playing,
34
+ sampleRate,
35
+ queuedBytes: len & 0x3ffff,
36
+ dmaAddress: "0x" + dramAddr.toString(16).toUpperCase(),
37
+ dacRate,
38
+ bitRate,
39
+ control,
40
+ status,
41
+ note: "N64 audio is RSP-mixed; the AI exposes the OUTPUT (sample rate + whether a buffer is playing + the DMA source), not per-channel voices. For per-channel, trace the game's RSP audio list in RDRAM.",
42
+ };
43
+ }
@@ -0,0 +1,65 @@
1
+ // PS1 SPU (Sound Processing Unit) decoder.
2
+ //
3
+ // Data source: the SPU's 0x400-word register block ($1F801C00-based), exposed by
4
+ // the romdev_spu_get export in the rebuilt pcsx_rearmed core. The block is indexed
5
+ // as 16-bit words; per-voice registers occupy the first 24×8 words (16 bytes each),
6
+ // then the global voice/control registers follow.
7
+ //
8
+ // The SPU has 24 ADPCM voices. Per voice (offset = voice*8 words):
9
+ // +0 VolumeLeft +1 VolumeRight +2 ADPCM SampleRate (pitch)
10
+ // +3 ADPCM StartAddr +4 ADSR lo +5 ADSR hi +6 ADSR CurrentVol +7 RepeatAddr
11
+ // Global (word index):
12
+ // 0xC0/0xC1 = main volume L/R, 0xCC/0xCD = KeyOn (24-bit across two words),
13
+ // 0xCE/0xCF = KeyOff, 0xD6 = SPU control (SPUCNT).
14
+
15
+ const NUM_VOICES = 24;
16
+
17
+ /** SPU pitch → Hz: the 16-bit pitch is in units of 44100/4096 Hz per step
18
+ * (4096 = "1.0" = 44100 Hz). */
19
+ function pitchToHz(pitch) {
20
+ return Math.round((pitch / 4096) * 44100);
21
+ }
22
+
23
+ /**
24
+ * @param {Uint16Array} regs the 0x400-word SPU register block
25
+ * @returns {{ chip:"spu", mainVolumeLeft:number, mainVolumeRight:number,
26
+ * control:number, voices:Array<object> }}
27
+ */
28
+ export function decodePs1Spu(regs) {
29
+ if (!regs || regs.length < 0x200) return { chip: "spu", voices: [] };
30
+ // Key-on / key-off are 24-bit, split across two 16-bit words.
31
+ const keyOn = (regs[0xCC] | (regs[0xCD] << 16)) >>> 0;
32
+ const keyOff = (regs[0xCE] | (regs[0xCF] << 16)) >>> 0;
33
+
34
+ const voices = [];
35
+ for (let v = 0; v < NUM_VOICES; v++) {
36
+ const b = v * 8;
37
+ const volL = regs[b + 0];
38
+ const volR = regs[b + 1];
39
+ const pitch = regs[b + 2];
40
+ const adsr = (regs[b + 4] | (regs[b + 5] << 16)) >>> 0;
41
+ const curVol = regs[b + 6];
42
+ const on = !!(keyOn & (1 << v));
43
+ voices.push({
44
+ voice: v,
45
+ keyOn: on,
46
+ keyOff: !!(keyOff & (1 << v)),
47
+ // signed 15-bit volumes (top bit = sweep mode; report the magnitude)
48
+ volumeLeft: volL & 0x7fff,
49
+ volumeRight: volR & 0x7fff,
50
+ pitch,
51
+ hz: pitch ? pitchToHz(pitch) : 0,
52
+ adsr: "0x" + adsr.toString(16).padStart(8, "0").toUpperCase(),
53
+ currentVolume: curVol,
54
+ active: curVol > 0 || on,
55
+ });
56
+ }
57
+ return {
58
+ chip: "spu",
59
+ mainVolumeLeft: regs[0xC0] & 0x7fff,
60
+ mainVolumeRight: regs[0xC1] & 0x7fff,
61
+ control: regs[0xD6],
62
+ keyOnMask: "0x" + keyOn.toString(16).toUpperCase(),
63
+ voices,
64
+ };
65
+ }
@@ -70,3 +70,17 @@ export const RETRO_LOG_ERROR = 3;
70
70
  // Sentinels
71
71
  /** Video refresh data pointer meaning "GL framebuffer is valid". */
72
72
  export const RETRO_HW_FRAME_BUFFER_VALID = -1 >>> 0; // unsigned -1
73
+
74
+ // HW render context types (retro_hw_context_type) — the GL flavors the
75
+ // HW-render cores (n64/ps1) request via RETRO_ENVIRONMENT_SET_HW_RENDER.
76
+ export const RETRO_HW_CONTEXT_NONE = 0;
77
+ export const RETRO_HW_CONTEXT_OPENGL = 1; // OpenGL 2.x (compat)
78
+ export const RETRO_HW_CONTEXT_OPENGLES2 = 2; // GLES 2.0
79
+ export const RETRO_HW_CONTEXT_OPENGL_CORE = 3; // OpenGL 3+ core
80
+ export const RETRO_HW_CONTEXT_OPENGLES3 = 4; // GLES 3.0
81
+ export const RETRO_HW_CONTEXT_OPENGLES_VERSION = 5; // GLES with explicit version
82
+
83
+ // romdev-internal sentinel: a HW-render readback frame is already RGBA8888 (from
84
+ // glReadPixels GL_RGBA), NOT the BGRA byte order libretro's XRGB8888 uses. The
85
+ // framebuffer decoder special-cases this so R/B aren't swapped.
86
+ export const ROMDEV_PIXEL_FORMAT_RGBA8888 = 0x52474241; // 'RGBA'