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.
Files changed (65) hide show
  1. package/CHANGELOG.md +322 -0
  2. package/examples/n64/platformer/main.c +158 -0
  3. package/examples/n64/puzzle/main.c +117 -0
  4. package/examples/n64/racing/main.c +147 -0
  5. package/examples/n64/shmup/main.c +122 -0
  6. package/examples/n64/sports/main.c +127 -0
  7. package/examples/ps1/platformer/main.c +158 -0
  8. package/examples/ps1/puzzle/main.c +125 -0
  9. package/examples/ps1/racing/main.c +147 -0
  10. package/examples/ps1/shmup/main.c +192 -0
  11. package/examples/ps1/sports/main.c +127 -0
  12. package/examples/ps1/sprite_move/main.c +38 -0
  13. package/package.json +9 -2
  14. package/src/analysis/analyze.js +169 -29
  15. package/src/analysis/decompile.js +4 -0
  16. package/src/analysis/decompiler/sleigh/mips.ldefs +25 -0
  17. package/src/analysis/decompiler/sleigh/mips32.pspec +78 -0
  18. package/src/analysis/decompiler/sleigh/mips32be.cspec +107 -0
  19. package/src/analysis/decompiler/sleigh/mips32be.sla +211162 -0
  20. package/src/analysis/decompiler/sleigh/mips32le.cspec +106 -0
  21. package/src/analysis/decompiler/sleigh/mips32le.sla +210624 -0
  22. package/src/analysis/rizin.js +22 -1
  23. package/src/cores/capabilities.js +90 -2
  24. package/src/cores/registry.js +12 -0
  25. package/src/host/LibretroGL.js +270 -0
  26. package/src/host/LibretroGLBridge.js +836 -0
  27. package/src/host/LibretroHost.js +144 -3
  28. package/src/host/callbacks.js +18 -2
  29. package/src/host/coreLoader.js +49 -2
  30. package/src/host/cpu-state.js +27 -0
  31. package/src/host/framebuffer.js +14 -1
  32. package/src/host/glOptionalDep.js +60 -0
  33. package/src/host/n64-ai-state.js +43 -0
  34. package/src/host/ps1-spu-state.js +65 -0
  35. package/src/host/retroConstants.js +14 -0
  36. package/src/mcp/tools/cheats.js +73 -4
  37. package/src/mcp/tools/disasm.js +2 -0
  38. package/src/mcp/tools/frame.js +18 -9
  39. package/src/mcp/tools/index.js +12 -2
  40. package/src/mcp/tools/lifecycle.js +1 -1
  41. package/src/mcp/tools/platform-tools.js +20 -4
  42. package/src/mcp/tools/platforms.js +38 -4
  43. package/src/mcp/tools/project.js +100 -0
  44. package/src/mcp/tools/rendering-context.js +2 -1
  45. package/src/mcp/tools/toolchain.js +1 -1
  46. package/src/mcp/tools/watch-memory.js +93 -20
  47. package/src/platforms/n64/lib/c/n64.c +196 -0
  48. package/src/platforms/n64/lib/c/n64.h +68 -0
  49. package/src/platforms/ps1/lib/c/psx.c +200 -0
  50. package/src/platforms/ps1/lib/c/psx.h +83 -0
  51. package/src/toolchains/cc65/cc65.js +11 -0
  52. package/src/toolchains/index.js +35 -0
  53. package/src/toolchains/mips-c/lib/be/libc.a +0 -0
  54. package/src/toolchains/mips-c/lib/be/libgcc.a +0 -0
  55. package/src/toolchains/mips-c/lib/be/libm.a +0 -0
  56. package/src/toolchains/mips-c/lib/el/libc.a +0 -0
  57. package/src/toolchains/mips-c/lib/el/libm.a +0 -0
  58. package/src/toolchains/mips-c/lib/n64-crt0.s +21 -0
  59. package/src/toolchains/mips-c/lib/n64-ipl3.s +30 -0
  60. package/src/toolchains/mips-c/lib/n64.ld +15 -0
  61. package/src/toolchains/mips-c/lib/ps1-crt0.s +20 -0
  62. package/src/toolchains/mips-c/lib/ps1.ld +15 -0
  63. package/src/toolchains/mips-c/lib/softint.c +37 -0
  64. package/src/toolchains/mips-c/mips-c.js +155 -0
  65. package/src/toolchains/mips-elf-gcc/gcc.js +130 -0
@@ -12,7 +12,7 @@
12
12
  // resolved by the existing disasm mappers — analysis here is whole-file.
13
13
  import { readFile } from "node:fs/promises";
14
14
  import path from "node:path";
15
- import { runRizinJson, RIZIN_ARCH } from "./rizin.js";
15
+ import { runRizinJson, RIZIN_ARCH, RIZIN_ENDIAN } from "./rizin.js";
16
16
  import { decompileFunction, SLEIGH_LANGID } from "./decompile.js";
17
17
  import { registersForPlatform } from "../platforms/common/registers.js";
18
18
 
@@ -133,13 +133,72 @@ export function sniffPlatform(p) {
133
133
  if (/\.(lnx|lyx)$/i.test(p)) return "lynx";
134
134
  if (/\.gba$/i.test(p)) return "gba";
135
135
  if (/\.pce$/i.test(p)) return "pce";
136
+ if (/\.(z64|n64|v64)$/i.test(p)) return "n64";
137
+ if (/\.(psexe|psx)$/i.test(p)) return "ps1"; // .exe/.bin ambiguous — pass platform explicitly
136
138
  if (/\.(gen|md|bin)$/i.test(p)) return "genesis";
137
139
  return null;
138
140
  }
139
141
 
140
142
  /** rizin asm.bits per arch (analysis defaults; rizin's loader usually sets
141
143
  * these for recognized formats, but raw blobs need a hint). */
142
- const BITS = { arm: 32, m68k: 32, snes: 16 };
144
+ const BITS = { arm: 32, m68k: 32, snes: 16, mips: 32 };
145
+
146
+ /**
147
+ * The rizin analysis-SEED command for a context. For the 8/16-bit platforms
148
+ * rizin's `aaa` recognizes the bin format / vectors and seeds itself. For a RAW
149
+ * MIPS image (N64/PS1) there's no recognized entry, so `aaa` finds nothing —
150
+ * instead define a function at the code start and recursively analyze its call
151
+ * graph (`af` + `aac`), which surfaces the real function tree (verified: 251
152
+ * functions on a real libdragon N64 ROM where `aaa` found 0).
153
+ * @param {{arch:string, codeStart:number}} ctx
154
+ * @returns {string} the seed command (no trailing semicolon)
155
+ */
156
+ function analysisSeed({ arch, codeStart }) {
157
+ if (arch === "mips") {
158
+ const at = "0x" + (codeStart || 0).toString(16);
159
+ return `af @ ${at}; aac @ ${at}`;
160
+ }
161
+ return "aaa";
162
+ }
163
+
164
+ /** MIPS analysis runs FLAT (rizin ignores -B on a raw malloc:// buffer), so rizin
165
+ * reports FILE-OFFSET addresses. For PS1 loadContext left-pads the .text so flat
166
+ * offset == the VA's low 20 bits (see there); we add the VA's HIGH bits back as
167
+ * `rebase` so callers get real VAs that round-trip through the decompile
168
+ * VA→fileOffset math. N64 (.z64) keeps its absolute VAs baked into the code and is
169
+ * analyzed flat from codeStart, so no rebase. */
170
+ function mipsAnalysisBase({ arch, platform, loadBase, codeStart }) {
171
+ const rebase = (arch === "mips" && platform === "ps1" && loadBase) ? (loadBase & 0xfff00000) >>> 0 : 0;
172
+ return { baddr: undefined, seedAt: codeStart, rebase };
173
+ }
174
+
175
+ /**
176
+ * Normalize an N64 ROM to BIG-ENDIAN (.z64) byte order. Dumps come in three
177
+ * orders, distinguished by the first 4 bytes of the header:
178
+ * .z64 (big) 80 37 12 40 — native; no change
179
+ * .v64 (byteswap)37 80 40 12 — swap every 2 bytes
180
+ * .n64 (little) 40 12 37 80 — swap every 4 bytes
181
+ * MIPS analysis wants the big-endian image so addresses + instruction words line up.
182
+ * @param {Uint8Array} bytes
183
+ * @returns {{ bytes: Uint8Array, reordered: string|null }}
184
+ */
185
+ function normalizeN64ByteOrder(bytes) {
186
+ if (bytes.length < 4) return { bytes, reordered: null };
187
+ const b = bytes;
188
+ const is = (a, c, d, e) => b[0] === a && b[1] === c && b[2] === d && b[3] === e;
189
+ if (is(0x80, 0x37, 0x12, 0x40)) return { bytes, reordered: null }; // .z64 big — native
190
+ if (is(0x37, 0x80, 0x40, 0x12)) { // .v64 byteswapped — swap pairs
191
+ const out = new Uint8Array(b.length & ~1);
192
+ for (let i = 0; i + 1 < b.length; i += 2) { out[i] = b[i + 1]; out[i + 1] = b[i]; }
193
+ return { bytes: out, reordered: "v64 (byteswapped) → z64" };
194
+ }
195
+ if (is(0x40, 0x12, 0x37, 0x80)) { // .n64 little — swap dwords
196
+ const out = new Uint8Array(b.length & ~3);
197
+ for (let i = 0; i + 3 < b.length; i += 4) { out[i] = b[i + 3]; out[i + 1] = b[i + 2]; out[i + 2] = b[i + 1]; out[i + 3] = b[i]; }
198
+ return { bytes: out, reordered: "n64 (little-endian dwords) → z64" };
199
+ }
200
+ return { bytes, reordered: null }; // unknown header — leave as-is
201
+ }
143
202
 
144
203
  /** Build the common rizin invocation context for a ROM + platform. Returns
145
204
  * { romBytes, arch, bits, note } — arch null means let rizin sniff. */
@@ -175,11 +234,53 @@ async function loadContext(romPath, platformOverride) {
175
234
  loadBase = romBytes[0] | (romBytes[1] << 8);
176
235
  romBytes = romBytes.subarray(2);
177
236
  }
237
+ const warningsEarly = [];
238
+ // N64: normalize to big-endian (.z64) byte order, then the ROM header's entry
239
+ // point (big-endian word at 0x08) is the CPU load base. The 0x1000-byte header
240
+ // (IPL3 bootcode) precedes the game; rizin analyzes the whole image but the
241
+ // entry tells functions where code starts.
242
+ let codeStart = 0; // file offset where executable code begins (post-header)
243
+ if (platform === "n64") {
244
+ const norm = normalizeN64ByteOrder(romBytes);
245
+ romBytes = norm.bytes;
246
+ if (norm.reordered) warningsEarly.push(`N64 ROM was ${norm.reordered} byte order — normalized to z64 (big-endian) for analysis.`);
247
+ if (romBytes.length >= 0x0c) {
248
+ // entry point: big-endian 32-bit at offset 0x08.
249
+ loadBase = (romBytes[0x08] << 24) | (romBytes[0x09] << 16) | (romBytes[0x0a] << 8) | romBytes[0x0b];
250
+ }
251
+ // The IPL3 bootcode occupies the first 0x1000 bytes; game code starts after it.
252
+ // rizin's `aaa` doesn't recognize a raw N64 ROM's entry, so analysis is seeded
253
+ // here (see ANALYSIS_SEED).
254
+ codeStart = 0x1000;
255
+ }
256
+ // PS1 PS-EXE: a 2048-byte header; the load (t_addr) is a little-endian word at
257
+ // 0x18, and the code follows the header. Strip the header so rizin sees code at
258
+ // the load address. A raw .bin (no PS-EXE magic) is left flat.
259
+ if (platform === "ps1" && romBytes.length >= 0x800 &&
260
+ romBytes[0] === 0x50 && romBytes[1] === 0x53 && romBytes[2] === 0x2d && romBytes[3] === 0x58) { // "PS-X"
261
+ loadBase = (romBytes[0x18] | (romBytes[0x19] << 8) | (romBytes[0x1a] << 16) | (romBytes[0x1b] << 24)) >>> 0;
262
+ const text = romBytes.subarray(0x800);
263
+ // rizin ignores -B on a raw malloc:// buffer, so it addresses flat from 0. PS1
264
+ // jal targets are ABSOLUTE VAs (e.g. jal 0x80010518) — to let rizin follow them,
265
+ // left-pad the .text by the load address's low 20 bits so flat offset N == the
266
+ // VA's low bits (jal masks to a 28-bit region). The high bits (0x80000000) are
267
+ // added back as `rebase`. Without this, every cross-function call dangles and
268
+ // only the entry blob is discovered. (Cap the pad so a weird t_addr can't OOM.)
269
+ const lowPad = loadBase & 0x000fffff;
270
+ if (lowPad > 0 && lowPad <= 0x200000) {
271
+ const padded = new Uint8Array(lowPad + text.length);
272
+ padded.set(text, lowPad);
273
+ romBytes = padded;
274
+ codeStart = lowPad;
275
+ } else {
276
+ romBytes = text;
277
+ }
278
+ }
178
279
 
179
280
  // A6: container/format sniff. Some dumps are interleaved/headered such that a
180
281
  // FLAT read scrambles every byte → fake "bad instruction" noise everywhere.
181
282
  // Detect + auto-correct, and warn so a flat disasm isn't silently wrong.
182
- const warnings = [];
283
+ const warnings = [...warningsEarly];
183
284
  if (platform === "genesis") {
184
285
  const smd = deinterleaveSmd(romBytes);
185
286
  if (smd) {
@@ -188,7 +289,7 @@ async function loadContext(romPath, platformOverride) {
188
289
  "auto-deinterleaved before analysis. A flat read of the original would scramble every instruction.");
189
290
  }
190
291
  }
191
- return { platform, romBytes, arch, bits: BITS[arch], approx, loadBase, warnings };
292
+ return { platform, romBytes, arch, bits: BITS[arch], endian: RIZIN_ENDIAN[platform], approx, loadBase, codeStart, warnings };
192
293
  }
193
294
 
194
295
  /** Detect + reverse Sega Mega Drive SMD interleaving. An .smd dump is a 512-byte
@@ -227,11 +328,12 @@ function hx(n) { return "0x" + (n >>> 0).toString(16); }
227
328
  * @returns {{platform, count, functions: Array<{address, name, size, nbbs, cc, callers, callees}>}}
228
329
  */
229
330
  export async function analyzeFunctions(romPath, platformOverride) {
230
- const { platform, romBytes, arch, bits, loadBase, warnings } = await loadContext(romPath, platformOverride);
231
- const fns = await runRizinJson({ romBytes, arch, bits, baddr: loadBase || undefined, commands: "aaa; aflj" });
331
+ const { platform, romBytes, arch, bits, endian, loadBase, codeStart, warnings } = await loadContext(romPath, platformOverride);
332
+ const { baddr, seedAt, rebase } = mipsAnalysisBase({ arch, platform, loadBase, codeStart });
333
+ const fns = await runRizinJson({ romBytes, arch, bits, endian, baddr, commands: `${analysisSeed({ arch, codeStart: seedAt })}; aflj` });
232
334
  const functions = fns.map((f) => ({
233
- address: f.offset,
234
- addressHex: hx(f.offset),
335
+ address: (f.offset + rebase) >>> 0,
336
+ addressHex: hx((f.offset + rebase) >>> 0),
235
337
  name: f.name,
236
338
  size: f.size,
237
339
  nbbs: f.nbbs, // basic-block count
@@ -269,30 +371,35 @@ export async function analyzeFunctions(romPath, platformOverride) {
269
371
  */
270
372
  export async function analyzeCfg(romPath, address, platformOverride) {
271
373
  if (address == null) throw new Error("analyze cfg: address required");
272
- const { platform, romBytes, arch, bits, loadBase } = await loadContext(romPath, platformOverride);
374
+ const { platform, romBytes, arch, bits, endian, loadBase, codeStart } = await loadContext(romPath, platformOverride);
273
375
  // afbj = basic blocks of the function as JSON: each block has addr/size/jump/
274
376
  // fail/ninstr. `jump` is the taken edge; `fail` (present only on conditional
275
377
  // blocks) is the fall-through. This is the structured CFG source — `agf json`
276
378
  // only gives a text body blob with untyped out_nodes.
379
+ // MIPS: seed the call graph first (the address is a vaddr from `functions`,
380
+ // which rizin recovers during `aac`). PS1 rebases to loadBase so the vaddr lines up.
381
+ const { baddr, seedAt, rebase } = mipsAnalysisBase({ arch, platform, loadBase, codeStart });
382
+ const flat = ((address >>> 0) - rebase) >>> 0; // VA → flat offset rizin uses
383
+ const rb = (a) => (a == null ? a : (a + rebase) >>> 0);
277
384
  const blocks = await runRizinJson({
278
- romBytes, arch, bits, baddr: loadBase || undefined,
279
- commands: `aaa; af @ ${hx(address)}; afbj @ ${hx(address)}`,
385
+ romBytes, arch, bits, endian, baddr,
386
+ commands: `${analysisSeed({ arch, codeStart: seedAt })}; af @ ${hx(flat)}; afbj @ ${hx(flat)}`,
280
387
  });
281
388
  if (!Array.isArray(blocks) || blocks.length === 0) {
282
389
  return { platform, arch, address, addressHex: hx(address), nodes: [], edges: [], note: "no function/blocks at address" };
283
390
  }
284
391
  const nodes = blocks.map((b) => ({
285
- id: b.addr,
286
- address: b.addr,
287
- addressHex: hx(b.addr),
392
+ id: rb(b.addr),
393
+ address: rb(b.addr),
394
+ addressHex: hx(rb(b.addr)),
288
395
  size: b.size,
289
396
  ninstr: b.ninstr,
290
397
  }));
291
398
  const edges = [];
292
399
  for (const b of blocks) {
293
400
  const conditional = b.fail != null;
294
- if (b.jump != null) edges.push({ from: b.addr, to: b.jump, type: conditional ? "branch_true" : "jump_or_fall" });
295
- if (b.fail != null) edges.push({ from: b.addr, to: b.fail, type: "branch_false" });
401
+ if (b.jump != null) edges.push({ from: rb(b.addr), to: rb(b.jump), type: conditional ? "branch_true" : "jump_or_fall" });
402
+ if (b.fail != null) edges.push({ from: rb(b.addr), to: rb(b.fail), type: "branch_false" });
296
403
  }
297
404
  return {
298
405
  platform, arch,
@@ -308,10 +415,13 @@ export async function analyzeCfg(romPath, address, platformOverride) {
308
415
  */
309
416
  export async function analyzeXrefs(romPath, address, platformOverride) {
310
417
  if (address == null) throw new Error("analyze xrefs: address required");
311
- const { platform, romBytes, arch, bits, loadBase } = await loadContext(romPath, platformOverride);
418
+ const { platform, romBytes, arch, bits, endian, loadBase, codeStart } = await loadContext(romPath, platformOverride);
419
+ const { baddr, seedAt, rebase } = mipsAnalysisBase({ arch, platform, loadBase, codeStart });
420
+ const flat = ((address >>> 0) - rebase) >>> 0;
421
+ const rb = (a) => (a == null ? a : (a + rebase) >>> 0);
312
422
  let refs;
313
423
  try {
314
- refs = await runRizinJson({ romBytes, arch, bits, baddr: loadBase || undefined, commands: `aaa; axtj @ ${hx(address)}` });
424
+ refs = await runRizinJson({ romBytes, arch, bits, endian, baddr, commands: `${analysisSeed({ arch, codeStart: seedAt })}; axtj @ ${hx(flat)}` });
315
425
  } catch (e) {
316
426
  // axtj prints nothing (not even `[]`) when there are zero refs → our JSON
317
427
  // guard throws. Treat "no JSON" as "no refs".
@@ -319,9 +429,9 @@ export async function analyzeXrefs(romPath, address, platformOverride) {
319
429
  else throw e;
320
430
  }
321
431
  const out = (refs ?? []).map((r) => ({
322
- from: r.from,
323
- fromHex: hx(r.from),
324
- to: r.to,
432
+ from: rb(r.from),
433
+ fromHex: hx(rb(r.from)),
434
+ to: rb(r.to),
325
435
  type: (r.type || "").toLowerCase(), // CALL / CODE / DATA / STRING
326
436
  opcode: r.opcode,
327
437
  }));
@@ -333,23 +443,25 @@ export async function analyzeXrefs(romPath, address, platformOverride) {
333
443
  * analysis pass. The "give me the shape of this ROM" call.
334
444
  */
335
445
  export async function analyzeStructure(romPath, platformOverride) {
336
- const { platform, romBytes, arch, bits, loadBase } = await loadContext(romPath, platformOverride);
337
- const baddr = loadBase || undefined;
446
+ const { platform, romBytes, arch, bits, endian, loadBase, codeStart } = await loadContext(romPath, platformOverride);
447
+ const { baddr, seedAt, rebase } = mipsAnalysisBase({ arch, platform, loadBase, codeStart });
448
+ const seed = analysisSeed({ arch, codeStart: seedAt });
338
449
  const [fns, strings, entries] = await Promise.all([
339
- runRizinJson({ romBytes, arch, bits, baddr, commands: "aaa; aflj" }).catch(() => []),
340
- runRizinJson({ romBytes, arch, bits, baddr, commands: "aaa; izj" }).catch(() => []),
341
- runRizinJson({ romBytes, arch, bits, baddr, commands: "aaa; iej" }).catch(() => []),
450
+ runRizinJson({ romBytes, arch, bits, endian, baddr, commands: `${seed}; aflj` }).catch(() => []),
451
+ runRizinJson({ romBytes, arch, bits, endian, baddr, commands: `${seed}; izj` }).catch(() => []),
452
+ runRizinJson({ romBytes, arch, bits, baddr, commands: `${seed}; iej` }).catch(() => []),
342
453
  ]);
454
+ const rb = (a) => (a == null ? a : (a + rebase) >>> 0);
343
455
  return {
344
456
  platform, arch,
345
457
  functionCount: Array.isArray(fns) ? fns.length : 0,
346
458
  stringCount: Array.isArray(strings) ? strings.length : 0,
347
- entrypoints: (Array.isArray(entries) ? entries : []).map((e) => ({ address: e.vaddr, addressHex: hx(e.vaddr) })),
459
+ entrypoints: (Array.isArray(entries) ? entries : []).map((e) => ({ address: rb(e.vaddr), addressHex: hx(rb(e.vaddr)) })),
348
460
  functions: (Array.isArray(fns) ? fns : []).slice(0, 512).map((f) => ({
349
- address: f.offset, addressHex: hx(f.offset), name: f.name, size: f.size, callers: f.indegree ?? 0,
461
+ address: rb(f.offset), addressHex: hx(rb(f.offset)), name: f.name, size: f.size, callers: f.indegree ?? 0,
350
462
  })),
351
463
  strings: (Array.isArray(strings) ? strings : []).slice(0, 256).map((s) => ({
352
- address: s.vaddr, addressHex: hx(s.vaddr), value: s.string,
464
+ address: rb(s.vaddr), addressHex: hx(rb(s.vaddr)), value: s.string,
353
465
  })),
354
466
  };
355
467
  }
@@ -507,6 +619,34 @@ export async function analyzeDecompile(romPath, address, platformOverride) {
507
619
  // to pure garbage.
508
620
  if (platform === "genesis") romBytes = deinterleaveSmd(romBytes) ?? romBytes;
509
621
 
622
+ // MIPS (N64/PS1): `address` is a vaddr from target='functions'. Map it to a file
623
+ // offset and decompile via the MIPS SLEIGH spec (MIPS:BE:32 for N64, MIPS:LE:32
624
+ // for PS1). N64: normalize byte order, fileOff = vaddr - entryVaddr + 0x1000 (post
625
+ // IPL3). PS1: strip PS-EXE, fileOff = vaddr - loadAddr.
626
+ if (platform === "n64" || platform === "ps1") {
627
+ let fileOff;
628
+ if (platform === "n64") {
629
+ romBytes = normalizeN64ByteOrder(romBytes).bytes;
630
+ const entry = ((romBytes[0x08] << 24) | (romBytes[0x09] << 16) | (romBytes[0x0a] << 8) | romBytes[0x0b]) >>> 0;
631
+ fileOff = ((address >>> 0) - entry + 0x1000) >>> 0;
632
+ } else {
633
+ let loadAddr = 0;
634
+ if (romBytes.length >= 0x800 && romBytes[0] === 0x50 && romBytes[1] === 0x53 && romBytes[2] === 0x2d && romBytes[3] === 0x58) {
635
+ loadAddr = (romBytes[0x18] | (romBytes[0x19] << 8) | (romBytes[0x1a] << 16) | (romBytes[0x1b] << 24)) >>> 0;
636
+ romBytes = romBytes.subarray(0x800);
637
+ }
638
+ fileOff = ((address >>> 0) - loadAddr) >>> 0;
639
+ }
640
+ if (fileOff >= romBytes.length) {
641
+ throw new Error(`decompile: ${platform} address ${hx(address)} maps to file offset ${hx(fileOff)}, outside the ${romBytes.length}-byte image. Use an address from target='functions'.`);
642
+ }
643
+ const rm = await decompileFunction({ platform, romBytes, fileOffset: fileOff });
644
+ return {
645
+ platform, langid: rm.langid, address, addressHex: hx(address),
646
+ code: prettyDecompile(rm.code, platform), warnings: rm.warnings,
647
+ };
648
+ }
649
+
510
650
  // SNES: banked 24-bit space. `address` is a LoROM/HiROM CPU address (what
511
651
  // target='functions'/'cfg' report). Lay the cart out by CPU address so BOTH
512
652
  // the function address AND its in-bank/JSL operands resolve, then decompile at
@@ -47,6 +47,10 @@ export const SLEIGH_LANGID = {
47
47
  genesis: "68000:BE:32:default",
48
48
  snes: "65816:LE:24:snes",
49
49
  pce: "HuC6280:LE:16:default",
50
+ // 32-bit MIPS tier. PS1 R3000 = little-endian; N64 R4300 = big-endian (games run
51
+ // 32-bit MIPS III code). Ghidra ships both MIPS variants in its stock SLEIGH.
52
+ ps1: "MIPS:LE:32:default",
53
+ n64: "MIPS:BE:32:default",
50
54
  };
51
55
 
52
56
  /**
@@ -0,0 +1,25 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <language_definitions>
3
+ <language processor="MIPS"
4
+ endian="big"
5
+ size="32"
6
+ variant="default"
7
+ version="1.7"
8
+ slafile="mips32be.sla"
9
+ processorspec="mips32.pspec"
10
+ id="MIPS:BE:32:default">
11
+ <description>MIPS32 big endian (N64 R4300, 32-bit code)</description>
12
+ <compiler name="default" spec="mips32be.cspec" id="default"/>
13
+ </language>
14
+ <language processor="MIPS"
15
+ endian="little"
16
+ size="32"
17
+ variant="default"
18
+ version="1.7"
19
+ slafile="mips32le.sla"
20
+ processorspec="mips32.pspec"
21
+ id="MIPS:LE:32:default">
22
+ <description>MIPS32 little endian (PS1 R3000)</description>
23
+ <compiler name="default" spec="mips32le.cspec" id="default"/>
24
+ </language>
25
+ </language_definitions>
@@ -0,0 +1,78 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+
3
+ <processor_spec>
4
+ <properties>
5
+ <property key="addressesDoNotAppearDirectlyInCode" value="true"/>
6
+ <property key="emulateInstructionStateModifierClass" value="ghidra.program.emulation.MIPSEmulateInstructionStateModifier"/>
7
+ <property key="assemblyRating:MIPS:BE:32:default" value="PLATINUM"/>
8
+ </properties>
9
+ <programcounter register="pc"/>
10
+ <context_data>
11
+ <context_set space="ram">
12
+ <set name="PAIR_INSTRUCTION_FLAG" val="0" description="1 if LWL/LWR instruction is a pair"/>
13
+ <set name="RELP" val="1" description="1 if mips16e, 0 if micromips"/>
14
+ </context_set>
15
+ </context_data>
16
+ <register_data>
17
+ <register name="contextreg" hidden="true"/>
18
+ <register name="ext_isjal" hidden="true"/>
19
+ <register name="ext_value" hidden="true"/>
20
+ <register name="ext_value_select" hidden="true"/>
21
+ <register name="ext_value_1005" hidden="true"/>
22
+ <register name="ext_value_1004" hidden="true"/>
23
+ <register name="ext_value_sa40" hidden="true"/>
24
+ <register name="ext_value_xreg" hidden="true"/>
25
+ <register name="ext_value_frame" hidden="true"/>
26
+ <register name="ext_value_areg" hidden="true"/>
27
+ <register name="ext_value_b0" hidden="true"/>
28
+ <register name="ext_value_b1" hidden="true"/>
29
+ <register name="ext_value_b2" hidden="true"/>
30
+ <register name="ext_value_b3" hidden="true"/>
31
+ <register name="ext_value_saz" hidden="true"/>
32
+ <register name="ext_value_1511" hidden="true"/>
33
+ <register name="ext_value_1511s" hidden="true"/>
34
+ <register name="ext_value_1411" hidden="true"/>
35
+ <register name="ext_value_1411s" hidden="true"/>
36
+ <register name="ext_tgt_2521" hidden="true"/>
37
+ <register name="ext_tgt_2016" hidden="true"/>
38
+ <register name="ext_is_ext" hidden="true"/>
39
+ <register name="ext_m16r32" hidden="true"/>
40
+ <register name="ext_m16r32a" hidden="true"/>
41
+ <register name="ext_reg_high" hidden="true"/>
42
+ <register name="ext_reg_low" hidden="true"/>
43
+ <register name="ext_svrs_xs" hidden="true"/>
44
+ <register name="ext_svrs_s1" hidden="true"/>
45
+ <register name="ext_svrs_s0" hidden="true"/>
46
+ <register name="ext_tgt_x" hidden="true"/>
47
+ <register name="ext_done" hidden="true"/>
48
+ <register name="ext_delay" hidden="true"/>
49
+ <register name="REL6" hidden="true"/>
50
+ <register name="RELP" hidden="true"/>
51
+ <register name="ext_t4" hidden="true"/>
52
+ <register name="ext_tra" hidden="true"/>
53
+ <register name="ext_32_code" hidden="true"/>
54
+ <register name="ext_32_codes" hidden="true"/>
55
+ <register name="ext_32_addim" hidden="true"/>
56
+ <register name="ext_32_addims" hidden="true"/>
57
+ <register name="ext_32_imm2" hidden="true"/>
58
+ <register name="ext_32_imm2s" hidden="true"/>
59
+ <register name="ext_32_imm3" hidden="true"/>
60
+ <register name="ext_32_imm3s" hidden="true"/>
61
+ <register name="ext_32_imm5" hidden="true"/>
62
+ <register name="ext_32_imm5s" hidden="true"/>
63
+ <register name="ext_32_imm6" hidden="true"/>
64
+ <register name="ext_32_rlist" hidden="true"/>
65
+ <register name="ext_32_base" hidden="true"/>
66
+ <register name="ext_32_basea" hidden="true"/>
67
+ <register name="ext_32_rd" hidden="true"/>
68
+ <register name="ext_32_rdset" hidden="true"/>
69
+ <register name="ext_32_rs1" hidden="true"/>
70
+ <register name="ext_32_rs1lo" hidden="true"/>
71
+ <register name="ext_32_rs1set" hidden="true"/>
72
+ <register name="ext_16_rs" hidden="true"/>
73
+ <register name="ext_16_rslo" hidden="true"/>
74
+ <register name="ext_16_rshi" hidden="true"/>
75
+ <register name="ext_off16_s" hidden="true"/>
76
+ <register name="ext_off16_u" hidden="true"/>
77
+ </register_data>
78
+ </processor_spec>
@@ -0,0 +1,107 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+
3
+ <compiler_spec>
4
+ <data_organization>
5
+ <pointer_size value="4"/>
6
+ <float_size value="4" />
7
+ <double_size value="8" />
8
+ <long_double_size value="8" />
9
+ <size_alignment_map>
10
+ <entry size="1" alignment="1" />
11
+ <entry size="2" alignment="2" />
12
+ <entry size="4" alignment="4" />
13
+ <entry size="8" alignment="8" />
14
+ </size_alignment_map>
15
+ </data_organization>
16
+
17
+ <stackpointer register="sp" space="ram"/>
18
+ <funcptr align="2"/>
19
+ <global>
20
+ <range space="ram"/>
21
+ <range space="register" first="0x2000" last="0x2fff"/>
22
+ </global>
23
+ <returnaddress>
24
+ <register name="ra"/>
25
+ </returnaddress>
26
+ <default_proto>
27
+ <prototype name="__stdcall" extrapop="0" stackshift="0">
28
+ <input>
29
+ <pentry minsize="1" maxsize="8" metatype="float">
30
+ <register name="f12_13"/>
31
+ </pentry>
32
+ <pentry minsize="1" maxsize="8" metatype="float">
33
+ <register name="f14_15"/>
34
+ </pentry>
35
+ <pentry minsize="1" maxsize="4">
36
+ <register name="a0"/>
37
+ </pentry>
38
+ <pentry minsize="1" maxsize="4">
39
+ <register name="a1"/>
40
+ </pentry>
41
+ <pentry minsize="1" maxsize="4">
42
+ <register name="a2"/>
43
+ </pentry>
44
+ <pentry minsize="1" maxsize="4">
45
+ <register name="a3"/>
46
+ </pentry>
47
+ <pentry minsize="1" maxsize="500" align="4">
48
+ <addr offset="16" space="stack"/>
49
+ </pentry>
50
+ </input>
51
+ <output>
52
+ <pentry minsize="1" maxsize="8" metatype="float">
53
+ <register name="f0_1"/>
54
+ </pentry>
55
+ <pentry minsize="1" maxsize="4">
56
+ <register name="v0"/>
57
+ </pentry>
58
+ <pentry minsize="5" maxsize="8">
59
+ <addr space="join" piece1="v0" piece2="v1"/>
60
+ </pentry>
61
+ </output>
62
+ <unaffected>
63
+ <register name="s0"/>
64
+ <register name="s1"/>
65
+ <register name="s2"/>
66
+ <register name="s3"/>
67
+ <register name="s4"/>
68
+ <register name="s5"/>
69
+ <register name="s6"/>
70
+ <register name="s7"/>
71
+ <register name="s8"/>
72
+ <register name="sp"/>
73
+ <register name="gp"/>
74
+ <register name="f20"/>
75
+ <register name="f22"/>
76
+ <register name="f24"/>
77
+ <register name="f26"/>
78
+ <register name="f28"/>
79
+ <register name="f30"/>
80
+ </unaffected>
81
+ <localrange>
82
+ <range space="stack" first="0xfff0bdc0" last="0xffffffff"/>
83
+ <range space="stack" first="0" last="15"/> <!-- This is backup storage space for register params, but we treat as locals -->
84
+ </localrange>
85
+ </prototype>
86
+ </default_proto>
87
+ <prototype name="processEntry" extrapop="0" stackshift="0">
88
+ <input pointermax="4">
89
+ <pentry minsize="1" maxsize="4">
90
+ <register name="v0"/>
91
+ </pentry>
92
+ <pentry minsize="1" maxsize="500" align="4">
93
+ <addr offset="0" space="stack"/>
94
+ </pentry>
95
+ </input>
96
+ <output killedbycall="true">
97
+ <pentry minsize="1" maxsize="4">
98
+ <register name="v0"/>
99
+ </pentry>
100
+ </output>
101
+ <unaffected>
102
+ <register name="sp"/>
103
+ </unaffected>
104
+ </prototype>
105
+
106
+
107
+ </compiler_spec>