romdevtools 0.44.0 → 0.70.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +572 -0
- package/README.md +9 -7
- package/examples/dreamcast/hello/main.c +24 -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 +11 -2
- package/src/analysis/analyze.js +224 -29
- package/src/analysis/decompile.js +7 -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 +24 -1
- package/src/cores/capabilities.js +122 -2
- package/src/cores/registry.js +17 -0
- package/src/host/LibretroGL.js +273 -0
- package/src/host/LibretroGLBridge.js +836 -0
- package/src/host/LibretroHost.js +203 -3
- package/src/host/callbacks.js +22 -2
- package/src/host/coreLoader.js +65 -5
- 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/disasm.js +2 -0
- package/src/mcp/tools/frame.js +18 -9
- 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 +116 -0
- package/src/mcp/tools/rendering-context.js +2 -1
- package/src/mcp/tools/toolchain.js +1 -1
- 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/index.js +65 -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/toolchains/sh-c/lib/dc-crt0.s +23 -0
- package/src/toolchains/sh-c/lib/dc.h +102 -0
- package/src/toolchains/sh-c/lib/dc.ld +11 -0
- package/src/toolchains/sh-c/lib/libc.a +0 -0
- package/src/toolchains/sh-c/lib/libgcc.a +0 -0
- package/src/toolchains/sh-c/lib/libm.a +0 -0
- package/src/toolchains/sh-c/sh-c.js +101 -0
- package/src/toolchains/sh-elf-gcc/gcc.js +122 -0
package/src/analysis/rizin.js
CHANGED
|
@@ -54,6 +54,21 @@ 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
|
+
dreamcast: "sh", // SH-4 (SuperH) — rizin's `sh` plugin covers it
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/** Byte order per platform, for shared-arch families that ship both (MIPS). Only
|
|
66
|
+
* platforms NOT matching their arch default need an entry; absent → use the
|
|
67
|
+
* loader/arch default. PS1 is little, N64 is big. Dreamcast SH-4 is little. */
|
|
68
|
+
export const RIZIN_ENDIAN = {
|
|
69
|
+
ps1: "little",
|
|
70
|
+
n64: "big",
|
|
71
|
+
dreamcast: "little",
|
|
57
72
|
};
|
|
58
73
|
|
|
59
74
|
/**
|
|
@@ -77,13 +92,21 @@ export const RIZIN_ARCH = {
|
|
|
77
92
|
* @returns {Promise<{exitCode:number, output:string, log:string, crash?:object}>}
|
|
78
93
|
*/
|
|
79
94
|
export async function runRizin(opts) {
|
|
80
|
-
const { romPath, romBytes, commands, arch, bits, baddr, writeable, timeoutMs } = opts;
|
|
95
|
+
const { romPath, romBytes, commands, arch, bits, baddr, writeable, timeoutMs, endian } = opts;
|
|
81
96
|
if (!commands) throw new Error("runRizin: commands required");
|
|
82
97
|
const bytes = romBytes ?? new Uint8Array(await readFile(romPath));
|
|
83
98
|
|
|
84
99
|
const pre = ["e scr.color=0", "e scr.interactive=false", "e scr.prompt=false"];
|
|
85
100
|
if (arch) pre.push(`e asm.arch=${arch}`);
|
|
86
101
|
if (bits) pre.push(`e asm.bits=${bits}`);
|
|
102
|
+
// Endianness matters for shared-arch families that ship both byte orders — most
|
|
103
|
+
// pressingly MIPS: PS1 (R3000) is little-endian, N64 (R4300) is big-endian, same
|
|
104
|
+
// `mips` plugin. Set both asm + cfg so the disasm AND the analysis loader agree.
|
|
105
|
+
if (endian === "big" || endian === "little") {
|
|
106
|
+
// `cfg.bigendian` is the rizin config var (there is NO `asm.bigendian` —
|
|
107
|
+
// setting it errors). cfg.bigendian drives BOTH the disasm and the analysis.
|
|
108
|
+
pre.push(`e cfg.bigendian=${endian === "big" ? "true" : "false"}`);
|
|
109
|
+
}
|
|
87
110
|
|
|
88
111
|
// Split trailing command off so its output (only) goes to the file. Earlier
|
|
89
112
|
// commands (aaa, config) print nothing we need. Rizin's `cmd > file` writes
|
|
@@ -202,10 +202,103 @@ 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: "3d", introspection: "shallow",
|
|
223
|
+
ops: {
|
|
224
|
+
// beetle_psx_hw: the GPU renders on the REAL GPU via the GLES3/WebGL2 hardware
|
|
225
|
+
// renderer through native-gles (like glide64-N64 + Flycast-DC), with OpenBIOS
|
|
226
|
+
// EMBEDDED (PCSX-Redux, MIT, region-free) — no Sony firmware to ship, no BIOS file.
|
|
227
|
+
// run/screenshot + cheats live; disasm + decompile work (MIPS Capstone + the
|
|
228
|
+
// MIPS:LE:32 SLEIGH spec). cpuState/audioDebug are OFF: those need beetle-side
|
|
229
|
+
// romdev_mips_regs_get/romdev_spu_get exports (a future core patch — the old
|
|
230
|
+
// pcsx_rearmed-software build had them, but it has no WebGL2 GPU path). build needs
|
|
231
|
+
// a PS1 toolchain (PSn00bSDK, not yet).
|
|
232
|
+
build: true, run: true, screenshot: true,
|
|
233
|
+
inspectSprites: false, inspectPalette: false, inspectBackground: false,
|
|
234
|
+
renderingContext: false, cpuState: false, audioDebug: false,
|
|
235
|
+
cart: false, disasm: true, decompile: true,
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
n64: {
|
|
239
|
+
cpuFamily: "mips", decompileQuality: "good", tier: "mips",
|
|
240
|
+
cpus: { main: "r4300", secondary: ["rsp"] },
|
|
241
|
+
audioChips: ["ai"], // AI = the audio OUTPUT interface (RSP-mixed; no per-voice chip)
|
|
242
|
+
memoryRegions: [...GENERIC_REGIONS],
|
|
243
|
+
renderingKind: "3d", introspection: "shallow",
|
|
244
|
+
ops: {
|
|
245
|
+
// parallel_n64: HW (GL) render via the optional native-gles bridge.
|
|
246
|
+
// run/screenshot + cpuState (R4300 regsnap) + cheats + breakpoint/watch +
|
|
247
|
+
// audioDebug(AI) live; disasm + decompile work; build via mips-elf-gcc.
|
|
248
|
+
// The 3D renderer has no tile/sprite inspectors (N/A by hardware).
|
|
249
|
+
build: true, run: true, screenshot: true,
|
|
250
|
+
inspectSprites: false, inspectPalette: false, inspectBackground: false,
|
|
251
|
+
renderingContext: false, cpuState: true, audioDebug: true,
|
|
252
|
+
cart: false, disasm: true, decompile: true,
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
dreamcast: {
|
|
257
|
+
cpuFamily: "sh", decompileQuality: "good", tier: "sh",
|
|
258
|
+
cpus: { main: "sh4", secondary: ["arm7"] }, // SH-4 main + ARM7 AICA sound CPU
|
|
259
|
+
audioChips: ["aica"],
|
|
260
|
+
memoryRegions: [...GENERIC_REGIONS],
|
|
261
|
+
renderingKind: "3d", introspection: "shallow",
|
|
262
|
+
ops: {
|
|
263
|
+
// Flycast WASM boots + RUNS homebrew .elf (reios HLE): the SH-4 executes guest
|
|
264
|
+
// code (run + memory introspection), and the PowerVR2 present-path works — flycast
|
|
265
|
+
// renders to the GL FBO and the host reads it back (verified: a framebuffer-writing
|
|
266
|
+
// program shows ~727k captured pixels). `build` lands with the sh-elf-gcc WASM
|
|
267
|
+
// toolchain. The 3D renderer has no tile/sprite inspectors (N/A by hw).
|
|
268
|
+
// disasm/decompile = SH-4 analysis slice (rizin `sh` + Ghidra SuperH4 SLEIGH).
|
|
269
|
+
build: true, run: true, screenshot: true,
|
|
270
|
+
inspectSprites: false, inspectPalette: false, inspectBackground: false,
|
|
271
|
+
renderingContext: false, cpuState: false, audioDebug: false,
|
|
272
|
+
cart: false, disasm: true, decompile: true,
|
|
273
|
+
},
|
|
274
|
+
},
|
|
205
275
|
};
|
|
206
276
|
|
|
207
|
-
/**
|
|
208
|
-
|
|
277
|
+
/** The 32-bit MIPS tier (PS1/N64) — marked `tier:"mips"`. A PARTIAL tier: they
|
|
278
|
+
* run + screenshot + disasm, but don't yet have the full op surface of the canonical
|
|
279
|
+
* 14 (no build toolchain, no MIPS decompile/cpuState, framebuffer/3D renderers have
|
|
280
|
+
* no tile/sprite inspectors). They're held to their OWN conformance, not the
|
|
281
|
+
* "all 14" cross-checks. They graduate to CONTRACT_PLATFORMS as the gaps close. */
|
|
282
|
+
export const MIPS_TIER_PLATFORMS = Object.entries(CAPABILITIES)
|
|
283
|
+
.filter(([, c]) => c.tier === "mips")
|
|
284
|
+
.map(([p]) => p);
|
|
285
|
+
|
|
286
|
+
/** The next-gen tiers (32-bit CPU families added after the canonical 14): MIPS
|
|
287
|
+
* (PS1/N64) + SH (Dreamcast) + future. Any platform carrying a CPU-family `tier`
|
|
288
|
+
* is held to its OWN conformance, not the "all 14" cross-checks; a new platform
|
|
289
|
+
* starts here (analysis-first) and graduates as run/build/etc. land. */
|
|
290
|
+
export const NEXTGEN_TIER_PLATFORMS = Object.entries(CAPABILITIES)
|
|
291
|
+
.filter(([, c]) => c.tier === "mips" || c.tier === "sh")
|
|
292
|
+
.map(([p]) => p);
|
|
293
|
+
|
|
294
|
+
/** Back-compat: the analysis-only set is now empty (PS1/N64 gained run/screenshot
|
|
295
|
+
* in the run-side wiring). Kept so the name still resolves for older imports. */
|
|
296
|
+
export const ANALYSIS_ONLY_PLATFORMS = [];
|
|
297
|
+
|
|
298
|
+
/** The canonical tier-1 platforms (the 14): full op surface, universal build/run/
|
|
299
|
+
* screenshot. Excludes the partial next-gen tiers above. */
|
|
300
|
+
export const CONTRACT_PLATFORMS = Object.keys(CAPABILITIES)
|
|
301
|
+
.filter((p) => !NEXTGEN_TIER_PLATFORMS.includes(p));
|
|
209
302
|
|
|
210
303
|
/** Does `platform` support `op`? Unknown platform/op → false. */
|
|
211
304
|
export function supports(platform, op) {
|
|
@@ -216,3 +309,30 @@ export function supports(platform, op) {
|
|
|
216
309
|
export function capabilitiesFor(platform) {
|
|
217
310
|
return CAPABILITIES[platform] ?? null;
|
|
218
311
|
}
|
|
312
|
+
|
|
313
|
+
// Why an op is unsupported, grounded in the HARDWARE — so `unsupported()`'s reason
|
|
314
|
+
// distinguishes "N/A by hardware, permanent" from "a decoder we haven't wired."
|
|
315
|
+
// Keyed on renderingKind: a framebuffer (PS1) / 3D (N64) renderer has no tile,
|
|
316
|
+
// sprite-attribute, nametable, or palette tables for the tile-era inspectors to
|
|
317
|
+
// read — those ops are MEANINGLESS on the hardware, not merely absent.
|
|
318
|
+
const RENDERING_NA = {
|
|
319
|
+
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'}).",
|
|
320
|
+
"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.",
|
|
321
|
+
};
|
|
322
|
+
const NA_OPS = new Set(["inspectSprites", "inspectPalette", "inspectBackground", "renderingContext"]);
|
|
323
|
+
|
|
324
|
+
/** A hardware-grounded reason an op is unsupported on a platform, or null if the
|
|
325
|
+
* op isn't one of the hardware-N/A introspection ops. Drives unsupported().reason
|
|
326
|
+
* so agents see "N/A by hardware" (don't retry / don't request a decoder) rather
|
|
327
|
+
* than a generic "no decoder for this platform". */
|
|
328
|
+
export function naReason(platform, op) {
|
|
329
|
+
const cap = CAPABILITIES[platform];
|
|
330
|
+
if (!cap || cap.ops?.[op]) return null; // supported → no N/A reason
|
|
331
|
+
if (op === "cart") {
|
|
332
|
+
return cap.renderingKind === "framebuffer"
|
|
333
|
+
? "this platform is disc-based (no cartridge ROM to inspect/patch as a cart)."
|
|
334
|
+
: null;
|
|
335
|
+
}
|
|
336
|
+
if (NA_OPS.has(op)) return RENDERING_NA[cap.renderingKind] ?? null;
|
|
337
|
+
return null;
|
|
338
|
+
}
|
package/src/cores/registry.js
CHANGED
|
@@ -53,6 +53,23 @@ 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 renders the RDP on the REAL GPU through glide64 (GL HLE) → native-gles
|
|
61
|
+
// (the host's WebGL2 bridge), same path as Flycast. The host forces the glide64 plugin
|
|
62
|
+
// via a core option. NOT software RDP (angrylion) — that was the old headless build.
|
|
63
|
+
n64: { platform: "n64", coreName: "parallel_n64", pkg: "romdev-core-parallel-n64", displayName: "Nintendo 64 (ParaLLEl N64, glide64 GL)", hwRender: true },
|
|
64
|
+
// beetle_psx_hw = mednafen PSX with the GLES3/WebGL2 HARDWARE renderer → rendered on
|
|
65
|
+
// the real GPU through native-gles (like glide64-N64 + Flycast-DC). Ships with OpenBIOS
|
|
66
|
+
// EMBEDDED (PCSX-Redux, MIT-licensed, region-free) so there's no copyrighted Sony
|
|
67
|
+
// firmware to ship and no BIOS file to supply — the GPU PS1 path with an open BIOS.
|
|
68
|
+
ps1: { platform: "ps1", coreName: "beetle_psx_hw", pkg: "romdev-core-beetle-psx-hw", displayName: "Sony PlayStation (Beetle PSX HW, OpenBIOS)", aka: "psx,playstation", hwRender: true },
|
|
69
|
+
// Flycast = full Dreamcast emulator, GLES3/WebGL2 HW-render (PowerVR2 is GPU-first,
|
|
70
|
+
// no software framebuffer path) → driven through the native-gles/webgl-node bridge
|
|
71
|
+
// like the GL N64 build. HLE BIOS (reios) on by default — no firmware to ship.
|
|
72
|
+
dreamcast: { platform: "dreamcast", coreName: "flycast", pkg: "romdev-core-flycast", displayName: "Sega Dreamcast (Flycast)", aka: "dc", hwRender: true },
|
|
56
73
|
};
|
|
57
74
|
|
|
58
75
|
/** Try to get {jsPath,wasmPath} for a core from its binary package. */
|
|
@@ -0,0 +1,273 @@
|
|
|
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
|
+
* @param {number} [cropW] active width the core reported (e.g. DC 640); when smaller
|
|
178
|
+
* than the FBO, read back only that bottom-left region instead of the whole viewport
|
|
179
|
+
* (the GL bottom-left origin puts the active framebuffer in the lower-left corner).
|
|
180
|
+
* @param {number} [cropH] active height the core reported (e.g. DC 480).
|
|
181
|
+
* Returns { pixels, width, height } or null if not active.
|
|
182
|
+
*/
|
|
183
|
+
readbackFrame(cropW, cropH) {
|
|
184
|
+
if (!this.active) return null;
|
|
185
|
+
|
|
186
|
+
gl.glFinish();
|
|
187
|
+
// Crop to the core's active resolution when it's a sub-region of the FBO; this
|
|
188
|
+
// strips the dead border a fixed-size GL FBO leaves around a smaller native frame.
|
|
189
|
+
const w = (cropW > 0 && cropW <= this.fboW) ? cropW : this.fboW;
|
|
190
|
+
const h = (cropH > 0 && cropH <= this.fboH) ? cropH : this.fboH;
|
|
191
|
+
const GL_RGBA = 0x1908, GL_UNSIGNED_BYTE = 0x1401;
|
|
192
|
+
|
|
193
|
+
if (!this._glPixels || this._glPixels.length !== w * h * 4) {
|
|
194
|
+
this._glPixels = new Uint8Array(w * h * 4);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
gl.glBindFramebuffer(0x8D40, 0); // GL_FRAMEBUFFER — read from default FBO
|
|
198
|
+
// x=0,y=0 is the bottom-left of the FBO, which is where the native frame is drawn.
|
|
199
|
+
gl.glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, this._glPixels);
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
// Flip vertically if bottom-left origin (GL convention)
|
|
203
|
+
let pixels;
|
|
204
|
+
if (this.bottomLeftOrigin) {
|
|
205
|
+
if (!this._flipped || this._flipped.length !== w * h * 4) {
|
|
206
|
+
this._flipped = new Uint8Array(w * h * 4);
|
|
207
|
+
}
|
|
208
|
+
const rowBytes = w * 4;
|
|
209
|
+
for (let y = 0; y < h; y++) {
|
|
210
|
+
const srcOff = (h - 1 - y) * rowBytes;
|
|
211
|
+
const dstOff = y * rowBytes;
|
|
212
|
+
this._flipped.set(this._glPixels.subarray(srcOff, srcOff + rowBytes), dstOff);
|
|
213
|
+
}
|
|
214
|
+
pixels = this._flipped;
|
|
215
|
+
} else {
|
|
216
|
+
pixels = this._glPixels;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return { pixels, width: w, height: h };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Resolve a GL function name to a WASM-callable function pointer.
|
|
224
|
+
* The core calls get_proc_address("glClear") and gets back a function
|
|
225
|
+
* table index it can call via indirect call.
|
|
226
|
+
*/
|
|
227
|
+
_getProcAddress(symPtr) {
|
|
228
|
+
const mod = this._mod;
|
|
229
|
+
const name = mod.UTF8ToString(symPtr);
|
|
230
|
+
|
|
231
|
+
// Check cache
|
|
232
|
+
if (this._procAddressCache.has(name)) {
|
|
233
|
+
return this._procAddressCache.get(name);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// PREFERRED: cores built with emscripten's own WebGL layer (GL_ENABLE_GET_PROC_
|
|
237
|
+
// ADDRESS=1, e.g. Flycast) resolve GL through emscripten_GetProcAddress, which
|
|
238
|
+
// returns a REAL function-table pointer into the WebGL JS implementation —
|
|
239
|
+
// correct signature, no stub. Use it when the core exports it. (The old
|
|
240
|
+
// native-gles `gl[name]` + addFunction(...,'i') stub path traps the moment the
|
|
241
|
+
// core calls a multi-arg GL fn through a 0-arg pointer — that's the
|
|
242
|
+
// context_reset "null function or function signature mismatch".)
|
|
243
|
+
if (typeof mod._emscripten_GetProcAddress === "function") {
|
|
244
|
+
const ptr = mod._emscripten_GetProcAddress(symPtr) >>> 0;
|
|
245
|
+
this._procAddressCache.set(name, ptr);
|
|
246
|
+
return ptr;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Fallback (native-gles bridge cores, e.g. the glide64 N64 build): the GL fns
|
|
250
|
+
// are linked directly into the core, so a non-zero "available" marker suffices —
|
|
251
|
+
// the core calls its own linked function, not this pointer.
|
|
252
|
+
const fn = gl[name];
|
|
253
|
+
if (!fn) {
|
|
254
|
+
this._procAddressCache.set(name, 0);
|
|
255
|
+
return 0;
|
|
256
|
+
}
|
|
257
|
+
const stub = mod.addFunction(() => { return 0; }, 'i');
|
|
258
|
+
this._procAddressCache.set(name, stub);
|
|
259
|
+
return stub;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
destroy() {
|
|
263
|
+
if (this.contextDestroyPtr && this._mod) {
|
|
264
|
+
try {
|
|
265
|
+
this._mod.dynCall('v', this.contextDestroyPtr, []);
|
|
266
|
+
} catch { /* ignore */ }
|
|
267
|
+
}
|
|
268
|
+
if (this.active) {
|
|
269
|
+
gl.destroyContext();
|
|
270
|
+
this.active = false;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|