romdevtools 0.44.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 (61) hide show
  1. package/CHANGELOG.md +293 -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/disasm.js +2 -0
  37. package/src/mcp/tools/frame.js +18 -9
  38. package/src/mcp/tools/lifecycle.js +1 -1
  39. package/src/mcp/tools/platform-tools.js +20 -4
  40. package/src/mcp/tools/platforms.js +38 -4
  41. package/src/mcp/tools/project.js +100 -0
  42. package/src/mcp/tools/rendering-context.js +2 -1
  43. package/src/mcp/tools/toolchain.js +1 -1
  44. package/src/platforms/n64/lib/c/n64.c +196 -0
  45. package/src/platforms/n64/lib/c/n64.h +68 -0
  46. package/src/platforms/ps1/lib/c/psx.c +200 -0
  47. package/src/platforms/ps1/lib/c/psx.h +83 -0
  48. package/src/toolchains/index.js +35 -0
  49. package/src/toolchains/mips-c/lib/be/libc.a +0 -0
  50. package/src/toolchains/mips-c/lib/be/libgcc.a +0 -0
  51. package/src/toolchains/mips-c/lib/be/libm.a +0 -0
  52. package/src/toolchains/mips-c/lib/el/libc.a +0 -0
  53. package/src/toolchains/mips-c/lib/el/libm.a +0 -0
  54. package/src/toolchains/mips-c/lib/n64-crt0.s +21 -0
  55. package/src/toolchains/mips-c/lib/n64-ipl3.s +30 -0
  56. package/src/toolchains/mips-c/lib/n64.ld +15 -0
  57. package/src/toolchains/mips-c/lib/ps1-crt0.s +20 -0
  58. package/src/toolchains/mips-c/lib/ps1.ld +15 -0
  59. package/src/toolchains/mips-c/lib/softint.c +37 -0
  60. package/src/toolchains/mips-c/mips-c.js +155 -0
  61. package/src/toolchains/mips-elf-gcc/gcc.js +130 -0
@@ -54,6 +54,19 @@ export const RIZIN_ARCH = {
54
54
  // DECOMPILER uses the proper HuC6280 SLEIGH spec — so CFG/xrefs/functions are
55
55
  // approximate on PCE while decompile is accurate.
56
56
  pce: "6502",
57
+ // 32-bit MIPS tier (analysis-first; the MIPS plugin ships in rizin.wasm). PS1 =
58
+ // R3000 little-endian; N64 = R4300 big-endian. Endianness comes from RIZIN_ENDIAN
59
+ // below — same arch, different byte order.
60
+ ps1: "mips",
61
+ n64: "mips",
62
+ };
63
+
64
+ /** Byte order per platform, for shared-arch families that ship both (MIPS). Only
65
+ * platforms NOT matching their arch default need an entry; absent → use the
66
+ * loader/arch default. PS1 is little, N64 is big. */
67
+ export const RIZIN_ENDIAN = {
68
+ ps1: "little",
69
+ n64: "big",
57
70
  };
58
71
 
59
72
  /**
@@ -77,13 +90,21 @@ export const RIZIN_ARCH = {
77
90
  * @returns {Promise<{exitCode:number, output:string, log:string, crash?:object}>}
78
91
  */
79
92
  export async function runRizin(opts) {
80
- const { romPath, romBytes, commands, arch, bits, baddr, writeable, timeoutMs } = opts;
93
+ const { romPath, romBytes, commands, arch, bits, baddr, writeable, timeoutMs, endian } = opts;
81
94
  if (!commands) throw new Error("runRizin: commands required");
82
95
  const bytes = romBytes ?? new Uint8Array(await readFile(romPath));
83
96
 
84
97
  const pre = ["e scr.color=0", "e scr.interactive=false", "e scr.prompt=false"];
85
98
  if (arch) pre.push(`e asm.arch=${arch}`);
86
99
  if (bits) pre.push(`e asm.bits=${bits}`);
100
+ // Endianness matters for shared-arch families that ship both byte orders — most
101
+ // pressingly MIPS: PS1 (R3000) is little-endian, N64 (R4300) is big-endian, same
102
+ // `mips` plugin. Set both asm + cfg so the disasm AND the analysis loader agree.
103
+ if (endian === "big" || endian === "little") {
104
+ // `cfg.bigendian` is the rizin config var (there is NO `asm.bigendian` —
105
+ // setting it errors). cfg.bigendian drives BOTH the disasm and the analysis.
106
+ pre.push(`e cfg.bigendian=${endian === "big" ? "true" : "false"}`);
107
+ }
87
108
 
88
109
  // Split trailing command off so its output (only) goes to the file. Earlier
89
110
  // commands (aaa, config) print nothing we need. Rizin's `cmd > file` writes
@@ -202,10 +202,71 @@ export const CAPABILITIES = {
202
202
  cart: false, disasm: true, decompile: true,
203
203
  },
204
204
  },
205
+
206
+ // ── 32-bit MIPS tier — ANALYSIS-FIRST (no run-side core yet) ──────────────
207
+ // disasm/decompile are wired through the shipped rizin.wasm MIPS plugin (PS1 =
208
+ // R3000 LE, N64 = R4300 BE). Everything run-side (build/run/screenshot/the
209
+ // tile/sprite inspectors) is FALSE: there is no emulator core in this slice, and
210
+ // the tile/nametable/OAM inspectors are MEANINGLESS on a framebuffer (PS1) / 3D
211
+ // (N64) renderer anyway — so an agent gets the uniform unsupported() signal
212
+ // instead of blindly calling inspectBackground on a polygon machine.
213
+ // disasm/cfg/xrefs/functions WORK (rizin's Capstone MIPS plugin). decompile is
214
+ // FALSE: the rz-ghidra decompiler ships only the 8 SLEIGH specs for the current
215
+ // tier (no MIPS.sla) — adding it is a romdev-analysis-decompiler rebuild, a later
216
+ // step. decompileQuality records the EXPECTED quality once that spec ships.
217
+ ps1: {
218
+ cpuFamily: "mips", decompileQuality: "good", tier: "mips",
219
+ cpus: { main: "r3000", secondary: ["gte"] },
220
+ audioChips: ["spu"],
221
+ memoryRegions: [...GENERIC_REGIONS],
222
+ renderingKind: "framebuffer", introspection: "shallow",
223
+ ops: {
224
+ // pcsx_rearmed: software render + HLE BIOS (no firmware, no GL). run/screenshot
225
+ // + cpuState (R3000 regsnap) + cheats live; disasm + decompile work (MIPS
226
+ // Capstone + the MIPS:LE:32 SLEIGH spec). build needs a PS1 toolchain
227
+ // (PSn00bSDK, not yet). The framebuffer renderer has no tile/sprite inspectors.
228
+ build: true, run: true, screenshot: true,
229
+ inspectSprites: false, inspectPalette: false, inspectBackground: false,
230
+ renderingContext: false, cpuState: true, audioDebug: true,
231
+ cart: false, disasm: true, decompile: true,
232
+ },
233
+ },
234
+ n64: {
235
+ cpuFamily: "mips", decompileQuality: "good", tier: "mips",
236
+ cpus: { main: "r4300", secondary: ["rsp"] },
237
+ audioChips: ["ai"], // AI = the audio OUTPUT interface (RSP-mixed; no per-voice chip)
238
+ memoryRegions: [...GENERIC_REGIONS],
239
+ renderingKind: "3d", introspection: "shallow",
240
+ ops: {
241
+ // parallel_n64: HW (GL) render via the optional native-gles bridge.
242
+ // run/screenshot + cpuState (R4300 regsnap) + cheats + breakpoint/watch +
243
+ // audioDebug(AI) live; disasm + decompile work; build via mips-elf-gcc.
244
+ // The 3D renderer has no tile/sprite inspectors (N/A by hardware).
245
+ build: true, run: true, screenshot: true,
246
+ inspectSprites: false, inspectPalette: false, inspectBackground: false,
247
+ renderingContext: false, cpuState: true, audioDebug: true,
248
+ cart: false, disasm: true, decompile: true,
249
+ },
250
+ },
205
251
  };
206
252
 
207
- /** All platform ids in the contract. */
208
- export const CONTRACT_PLATFORMS = Object.keys(CAPABILITIES);
253
+ /** The 32-bit MIPS tier (PS1/N64) marked `tier:"mips"`. A PARTIAL tier: they
254
+ * run + screenshot + disasm, but don't yet have the full op surface of the canonical
255
+ * 14 (no build toolchain, no MIPS decompile/cpuState, framebuffer/3D renderers have
256
+ * no tile/sprite inspectors). They're held to their OWN conformance, not the
257
+ * "all 14" cross-checks. They graduate to CONTRACT_PLATFORMS as the gaps close. */
258
+ export const MIPS_TIER_PLATFORMS = Object.entries(CAPABILITIES)
259
+ .filter(([, c]) => c.tier === "mips")
260
+ .map(([p]) => p);
261
+
262
+ /** Back-compat: the analysis-only set is now empty (PS1/N64 gained run/screenshot
263
+ * in the run-side wiring). Kept so the name still resolves for older imports. */
264
+ export const ANALYSIS_ONLY_PLATFORMS = [];
265
+
266
+ /** The canonical tier-1 platforms (the 14): full op surface, universal build/run/
267
+ * screenshot. Excludes the partial MIPS tier above. */
268
+ export const CONTRACT_PLATFORMS = Object.keys(CAPABILITIES)
269
+ .filter((p) => !MIPS_TIER_PLATFORMS.includes(p));
209
270
 
210
271
  /** Does `platform` support `op`? Unknown platform/op → false. */
211
272
  export function supports(platform, op) {
@@ -216,3 +277,30 @@ export function supports(platform, op) {
216
277
  export function capabilitiesFor(platform) {
217
278
  return CAPABILITIES[platform] ?? null;
218
279
  }
280
+
281
+ // Why an op is unsupported, grounded in the HARDWARE — so `unsupported()`'s reason
282
+ // distinguishes "N/A by hardware, permanent" from "a decoder we haven't wired."
283
+ // Keyed on renderingKind: a framebuffer (PS1) / 3D (N64) renderer has no tile,
284
+ // sprite-attribute, nametable, or palette tables for the tile-era inspectors to
285
+ // read — those ops are MEANINGLESS on the hardware, not merely absent.
286
+ const RENDERING_NA = {
287
+ framebuffer: "this platform renders to a flat framebuffer — there are no tile/sprite-attribute/nametable/palette tables to inspect (the GPU draws pixels/polys directly). Read the raw framebuffer via memory({region:'video_ram'}).",
288
+ "3d": "this platform is a 3D (polygon) renderer — there are no tile/sprite-attribute/nametable/palette tables to inspect (geometry is transformed + rasterized, not composed from tile maps). Inspect the scene via memory({region:'system_ram'}) / cpu state.",
289
+ };
290
+ const NA_OPS = new Set(["inspectSprites", "inspectPalette", "inspectBackground", "renderingContext"]);
291
+
292
+ /** A hardware-grounded reason an op is unsupported on a platform, or null if the
293
+ * op isn't one of the hardware-N/A introspection ops. Drives unsupported().reason
294
+ * so agents see "N/A by hardware" (don't retry / don't request a decoder) rather
295
+ * than a generic "no decoder for this platform". */
296
+ export function naReason(platform, op) {
297
+ const cap = CAPABILITIES[platform];
298
+ if (!cap || cap.ops?.[op]) return null; // supported → no N/A reason
299
+ if (op === "cart") {
300
+ return cap.renderingKind === "framebuffer"
301
+ ? "this platform is disc-based (no cartridge ROM to inspect/patch as a cart)."
302
+ : null;
303
+ }
304
+ if (NA_OPS.has(op)) return RENDERING_NA[cap.renderingKind] ?? null;
305
+ return null;
306
+ }
@@ -53,6 +53,18 @@ export const CORES = {
53
53
  c64: { platform: "c64", coreName: "vice_x64", pkg: "romdev-core-vice", displayName: "Commodore 64 (VICE x64)" },
54
54
  pce: { platform: "pce", coreName: "geargrafx", pkg: "romdev-core-geargrafx", displayName: "PC Engine / TurboGrafx-16 (Geargrafx)", aka: "turbografx,tg16,pcengine" },
55
55
  msx: { platform: "msx", coreName: "bluemsx", pkg: "romdev-core-bluemsx", displayName: "MSX / MSX2 (blueMSX)", aka: "msx2" },
56
+ // 32-bit MIPS tier. These cores HW-render (GL): the host lazy-loads the OPTIONAL
57
+ // webgl-node bridge only when one of these boots (hwRender:true). The other 14 are
58
+ // software-rendered and never touch GL, so a headless user without the GPU module
59
+ // is unaffected.
60
+ // parallel_n64 built headless-angrylion (software RDP + VI scanout, no GL) so raw
61
+ // CPU-rendered homebrew framebuffers display — the clean no-GL-dependency N64 path,
62
+ // like pcsx_rearmed for PS1. (The GL/glide64 build only presents RDP display-lists.)
63
+ n64: { platform: "n64", coreName: "parallel_n64", pkg: "romdev-core-parallel-n64", displayName: "Nintendo 64 (ParaLLEl N64, software)", hwRender: false },
64
+ // pcsx_rearmed = SOFTWARE renderer + built-in HLE BIOS (no firmware to ship, no
65
+ // GL dependency) — the clean no-dependency PS1 path, like the other 14. The GL
66
+ // beetle_psx_hw (+ real BIOS) stays available as a higher-fidelity alternative.
67
+ ps1: { platform: "ps1", coreName: "pcsx_rearmed", pkg: "romdev-core-pcsx-rearmed", displayName: "Sony PlayStation (PCSX-ReARMed, HLE)", aka: "psx,playstation" },
56
68
  };
57
69
 
58
70
  /** Try to get {jsPath,wasmPath} for a core from its binary package. */
@@ -0,0 +1,270 @@
1
+ // LibretroGL.js — Hardware-rendered libretro core support
2
+ //
3
+ // Handles RETRO_ENVIRONMENT_SET_HW_RENDER by creating an EGL context via
4
+ // native-gles and providing the three required callbacks:
5
+ // context_reset, get_current_framebuffer, get_proc_address
6
+ //
7
+ // The core renders to an FBO. After retro_run(), we readback via glReadPixels
8
+ // and send the pixels to VideoOutput (same as wasmcart GL carts).
9
+
10
+ import {
11
+ RETRO_HW_CONTEXT_OPENGLES2,
12
+ RETRO_HW_CONTEXT_OPENGLES3,
13
+ RETRO_HW_CONTEXT_OPENGLES_VERSION,
14
+ RETRO_HW_CONTEXT_OPENGL,
15
+ RETRO_HW_CONTEXT_OPENGL_CORE,
16
+ } from './retroConstants.js';
17
+ import { loadNativeGles } from './glOptionalDep.js';
18
+
19
+ // `native-gles` is an OPTIONAL native dependency — only the HW-render cores
20
+ // (n64/ps1) need it. It's loaded lazily at setup() so the 14 software-rendered
21
+ // cores and headless installs without the GPU module are completely unaffected.
22
+ let gl = null;
23
+
24
+ // retro_hw_render_callback struct offsets (WASM32):
25
+ // +0 i32 context_type
26
+ // +4 ptr context_reset (set by frontend)
27
+ // +8 ptr get_current_framebuffer (set by frontend)
28
+ // +12 ptr get_proc_address (set by frontend)
29
+ // +16 u8 depth
30
+ // +17 u8 stencil
31
+ // +18 u8 bottom_left_origin
32
+ // +20 u32 version_major (aligned)
33
+ // +24 u32 version_minor
34
+ // +28 u8 cache_context
35
+ // +32 ptr context_destroy (set by core)
36
+ // +36 u8 debug_context
37
+
38
+ export class LibretroGL {
39
+ constructor() {
40
+ this.active = false;
41
+ this.contextType = 0;
42
+ this.bottomLeftOrigin = true;
43
+ this.fbo = 0;
44
+ this.fboW = 0;
45
+ this.fboH = 0;
46
+ this.contextResetPtr = 0; // WASM function pointer
47
+ this.contextDestroyPtr = 0; // WASM function pointer
48
+ this._mod = null;
49
+ this._glPixels = null;
50
+ this._flipped = null;
51
+
52
+ // GL function name → addFunction wrapper pointer (cached)
53
+ this._procAddressCache = new Map();
54
+ }
55
+
56
+ /**
57
+ * Load the optional native GL module (lazy). MUST be awaited before the core
58
+ * boots, since the 14 software cores never call this and the module is an
59
+ * optional dependency. Populates the module-level `gl` used by
60
+ * createContext/readbackFrame/destroy.
61
+ * @returns {Promise<void>}
62
+ */
63
+ async ensureGl() {
64
+ if (!gl) gl = await loadNativeGles();
65
+ }
66
+
67
+ /** Make this core's GL context current. Called before each _retro_run so the
68
+ * core renders into OUR FBO (a multi-host server may have switched contexts). */
69
+ makeCurrent() {
70
+ if (this.active && gl) gl.makeCurrent();
71
+ }
72
+
73
+ /**
74
+ * Handle RETRO_ENVIRONMENT_SET_HW_RENDER.
75
+ * Reads the struct, creates EGL context, writes callback pointers back.
76
+ * @param {object} mod - Emscripten module
77
+ * @param {number} dataPtr - pointer to retro_hw_render_callback
78
+ * @returns {boolean} true if HW render is supported
79
+ */
80
+ setup(mod, dataPtr) {
81
+ this._mod = mod;
82
+
83
+ // Read struct fields set by the core
84
+ const contextType = mod.getValue(dataPtr, 'i32');
85
+ const depth = mod.HEAPU8[dataPtr + 16];
86
+ const stencil = mod.HEAPU8[dataPtr + 17];
87
+ this.bottomLeftOrigin = !!mod.HEAPU8[dataPtr + 18];
88
+ const versionMajor = mod.getValue(dataPtr + 20, 'i32');
89
+ const versionMinor = mod.getValue(dataPtr + 24, 'i32');
90
+ this.contextDestroyPtr = mod.getValue(dataPtr + 32, 'i32');
91
+
92
+ // Validate: we only support GL ES contexts
93
+ const supported = [
94
+ RETRO_HW_CONTEXT_OPENGLES2,
95
+ RETRO_HW_CONTEXT_OPENGLES3,
96
+ RETRO_HW_CONTEXT_OPENGLES_VERSION,
97
+ RETRO_HW_CONTEXT_OPENGL,
98
+ RETRO_HW_CONTEXT_OPENGL_CORE,
99
+ ];
100
+ if (!supported.includes(contextType)) {
101
+ console.error(`[libretro-gl] Unsupported HW context type: ${contextType}`);
102
+ return false;
103
+ }
104
+
105
+ this.contextType = contextType;
106
+ console.error(`[libretro-gl] HW render requested: type=${contextType} version=${versionMajor}.${versionMinor} depth=${depth} stencil=${stencil} bottomLeft=${this.bottomLeftOrigin}`);
107
+
108
+ // Create callback wrappers the WASM core can call
109
+
110
+ // get_current_framebuffer() → returns FBO 0 (default framebuffer)
111
+ // The core renders through Emscripten's GL which manages its own FBOs.
112
+ // We read back from FBO 0 after retro_run.
113
+ const getFBCb = mod.addFunction(() => {
114
+ return 0;
115
+ }, 'i');
116
+
117
+ // get_proc_address(const char* sym) → returns function pointer
118
+ const getProcCb = mod.addFunction((symPtr) => {
119
+ return this._getProcAddress(symPtr);
120
+ }, 'ii');
121
+
122
+ // Write our callback pointers into the struct
123
+ // context_reset at +4 — we'll call the core's context_reset after context creation
124
+ // For now, store the core's context_reset so we can call it later
125
+ this.contextResetPtr = mod.getValue(dataPtr + 4, 'i32');
126
+ // We don't write context_reset back — the core already set it.
127
+ // We only need to write get_current_framebuffer and get_proc_address.
128
+ mod.setValue(dataPtr + 8, getFBCb, 'i32');
129
+ mod.setValue(dataPtr + 12, getProcCb, 'i32');
130
+
131
+ this.active = true;
132
+ return true;
133
+ }
134
+
135
+ /**
136
+ * Create FBO for the core to render into. Called after retro_load_game succeeds
137
+ * and AV info is available.
138
+ *
139
+ * @param {number} width
140
+ * @param {number} height
141
+ * @param {boolean} contextExists - if true, EGL context was already created externally
142
+ */
143
+ createContext(width, height, contextExists = false) {
144
+ this.fboW = width;
145
+ this.fboH = height;
146
+
147
+ if (contextExists) {
148
+ // Context already created by LibretroHost before core loading.
149
+ // Resize if needed and ensure it's current.
150
+ gl.resizeContext(width, height);
151
+ gl.makeCurrent();
152
+ } else {
153
+ // Create pbuffer EGL context
154
+ if (!gl.createContext(width, height)) {
155
+ console.error('[libretro-gl] Failed to create EGL context');
156
+ return false;
157
+ }
158
+ gl.makeCurrent();
159
+ }
160
+
161
+ // Core renders to default FBO (0). We read back from there after retro_run.
162
+ // The Emscripten GL layer manages its own FBO tracking — we don't create one.
163
+ this.fbo = 0;
164
+ gl.glViewport(0, 0, width, height);
165
+ console.error(`[libretro-gl] Using default FBO: ${width}x${height}`);
166
+
167
+ // Call the core's context_reset callback
168
+ if (this.contextResetPtr) {
169
+ this._mod.dynCall('v', this.contextResetPtr, []);
170
+ }
171
+
172
+ return true;
173
+ }
174
+
175
+ /**
176
+ * Read back the rendered frame as RGBA pixels.
177
+ * Returns { pixels, width, height } or null if not active.
178
+ */
179
+ readbackFrame() {
180
+ if (!this.active) return null;
181
+
182
+ gl.glFinish();
183
+ const w = this.fboW, h = this.fboH;
184
+ const GL_RGBA = 0x1908, GL_UNSIGNED_BYTE = 0x1401;
185
+
186
+ if (!this._glPixels || this._glPixels.length !== w * h * 4) {
187
+ this._glPixels = new Uint8Array(w * h * 4);
188
+ }
189
+
190
+ gl.glBindFramebuffer(0x8D40, 0); // GL_FRAMEBUFFER — read from default FBO
191
+ gl.glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, this._glPixels);
192
+
193
+
194
+ // Flip vertically if bottom-left origin (GL convention)
195
+ let pixels;
196
+ if (this.bottomLeftOrigin) {
197
+ if (!this._flipped || this._flipped.length !== w * h * 4) {
198
+ this._flipped = new Uint8Array(w * h * 4);
199
+ }
200
+ const rowBytes = w * 4;
201
+ for (let y = 0; y < h; y++) {
202
+ const srcOff = (h - 1 - y) * rowBytes;
203
+ const dstOff = y * rowBytes;
204
+ this._flipped.set(this._glPixels.subarray(srcOff, srcOff + rowBytes), dstOff);
205
+ }
206
+ pixels = this._flipped;
207
+ } else {
208
+ pixels = this._glPixels;
209
+ }
210
+
211
+ return { pixels, width: w, height: h };
212
+ }
213
+
214
+ /**
215
+ * Resolve a GL function name to a WASM-callable function pointer.
216
+ * The core calls get_proc_address("glClear") and gets back a function
217
+ * table index it can call via indirect call.
218
+ */
219
+ _getProcAddress(symPtr) {
220
+ const mod = this._mod;
221
+ const name = mod.UTF8ToString(symPtr);
222
+
223
+ // Check cache
224
+ if (this._procAddressCache.has(name)) {
225
+ return this._procAddressCache.get(name);
226
+ }
227
+
228
+ // Look up the function on native-gles
229
+ const fn = gl[name];
230
+ if (!fn) {
231
+ // Not found — some cores probe for extension functions
232
+ this._procAddressCache.set(name, 0);
233
+ return 0;
234
+ }
235
+
236
+ // We need to create an Emscripten addFunction wrapper that bridges
237
+ // the WASM call to the native GL call. The challenge is that each
238
+ // GL function has a different signature.
239
+ //
240
+ // For libretro cores compiled with Emscripten, they typically call
241
+ // GL functions directly (linked at compile time) rather than through
242
+ // get_proc_address. get_proc_address is mainly used for extension
243
+ // functions. We return 0 for now if they're probing — the core's
244
+ // Emscripten build should have the GL functions linked directly.
245
+ //
246
+ // If a core truly needs runtime-resolved GL, we'd need a signature
247
+ // table mapping GL function names to their parameter types.
248
+
249
+ // For common GL functions, return a stub that indicates "available"
250
+ // The actual calls go through the Emscripten-linked GL functions.
251
+ // Returning non-zero tells the core the function exists.
252
+ // We create a minimal no-op wrapper since the core will call the
253
+ // directly-linked version, not this pointer.
254
+ const stub = mod.addFunction(() => { return 0; }, 'i');
255
+ this._procAddressCache.set(name, stub);
256
+ return stub;
257
+ }
258
+
259
+ destroy() {
260
+ if (this.contextDestroyPtr && this._mod) {
261
+ try {
262
+ this._mod.dynCall('v', this.contextDestroyPtr, []);
263
+ } catch { /* ignore */ }
264
+ }
265
+ if (this.active) {
266
+ gl.destroyContext();
267
+ this.active = false;
268
+ }
269
+ }
270
+ }