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/host/LibretroHost.js
CHANGED
|
@@ -36,6 +36,17 @@ import {
|
|
|
36
36
|
* to function headlessly (no menu = no user pick). Empty today — all
|
|
37
37
|
* shipped cores load with their defaults.
|
|
38
38
|
*/
|
|
39
|
+
// Core filename stem → platform, for the cores whose renderer (or other boot-time
|
|
40
|
+
// behavior) is chosen from an option at retro_init. loadCore uses this to pre-seed
|
|
41
|
+
// PLATFORM_CORE_OPTIONS before the core registers its variables, so the override
|
|
42
|
+
// wins over the core's default (e.g. glide64-GL over angrylion-software for N64).
|
|
43
|
+
const CORE_STEM_TO_PLATFORM = {
|
|
44
|
+
parallel_n64: "n64",
|
|
45
|
+
pcsx_rearmed: "ps1",
|
|
46
|
+
flycast: "dreamcast",
|
|
47
|
+
beetle_psx_hw: "ps1",
|
|
48
|
+
};
|
|
49
|
+
|
|
39
50
|
const PLATFORM_CORE_OPTIONS = {
|
|
40
51
|
// blueMSX defaults its machine to "SEGA - SC-3000" (an SG-1000 clone, wrong for
|
|
41
52
|
// MSX carts). Force the open MSX2+ C-BIOS machine — a superset that also runs
|
|
@@ -55,6 +66,24 @@ const PLATFORM_CORE_OPTIONS = {
|
|
|
55
66
|
// beetle_psx_hw defaults to the software renderer; force hardware_gl so it
|
|
56
67
|
// renders through the GL context the host set up (otherwise no FBO frames).
|
|
57
68
|
ps1: { beetle_psx_hw_renderer: "hardware_gl" },
|
|
69
|
+
// Flycast defaults to THREADED rendering — the render thread + the main thread's
|
|
70
|
+
// wait on it makes retro_run unwind under emscripten pthreads (yields to the event
|
|
71
|
+
// loop, incompatible with our synchronous frame-step). Disable it so render() runs
|
|
72
|
+
// synchronously on the calling thread (Emulator::render → run(), no cross-thread
|
|
73
|
+
// wait). Also keep per-frame sync (no auto frame-skip) so one retro_run = one frame.
|
|
74
|
+
// hle_bios (reios) MUST be on: it's flycast's HLE BIOS, and only the reios boot
|
|
75
|
+
// path loads a raw homebrew .elf (reios_loadElf copies PT_LOAD segments to their
|
|
76
|
+
// vaddr + jumps). With it off (the default), flycast wants a real dc_boot.bin we
|
|
77
|
+
// don't ship → the .elf never loads (RAM stays empty, CPU never runs our code).
|
|
78
|
+
// emulate_framebuffer scans out the DC framebuffer directly on every VBlank (the 2D
|
|
79
|
+
// path, no TA list) — so simple homebrew that writes RGB565 pixels to VRAM presents
|
|
80
|
+
// reliably without building a full PowerVR2 tile list. (Threaded rendering is also
|
|
81
|
+
// force-disabled in the core itself; the option here is belt-and-suspenders.)
|
|
82
|
+
dreamcast: {
|
|
83
|
+
flycast_threaded_rendering: "disabled",
|
|
84
|
+
flycast_hle_bios: "enabled",
|
|
85
|
+
flycast_emulate_framebuffer: "enabled",
|
|
86
|
+
},
|
|
58
87
|
// VICE mounts a .d64/.tap/.crt but, with autostart off, just sits at the BASIC
|
|
59
88
|
// `READY.` prompt — the agent would see a blue boot screen, not the game. Force
|
|
60
89
|
// autostart so a disk/tape image runs the first program automatically (same as
|
|
@@ -135,6 +164,29 @@ function mirrorDirToFS(FS, hostDir, fsDir) {
|
|
|
135
164
|
}
|
|
136
165
|
}
|
|
137
166
|
|
|
167
|
+
/** Mirror a host dir into a PROXIED core's APP-THREAD MEMFS (a per-thread JS heap, invisible to
|
|
168
|
+
* the main-thread FS). Each file's bytes go through shared WASM memory; romdev_app_fs_write does
|
|
169
|
+
* the FS.writeFile on the app thread, where the core's fopen runs. */
|
|
170
|
+
function mirrorDirToAppFS(mod, hostDir, fsDir) {
|
|
171
|
+
for (const name of readdirSync(hostDir)) {
|
|
172
|
+
const hostPath = path.join(hostDir, name);
|
|
173
|
+
const fsPath = fsDir + "/" + name;
|
|
174
|
+
const st = statSync(hostPath);
|
|
175
|
+
if (st.isDirectory()) {
|
|
176
|
+
mirrorDirToAppFS(mod, hostPath, fsPath);
|
|
177
|
+
} else if (st.isFile()) {
|
|
178
|
+
const bytes = readFileSync(hostPath);
|
|
179
|
+
const dataPtr = mod._malloc(bytes.length || 1);
|
|
180
|
+
mod.HEAPU8.set(bytes, dataPtr);
|
|
181
|
+
const pb = Buffer.from(fsPath + "\0", "utf-8");
|
|
182
|
+
const pathPtr = mod._malloc(pb.length);
|
|
183
|
+
mod.HEAPU8.set(pb, pathPtr);
|
|
184
|
+
try { mod._romdev_app_fs_write(pathPtr, dataPtr, bytes.length); } catch { /* skip */ }
|
|
185
|
+
mod._free(dataPtr); mod._free(pathPtr);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
138
190
|
/**
|
|
139
191
|
* When loadMedia is called with `bytes:` and no `virtualName`, this is
|
|
140
192
|
* the extension we tack onto "/rom" so the core knows which platform
|
|
@@ -252,11 +304,29 @@ export class LibretroHost {
|
|
|
252
304
|
* optional native GL stack and creates a headless context BEFORE the core boots
|
|
253
305
|
* (GL calls happen during init/load). The 14 software cores omit this.
|
|
254
306
|
*/
|
|
307
|
+
/** Infer the platform from a core's js filename stem (e.g.
|
|
308
|
+
* ".../parallel_n64_libretro.js" → "n64"), so loadCore can pre-seed that
|
|
309
|
+
* platform's option overrides without the caller threading the platform
|
|
310
|
+
* through. Covers the cores whose boot-time renderer choice depends on an
|
|
311
|
+
* option; returns null otherwise (the overrides are then a no-op anyway). */
|
|
312
|
+
_platformForCore(jsPath) {
|
|
313
|
+
const stem = (jsPath.split("/").pop() || "").replace(/_libretro\.js$/, "");
|
|
314
|
+
return CORE_STEM_TO_PLATFORM[stem] ?? null;
|
|
315
|
+
}
|
|
316
|
+
|
|
255
317
|
async loadCore(jsPath, wasmPath, opts = {}) {
|
|
256
318
|
if (this.mod) throw new Error("core already loaded; create a new host");
|
|
257
319
|
|
|
320
|
+
// Proxied (multi-threaded) cores — e.g. PPSSPP/PSP — run on a dedicated "app thread" so the
|
|
321
|
+
// JS main thread never blocks while the core's worker threads proxy back to it (which would
|
|
322
|
+
// deadlock the synchronous frame-stepping). They own the ENTIRE GL stack on the app thread
|
|
323
|
+
// (native-gles + webgl-node loaded + context created + readback all there). The registry
|
|
324
|
+
// marks them; the single-threaded GL cores keep the main-thread GL path below.
|
|
325
|
+
this._proxied = !!opts.proxied;
|
|
326
|
+
this.state.proxied = this._proxied;
|
|
327
|
+
|
|
258
328
|
let glCanvas = null;
|
|
259
|
-
if (opts.hwRender) {
|
|
329
|
+
if (opts.hwRender && !this._proxied) {
|
|
260
330
|
// Set up the HW-render path: LibretroGL owns the FBO/readback; a webgl-node
|
|
261
331
|
// mock canvas backs the minified core's GLctx. Both pull the OPTIONAL native
|
|
262
332
|
// GL deps lazily (clear install hint if absent).
|
|
@@ -278,16 +348,83 @@ export class LibretroHost {
|
|
|
278
348
|
const mod = await loadLibretroCore({ jsPath, wasmPath, glCanvas });
|
|
279
349
|
this.mod = mod;
|
|
280
350
|
|
|
281
|
-
|
|
351
|
+
if (this._proxied) {
|
|
352
|
+
// Spawn the app thread, then build the whole GL stack on it (native-gles + webgl-node +
|
|
353
|
+
// the Emscripten GL context). 480x272 is PSP native; the readback crops per-frame.
|
|
354
|
+
if (typeof mod._romdev_proxy_init !== "function") {
|
|
355
|
+
throw new Error("core marked proxied but missing _romdev_proxy_init export");
|
|
356
|
+
}
|
|
357
|
+
mod._romdev_proxy_init();
|
|
358
|
+
// Confirm the app thread's event loop is actually SERVICING its proxy queue before driving
|
|
359
|
+
// it — app_ready is set by a proxied ping (ping_on_app), which only runs once the queue is
|
|
360
|
+
// live. Setting it eagerly would race: a proxied call issued before the loop pumps would sit
|
|
361
|
+
// unprocessed and a proxy_sync would hang main. Pump + yield so the async ping is delivered.
|
|
362
|
+
mod._romdev_app_ping();
|
|
363
|
+
while (mod._romdev_app_ready() !== 1) {
|
|
364
|
+
mod._romdev_pump_main_queue();
|
|
365
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
366
|
+
}
|
|
367
|
+
if (opts.hwRender) {
|
|
368
|
+
// Async GL setup: the app thread imports native-gles/webgl-node + creates the context,
|
|
369
|
+
// which needs the main JS event loop. So kick it async + poll while yielding (a blocking
|
|
370
|
+
// proxy_sync would freeze the event loop the import() depends on → deadlock).
|
|
371
|
+
mod._romdev_proxied_gl_setup_start(480, 272);
|
|
372
|
+
while (mod._romdev_gl_setup_phase() !== 2) {
|
|
373
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
374
|
+
}
|
|
375
|
+
if (mod._romdev_gl_setup_result() !== 1) {
|
|
376
|
+
throw new Error("proxied GL setup failed on the app thread");
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Canvas HW-render cores (non-proxied): initialize the Emscripten GL context so `GLctx` is
|
|
282
382
|
// set BEFORE any GL call (the core's context_reset / first getParameter). Must
|
|
283
383
|
// run before _retro_init.
|
|
284
384
|
if (glCanvas && mod.GL) {
|
|
285
|
-
|
|
286
|
-
|
|
385
|
+
{
|
|
386
|
+
const handle = mod.GL.createContext(glCanvas, { majorVersion: 2 });
|
|
387
|
+
if (handle > 0) mod.GL.makeContextCurrent(handle);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (this._proxied) {
|
|
392
|
+
// Proxied cores: install the JS callback impls on Module (the C trampolines proxy each
|
|
393
|
+
// callback from the app thread back to main to run these). Register the trampolines on
|
|
394
|
+
// MAIN — they're plain C function pointers the app thread can call, and retro_init/
|
|
395
|
+
// pthread_create must run on main (Worker allocation proxies to main; it'd deadlock if
|
|
396
|
+
// main were blocked proxying init to the app thread). Only load_game/run proxy to app.
|
|
397
|
+
const { registerProxiedCallbacks } = await import("./callbacks.js");
|
|
398
|
+
registerProxiedCallbacks({ mod, state: this.state, log: this.log });
|
|
399
|
+
mod._romdev_register_callbacks();
|
|
400
|
+
} else {
|
|
401
|
+
registerCallbacks({ mod, state: this.state, log: this.log });
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Pre-seed per-platform core-option overrides BEFORE _retro_init. The core
|
|
405
|
+
// registers its variables (SET_VARIABLES) during retro_init and decides its
|
|
406
|
+
// renderer THEN — e.g. parallel_n64 reads `parallel-n64-gfxplugin` to pick
|
|
407
|
+
// glide64 (GL/HW-render) vs angrylion (software). Seeding the override here
|
|
408
|
+
// (not in loadMedia, which is too late) makes the SET_VARIABLES handler keep
|
|
409
|
+
// our value instead of the core's default, so HW render engages from boot.
|
|
410
|
+
const platform = opts.platform ?? this._platformForCore(jsPath);
|
|
411
|
+
if (platform) {
|
|
412
|
+
const overrides = PLATFORM_CORE_OPTIONS[platform];
|
|
413
|
+
if (overrides) {
|
|
414
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
415
|
+
this.state.coreVariables.set(key, { value });
|
|
416
|
+
}
|
|
417
|
+
this.state.variablesUpdated = true;
|
|
418
|
+
}
|
|
287
419
|
}
|
|
288
420
|
|
|
289
|
-
|
|
290
|
-
|
|
421
|
+
// retro_init runs on MAIN even for proxied cores (it spawns the threadManager workers via
|
|
422
|
+
// pthread_create, which needs the main thread free — not blocked proxying init to app).
|
|
423
|
+
// EXCEPT proxied cores defer it to loadMedia: PPSSPP's retro_init touches the system dir
|
|
424
|
+
// (fonts/flash0), which for proxied cores is only mirrored into the app-thread FS during
|
|
425
|
+
// loadMedia — calling retro_init before the mount makes the loader hang on missing assets.
|
|
426
|
+
if (!this._proxied) mod._retro_init();
|
|
427
|
+
else this._retroInitPending = true;
|
|
291
428
|
this.status.corePath = jsPath;
|
|
292
429
|
}
|
|
293
430
|
|
|
@@ -307,6 +444,10 @@ export class LibretroHost {
|
|
|
307
444
|
async loadMedia(args) {
|
|
308
445
|
const mod = this._needMod();
|
|
309
446
|
const { platform } = args;
|
|
447
|
+
// Allow the systemDir to be supplied per-load (not just via the constructor) — proxied cores
|
|
448
|
+
// (PSP) need it to mirror BIOS/font assets into the app-thread FS, and callers commonly pass
|
|
449
|
+
// it to loadMedia rather than at construction.
|
|
450
|
+
if (args.systemDir && !this.systemDir) this.systemDir = args.systemDir;
|
|
310
451
|
// Derive the kind from the file/virtual extension when the caller didn't say
|
|
311
452
|
// — so a C64 .d64 reports mediaKind:"disk" (writable save target) vs a .prg
|
|
312
453
|
// "program". For an in-memory load, the virtualName carries the ext.
|
|
@@ -340,7 +481,13 @@ export class LibretroHost {
|
|
|
340
481
|
if (this.systemDir && !this._systemDirMounted && mod.FS) {
|
|
341
482
|
try {
|
|
342
483
|
const FS_SYS = "/system";
|
|
343
|
-
|
|
484
|
+
// Proxied cores read the system dir on the app thread (separate per-thread MEMFS); mirror
|
|
485
|
+
// ONLY there. Non-proxied cores read main's FS. (Mirroring to both is wasted work.)
|
|
486
|
+
if (this._proxied) {
|
|
487
|
+
mirrorDirToAppFS(mod, this.systemDir, FS_SYS);
|
|
488
|
+
} else {
|
|
489
|
+
mirrorDirToFS(mod.FS, this.systemDir, FS_SYS);
|
|
490
|
+
}
|
|
344
491
|
// Redirect the core's reported system dir to the in-FS copy.
|
|
345
492
|
if (this.state) this.state.systemDir = FS_SYS;
|
|
346
493
|
this._systemDirMounted = true;
|
|
@@ -373,7 +520,10 @@ export class LibretroHost {
|
|
|
373
520
|
throw new Error("loadMedia requires either `path` or `bytes`");
|
|
374
521
|
}
|
|
375
522
|
const vfsPath = "/rom" + ext;
|
|
376
|
-
|
|
523
|
+
// For proxied cores the core fopen's on the app thread, so the ROM goes to the app-thread
|
|
524
|
+
// MEMFS below (romdev_app_fs_write) — writing it to the main-thread FS too is wasted work
|
|
525
|
+
// (and for a 60MB ISO, a needless copy). Non-proxied cores read main's FS directly.
|
|
526
|
+
if (mod.FS && !this._proxied) {
|
|
377
527
|
try {
|
|
378
528
|
mod.FS.writeFile(vfsPath, data);
|
|
379
529
|
} catch (e) {
|
|
@@ -391,6 +541,14 @@ export class LibretroHost {
|
|
|
391
541
|
const pathPtr = mod._malloc(pathBytes.length);
|
|
392
542
|
mod.HEAPU8.set(pathBytes, pathPtr);
|
|
393
543
|
|
|
544
|
+
// Proxied cores run on the app thread, whose MEMFS is a SEPARATE (per-thread) JS heap — the
|
|
545
|
+
// main-thread FS.writeFile above is invisible there. PPSSPP fopen's the path on the app
|
|
546
|
+
// thread, so write the ROM into the app thread's MEMFS too (the bytes are already in shared
|
|
547
|
+
// WASM memory at dataPtr).
|
|
548
|
+
if (this._proxied) {
|
|
549
|
+
mod._romdev_app_fs_write(pathPtr, dataPtr, data.length);
|
|
550
|
+
}
|
|
551
|
+
|
|
394
552
|
// retro_game_info struct (16 bytes on wasm32).
|
|
395
553
|
const infoPtr = mod._malloc(16);
|
|
396
554
|
mod.setValue(infoPtr + 0, pathPtr, "i32");
|
|
@@ -398,7 +556,38 @@ export class LibretroHost {
|
|
|
398
556
|
mod.setValue(infoPtr + 8, data.length, "i32");
|
|
399
557
|
mod.setValue(infoPtr + 12, 0, "i32"); // meta = null
|
|
400
558
|
|
|
401
|
-
|
|
559
|
+
// Proxied cores: run retro_load_game on the app thread ASYNCHRONOUSLY (it creates the GL
|
|
560
|
+
// context + spawns the core's worker threads there). We kick it async then poll the done
|
|
561
|
+
// flag while YIELDING to the JS event loop — the event loop must keep turning so emscripten
|
|
562
|
+
// can service the app-thread's pooled-Worker grabs / postMessage wakeups. A blocking call
|
|
563
|
+
// would freeze the event loop → the core's threads can't be scheduled → deadlock.
|
|
564
|
+
let ok;
|
|
565
|
+
if (this._proxied) {
|
|
566
|
+
// Deferred retro_init (see loadCore): now that the system dir is mirrored into the app-thread
|
|
567
|
+
// FS, the core can init (fonts/flash0 present) without hanging.
|
|
568
|
+
if (this._retroInitPending) {
|
|
569
|
+
mod._retro_init();
|
|
570
|
+
this._retroInitPending = false;
|
|
571
|
+
}
|
|
572
|
+
mod._romdev_proxied_load_game_start(infoPtr);
|
|
573
|
+
while (mod._romdev_proxied_load_state() === 0) {
|
|
574
|
+
mod._romdev_pump_main_queue(); // run callbacks the app thread proxied to main
|
|
575
|
+
await new Promise((r) => setTimeout(r, 0)); // yield → emscripten services worker ops
|
|
576
|
+
}
|
|
577
|
+
ok = mod._romdev_proxied_load_state() === 1;
|
|
578
|
+
// NOTE: PPSSPP's ContextReset (creates DrawContext + GPU) must run for rendering — without it
|
|
579
|
+
// gpu/draw are null and EmuFrame renders nothing (the blank-frame root cause). Firing it here
|
|
580
|
+
// currently heap-corrupts inside the GL render-manager construction (under investigation), so
|
|
581
|
+
// it's disabled for now; the core loads + runs stably without it (CPU/audio work, no video).
|
|
582
|
+
// ContextReset (creates GPU/DrawContext) survives intermittently after the webgl-node
|
|
583
|
+
// MAX_UNIFORM_BLOCK_SIZE fix but still hits a non-deterministic native-heap corruption in
|
|
584
|
+
// PPSSPP's GL init — disabled until that's pinned (ASAN). Core loads + runs without it.
|
|
585
|
+
if (ok && this.state.proxiedContextResetPtr) {
|
|
586
|
+
mod._romdev_proxied_fire_context_reset(this.state.proxiedContextResetPtr);
|
|
587
|
+
}
|
|
588
|
+
} else {
|
|
589
|
+
ok = mod._retro_load_game(infoPtr);
|
|
590
|
+
}
|
|
402
591
|
|
|
403
592
|
// Free the struct itself. Don't free pathPtr or dataPtr — the core may
|
|
404
593
|
// retain pointers into them for the life of the loaded game.
|
|
@@ -455,7 +644,10 @@ export class LibretroHost {
|
|
|
455
644
|
// };
|
|
456
645
|
// };
|
|
457
646
|
const avInfoPtr = mod._malloc(64);
|
|
458
|
-
|
|
647
|
+
// Proxied cores keep core state on the app thread; retro_get_system_av_info reads it, so it
|
|
648
|
+
// must run there (a direct main-thread call reads garbage / can wedge). Proxy it.
|
|
649
|
+
if (this._proxied) mod._romdev_proxied_av_info(avInfoPtr);
|
|
650
|
+
else mod._retro_get_system_av_info(avInfoPtr);
|
|
459
651
|
this.status.fbWidth = mod.getValue(avInfoPtr + 0, "i32");
|
|
460
652
|
this.status.fbHeight = mod.getValue(avInfoPtr + 4, "i32");
|
|
461
653
|
// aspect_ratio (+16) is the intended DISPLAY aspect (what a CRT would
|
|
@@ -483,11 +675,17 @@ export class LibretroHost {
|
|
|
483
675
|
this.hwRender.createContext(maxW, maxH, this._glContextCreated);
|
|
484
676
|
}
|
|
485
677
|
|
|
486
|
-
// Configure controller port 0 as joypad (some cores default to NONE).
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
678
|
+
// Configure controller port 0 as joypad (some cores default to NONE). Proxied cores touch
|
|
679
|
+
// core state on the app thread, so proxy it there.
|
|
680
|
+
if (this._proxied) {
|
|
681
|
+
mod._romdev_proxied_set_controller(0, RETRO_DEVICE_JOYPAD);
|
|
682
|
+
mod._romdev_proxied_set_controller(1, RETRO_DEVICE_JOYPAD);
|
|
683
|
+
} else {
|
|
684
|
+
mod._retro_set_controller_port_device(0, RETRO_DEVICE_JOYPAD);
|
|
685
|
+
// Port 1 too — needed for 2P. The C64/VICE 2P path (two live control ports)
|
|
686
|
+
// only reads RetroPad port 1 when it's registered as a joypad device.
|
|
687
|
+
mod._retro_set_controller_port_device(1, RETRO_DEVICE_JOYPAD);
|
|
688
|
+
}
|
|
491
689
|
|
|
492
690
|
// ---- Settle the framebuffer to the ROM's chosen geometry ----
|
|
493
691
|
//
|
|
@@ -510,7 +708,10 @@ export class LibretroHost {
|
|
|
510
708
|
// VDP in 1-3 frames; SNES in 1; NES in 1-2. The cost is ~5-30 ms
|
|
511
709
|
// on loadMedia, paid once. Skip when loaded paused (caller is
|
|
512
710
|
// already in control).
|
|
513
|
-
|
|
711
|
+
// Proxied cores (PSP) drive frames through the async run path (run_start + poll); the
|
|
712
|
+
// synchronous settle loop below uses the blocking sync run, so skip it. PSP geometry is the
|
|
713
|
+
// fixed 480×272 av_info already read, so there's no pre-init default to settle past.
|
|
714
|
+
if (!this.status.paused && !this._proxied) {
|
|
514
715
|
// Settle frames are core warm-up, not agent-visible gameplay
|
|
515
716
|
// frames — don't increment frameCount. From the agent's POV the
|
|
516
717
|
// first stepFrames(N) should advance the count by exactly N.
|
|
@@ -530,7 +731,7 @@ export class LibretroHost {
|
|
|
530
731
|
let stepped = 0;
|
|
531
732
|
let firstRefreshAt = -1;
|
|
532
733
|
while (stepped < MAX_SETTLE) {
|
|
533
|
-
|
|
734
|
+
this._runCore();
|
|
534
735
|
this._afterRun();
|
|
535
736
|
stepped++;
|
|
536
737
|
if (firstRefreshAt < 0 && this.state.lastFrame && this.state.lastFrame !== beforeRefreshSnapshot) {
|
|
@@ -567,11 +768,11 @@ export class LibretroHost {
|
|
|
567
768
|
* @returns {number} frames actually run
|
|
568
769
|
*/
|
|
569
770
|
stepFrames(n) {
|
|
570
|
-
|
|
771
|
+
this._needMod();
|
|
571
772
|
this._needMedia();
|
|
572
773
|
if (this.status.paused) return 0;
|
|
573
774
|
for (let i = 0; i < n; i++) {
|
|
574
|
-
|
|
775
|
+
this._runCore();
|
|
575
776
|
this._afterRun();
|
|
576
777
|
this.status.frameCount++;
|
|
577
778
|
}
|
|
@@ -582,13 +783,49 @@ export class LibretroHost {
|
|
|
582
783
|
return n;
|
|
583
784
|
}
|
|
584
785
|
|
|
786
|
+
/** Run one frame. Proxied cores route retro_run onto the app thread (so the JS main thread
|
|
787
|
+
* stays free to service the core's worker-thread proxy calls); others call directly. */
|
|
788
|
+
_runCore() {
|
|
789
|
+
if (this._proxied) this.mod._romdev_proxied_run();
|
|
790
|
+
else this.mod._retro_run();
|
|
791
|
+
}
|
|
792
|
+
|
|
585
793
|
/** Post-_retro_run hook: HW-render readback. The video callback set
|
|
586
794
|
* state.hwFramePending during run; now (GL state stable) read back the FBO into
|
|
587
795
|
* state.lastFrame as an RGBA frame. No-op for the 14 software cores. */
|
|
588
796
|
_afterRun() {
|
|
797
|
+
if (this._proxied) {
|
|
798
|
+
// Proxied cores own GL on the app thread; read the frame back THERE into a WASM buffer,
|
|
799
|
+
// then copy it out here. The core reports its size via video_refresh (hwFrameW/H).
|
|
800
|
+
if (this.state.hwFramePending) {
|
|
801
|
+
this.state.hwFramePending = false;
|
|
802
|
+
const w = this.state.hwFrameW || 480, h = this.state.hwFrameH || 272;
|
|
803
|
+
const mod = this.mod;
|
|
804
|
+
const n = w * h * 4;
|
|
805
|
+
if (!this._pxBuf || this._pxBufN !== n) {
|
|
806
|
+
if (this._pxBuf) mod._free(this._pxBuf);
|
|
807
|
+
this._pxBuf = mod._malloc(n); this._pxBufN = n;
|
|
808
|
+
}
|
|
809
|
+
mod._romdev_proxied_readback(w, h, this._pxBuf);
|
|
810
|
+
// native-gles is bottom-left origin → flip vertically into lastFrame.
|
|
811
|
+
const src = mod.HEAPU8.subarray(this._pxBuf, this._pxBuf + n);
|
|
812
|
+
const pixels = new Uint8Array(n);
|
|
813
|
+
const rowBytes = w * 4;
|
|
814
|
+
for (let y = 0; y < h; y++) {
|
|
815
|
+
pixels.set(src.subarray((h - 1 - y) * rowBytes, (h - y) * rowBytes), y * rowBytes);
|
|
816
|
+
}
|
|
817
|
+
this.state.lastFrame = {
|
|
818
|
+
width: w, height: h, pitch: rowBytes,
|
|
819
|
+
format: ROMDEV_PIXEL_FORMAT_RGBA8888, pixels, rgba: true,
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
589
824
|
if (this.state.hwFramePending && this.hwRender?.active) {
|
|
590
825
|
this.state.hwFramePending = false;
|
|
591
|
-
|
|
826
|
+
// Crop the GL FBO to the core's reported active resolution (e.g. DC 640x480 in an
|
|
827
|
+
// 853x853 FBO) so screenshots don't carry the dead viewport border.
|
|
828
|
+
const frame = this.hwRender.readbackFrame(this.state.hwFrameW, this.state.hwFrameH);
|
|
592
829
|
if (frame) {
|
|
593
830
|
this.state.lastFrame = {
|
|
594
831
|
width: frame.width, height: frame.height,
|
|
@@ -604,9 +841,9 @@ export class LibretroHost {
|
|
|
604
841
|
* playtest loop can't race). Advances the (monotonic) frame counter by 1.
|
|
605
842
|
* Returns the frame count after. */
|
|
606
843
|
renderOneFrame() {
|
|
607
|
-
|
|
844
|
+
this._needMod();
|
|
608
845
|
this._needMedia();
|
|
609
|
-
|
|
846
|
+
this._runCore();
|
|
610
847
|
this._afterRun();
|
|
611
848
|
this.status.frameCount++;
|
|
612
849
|
if (this.state.lastFrame) {
|
|
@@ -1484,6 +1721,47 @@ export class LibretroHost {
|
|
|
1484
1721
|
}
|
|
1485
1722
|
}
|
|
1486
1723
|
|
|
1724
|
+
/** True when the Dreamcast core (flycast) exposes the live SH-4 register block. */
|
|
1725
|
+
sh4RegsSupported() {
|
|
1726
|
+
return !!(this.mod && typeof this.mod._romdev_sh4_regs_get === "function");
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
/** Read the live SH-4 register block as a Uint32Array(24):
|
|
1730
|
+
* [0..15]=r0..r15, 16=pc, 17=pr, 18=gbr, 19=vbr, 20=sr, 21=mac.l, 22=mac.h, 23=fpul.
|
|
1731
|
+
* null if the core doesn't expose it. */
|
|
1732
|
+
getSh4Regs() {
|
|
1733
|
+
const mod = this.mod;
|
|
1734
|
+
if (!mod || typeof mod._romdev_sh4_regs_get !== "function") return null;
|
|
1735
|
+
const ptr = mod._malloc(24 * 4);
|
|
1736
|
+
try {
|
|
1737
|
+
mod._romdev_sh4_regs_get(ptr);
|
|
1738
|
+
return new Uint32Array(mod.HEAPU8.buffer, ptr, 24).slice();
|
|
1739
|
+
} finally {
|
|
1740
|
+
mod._free(ptr);
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
/** True when the Dreamcast core exposes the AICA register file
|
|
1745
|
+
* (getAudioState chip:'aica'). */
|
|
1746
|
+
aicaRegsSupported() {
|
|
1747
|
+
return !!(this.mod && typeof this.mod._romdev_aica_get === "function");
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
/** Read the AICA register window (channels + CommonData) as a Uint8Array.
|
|
1751
|
+
* Default 0x3000 bytes (64 SGC channel blocks @ 0x80 each + CommonData @ 0x2800).
|
|
1752
|
+
* null if the core doesn't expose it. */
|
|
1753
|
+
getAicaRegs(bytes = 0x3000) {
|
|
1754
|
+
const mod = this.mod;
|
|
1755
|
+
if (!mod || typeof mod._romdev_aica_get !== "function") return null;
|
|
1756
|
+
const ptr = mod._malloc(bytes);
|
|
1757
|
+
try {
|
|
1758
|
+
mod._romdev_aica_get(ptr, bytes);
|
|
1759
|
+
return new Uint8Array(mod.HEAPU8.buffer, ptr, bytes).slice();
|
|
1760
|
+
} finally {
|
|
1761
|
+
mod._free(ptr);
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1487
1765
|
/** True when the PS1 core exposes the SPU register block (getAudioState chip:'spu'). */
|
|
1488
1766
|
spuRegsSupported() {
|
|
1489
1767
|
return !!(this.mod && typeof this.mod._romdev_spu_get === "function");
|
|
@@ -1711,7 +1989,7 @@ export class LibretroHost {
|
|
|
1711
1989
|
* @returns {number} frames actually run
|
|
1712
1990
|
*/
|
|
1713
1991
|
_runFramesExclusive(body, maxFrames) {
|
|
1714
|
-
|
|
1992
|
+
this._needMod();
|
|
1715
1993
|
// Suspend ONLY the playtest window's render-tick stepping for the duration —
|
|
1716
1994
|
// not `status.paused` (the agent's pause is a separate concept, and the core
|
|
1717
1995
|
// run must be identical whether or not the user paused). The playtest tick
|
|
@@ -1722,7 +2000,7 @@ export class LibretroHost {
|
|
|
1722
2000
|
let framesRun = 0;
|
|
1723
2001
|
try {
|
|
1724
2002
|
for (let i = 0; i < maxFrames; i++) {
|
|
1725
|
-
|
|
2003
|
+
this._runCore();
|
|
1726
2004
|
this.status.frameCount++;
|
|
1727
2005
|
framesRun++;
|
|
1728
2006
|
if (body(i)) break;
|
package/src/host/callbacks.js
CHANGED
|
@@ -170,8 +170,12 @@ export function registerCallbacks(args) {
|
|
|
170
170
|
// GL state is only stable AFTER _retro_run returns. Flag it; stepFrames does the
|
|
171
171
|
// glReadPixels post-run.
|
|
172
172
|
const uPtr = dataPtr >>> 0;
|
|
173
|
-
if (uPtr === RETRO_HW_FRAME_BUFFER_VALID && state.hwRender?.active) {
|
|
173
|
+
if (uPtr === RETRO_HW_FRAME_BUFFER_VALID && (state.hwRender?.active || state.proxied)) {
|
|
174
174
|
state.hwFramePending = true;
|
|
175
|
+
// Remember the core's reported active resolution (e.g. DC 640x480) so the
|
|
176
|
+
// readback can crop the (often larger, e.g. 853x853) GL FBO to just the
|
|
177
|
+
// rendered region instead of returning the whole viewport with dead borders.
|
|
178
|
+
if (w > 0 && h > 0) { state.hwFrameW = w; state.hwFrameH = h; }
|
|
175
179
|
return;
|
|
176
180
|
}
|
|
177
181
|
if (dataPtr === 0 || uPtr === RETRO_HW_FRAME_BUFFER_VALID) return;
|
|
@@ -224,6 +228,49 @@ export function registerCallbacks(args) {
|
|
|
224
228
|
mod._retro_set_input_state(inputStateCb);
|
|
225
229
|
}
|
|
226
230
|
|
|
231
|
+
/**
|
|
232
|
+
* Proxied (app-thread) cores: the C shim registers C trampolines with the core that proxy each
|
|
233
|
+
* callback back to the MAIN thread to run these JS impls. So instead of addFunction (which binds
|
|
234
|
+
* a closure to the calling thread and can't be invoked from the app thread), we install the JS
|
|
235
|
+
* impls as Module fns the trampolines call via EM_ASM. Same bodies as registerCallbacks, minus
|
|
236
|
+
* the addFunction/retro_set_* (the shim does those on the app thread).
|
|
237
|
+
*/
|
|
238
|
+
export function registerProxiedCallbacks(args) {
|
|
239
|
+
const { mod, state, log } = args;
|
|
240
|
+
|
|
241
|
+
mod.romdev_envCb = (cmd, dataPtr) => (handleEnv(mod, state, cmd, dataPtr, log) ? 1 : 0);
|
|
242
|
+
|
|
243
|
+
mod.romdev_videoCb = (dataPtr, w, h, pitch) => {
|
|
244
|
+
const uPtr = dataPtr >>> 0;
|
|
245
|
+
if (uPtr === RETRO_HW_FRAME_BUFFER_VALID && (state.hwRender?.active || state.proxied)) {
|
|
246
|
+
state.hwFramePending = true;
|
|
247
|
+
if (w > 0 && h > 0) { state.hwFrameW = w; state.hwFrameH = h; }
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (dataPtr === 0 || uPtr === RETRO_HW_FRAME_BUFFER_VALID) return;
|
|
251
|
+
const bytes = h * pitch;
|
|
252
|
+
const view = mod.HEAPU8.subarray(dataPtr, dataPtr + bytes);
|
|
253
|
+
state.lastFrame = { width: w, height: h, pitch, format: state.pixelFormat, pixels: new Uint8Array(view) };
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
mod.romdev_audioBatchCb = (dataPtr, frames) => {
|
|
257
|
+
if (dataPtr === 0 || frames === 0) return frames;
|
|
258
|
+
const byteLen = frames * 2 * 2;
|
|
259
|
+
const samples = new Int16Array(mod.HEAPU8.buffer, dataPtr, byteLen / 2).slice();
|
|
260
|
+
state.audioRing.push(samples);
|
|
261
|
+
return frames;
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
mod.romdev_inputCb = (port, device, _idx, id) => {
|
|
265
|
+
if (device === 3) return state.keysDown.has(id) ? 1 : 0;
|
|
266
|
+
const portBits = state.inputPorts[port];
|
|
267
|
+
if (!portBits) return 0;
|
|
268
|
+
const bits = portBits[0];
|
|
269
|
+
if (id === 256) return bits;
|
|
270
|
+
return bits & (1 << id) ? 1 : 0;
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
227
274
|
function allocString(mod, state, str) {
|
|
228
275
|
const bytes = Buffer.from(str + "\0", "utf-8");
|
|
229
276
|
const ptr = mod._malloc(bytes.length);
|
|
@@ -296,6 +343,19 @@ function handleEnv(mod, state, rawCmd, dataPtr, log) {
|
|
|
296
343
|
// already a useful diagnostic — most cores pass a fully-formed message
|
|
297
344
|
// (no varargs substitution needed) or a message + N args we can't decode.
|
|
298
345
|
// Read the fmt as-is so log output is actually readable.
|
|
346
|
+
// Proxied cores call the log callback from the app thread, so it must be a C function
|
|
347
|
+
// pointer (a main-thread addFunction is a bad table index there). The shim's romdev_log_cb
|
|
348
|
+
// routes to Module.romdev_logCb (installed below).
|
|
349
|
+
if (state.proxied) {
|
|
350
|
+
mod.romdev_logCb = (level, fmtPtr) => {
|
|
351
|
+
if (!log) return;
|
|
352
|
+
let msg = "<unreadable>";
|
|
353
|
+
try { msg = mod.UTF8ToString(fmtPtr); } catch { /* */ }
|
|
354
|
+
log(level, msg);
|
|
355
|
+
};
|
|
356
|
+
mod.setValue(dataPtr, mod._romdev_log_cb_ptr(), "i32");
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
299
359
|
if (!state._logCbPtr) {
|
|
300
360
|
state._logCbPtr = mod.addFunction((level, fmtPtr, _vaPtr) => {
|
|
301
361
|
if (!log) return;
|
|
@@ -444,6 +504,17 @@ function handleEnv(mod, state, rawCmd, dataPtr, log) {
|
|
|
444
504
|
// software cores, or no GL stack), reject so the core falls back to software.
|
|
445
505
|
case E.SET_HW_RENDER:
|
|
446
506
|
if (state.hwRender) return state.hwRender.setup(mod, dataPtr);
|
|
507
|
+
// Proxied cores own GL on the app thread. Fill the retro_hw_render_callback struct with
|
|
508
|
+
// C function pointers (valid on the app thread, unlike main-thread addFunction): the core
|
|
509
|
+
// reads context_reset (+4) itself; we write get_current_framebuffer (+8) + get_proc_address
|
|
510
|
+
// (+12) from the shim. bottom_left_origin (+18) is true for GL.
|
|
511
|
+
if (state.proxied) {
|
|
512
|
+
state.proxiedContextResetPtr = mod.getValue(dataPtr + 4, "i32");
|
|
513
|
+
mod.setValue(dataPtr + 8, mod._romdev_hw_get_fb_ptr(), "i32");
|
|
514
|
+
mod.setValue(dataPtr + 12, mod._romdev_hw_get_proc_ptr(), "i32");
|
|
515
|
+
state.proxiedBottomLeft = !!mod.HEAPU8[dataPtr + 18];
|
|
516
|
+
return true;
|
|
517
|
+
}
|
|
447
518
|
return false;
|
|
448
519
|
|
|
449
520
|
// Things we can't or don't want to support — reject so core falls back.
|
package/src/host/coreLoader.js
CHANGED
|
@@ -37,10 +37,15 @@ export async function loadLibretroCore(args) {
|
|
|
37
37
|
|
|
38
38
|
/** @type {Record<string, unknown>} */
|
|
39
39
|
const opts = {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
printErr: () => {},
|
|
40
|
+
print: process.env.ROMDEV_CORE_LOG ? (s) => console.error("[core]", s) : () => {},
|
|
41
|
+
printErr: process.env.ROMDEV_CORE_LOG ? (s) => console.error("[core:err]", s) : () => {},
|
|
43
42
|
};
|
|
43
|
+
// The cores are linked with INVOKE_RUN=0, so main() never auto-runs regardless.
|
|
44
|
+
// Do NOT set noInitialRun on GL (canvas) cores: it suppresses the Emscripten GL
|
|
45
|
+
// runtime init, so `Module.GL` (and thus the WebGL2/native-gles context the core's
|
|
46
|
+
// SET_HW_RENDER path needs) is never created. For non-GL cores it's harmless either
|
|
47
|
+
// way; we set it only there to keep their startup identical to before.
|
|
48
|
+
if (!glCanvas) opts.noInitialRun = true;
|
|
44
49
|
|
|
45
50
|
// HW-render cores: minified glue uses GLctx = canvas.getContext('webgl2'), so we
|
|
46
51
|
// hand it a webgl-node mock canvas + install the WebGL2 globals Emscripten probes.
|
|
@@ -84,8 +89,16 @@ export async function loadLibretroCore(args) {
|
|
|
84
89
|
});
|
|
85
90
|
return {};
|
|
86
91
|
};
|
|
87
|
-
} else if (wasmPath) {
|
|
92
|
+
} else if (wasmPath && !glCanvas) {
|
|
88
93
|
opts.wasmBinary = await readFile(wasmPath);
|
|
94
|
+
} else if (wasmPath && glCanvas) {
|
|
95
|
+
// GL (canvas) cores: do NOT hand Emscripten a pre-read wasmBinary. Passing it
|
|
96
|
+
// routes instantiation through a path that skips the GL runtime init, so
|
|
97
|
+
// `Module.GL` (the WebGL2/native-gles context the SET_HW_RENDER path needs) is
|
|
98
|
+
// never created. Let Emscripten locate `<name>.wasm` next to the `.js` itself
|
|
99
|
+
// (via locateFile) — the same way retroemu loads parallel_n64/flycast.
|
|
100
|
+
const wasmDir = wasmPath.slice(0, wasmPath.lastIndexOf("/") + 1);
|
|
101
|
+
opts.locateFile = (file) => wasmDir + file;
|
|
89
102
|
}
|
|
90
103
|
|
|
91
104
|
const mod = await factory(opts);
|
package/src/host/cpu-state.js
CHANGED
|
@@ -163,6 +163,35 @@ function decodeMips(regs) {
|
|
|
163
163
|
};
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
/** Decode the SH-4 register block (Dreamcast) from getSh4Regs():
|
|
167
|
+
* [0..15]=r0..r15, 16=pc, 17=pr, 18=gbr, 19=vbr, 20=sr, 21=mac.l, 22=mac.h, 23=fpul. */
|
|
168
|
+
function decodeSh4(regs) {
|
|
169
|
+
if (!regs || regs.length < 24) return null;
|
|
170
|
+
const hx = (v) => "$" + (v >>> 0).toString(16).toUpperCase().padStart(8, "0");
|
|
171
|
+
const named = {};
|
|
172
|
+
for (let i = 0; i < 16; i++) named["r" + i] = hx(regs[i]);
|
|
173
|
+
named.pr = hx(regs[17]);
|
|
174
|
+
named.gbr = hx(regs[18]);
|
|
175
|
+
named.vbr = hx(regs[19]);
|
|
176
|
+
named.sr = hx(regs[20]);
|
|
177
|
+
named.macl = hx(regs[21]);
|
|
178
|
+
named.mach = hx(regs[22]);
|
|
179
|
+
named.fpul = hx(regs[23]);
|
|
180
|
+
// SR flags: T(0) S(1) Q(8) M(9) BL(28) RB(29) MD(30)
|
|
181
|
+
const sr = regs[20] >>> 0;
|
|
182
|
+
return {
|
|
183
|
+
pc: regs[16] >>> 0,
|
|
184
|
+
pcHex: hx(regs[16]),
|
|
185
|
+
registers: named,
|
|
186
|
+
flags: {
|
|
187
|
+
T: !!(sr & 0x1), S: !!(sr & 0x2),
|
|
188
|
+
Q: !!(sr & 0x100), M: !!(sr & 0x200),
|
|
189
|
+
BL: !!(sr & 0x10000000), RB: !!(sr & 0x20000000), MD: !!(sr & 0x40000000),
|
|
190
|
+
raw: hx(sr),
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
166
195
|
function decode6502(bytes) {
|
|
167
196
|
if (!bytes || bytes.length < 25) return null;
|
|
168
197
|
const u16le = (o) => bytes[o] | (bytes[o + 1] << 8);
|
|
@@ -327,6 +356,9 @@ export function getCPUState(host, platform, cpu = "main") {
|
|
|
327
356
|
if (platform === "n64" || platform === "ps1") {
|
|
328
357
|
return decodeMips(host.getMipsRegs?.());
|
|
329
358
|
}
|
|
359
|
+
if (platform === "dreamcast") {
|
|
360
|
+
return decodeSh4(host.getSh4Regs?.());
|
|
361
|
+
}
|
|
330
362
|
if (platform === "nes") {
|
|
331
363
|
const bytes = host.readMemory("nes_cpu_regs", 0, 28);
|
|
332
364
|
return decode6502(bytes);
|