romdevtools 0.44.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 (61) hide show
  1. package/CHANGELOG.md +293 -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/disasm.js +2 -0
  37. package/src/mcp/tools/frame.js +18 -9
  38. package/src/mcp/tools/lifecycle.js +1 -1
  39. package/src/mcp/tools/platform-tools.js +20 -4
  40. package/src/mcp/tools/platforms.js +38 -4
  41. package/src/mcp/tools/project.js +100 -0
  42. package/src/mcp/tools/rendering-context.js +2 -1
  43. package/src/mcp/tools/toolchain.js +1 -1
  44. package/src/platforms/n64/lib/c/n64.c +196 -0
  45. package/src/platforms/n64/lib/c/n64.h +68 -0
  46. package/src/platforms/ps1/lib/c/psx.c +200 -0
  47. package/src/platforms/ps1/lib/c/psx.h +83 -0
  48. package/src/toolchains/index.js +35 -0
  49. package/src/toolchains/mips-c/lib/be/libc.a +0 -0
  50. package/src/toolchains/mips-c/lib/be/libgcc.a +0 -0
  51. package/src/toolchains/mips-c/lib/be/libm.a +0 -0
  52. package/src/toolchains/mips-c/lib/el/libc.a +0 -0
  53. package/src/toolchains/mips-c/lib/el/libm.a +0 -0
  54. package/src/toolchains/mips-c/lib/n64-crt0.s +21 -0
  55. package/src/toolchains/mips-c/lib/n64-ipl3.s +30 -0
  56. package/src/toolchains/mips-c/lib/n64.ld +15 -0
  57. package/src/toolchains/mips-c/lib/ps1-crt0.s +20 -0
  58. package/src/toolchains/mips-c/lib/ps1.ld +15 -0
  59. package/src/toolchains/mips-c/lib/softint.c +37 -0
  60. package/src/toolchains/mips-c/mips-c.js +155 -0
  61. package/src/toolchains/mips-elf-gcc/gcc.js +130 -0
@@ -0,0 +1,200 @@
1
+ /* psx.c — PS1 helpers + software 3D pipeline (see psx.h). */
2
+ #include "psx.h"
3
+
4
+ /* ── GPU bring-up ── */
5
+ void psx_init(void)
6
+ {
7
+ GP1 = 0x00000000u; /* reset */
8
+ GP1 = 0x08000001u; /* display mode 320x240 NTSC 15bpp */
9
+ GP1 = 0x03000000u; /* display enable (arg 0 = on) */
10
+ GP1 = 0x05000000u; /* display start (0,0) */
11
+ GP1 = 0x06000000u | (0x260u | ((0x260u + 320u*8u) << 12));
12
+ GP1 = 0x07000000u | (16u | ((16u + 240u) << 10));
13
+ GP0 = 0xE3000000u; /* draw area TL (0,0) */
14
+ GP0 = 0xE4000000u | (319u) | (239u << 10); /* draw area BR */
15
+ GP0 = 0xE5000000u; /* draw offset (0,0) */
16
+ }
17
+
18
+ void psx_clear(unsigned int bgr) {
19
+ /* GP0 0x02 = fast fill of a VRAM rectangle (ignores draw area, no blending). */
20
+ GP0 = 0x02000000u | (bgr & 0x00FFFFFFu);
21
+ GP0 = 0; /* (x,y) = (0,0) */
22
+ GP0 = (240u << 16) | 320u; /* (h,w) */
23
+ }
24
+
25
+ void psx_rect(int x, int y, int w, int h, unsigned int bgr)
26
+ {
27
+ /* GP0 0x60 = monochrome variable-size RECTANGLE: color, (y,x) corner, (h,w) size. */
28
+ GP0 = 0x60000000u | (bgr & 0x00FFFFFFu);
29
+ GP0 = (((unsigned)y & 0xFFFF) << 16) | ((unsigned)x & 0xFFFF);
30
+ GP0 = (((unsigned)h & 0xFFFF) << 16) | ((unsigned)w & 0xFFFF);
31
+ }
32
+
33
+ void psx_tri2d(int x0,int y0,int x1,int y1,int x2,int y2,unsigned int bgr)
34
+ {
35
+ GP0 = 0x20000000u | (bgr & 0x00FFFFFFu); /* flat opaque triangle */
36
+ GP0 = ((unsigned)(y0)<<16)|((unsigned)(x0)&0xFFFF);
37
+ GP0 = ((unsigned)(y1)<<16)|((unsigned)(x1)&0xFFFF);
38
+ GP0 = ((unsigned)(y2)<<16)|((unsigned)(x2)&0xFFFF);
39
+ }
40
+
41
+ void psx_vsync(void) { volatile int i; for (i = 0; i < 60000; i++) { } }
42
+
43
+ /* ── input: read controller port 0 via the SIO (the way real PS1 homebrew does;
44
+ the HLE core feeds host input into this SIO emulation). The digital pad reply
45
+ is 0x5A then two button bytes (active-LOW). We return active-HIGH. ── */
46
+ #define SIO_DATA (*(volatile unsigned char*)0x1F801040)
47
+ #define SIO_STAT (*(volatile unsigned short*)0x1F801044)
48
+ #define SIO_CTRL (*(volatile unsigned short*)0x1F80104A)
49
+ #define SIO_BAUD (*(volatile unsigned short*)0x1F80104E)
50
+ #define SIO_MODE (*(volatile unsigned short*)0x1F801048)
51
+
52
+ static unsigned char sio_xfer(unsigned char out) {
53
+ volatile int t;
54
+ while (!(SIO_STAT & 0x4)) { } /* TX ready */
55
+ SIO_DATA = out;
56
+ for (t = 0; t < 200; t++) { } /* settle */
57
+ while (!(SIO_STAT & 0x2)) { } /* RX ready */
58
+ return SIO_DATA;
59
+ }
60
+
61
+ unsigned int psx_pad(void) {
62
+ unsigned char b0, b1; volatile int t;
63
+ /* init SIO for controller (8N1, ~250kbps) */
64
+ SIO_MODE = 0x000D; SIO_BAUD = 0x0088; SIO_CTRL = 0x1003; /* TX/RX en + DTR + port0 */
65
+ sio_xfer(0x01); /* address: controller */
66
+ if (sio_xfer(0x42) == 0xFF) { SIO_CTRL = 0; return 0; } /* request data; 0xFF=no pad */
67
+ sio_xfer(0x00); /* discard 0x5A */
68
+ b0 = sio_xfer(0x00); /* buttons lo (active-low) */
69
+ b1 = sio_xfer(0x00); /* buttons hi (active-low) */
70
+ SIO_CTRL = 0; /* deselect */
71
+ for (t = 0; t < 100; t++) { }
72
+ return (~((unsigned int)b0 | ((unsigned int)b1 << 8))) & 0xFFFF; /* → active-high */
73
+ }
74
+ int psx_pressed(unsigned int mask) { return (psx_pad() & mask) ? 1 : 0; }
75
+
76
+ /* ── trig: 256-entry quarter→full sine, arg in "binary angle" 16.16 where
77
+ FIX(256) = one full turn. ── */
78
+ static const short SINTAB[64] = {
79
+ 0,804,1608,2410,3212,4011,4808,5602,6393,7179,7962,8739,9512,10278,11039,11793,
80
+ 12539,13279,14010,14732,15446,16151,16846,17530,18204,18868,19519,20159,20787,21403,
81
+ 22005,22594,23170,23731,24279,24811,25329,25832,26319,26790,27245,27683,28105,28510,
82
+ 28898,29268,29621,29956,30273,30571,30852,31113,31356,31580,31785,31971,32137,32285,
83
+ 32412,32521,32609,32678,32728,32757 };
84
+ static fix sin_lut(int idx) { /* idx 0..255 → 16.16 sine */
85
+ int q = (idx >> 6) & 3, p = idx & 63; int v;
86
+ if (q == 0) v = SINTAB[p];
87
+ else if (q == 1) v = SINTAB[63 - p];
88
+ else if (q == 2) v = -SINTAB[p];
89
+ else v = -SINTAB[63 - p];
90
+ return ((fix)v << 16) / 32768; /* normalize to 16.16 (~1.0 peak) */
91
+ }
92
+ fix psx_sin(fix a) { return sin_lut((F2I(a)) & 255); }
93
+ fix psx_cos(fix a) { return sin_lut((F2I(a) + 64) & 255); }
94
+
95
+ /* ── camera + model state ── */
96
+ static fix cam_x, cam_y, cam_z, cam_cy, cam_sy, cam_cp, cam_sp;
97
+ static fix mdl_x, mdl_y, mdl_z, mdl_cy, mdl_sy;
98
+
99
+ void psx_camera(fix ex, fix ey, fix ez, fix yaw, fix pitch)
100
+ {
101
+ cam_x = ex; cam_y = ey; cam_z = ez;
102
+ cam_cy = psx_cos(yaw); cam_sy = psx_sin(yaw);
103
+ cam_cp = psx_cos(pitch); cam_sp = psx_sin(pitch);
104
+ }
105
+ void psx_model(fix tx, fix ty, fix tz, fix yaw)
106
+ {
107
+ mdl_x = tx; mdl_y = ty; mdl_z = tz;
108
+ mdl_cy = psx_cos(yaw); mdl_sy = psx_sin(yaw);
109
+ }
110
+
111
+ /* model-space vertex → camera space (16.16). */
112
+ static Vec3 to_cam(Vec3 v)
113
+ {
114
+ /* model yaw (around Y) + translate */
115
+ fix mx = FMUL(v.x, mdl_cy) + FMUL(v.z, mdl_sy);
116
+ fix mz = -FMUL(v.x, mdl_sy) + FMUL(v.z, mdl_cy);
117
+ fix wx = mx + mdl_x, wy = v.y + mdl_y, wz = mz + mdl_z;
118
+ /* world → relative to camera */
119
+ fix rx = wx - cam_x, ry = wy - cam_y, rz = wz - cam_z;
120
+ /* camera yaw */
121
+ fix cx = FMUL(rx, cam_cy) - FMUL(rz, cam_sy);
122
+ fix cz = FMUL(rx, cam_sy) + FMUL(rz, cam_cy);
123
+ /* camera pitch */
124
+ fix cy = FMUL(ry, cam_cp) - FMUL(cz, cam_sp);
125
+ fix cz2 = FMUL(ry, cam_sp) + FMUL(cz, cam_cp);
126
+ Vec3 o; o.x = cx; o.y = cy; o.z = cz2; return o;
127
+ }
128
+
129
+ /* perspective project camera-space → screen. returns 0 if behind/near plane. */
130
+ #define NEAR FIXF(0.5f)
131
+ #define FOV FIX(220) /* focal length in pixels-ish */
132
+ static int project(Vec3 c, int *sx, int *sy)
133
+ {
134
+ if (c.z <= NEAR) return 0;
135
+ fix sxf = FDIV(FMUL(c.x, FOV), c.z);
136
+ fix syf = FDIV(FMUL(c.y, FOV), c.z);
137
+ *sx = (SCREEN_W / 2) + F2I(sxf);
138
+ *sy = (SCREEN_H / 2) - F2I(syf);
139
+ return 1;
140
+ }
141
+
142
+ fix psx_depth3(Vec3 a, Vec3 b, Vec3 c)
143
+ { Vec3 ca=to_cam(a), cb=to_cam(b), cc=to_cam(c); return (ca.z + cb.z + cc.z) / 3; }
144
+ fix psx_depth4(Vec3 a, Vec3 b, Vec3 c, Vec3 d)
145
+ { Vec3 ca=to_cam(a),cb=to_cam(b),cc=to_cam(c),cd=to_cam(d); return (ca.z+cb.z+cc.z+cd.z)/4; }
146
+
147
+ void psx_tri3d(Vec3 a, Vec3 b, Vec3 c, unsigned int bgr)
148
+ {
149
+ Vec3 ca=to_cam(a), cb=to_cam(b), cc=to_cam(c);
150
+ int x0,y0,x1,y1,x2,y2;
151
+ if (!project(ca,&x0,&y0) || !project(cb,&x1,&y1) || !project(cc,&x2,&y2)) return;
152
+ /* back-face cull (screen-space cross product) */
153
+ int cross = (x1-x0)*(y2-y0) - (x2-x0)*(y1-y0);
154
+ if (cross <= 0) return;
155
+ psx_tri2d(x0,y0,x1,y1,x2,y2,bgr);
156
+ }
157
+
158
+ void psx_tri3d_nc(Vec3 a, Vec3 b, Vec3 c, unsigned int bgr)
159
+ {
160
+ Vec3 ca=to_cam(a), cb=to_cam(b), cc=to_cam(c);
161
+ int x0,y0,x1,y1,x2,y2;
162
+ if (!project(ca,&x0,&y0) || !project(cb,&x1,&y1) || !project(cc,&x2,&y2)) return;
163
+ psx_tri2d(x0,y0,x1,y1,x2,y2,bgr); /* no back-face cull */
164
+ }
165
+ void psx_quad3d_nc(Vec3 a, Vec3 b, Vec3 c, Vec3 d, unsigned int bgr)
166
+ { psx_tri3d_nc(a,b,c,bgr); psx_tri3d_nc(a,c,d,bgr); }
167
+
168
+ void psx_quad3d(Vec3 a, Vec3 b, Vec3 c, Vec3 d, unsigned int bgr)
169
+ {
170
+ /* split into two tris; both share the back-face test inside */
171
+ psx_tri3d(a,b,c,bgr);
172
+ psx_tri3d(a,c,d,bgr);
173
+ }
174
+
175
+ /* ── RNG (xorshift) ── */
176
+ static unsigned int rng_state = 0x12345678u;
177
+ void psx_srand(unsigned int s) { rng_state = s ? s : 1; }
178
+ unsigned int psx_rand(void)
179
+ { unsigned int x = rng_state; x ^= x << 13; x ^= x >> 17; x ^= x << 5; return rng_state = x; }
180
+
181
+ /* ── blocky decimal number (3x5 cells scaled to 8x10), for HUD/score ── */
182
+ static const unsigned char DIGITS[10][5] = {
183
+ {0x7,0x5,0x5,0x5,0x7},{0x2,0x6,0x2,0x2,0x7},{0x7,0x1,0x7,0x4,0x7},{0x7,0x1,0x3,0x1,0x7},
184
+ {0x5,0x5,0x7,0x1,0x1},{0x7,0x4,0x7,0x1,0x7},{0x7,0x4,0x7,0x5,0x7},{0x7,0x1,0x1,0x1,0x1},
185
+ {0x7,0x5,0x7,0x5,0x7},{0x7,0x5,0x7,0x1,0x7} };
186
+ static void draw_digit(int x, int y, int d, unsigned int bgr)
187
+ {
188
+ int r, c;
189
+ for (r = 0; r < 5; r++)
190
+ for (c = 0; c < 3; c++)
191
+ if (DIGITS[d][r] & (1 << (2 - c)))
192
+ psx_rect(x + c*3, y + r*3, 3, 3, bgr);
193
+ }
194
+ void psx_number(int x, int y, unsigned int value, unsigned int bgr)
195
+ {
196
+ char buf[10]; int n = 0, i;
197
+ if (value == 0) buf[n++] = 0;
198
+ while (value && n < 10) { buf[n++] = value % 10; value /= 10; }
199
+ for (i = 0; i < n; i++) draw_digit(x + (n-1-i)*12, y, buf[i], bgr);
200
+ }
@@ -0,0 +1,83 @@
1
+ /* psx.h — PS1 (R3000) helper lib with a software 3D pipeline, for romdev.
2
+ *
3
+ * A minimal-but-real 3D engine + GPU/pad helpers — what the PlayStation is FOR.
4
+ * Fixed-point (16.16) vector math, a camera (position + yaw/pitch), perspective
5
+ * projection, and painter-sorted flat-shaded triangles/quads drawn through the
6
+ * GPU. No GTE/SDK dependency — pure C math, so the same pipeline ports to N64.
7
+ *
8
+ * Build: build({ platform:"ps1", language:"c" }). Output is a PS-EXE the HLE BIOS
9
+ * loads at 0x80010000; main() loops forever. Color is BGR (GPU native); RGB() packs.
10
+ *
11
+ * 2D is also available (psx_rect / psx_sprite) for HUDs + the puzzle game.
12
+ */
13
+ #ifndef ROMDEV_PSX_H
14
+ #define ROMDEV_PSX_H
15
+
16
+ /* ── fixed-point (16.16) ── */
17
+ typedef int fix; /* 16.16 */
18
+ #define FIX(n) ((fix)((n) << 16))
19
+ #define FIXF(f) ((fix)((f) * 65536.0f))
20
+ #define FMUL(a,b) ((fix)(((long long)(a) * (b)) >> 16))
21
+ #define FDIV(a,b) ((fix)(((long long)(a) << 16) / (b)))
22
+ #define F2I(a) ((a) >> 16)
23
+
24
+ typedef struct { fix x, y, z; } Vec3;
25
+
26
+ /* ── GPU ports ── */
27
+ #define GP0 (*(volatile unsigned int*)0x1F801810)
28
+ #define GP1 (*(volatile unsigned int*)0x1F801814)
29
+
30
+ /* ── pad (digital, active-HIGH: bit set = pressed) ── */
31
+ #define PAD_SELECT (1u<<0)
32
+ #define PAD_START (1u<<3)
33
+ #define PAD_UP (1u<<4)
34
+ #define PAD_RIGHT (1u<<5)
35
+ #define PAD_DOWN (1u<<6)
36
+ #define PAD_LEFT (1u<<7)
37
+ #define PAD_L1 (1u<<10)
38
+ #define PAD_R1 (1u<<11)
39
+ #define PAD_TRIANGLE (1u<<12)
40
+ #define PAD_CIRCLE (1u<<13)
41
+ #define PAD_CROSS (1u<<14)
42
+ #define PAD_SQUARE (1u<<15)
43
+
44
+ #define RGB(r,g,b) (((unsigned int)(b)<<16)|((unsigned int)(g)<<8)|(unsigned int)(r))
45
+ #define SCREEN_W 320
46
+ #define SCREEN_H 240
47
+
48
+ /* ── GPU / 2D ── */
49
+ void psx_init(void);
50
+ void psx_clear(unsigned int bgr);
51
+ void psx_rect(int x, int y, int w, int h, unsigned int bgr);
52
+ void psx_tri2d(int x0,int y0,int x1,int y1,int x2,int y2,unsigned int bgr);
53
+ void psx_vsync(void);
54
+
55
+ /* ── input ── */
56
+ unsigned int psx_pad(void); /* current buttons (active-high) */
57
+ int psx_pressed(unsigned int mask); /* 1 if any of mask is held */
58
+
59
+ /* ── 3D pipeline ── */
60
+ /* camera: eye position + yaw (around Y) + pitch (around X), all 16.16. */
61
+ void psx_camera(fix ex, fix ey, fix ez, fix yaw, fix pitch);
62
+ /* set a model translation+rotation applied before the camera (simple: yaw only). */
63
+ void psx_model(fix tx, fix ty, fix tz, fix yaw);
64
+ /* draw a flat triangle of 3 model-space vertices; projected + back-face/near
65
+ culled. Z-buffered by draw order (call back-to-front, or use psx_sort). */
66
+ void psx_tri3d(Vec3 a, Vec3 b, Vec3 c, unsigned int bgr);
67
+ void psx_quad3d(Vec3 a, Vec3 b, Vec3 c, Vec3 d, unsigned int bgr);
68
+ /* no-back-face-cull variants — for ground planes / floors (ambiguous winding). */
69
+ void psx_tri3d_nc(Vec3 a, Vec3 b, Vec3 c, unsigned int bgr);
70
+ void psx_quad3d_nc(Vec3 a, Vec3 b, Vec3 c, Vec3 d, unsigned int bgr);
71
+ /* returns the average camera-space Z of 3/4 model verts (for manual sorting). */
72
+ fix psx_depth3(Vec3 a, Vec3 b, Vec3 c);
73
+ fix psx_depth4(Vec3 a, Vec3 b, Vec3 c, Vec3 d);
74
+
75
+ /* ── misc ── */
76
+ fix psx_sin(fix a); /* a in 16.16 turns? no — a in fix radians-ish (see psx.c) */
77
+ fix psx_cos(fix a);
78
+ unsigned int psx_rand(void);
79
+ void psx_srand(unsigned int seed);
80
+ /* draw a small decimal number (HUD/score), 8x8 blocky digits. */
81
+ void psx_number(int x, int y, unsigned int value, unsigned int bgr);
82
+
83
+ #endif
@@ -85,6 +85,12 @@ const LANGUAGE_TOOLCHAIN = {
85
85
  c: { toolchain: "sdcc", available: true, note: "C for MSX via SDCC's z80 port. Cartridge ROM with the 'AB' header + INIT pointer at $4000; boots on C-BIOS (open MSX BIOS). MSX2 by default (runs MSX1 carts too)." },
86
86
  asm: { toolchain: "sdcc", available: true },
87
87
  },
88
+ ps1: {
89
+ c: { toolchain: "mips-elf-gcc", available: true, note: "C for PS1 via gcc 14.2.0 + binutils + newlib (mips-elf, little-endian R3000), compiled to WASM. The bare path: a minimal crt0 sets the stack + clears .bss + calls main(); the output is a PS-EXE the HLE BIOS loads (load at 0x80010000). No SDK (PSn00bSDK) yet — bring your own GPU/SPU register writes, or use this for logic. write to GPU ports 0x1F801810/0x1F801814." },
90
+ },
91
+ n64: {
92
+ c: { toolchain: "mips-elf-gcc", available: true, note: "C for N64 via gcc 14.2.0 + binutils + newlib (mips-elf, big-endian R4300), compiled to WASM. Bare path: minimal crt0 (stack + .bss + main()) → flat code image at 0x80000400. NOTE: a fully bootable N64 ROM needs the IPL3 bootcode + a libdragon-style header (libdragon SDK forthcoming) — this path exercises the toolchain + is the basis for the SDK." },
93
+ },
88
94
  };
89
95
 
90
96
  /**
@@ -115,6 +121,8 @@ const PLATFORM_DEFAULT_LANGUAGE = {
115
121
  spc700: "asm",
116
122
  pce: "c",
117
123
  msx: "c",
124
+ ps1: "c",
125
+ n64: "c",
118
126
  };
119
127
 
120
128
  /**
@@ -467,6 +475,33 @@ export async function buildForPlatform(args) {
467
475
  // below (SDCC_PORTS["gb"] = sm83 port).
468
476
  }
469
477
 
478
+ if (args.platform === "ps1" || args.platform === "n64") {
479
+ // MIPS C: the bare gcc+newlib+libgcc path (no SDK yet). cc1→as→ld→objcopy
480
+ // through the mips-elf-gcc WASM toolchain; PS1 (R3000, little-endian) wraps the
481
+ // image in a PS-EXE the HLE BIOS loads; N64 (R4300, big-endian) emits a flat
482
+ // .bin (real N64 boot needs libdragon — forthcoming). language defaults to "c".
483
+ const { buildMipsC } = await import("./mips-c/mips-c.js");
484
+ const r = await buildMipsC({
485
+ source: args.source,
486
+ sources: args.sources,
487
+ headers: args.includes,
488
+ platform: args.platform,
489
+ cc1Options: args.options,
490
+ });
491
+ return {
492
+ ok: r.ok,
493
+ binary: r.binary,
494
+ listing: "",
495
+ symbols: r.symbols ?? "",
496
+ log: r.log,
497
+ issues: parseBuildLog(r.log),
498
+ exitCode: r.exitCode,
499
+ toolchain: "mips-elf-gcc",
500
+ stage: r.stage,
501
+ ...(r.crash ? { crash: r.crash } : {}),
502
+ };
503
+ }
504
+
470
505
  if (args.platform === "gba") {
471
506
  // R24 + R28: language:"c" routes through the arm-none-eabi gcc +
472
507
  // binutils WASM toolchain (cc1-arm → as → ld → objcopy). Three
@@ -0,0 +1,21 @@
1
+ /* Minimal N64 (R4300) crt0 — bare stack + .bss clear + main(). NOTE: a real
2
+ bootable N64 ROM needs the IPL3 bootcode + a libdragon-style header; this
3
+ minimal crt0 exercises the mips-elf toolchain (big-endian) and produces a flat
4
+ code image. Full N64 boot = libdragon (STAGE 3). */
5
+ .set noreorder
6
+ .section .text.start, "ax"
7
+ .global _start
8
+ _start:
9
+ la $sp, __sp_top
10
+ la $t0, __bss_start
11
+ la $t1, __bss_end
12
+ 1: beq $t0, $t1, 2f
13
+ nop
14
+ sw $zero, 0($t0)
15
+ addiu $t0, $t0, 4
16
+ b 1b
17
+ nop
18
+ 2: jal main
19
+ nop
20
+ 3: b 3b
21
+ nop
@@ -0,0 +1,30 @@
1
+ /* Minimal N64 IPL3 (clean-room): runs in SP DMEM at 0xA4000040 after the HLE/real
2
+ PIF boot copies ROM[0x40..0xFFF] here. Its job: PI-DMA the game from cart
3
+ (0xB0001000) to RDRAM (0x80000400), then jump to the game entry. Assembled to
4
+ the 0xFC0-byte IPL3 slot. */
5
+ .set noreorder
6
+ .set noat
7
+ .global ipl3_start
8
+ ipl3_start:
9
+ /* PI DMA: copy GAME_SIZE bytes from cart 0x10001000 to RDRAM 0x00000400 */
10
+ lui $t0, 0xA460 /* PI base 0xA4600000 */
11
+ /* PI_DRAM_ADDR = 0x00000400 (RDRAM dest, phys) */
12
+ li $t1, 0x00000400
13
+ sw $t1, 0x00($t0) /* PI_DRAM_ADDR_REG */
14
+ /* PI_CART_ADDR = 0x10001000 (cart src, phys; game starts at ROM 0x1000) */
15
+ lui $t1, 0x1000
16
+ ori $t1, $t1, 0x1000
17
+ sw $t1, 0x04($t0) /* PI_CART_ADDR_REG */
18
+ /* PI_WR_LEN = GAME_SIZE-1 (write to RDRAM) */
19
+ li $t1, (GAME_SIZE - 1)
20
+ sw $t1, 0x0C($t0) /* PI_WR_LEN_REG starts the DMA */
21
+ 1: /* wait for PI not busy (PI_STATUS bit0/1) */
22
+ lw $t2, 0x10($t0)
23
+ andi $t2, $t2, 0x3
24
+ bnez $t2, 1b
25
+ nop
26
+ /* jump to the game entry in RDRAM (cached kseg0) */
27
+ lui $t3, 0x8000
28
+ ori $t3, $t3, 0x0400
29
+ jr $t3
30
+ nop
@@ -0,0 +1,15 @@
1
+ OUTPUT_FORMAT("elf32-bigmips")
2
+ OUTPUT_ARCH(mips)
3
+ ENTRY(_start)
4
+ MEMORY { ram (rwx) : ORIGIN = 0x80000400, LENGTH = 0x3FFC00 }
5
+ SECTIONS {
6
+ .text : { *(.text.start) *(.text*) *(.rodata*) } > ram
7
+ .data : { *(.data*) *(.sdata*) *(.lit8) *(.lit4) *(.scommon) } > ram
8
+ . = ALIGN(8);
9
+ __bss_start = .;
10
+ .bss : { *(.bss*) *(.sbss*) *(.scommon) *(COMMON) } > ram
11
+ . = ALIGN(8);
12
+ __bss_end = .;
13
+ __sp_top = 0x803FFFF0;
14
+ /DISCARD/ : { *(.MIPS.abiflags) *(.reginfo) *(.comment) }
15
+ }
@@ -0,0 +1,20 @@
1
+ /* Minimal PS1 (R3000) crt0 — sets the stack, clears .bss, calls main(), loops.
2
+ The PS-EXE header (set by the JS wrapper) points the BIOS entry here. */
3
+ .set noreorder
4
+ .section .text.start, "ax"
5
+ .global _start
6
+ _start:
7
+ la $sp, __sp_top /* stack top */
8
+ /* zero .bss */
9
+ la $t0, __bss_start
10
+ la $t1, __bss_end
11
+ 1: beq $t0, $t1, 2f
12
+ nop
13
+ sw $zero, 0($t0)
14
+ addiu $t0, $t0, 4
15
+ b 1b
16
+ nop
17
+ 2: jal main
18
+ nop
19
+ 3: b 3b /* main returned — hang */
20
+ nop
@@ -0,0 +1,15 @@
1
+ OUTPUT_FORMAT("elf32-littlemips")
2
+ OUTPUT_ARCH(mips)
3
+ ENTRY(_start)
4
+ MEMORY { ram (rwx) : ORIGIN = 0x80010000, LENGTH = 0x1F0000 }
5
+ SECTIONS {
6
+ .text : { *(.text.start) *(.text*) *(.rodata*) } > ram
7
+ .data : { *(.data*) *(.sdata*) *(.lit8) *(.lit4) *(.scommon) } > ram
8
+ . = ALIGN(4);
9
+ __bss_start = .;
10
+ .bss : { *(.bss*) *(.sbss*) *(.scommon) *(COMMON) } > ram
11
+ . = ALIGN(4);
12
+ __bss_end = .;
13
+ __sp_top = 0x801FFFF0;
14
+ /DISCARD/ : { *(.MIPS.abiflags) *(.reginfo) *(.comment) }
15
+ }
@@ -0,0 +1,37 @@
1
+ /* softint.c — the few libgcc helpers a fixed-point game needs, in plain C, so the
2
+ build doesn't depend on an endian-specific libgcc.a. Compiled per-build with the
3
+ target endian. Currently: 64-bit signed/unsigned divide + modulo (__divdi3 etc).
4
+ MIPS has native 32x32->64 multiply, so __muldi3 isn't needed. */
5
+ typedef long long DItype;
6
+ typedef unsigned long long UDItype;
7
+
8
+ static UDItype udivmod64(UDItype num, UDItype den, UDItype *rem) {
9
+ UDItype quot = 0, qbit = 1;
10
+ if (den == 0) { if (rem) *rem = 0; return 0; }
11
+ /* normalize: shift den up until > num or top bit set */
12
+ while ((DItype)den >= 0) { den <<= 1; qbit <<= 1; }
13
+ while (qbit) {
14
+ if (den <= num) { num -= den; quot += qbit; }
15
+ den >>= 1; qbit >>= 1;
16
+ }
17
+ if (rem) *rem = num;
18
+ return quot;
19
+ }
20
+
21
+ UDItype __udivdi3(UDItype a, UDItype b) { return udivmod64(a, b, 0); }
22
+ UDItype __umoddi3(UDItype a, UDItype b) { UDItype r; udivmod64(a, b, &r); return r; }
23
+
24
+ DItype __divdi3(DItype a, DItype b) {
25
+ int neg = 0; UDItype ua, ub, q;
26
+ if (a < 0) { a = -a; neg ^= 1; } if (b < 0) { b = -b; neg ^= 1; }
27
+ ua = (UDItype)a; ub = (UDItype)b;
28
+ q = udivmod64(ua, ub, 0);
29
+ return neg ? -(DItype)q : (DItype)q;
30
+ }
31
+ DItype __moddi3(DItype a, DItype b) {
32
+ int neg = 0; UDItype ua, ub, r;
33
+ if (a < 0) { a = -a; neg = 1; } if (b < 0) { b = -b; }
34
+ ua = (UDItype)a; ub = (UDItype)b;
35
+ udivmod64(ua, ub, &r);
36
+ return neg ? -(DItype)r : (DItype)r;
37
+ }
@@ -0,0 +1,155 @@
1
+ // mips-c — minimal C → MIPS binary builder for PS1 (and N64), the bare
2
+ // gcc+newlib+libgcc path (no SDK yet). Mirrors genesis-c.js's buildMinimal.
3
+ //
4
+ // buildMipsC({ source, sources, headers, platform }) →
5
+ // PS1: a runnable PS-EXE ('PS-X EXE' header + .text at 0x80010000)
6
+ // N64: a flat .bin (boot glue is libdragon's job — minimal path for now)
7
+ //
8
+ // Pipeline per source: cc1 → as → (link all with crt0 + libc/libm/libgcc) →
9
+ // objcopy → wrap. Endianness from the platform (PS1 little / N64 big).
10
+
11
+ import { fileURLToPath } from "node:url";
12
+ import { readFile } from "node:fs/promises";
13
+ import path from "node:path";
14
+
15
+ import { runCc1mips, runMipsAs, runMipsLd, runMipsObjcopy } from "../mips-elf-gcc/gcc.js";
16
+
17
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
+ const LIB = path.join(__dirname, "lib");
19
+
20
+ /** Wrap a raw .text image in a PS-EXE header (2048 bytes). The PS1 BIOS loads
21
+ * `t_size` bytes from the file (after the header) to `t_addr` and jumps to pc0. */
22
+ function wrapPsExe(text, loadAddr = 0x80010000, spBase = 0x801ffff0) {
23
+ const hdr = new Uint8Array(2048);
24
+ hdr.set(new TextEncoder().encode("PS-X EXE"), 0);
25
+ const put = (o, v) => { hdr[o] = v & 0xff; hdr[o + 1] = (v >> 8) & 0xff; hdr[o + 2] = (v >> 16) & 0xff; hdr[o + 3] = (v >>> 24) & 0xff; };
26
+ put(0x10, loadAddr); // pc0 (entry)
27
+ put(0x18, loadAddr); // t_addr (load address)
28
+ put(0x1c, (text.length + 3) & ~3); // t_size
29
+ put(0x30, spBase); // s_addr (stack base)
30
+ const out = new Uint8Array(2048 + text.length);
31
+ out.set(hdr, 0); out.set(text, 2048);
32
+ return out;
33
+ }
34
+
35
+ // Minimal clean-room N64 IPL3 (assembled from lib/n64-ipl3.s): runs in SP DMEM at
36
+ // 0xA4000040 after the (HLE or real) PIF boot copies ROM[0x40..0xFFF] there. It
37
+ // PI-DMAs 1 MB from cart 0xB0001000 → RDRAM 0x80000400 and jumps to the game entry.
38
+ const N64_IPL3 = [
39
+ 0x3c08a460, 0x24090400, 0xad090000, 0x3c091000, 0x35291000, 0xad090004,
40
+ 0x3c09000f, 0x3529ffff, 0xad09000c, 0x8d0a0010, 0x314a0003, 0x1540fffd,
41
+ 0x00000000, 0x3c0b8000, 0x356b0400, 0x01600008, 0x00000000,
42
+ ];
43
+
44
+ /** Wrap a raw .text image (linked at 0x80000400) in a bootable big-endian .z64:
45
+ * 64-byte header (0x80371240 magic + entry) + IPL3 at 0x40 + game at 0x1000. The
46
+ * parallel_n64 HLE boot runs the IPL3, which copies the game to RDRAM and jumps. */
47
+ function wrapN64Rom(text, entry = 0x80000400) {
48
+ const GAME_OFF = 0x1000;
49
+ const size = GAME_OFF + ((text.length + 3) & ~3);
50
+ const rom = new Uint8Array(size < 0x101000 ? 0x101000 : size); // ≥1MB (IPL3 copies 1MB)
51
+ const be32 = (o, v) => { rom[o] = (v >>> 24) & 0xff; rom[o + 1] = (v >> 16) & 0xff; rom[o + 2] = (v >> 8) & 0xff; rom[o + 3] = v & 0xff; };
52
+ // header: PI BSD config / magic, clock, entry, release
53
+ be32(0x00, 0x80371240); // magic (Z64, native big-endian)
54
+ be32(0x04, 0x0000000f); // clock rate
55
+ be32(0x08, entry); // boot entry (informational; IPL3 jumps explicitly)
56
+ be32(0x0c, 0x00001444); // release
57
+ // name (0x20..0x33)
58
+ const name = "ROMDEV HOMEBREW ";
59
+ for (let i = 0; i < 20; i++) rom[0x20 + i] = name.charCodeAt(i);
60
+ be32(0x3c, 0x0000004e); // cartridge id "NE" placeholder + region
61
+ // IPL3 at 0x40
62
+ for (let i = 0; i < N64_IPL3.length; i++) be32(0x40 + i * 4, N64_IPL3[i]);
63
+ // game at 0x1000
64
+ rom.set(text, GAME_OFF);
65
+ return rom;
66
+ }
67
+
68
+ /**
69
+ * Build a minimal C program to a runnable PS1/N64 image.
70
+ * @param {Object} args
71
+ * @param {string} [args.source] single C source
72
+ * @param {Record<string,string>} [args.sources] multi-file (name → text)
73
+ * @param {Record<string,string>} [args.headers] virtual headers
74
+ * @param {string} args.platform "ps1" | "n64"
75
+ * @param {string[]} [args.cc1Options]
76
+ */
77
+ export async function buildMipsC(args) {
78
+ const platform = args.platform;
79
+ const endian = platform === "ps1" ? "little" : "big";
80
+ const headers = args.headers ?? {};
81
+ const cc1Options = [...(args.cc1Options ?? []), "-O2", "-G0", "-ffreestanding", "-fno-builtin", "-Wall"];
82
+ const sources = args.sources ?? (args.source != null ? { "main.c": args.source } : {});
83
+ let log = "";
84
+
85
+ /** @type {Record<string, Uint8Array>} */
86
+ const userObjs = {};
87
+ for (const cName of Object.keys(sources).filter((n) => /\.c$/i.test(n))) {
88
+ const cc = await runCc1mips({ source: sources[cName], headers, options: cc1Options, endian });
89
+ log += `--- cc1 (${cName}) ---\n${cc.log || "(ok)"}\n`;
90
+ if (cc.exitCode !== 0 || !cc.asmSource) return { ok: false, binary: null, log, exitCode: cc.exitCode || 1, stage: `cc1 (${cName})`, ...(cc.crash ? { crash: cc.crash } : {}) };
91
+ const as = await runMipsAs({ source: cc.asmSource, endian });
92
+ log += `--- as (${cName}) ---\n${as.log || "(ok)"}\n`;
93
+ if (as.exitCode !== 0 || !as.object) return { ok: false, binary: null, log, exitCode: as.exitCode || 1, stage: `as (${cName})`, ...(as.crash ? { crash: as.crash } : {}) };
94
+ userObjs[cName.replace(/\.c$/i, ".o")] = as.object;
95
+ }
96
+ // raw .s sources too
97
+ for (const sName of Object.keys(sources).filter((n) => /\.(s|asm)$/i.test(n))) {
98
+ const as = await runMipsAs({ source: sources[sName], endian });
99
+ log += `--- as (${sName}) ---\n${as.log || "(ok)"}\n`;
100
+ if (as.exitCode !== 0 || !as.object) return { ok: false, binary: null, log, exitCode: as.exitCode || 1, stage: `as (${sName})`, ...(as.crash ? { crash: as.crash } : {}) };
101
+ userObjs[sName.replace(/\.(s|asm)$/i, ".o")] = as.object;
102
+ }
103
+
104
+ // crt0 (per platform)
105
+ const crt0Name = platform === "ps1" ? "ps1-crt0.s" : "n64-crt0.s";
106
+ const crt0Src = await readFile(path.join(LIB, crt0Name), "utf-8");
107
+ const crt0As = await runMipsAs({ source: crt0Src, endian });
108
+ log += `--- as (${crt0Name}) ---\n${crt0As.log || "(ok)"}\n`;
109
+ if (crt0As.exitCode !== 0 || !crt0As.object) return { ok: false, binary: null, log, exitCode: crt0As.exitCode || 1, stage: "as (crt0)", ...(crt0As.crash ? { crash: crt0As.crash } : {}) };
110
+
111
+ // softint.c — the few libgcc helpers (64-bit divide/mod) in plain C, so the
112
+ // link doesn't need an endian-specific libgcc.a (the EL libgcc isn't bundled).
113
+ const softSrc = await readFile(path.join(LIB, "softint.c"), "utf-8");
114
+ const softCc = await runCc1mips({ source: softSrc, options: cc1Options, endian });
115
+ const softAs = softCc.asmSource ? await runMipsAs({ source: softCc.asmSource, endian }) : { exitCode: 1 };
116
+ if (softCc.exitCode !== 0 || softAs.exitCode !== 0 || !softAs.object) {
117
+ return { ok: false, binary: null, log: log + (softCc.log || "") + (softAs.log || ""), exitCode: 1, stage: "softint" };
118
+ }
119
+
120
+ // link
121
+ const ldName = platform === "ps1" ? "ps1.ld" : "n64.ld";
122
+ const linkScript = await readFile(path.join(LIB, ldName), "utf-8");
123
+ // newlib + libgcc are endian-specific: el/ (PS1 little) vs be/ (N64 big). libgcc
124
+ // is only bundled for be/ — softint.c covers the EL case, so libgcc is optional.
125
+ const libDir = path.join(LIB, endian === "little" ? "el" : "be");
126
+ const [libc, libm] = await Promise.all([
127
+ readFile(path.join(libDir, "libc.a")), readFile(path.join(libDir, "libm.a")),
128
+ ]);
129
+ const archives = { "libc.a": new Uint8Array(libc), "libm.a": new Uint8Array(libm) };
130
+ const libraries = ["c", "m"];
131
+ try {
132
+ const libgcc = await readFile(path.join(libDir, "libgcc.a"));
133
+ archives["libgcc.a"] = new Uint8Array(libgcc);
134
+ libraries.unshift("gcc");
135
+ } catch { /* no endian libgcc — softint.c provides the needed helpers */ }
136
+ const ld = await runMipsLd({
137
+ objects: { "crt0.o": crt0As.object, "softint.o": softAs.object, ...userObjs },
138
+ linkScript, endian,
139
+ archives,
140
+ libraries,
141
+ libraryPaths: ["/work"],
142
+ options: ["--no-warn-rwx-segments"],
143
+ });
144
+ log += `--- ld ---\n${ld.log || "(ok)"}\n`;
145
+ if (ld.exitCode !== 0 || !ld.elf) return { ok: false, binary: null, log, exitCode: ld.exitCode || 1, stage: "ld", ...(ld.crash ? { crash: ld.crash } : {}) };
146
+
147
+ const oc = await runMipsObjcopy({ elf: ld.elf });
148
+ log += `--- objcopy ---\n${oc.log || "(ok)"}\n`;
149
+ if (oc.exitCode !== 0 || !oc.binary) return { ok: false, binary: null, log, exitCode: oc.exitCode || 1, stage: "objcopy", ...(oc.crash ? { crash: oc.crash } : {}) };
150
+
151
+ const binary = platform === "ps1" ? wrapPsExe(oc.binary)
152
+ : platform === "n64" ? wrapN64Rom(oc.binary)
153
+ : oc.binary;
154
+ return { ok: true, binary, log, exitCode: 0, stage: "done", ...(ld.map ? { symbols: ld.map } : {}) };
155
+ }