romdevtools 0.43.0 → 0.56.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 +322 -0
- package/examples/n64/platformer/main.c +158 -0
- package/examples/n64/puzzle/main.c +117 -0
- package/examples/n64/racing/main.c +147 -0
- package/examples/n64/shmup/main.c +122 -0
- package/examples/n64/sports/main.c +127 -0
- package/examples/ps1/platformer/main.c +158 -0
- package/examples/ps1/puzzle/main.c +125 -0
- package/examples/ps1/racing/main.c +147 -0
- package/examples/ps1/shmup/main.c +192 -0
- package/examples/ps1/sports/main.c +127 -0
- package/examples/ps1/sprite_move/main.c +38 -0
- package/package.json +9 -2
- package/src/analysis/analyze.js +169 -29
- package/src/analysis/decompile.js +4 -0
- package/src/analysis/decompiler/sleigh/mips.ldefs +25 -0
- package/src/analysis/decompiler/sleigh/mips32.pspec +78 -0
- package/src/analysis/decompiler/sleigh/mips32be.cspec +107 -0
- package/src/analysis/decompiler/sleigh/mips32be.sla +211162 -0
- package/src/analysis/decompiler/sleigh/mips32le.cspec +106 -0
- package/src/analysis/decompiler/sleigh/mips32le.sla +210624 -0
- package/src/analysis/rizin.js +22 -1
- package/src/cores/capabilities.js +90 -2
- package/src/cores/registry.js +12 -0
- package/src/host/LibretroGL.js +270 -0
- package/src/host/LibretroGLBridge.js +836 -0
- package/src/host/LibretroHost.js +144 -3
- package/src/host/callbacks.js +18 -2
- package/src/host/coreLoader.js +49 -2
- package/src/host/cpu-state.js +27 -0
- package/src/host/framebuffer.js +14 -1
- package/src/host/glOptionalDep.js +60 -0
- package/src/host/n64-ai-state.js +43 -0
- package/src/host/ps1-spu-state.js +65 -0
- package/src/host/retroConstants.js +14 -0
- package/src/mcp/tools/cheats.js +73 -4
- package/src/mcp/tools/disasm.js +2 -0
- package/src/mcp/tools/frame.js +18 -9
- package/src/mcp/tools/index.js +12 -2
- package/src/mcp/tools/lifecycle.js +1 -1
- package/src/mcp/tools/platform-tools.js +20 -4
- package/src/mcp/tools/platforms.js +38 -4
- package/src/mcp/tools/project.js +100 -0
- package/src/mcp/tools/rendering-context.js +2 -1
- package/src/mcp/tools/toolchain.js +1 -1
- package/src/mcp/tools/watch-memory.js +93 -20
- package/src/platforms/n64/lib/c/n64.c +196 -0
- package/src/platforms/n64/lib/c/n64.h +68 -0
- package/src/platforms/ps1/lib/c/psx.c +200 -0
- package/src/platforms/ps1/lib/c/psx.h +83 -0
- package/src/toolchains/cc65/cc65.js +11 -0
- package/src/toolchains/index.js +35 -0
- package/src/toolchains/mips-c/lib/be/libc.a +0 -0
- package/src/toolchains/mips-c/lib/be/libgcc.a +0 -0
- package/src/toolchains/mips-c/lib/be/libm.a +0 -0
- package/src/toolchains/mips-c/lib/el/libc.a +0 -0
- package/src/toolchains/mips-c/lib/el/libm.a +0 -0
- package/src/toolchains/mips-c/lib/n64-crt0.s +21 -0
- package/src/toolchains/mips-c/lib/n64-ipl3.s +30 -0
- package/src/toolchains/mips-c/lib/n64.ld +15 -0
- package/src/toolchains/mips-c/lib/ps1-crt0.s +20 -0
- package/src/toolchains/mips-c/lib/ps1.ld +15 -0
- package/src/toolchains/mips-c/lib/softint.c +37 -0
- package/src/toolchains/mips-c/mips-c.js +155 -0
- package/src/toolchains/mips-elf-gcc/gcc.js +130 -0
|
@@ -206,6 +206,26 @@ export function makePressDriver(host, presses) {
|
|
|
206
206
|
};
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
+
/**
|
|
210
|
+
* Flush any held-input shadow before a driven run. Back-to-back `pressDuring`
|
|
211
|
+
* runs on the SAME live host can leak the PRIOR run's held button into the next
|
|
212
|
+
* run's frame 0 — the game latches the pad into its own RAM each frame, and the
|
|
213
|
+
* new run's frame-0 logic reads that stale chord before the new input propagates.
|
|
214
|
+
* That makes a negative control (hold A to prove A does NOT reach a B-only branch)
|
|
215
|
+
* FALSE-POSITIVE on frame 1. (v0.41.0 feedback 213831 #1.) Calling this first sets
|
|
216
|
+
* the pad neutral and steps `n` frames so the game's input shadow settles to
|
|
217
|
+
* neutral; then the schedule drives a clean run. Only steps when a press schedule
|
|
218
|
+
* is actually given (no-op otherwise, so it never disturbs an inherited pad).
|
|
219
|
+
* @param {import("../../host/index.js").LibretroHost} host
|
|
220
|
+
* @param {number} n neutral frames to settle (0 = skip)
|
|
221
|
+
* @param {boolean} driven whether this run has a pressDuring schedule
|
|
222
|
+
*/
|
|
223
|
+
function settleHeldInput(host, n, driven) {
|
|
224
|
+
if (!driven || !n || n <= 0) return;
|
|
225
|
+
host.setInput({ ports: [{}, {}] });
|
|
226
|
+
host.stepFrames(n | 0);
|
|
227
|
+
}
|
|
228
|
+
|
|
209
229
|
// Single source of truth: the same canonical region vocabulary readMemory uses
|
|
210
230
|
// (host/types.js). Derived from the host map so new regions flow through
|
|
211
231
|
// automatically and the two tools never disagree. Used by the ONE primary
|
|
@@ -598,7 +618,7 @@ export function registerWatchMemoryTools(server, z, sessionKey) {
|
|
|
598
618
|
|
|
599
619
|
// breakpoint({on:write|read|pc}) STOP-on-first. on:write precision:exact=bpFindWriter
|
|
600
620
|
// (core watchpoint, true PC under IRQ), precision:sampled=bpRunUntilWrite (frame PC).
|
|
601
|
-
async function bpFindWriter({ address, maxFrames = 600, pressDuring, abortIf, condition, conditionValue }) {
|
|
621
|
+
async function bpFindWriter({ address, maxFrames = 600, pressDuring, settleFrames = 0, abortIf, condition, conditionValue }) {
|
|
602
622
|
const host = getHost(sessionKey);
|
|
603
623
|
if (!host.watchpointSupported || !host.watchpointSupported()) {
|
|
604
624
|
return jsonContent({
|
|
@@ -616,10 +636,14 @@ export function registerWatchMemoryTools(server, z, sessionKey) {
|
|
|
616
636
|
// last write of the frame. Core support is feature-detected; if the loaded
|
|
617
637
|
// core build predates condition support, we fall back to a host-side
|
|
618
638
|
// 'equals' filter on the reported value (inc/dec need the core's old byte).
|
|
639
|
+
const presses0 = (pressDuring ?? []).slice().sort((a, b) => a.frame - b.frame);
|
|
640
|
+
// Flush a prior run's held input BEFORE arming, so a back-to-back driven run
|
|
641
|
+
// starts from a neutral pad (see settleHeldInput / 213831 #1).
|
|
642
|
+
settleHeldInput(host, settleFrames, presses0.length > 0);
|
|
619
643
|
const wantCond = condition != null;
|
|
620
644
|
const coreCond = host.setWatchpoint(address, true, wantCond ? { condition, value: conditionValue } : undefined);
|
|
621
645
|
const coreHandledCond = wantCond && coreCond && coreCond.conditionApplied === true;
|
|
622
|
-
const presses =
|
|
646
|
+
const presses = presses0;
|
|
623
647
|
const pressDriver = makePressDriver(host, presses);
|
|
624
648
|
// Abort-guard: sample caller-named "still valid?" bytes each frame; if any
|
|
625
649
|
// changes, a driven run that DERAILED (player died → title screen, scene
|
|
@@ -761,7 +785,7 @@ export function registerWatchMemoryTools(server, z, sessionKey) {
|
|
|
761
785
|
});
|
|
762
786
|
}
|
|
763
787
|
|
|
764
|
-
async function bpRunUntilPC({ address, maxFrames = 600, pressDuring, captureMemory }) {
|
|
788
|
+
async function bpRunUntilPC({ address, maxFrames = 600, pressDuring, settleFrames = 0, captureMemory }) {
|
|
765
789
|
const host = getHost(sessionKey);
|
|
766
790
|
if (!host.pcBreakSupported || !host.pcBreakSupported()) {
|
|
767
791
|
return jsonContent({
|
|
@@ -771,6 +795,10 @@ export function registerWatchMemoryTools(server, z, sessionKey) {
|
|
|
771
795
|
});
|
|
772
796
|
}
|
|
773
797
|
const presses = (pressDuring ?? []).slice().sort((a, b) => a.frame - b.frame);
|
|
798
|
+
// Flush a prior run's held-button shadow BEFORE arming (so the settle frames
|
|
799
|
+
// don't trip the breakpoint) — prevents a back-to-back negative control from
|
|
800
|
+
// false-positiving on frame 0. See settleHeldInput.
|
|
801
|
+
settleHeldInput(host, settleFrames, presses.length > 0);
|
|
774
802
|
const pressDriver = makePressDriver(host, presses);
|
|
775
803
|
host.setPCBreak(address, true, false);
|
|
776
804
|
let hit = false, framesRun = 0, last = null;
|
|
@@ -797,9 +825,13 @@ export function registerWatchMemoryTools(server, z, sessionKey) {
|
|
|
797
825
|
...(presses.length ? { pressesScheduled: presses.length, pressesApplied: pressDriver.applied() } : {}),
|
|
798
826
|
...(pcNow != null ? { pcNow: "$" + pcNow.toString(16).toUpperCase() } : {}),
|
|
799
827
|
note: (drove
|
|
800
|
-
? "PC never reached that address within maxFrames EVEN WITH the scheduled input
|
|
801
|
-
"
|
|
802
|
-
"
|
|
828
|
+
? "PC never reached that address within maxFrames EVEN WITH the scheduled input. Either (a) this is " +
|
|
829
|
+
"the WRONG ADDRESS for the path that actually ran (a different routine handles it), (b) the address " +
|
|
830
|
+
"isn't an instruction boundary (mid-instruction never matches REG_PC), OR (c) the condition " +
|
|
831
|
+
"LEGITIMATELY did not occur — i.e. this hit:false is the DESIRED result of a negative control " +
|
|
832
|
+
"(you're proving input X does NOT reach this branch; e.g. an A-vs-B discriminator where only the " +
|
|
833
|
+
"other button should hit). If you confirmed the address is reachable on the positive run, (c) is " +
|
|
834
|
+
"the expected outcome, not a failure. "
|
|
803
835
|
: "PC never reached that address within maxFrames. Either the code path didn't execute (drive " +
|
|
804
836
|
"it with pressDuring to reach the right game state), or the address isn't an instruction " +
|
|
805
837
|
"boundary (mid-instruction never matches REG_PC). ") +
|
|
@@ -1091,6 +1123,7 @@ export function registerWatchMemoryTools(server, z, sessionKey) {
|
|
|
1091
1123
|
port: z.number().int().min(0).max(3).default(0),
|
|
1092
1124
|
holdFrames: z.number().int().min(1).default(2),
|
|
1093
1125
|
})).optional().describe("Schedule input while waiting (drive the game to the state that triggers the condition). If OMITTED, this run inherits whatever input({op:'set'}) last held — same as frame({op:'step'}). If GIVEN, the schedule OWNS the pad for the whole run (a prior input({op:'set'}) is ignored); use it to drive the watched window itself. Entries with OVERLAPPING windows on the same port are OR'd into a chord (e.g. b+right held while a fires mid-window), not overwritten."),
|
|
1126
|
+
settleFrames: z.number().int().min(0).max(120).default(0).describe("on:'pc'/'write' with pressDuring — release the pad to NEUTRAL and step this many frames BEFORE the run, so the PRIOR run's held-button shadow (the game latches the pad into its own RAM each frame) doesn't bleed into this run's frame 0. Set ~10-30 for back-to-back A/B-discriminator / negative-control runs on the same live host (hold A to prove A does NOT reach a B-only branch) — without it the stale chord can false-positive on frame 1. No-op without pressDuring."),
|
|
1094
1127
|
abortIf: z.array(z.object({
|
|
1095
1128
|
region: regionStr("memory region (default system_ram)").optional(),
|
|
1096
1129
|
offset: z.number().int().min(0).describe("byte offset within the region"),
|
|
@@ -1295,7 +1328,7 @@ export function registerWatchMemoryTools(server, z, sessionKey) {
|
|
|
1295
1328
|
|
|
1296
1329
|
// ── Range watch + coverage trace (item 2, discovery) ────────────────────────
|
|
1297
1330
|
|
|
1298
|
-
async function wRange({ start, end, kind = "both", frames = 120, pressDuring, limit = 200, fromState, fromStatePath }) {
|
|
1331
|
+
async function wRange({ start, end, kind = "both", frames = 120, pressDuring, limit = 200, dedupe = false, distinctPCsOnly = false, fromState, fromStatePath }) {
|
|
1299
1332
|
const host = getHost(sessionKey);
|
|
1300
1333
|
if (!host.rangeWatchSupported || !host.rangeWatchSupported()) {
|
|
1301
1334
|
return jsonContent({ notSupported: true, events: [],
|
|
@@ -1313,20 +1346,59 @@ export function registerWatchMemoryTools(server, z, sessionKey) {
|
|
|
1313
1346
|
if (presses.length) pressDriver.applyForFrame(0);
|
|
1314
1347
|
const r = host.watchRange(start, end, kind, frames);
|
|
1315
1348
|
pressDriver.finish();
|
|
1316
|
-
const
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
//
|
|
1322
|
-
|
|
1323
|
-
|
|
1349
|
+
const hx = (n, w = 0) => "$" + n.toString(16).toUpperCase().padStart(w, "0");
|
|
1350
|
+
const hxv = (n) => "0x" + n.toString(16).toUpperCase().padStart(2, "0");
|
|
1351
|
+
|
|
1352
|
+
// Per-PC digest — the actionable "which routines touch this range" answer.
|
|
1353
|
+
// Each writer's hit count + a sample address/value, sorted by frequency. This
|
|
1354
|
+
// is what a "who writes here?" query actually needs; the per-event flood (a
|
|
1355
|
+
// per-frame counter inc'd at one PC → hundreds of near-identical rows) is
|
|
1356
|
+
// ~95% wasted tokens for that question. (v0.41.0 feedback 133737 N1.)
|
|
1357
|
+
const byPC = new Map();
|
|
1358
|
+
for (const e of r.events) {
|
|
1359
|
+
let g = byPC.get(e.pc);
|
|
1360
|
+
if (!g) { g = { pc: e.pc, count: 0, sampleAddress: e.address, sampleValue: e.value }; byPC.set(e.pc, g); }
|
|
1361
|
+
g.count++;
|
|
1362
|
+
}
|
|
1363
|
+
const byPCList = [...byPC.values()].sort((a, b) => b.count - a.count).slice(0, 64)
|
|
1364
|
+
.map((g) => ({ pc: hx(g.pc), count: g.count, sampleAddress: hx(g.sampleAddress), sampleValue: hxv(g.sampleValue) }));
|
|
1365
|
+
const distinctPCs = byPCList.map((g) => g.pc);
|
|
1366
|
+
|
|
1367
|
+
// distinctPCsOnly → return JUST the digest, suppress the raw event list (the
|
|
1368
|
+
// common "discover the writers" use; no per-event tokens spent).
|
|
1369
|
+
if (distinctPCsOnly) {
|
|
1370
|
+
return attachObserverFrame(jsonContent({
|
|
1371
|
+
range: hx(start) + ".." + hx(end), kind, total: r.total, truncated: r.truncated,
|
|
1372
|
+
...(stateInfo ? { restoredFrom: stateInfo } : {}),
|
|
1373
|
+
distinctPCs, byPC: byPCList,
|
|
1374
|
+
note: "distinctPCsOnly: per-PC digest only (raw events suppressed). Each PC is a routine that touches the range; `count` is how often it fired, `sampleAddress`/`sampleValue` a representative hit. disasm({target:'rom'}) a PC to identify it. Drop distinctPCsOnly (or set dedupe:true) for the events." +
|
|
1375
|
+
(r.truncated ? " TRUNCATED: more events than the buffer held — narrow start..end/frames." : ""),
|
|
1376
|
+
}), host);
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
// dedupe → collapse identical (pc,address,value) rows to one with an
|
|
1380
|
+
// `occurrences` count, so a byte written the same way every frame is 1 row
|
|
1381
|
+
// not hundreds (parity with on:'dma's dedupe).
|
|
1382
|
+
let events;
|
|
1383
|
+
if (dedupe) {
|
|
1384
|
+
const seen = new Map();
|
|
1385
|
+
for (const e of r.events) {
|
|
1386
|
+
const k = `${e.pc}|${e.address}|${e.value}`;
|
|
1387
|
+
const g = seen.get(k);
|
|
1388
|
+
if (g) g.occurrences++;
|
|
1389
|
+
else seen.set(k, { pc: hx(e.pc), address: hx(e.address), value: hxv(e.value), occurrences: 1 });
|
|
1390
|
+
}
|
|
1391
|
+
events = [...seen.values()].sort((a, b) => b.occurrences - a.occurrences).slice(0, limit);
|
|
1392
|
+
} else {
|
|
1393
|
+
events = r.events.slice(0, limit).map((e) => ({ pc: hx(e.pc), address: hx(e.address), value: hxv(e.value) }));
|
|
1394
|
+
}
|
|
1324
1395
|
return attachObserverFrame(jsonContent({
|
|
1325
|
-
range:
|
|
1396
|
+
range: hx(start) + ".." + hx(end),
|
|
1326
1397
|
kind, total: r.total, returned: events.length, truncated: r.truncated,
|
|
1398
|
+
...(dedupe ? { deduped: true, uniqueEvents: events.length } : {}),
|
|
1327
1399
|
...(stateInfo ? { restoredFrom: stateInfo } : {}),
|
|
1328
|
-
distinctPCs, events,
|
|
1329
|
-
note: "distinctPCs is the actionable summary — each is a routine that touches this range; disasm({target:'rom'}) one to identify the renderer/reader. " +
|
|
1400
|
+
distinctPCs, byPC: byPCList, events,
|
|
1401
|
+
note: "distinctPCs/byPC is the actionable summary — each PC is a routine that touches this range; disasm({target:'rom'}) one to identify the renderer/reader. For a 'who writes here?' query, distinctPCsOnly:true returns JUST the digest (no per-event flood); dedupe:true collapses per-frame churn to unique (pc,address,value) rows with `occurrences`. " +
|
|
1330
1402
|
(r.truncated ? "TRUNCATED: more events than the buffer held — narrow `start..end` or `frames` for the full set." : ""),
|
|
1331
1403
|
}), host);
|
|
1332
1404
|
}
|
|
@@ -1361,7 +1433,7 @@ export function registerWatchMemoryTools(server, z, sessionKey) {
|
|
|
1361
1433
|
"• on:'mem' — the power tool: answer 'what code is touching this RAM byte?' OR extract a frame-accurate event timeline (music-driver note onsets, physics arcs). Reports every frame that changed a watched byte as {frame,offset,before,after,pc}. " +
|
|
1362
1434
|
"Extras: `ranges:[{region,offset,length,label}]` watches MANY disjoint regions in ONE pass (identical frames); `onChange:'reset'|'increase'|'decrease'|'any'` edge filter (reset = counter-reload = the note-onset signal); `valueFilter:{min,max}`; `format:'series'` = compact columnar value-vs-frame curve (~10× smaller for a ramp); `sampleEvery`; `groupByPC` (collapse by sampled PC); `cheatLabels` (auto-name addresses from the cheat DB); `outputPath` streams all events as NDJSON; `stopOnFirst` exits on the first match. " +
|
|
1363
1435
|
"**CAVEAT: frame-level, not instruction-level (last value per frame); the sampled `pc` is a frame-boundary sample — for ISR-driven writes use breakpoint({on:'write', precision:'exact'}) for the real writer.**\n" +
|
|
1364
|
-
"• on:'range' — DISCOVERY: log EVERY instruction that reads or writes ANYWHERE in [start,end]. The fix for 'I don't know which PC touches this'. Returns {pc,address,value}[] + the actionable distinctPCs. (Ring-buffered: `truncated:true` if it overflows.) `fromState`/`fromStatePath` restores a savestate FIRST so the trace runs from a known moment (jump to the boss, then see what writes HP) — deterministic + repeatable.\n" +
|
|
1436
|
+
"• on:'range' — DISCOVERY: log EVERY instruction that reads or writes ANYWHERE in [start,end]. The fix for 'I don't know which PC touches this'. Returns {pc,address,value}[] + the actionable distinctPCs + a per-PC digest (byPC). For a pure 'who writes here?' query, `distinctPCsOnly:true` returns JUST the digest (no per-event flood — a per-frame counter inc'd at one PC otherwise floods hundreds of near-identical rows); `dedupe:true` collapses identical (pc,address,value) events to one row with `occurrences`. (Ring-buffered: `truncated:true` if it overflows.) `fromState`/`fromStatePath` restores a savestate FIRST so the trace runs from a known moment (jump to the boss, then see what writes HP) — deterministic + repeatable.\n" +
|
|
1365
1437
|
"• on:'pc' — DISCOVERY (coverage trace): record every DISTINCT PC executed within [start,end] — 'what code runs here?'. Log execution in the bank where you suspect the renderer lives during the moment it draws, then disassemble the PCs. Also takes `fromState`/`fromStatePath` to trace from a restored moment.\n" +
|
|
1366
1438
|
"• on:'dma' — GENESIS ONLY: trace mem→VDP DMAs (the answer to 'this name/portrait/logo is a pre-rendered bitmap DMA'd into VRAM — WHERE in ROM?', which on:'write' can't catch). `precision:'exact'` (default) logs every mem→VDP DMA with its VRAM DESTINATION + ROM SOURCE + length (filter by `vramDest`±`destWindow`; `dedupe` collapses the per-frame refresh; `sourceFilter:'rom-only'` drops RAM→VRAM noise; catches a same-frame second DMA). `precision:'sampled'` is the cheap frame-sampled source-register read (may miss two DMAs in one frame, dest-agnostic). `perFrame:true` switches to FEEL/PERF MODE: a per-frame timeline of VDP-DMA WORK ({frame,dmas,bytes,romBytes,ramBytes} + peakFrame + `spikes`) — the cheap 'why does horizontal movement feel choppy?' diagnostic (a per-frame byte spike = too much VDP work in the loop, e.g. a tilemap rewrite). On non-Genesis cores returns `notSupported`.\n" +
|
|
1367
1439
|
"• on:'copy' — ALL 14 PLATFORMS: log every write landing in a VRAM/dest address window [start,end] with the EXECUTING instruction's PC — the generic answer to 'this tile/nametable/portrait on screen: which routine uploads it?'. Port-based video memory (NES $2007, SNES $2118/19 — incl. the DMA path, PCE VWR, MSX/SMS/GG VDP data port, Genesis data port) is hooked INSIDE the core, so `start`/`end` are VRAM addresses (NES PPU $0000-$3FFF; SNES VRAM byte addr; PCE VRAM word addr; MSX/SMS/GG VRAM addr). Direct-mapped platforms (GB/GBC $8000-$9FFF, GBA 0x06000000+, C64/Lynx/7800 RAM framebuffers) route through the CPU-address range log automatically — pass CPU addresses there. Follow up with breakpoint({on:'pc', address: pc}) to get registersAtHit at the uploader.",
|
|
@@ -1385,6 +1457,7 @@ export function registerWatchMemoryTools(server, z, sessionKey) {
|
|
|
1385
1457
|
stopOnFirst: z.boolean().default(false).describe("on:'mem' — stop on the first filter-passing change instead of running the full duration. (For a true stop-on-first breakpoint, prefer the `breakpoint` tool.)"),
|
|
1386
1458
|
// on:'range'
|
|
1387
1459
|
kind: z.enum(["read", "write", "both"]).default("both").describe("on:'range' — watch reads, writes, or both."),
|
|
1460
|
+
distinctPCsOnly: z.boolean().default(false).describe("on:'range' — return JUST the per-PC digest (distinctPCs + byPC[{pc,count,sampleAddress,sampleValue}]) and SUPPRESS the raw event list. The token-cheap form of the common 'which routines touch this range?' query — a per-frame counter inc'd at one PC floods hundreds of near-identical events otherwise."),
|
|
1388
1461
|
// on:'range' / on:'pc' window
|
|
1389
1462
|
start: z.number().int().min(0).optional().describe("on:'range'/'pc' — low CPU address of the window."),
|
|
1390
1463
|
end: z.number().int().min(0).optional().describe("on:'range'/'pc' — high CPU address (inclusive)."),
|
|
@@ -1397,7 +1470,7 @@ export function registerWatchMemoryTools(server, z, sessionKey) {
|
|
|
1397
1470
|
precision: z.enum(["exact", "sampled"]).default("exact").describe("on:'dma' — exact=per-DMA core log with VRAM dest + ROM source (catches same-frame DMAs); sampled=frame-sampled source-register read (cheaper, may miss two DMAs in one frame, dest-agnostic). Ignored when perFrame:true."),
|
|
1398
1471
|
vramDest: z.number().int().min(0).optional().describe("on:'dma' precision:'exact' — keep only DMAs whose VRAM destination is within ±`destWindow` of this address."),
|
|
1399
1472
|
destWindow: z.number().int().min(0).default(0x40).describe("on:'dma' precision:'exact' — match window around vramDest (default 64 bytes ≈ 1 tile)."),
|
|
1400
|
-
dedupe: z.boolean().
|
|
1473
|
+
dedupe: z.boolean().optional().describe("Collapse identical events to one entry with an `occurrences` count. on:'dma' precision:'exact' — identical DMAs (same dest+source+length+code), DEFAULT ON. on:'range' — identical (pc,address,value) writes, DEFAULT OFF (turns per-frame churn from hundreds of rows into a few)."),
|
|
1401
1474
|
sourceFilter: z.enum(["all", "rom-only", "ram-only"]).default("all").describe("on:'dma' precision:'exact' — 'rom-only' drops the RAM→VRAM per-frame refresh noise; 'ram-only' keeps only it."),
|
|
1402
1475
|
romPreviewBytes: z.number().int().min(0).max(64).default(0).describe("on:'dma' — bytes of the ROM source to preview per DMA (exact default 0; sampled default 16)."),
|
|
1403
1476
|
minLengthBytes: z.number().int().min(0).max(65536).default(0).describe("on:'dma' precision:'sampled' — ignore DMAs shorter than this many bytes (filters tiny scroll/sprite updates so graphic uploads stand out)."),
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/* n64.c — N64 helpers + software 3D pipeline (see n64.h). Framebuffer backend. */
|
|
2
|
+
#include "n64.h"
|
|
3
|
+
|
|
4
|
+
/* Two 320x240x16bpp framebuffers in RDRAM (double-buffered), cached kseg0. */
|
|
5
|
+
#define FB0 0xA0100000u /* uncached kseg1 — writes go straight to RDRAM */
|
|
6
|
+
#define FB1 0xA0120000u
|
|
7
|
+
static volatile unsigned short *fb; /* the back buffer we draw into */
|
|
8
|
+
static unsigned int front;
|
|
9
|
+
|
|
10
|
+
/* VI registers (0xA4400000). */
|
|
11
|
+
#define VI(n) (*(volatile unsigned int*)(0xA4400000u + (n)*4))
|
|
12
|
+
#define VI_STATUS 0
|
|
13
|
+
#define VI_ORIGIN 1
|
|
14
|
+
#define VI_WIDTH 2
|
|
15
|
+
#define VI_V_CURRENT 4
|
|
16
|
+
#define VI_H_START 9
|
|
17
|
+
#define VI_V_START 10
|
|
18
|
+
#define VI_X_SCALE 12
|
|
19
|
+
#define VI_Y_SCALE 13
|
|
20
|
+
|
|
21
|
+
void n64_init(void)
|
|
22
|
+
{
|
|
23
|
+
/* NTSC 320x240 16bpp — register indices per the N64 VI map:
|
|
24
|
+
0=STATUS 1=ORIGIN 2=WIDTH 3=V_INTR 4=CURRENT 5=BURST 6=V_SYNC 7=H_SYNC
|
|
25
|
+
8=LEAP 9=H_START 10=V_START 11=V_BURST 12=X_SCALE 13=Y_SCALE. */
|
|
26
|
+
VI(0) = 0x0000320E; /* STATUS: 16bpp(type=2) + AA + dither + pixel_advance */
|
|
27
|
+
VI(2) = 320; /* WIDTH */
|
|
28
|
+
VI(3) = 2; /* V_INTR */
|
|
29
|
+
VI(4) = 0; /* CURRENT */
|
|
30
|
+
VI(5) = 0x03E52239; /* BURST (NTSC) */
|
|
31
|
+
VI(6) = 0x0000020D; /* V_SYNC = 525 */
|
|
32
|
+
VI(7) = 0x00000C15; /* H_SYNC */
|
|
33
|
+
VI(8) = 0x0C150C15; /* LEAP */
|
|
34
|
+
VI(9) = 0x006C02EC; /* H_START: 108..748 */
|
|
35
|
+
VI(10) = 0x002501FF; /* V_START: 37..511 */
|
|
36
|
+
VI(11) = 0x000E0204; /* V_BURST */
|
|
37
|
+
VI(12) = 0x00000200; /* X_SCALE (320) */
|
|
38
|
+
VI(13) = 0x00000400; /* Y_SCALE (240) */
|
|
39
|
+
front = FB0;
|
|
40
|
+
fb = (volatile unsigned short *)FB1;
|
|
41
|
+
VI(1) = FB0 & 0x1FFFFFFF; /* ORIGIN */
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
static inline void putpx(int x, int y, unsigned short c)
|
|
45
|
+
{
|
|
46
|
+
if ((unsigned)x < SCREEN_W && (unsigned)y < SCREEN_H) fb[y * SCREEN_W + x] = c;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
void n64_clear(unsigned short col)
|
|
50
|
+
{
|
|
51
|
+
int i; for (i = 0; i < SCREEN_W * SCREEN_H; i++) fb[i] = col;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
void n64_rect(int x, int y, int w, int h, unsigned short col)
|
|
55
|
+
{
|
|
56
|
+
int r, c;
|
|
57
|
+
for (r = 0; r < h; r++) for (c = 0; c < w; c++) putpx(x + c, y + r, col);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* edge function for the rasterizer */
|
|
61
|
+
static inline int edge(int ax, int ay, int bx, int by, int cx, int cy)
|
|
62
|
+
{ return (bx - ax) * (cy - ay) - (by - ay) * (cx - ax); }
|
|
63
|
+
|
|
64
|
+
void n64_tri2d(int x0,int y0,int x1,int y1,int x2,int y2,unsigned short col)
|
|
65
|
+
{
|
|
66
|
+
int minx = x0, maxx = x0, miny = y0, maxy = y0, px, py, w0, w1, w2, area;
|
|
67
|
+
if (x1 < minx) minx = x1; if (x1 > maxx) maxx = x1;
|
|
68
|
+
if (x2 < minx) minx = x2; if (x2 > maxx) maxx = x2;
|
|
69
|
+
if (y1 < miny) miny = y1; if (y1 > maxy) maxy = y1;
|
|
70
|
+
if (y2 < miny) miny = y2; if (y2 > maxy) maxy = y2;
|
|
71
|
+
if (minx < 0) minx = 0; if (miny < 0) miny = 0;
|
|
72
|
+
if (maxx >= SCREEN_W) maxx = SCREEN_W - 1; if (maxy >= SCREEN_H) maxy = SCREEN_H - 1;
|
|
73
|
+
area = edge(x0, y0, x1, y1, x2, y2);
|
|
74
|
+
if (area == 0) return;
|
|
75
|
+
for (py = miny; py <= maxy; py++) {
|
|
76
|
+
for (px = minx; px <= maxx; px++) {
|
|
77
|
+
w0 = edge(x1, y1, x2, y2, px, py);
|
|
78
|
+
w1 = edge(x2, y2, x0, y0, px, py);
|
|
79
|
+
w2 = edge(x0, y0, x1, y1, px, py);
|
|
80
|
+
if (area > 0) { if (w0 >= 0 && w1 >= 0 && w2 >= 0) fb[py * SCREEN_W + px] = col; }
|
|
81
|
+
else { if (w0 <= 0 && w1 <= 0 && w2 <= 0) fb[py * SCREEN_W + px] = col; }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
void n64_flip(void)
|
|
87
|
+
{
|
|
88
|
+
/* swap buffers + point VI at the freshly drawn one, then wait for a frame. */
|
|
89
|
+
unsigned int newfront = (unsigned int)(unsigned long)fb;
|
|
90
|
+
VI(1) = newfront & 0x1FFFFFFF;
|
|
91
|
+
fb = (volatile unsigned short *)(front);
|
|
92
|
+
front = newfront;
|
|
93
|
+
{ volatile int i; for (i = 0; i < 200000; i++) { } }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* ── input: read controller port 0 via the SI/PIF. The PIF command 0x01 polls
|
|
97
|
+
the pad; the 16-bit button word is at PIF RAM. We use the simple JoyBus poll. ── */
|
|
98
|
+
static unsigned int read_pad(void)
|
|
99
|
+
{
|
|
100
|
+
volatile unsigned int *pif = (volatile unsigned int *)0xBFC007C0; /* PIF RAM */
|
|
101
|
+
volatile unsigned int *si = (volatile unsigned int *)0xA4800000;
|
|
102
|
+
unsigned int buttons;
|
|
103
|
+
/* command block: read controller 0 (1 byte cmd 0x01, 1 byte send, ...) */
|
|
104
|
+
pif[0] = 0xFF010401; pif[1] = 0xFFFFFFFF; pif[2] = 0xFFFFFFFF;
|
|
105
|
+
pif[3] = 0xFE000000; pif[4] = 0; pif[5] = 0; pif[6] = 0; pif[7] = 1;
|
|
106
|
+
si[1] = 0x1FC007C0; /* SI_PIF_ADDR_RD64B: kick the PIF read */
|
|
107
|
+
{ volatile int t; for (t = 0; t < 5000; t++) { } }
|
|
108
|
+
buttons = pif[1] >> 16; /* the button half-word */
|
|
109
|
+
return (~0u) & buttons; /* JoyBus buttons are active-high already */
|
|
110
|
+
}
|
|
111
|
+
unsigned int n64_pad(void) { return read_pad(); }
|
|
112
|
+
int n64_pressed(unsigned int mask) { return (n64_pad() & mask) ? 1 : 0; }
|
|
113
|
+
|
|
114
|
+
/* ── trig (shared with PS1 lib: 256-step binary angle) ── */
|
|
115
|
+
static const short SINTAB[64] = {
|
|
116
|
+
0,804,1608,2410,3212,4011,4808,5602,6393,7179,7962,8739,9512,10278,11039,11793,
|
|
117
|
+
12539,13279,14010,14732,15446,16151,16846,17530,18204,18868,19519,20159,20787,21403,
|
|
118
|
+
22005,22594,23170,23731,24279,24811,25329,25832,26319,26790,27245,27683,28105,28510,
|
|
119
|
+
28898,29268,29621,29956,30273,30571,30852,31113,31356,31580,31785,31971,32137,32285,
|
|
120
|
+
32412,32521,32609,32678,32728,32757 };
|
|
121
|
+
static fix sin_lut(int idx) {
|
|
122
|
+
int q = (idx >> 6) & 3, p = idx & 63; int v;
|
|
123
|
+
if (q == 0) v = SINTAB[p]; else if (q == 1) v = SINTAB[63 - p];
|
|
124
|
+
else if (q == 2) v = -SINTAB[p]; else v = -SINTAB[63 - p];
|
|
125
|
+
return ((fix)v << 16) / 32768;
|
|
126
|
+
}
|
|
127
|
+
fix n64_sin(fix a) { return sin_lut((F2I(a)) & 255); }
|
|
128
|
+
fix n64_cos(fix a) { return sin_lut((F2I(a) + 64) & 255); }
|
|
129
|
+
|
|
130
|
+
/* ── camera + model (identical to the PS1 lib) ── */
|
|
131
|
+
static fix cam_x, cam_y, cam_z, cam_cy, cam_sy, cam_cp, cam_sp;
|
|
132
|
+
static fix mdl_x, mdl_y, mdl_z, mdl_cy, mdl_sy;
|
|
133
|
+
void n64_camera(fix ex, fix ey, fix ez, fix yaw, fix pitch)
|
|
134
|
+
{ cam_x=ex;cam_y=ey;cam_z=ez; cam_cy=n64_cos(yaw);cam_sy=n64_sin(yaw); cam_cp=n64_cos(pitch);cam_sp=n64_sin(pitch); }
|
|
135
|
+
void n64_model(fix tx, fix ty, fix tz, fix yaw)
|
|
136
|
+
{ mdl_x=tx;mdl_y=ty;mdl_z=tz; mdl_cy=n64_cos(yaw);mdl_sy=n64_sin(yaw); }
|
|
137
|
+
|
|
138
|
+
static Vec3 to_cam(Vec3 v)
|
|
139
|
+
{
|
|
140
|
+
fix mx = FMUL(v.x, mdl_cy) + FMUL(v.z, mdl_sy);
|
|
141
|
+
fix mz = -FMUL(v.x, mdl_sy) + FMUL(v.z, mdl_cy);
|
|
142
|
+
fix wx = mx + mdl_x, wy = v.y + mdl_y, wz = mz + mdl_z;
|
|
143
|
+
fix rx = wx - cam_x, ry = wy - cam_y, rz = wz - cam_z;
|
|
144
|
+
fix cx = FMUL(rx, cam_cy) - FMUL(rz, cam_sy);
|
|
145
|
+
fix cz = FMUL(rx, cam_sy) + FMUL(rz, cam_cy);
|
|
146
|
+
fix cy = FMUL(ry, cam_cp) - FMUL(cz, cam_sp);
|
|
147
|
+
fix cz2 = FMUL(ry, cam_sp) + FMUL(cz, cam_cp);
|
|
148
|
+
Vec3 o; o.x = cx; o.y = cy; o.z = cz2; return o;
|
|
149
|
+
}
|
|
150
|
+
#define NEAR FIXF(0.5f)
|
|
151
|
+
#define FOV FIX(220)
|
|
152
|
+
static int project(Vec3 c, int *sx, int *sy)
|
|
153
|
+
{
|
|
154
|
+
if (c.z <= NEAR) return 0;
|
|
155
|
+
*sx = (SCREEN_W / 2) + F2I(FDIV(FMUL(c.x, FOV), c.z));
|
|
156
|
+
*sy = (SCREEN_H / 2) - F2I(FDIV(FMUL(c.y, FOV), c.z));
|
|
157
|
+
return 1;
|
|
158
|
+
}
|
|
159
|
+
void n64_tri3d(Vec3 a, Vec3 b, Vec3 c, unsigned short col)
|
|
160
|
+
{
|
|
161
|
+
Vec3 ca=to_cam(a), cb=to_cam(b), cc=to_cam(c);
|
|
162
|
+
int x0,y0,x1,y1,x2,y2;
|
|
163
|
+
if (!project(ca,&x0,&y0)||!project(cb,&x1,&y1)||!project(cc,&x2,&y2)) return;
|
|
164
|
+
if ((x1-x0)*(y2-y0)-(x2-x0)*(y1-y0) <= 0) return; /* back-face cull */
|
|
165
|
+
n64_tri2d(x0,y0,x1,y1,x2,y2,col);
|
|
166
|
+
}
|
|
167
|
+
void n64_quad3d(Vec3 a, Vec3 b, Vec3 c, Vec3 d, unsigned short col)
|
|
168
|
+
{ n64_tri3d(a,b,c,col); n64_tri3d(a,c,d,col); }
|
|
169
|
+
void n64_tri3d_nc(Vec3 a, Vec3 b, Vec3 c, unsigned short col)
|
|
170
|
+
{
|
|
171
|
+
Vec3 ca=to_cam(a), cb=to_cam(b), cc=to_cam(c);
|
|
172
|
+
int x0,y0,x1,y1,x2,y2;
|
|
173
|
+
if (!project(ca,&x0,&y0)||!project(cb,&x1,&y1)||!project(cc,&x2,&y2)) return;
|
|
174
|
+
n64_tri2d(x0,y0,x1,y1,x2,y2,col);
|
|
175
|
+
}
|
|
176
|
+
void n64_quad3d_nc(Vec3 a, Vec3 b, Vec3 c, Vec3 d, unsigned short col)
|
|
177
|
+
{ n64_tri3d_nc(a,b,c,col); n64_tri3d_nc(a,c,d,col); }
|
|
178
|
+
|
|
179
|
+
/* ── RNG ── */
|
|
180
|
+
static unsigned int rng = 0x12345678u;
|
|
181
|
+
void n64_srand(unsigned int s) { rng = s ? s : 1; }
|
|
182
|
+
unsigned int n64_rand(void) { unsigned int x=rng; x^=x<<13; x^=x>>17; x^=x<<5; return rng=x; }
|
|
183
|
+
|
|
184
|
+
/* ── HUD number (3x5 cells scaled) ── */
|
|
185
|
+
static const unsigned char DIG[10][5] = {
|
|
186
|
+
{0x7,0x5,0x5,0x5,0x7},{0x2,0x6,0x2,0x2,0x7},{0x7,0x1,0x7,0x4,0x7},{0x7,0x1,0x3,0x1,0x7},
|
|
187
|
+
{0x5,0x5,0x7,0x1,0x1},{0x7,0x4,0x7,0x1,0x7},{0x7,0x4,0x7,0x5,0x7},{0x7,0x1,0x1,0x1,0x1},
|
|
188
|
+
{0x7,0x5,0x7,0x5,0x7},{0x7,0x5,0x7,0x1,0x7} };
|
|
189
|
+
static void digit(int x,int y,int d,unsigned short col)
|
|
190
|
+
{ int r,c; for(r=0;r<5;r++)for(c=0;c<3;c++) if(DIG[d][r]&(1<<(2-c))) n64_rect(x+c*3,y+r*3,3,3,col); }
|
|
191
|
+
void n64_number(int x, int y, unsigned int value, unsigned short col)
|
|
192
|
+
{
|
|
193
|
+
char buf[10]; int n=0,i;
|
|
194
|
+
if(!value)buf[n++]=0; while(value&&n<10){buf[n++]=value%10;value/=10;}
|
|
195
|
+
for(i=0;i<n;i++) digit(x+(n-1-i)*12,y,buf[i],col);
|
|
196
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/* n64.h — N64 (R4300) helper lib with a software 3D pipeline, for romdev.
|
|
2
|
+
*
|
|
3
|
+
* The N64 is a 3D machine; this is a real software 3D engine (identical fixed-point
|
|
4
|
+
* math to the PS1 lib, so games port directly) with an N64 framebuffer backend: a
|
|
5
|
+
* 320x240 16-bit (RGBA5551) framebuffer in RDRAM that the Video Interface scans out,
|
|
6
|
+
* and a software triangle rasterizer (the RDP is far more complex; direct FB writes
|
|
7
|
+
* are portable + reliable). Big-endian.
|
|
8
|
+
*
|
|
9
|
+
* Build: build({ platform:"n64", language:"c" }) → a self-booting .z64 (our clean
|
|
10
|
+
* IPL3 DMAs the game to RDRAM and jumps in). main() loops forever.
|
|
11
|
+
*/
|
|
12
|
+
#ifndef ROMDEV_N64_H
|
|
13
|
+
#define ROMDEV_N64_H
|
|
14
|
+
|
|
15
|
+
typedef int fix; /* 16.16 */
|
|
16
|
+
#define FIX(n) ((fix)((n) << 16))
|
|
17
|
+
#define FIXF(f) ((fix)((f) * 65536.0f))
|
|
18
|
+
#define FMUL(a,b) ((fix)(((long long)(a) * (b)) >> 16))
|
|
19
|
+
#define FDIV(a,b) ((fix)(((long long)(a) << 16) / (b)))
|
|
20
|
+
#define F2I(a) ((a) >> 16)
|
|
21
|
+
|
|
22
|
+
typedef struct { fix x, y, z; } Vec3;
|
|
23
|
+
|
|
24
|
+
#define SCREEN_W 320
|
|
25
|
+
#define SCREEN_H 240
|
|
26
|
+
|
|
27
|
+
/* RGBA5551 color (the VI framebuffer format): 5-5-5-1. */
|
|
28
|
+
#define RGB(r,g,b) ((unsigned short)((((r)>>3)<<11)|(((g)>>3)<<6)|(((b)>>3)<<1)|1))
|
|
29
|
+
|
|
30
|
+
/* controller digital buttons (active-high after read). */
|
|
31
|
+
#define PAD_A 0x8000
|
|
32
|
+
#define PAD_B 0x4000
|
|
33
|
+
#define PAD_Z 0x2000
|
|
34
|
+
#define PAD_START 0x1000
|
|
35
|
+
#define PAD_UP 0x0800
|
|
36
|
+
#define PAD_DOWN 0x0400
|
|
37
|
+
#define PAD_LEFT 0x0200
|
|
38
|
+
#define PAD_RIGHT 0x0100
|
|
39
|
+
#define PAD_L 0x0020
|
|
40
|
+
#define PAD_R 0x0010
|
|
41
|
+
|
|
42
|
+
/* ── framebuffer / 2D ── */
|
|
43
|
+
void n64_init(void);
|
|
44
|
+
void n64_clear(unsigned short col);
|
|
45
|
+
void n64_rect(int x, int y, int w, int h, unsigned short col);
|
|
46
|
+
void n64_tri2d(int x0,int y0,int x1,int y1,int x2,int y2,unsigned short col);
|
|
47
|
+
void n64_flip(void); /* present the framebuffer (swap + wait vblank) */
|
|
48
|
+
|
|
49
|
+
/* ── input ── */
|
|
50
|
+
unsigned int n64_pad(void);
|
|
51
|
+
int n64_pressed(unsigned int mask);
|
|
52
|
+
|
|
53
|
+
/* ── 3D pipeline (same API as the PS1 lib) ── */
|
|
54
|
+
void n64_camera(fix ex, fix ey, fix ez, fix yaw, fix pitch);
|
|
55
|
+
void n64_model(fix tx, fix ty, fix tz, fix yaw);
|
|
56
|
+
void n64_tri3d(Vec3 a, Vec3 b, Vec3 c, unsigned short col);
|
|
57
|
+
void n64_quad3d(Vec3 a, Vec3 b, Vec3 c, Vec3 d, unsigned short col);
|
|
58
|
+
void n64_tri3d_nc(Vec3 a, Vec3 b, Vec3 c, unsigned short col);
|
|
59
|
+
void n64_quad3d_nc(Vec3 a, Vec3 b, Vec3 c, Vec3 d, unsigned short col);
|
|
60
|
+
|
|
61
|
+
/* ── misc ── */
|
|
62
|
+
fix n64_sin(fix a);
|
|
63
|
+
fix n64_cos(fix a);
|
|
64
|
+
unsigned int n64_rand(void);
|
|
65
|
+
void n64_srand(unsigned int seed);
|
|
66
|
+
void n64_number(int x, int y, unsigned int value, unsigned short col);
|
|
67
|
+
|
|
68
|
+
#endif
|