romdevtools 0.56.1 → 0.71.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 +338 -0
- package/README.md +9 -7
- package/examples/dreamcast/hello/main.c +24 -0
- package/examples/dreamcast/platformer/main.c +31 -0
- package/examples/dreamcast/puzzle/main.c +44 -0
- package/examples/dreamcast/racing/main.c +39 -0
- package/examples/dreamcast/shmup/main.c +50 -0
- package/examples/dreamcast/sports/main.c +39 -0
- package/package.json +5 -3
- package/src/analysis/analyze.js +60 -5
- package/src/analysis/decompile.js +3 -0
- package/src/analysis/rizin.js +3 -1
- package/src/cores/capabilities.js +43 -7
- package/src/cores/registry.js +13 -8
- package/src/host/LibretroGL.js +26 -23
- package/src/host/LibretroHost.js +302 -24
- package/src/host/callbacks.js +72 -1
- package/src/host/coreLoader.js +17 -4
- package/src/host/cpu-state.js +32 -0
- package/src/host/dc-aica-state.js +67 -0
- package/src/mcp/tools/audio.js +1 -1
- package/src/mcp/tools/disasm.js +1 -1
- package/src/mcp/tools/index.js +1 -1
- package/src/mcp/tools/platform-docs.js +1 -1
- package/src/mcp/tools/platform-tools.js +9 -1
- package/src/mcp/tools/project.js +16 -0
- package/src/mcp/tools/toolchain.js +115 -10
- package/src/platforms/dreamcast/MENTAL_MODEL.md +87 -0
- package/src/platforms/dreamcast/TROUBLESHOOTING.md +55 -0
- package/src/platforms/dreamcast/UPSTREAM_SOURCES.md +57 -0
- package/src/platforms/n64/MENTAL_MODEL.md +84 -0
- package/src/platforms/n64/TROUBLESHOOTING.md +60 -0
- package/src/platforms/n64/UPSTREAM_SOURCES.md +52 -0
- package/src/platforms/n64/lib/c/n64.c +181 -80
- package/src/platforms/ps1/MENTAL_MODEL.md +85 -0
- package/src/platforms/ps1/TROUBLESHOOTING.md +55 -0
- package/src/platforms/ps1/UPSTREAM_SOURCES.md +54 -0
- package/src/platforms/snes/TROUBLESHOOTING.md +10 -0
- package/src/toolchains/asar/asar.js +84 -14
- package/src/toolchains/index.js +30 -0
- package/src/toolchains/mips-c/mips-c.js +35 -1
- package/src/toolchains/sh-c/lib/dc-crt0.s +23 -0
- package/src/toolchains/sh-c/lib/dc.h +152 -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 +104 -0
- package/src/toolchains/sh-elf-gcc/gcc.js +122 -0
package/src/analysis/analyze.js
CHANGED
|
@@ -135,13 +135,14 @@ export function sniffPlatform(p) {
|
|
|
135
135
|
if (/\.pce$/i.test(p)) return "pce";
|
|
136
136
|
if (/\.(z64|n64|v64)$/i.test(p)) return "n64";
|
|
137
137
|
if (/\.(psexe|psx)$/i.test(p)) return "ps1"; // .exe/.bin ambiguous — pass platform explicitly
|
|
138
|
+
if (/\.(cdi|gdi)$/i.test(p)) return "dreamcast"; // DC disc images; .elf is cross-platform — pass platform explicitly
|
|
138
139
|
if (/\.(gen|md|bin)$/i.test(p)) return "genesis";
|
|
139
140
|
return null;
|
|
140
141
|
}
|
|
141
142
|
|
|
142
143
|
/** rizin asm.bits per arch (analysis defaults; rizin's loader usually sets
|
|
143
144
|
* these for recognized formats, but raw blobs need a hint). */
|
|
144
|
-
const BITS = { arm: 32, m68k: 32, snes: 16, mips: 32 };
|
|
145
|
+
const BITS = { arm: 32, m68k: 32, snes: 16, mips: 32, sh: 32 };
|
|
145
146
|
|
|
146
147
|
/**
|
|
147
148
|
* The rizin analysis-SEED command for a context. For the 8/16-bit platforms
|
|
@@ -154,7 +155,10 @@ const BITS = { arm: 32, m68k: 32, snes: 16, mips: 32 };
|
|
|
154
155
|
* @returns {string} the seed command (no trailing semicolon)
|
|
155
156
|
*/
|
|
156
157
|
function analysisSeed({ arch, codeStart }) {
|
|
157
|
-
|
|
158
|
+
// MIPS (N64/PS1) and SH-4 (Dreamcast) are raw code images with no rizin-recognized
|
|
159
|
+
// entry, so `aaa` finds nothing — seed a function at the code start + recursively
|
|
160
|
+
// analyze its call graph (`af` + `aac`). The 8/16-bit bin formats self-seed via aaa.
|
|
161
|
+
if (arch === "mips" || arch === "sh") {
|
|
158
162
|
const at = "0x" + (codeStart || 0).toString(16);
|
|
159
163
|
return `af @ ${at}; aac @ ${at}`;
|
|
160
164
|
}
|
|
@@ -167,8 +171,12 @@ function analysisSeed({ arch, codeStart }) {
|
|
|
167
171
|
* `rebase` so callers get real VAs that round-trip through the decompile
|
|
168
172
|
* VA→fileOffset math. N64 (.z64) keeps its absolute VAs baked into the code and is
|
|
169
173
|
* analyzed flat from codeStart, so no rebase. */
|
|
170
|
-
function mipsAnalysisBase({
|
|
171
|
-
|
|
174
|
+
function mipsAnalysisBase({ platform, loadBase, codeStart }) {
|
|
175
|
+
// PS1 (left-padded to the VA's low 20 bits) and Dreamcast (same, loadBase
|
|
176
|
+
// 0x8c010000) need the VA's HIGH bits added back so addresses round-trip. N64 runs
|
|
177
|
+
// flat with VAs already baked in (no rebase).
|
|
178
|
+
const padded = (platform === "ps1" || platform === "dreamcast") && loadBase;
|
|
179
|
+
const rebase = padded ? (loadBase & 0xfff00000) >>> 0 : 0;
|
|
172
180
|
return { baddr: undefined, seedAt: codeStart, rebase };
|
|
173
181
|
}
|
|
174
182
|
|
|
@@ -276,6 +284,39 @@ async function loadContext(romPath, platformOverride) {
|
|
|
276
284
|
romBytes = text;
|
|
277
285
|
}
|
|
278
286
|
}
|
|
287
|
+
// Dreamcast SH-4: KOS produces an ELF (load base 0x8c010000) or a stripped flat
|
|
288
|
+
// binary. For an ELF, strip to the first PT_LOAD segment's bytes + take its vaddr
|
|
289
|
+
// as loadBase. Like PS1, SH-4 uses PC-relative + absolute addressing, so left-pad
|
|
290
|
+
// so flat offset == the VA's low bits and add the high bits back as rebase.
|
|
291
|
+
if (platform === "dreamcast") {
|
|
292
|
+
let text = romBytes, loadVa = 0x8c010000;
|
|
293
|
+
if (romBytes.length >= 0x34 && romBytes[0] === 0x7f && romBytes[1] === 0x45 &&
|
|
294
|
+
romBytes[2] === 0x4c && romBytes[3] === 0x46) { // "\x7fELF"
|
|
295
|
+
// ELF32 LE: e_phoff @0x1c, e_phentsize @0x2a, e_phnum @0x2c. Find first PT_LOAD.
|
|
296
|
+
const u32 = (o) => (romBytes[o] | (romBytes[o + 1] << 8) | (romBytes[o + 2] << 16) | (romBytes[o + 3] << 24)) >>> 0;
|
|
297
|
+
const u16 = (o) => romBytes[o] | (romBytes[o + 1] << 8);
|
|
298
|
+
const phoff = u32(0x1c), phentsize = u16(0x2a), phnum = u16(0x2c);
|
|
299
|
+
for (let i = 0; i < phnum; i++) {
|
|
300
|
+
const ph = phoff + i * phentsize;
|
|
301
|
+
if (u32(ph) === 1) { // PT_LOAD
|
|
302
|
+
const p_offset = u32(ph + 4), p_vaddr = u32(ph + 8), p_filesz = u32(ph + 16);
|
|
303
|
+
loadVa = p_vaddr >>> 0;
|
|
304
|
+
text = romBytes.subarray(p_offset, p_offset + p_filesz);
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
loadBase = loadVa >>> 0;
|
|
310
|
+
const lowPad = loadBase & 0x000fffff;
|
|
311
|
+
if (lowPad > 0 && lowPad <= 0x200000) {
|
|
312
|
+
const padded = new Uint8Array(lowPad + text.length);
|
|
313
|
+
padded.set(text, lowPad);
|
|
314
|
+
romBytes = padded;
|
|
315
|
+
codeStart = lowPad;
|
|
316
|
+
} else {
|
|
317
|
+
romBytes = text;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
279
320
|
|
|
280
321
|
// A6: container/format sniff. Some dumps are interleaved/headered such that a
|
|
281
322
|
// FLAT read scrambles every byte → fake "bad instruction" noise everywhere.
|
|
@@ -623,12 +664,26 @@ export async function analyzeDecompile(romPath, address, platformOverride) {
|
|
|
623
664
|
// offset and decompile via the MIPS SLEIGH spec (MIPS:BE:32 for N64, MIPS:LE:32
|
|
624
665
|
// for PS1). N64: normalize byte order, fileOff = vaddr - entryVaddr + 0x1000 (post
|
|
625
666
|
// IPL3). PS1: strip PS-EXE, fileOff = vaddr - loadAddr.
|
|
626
|
-
if (platform === "n64" || platform === "ps1") {
|
|
667
|
+
if (platform === "n64" || platform === "ps1" || platform === "dreamcast") {
|
|
627
668
|
let fileOff;
|
|
628
669
|
if (platform === "n64") {
|
|
629
670
|
romBytes = normalizeN64ByteOrder(romBytes).bytes;
|
|
630
671
|
const entry = ((romBytes[0x08] << 24) | (romBytes[0x09] << 16) | (romBytes[0x0a] << 8) | romBytes[0x0b]) >>> 0;
|
|
631
672
|
fileOff = ((address >>> 0) - entry + 0x1000) >>> 0;
|
|
673
|
+
} else if (platform === "dreamcast") {
|
|
674
|
+
// SH-4: strip the ELF to its first PT_LOAD segment (vaddr = loadBase), or treat
|
|
675
|
+
// a raw image as flat at 0x8c010000. fileOff = address - segment vaddr.
|
|
676
|
+
let loadVa = 0x8c010000;
|
|
677
|
+
if (romBytes.length >= 0x34 && romBytes[0] === 0x7f && romBytes[1] === 0x45 && romBytes[2] === 0x4c && romBytes[3] === 0x46) {
|
|
678
|
+
const u32 = (o) => (romBytes[o] | (romBytes[o + 1] << 8) | (romBytes[o + 2] << 16) | (romBytes[o + 3] << 24)) >>> 0;
|
|
679
|
+
const u16 = (o) => romBytes[o] | (romBytes[o + 1] << 8);
|
|
680
|
+
const phoff = u32(0x1c), phentsize = u16(0x2a), phnum = u16(0x2c);
|
|
681
|
+
for (let i = 0; i < phnum; i++) {
|
|
682
|
+
const ph = phoff + i * phentsize;
|
|
683
|
+
if (u32(ph) === 1) { loadVa = u32(ph + 8) >>> 0; romBytes = romBytes.subarray(u32(ph + 4), u32(ph + 4) + u32(ph + 16)); break; }
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
fileOff = ((address >>> 0) - loadVa) >>> 0;
|
|
632
687
|
} else {
|
|
633
688
|
let loadAddr = 0;
|
|
634
689
|
if (romBytes.length >= 0x800 && romBytes[0] === 0x50 && romBytes[1] === 0x53 && romBytes[2] === 0x2d && romBytes[3] === 0x58) {
|
|
@@ -51,6 +51,9 @@ export const SLEIGH_LANGID = {
|
|
|
51
51
|
// 32-bit MIPS III code). Ghidra ships both MIPS variants in its stock SLEIGH.
|
|
52
52
|
ps1: "MIPS:LE:32:default",
|
|
53
53
|
n64: "MIPS:BE:32:default",
|
|
54
|
+
// Dreamcast = SH-4 (SuperH), little-endian. Ghidra ships SuperH4 in its stock
|
|
55
|
+
// SLEIGH; we compile SuperH4_le.sla (see scripts/build-decompiler.sh).
|
|
56
|
+
dreamcast: "SuperH4:LE:32:default",
|
|
54
57
|
};
|
|
55
58
|
|
|
56
59
|
/**
|
package/src/analysis/rizin.js
CHANGED
|
@@ -59,14 +59,16 @@ export const RIZIN_ARCH = {
|
|
|
59
59
|
// below — same arch, different byte order.
|
|
60
60
|
ps1: "mips",
|
|
61
61
|
n64: "mips",
|
|
62
|
+
dreamcast: "sh", // SH-4 (SuperH) — rizin's `sh` plugin covers it
|
|
62
63
|
};
|
|
63
64
|
|
|
64
65
|
/** Byte order per platform, for shared-arch families that ship both (MIPS). Only
|
|
65
66
|
* platforms NOT matching their arch default need an entry; absent → use the
|
|
66
|
-
* loader/arch default. PS1 is little, N64 is big. */
|
|
67
|
+
* loader/arch default. PS1 is little, N64 is big. Dreamcast SH-4 is little. */
|
|
67
68
|
export const RIZIN_ENDIAN = {
|
|
68
69
|
ps1: "little",
|
|
69
70
|
n64: "big",
|
|
71
|
+
dreamcast: "little",
|
|
70
72
|
};
|
|
71
73
|
|
|
72
74
|
/**
|
|
@@ -219,12 +219,17 @@ export const CAPABILITIES = {
|
|
|
219
219
|
cpus: { main: "r3000", secondary: ["gte"] },
|
|
220
220
|
audioChips: ["spu"],
|
|
221
221
|
memoryRegions: [...GENERIC_REGIONS],
|
|
222
|
-
renderingKind: "
|
|
222
|
+
renderingKind: "3d", introspection: "shallow",
|
|
223
223
|
ops: {
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
//
|
|
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 + cpuState + audioDebug live; disasm + decompile work
|
|
228
|
+
// (MIPS Capstone + the MIPS:LE:32 SLEIGH spec). cpuState (R3000A GPR_full/BACKED_PC)
|
|
229
|
+
// + audioDebug (SPU $1F801C00 register block) come from beetle-side
|
|
230
|
+
// romdev_mips_regs_get/romdev_spu_get exports patched into cpu.c (see
|
|
231
|
+
// scripts/patches/romdev-snippets/beetle-psx-regsnap.c). build needs a PS1
|
|
232
|
+
// toolchain (PSn00bSDK, not yet).
|
|
228
233
|
build: true, run: true, screenshot: true,
|
|
229
234
|
inspectSprites: false, inspectPalette: false, inspectBackground: false,
|
|
230
235
|
renderingContext: false, cpuState: true, audioDebug: true,
|
|
@@ -248,6 +253,29 @@ export const CAPABILITIES = {
|
|
|
248
253
|
cart: false, disasm: true, decompile: true,
|
|
249
254
|
},
|
|
250
255
|
},
|
|
256
|
+
|
|
257
|
+
dreamcast: {
|
|
258
|
+
cpuFamily: "sh", decompileQuality: "good", tier: "sh",
|
|
259
|
+
cpus: { main: "sh4", secondary: ["arm7"] }, // SH-4 main + ARM7 AICA sound CPU
|
|
260
|
+
audioChips: ["aica"],
|
|
261
|
+
memoryRegions: [...GENERIC_REGIONS],
|
|
262
|
+
renderingKind: "3d", introspection: "shallow",
|
|
263
|
+
ops: {
|
|
264
|
+
// Flycast WASM boots + RUNS homebrew .elf (reios HLE): the SH-4 executes guest
|
|
265
|
+
// code (run + memory introspection), and the PowerVR2 present-path works — flycast
|
|
266
|
+
// renders to the GL FBO and the host reads it back (verified: a framebuffer-writing
|
|
267
|
+
// program shows ~727k captured pixels). `build` lands with the sh-elf-gcc WASM
|
|
268
|
+
// toolchain. cpuState (SH-4 Sh4cntx regs) + audioDebug (AICA 64-channel register
|
|
269
|
+
// window) come from romdev_sh4_regs_get/romdev_aica_get patched into the flycast
|
|
270
|
+
// libretro entry (see scripts/patches/romdev-snippets/flycast-debug.c). The 3D
|
|
271
|
+
// renderer has no tile/sprite inspectors (N/A by hw). disasm/decompile = SH-4
|
|
272
|
+
// analysis slice (rizin `sh` + Ghidra SuperH4 SLEIGH).
|
|
273
|
+
build: true, run: true, screenshot: true,
|
|
274
|
+
inspectSprites: false, inspectPalette: false, inspectBackground: false,
|
|
275
|
+
renderingContext: false, cpuState: true, audioDebug: true,
|
|
276
|
+
cart: false, disasm: true, decompile: true,
|
|
277
|
+
},
|
|
278
|
+
},
|
|
251
279
|
};
|
|
252
280
|
|
|
253
281
|
/** The 32-bit MIPS tier (PS1/N64) — marked `tier:"mips"`. A PARTIAL tier: they
|
|
@@ -259,14 +287,22 @@ export const MIPS_TIER_PLATFORMS = Object.entries(CAPABILITIES)
|
|
|
259
287
|
.filter(([, c]) => c.tier === "mips")
|
|
260
288
|
.map(([p]) => p);
|
|
261
289
|
|
|
290
|
+
/** The next-gen tiers (32-bit CPU families added after the canonical 14): MIPS
|
|
291
|
+
* (PS1/N64) + SH (Dreamcast) + future. Any platform carrying a CPU-family `tier`
|
|
292
|
+
* is held to its OWN conformance, not the "all 14" cross-checks; a new platform
|
|
293
|
+
* starts here (analysis-first) and graduates as run/build/etc. land. */
|
|
294
|
+
export const NEXTGEN_TIER_PLATFORMS = Object.entries(CAPABILITIES)
|
|
295
|
+
.filter(([, c]) => c.tier === "mips" || c.tier === "sh")
|
|
296
|
+
.map(([p]) => p);
|
|
297
|
+
|
|
262
298
|
/** Back-compat: the analysis-only set is now empty (PS1/N64 gained run/screenshot
|
|
263
299
|
* in the run-side wiring). Kept so the name still resolves for older imports. */
|
|
264
300
|
export const ANALYSIS_ONLY_PLATFORMS = [];
|
|
265
301
|
|
|
266
302
|
/** The canonical tier-1 platforms (the 14): full op surface, universal build/run/
|
|
267
|
-
* screenshot. Excludes the partial
|
|
303
|
+
* screenshot. Excludes the partial next-gen tiers above. */
|
|
268
304
|
export const CONTRACT_PLATFORMS = Object.keys(CAPABILITIES)
|
|
269
|
-
.filter((p) => !
|
|
305
|
+
.filter((p) => !NEXTGEN_TIER_PLATFORMS.includes(p));
|
|
270
306
|
|
|
271
307
|
/** Does `platform` support `op`? Unknown platform/op → false. */
|
|
272
308
|
export function supports(platform, op) {
|
package/src/cores/registry.js
CHANGED
|
@@ -57,14 +57,19 @@ export const CORES = {
|
|
|
57
57
|
// webgl-node bridge only when one of these boots (hwRender:true). The other 14 are
|
|
58
58
|
// software-rendered and never touch GL, so a headless user without the GPU module
|
|
59
59
|
// is unaffected.
|
|
60
|
-
// parallel_n64
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
n64: { platform: "n64", coreName: "parallel_n64", pkg: "romdev-core-parallel-n64", displayName: "Nintendo 64 (ParaLLEl N64,
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
|
|
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 },
|
|
68
73
|
};
|
|
69
74
|
|
|
70
75
|
/** Try to get {jsPath,wasmPath} for a core from its binary package. */
|
package/src/host/LibretroGL.js
CHANGED
|
@@ -174,13 +174,20 @@ export class LibretroGL {
|
|
|
174
174
|
|
|
175
175
|
/**
|
|
176
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).
|
|
177
181
|
* Returns { pixels, width, height } or null if not active.
|
|
178
182
|
*/
|
|
179
|
-
readbackFrame() {
|
|
183
|
+
readbackFrame(cropW, cropH) {
|
|
180
184
|
if (!this.active) return null;
|
|
181
185
|
|
|
182
186
|
gl.glFinish();
|
|
183
|
-
|
|
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;
|
|
184
191
|
const GL_RGBA = 0x1908, GL_UNSIGNED_BYTE = 0x1401;
|
|
185
192
|
|
|
186
193
|
if (!this._glPixels || this._glPixels.length !== w * h * 4) {
|
|
@@ -188,6 +195,7 @@ export class LibretroGL {
|
|
|
188
195
|
}
|
|
189
196
|
|
|
190
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.
|
|
191
199
|
gl.glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, this._glPixels);
|
|
192
200
|
|
|
193
201
|
|
|
@@ -225,32 +233,27 @@ export class LibretroGL {
|
|
|
225
233
|
return this._procAddressCache.get(name);
|
|
226
234
|
}
|
|
227
235
|
|
|
228
|
-
//
|
|
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.
|
|
229
252
|
const fn = gl[name];
|
|
230
253
|
if (!fn) {
|
|
231
|
-
// Not found — some cores probe for extension functions
|
|
232
254
|
this._procAddressCache.set(name, 0);
|
|
233
255
|
return 0;
|
|
234
256
|
}
|
|
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
257
|
const stub = mod.addFunction(() => { return 0; }, 'i');
|
|
255
258
|
this._procAddressCache.set(name, stub);
|
|
256
259
|
return stub;
|