romdevtools 0.44.0 → 0.70.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.
Files changed (71) hide show
  1. package/CHANGELOG.md +572 -0
  2. package/README.md +9 -7
  3. package/examples/dreamcast/hello/main.c +24 -0
  4. package/examples/n64/platformer/main.c +158 -0
  5. package/examples/n64/puzzle/main.c +117 -0
  6. package/examples/n64/racing/main.c +147 -0
  7. package/examples/n64/shmup/main.c +122 -0
  8. package/examples/n64/sports/main.c +127 -0
  9. package/examples/ps1/platformer/main.c +158 -0
  10. package/examples/ps1/puzzle/main.c +125 -0
  11. package/examples/ps1/racing/main.c +147 -0
  12. package/examples/ps1/shmup/main.c +192 -0
  13. package/examples/ps1/sports/main.c +127 -0
  14. package/examples/ps1/sprite_move/main.c +38 -0
  15. package/package.json +11 -2
  16. package/src/analysis/analyze.js +224 -29
  17. package/src/analysis/decompile.js +7 -0
  18. package/src/analysis/decompiler/sleigh/mips.ldefs +25 -0
  19. package/src/analysis/decompiler/sleigh/mips32.pspec +78 -0
  20. package/src/analysis/decompiler/sleigh/mips32be.cspec +107 -0
  21. package/src/analysis/decompiler/sleigh/mips32be.sla +211162 -0
  22. package/src/analysis/decompiler/sleigh/mips32le.cspec +106 -0
  23. package/src/analysis/decompiler/sleigh/mips32le.sla +210624 -0
  24. package/src/analysis/rizin.js +24 -1
  25. package/src/cores/capabilities.js +122 -2
  26. package/src/cores/registry.js +17 -0
  27. package/src/host/LibretroGL.js +273 -0
  28. package/src/host/LibretroGLBridge.js +836 -0
  29. package/src/host/LibretroHost.js +203 -3
  30. package/src/host/callbacks.js +22 -2
  31. package/src/host/coreLoader.js +65 -5
  32. package/src/host/cpu-state.js +27 -0
  33. package/src/host/framebuffer.js +14 -1
  34. package/src/host/glOptionalDep.js +60 -0
  35. package/src/host/n64-ai-state.js +43 -0
  36. package/src/host/ps1-spu-state.js +65 -0
  37. package/src/host/retroConstants.js +14 -0
  38. package/src/mcp/tools/disasm.js +2 -0
  39. package/src/mcp/tools/frame.js +18 -9
  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 +116 -0
  44. package/src/mcp/tools/rendering-context.js +2 -1
  45. package/src/mcp/tools/toolchain.js +1 -1
  46. package/src/platforms/n64/lib/c/n64.c +196 -0
  47. package/src/platforms/n64/lib/c/n64.h +68 -0
  48. package/src/platforms/ps1/lib/c/psx.c +200 -0
  49. package/src/platforms/ps1/lib/c/psx.h +83 -0
  50. package/src/toolchains/index.js +65 -0
  51. package/src/toolchains/mips-c/lib/be/libc.a +0 -0
  52. package/src/toolchains/mips-c/lib/be/libgcc.a +0 -0
  53. package/src/toolchains/mips-c/lib/be/libm.a +0 -0
  54. package/src/toolchains/mips-c/lib/el/libc.a +0 -0
  55. package/src/toolchains/mips-c/lib/el/libm.a +0 -0
  56. package/src/toolchains/mips-c/lib/n64-crt0.s +21 -0
  57. package/src/toolchains/mips-c/lib/n64-ipl3.s +30 -0
  58. package/src/toolchains/mips-c/lib/n64.ld +15 -0
  59. package/src/toolchains/mips-c/lib/ps1-crt0.s +20 -0
  60. package/src/toolchains/mips-c/lib/ps1.ld +15 -0
  61. package/src/toolchains/mips-c/lib/softint.c +37 -0
  62. package/src/toolchains/mips-c/mips-c.js +155 -0
  63. package/src/toolchains/mips-elf-gcc/gcc.js +130 -0
  64. package/src/toolchains/sh-c/lib/dc-crt0.s +23 -0
  65. package/src/toolchains/sh-c/lib/dc.h +102 -0
  66. package/src/toolchains/sh-c/lib/dc.ld +11 -0
  67. package/src/toolchains/sh-c/lib/libc.a +0 -0
  68. package/src/toolchains/sh-c/lib/libgcc.a +0 -0
  69. package/src/toolchains/sh-c/lib/libm.a +0 -0
  70. package/src/toolchains/sh-c/sh-c.js +101 -0
  71. package/src/toolchains/sh-elf-gcc/gcc.js +122 -0
@@ -0,0 +1,127 @@
1
+ /*
2
+ * sports/main.c — SLAM COURT: a 3D PlayStation sports game (air-hockey / pong).
3
+ *
4
+ * A 3D playfield seen in perspective: you control the near paddle, the CPU the far
5
+ * one. A ball bounces down the court in 3D (X across, Z into the screen); rally it
6
+ * past the CPU to score, miss and the CPU scores. First to 7. Title -> match ->
7
+ * game-over. The whole court is drawn with the perspective pipeline; the ball's
8
+ * size changes with depth, selling the 3D.
9
+ *
10
+ * Build: build({ platform:"ps1", language:"c" }). Controls: LEFT/RIGHT move your
11
+ * paddle, START begin/restart.
12
+ *
13
+ * 3D technique: the court floor is a big no-cull quad; paddles + ball are cubes at
14
+ * world positions. The CPU paddle tracks the ball's X with a capped speed. 16.16 fp.
15
+ */
16
+ #include "psx.h"
17
+
18
+ enum { TITLE, PLAY, OVER };
19
+
20
+ static fix px; /* player paddle X (near, z = small) */
21
+ static fix cx; /* cpu paddle X (far, z = big) */
22
+ static fix bx, bz, bvx, bvz; /* ball position + velocity */
23
+ static int you, cpu, state;
24
+ static unsigned int prev_pad;
25
+
26
+ #define COURT_HW FIX(5) /* court half-width */
27
+ #define NEAR_Z FIX(3)
28
+ #define FAR_Z FIX(22)
29
+
30
+ static void draw_cube_at(fix x, fix y, fix z, fix s, unsigned int col)
31
+ {
32
+ Vec3 v[8]; int i;
33
+ static const int sx[8]={-1,1,1,-1,-1,1,1,-1},sy[8]={-1,-1,1,1,-1,-1,1,1},sz[8]={-1,-1,-1,-1,1,1,1,1};
34
+ psx_model(x,y,z,0);
35
+ for(i=0;i<8;i++){v[i].x=sx[i]*s;v[i].y=sy[i]*s;v[i].z=sz[i]*s;}
36
+ psx_quad3d(v[0],v[1],v[2],v[3],col); psx_quad3d(v[5],v[4],v[7],v[6],col);
37
+ psx_quad3d(v[4],v[0],v[3],v[7],col); psx_quad3d(v[1],v[5],v[6],v[2],col);
38
+ psx_quad3d(v[4],v[5],v[1],v[0],col); psx_quad3d(v[3],v[2],v[6],v[7],col);
39
+ }
40
+
41
+ static void serve(int toward_cpu)
42
+ {
43
+ bx = 0; bz = (NEAR_Z + FAR_Z) >> 1;
44
+ bvx = (psx_rand() & 1) ? FIXF(0.12f) : FIXF(-0.12f);
45
+ bvz = toward_cpu ? FIXF(0.45f) : FIXF(-0.45f);
46
+ }
47
+
48
+ static void reset_game(void) { px = 0; cx = 0; you = 0; cpu = 0; serve(1); }
49
+
50
+ static void update(void)
51
+ {
52
+ unsigned int pad = psx_pad();
53
+ if (pad & PAD_LEFT) px -= FIXF(0.22f);
54
+ if (pad & PAD_RIGHT) px += FIXF(0.22f);
55
+ if (px < -COURT_HW) px = -COURT_HW; if (px > COURT_HW) px = COURT_HW;
56
+
57
+ /* cpu tracks the ball, capped */
58
+ { fix d = bx - cx; if (d > FIXF(0.16f)) d = FIXF(0.16f); if (d < -FIXF(0.16f)) d = -FIXF(0.16f); cx += d; }
59
+
60
+ bx += bvx; bz += bvz;
61
+ if (bx < -COURT_HW || bx > COURT_HW) bvx = -bvx; /* side walls */
62
+
63
+ /* near paddle (player) */
64
+ if (bz <= NEAR_Z) {
65
+ fix d = bx - px; if (d < 0) d = -d;
66
+ if (d < FIX(2)) { bvz = -bvz; bz = NEAR_Z; bvx += (bx - px) >> 4; }
67
+ else { cpu++; if (cpu >= 7) { state = OVER; } else serve(1); }
68
+ }
69
+ /* far paddle (cpu) */
70
+ if (bz >= FAR_Z) {
71
+ fix d = bx - cx; if (d < 0) d = -d;
72
+ if (d < FIX(2)) { bvz = -bvz; bz = FAR_Z; }
73
+ else { you++; if (you >= 7) { state = OVER; } else serve(0); }
74
+ }
75
+ prev_pad = pad;
76
+ }
77
+
78
+ static void render(void)
79
+ {
80
+ Vec3 a,b,c,d;
81
+ psx_clear(RGB(20, 40, 30));
82
+ psx_camera(0, FIX(4), FIX(-1), 0, FIXF(-0.30f)); /* look down the court */
83
+
84
+ /* court floor */
85
+ a.x=-COURT_HW; a.y=0; a.z=NEAR_Z; b.x=COURT_HW; b.y=0; b.z=NEAR_Z;
86
+ c.x=COURT_HW; c.y=0; c.z=FAR_Z; d.x=-COURT_HW; d.y=0; d.z=FAR_Z;
87
+ psx_quad3d_nc(a,b,c,d, RGB(40, 90, 60));
88
+ /* center line */
89
+ a.x=-COURT_HW; a.z=(NEAR_Z+FAR_Z)>>1; b.x=COURT_HW; b.z=a.z;
90
+ c.x=COURT_HW; c.z=a.z+FIXF(0.3f); d.x=-COURT_HW; d.z=c.z;
91
+ a.y=FIXF(0.02f);b.y=a.y;c.y=a.y;d.y=a.y;
92
+ psx_quad3d_nc(a,b,c,d, RGB(220,220,220));
93
+
94
+ draw_cube_at(px, FIXF(0.5f), NEAR_Z, FIXF(0.9f), RGB(80, 180, 255)); /* you */
95
+ draw_cube_at(cx, FIXF(0.5f), FAR_Z, FIXF(0.9f), RGB(255, 100, 80)); /* cpu */
96
+ draw_cube_at(bx, FIXF(0.5f), bz, FIXF(0.4f), RGB(255, 240, 120)); /* ball */
97
+
98
+ psx_number(40, 8, (unsigned)you, RGB(80, 180, 255));
99
+ psx_number(240, 8, (unsigned)cpu, RGB(255, 100, 80));
100
+ }
101
+
102
+ int main(void)
103
+ {
104
+ psx_init();
105
+ psx_srand(0x5A11);
106
+ state = TITLE; prev_pad = 0;
107
+ for (;;) {
108
+ unsigned int pad = psx_pad();
109
+ if (state == TITLE) {
110
+ static fix t; t += FIX(3);
111
+ psx_clear(RGB(15, 30, 25));
112
+ psx_camera(0, 0, FIX(-1), 0, 0);
113
+ draw_cube_at(0, 0, FIX(6), FIX(1), RGB(80, 180, 255));
114
+ if ((pad & PAD_START) && !(prev_pad & PAD_START)) { reset_game(); state = PLAY; }
115
+ prev_pad = pad;
116
+ } else if (state == PLAY) { update(); render(); }
117
+ else {
118
+ psx_clear(RGB(8, 20, 16));
119
+ psx_number(120, 100, (unsigned)you, RGB(80, 180, 255));
120
+ psx_number(160, 100, (unsigned)cpu, RGB(255, 100, 80));
121
+ if ((pad & PAD_START) && !(prev_pad & PAD_START)) { reset_game(); state = PLAY; }
122
+ prev_pad = pad;
123
+ }
124
+ psx_vsync();
125
+ }
126
+ return 0;
127
+ }
@@ -0,0 +1,38 @@
1
+ /*
2
+ * sprite_move/main.c — PS1 starter (mips-elf-gcc + the bundled psx.h helper).
3
+ *
4
+ * Boots a visible 320x240 display and animates a colored box bouncing around the
5
+ * screen — the canonical "GPU is alive, here's a moving primitive" PS1 starter.
6
+ * Exercises the whole helper lib: psx_init (GPU bring-up), psx_clear + psx_rect
7
+ * (flat polygons), psx_vsync (pacing).
8
+ *
9
+ * Build with: build({ platform:"ps1", language:"c" }) — language defaults to C.
10
+ * Output is a PS-EXE the HLE BIOS loads at 0x80010000; main() loops forever
11
+ * (no OS to return to).
12
+ *
13
+ * PS1 NOTE: there is no tile/sprite/nametable hardware — the GPU draws polygons
14
+ * into a framebuffer. A "sprite" here is a textured/flat quad you draw each frame.
15
+ */
16
+ #include "psx.h"
17
+
18
+ int main(void)
19
+ {
20
+ int x = 40, y = 40, dx = 2, dy = 2;
21
+ const int W = 32, H = 32;
22
+
23
+ psx_init();
24
+
25
+ for (;;) {
26
+ /* move + bounce */
27
+ x += dx; y += dy;
28
+ if (x < 0 || x + W > 320) dx = -dx;
29
+ if (y < 0 || y + H > 240) dy = -dy;
30
+ if (x < 0) x = 0; if (x + W > 320) x = 320 - W;
31
+ if (y < 0) y = 0; if (y + H > 240) y = 240 - H;
32
+
33
+ psx_clear(RGB(20, 30, 60)); /* deep-blue background */
34
+ psx_rect(x, y, W, H, RGB(255, 180, 40)); /* the bouncing box */
35
+ psx_vsync();
36
+ }
37
+ return 0;
38
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "romdevtools",
3
- "version": "0.44.0",
4
- "description": "Tool server giving coding agents full control of homebrew ROM development AND reverse-engineering/romhacking across 14 retro platforms (NES, SNES, GB, Genesis, Atari, C64, PC Engine, MSX, ...) via WASM toolchains + emulator cores. Use over plain HTTP, as an Agent Skill, or as an MCP server.",
3
+ "version": "0.70.0",
4
+ "description": "Tool server giving coding agents full control of homebrew ROM development AND reverse-engineering/romhacking across 17 retro platforms (NES, SNES, GB, Genesis, Atari, C64, PC Engine, MSX, PlayStation, N64, Dreamcast, ...) via WASM toolchains + emulator cores. Use over plain HTTP, as an Agent Skill, or as an MCP server.",
5
5
  "type": "module",
6
6
  "main": "src/mcp/server.js",
7
7
  "bin": {
@@ -58,6 +58,9 @@
58
58
  "romdev-core-geargrafx": "0.7.0",
59
59
  "romdev-core-gpgx": "0.12.0",
60
60
  "romdev-core-handy": "0.7.0",
61
+ "romdev-core-flycast": "0.1.0",
62
+ "romdev-core-parallel-n64": "0.1.0",
63
+ "romdev-core-beetle-psx-hw": "0.1.0",
61
64
  "romdev-core-prosystem": "0.8.0",
62
65
  "romdev-core-vice": "0.9.0",
63
66
  "romdev-famitone": "0.1.0",
@@ -67,8 +70,10 @@
67
70
  "romdev-platform-snes": "0.8.0",
68
71
  "romdev-toolchain-cc65": "0.1.1",
69
72
  "romdev-toolchain-m68k-gcc": "0.2.0",
73
+ "romdev-toolchain-mips-gcc": "0.1.0",
70
74
  "romdev-toolchain-rgbds": "0.1.0",
71
75
  "romdev-toolchain-sdcc": "0.2.0",
76
+ "romdev-toolchain-sh-gcc": "0.1.0",
72
77
  "romdev-toolchain-vasm": "0.1.0",
73
78
  "romdev-xgm2": "0.1.0",
74
79
  "romdev_game_codes": "0.1.0",
@@ -77,6 +82,10 @@
77
82
  "yauzl": "^3.3.1",
78
83
  "zod": "^4.4.3"
79
84
  },
85
+ "optionalDependencies": {
86
+ "native-gles": "^0.5.0",
87
+ "webgl-node": "^1.2.0"
88
+ },
80
89
  "files": [
81
90
  "src",
82
91
  "!src/**/*.test.js",
@@ -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,80 @@ 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
138
+ if (/\.(cdi|gdi)$/i.test(p)) return "dreamcast"; // DC disc images; .elf is cross-platform — pass platform explicitly
136
139
  if (/\.(gen|md|bin)$/i.test(p)) return "genesis";
137
140
  return null;
138
141
  }
139
142
 
140
143
  /** rizin asm.bits per arch (analysis defaults; rizin's loader usually sets
141
144
  * these for recognized formats, but raw blobs need a hint). */
142
- const BITS = { arm: 32, m68k: 32, snes: 16 };
145
+ const BITS = { arm: 32, m68k: 32, snes: 16, mips: 32, sh: 32 };
146
+
147
+ /**
148
+ * The rizin analysis-SEED command for a context. For the 8/16-bit platforms
149
+ * rizin's `aaa` recognizes the bin format / vectors and seeds itself. For a RAW
150
+ * MIPS image (N64/PS1) there's no recognized entry, so `aaa` finds nothing —
151
+ * instead define a function at the code start and recursively analyze its call
152
+ * graph (`af` + `aac`), which surfaces the real function tree (verified: 251
153
+ * functions on a real libdragon N64 ROM where `aaa` found 0).
154
+ * @param {{arch:string, codeStart:number}} ctx
155
+ * @returns {string} the seed command (no trailing semicolon)
156
+ */
157
+ function analysisSeed({ arch, codeStart }) {
158
+ // MIPS (N64/PS1) and SH-4 (Dreamcast) are raw code images with no rizin-recognized
159
+ // entry, so `aaa` finds nothing — seed a function at the code start + recursively
160
+ // analyze its call graph (`af` + `aac`). The 8/16-bit bin formats self-seed via aaa.
161
+ if (arch === "mips" || arch === "sh") {
162
+ const at = "0x" + (codeStart || 0).toString(16);
163
+ return `af @ ${at}; aac @ ${at}`;
164
+ }
165
+ return "aaa";
166
+ }
167
+
168
+ /** MIPS analysis runs FLAT (rizin ignores -B on a raw malloc:// buffer), so rizin
169
+ * reports FILE-OFFSET addresses. For PS1 loadContext left-pads the .text so flat
170
+ * offset == the VA's low 20 bits (see there); we add the VA's HIGH bits back as
171
+ * `rebase` so callers get real VAs that round-trip through the decompile
172
+ * VA→fileOffset math. N64 (.z64) keeps its absolute VAs baked into the code and is
173
+ * analyzed flat from codeStart, so no rebase. */
174
+ function mipsAnalysisBase({ platform, loadBase, codeStart }) {
175
+ // PS1 (left-padded to the VA's low 20 bits) and Dreamcast (same, loadBase
176
+ // 0x8c010000) need the VA's HIGH bits added back so addresses round-trip. N64 runs
177
+ // flat with VAs already baked in (no rebase).
178
+ const padded = (platform === "ps1" || platform === "dreamcast") && loadBase;
179
+ const rebase = padded ? (loadBase & 0xfff00000) >>> 0 : 0;
180
+ return { baddr: undefined, seedAt: codeStart, rebase };
181
+ }
182
+
183
+ /**
184
+ * Normalize an N64 ROM to BIG-ENDIAN (.z64) byte order. Dumps come in three
185
+ * orders, distinguished by the first 4 bytes of the header:
186
+ * .z64 (big) 80 37 12 40 — native; no change
187
+ * .v64 (byteswap)37 80 40 12 — swap every 2 bytes
188
+ * .n64 (little) 40 12 37 80 — swap every 4 bytes
189
+ * MIPS analysis wants the big-endian image so addresses + instruction words line up.
190
+ * @param {Uint8Array} bytes
191
+ * @returns {{ bytes: Uint8Array, reordered: string|null }}
192
+ */
193
+ function normalizeN64ByteOrder(bytes) {
194
+ if (bytes.length < 4) return { bytes, reordered: null };
195
+ const b = bytes;
196
+ const is = (a, c, d, e) => b[0] === a && b[1] === c && b[2] === d && b[3] === e;
197
+ if (is(0x80, 0x37, 0x12, 0x40)) return { bytes, reordered: null }; // .z64 big — native
198
+ if (is(0x37, 0x80, 0x40, 0x12)) { // .v64 byteswapped — swap pairs
199
+ const out = new Uint8Array(b.length & ~1);
200
+ for (let i = 0; i + 1 < b.length; i += 2) { out[i] = b[i + 1]; out[i + 1] = b[i]; }
201
+ return { bytes: out, reordered: "v64 (byteswapped) → z64" };
202
+ }
203
+ if (is(0x40, 0x12, 0x37, 0x80)) { // .n64 little — swap dwords
204
+ const out = new Uint8Array(b.length & ~3);
205
+ 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]; }
206
+ return { bytes: out, reordered: "n64 (little-endian dwords) → z64" };
207
+ }
208
+ return { bytes, reordered: null }; // unknown header — leave as-is
209
+ }
143
210
 
144
211
  /** Build the common rizin invocation context for a ROM + platform. Returns
145
212
  * { romBytes, arch, bits, note } — arch null means let rizin sniff. */
@@ -175,11 +242,86 @@ async function loadContext(romPath, platformOverride) {
175
242
  loadBase = romBytes[0] | (romBytes[1] << 8);
176
243
  romBytes = romBytes.subarray(2);
177
244
  }
245
+ const warningsEarly = [];
246
+ // N64: normalize to big-endian (.z64) byte order, then the ROM header's entry
247
+ // point (big-endian word at 0x08) is the CPU load base. The 0x1000-byte header
248
+ // (IPL3 bootcode) precedes the game; rizin analyzes the whole image but the
249
+ // entry tells functions where code starts.
250
+ let codeStart = 0; // file offset where executable code begins (post-header)
251
+ if (platform === "n64") {
252
+ const norm = normalizeN64ByteOrder(romBytes);
253
+ romBytes = norm.bytes;
254
+ if (norm.reordered) warningsEarly.push(`N64 ROM was ${norm.reordered} byte order — normalized to z64 (big-endian) for analysis.`);
255
+ if (romBytes.length >= 0x0c) {
256
+ // entry point: big-endian 32-bit at offset 0x08.
257
+ loadBase = (romBytes[0x08] << 24) | (romBytes[0x09] << 16) | (romBytes[0x0a] << 8) | romBytes[0x0b];
258
+ }
259
+ // The IPL3 bootcode occupies the first 0x1000 bytes; game code starts after it.
260
+ // rizin's `aaa` doesn't recognize a raw N64 ROM's entry, so analysis is seeded
261
+ // here (see ANALYSIS_SEED).
262
+ codeStart = 0x1000;
263
+ }
264
+ // PS1 PS-EXE: a 2048-byte header; the load (t_addr) is a little-endian word at
265
+ // 0x18, and the code follows the header. Strip the header so rizin sees code at
266
+ // the load address. A raw .bin (no PS-EXE magic) is left flat.
267
+ if (platform === "ps1" && romBytes.length >= 0x800 &&
268
+ romBytes[0] === 0x50 && romBytes[1] === 0x53 && romBytes[2] === 0x2d && romBytes[3] === 0x58) { // "PS-X"
269
+ loadBase = (romBytes[0x18] | (romBytes[0x19] << 8) | (romBytes[0x1a] << 16) | (romBytes[0x1b] << 24)) >>> 0;
270
+ const text = romBytes.subarray(0x800);
271
+ // rizin ignores -B on a raw malloc:// buffer, so it addresses flat from 0. PS1
272
+ // jal targets are ABSOLUTE VAs (e.g. jal 0x80010518) — to let rizin follow them,
273
+ // left-pad the .text by the load address's low 20 bits so flat offset N == the
274
+ // VA's low bits (jal masks to a 28-bit region). The high bits (0x80000000) are
275
+ // added back as `rebase`. Without this, every cross-function call dangles and
276
+ // only the entry blob is discovered. (Cap the pad so a weird t_addr can't OOM.)
277
+ const lowPad = loadBase & 0x000fffff;
278
+ if (lowPad > 0 && lowPad <= 0x200000) {
279
+ const padded = new Uint8Array(lowPad + text.length);
280
+ padded.set(text, lowPad);
281
+ romBytes = padded;
282
+ codeStart = lowPad;
283
+ } else {
284
+ romBytes = text;
285
+ }
286
+ }
287
+ // Dreamcast SH-4: KOS produces an ELF (load base 0x8c010000) or a stripped flat
288
+ // binary. For an ELF, strip to the first PT_LOAD segment's bytes + take its vaddr
289
+ // as loadBase. Like PS1, SH-4 uses PC-relative + absolute addressing, so left-pad
290
+ // so flat offset == the VA's low bits and add the high bits back as rebase.
291
+ if (platform === "dreamcast") {
292
+ let text = romBytes, loadVa = 0x8c010000;
293
+ if (romBytes.length >= 0x34 && romBytes[0] === 0x7f && romBytes[1] === 0x45 &&
294
+ romBytes[2] === 0x4c && romBytes[3] === 0x46) { // "\x7fELF"
295
+ // ELF32 LE: e_phoff @0x1c, e_phentsize @0x2a, e_phnum @0x2c. Find first PT_LOAD.
296
+ const u32 = (o) => (romBytes[o] | (romBytes[o + 1] << 8) | (romBytes[o + 2] << 16) | (romBytes[o + 3] << 24)) >>> 0;
297
+ const u16 = (o) => romBytes[o] | (romBytes[o + 1] << 8);
298
+ const phoff = u32(0x1c), phentsize = u16(0x2a), phnum = u16(0x2c);
299
+ for (let i = 0; i < phnum; i++) {
300
+ const ph = phoff + i * phentsize;
301
+ if (u32(ph) === 1) { // PT_LOAD
302
+ const p_offset = u32(ph + 4), p_vaddr = u32(ph + 8), p_filesz = u32(ph + 16);
303
+ loadVa = p_vaddr >>> 0;
304
+ text = romBytes.subarray(p_offset, p_offset + p_filesz);
305
+ break;
306
+ }
307
+ }
308
+ }
309
+ loadBase = loadVa >>> 0;
310
+ const lowPad = loadBase & 0x000fffff;
311
+ if (lowPad > 0 && lowPad <= 0x200000) {
312
+ const padded = new Uint8Array(lowPad + text.length);
313
+ padded.set(text, lowPad);
314
+ romBytes = padded;
315
+ codeStart = lowPad;
316
+ } else {
317
+ romBytes = text;
318
+ }
319
+ }
178
320
 
179
321
  // A6: container/format sniff. Some dumps are interleaved/headered such that a
180
322
  // FLAT read scrambles every byte → fake "bad instruction" noise everywhere.
181
323
  // Detect + auto-correct, and warn so a flat disasm isn't silently wrong.
182
- const warnings = [];
324
+ const warnings = [...warningsEarly];
183
325
  if (platform === "genesis") {
184
326
  const smd = deinterleaveSmd(romBytes);
185
327
  if (smd) {
@@ -188,7 +330,7 @@ async function loadContext(romPath, platformOverride) {
188
330
  "auto-deinterleaved before analysis. A flat read of the original would scramble every instruction.");
189
331
  }
190
332
  }
191
- return { platform, romBytes, arch, bits: BITS[arch], approx, loadBase, warnings };
333
+ return { platform, romBytes, arch, bits: BITS[arch], endian: RIZIN_ENDIAN[platform], approx, loadBase, codeStart, warnings };
192
334
  }
193
335
 
194
336
  /** Detect + reverse Sega Mega Drive SMD interleaving. An .smd dump is a 512-byte
@@ -227,11 +369,12 @@ function hx(n) { return "0x" + (n >>> 0).toString(16); }
227
369
  * @returns {{platform, count, functions: Array<{address, name, size, nbbs, cc, callers, callees}>}}
228
370
  */
229
371
  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" });
372
+ const { platform, romBytes, arch, bits, endian, loadBase, codeStart, warnings } = await loadContext(romPath, platformOverride);
373
+ const { baddr, seedAt, rebase } = mipsAnalysisBase({ arch, platform, loadBase, codeStart });
374
+ const fns = await runRizinJson({ romBytes, arch, bits, endian, baddr, commands: `${analysisSeed({ arch, codeStart: seedAt })}; aflj` });
232
375
  const functions = fns.map((f) => ({
233
- address: f.offset,
234
- addressHex: hx(f.offset),
376
+ address: (f.offset + rebase) >>> 0,
377
+ addressHex: hx((f.offset + rebase) >>> 0),
235
378
  name: f.name,
236
379
  size: f.size,
237
380
  nbbs: f.nbbs, // basic-block count
@@ -269,30 +412,35 @@ export async function analyzeFunctions(romPath, platformOverride) {
269
412
  */
270
413
  export async function analyzeCfg(romPath, address, platformOverride) {
271
414
  if (address == null) throw new Error("analyze cfg: address required");
272
- const { platform, romBytes, arch, bits, loadBase } = await loadContext(romPath, platformOverride);
415
+ const { platform, romBytes, arch, bits, endian, loadBase, codeStart } = await loadContext(romPath, platformOverride);
273
416
  // afbj = basic blocks of the function as JSON: each block has addr/size/jump/
274
417
  // fail/ninstr. `jump` is the taken edge; `fail` (present only on conditional
275
418
  // blocks) is the fall-through. This is the structured CFG source — `agf json`
276
419
  // only gives a text body blob with untyped out_nodes.
420
+ // MIPS: seed the call graph first (the address is a vaddr from `functions`,
421
+ // which rizin recovers during `aac`). PS1 rebases to loadBase so the vaddr lines up.
422
+ const { baddr, seedAt, rebase } = mipsAnalysisBase({ arch, platform, loadBase, codeStart });
423
+ const flat = ((address >>> 0) - rebase) >>> 0; // VA → flat offset rizin uses
424
+ const rb = (a) => (a == null ? a : (a + rebase) >>> 0);
277
425
  const blocks = await runRizinJson({
278
- romBytes, arch, bits, baddr: loadBase || undefined,
279
- commands: `aaa; af @ ${hx(address)}; afbj @ ${hx(address)}`,
426
+ romBytes, arch, bits, endian, baddr,
427
+ commands: `${analysisSeed({ arch, codeStart: seedAt })}; af @ ${hx(flat)}; afbj @ ${hx(flat)}`,
280
428
  });
281
429
  if (!Array.isArray(blocks) || blocks.length === 0) {
282
430
  return { platform, arch, address, addressHex: hx(address), nodes: [], edges: [], note: "no function/blocks at address" };
283
431
  }
284
432
  const nodes = blocks.map((b) => ({
285
- id: b.addr,
286
- address: b.addr,
287
- addressHex: hx(b.addr),
433
+ id: rb(b.addr),
434
+ address: rb(b.addr),
435
+ addressHex: hx(rb(b.addr)),
288
436
  size: b.size,
289
437
  ninstr: b.ninstr,
290
438
  }));
291
439
  const edges = [];
292
440
  for (const b of blocks) {
293
441
  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" });
442
+ if (b.jump != null) edges.push({ from: rb(b.addr), to: rb(b.jump), type: conditional ? "branch_true" : "jump_or_fall" });
443
+ if (b.fail != null) edges.push({ from: rb(b.addr), to: rb(b.fail), type: "branch_false" });
296
444
  }
297
445
  return {
298
446
  platform, arch,
@@ -308,10 +456,13 @@ export async function analyzeCfg(romPath, address, platformOverride) {
308
456
  */
309
457
  export async function analyzeXrefs(romPath, address, platformOverride) {
310
458
  if (address == null) throw new Error("analyze xrefs: address required");
311
- const { platform, romBytes, arch, bits, loadBase } = await loadContext(romPath, platformOverride);
459
+ const { platform, romBytes, arch, bits, endian, loadBase, codeStart } = await loadContext(romPath, platformOverride);
460
+ const { baddr, seedAt, rebase } = mipsAnalysisBase({ arch, platform, loadBase, codeStart });
461
+ const flat = ((address >>> 0) - rebase) >>> 0;
462
+ const rb = (a) => (a == null ? a : (a + rebase) >>> 0);
312
463
  let refs;
313
464
  try {
314
- refs = await runRizinJson({ romBytes, arch, bits, baddr: loadBase || undefined, commands: `aaa; axtj @ ${hx(address)}` });
465
+ refs = await runRizinJson({ romBytes, arch, bits, endian, baddr, commands: `${analysisSeed({ arch, codeStart: seedAt })}; axtj @ ${hx(flat)}` });
315
466
  } catch (e) {
316
467
  // axtj prints nothing (not even `[]`) when there are zero refs → our JSON
317
468
  // guard throws. Treat "no JSON" as "no refs".
@@ -319,9 +470,9 @@ export async function analyzeXrefs(romPath, address, platformOverride) {
319
470
  else throw e;
320
471
  }
321
472
  const out = (refs ?? []).map((r) => ({
322
- from: r.from,
323
- fromHex: hx(r.from),
324
- to: r.to,
473
+ from: rb(r.from),
474
+ fromHex: hx(rb(r.from)),
475
+ to: rb(r.to),
325
476
  type: (r.type || "").toLowerCase(), // CALL / CODE / DATA / STRING
326
477
  opcode: r.opcode,
327
478
  }));
@@ -333,23 +484,25 @@ export async function analyzeXrefs(romPath, address, platformOverride) {
333
484
  * analysis pass. The "give me the shape of this ROM" call.
334
485
  */
335
486
  export async function analyzeStructure(romPath, platformOverride) {
336
- const { platform, romBytes, arch, bits, loadBase } = await loadContext(romPath, platformOverride);
337
- const baddr = loadBase || undefined;
487
+ const { platform, romBytes, arch, bits, endian, loadBase, codeStart } = await loadContext(romPath, platformOverride);
488
+ const { baddr, seedAt, rebase } = mipsAnalysisBase({ arch, platform, loadBase, codeStart });
489
+ const seed = analysisSeed({ arch, codeStart: seedAt });
338
490
  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(() => []),
491
+ runRizinJson({ romBytes, arch, bits, endian, baddr, commands: `${seed}; aflj` }).catch(() => []),
492
+ runRizinJson({ romBytes, arch, bits, endian, baddr, commands: `${seed}; izj` }).catch(() => []),
493
+ runRizinJson({ romBytes, arch, bits, baddr, commands: `${seed}; iej` }).catch(() => []),
342
494
  ]);
495
+ const rb = (a) => (a == null ? a : (a + rebase) >>> 0);
343
496
  return {
344
497
  platform, arch,
345
498
  functionCount: Array.isArray(fns) ? fns.length : 0,
346
499
  stringCount: Array.isArray(strings) ? strings.length : 0,
347
- entrypoints: (Array.isArray(entries) ? entries : []).map((e) => ({ address: e.vaddr, addressHex: hx(e.vaddr) })),
500
+ entrypoints: (Array.isArray(entries) ? entries : []).map((e) => ({ address: rb(e.vaddr), addressHex: hx(rb(e.vaddr)) })),
348
501
  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,
502
+ address: rb(f.offset), addressHex: hx(rb(f.offset)), name: f.name, size: f.size, callers: f.indegree ?? 0,
350
503
  })),
351
504
  strings: (Array.isArray(strings) ? strings : []).slice(0, 256).map((s) => ({
352
- address: s.vaddr, addressHex: hx(s.vaddr), value: s.string,
505
+ address: rb(s.vaddr), addressHex: hx(rb(s.vaddr)), value: s.string,
353
506
  })),
354
507
  };
355
508
  }
@@ -507,6 +660,48 @@ export async function analyzeDecompile(romPath, address, platformOverride) {
507
660
  // to pure garbage.
508
661
  if (platform === "genesis") romBytes = deinterleaveSmd(romBytes) ?? romBytes;
509
662
 
663
+ // MIPS (N64/PS1): `address` is a vaddr from target='functions'. Map it to a file
664
+ // offset and decompile via the MIPS SLEIGH spec (MIPS:BE:32 for N64, MIPS:LE:32
665
+ // for PS1). N64: normalize byte order, fileOff = vaddr - entryVaddr + 0x1000 (post
666
+ // IPL3). PS1: strip PS-EXE, fileOff = vaddr - loadAddr.
667
+ if (platform === "n64" || platform === "ps1" || platform === "dreamcast") {
668
+ let fileOff;
669
+ if (platform === "n64") {
670
+ romBytes = normalizeN64ByteOrder(romBytes).bytes;
671
+ const entry = ((romBytes[0x08] << 24) | (romBytes[0x09] << 16) | (romBytes[0x0a] << 8) | romBytes[0x0b]) >>> 0;
672
+ fileOff = ((address >>> 0) - entry + 0x1000) >>> 0;
673
+ } else if (platform === "dreamcast") {
674
+ // SH-4: strip the ELF to its first PT_LOAD segment (vaddr = loadBase), or treat
675
+ // a raw image as flat at 0x8c010000. fileOff = address - segment vaddr.
676
+ let loadVa = 0x8c010000;
677
+ if (romBytes.length >= 0x34 && romBytes[0] === 0x7f && romBytes[1] === 0x45 && romBytes[2] === 0x4c && romBytes[3] === 0x46) {
678
+ const u32 = (o) => (romBytes[o] | (romBytes[o + 1] << 8) | (romBytes[o + 2] << 16) | (romBytes[o + 3] << 24)) >>> 0;
679
+ const u16 = (o) => romBytes[o] | (romBytes[o + 1] << 8);
680
+ const phoff = u32(0x1c), phentsize = u16(0x2a), phnum = u16(0x2c);
681
+ for (let i = 0; i < phnum; i++) {
682
+ const ph = phoff + i * phentsize;
683
+ if (u32(ph) === 1) { loadVa = u32(ph + 8) >>> 0; romBytes = romBytes.subarray(u32(ph + 4), u32(ph + 4) + u32(ph + 16)); break; }
684
+ }
685
+ }
686
+ fileOff = ((address >>> 0) - loadVa) >>> 0;
687
+ } else {
688
+ let loadAddr = 0;
689
+ if (romBytes.length >= 0x800 && romBytes[0] === 0x50 && romBytes[1] === 0x53 && romBytes[2] === 0x2d && romBytes[3] === 0x58) {
690
+ loadAddr = (romBytes[0x18] | (romBytes[0x19] << 8) | (romBytes[0x1a] << 16) | (romBytes[0x1b] << 24)) >>> 0;
691
+ romBytes = romBytes.subarray(0x800);
692
+ }
693
+ fileOff = ((address >>> 0) - loadAddr) >>> 0;
694
+ }
695
+ if (fileOff >= romBytes.length) {
696
+ 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'.`);
697
+ }
698
+ const rm = await decompileFunction({ platform, romBytes, fileOffset: fileOff });
699
+ return {
700
+ platform, langid: rm.langid, address, addressHex: hx(address),
701
+ code: prettyDecompile(rm.code, platform), warnings: rm.warnings,
702
+ };
703
+ }
704
+
510
705
  // SNES: banked 24-bit space. `address` is a LoROM/HiROM CPU address (what
511
706
  // target='functions'/'cfg' report). Lay the cart out by CPU address so BOTH
512
707
  // the function address AND its in-bank/JSL operands resolve, then decompile at
@@ -47,6 +47,13 @@ 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",
54
+ // Dreamcast = SH-4 (SuperH), little-endian. Ghidra ships SuperH4 in its stock
55
+ // SLEIGH; we compile SuperH4_le.sla (see scripts/build-decompiler.sh).
56
+ dreamcast: "SuperH4:LE:32:default",
50
57
  };
51
58
 
52
59
  /**
@@ -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>