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.
- package/CHANGELOG.md +322 -0
- package/examples/n64/platformer/main.c +158 -0
- package/examples/n64/puzzle/main.c +117 -0
- package/examples/n64/racing/main.c +147 -0
- package/examples/n64/shmup/main.c +122 -0
- package/examples/n64/sports/main.c +127 -0
- package/examples/ps1/platformer/main.c +158 -0
- package/examples/ps1/puzzle/main.c +125 -0
- package/examples/ps1/racing/main.c +147 -0
- package/examples/ps1/shmup/main.c +192 -0
- package/examples/ps1/sports/main.c +127 -0
- package/examples/ps1/sprite_move/main.c +38 -0
- package/package.json +9 -2
- package/src/analysis/analyze.js +169 -29
- package/src/analysis/decompile.js +4 -0
- package/src/analysis/decompiler/sleigh/mips.ldefs +25 -0
- package/src/analysis/decompiler/sleigh/mips32.pspec +78 -0
- package/src/analysis/decompiler/sleigh/mips32be.cspec +107 -0
- package/src/analysis/decompiler/sleigh/mips32be.sla +211162 -0
- package/src/analysis/decompiler/sleigh/mips32le.cspec +106 -0
- package/src/analysis/decompiler/sleigh/mips32le.sla +210624 -0
- package/src/analysis/rizin.js +22 -1
- package/src/cores/capabilities.js +90 -2
- package/src/cores/registry.js +12 -0
- package/src/host/LibretroGL.js +270 -0
- package/src/host/LibretroGLBridge.js +836 -0
- package/src/host/LibretroHost.js +144 -3
- package/src/host/callbacks.js +18 -2
- package/src/host/coreLoader.js +49 -2
- package/src/host/cpu-state.js +27 -0
- package/src/host/framebuffer.js +14 -1
- package/src/host/glOptionalDep.js +60 -0
- package/src/host/n64-ai-state.js +43 -0
- package/src/host/ps1-spu-state.js +65 -0
- package/src/host/retroConstants.js +14 -0
- package/src/mcp/tools/cheats.js +73 -4
- package/src/mcp/tools/disasm.js +2 -0
- package/src/mcp/tools/frame.js +18 -9
- package/src/mcp/tools/index.js +12 -2
- package/src/mcp/tools/lifecycle.js +1 -1
- package/src/mcp/tools/platform-tools.js +20 -4
- package/src/mcp/tools/platforms.js +38 -4
- package/src/mcp/tools/project.js +100 -0
- package/src/mcp/tools/rendering-context.js +2 -1
- package/src/mcp/tools/toolchain.js +1 -1
- package/src/mcp/tools/watch-memory.js +93 -20
- package/src/platforms/n64/lib/c/n64.c +196 -0
- package/src/platforms/n64/lib/c/n64.h +68 -0
- package/src/platforms/ps1/lib/c/psx.c +200 -0
- package/src/platforms/ps1/lib/c/psx.h +83 -0
- package/src/toolchains/cc65/cc65.js +11 -0
- package/src/toolchains/index.js +35 -0
- package/src/toolchains/mips-c/lib/be/libc.a +0 -0
- package/src/toolchains/mips-c/lib/be/libgcc.a +0 -0
- package/src/toolchains/mips-c/lib/be/libm.a +0 -0
- package/src/toolchains/mips-c/lib/el/libc.a +0 -0
- package/src/toolchains/mips-c/lib/el/libm.a +0 -0
- package/src/toolchains/mips-c/lib/n64-crt0.s +21 -0
- package/src/toolchains/mips-c/lib/n64-ipl3.s +30 -0
- package/src/toolchains/mips-c/lib/n64.ld +15 -0
- package/src/toolchains/mips-c/lib/ps1-crt0.s +20 -0
- package/src/toolchains/mips-c/lib/ps1.ld +15 -0
- package/src/toolchains/mips-c/lib/softint.c +37 -0
- package/src/toolchains/mips-c/mips-c.js +155 -0
- package/src/toolchains/mips-elf-gcc/gcc.js +130 -0
package/src/analysis/rizin.js
CHANGED
|
@@ -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
|
-
/**
|
|
208
|
-
|
|
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
|
+
}
|
package/src/cores/registry.js
CHANGED
|
@@ -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
|
+
}
|