romdevtools 0.27.0 → 0.28.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/AGENTS.md +5 -3
- package/CHANGELOG.md +309 -0
- package/README.md +1 -1
- package/examples/README.md +1 -1
- package/examples/atari2600/templates/platformer.asm +18 -9
- package/examples/atari2600/templates/racing.asm +25 -4
- package/examples/atari2600/templates/shmup.asm +30 -5
- package/examples/atari2600/templates/sports.asm +41 -9
- package/examples/atari7800/templates/hello_sprite.c +8 -4
- package/examples/atari7800/templates/platformer.c +12 -8
- package/examples/atari7800/templates/puzzle.c +7 -4
- package/examples/atari7800/templates/racing.c +5 -2
- package/examples/atari7800/templates/shmup.c +8 -4
- package/examples/atari7800/templates/sports.c +6 -3
- package/examples/c64/templates/platformer.c +28 -24
- package/examples/c64/templates/puzzle.c +77 -16
- package/examples/c64/templates/racing.c +9 -0
- package/examples/c64/templates/shmup.c +13 -1
- package/examples/c64/templates/sports.c +9 -4
- package/examples/gb/templates/platformer.c +6 -2
- package/examples/gb/templates/puzzle.c +279 -101
- package/examples/gb/templates/racing.c +13 -1
- package/examples/gb/templates/shmup.c +13 -1
- package/examples/gb/templates/sports.c +9 -3
- package/examples/gba/templates/platformer.c +7 -13
- package/examples/gba/templates/puzzle.c +93 -15
- package/examples/gba/templates/racing.c +13 -1
- package/examples/gba/templates/shmup.c +13 -1
- package/examples/gba/templates/sports.c +17 -5
- package/examples/gbc/templates/platformer.c +6 -2
- package/examples/gbc/templates/puzzle.c +878 -178
- package/examples/gbc/templates/racing.c +13 -1
- package/examples/gbc/templates/shmup.c +13 -1
- package/examples/gbc/templates/sports.c +9 -3
- package/examples/genesis/templates/puzzle.c +76 -15
- package/examples/genesis/templates/racing.c +13 -1
- package/examples/genesis/templates/shmup_2p.c +13 -1
- package/examples/gg/templates/platformer.c +4 -0
- package/examples/gg/templates/puzzle.c +80 -14
- package/examples/gg/templates/racing.c +17 -1
- package/examples/gg/templates/shmup.c +17 -1
- package/examples/gg/templates/sports.c +4 -0
- package/examples/lynx/templates/platformer.c +25 -6
- package/examples/lynx/templates/puzzle.c +77 -14
- package/examples/lynx/templates/shmup.c +13 -1
- package/examples/lynx/templates/sports.c +5 -2
- package/examples/msx/platformer/main.c +2 -0
- package/examples/msx/puzzle/main.c +78 -15
- package/examples/msx/racing/main.c +1 -0
- package/examples/msx/shmup/main.c +1 -0
- package/examples/msx/sports/main.c +3 -2
- package/examples/nes/templates/platformer.c +11 -3
- package/examples/nes/templates/puzzle.c +81 -21
- package/examples/nes/templates/racing.c +15 -1
- package/examples/nes/templates/shmup.c +1 -0
- package/examples/nes/templates/sports.c +1 -0
- package/examples/pce/platformer/main.c +3 -1
- package/examples/pce/puzzle/main.c +78 -12
- package/examples/pce/racing/main.c +1 -0
- package/examples/pce/shmup/main.c +5 -4
- package/examples/pce/sports/main.c +4 -3
- package/examples/sms/templates/platformer.c +4 -0
- package/examples/sms/templates/puzzle.c +80 -14
- package/examples/sms/templates/racing.c +17 -1
- package/examples/sms/templates/shmup.c +17 -1
- package/examples/sms/templates/shmup_2p.c +17 -1
- package/examples/sms/templates/sports.c +4 -0
- package/examples/snes/templates/platformer.c +32 -15
- package/examples/snes/templates/puzzle.c +84 -16
- package/examples/snes/templates/racing.c +20 -1
- package/examples/snes/templates/shmup.c +20 -2
- package/examples/snes/templates/sports.c +7 -0
- package/package.json +12 -12
- package/src/cores/wasm/bluemsx_libretro.js +1 -1
- package/src/cores/wasm/bluemsx_libretro.wasm +0 -0
- package/src/cores/wasm/fceumm_libretro.js +1 -1
- package/src/cores/wasm/fceumm_libretro.wasm +0 -0
- package/src/cores/wasm/gambatte_libretro.js +1 -1
- package/src/cores/wasm/gambatte_libretro.wasm +0 -0
- package/src/cores/wasm/geargrafx_libretro.js +1 -1
- package/src/cores/wasm/geargrafx_libretro.wasm +0 -0
- package/src/cores/wasm/genesis_plus_gx_libretro.js +1 -1
- package/src/cores/wasm/genesis_plus_gx_libretro.wasm +0 -0
- package/src/cores/wasm/handy_libretro.js +1 -1
- package/src/cores/wasm/handy_libretro.wasm +0 -0
- package/src/cores/wasm/mgba_libretro.js +1 -1
- package/src/cores/wasm/mgba_libretro.wasm +0 -0
- package/src/cores/wasm/prosystem_libretro.js +1 -1
- package/src/cores/wasm/prosystem_libretro.wasm +0 -0
- package/src/cores/wasm/snes9x_libretro.js +1 -1
- package/src/cores/wasm/snes9x_libretro.wasm +0 -0
- package/src/cores/wasm/stella2014_libretro.js +1 -1
- package/src/cores/wasm/stella2014_libretro.wasm +0 -0
- package/src/cores/wasm/vice_x64_libretro.js +1 -1
- package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
- package/src/host/LibretroHost.js +245 -10
- package/src/mcp/server.js +6 -0
- package/src/mcp/tools/disasm-rebuild.js +315 -65
- package/src/mcp/tools/disasm.js +149 -28
- package/src/mcp/tools/find-references.js +216 -51
- package/src/mcp/tools/frame.js +11 -4
- package/src/mcp/tools/index.js +15 -1
- package/src/mcp/tools/input.js +26 -3
- package/src/mcp/tools/memory.js +208 -39
- package/src/mcp/tools/playtest.js +56 -4
- package/src/mcp/tools/project.js +35 -9
- package/src/mcp/tools/toolchain.js +43 -10
- package/src/mcp/tools/watch-memory.js +141 -24
- package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +64 -17
- package/src/platforms/atari2600/MENTAL_MODEL.md +5 -1
- package/src/platforms/atari2600/TROUBLESHOOTING.md +40 -0
- package/src/platforms/atari7800/MENTAL_MODEL.md +27 -6
- package/src/platforms/gb/MENTAL_MODEL.md +16 -1
- package/src/platforms/gb/TROUBLESHOOTING.md +42 -0
- package/src/platforms/gb/lib/c/patch-header.js +7 -4
- package/src/platforms/gbc/MENTAL_MODEL.md +12 -0
- package/src/platforms/gbc/TROUBLESHOOTING.md +21 -0
- package/src/platforms/gbc/lib/c/font.h +43 -0
- package/src/platforms/gbc/lib/c/patch-header.js +7 -4
- package/src/platforms/genesis/MENTAL_MODEL.md +40 -6
- package/src/platforms/genesis/lib/c/genesis_sfx.c +37 -0
- package/src/platforms/genesis/lib/c/genesis_sfx.h +1 -0
- package/src/platforms/gg/TROUBLESHOOTING.md +13 -17
- package/src/platforms/gg/lib/c/gg_crt0.s +14 -2
- package/src/platforms/lynx/lib/c/lynx_sfx.c +38 -2
- package/src/platforms/lynx/lib/c/lynx_sfx.h +1 -0
- package/src/platforms/msx/MENTAL_MODEL.md +6 -0
- package/src/platforms/msx/TROUBLESHOOTING.md +21 -0
- package/src/platforms/msx/lib/c/msx_crt0.s +27 -0
- package/src/platforms/msx/lib/c/msx_hw.h +2 -0
- package/src/platforms/msx/lib/c/msx_vdp.c +45 -0
- package/src/platforms/nes/MENTAL_MODEL.md +10 -3
- package/src/platforms/nes/lib/c/nes_runtime.c +41 -0
- package/src/platforms/nes/lib/c/nes_runtime.h +2 -0
- package/src/platforms/pce/MENTAL_MODEL.md +9 -0
- package/src/platforms/pce/TROUBLESHOOTING.md +9 -0
- package/src/platforms/pce/lib/c/pce_hw.h +2 -1
- package/src/platforms/pce/lib/c/pce_sound.c +22 -0
- package/src/platforms/sms/MENTAL_MODEL.md +5 -0
- package/src/platforms/sms/TROUBLESHOOTING.md +6 -0
- package/src/platforms/sms/lib/c/sms_crt0.s +14 -2
- package/src/platforms/snes/MENTAL_MODEL.md +5 -0
- package/src/playtest/playtest.js +73 -3
- package/src/toolchains/index.js +37 -8
package/src/host/LibretroHost.js
CHANGED
|
@@ -776,6 +776,18 @@ export class LibretroHost {
|
|
|
776
776
|
return mod._retro_get_memory_size(id) || 0;
|
|
777
777
|
}
|
|
778
778
|
|
|
779
|
+
/** gpgx stores 68k work RAM as 16-bit words in host-LE order — the CPU's
|
|
780
|
+
* byte at $FF0000+A physically lives at work_ram[A^1] (core/macros.h
|
|
781
|
+
* READ_BYTE/WRITE_BYTE on little-endian builds). Normalize the system_ram
|
|
782
|
+
* region to CPU byte order here so offset X IS the byte the 68k sees at
|
|
783
|
+
* $FF0000+X — otherwise every byte-granular tool (search, diff, write,
|
|
784
|
+
* classify) is off-by-XOR-1 vs disassembly addresses and cheat-DB maps
|
|
785
|
+
* (self-consistent within raw-only loops, which is why it hid; poisonous
|
|
786
|
+
* the moment an address crosses to/from the CPU view). */
|
|
787
|
+
_byteSwapRegion(region) {
|
|
788
|
+
return region === "system_ram" && this.status.platform === "genesis";
|
|
789
|
+
}
|
|
790
|
+
|
|
779
791
|
readMemory(region, offset, length) {
|
|
780
792
|
const mod = this._needMod();
|
|
781
793
|
const id = MemoryRegionToRetro[region];
|
|
@@ -786,6 +798,12 @@ export class LibretroHost {
|
|
|
786
798
|
if (offset < 0 || offset + length > size) {
|
|
787
799
|
throw new RangeError(`read out of bounds: offset=${offset} len=${length} size=${size}`);
|
|
788
800
|
}
|
|
801
|
+
if (this._byteSwapRegion(region)) {
|
|
802
|
+
const heap = mod.HEAPU8;
|
|
803
|
+
const out = new Uint8Array(length);
|
|
804
|
+
for (let i = 0; i < length; i++) out[i] = heap[ptr + ((offset + i) ^ 1)];
|
|
805
|
+
return out;
|
|
806
|
+
}
|
|
789
807
|
return new Uint8Array(mod.HEAPU8.buffer, ptr + offset, length).slice();
|
|
790
808
|
}
|
|
791
809
|
|
|
@@ -804,6 +822,11 @@ export class LibretroHost {
|
|
|
804
822
|
if (offset < 0 || offset + bytes.length > size) {
|
|
805
823
|
throw new RangeError(`write out of bounds: offset=${offset} len=${bytes.length} size=${size}`);
|
|
806
824
|
}
|
|
825
|
+
if (this._byteSwapRegion(region)) {
|
|
826
|
+
const heap = mod.HEAPU8;
|
|
827
|
+
for (let i = 0; i < bytes.length; i++) heap[ptr + ((offset + i) ^ 1)] = bytes[i];
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
807
830
|
mod.HEAPU8.set(bytes, ptr + offset);
|
|
808
831
|
}
|
|
809
832
|
|
|
@@ -1263,6 +1286,161 @@ export class LibretroHost {
|
|
|
1263
1286
|
return !!(this.mod && typeof this.mod._romdev_watchdog_set === "function");
|
|
1264
1287
|
}
|
|
1265
1288
|
|
|
1289
|
+
/** True when this core build exposes the at-hit register snapshot (gpgx). */
|
|
1290
|
+
regSnapSupported() {
|
|
1291
|
+
return !!(this.mod && typeof this.mod._romdev_regsnap_get === "function");
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
/**
|
|
1295
|
+
* Read the at-hit register snapshot: the FULL register file frozen by the
|
|
1296
|
+
* core hook at the instant a pc-break / watchdog / write-watch / read-watch
|
|
1297
|
+
* fired. The live register file keeps running after a hit (per-scanline CPU
|
|
1298
|
+
* scheduling / next-frame re-entry), so post-hit register reads drift —
|
|
1299
|
+
* this snapshot is the truth. Shipped by ALL patched cores (all 14
|
|
1300
|
+
* platforms). Returns { kind, named } or null when no hit has been
|
|
1301
|
+
* snapshotted (or the core build predates the export). kind:
|
|
1302
|
+
* 1=pc-break/step, 2=watchdog, 3=write-watch, 4=read-watch. `named` keys
|
|
1303
|
+
* follow each CPU's own register file; `pc` is always the EXECUTING
|
|
1304
|
+
* instruction (ARM: its pipeline PC, the same convention breakpoint
|
|
1305
|
+
* addresses use). Pass clear to reset the kind.
|
|
1306
|
+
*/
|
|
1307
|
+
getRegSnapshot(clear = false) {
|
|
1308
|
+
const mod = this.mod;
|
|
1309
|
+
if (!mod || typeof mod._romdev_regsnap_get !== "function") return null;
|
|
1310
|
+
const ptr = mod._malloc(21 * 4);
|
|
1311
|
+
try {
|
|
1312
|
+
mod._romdev_regsnap_get(ptr, clear ? 1 : 0);
|
|
1313
|
+
const u = new Uint32Array(mod.HEAPU8.buffer, ptr, 21);
|
|
1314
|
+
const kind = u[0];
|
|
1315
|
+
if (!kind) return null;
|
|
1316
|
+
const r = Array.from(u.subarray(2, 2 + Math.min(u[1] >>> 0, 19)));
|
|
1317
|
+
const platform = this.status.platform;
|
|
1318
|
+
const h2 = (v) => "$" + (v & 0xFF).toString(16).toUpperCase();
|
|
1319
|
+
const h4 = (v) => "$" + (v & 0xFFFF).toString(16).toUpperCase();
|
|
1320
|
+
const hx = (v) => "$" + (v >>> 0).toString(16).toUpperCase();
|
|
1321
|
+
let named;
|
|
1322
|
+
if (platform === "genesis") {
|
|
1323
|
+
// m68k regId order: D0-7, A0-7, PC(instr start), SR, SP.
|
|
1324
|
+
named = {};
|
|
1325
|
+
for (let i = 0; i < 8; i++) named["d" + i] = hx(r[i]);
|
|
1326
|
+
for (let i = 0; i < 8; i++) named["a" + i] = hx(r[8 + i]);
|
|
1327
|
+
named.pc = hx(r[16]);
|
|
1328
|
+
named.sr = h4(r[17]);
|
|
1329
|
+
named.sp = hx(r[18]);
|
|
1330
|
+
} else if (platform === "gba") {
|
|
1331
|
+
// ARM regId order: r0-r15 raw, CPSR at 16, instr pipeline PC at 17, SP at 18.
|
|
1332
|
+
named = {};
|
|
1333
|
+
for (let i = 0; i < 16; i++) named["r" + i] = hx(r[i]);
|
|
1334
|
+
named.cpsr = hx(r[16]);
|
|
1335
|
+
named.pc = hx(r[17]); // EXECUTING instruction's pipeline PC (pc-break convention)
|
|
1336
|
+
named.sp = hx(r[18]);
|
|
1337
|
+
} else if (platform === "snes") {
|
|
1338
|
+
// 65816 regId order: A, X, Y, P, S, DB, D, …, PBPC(instr start).
|
|
1339
|
+
named = {
|
|
1340
|
+
a: h4(r[0]), x: h4(r[1]), y: h4(r[2]), p: h4(r[3]), s: h4(r[4]),
|
|
1341
|
+
db: h2(r[5]), d: h4(r[6]), pc: hx(r[16]),
|
|
1342
|
+
};
|
|
1343
|
+
} else if (platform === "gb" || platform === "gbc") {
|
|
1344
|
+
named = {
|
|
1345
|
+
a: h2(r[0]), f: h2(r[1]), b: h2(r[2]), c: h2(r[3]),
|
|
1346
|
+
d: h2(r[4]), e: h2(r[5]), h: h2(r[6]), l: h2(r[7]),
|
|
1347
|
+
pc: h4(r[16]), sp: h4(r[18]),
|
|
1348
|
+
};
|
|
1349
|
+
} else if (platform === "sms" || platform === "gg" || platform === "msx") {
|
|
1350
|
+
named = {
|
|
1351
|
+
a: h2(r[0]), f: h2(r[1]), b: h2(r[2]), c: h2(r[3]),
|
|
1352
|
+
d: h2(r[4]), e: h2(r[5]), h: h2(r[6]), l: h2(r[7]),
|
|
1353
|
+
ix: h4(r[8]), iy: h4(r[9]), pc: h4(r[16]), sp: h4(r[18]),
|
|
1354
|
+
};
|
|
1355
|
+
} else {
|
|
1356
|
+
// 6502 family (nes, atari2600, atari7800, c64, lynx, pce/huc6280):
|
|
1357
|
+
// regId order A, X, Y, P, S, …, PC(instr start).
|
|
1358
|
+
named = {
|
|
1359
|
+
a: h2(r[0]), x: h2(r[1]), y: h2(r[2]), p: h2(r[3]), s: h2(r[4]),
|
|
1360
|
+
pc: h4(r[16]),
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
return { kind, named };
|
|
1364
|
+
} finally {
|
|
1365
|
+
mod._free(ptr);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
/** True when this core build exposes the pure-CPU run (gpgx). */
|
|
1370
|
+
runPureSupported() {
|
|
1371
|
+
return !!(this.mod && typeof this.mod._romdev_run_pure === "function");
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
/** True when this core build exposes the interrupt block (the pure-call
|
|
1375
|
+
* primitive on cores without a separable CPU loop). */
|
|
1376
|
+
irqBlockSupported() {
|
|
1377
|
+
return !!(this.mod && typeof this.mod._romdev_irqblock_set === "function");
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
/** Suppress (or restore) interrupt DELIVERY to the active CPU. While
|
|
1381
|
+
* blocked, pending IRQ/NMI lines stay pending and no game handler can run
|
|
1382
|
+
* — the mechanism behind pure calls on cores whose CPU/video loops are
|
|
1383
|
+
* interleaved (everything except gpgx, which steps the CPU alone). */
|
|
1384
|
+
setIrqBlock(on) {
|
|
1385
|
+
const mod = this._needMod();
|
|
1386
|
+
if (typeof mod._romdev_irqblock_set !== "function") {
|
|
1387
|
+
throw new Error("this core build does not expose the interrupt block (rebuild with romdev_irqblock_set).");
|
|
1388
|
+
}
|
|
1389
|
+
mod._romdev_irqblock_set(on ? 1 : 0);
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
/** True when a pure call is possible on this platform by ANY mechanism:
|
|
1393
|
+
* a separable CPU run (gpgx), an interrupt block, or hardware with no
|
|
1394
|
+
* interrupts at all (the 2600's 6507 has no IRQ/NMI lines wired — every
|
|
1395
|
+
* call is inherently pure). */
|
|
1396
|
+
pureCallSupported() {
|
|
1397
|
+
return this.runPureSupported() || this.irqBlockSupported() || this.status.platform === "atari2600";
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
/** True when this core build exposes the VRAM-port copy trace (port-based
|
|
1401
|
+
* video memory: NES/SNES/PCE/MSX/SMS/GG/Genesis). Direct-mapped platforms
|
|
1402
|
+
* answer the same question through watchRange on the CPU-visible VRAM. */
|
|
1403
|
+
vramWatchSupported() {
|
|
1404
|
+
const mod = this.mod;
|
|
1405
|
+
return !!(mod && typeof mod._romdev_vramwatch_set === "function" && typeof mod._romdev_vramwatch_get === "function");
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
/**
|
|
1409
|
+
* Run `frames` frames logging every data-port write landing in the VRAM
|
|
1410
|
+
* address window [lo,hi] — {vramAddr, pc, value} per event, pc being the
|
|
1411
|
+
* EXECUTING instruction (during DMA on SNES, the instruction that triggered
|
|
1412
|
+
* it). The "where does this graphic come from?" primitive for port-based
|
|
1413
|
+
* video memory. Returns { events, total, stored, truncated }.
|
|
1414
|
+
*/
|
|
1415
|
+
watchVram(lo, hi, frames, perFrame) {
|
|
1416
|
+
const mod = this._needMod();
|
|
1417
|
+
this._needMedia();
|
|
1418
|
+
if (!this.vramWatchSupported()) throw new Error("VRAM copy trace not supported by this core.");
|
|
1419
|
+
mod._romdev_vramwatch_set(lo >>> 0, hi >>> 0, 1);
|
|
1420
|
+
try {
|
|
1421
|
+
this._runFramesExclusive(perFrame ?? (() => false), frames);
|
|
1422
|
+
} finally {
|
|
1423
|
+
// drained below; disarm after
|
|
1424
|
+
}
|
|
1425
|
+
const CAP = 1024;
|
|
1426
|
+
const outPtr = mod._malloc(CAP * 3 * 4);
|
|
1427
|
+
const out2Ptr = mod._malloc(8);
|
|
1428
|
+
try {
|
|
1429
|
+
const n = mod._romdev_vramwatch_get(outPtr, CAP, out2Ptr);
|
|
1430
|
+
const u = new Uint32Array(mod.HEAPU8.buffer, outPtr, n * 3);
|
|
1431
|
+
const u2 = new Uint32Array(mod.HEAPU8.buffer, out2Ptr, 2);
|
|
1432
|
+
const events = [];
|
|
1433
|
+
for (let i = 0; i < n; i++) {
|
|
1434
|
+
events.push({ vramAddr: u[i * 3], pc: u[i * 3 + 1], value: u[i * 3 + 2] & 0xFF });
|
|
1435
|
+
}
|
|
1436
|
+
return { events, total: u2[0], stored: u2[1], truncated: u2[0] > u2[1] };
|
|
1437
|
+
} finally {
|
|
1438
|
+
mod._free(outPtr);
|
|
1439
|
+
mod._free(out2Ptr);
|
|
1440
|
+
mod._romdev_vramwatch_set(0, 0, 0);
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1266
1444
|
/** Arm/disarm the read watchpoint on a CPU address. */
|
|
1267
1445
|
setReadWatch(address, enabled = true) {
|
|
1268
1446
|
const mod = this._needMod();
|
|
@@ -1488,6 +1666,13 @@ export class LibretroHost {
|
|
|
1488
1666
|
const {
|
|
1489
1667
|
pc, regs = {}, spReg = prof.spReg, pcReg = prof.pcReg, sentinelPC = prof.defaultSentinel,
|
|
1490
1668
|
sentinelBytes = prof.retBytes, maxFrames = 600, sandbox = true, capture,
|
|
1669
|
+
// pure: step ONLY the active CPU (no frame machinery — VDP lines, co-CPU,
|
|
1670
|
+
// interrupt raising). Without it, each "frame" of the call runs the
|
|
1671
|
+
// game's OWN per-frame logic concurrently (VBlank handlers via RAM
|
|
1672
|
+
// vectors etc.), which can stomp the buffer the driven routine is
|
|
1673
|
+
// writing — a real session diffed a CORRECT codec reimplementation
|
|
1674
|
+
// against that poisoned output for hours. gpgx (Genesis/SMS/GG) only.
|
|
1675
|
+
pure = false,
|
|
1491
1676
|
// presetMemory: [{addr, bytes}] CPU-space writes applied before the call
|
|
1492
1677
|
// (codecs that read a global from RAM — a dest stride, a mode flag, etc).
|
|
1493
1678
|
presetMemory = [],
|
|
@@ -1523,6 +1708,7 @@ export class LibretroHost {
|
|
|
1523
1708
|
|
|
1524
1709
|
const snapshot = sandbox ? this.serializeState() : null;
|
|
1525
1710
|
let captured, returned = false, framesRun = 0, watchdogTripped = false, stoppedAtPC = false;
|
|
1711
|
+
let pureMode = null;
|
|
1526
1712
|
try {
|
|
1527
1713
|
// Apply pre-call memory writes (CPU-space).
|
|
1528
1714
|
for (const m of presetMemory) {
|
|
@@ -1575,19 +1761,67 @@ export class LibretroHost {
|
|
|
1575
1761
|
const target = (stopAtPC !== undefined ? stopAtPC : sentinelPC) >>> 0;
|
|
1576
1762
|
this.setPCBreak(target, true, false);
|
|
1577
1763
|
let finalState = null;
|
|
1764
|
+
let irqBlocked = false;
|
|
1578
1765
|
try {
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1766
|
+
if (pure && this.runPureSupported()) {
|
|
1767
|
+
// STRONGEST pure mode (gpgx): step ONLY the CPU — no frame machinery
|
|
1768
|
+
// at all. Mask m68k interrupts so a PENDING VINT raised before the
|
|
1769
|
+
// call can't redirect entry (no NEW interrupts are raised — the
|
|
1770
|
+
// system loop never runs). The sandbox restore (or the game's own
|
|
1771
|
+
// RTE discipline) makes the IPL change invisible afterward.
|
|
1772
|
+
pureMode = "cpu-only";
|
|
1773
|
+
if (this.status.platform === "genesis") {
|
|
1774
|
+
this.setReg(17, (this.getReg(17) | 0x0700) & 0xFFFF);
|
|
1587
1775
|
}
|
|
1588
|
-
|
|
1589
|
-
|
|
1776
|
+
// Drive the CPU in cycle chunks, checking the sentinel/watchdog
|
|
1777
|
+
// between chunks. The watchdog bounds total instructions; the chunk
|
|
1778
|
+
// cap is only a backstop against a watchdog-less older core.
|
|
1779
|
+
const CHUNK_CYCLES = 1_000_000;
|
|
1780
|
+
const maxChunks = 512;
|
|
1781
|
+
for (let i = 0; i < maxChunks; i++) {
|
|
1782
|
+
this.mod._romdev_run_pure(CHUNK_CYCLES);
|
|
1783
|
+
const st = this.getPCBreak(false);
|
|
1784
|
+
if (st.hit) {
|
|
1785
|
+
finalState = st;
|
|
1786
|
+
if (st.watchdog) watchdogTripped = true;
|
|
1787
|
+
else if (stopAtPC !== undefined) stoppedAtPC = true;
|
|
1788
|
+
else returned = true;
|
|
1789
|
+
break;
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
} else {
|
|
1793
|
+
if (pure) {
|
|
1794
|
+
// INTERRUPT-BLOCKED pure mode (every other core): the frame
|
|
1795
|
+
// machinery still runs (video/timers advance — harmless, they
|
|
1796
|
+
// don't write game RAM), but interrupt DELIVERY is suppressed, so
|
|
1797
|
+
// no game handler can execute. The only running game code is the
|
|
1798
|
+
// routine we called — the same guarantee that matters for the
|
|
1799
|
+
// output buffer. The 2600's 6507 has no interrupt lines at all,
|
|
1800
|
+
// so every call there is pure by hardware.
|
|
1801
|
+
if (this.irqBlockSupported()) {
|
|
1802
|
+
this.setIrqBlock(true);
|
|
1803
|
+
irqBlocked = true;
|
|
1804
|
+
pureMode = "irq-blocked";
|
|
1805
|
+
} else if (this.status.platform === "atari2600") {
|
|
1806
|
+
pureMode = "no-interrupts";
|
|
1807
|
+
} else {
|
|
1808
|
+
throw new Error("cpu({op:'call', pure:true}) not supported by this core build (needs the romdev_irqblock_set export — update the core package).");
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
framesRun = this._runFramesExclusive(() => {
|
|
1812
|
+
const st = this.getPCBreak(false);
|
|
1813
|
+
if (st.hit) {
|
|
1814
|
+
finalState = st;
|
|
1815
|
+
if (st.watchdog) watchdogTripped = true;
|
|
1816
|
+
else if (stopAtPC !== undefined) stoppedAtPC = true;
|
|
1817
|
+
else returned = true;
|
|
1818
|
+
return true;
|
|
1819
|
+
}
|
|
1820
|
+
return false;
|
|
1821
|
+
}, maxFrames);
|
|
1822
|
+
}
|
|
1590
1823
|
} finally {
|
|
1824
|
+
if (irqBlocked) { try { this.setIrqBlock(false); } catch { /* core gone */ } }
|
|
1591
1825
|
this.setPCBreak(0, false, false);
|
|
1592
1826
|
this.setWatchdog(0);
|
|
1593
1827
|
if (!finalState) finalState = this.getPCBreak(true); else this.getPCBreak(true);
|
|
@@ -1607,6 +1841,7 @@ export class LibretroHost {
|
|
|
1607
1841
|
const fin = this._lastCallResult || {};
|
|
1608
1842
|
return {
|
|
1609
1843
|
returned, framesRun,
|
|
1844
|
+
...(pure ? { pure: true, pureMode } : {}),
|
|
1610
1845
|
...(watchdogTripped ? { watchdog: true, reason: "watchdog: hit the instruction budget (likely a runaway loop — wrong A0/regs, a needed preset, or legitimately huge; raise maxInstructions or check the entry setup)" } : {}),
|
|
1611
1846
|
...(stoppedAtPC ? { stoppedAtPC: "$" + (stopAtPC >>> 0).toString(16).toUpperCase() } : {}),
|
|
1612
1847
|
...(fin.finalPC != null ? { finalPC: "$" + fin.finalPC.toString(16).toUpperCase(), finalPCRaw: fin.finalPC } : {}),
|
package/src/mcp/server.js
CHANGED
|
@@ -418,6 +418,12 @@ async function main() {
|
|
|
418
418
|
log.info(`optional observer: http://${bannerHost}:${port}/livestream`);
|
|
419
419
|
log.info("");
|
|
420
420
|
log.info("connect your coding agent: https://github.com/monteslu/romdev#connect");
|
|
421
|
+
// One conditional line so an agent knows the constraint BEFORE promising a
|
|
422
|
+
// playtest window to a human (the op itself still errors with the full fix).
|
|
423
|
+
if (process.platform === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) {
|
|
424
|
+
log.info("");
|
|
425
|
+
log.info("note: no display detected (headless) — playtest({op:'open'}) is unavailable; all other tools work.");
|
|
426
|
+
}
|
|
421
427
|
});
|
|
422
428
|
const extraServers = [];
|
|
423
429
|
for (const h of bindHosts.slice(1)) {
|