romdevtools 0.70.0 → 0.71.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 +80 -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 +3 -3
- package/src/cores/capabilities.js +13 -9
- package/src/host/LibretroHost.js +242 -23
- package/src/host/callbacks.js +68 -1
- 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/platforms.js +46 -10
- package/src/mcp/tools/toolchain.js +140 -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/gba/MENTAL_MODEL.md +22 -1
- 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/mips-c/mips-c.js +35 -1
- package/src/toolchains/sh-c/lib/dc.h +65 -15
- package/src/toolchains/sh-c/sh-c.js +6 -3
package/src/host/LibretroHost.js
CHANGED
|
@@ -164,6 +164,29 @@ function mirrorDirToFS(FS, hostDir, fsDir) {
|
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
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
|
+
|
|
167
190
|
/**
|
|
168
191
|
* When loadMedia is called with `bytes:` and no `virtualName`, this is
|
|
169
192
|
* the extension we tack onto "/rom" so the core knows which platform
|
|
@@ -294,8 +317,16 @@ export class LibretroHost {
|
|
|
294
317
|
async loadCore(jsPath, wasmPath, opts = {}) {
|
|
295
318
|
if (this.mod) throw new Error("core already loaded; create a new host");
|
|
296
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
|
+
|
|
297
328
|
let glCanvas = null;
|
|
298
|
-
if (opts.hwRender) {
|
|
329
|
+
if (opts.hwRender && !this._proxied) {
|
|
299
330
|
// Set up the HW-render path: LibretroGL owns the FBO/readback; a webgl-node
|
|
300
331
|
// mock canvas backs the minified core's GLctx. Both pull the OPTIONAL native
|
|
301
332
|
// GL deps lazily (clear install hint if absent).
|
|
@@ -317,15 +348,58 @@ export class LibretroHost {
|
|
|
317
348
|
const mod = await loadLibretroCore({ jsPath, wasmPath, glCanvas });
|
|
318
349
|
this.mod = mod;
|
|
319
350
|
|
|
320
|
-
|
|
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
|
|
321
382
|
// set BEFORE any GL call (the core's context_reset / first getParameter). Must
|
|
322
383
|
// run before _retro_init.
|
|
323
384
|
if (glCanvas && mod.GL) {
|
|
324
|
-
|
|
325
|
-
|
|
385
|
+
{
|
|
386
|
+
const handle = mod.GL.createContext(glCanvas, { majorVersion: 2 });
|
|
387
|
+
if (handle > 0) mod.GL.makeContextCurrent(handle);
|
|
388
|
+
}
|
|
326
389
|
}
|
|
327
390
|
|
|
328
|
-
|
|
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
|
+
}
|
|
329
403
|
|
|
330
404
|
// Pre-seed per-platform core-option overrides BEFORE _retro_init. The core
|
|
331
405
|
// registers its variables (SET_VARIABLES) during retro_init and decides its
|
|
@@ -344,7 +418,13 @@ export class LibretroHost {
|
|
|
344
418
|
}
|
|
345
419
|
}
|
|
346
420
|
|
|
347
|
-
|
|
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;
|
|
348
428
|
this.status.corePath = jsPath;
|
|
349
429
|
}
|
|
350
430
|
|
|
@@ -364,6 +444,10 @@ export class LibretroHost {
|
|
|
364
444
|
async loadMedia(args) {
|
|
365
445
|
const mod = this._needMod();
|
|
366
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;
|
|
367
451
|
// Derive the kind from the file/virtual extension when the caller didn't say
|
|
368
452
|
// — so a C64 .d64 reports mediaKind:"disk" (writable save target) vs a .prg
|
|
369
453
|
// "program". For an in-memory load, the virtualName carries the ext.
|
|
@@ -397,7 +481,13 @@ export class LibretroHost {
|
|
|
397
481
|
if (this.systemDir && !this._systemDirMounted && mod.FS) {
|
|
398
482
|
try {
|
|
399
483
|
const FS_SYS = "/system";
|
|
400
|
-
|
|
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
|
+
}
|
|
401
491
|
// Redirect the core's reported system dir to the in-FS copy.
|
|
402
492
|
if (this.state) this.state.systemDir = FS_SYS;
|
|
403
493
|
this._systemDirMounted = true;
|
|
@@ -430,7 +520,10 @@ export class LibretroHost {
|
|
|
430
520
|
throw new Error("loadMedia requires either `path` or `bytes`");
|
|
431
521
|
}
|
|
432
522
|
const vfsPath = "/rom" + ext;
|
|
433
|
-
|
|
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) {
|
|
434
527
|
try {
|
|
435
528
|
mod.FS.writeFile(vfsPath, data);
|
|
436
529
|
} catch (e) {
|
|
@@ -448,6 +541,14 @@ export class LibretroHost {
|
|
|
448
541
|
const pathPtr = mod._malloc(pathBytes.length);
|
|
449
542
|
mod.HEAPU8.set(pathBytes, pathPtr);
|
|
450
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
|
+
|
|
451
552
|
// retro_game_info struct (16 bytes on wasm32).
|
|
452
553
|
const infoPtr = mod._malloc(16);
|
|
453
554
|
mod.setValue(infoPtr + 0, pathPtr, "i32");
|
|
@@ -455,7 +556,38 @@ export class LibretroHost {
|
|
|
455
556
|
mod.setValue(infoPtr + 8, data.length, "i32");
|
|
456
557
|
mod.setValue(infoPtr + 12, 0, "i32"); // meta = null
|
|
457
558
|
|
|
458
|
-
|
|
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
|
+
}
|
|
459
591
|
|
|
460
592
|
// Free the struct itself. Don't free pathPtr or dataPtr — the core may
|
|
461
593
|
// retain pointers into them for the life of the loaded game.
|
|
@@ -512,7 +644,10 @@ export class LibretroHost {
|
|
|
512
644
|
// };
|
|
513
645
|
// };
|
|
514
646
|
const avInfoPtr = mod._malloc(64);
|
|
515
|
-
|
|
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);
|
|
516
651
|
this.status.fbWidth = mod.getValue(avInfoPtr + 0, "i32");
|
|
517
652
|
this.status.fbHeight = mod.getValue(avInfoPtr + 4, "i32");
|
|
518
653
|
// aspect_ratio (+16) is the intended DISPLAY aspect (what a CRT would
|
|
@@ -540,11 +675,17 @@ export class LibretroHost {
|
|
|
540
675
|
this.hwRender.createContext(maxW, maxH, this._glContextCreated);
|
|
541
676
|
}
|
|
542
677
|
|
|
543
|
-
// Configure controller port 0 as joypad (some cores default to NONE).
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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
|
+
}
|
|
548
689
|
|
|
549
690
|
// ---- Settle the framebuffer to the ROM's chosen geometry ----
|
|
550
691
|
//
|
|
@@ -567,7 +708,10 @@ export class LibretroHost {
|
|
|
567
708
|
// VDP in 1-3 frames; SNES in 1; NES in 1-2. The cost is ~5-30 ms
|
|
568
709
|
// on loadMedia, paid once. Skip when loaded paused (caller is
|
|
569
710
|
// already in control).
|
|
570
|
-
|
|
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) {
|
|
571
715
|
// Settle frames are core warm-up, not agent-visible gameplay
|
|
572
716
|
// frames — don't increment frameCount. From the agent's POV the
|
|
573
717
|
// first stepFrames(N) should advance the count by exactly N.
|
|
@@ -587,7 +731,7 @@ export class LibretroHost {
|
|
|
587
731
|
let stepped = 0;
|
|
588
732
|
let firstRefreshAt = -1;
|
|
589
733
|
while (stepped < MAX_SETTLE) {
|
|
590
|
-
|
|
734
|
+
this._runCore();
|
|
591
735
|
this._afterRun();
|
|
592
736
|
stepped++;
|
|
593
737
|
if (firstRefreshAt < 0 && this.state.lastFrame && this.state.lastFrame !== beforeRefreshSnapshot) {
|
|
@@ -624,11 +768,11 @@ export class LibretroHost {
|
|
|
624
768
|
* @returns {number} frames actually run
|
|
625
769
|
*/
|
|
626
770
|
stepFrames(n) {
|
|
627
|
-
|
|
771
|
+
this._needMod();
|
|
628
772
|
this._needMedia();
|
|
629
773
|
if (this.status.paused) return 0;
|
|
630
774
|
for (let i = 0; i < n; i++) {
|
|
631
|
-
|
|
775
|
+
this._runCore();
|
|
632
776
|
this._afterRun();
|
|
633
777
|
this.status.frameCount++;
|
|
634
778
|
}
|
|
@@ -639,10 +783,44 @@ export class LibretroHost {
|
|
|
639
783
|
return n;
|
|
640
784
|
}
|
|
641
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
|
+
|
|
642
793
|
/** Post-_retro_run hook: HW-render readback. The video callback set
|
|
643
794
|
* state.hwFramePending during run; now (GL state stable) read back the FBO into
|
|
644
795
|
* state.lastFrame as an RGBA frame. No-op for the 14 software cores. */
|
|
645
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
|
+
}
|
|
646
824
|
if (this.state.hwFramePending && this.hwRender?.active) {
|
|
647
825
|
this.state.hwFramePending = false;
|
|
648
826
|
// Crop the GL FBO to the core's reported active resolution (e.g. DC 640x480 in an
|
|
@@ -663,9 +841,9 @@ export class LibretroHost {
|
|
|
663
841
|
* playtest loop can't race). Advances the (monotonic) frame counter by 1.
|
|
664
842
|
* Returns the frame count after. */
|
|
665
843
|
renderOneFrame() {
|
|
666
|
-
|
|
844
|
+
this._needMod();
|
|
667
845
|
this._needMedia();
|
|
668
|
-
|
|
846
|
+
this._runCore();
|
|
669
847
|
this._afterRun();
|
|
670
848
|
this.status.frameCount++;
|
|
671
849
|
if (this.state.lastFrame) {
|
|
@@ -1543,6 +1721,47 @@ export class LibretroHost {
|
|
|
1543
1721
|
}
|
|
1544
1722
|
}
|
|
1545
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
|
+
|
|
1546
1765
|
/** True when the PS1 core exposes the SPU register block (getAudioState chip:'spu'). */
|
|
1547
1766
|
spuRegsSupported() {
|
|
1548
1767
|
return !!(this.mod && typeof this.mod._romdev_spu_get === "function");
|
|
@@ -1770,7 +1989,7 @@ export class LibretroHost {
|
|
|
1770
1989
|
* @returns {number} frames actually run
|
|
1771
1990
|
*/
|
|
1772
1991
|
_runFramesExclusive(body, maxFrames) {
|
|
1773
|
-
|
|
1992
|
+
this._needMod();
|
|
1774
1993
|
// Suspend ONLY the playtest window's render-tick stepping for the duration —
|
|
1775
1994
|
// not `status.paused` (the agent's pause is a separate concept, and the core
|
|
1776
1995
|
// run must be identical whether or not the user paused). The playtest tick
|
|
@@ -1781,7 +2000,7 @@ export class LibretroHost {
|
|
|
1781
2000
|
let framesRun = 0;
|
|
1782
2001
|
try {
|
|
1783
2002
|
for (let i = 0; i < maxFrames; i++) {
|
|
1784
|
-
|
|
2003
|
+
this._runCore();
|
|
1785
2004
|
this.status.frameCount++;
|
|
1786
2005
|
framesRun++;
|
|
1787
2006
|
if (body(i)) break;
|
package/src/host/callbacks.js
CHANGED
|
@@ -170,7 +170,7 @@ 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
175
|
// Remember the core's reported active resolution (e.g. DC 640x480) so the
|
|
176
176
|
// readback can crop the (often larger, e.g. 853x853) GL FBO to just the
|
|
@@ -228,6 +228,49 @@ export function registerCallbacks(args) {
|
|
|
228
228
|
mod._retro_set_input_state(inputStateCb);
|
|
229
229
|
}
|
|
230
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
|
+
|
|
231
274
|
function allocString(mod, state, str) {
|
|
232
275
|
const bytes = Buffer.from(str + "\0", "utf-8");
|
|
233
276
|
const ptr = mod._malloc(bytes.length);
|
|
@@ -300,6 +343,19 @@ function handleEnv(mod, state, rawCmd, dataPtr, log) {
|
|
|
300
343
|
// already a useful diagnostic — most cores pass a fully-formed message
|
|
301
344
|
// (no varargs substitution needed) or a message + N args we can't decode.
|
|
302
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
|
+
}
|
|
303
359
|
if (!state._logCbPtr) {
|
|
304
360
|
state._logCbPtr = mod.addFunction((level, fmtPtr, _vaPtr) => {
|
|
305
361
|
if (!log) return;
|
|
@@ -448,6 +504,17 @@ function handleEnv(mod, state, rawCmd, dataPtr, log) {
|
|
|
448
504
|
// software cores, or no GL stack), reject so the core falls back to software.
|
|
449
505
|
case E.SET_HW_RENDER:
|
|
450
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
|
+
}
|
|
451
518
|
return false;
|
|
452
519
|
|
|
453
520
|
// Things we can't or don't want to support — reject so core falls back.
|
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);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Dreamcast AICA sound-chip decode — the "what is each of the 64 PCM channels
|
|
2
|
+
// doing this frame?" view that getAudioState gives the other platforms' chips.
|
|
3
|
+
// Input is the raw AICA register window (Uint8Array) from host.getAicaRegs():
|
|
4
|
+
// 64 channels × 0x80 bytes starting at 0x000, plus CommonData at 0x2800.
|
|
5
|
+
//
|
|
6
|
+
// The AICA is a 64-channel PCM/ADPCM sampler. Canonical per-channel register map
|
|
7
|
+
// (offset within the channel's 0x80 block), little-endian 16-bit registers:
|
|
8
|
+
// 0x00 : KYONEX(15) KYONB(14) ... LPCTL(9) PCMS(8..7) SA-high(6..0)
|
|
9
|
+
// 0x04 : SA-low (sample start address, low 16)
|
|
10
|
+
// 0x08 : LSA (loop start) 0x0C : LEA (loop end)
|
|
11
|
+
// 0x10 : D2R/D1R/AR (attack/decay) 0x14 : RR/DL/KRS/LS (release/sustain)
|
|
12
|
+
// 0x18 : LPSLNK / OCT / FNS (pitch: OCT bits 14..11 signed, FNS bits 9..0)
|
|
13
|
+
// 0x24 : DISDL (direct send level, bits 11..8) / DIPAN (pan, bits 4..0)
|
|
14
|
+
// CommonData (0x2800): 0x00 MVOL (master volume, bits 3..0) + MONO/MEM8MB/etc.
|
|
15
|
+
|
|
16
|
+
const CH_STRIDE = 0x80;
|
|
17
|
+
const NUM_CH = 64;
|
|
18
|
+
const COMMON = 0x2800;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {Uint8Array|null} reg the AICA register window from getAicaRegs()
|
|
22
|
+
* @returns {{ chip:"aica", masterVolume:number, voices:Array<object>,
|
|
23
|
+
* activeVoices:number } | null}
|
|
24
|
+
*/
|
|
25
|
+
export function decodeAica(reg) {
|
|
26
|
+
if (!reg || reg.length < COMMON + 2) return null;
|
|
27
|
+
const u16 = (o) => (reg[o] | (reg[o + 1] << 8)) & 0xffff;
|
|
28
|
+
|
|
29
|
+
const voices = [];
|
|
30
|
+
let active = 0;
|
|
31
|
+
for (let c = 0; c < NUM_CH; c++) {
|
|
32
|
+
const base = c * CH_STRIDE;
|
|
33
|
+
if (base + 0x28 > reg.length) break;
|
|
34
|
+
const play = u16(base + 0x00);
|
|
35
|
+
const keyOn = !!(play & 0x4000); // KYONB
|
|
36
|
+
const loop = !!(play & 0x0200); // LPCTL
|
|
37
|
+
const pcms = (play >> 7) & 0x3; // 0/1 = 16/8-bit PCM, 2 = ADPCM
|
|
38
|
+
const saHi = play & 0x7f;
|
|
39
|
+
const saLo = u16(base + 0x04);
|
|
40
|
+
const sampleAddr = ((saHi << 16) | saLo) >>> 0;
|
|
41
|
+
const lsa = u16(base + 0x08);
|
|
42
|
+
const lea = u16(base + 0x0c);
|
|
43
|
+
const pitch = u16(base + 0x18);
|
|
44
|
+
const oct = (pitch >> 11) & 0xf; // signed 4-bit
|
|
45
|
+
const fns = pitch & 0x3ff;
|
|
46
|
+
const env = u16(base + 0x24);
|
|
47
|
+
const disdl = (env >> 8) & 0xf; // direct send level (volume)
|
|
48
|
+
const dipan = env & 0x1f; // pan
|
|
49
|
+
if (keyOn && disdl > 0) active++;
|
|
50
|
+
voices.push({
|
|
51
|
+
ch: c,
|
|
52
|
+
keyOn,
|
|
53
|
+
loop,
|
|
54
|
+
format: pcms === 2 ? "ADPCM" : (pcms === 1 ? "PCM8" : "PCM16"),
|
|
55
|
+
sampleAddr: "$" + sampleAddr.toString(16).toUpperCase(),
|
|
56
|
+
loopStart: lsa,
|
|
57
|
+
loopEnd: lea,
|
|
58
|
+
octave: oct >= 8 ? oct - 16 : oct, // 4-bit signed
|
|
59
|
+
fns,
|
|
60
|
+
volume: disdl, // 0..15 direct send level
|
|
61
|
+
pan: dipan,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const mvol = u16(COMMON + 0x00) & 0xf;
|
|
66
|
+
return { chip: "aica", masterVolume: mvol, activeVoices: active, voices };
|
|
67
|
+
}
|
package/src/mcp/tools/audio.js
CHANGED
|
@@ -367,7 +367,7 @@ export function registerAudioTools(server, z, sessionKey) {
|
|
|
367
367
|
"sampled SFX fire' on Genesis is still record-and-FFT.",
|
|
368
368
|
{
|
|
369
369
|
op: z.enum(["inspect", "record"]).describe("inspect a sound chip's live state (single-frame, or a frames trace); or record audio to a WAV."),
|
|
370
|
-
chip: z.enum(["nes", "gb", "gba", "dsp", "psg", "ym2612", "sid", "mikey", "pce", "ay8910"]).optional().describe("op=inspect: which sound chip to decode (
|
|
370
|
+
chip: z.enum(["nes", "gb", "gba", "dsp", "psg", "ym2612", "sid", "mikey", "pce", "ay8910", "spu", "ai", "aica"]).optional().describe("op=inspect: which sound chip to decode. Tile systems: nes/gb/gba/dsp(SNES)/psg(SN76489)/ym2612(Genesis FM)/sid(C64)/mikey(Lynx)/pce/ay8910(MSX). 3D systems: spu (PS1, 24 ADPCM voices), ai (N64 AI output), aica (Dreamcast, 64 PCM/ADPCM channels)."),
|
|
371
371
|
frames: z.number().int().min(1).max(60000).optional().describe("op=record: emulator frames to capture (default 180 = 3s NTSC). op=inspect: if set, TRACE the chip over N frames into a per-channel timeline; OMIT for a single-frame snapshot."),
|
|
372
372
|
sampleEvery: z.number().int().min(1).default(1).describe("op=inspect trace: sample the chip only every Nth frame (thins a long trace; the last frame is always sampled). Changes on skipped frames surface at the next sampled frame, so >1 loses fine timing — keep at 1 to catch every transition."),
|
|
373
373
|
path: z.string().optional().describe("op=record: absolute path to write the WAV (required for record)."),
|
package/src/mcp/tools/disasm.js
CHANGED
|
@@ -1461,7 +1461,7 @@ export function registerDisasmTools(server, z) {
|
|
|
1461
1461
|
"read/write); also walks the vector table. Banked carts are scanned PER BANK (all of the formats above) — " +
|
|
1462
1462
|
"refs carry `prgBank` (NES) / `romBank` (everything else). LIMITATION: direct addressing only " +
|
|
1463
1463
|
"(indirect/computed jumps are missed).\n" +
|
|
1464
|
-
"── RE ENGINE (Rizin + Ghidra, all
|
|
1464
|
+
"── RE ENGINE (Rizin + Ghidra, all platforms incl. the 3D CPUs: MIPS R3000/R4300 + SH-4) ──\n" +
|
|
1465
1465
|
"'functions' = Rizin auto-detected function list {address,size,nbbs,cc,callers,callees,looksLikeData}; the " +
|
|
1466
1466
|
"structural map of an unknown ROM. Sorted REAL CODE FIRST (by nbbs/cc) — don't rank by `size`, which is a lie " +
|
|
1467
1467
|
"(rizin folds data tables into giant pseudo-functions). `looksLikeData:true` (and the top-level `dataCount`) flags " +
|
package/src/mcp/tools/index.js
CHANGED
|
@@ -155,7 +155,7 @@ const CATEGORIES = [
|
|
|
155
155
|
},
|
|
156
156
|
{
|
|
157
157
|
name: "debug",
|
|
158
|
-
description: "Cross-platform debugging + reverse-engineering: inspectSprites, inspectPalette, getCPUState (main/spc700/z80), getAudioState (dsp/psg/ym2612), and the disasm/RE engine — disassemble (raw/ROM/rebuildable-project), plus the Rizin/Ghidra ops disasm({target:'cfg'|'xrefs'|'functions'|'decompile'}) (control-flow graphs, deep xrefs, auto-detected functions, and C pseudocode
|
|
158
|
+
description: "Cross-platform debugging + reverse-engineering: inspectSprites, inspectPalette, getCPUState (main/spc700/z80, plus the 3D CPUs: MIPS R3000/R4300, SH-4), getAudioState (dsp/psg/ym2612/… + spu/ai/aica for PS1/N64/Dreamcast), and the disasm/RE engine — disassemble (raw/ROM/rebuildable-project), plus the Rizin/Ghidra ops disasm({target:'cfg'|'xrefs'|'functions'|'decompile'}) (control-flow graphs, deep xrefs, auto-detected functions, and C pseudocode) and symbols({op:'analyze'}) (one-shot structural map).",
|
|
159
159
|
useWhen: ["sprites rendering wrong", "audio silent or distorted", "CPU stuck in unknown state", "need to read what existing ROM bytes do", "reverse-engineering an unknown ROM — carve its functions/structure before labeling them live", "want C-like pseudocode to understand a routine"],
|
|
160
160
|
register: (s, z, k) => {
|
|
161
161
|
registerPlatformSpecificTools(s, z, k); // inspectSprites/Palette, getCPUState, getDspState, ...
|
|
@@ -70,7 +70,7 @@ export async function listPlatformDocsCore({ platform }) {
|
|
|
70
70
|
docs,
|
|
71
71
|
note: docs.length === 0
|
|
72
72
|
? `No docs shipped for '${platform}' yet. Try a different platform, or fork an example game (examples({op:'fork'})) for boilerplate. (For RE/patching workflow, see platform({op:'doc', platform:'romhacking', name:'playbook'}).)`
|
|
73
|
-
: `Call platform({op:'doc', platform, name}) to read one. 'name' is 'mental_model' or '
|
|
73
|
+
: `Call platform({op:'doc', platform, name}) to read one. 'name' is 'mental_model', 'troubleshooting', or 'upstream_sources' (whichever this platform ships — see the docs[] list above). For RE/patching workflow across platforms, see platform({op:'doc', platform:'romhacking', name:'playbook'}).`,
|
|
74
74
|
};
|
|
75
75
|
}
|
|
76
76
|
|