romdevtools 0.70.0 → 0.71.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 (33) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/examples/dreamcast/platformer/main.c +31 -0
  3. package/examples/dreamcast/puzzle/main.c +44 -0
  4. package/examples/dreamcast/racing/main.c +39 -0
  5. package/examples/dreamcast/shmup/main.c +50 -0
  6. package/examples/dreamcast/sports/main.c +39 -0
  7. package/package.json +3 -3
  8. package/src/cores/capabilities.js +13 -9
  9. package/src/host/LibretroHost.js +242 -23
  10. package/src/host/callbacks.js +68 -1
  11. package/src/host/cpu-state.js +32 -0
  12. package/src/host/dc-aica-state.js +67 -0
  13. package/src/mcp/tools/audio.js +1 -1
  14. package/src/mcp/tools/disasm.js +1 -1
  15. package/src/mcp/tools/index.js +1 -1
  16. package/src/mcp/tools/platform-docs.js +1 -1
  17. package/src/mcp/tools/platform-tools.js +9 -1
  18. package/src/mcp/tools/toolchain.js +115 -10
  19. package/src/platforms/dreamcast/MENTAL_MODEL.md +87 -0
  20. package/src/platforms/dreamcast/TROUBLESHOOTING.md +55 -0
  21. package/src/platforms/dreamcast/UPSTREAM_SOURCES.md +57 -0
  22. package/src/platforms/n64/MENTAL_MODEL.md +84 -0
  23. package/src/platforms/n64/TROUBLESHOOTING.md +60 -0
  24. package/src/platforms/n64/UPSTREAM_SOURCES.md +52 -0
  25. package/src/platforms/n64/lib/c/n64.c +181 -80
  26. package/src/platforms/ps1/MENTAL_MODEL.md +85 -0
  27. package/src/platforms/ps1/TROUBLESHOOTING.md +55 -0
  28. package/src/platforms/ps1/UPSTREAM_SOURCES.md +54 -0
  29. package/src/platforms/snes/TROUBLESHOOTING.md +10 -0
  30. package/src/toolchains/asar/asar.js +84 -14
  31. package/src/toolchains/mips-c/mips-c.js +35 -1
  32. package/src/toolchains/sh-c/lib/dc.h +65 -15
  33. package/src/toolchains/sh-c/sh-c.js +6 -3
@@ -1,117 +1,218 @@
1
- /* n64.c — N64 helpers + software 3D pipeline (see n64.h). Framebuffer backend. */
1
+ /* n64.c — N64 helpers with a GPU (RDP/GBI) drawing backend.
2
+ *
3
+ * WHY THIS IS NOT A SOFTWARE RASTERIZER: the shipping N64 core renders through
4
+ * glide64 (a GL HLE plugin) — it presents the game's RDP/GBI **display lists** on
5
+ * the real GPU, NOT a raw CPU-written framebuffer. A software rasterizer that pokes
6
+ * pixels into RDRAM shows BLACK on glide64 (and would be <1fps even if it didn't).
7
+ * So this lib builds a GBI display list each frame and kicks the RSP/RDP; glide64
8
+ * HLEs it onto the GPU.
9
+ *
10
+ * HOW glide64 ACCEPTS OUR LIST (no Nintendo microcode shipped):
11
+ * - The RSP-HLE treats an OSTask with type==1 (M_GFXTASK) as a graphics task and
12
+ * hands the display list to glide64.
13
+ * - glide64 picks its command table by CRC-summing 3072 bytes of the task's
14
+ * "ucode" region and matching a known-ucode CRC. The bytes are NEVER executed
15
+ * (HLE), only summed — so we embed a 3072-byte blob that SUMS to a real F3DEX2
16
+ * CRC (0x5d3099f1). glide64 then interprets our list as standard F3DEX2.
17
+ * - We emit standard F3DEX2 GBI: set color image / scissor / fill rectangles for
18
+ * clear+rects, and shaded vertex triangles for tri2d/tri3d. Solid colors only.
19
+ */
2
20
  #include "n64.h"
3
21
 
4
- /* Two 320x240x16bpp framebuffers in RDRAM (double-buffered), cached kseg0. */
5
- #define FB0 0xA0100000u /* uncached kseg1 writes go straight to RDRAM */
6
- #define FB1 0xA0120000u
7
- static volatile unsigned short *fb; /* the back buffer we draw into */
8
- static unsigned int front;
9
-
10
- /* VI registers (0xA4400000). */
11
- #define VI(n) (*(volatile unsigned int*)(0xA4400000u + (n)*4))
12
- #define VI_STATUS 0
13
- #define VI_ORIGIN 1
14
- #define VI_WIDTH 2
15
- #define VI_V_CURRENT 4
16
- #define VI_H_START 9
17
- #define VI_V_START 10
18
- #define VI_X_SCALE 12
19
- #define VI_Y_SCALE 13
22
+ /* ── RCP register blocks ── */
23
+ #define SP(n) (*(volatile unsigned int*)(0xA4040000u + (n)*4)) /* RSP */
24
+ #define DP(n) (*(volatile unsigned int*)(0xA4100000u + (n)*4)) /* RDP cmd */
25
+ #define VI(n) (*(volatile unsigned int*)(0xA4400000u + (n)*4)) /* video */
26
+
27
+ #define SP_MEM_ADDR 0 /* SP DMEM/IMEM address */
28
+ #define SP_DRAM_ADDR 1
29
+ #define SP_RD_LEN 2
30
+ #define SP_WR_LEN 3
31
+ #define SP_STATUS 4
32
+
33
+ #define FB_ADDR 0x00200000u /* 320x240x16bpp color image in RDRAM */
34
+ #define DL_ADDR 0x00280000u /* the GBI display list buffer (room for many spans) */
35
+ #define UC_ADDR 0x00290000u /* the fake "ucode" blob (CRC bait) */
36
+
37
+ /* uncached pointers (kseg1) into those RDRAM regions */
38
+ #define U16P(a) ((volatile unsigned short*)(0xA0000000u | (a)))
39
+ #define U32P(a) ((volatile unsigned int*) (0xA0000000u | (a)))
40
+
41
+ static volatile unsigned int *dl; /* write cursor into the display list */
42
+ static unsigned int dl_count; /* words written */
43
+
44
+ /* ── GBI / F3DEX2 command opcodes (the ones the fill-rect path uses) ── */
45
+ #define G_ENDDL 0xDF
46
+ #define G_SETOTHERMODE_H 0xE3
47
+ #define G_RDPPIPESYNC 0xE7
48
+ #define G_RDPFULLSYNC 0xE9
49
+ #define G_SETSCISSOR 0xED
50
+ #define G_FILLRECT 0xF6
51
+ #define G_SETFILLCOLOR 0xF7
52
+ #define G_SETCIMG 0xFF
53
+
54
+ static inline void dl_w(unsigned int w0, unsigned int w1)
55
+ { dl[dl_count++] = w0; dl[dl_count++] = w1; }
20
56
 
21
57
  void n64_init(void)
22
58
  {
23
- /* NTSC 320x240 16bpp — register indices per the N64 VI map:
24
- 0=STATUS 1=ORIGIN 2=WIDTH 3=V_INTR 4=CURRENT 5=BURST 6=V_SYNC 7=H_SYNC
25
- 8=LEAP 9=H_START 10=V_START 11=V_BURST 12=X_SCALE 13=Y_SCALE. */
26
- VI(0) = 0x0000320E; /* STATUS: 16bpp(type=2) + AA + dither + pixel_advance */
27
- VI(2) = 320; /* WIDTH */
28
- VI(3) = 2; /* V_INTR */
29
- VI(4) = 0; /* CURRENT */
30
- VI(5) = 0x03E52239; /* BURST (NTSC) */
31
- VI(6) = 0x0000020D; /* V_SYNC = 525 */
32
- VI(7) = 0x00000C15; /* H_SYNC */
33
- VI(8) = 0x0C150C15; /* LEAP */
34
- VI(9) = 0x006C02EC; /* H_START: 108..748 */
35
- VI(10) = 0x002501FF; /* V_START: 37..511 */
36
- VI(11) = 0x000E0204; /* V_BURST */
37
- VI(12) = 0x00000200; /* X_SCALE (320) */
38
- VI(13) = 0x00000400; /* Y_SCALE (240) */
39
- front = FB0;
40
- fb = (volatile unsigned short *)FB1;
41
- VI(1) = FB0 & 0x1FFFFFFF; /* ORIGIN */
59
+ int i;
60
+ /* NTSC 320x240 16bpp VI setup, scanning out FB_ADDR — glide64 reads our color
61
+ image from the SetColorImage GBI cmd, but the VI must still be programmed so
62
+ there is a valid display target. */
63
+ VI(0) = 0x0000320E; VI(2) = 320; VI(3) = 2; VI(5) = 0x03E52239;
64
+ VI(6) = 0x0000020D; VI(7) = 0x00000C15; VI(8) = 0x0C150C15;
65
+ VI(9) = 0x006C02EC; VI(10)= 0x002501FF; VI(11)= 0x000E0204;
66
+ VI(12) = 0x00000200; VI(13)= 0x00000400;
67
+ VI(1) = FB_ADDR;
68
+
69
+ /* Build the CRC-bait "ucode": 3072 bytes that SUM (as u32 words) to the F3DEX2
70
+ CRC glide64 expects (UCODE_ZELDA_OOT = 0x5d3099f1). All words 0 except one. */
71
+ { volatile unsigned int *uc = U32P(UC_ADDR);
72
+ for (i = 0; i < 3072/4; i++) uc[i] = 0;
73
+ uc[0] = 0x5d3099f1u; }
42
74
  }
43
75
 
44
- static inline void putpx(int x, int y, unsigned short c)
76
+ /* Begin a fresh display list for this frame: sync + set the color image + scissor +
77
+ fill cycle, and a default combine that passes the fill/shade through. */
78
+ static void dl_begin(void)
45
79
  {
46
- if ((unsigned)x < SCREEN_W && (unsigned)y < SCREEN_H) fb[y * SCREEN_W + x] = c;
80
+ dl = U32P(DL_ADDR);
81
+ dl_count = 0;
82
+ dl_w((G_RDPPIPESYNC << 24), 0);
83
+ /* SetColorImage: fmt=0(RGBA) size=2(16b) width-1=319, addr=FB */
84
+ dl_w((G_SETCIMG << 24) | (0 << 21) | (2 << 19) | 319, FB_ADDR);
85
+ /* SetScissor: (0,0)-(320,240) in 10.2 fixed (<<2) */
86
+ dl_w((G_SETSCISSOR << 24) | (0 << 12) | 0, (320 << 14) | (240 << 2));
87
+ /* SetOtherMode_H: cycle type = FILL (3<<20 within the H word). F3DEX2 form:
88
+ shift=0x14(20) len=2 → set the 2 cycle-type bits to 3 (G_CYC_FILL). */
89
+ dl_w((G_SETOTHERMODE_H << 24) | ((32 - 20 - 2) << 8) | (2 - 1), 3u << 20);
90
+ }
91
+
92
+ /* End the list with sync + ENDDL, then ship it to the RSP/RDP as a GFX OSTask. */
93
+ static void dl_end_and_run(void)
94
+ {
95
+ dl_w((G_RDPFULLSYNC << 24), 0);
96
+ dl_w((G_ENDDL << 24), 0);
97
+
98
+ /* Write the OSTask into SP DMEM at 0xFC0 (SP DMEM is at 0xA4000000). */
99
+ volatile unsigned int *task = (volatile unsigned int *)0xA4000FC0u;
100
+ task[0] = 1; /* 0xFC0 TASK_TYPE = M_GFXTASK (glide64 path) */
101
+ task[1] = 0; /* 0xFC4 flags */
102
+ task[2] = 0; /* 0xFC8 ucode_boot */
103
+ task[3] = 0; /* 0xFCC ucode_boot_size */
104
+ task[4] = UC_ADDR; /* 0xFD0 ucode (CRC-summed by glide64) */
105
+ task[5] = 0xC00; /* 0xFD4 ucode_size (>= 3072 so the full CRC region reads) */
106
+ task[6] = UC_ADDR; /* 0xFD8 ucode_data */
107
+ task[7] = 0x800; /* 0xFDC ucode_data_size */
108
+ task[8] = 0; /* 0xFE0 dram_stack */
109
+ task[9] = 0x400; /* 0xFE4 dram_stack_size */
110
+ task[10] = 0; /* 0xFE8 output_buff */
111
+ task[11] = 0; /* 0xFEC output_buff_size */
112
+ task[12] = DL_ADDR; /* 0xFF0 data_ptr → our GBI display list */
113
+ task[13] = dl_count * 4; /* 0xFF4 data_size (bytes) */
114
+ task[14] = 0; /* 0xFF8 yield_data_ptr */
115
+ task[15] = 0; /* 0xFFC yield_data_size */
116
+
117
+ /* Kick the RSP. The SP status-write handler runs the task only when BOTH HALT
118
+ and BROKE are clear after the write: bit0 (0x1) clears HALT, bit2 (0x4) clears
119
+ BROKE. Setting interrupt-on-break (0x100) makes the task signal completion.
120
+ The RSP-HLE then reads the OSTask at DMEM 0xFC0, sees type==1, and forwards
121
+ our display list to glide64. (SP_PC at 0xA4080000 is irrelevant under HLE —
122
+ the ucode never executes; only the OSTask fields + the CRC matter.) */
123
+ SP(SP_STATUS) = 0x00105; /* clear HALT (0x1) + clear BROKE (0x4) + intr-on-break (0x100) */
47
124
  }
48
125
 
49
126
  void n64_clear(unsigned short col)
50
127
  {
51
- int i; for (i = 0; i < SCREEN_W * SCREEN_H; i++) fb[i] = col;
128
+ dl_begin();
129
+ /* SetFillColor: 16bpp color packed twice into 32 bits. */
130
+ unsigned int c32 = ((unsigned int)col << 16) | col;
131
+ dl_w((G_SETFILLCOLOR << 24), c32);
132
+ /* FillRectangle covering the whole screen (coords in 10.2 fixed, inclusive). */
133
+ dl_w((G_FILLRECT << 24) | (((320 - 1) << 2) << 12) | ((240 - 1) << 2),
134
+ (0 << 12) | 0);
52
135
  }
53
136
 
54
137
  void n64_rect(int x, int y, int w, int h, unsigned short col)
55
138
  {
56
- int r, c;
57
- for (r = 0; r < h; r++) for (c = 0; c < w; c++) putpx(x + c, y + r, col);
139
+ int x1 = x + w - 1, y1 = y + h - 1;
140
+ if (x < 0) x = 0; if (y < 0) y = 0;
141
+ if (x1 > 319) x1 = 319; if (y1 > 239) y1 = 239;
142
+ if (x1 < x || y1 < y) return;
143
+ unsigned int c32 = ((unsigned int)col << 16) | col;
144
+ dl_w((G_SETFILLCOLOR << 24), c32);
145
+ dl_w((G_FILLRECT << 24) | ((x1 << 2) << 12) | (y1 << 2),
146
+ ((x << 2) << 12) | (y << 2));
58
147
  }
59
148
 
60
- /* edge function for the rasterizer */
61
- static inline int edge(int ax, int ay, int bx, int by, int cx, int cy)
62
- { return (bx - ax) * (cy - ay) - (by - ay) * (cx - ax); }
63
-
64
- void n64_tri2d(int x0,int y0,int x1,int y1,int x2,int y2,unsigned short col)
149
+ /* Triangle: a flat-shaded, screen-space triangle. The verts are already projected
150
+ to 2D pixels by the 3D pipeline. Rather than drive glide64's full F3DEX2 vertex/
151
+ matrix/combine pipeline (heavy state), we scan-convert the triangle into a span of
152
+ GPU FILL RECTANGLES — one per scanline. These are the SAME hardware-accelerated
153
+ fill-rects that clear/rect use (rasterized by glide64 on the GPU, NOT CPU pixels),
154
+ so it stays fast + renders correctly under the fill-cycle render state. The
155
+ examples draw a handful of large flat quads, so the per-scanline rect count is low. */
156
+ static void emit_tri(int x0,int y0,int x1,int y1,int x2,int y2,unsigned short col)
65
157
  {
66
- int minx = x0, maxx = x0, miny = y0, maxy = y0, px, py, w0, w1, w2, area;
67
- if (x1 < minx) minx = x1; if (x1 > maxx) maxx = x1;
68
- if (x2 < minx) minx = x2; if (x2 > maxx) maxx = x2;
69
- if (y1 < miny) miny = y1; if (y1 > maxy) maxy = y1;
70
- if (y2 < miny) miny = y2; if (y2 > maxy) maxy = y2;
71
- if (minx < 0) minx = 0; if (miny < 0) miny = 0;
72
- if (maxx >= SCREEN_W) maxx = SCREEN_W - 1; if (maxy >= SCREEN_H) maxy = SCREEN_H - 1;
73
- area = edge(x0, y0, x1, y1, x2, y2);
74
- if (area == 0) return;
75
- for (py = miny; py <= maxy; py++) {
76
- for (px = minx; px <= maxx; px++) {
77
- w0 = edge(x1, y1, x2, y2, px, py);
78
- w1 = edge(x2, y2, x0, y0, px, py);
79
- w2 = edge(x0, y0, x1, y1, px, py);
80
- if (area > 0) { if (w0 >= 0 && w1 >= 0 && w2 >= 0) fb[py * SCREEN_W + px] = col; }
81
- else { if (w0 <= 0 && w1 <= 0 && w2 <= 0) fb[py * SCREEN_W + px] = col; }
158
+ /* sort vertices by y (y0 <= y1 <= y2) */
159
+ int t;
160
+ if (y0 > y1) { t=x0;x0=x1;x1=t; t=y0;y0=y1;y1=t; }
161
+ if (y1 > y2) { t=x1;x1=x2;x2=t; t=y1;y1=y2;y2=t; }
162
+ if (y0 > y1) { t=x0;x0=x1;x1=t; t=y0;y0=y1;y1=t; }
163
+ if (y2 == y0) return; /* degenerate */
164
+
165
+ int y;
166
+ for (y = y0; y <= y2; y++) {
167
+ if (y < 0 || y > 239) continue;
168
+ /* x along the long edge 0→2 */
169
+ int xa = x0 + (x2 - x0) * (y - y0) / (y2 - y0);
170
+ int xb;
171
+ if (y < y1) {
172
+ if (y1 == y0) continue;
173
+ xb = x0 + (x1 - x0) * (y - y0) / (y1 - y0);
174
+ } else {
175
+ if (y2 == y1) continue;
176
+ xb = x1 + (x2 - x1) * (y - y1) / (y2 - y1);
82
177
  }
178
+ if (xa > xb) { t = xa; xa = xb; xb = t; }
179
+ if (xa < 0) xa = 0; if (xb > 319) xb = 319;
180
+ if (xb < xa) continue;
181
+ /* one GPU fill-rect for this 1px-tall span */
182
+ unsigned int c32 = ((unsigned int)col << 16) | col;
183
+ dl_w((G_SETFILLCOLOR << 24), c32);
184
+ dl_w((G_FILLRECT << 24) | ((xb << 2) << 12) | (y << 2),
185
+ ((xa << 2) << 12) | (y << 2));
83
186
  }
84
187
  }
85
188
 
189
+ void n64_tri2d(int x0,int y0,int x1,int y1,int x2,int y2,unsigned short col)
190
+ { emit_tri(x0,y0,x1,y1,x2,y2,col); }
191
+
86
192
  void n64_flip(void)
87
193
  {
88
- /* swap buffers + point VI at the freshly drawn one, then wait for a frame. */
89
- unsigned int newfront = (unsigned int)(unsigned long)fb;
90
- VI(1) = newfront & 0x1FFFFFFF;
91
- fb = (volatile unsigned short *)(front);
92
- front = newfront;
93
- { volatile int i; for (i = 0; i < 200000; i++) { } }
194
+ dl_end_and_run();
195
+ /* wait for the frame to scan out + the RDP to drain. */
196
+ { volatile int i; for (i = 0; i < 100000; i++) { } }
94
197
  }
95
198
 
96
- /* ── input: read controller port 0 via the SI/PIF. The PIF command 0x01 polls
97
- the pad; the 16-bit button word is at PIF RAM. We use the simple JoyBus poll. ── */
199
+ /* ── input: read controller port 0 via the SI/PIF JoyBus poll. ── */
98
200
  static unsigned int read_pad(void)
99
201
  {
100
- volatile unsigned int *pif = (volatile unsigned int *)0xBFC007C0; /* PIF RAM */
202
+ volatile unsigned int *pif = (volatile unsigned int *)0xBFC007C0;
101
203
  volatile unsigned int *si = (volatile unsigned int *)0xA4800000;
102
204
  unsigned int buttons;
103
- /* command block: read controller 0 (1 byte cmd 0x01, 1 byte send, ...) */
104
205
  pif[0] = 0xFF010401; pif[1] = 0xFFFFFFFF; pif[2] = 0xFFFFFFFF;
105
206
  pif[3] = 0xFE000000; pif[4] = 0; pif[5] = 0; pif[6] = 0; pif[7] = 1;
106
- si[1] = 0x1FC007C0; /* SI_PIF_ADDR_RD64B: kick the PIF read */
207
+ si[1] = 0x1FC007C0;
107
208
  { volatile int t; for (t = 0; t < 5000; t++) { } }
108
- buttons = pif[1] >> 16; /* the button half-word */
109
- return (~0u) & buttons; /* JoyBus buttons are active-high already */
209
+ buttons = pif[1] >> 16;
210
+ return (~0u) & buttons;
110
211
  }
111
212
  unsigned int n64_pad(void) { return read_pad(); }
112
213
  int n64_pressed(unsigned int mask) { return (n64_pad() & mask) ? 1 : 0; }
113
214
 
114
- /* ── trig (shared with PS1 lib: 256-step binary angle) ── */
215
+ /* ── trig (256-step binary angle) ── */
115
216
  static const short SINTAB[64] = {
116
217
  0,804,1608,2410,3212,4011,4808,5602,6393,7179,7962,8739,9512,10278,11039,11793,
117
218
  12539,13279,14010,14732,15446,16151,16846,17530,18204,18868,19519,20159,20787,21403,
@@ -127,7 +228,7 @@ static fix sin_lut(int idx) {
127
228
  fix n64_sin(fix a) { return sin_lut((F2I(a)) & 255); }
128
229
  fix n64_cos(fix a) { return sin_lut((F2I(a) + 64) & 255); }
129
230
 
130
- /* ── camera + model (identical to the PS1 lib) ── */
231
+ /* ── camera + model + projection (math unchanged; output drives emit_tri) ── */
131
232
  static fix cam_x, cam_y, cam_z, cam_cy, cam_sy, cam_cp, cam_sp;
132
233
  static fix mdl_x, mdl_y, mdl_z, mdl_cy, mdl_sy;
133
234
  void n64_camera(fix ex, fix ey, fix ez, fix yaw, fix pitch)
@@ -162,7 +263,7 @@ void n64_tri3d(Vec3 a, Vec3 b, Vec3 c, unsigned short col)
162
263
  int x0,y0,x1,y1,x2,y2;
163
264
  if (!project(ca,&x0,&y0)||!project(cb,&x1,&y1)||!project(cc,&x2,&y2)) return;
164
265
  if ((x1-x0)*(y2-y0)-(x2-x0)*(y1-y0) <= 0) return; /* back-face cull */
165
- n64_tri2d(x0,y0,x1,y1,x2,y2,col);
266
+ emit_tri(x0,y0,x1,y1,x2,y2,col);
166
267
  }
167
268
  void n64_quad3d(Vec3 a, Vec3 b, Vec3 c, Vec3 d, unsigned short col)
168
269
  { n64_tri3d(a,b,c,col); n64_tri3d(a,c,d,col); }
@@ -171,7 +272,7 @@ void n64_tri3d_nc(Vec3 a, Vec3 b, Vec3 c, unsigned short col)
171
272
  Vec3 ca=to_cam(a), cb=to_cam(b), cc=to_cam(c);
172
273
  int x0,y0,x1,y1,x2,y2;
173
274
  if (!project(ca,&x0,&y0)||!project(cb,&x1,&y1)||!project(cc,&x2,&y2)) return;
174
- n64_tri2d(x0,y0,x1,y1,x2,y2,col);
275
+ emit_tri(x0,y0,x1,y1,x2,y2,col);
175
276
  }
176
277
  void n64_quad3d_nc(Vec3 a, Vec3 b, Vec3 c, Vec3 d, unsigned short col)
177
278
  { n64_tri3d_nc(a,b,c,col); n64_tri3d_nc(a,c,d,col); }
@@ -181,7 +282,7 @@ static unsigned int rng = 0x12345678u;
181
282
  void n64_srand(unsigned int s) { rng = s ? s : 1; }
182
283
  unsigned int n64_rand(void) { unsigned int x=rng; x^=x<<13; x^=x>>17; x^=x<<5; return rng=x; }
183
284
 
184
- /* ── HUD number (3x5 cells scaled) ── */
285
+ /* ── HUD number (3x5 cells via filled rects) ── */
185
286
  static const unsigned char DIG[10][5] = {
186
287
  {0x7,0x5,0x5,0x5,0x7},{0x2,0x6,0x2,0x2,0x7},{0x7,0x1,0x7,0x4,0x7},{0x7,0x1,0x3,0x1,0x7},
187
288
  {0x5,0x5,0x7,0x1,0x1},{0x7,0x4,0x7,0x1,0x7},{0x7,0x4,0x7,0x5,0x7},{0x7,0x1,0x1,0x1,0x1},
@@ -0,0 +1,85 @@
1
+ # PlayStation (PS1) — mental model
2
+
3
+ The PS1 is a **3D machine**: a 33.87 MHz MIPS R3000A CPU + the GPU (2D/3D
4
+ rasterizer, drawing into 1 MB VRAM) + the GTE (Geometry Transformation Engine,
5
+ a CPU coprocessor for 3D math) + the SPU (24-voice ADPCM sound). romdev runs it on
6
+ **beetle_psx_hw** (Mednafen PSX) built to WASM, presenting through the
7
+ **native-gles / WebGL2 hardware GPU**.
8
+
9
+ ## The one thing to know about rendering
10
+
11
+ PS1 homebrew draws by issuing **GPU primitives** (GP0 command packets) — not by
12
+ writing a raw CPU framebuffer. The core is `beetle_psx_hw`, the **hardware** renderer
13
+ (`hwRender: true`): it rasterizes the GPU command stream on the real GPU via
14
+ GLES3/WebGL2 (through romdev's native-gles bridge), exactly like a console. So the
15
+ bundled `psx.c` helper lib is a small **software-3D front end** (16.16 fixed-point
16
+ transform + project + cull + painter sort) whose **back end emits GP0 flat-poly
17
+ primitives** to the GPU — the GPU does the actual rasterization. (This differs from
18
+ N64, which software-rasterizes into RDRAM; PS1 hands triangles to the GPU.)
19
+
20
+ GP0 gotcha that cost real time: **`GP0 0x60` is a variable-size rectangle**, encoded
21
+ as 3 words (color, top-left corner, size) — NOT a 4-vertex polygon. Feeding it
22
+ polygon vertices stretches rects to the screen edge. Use the right command for the
23
+ primitive (triangles = `0x20`/`0x24`/`0x28`/`0x2C` family; rects = `0x60`). Read the
24
+ GP0 encoding carefully.
25
+
26
+ ## CPU / memory map (R3000A, MIPS I, 32-bit, little-endian)
27
+
28
+ | Region | Address (KUSEG/KSEG) | Notes |
29
+ |--------|----------------------|-------|
30
+ | Main RAM | `0x8001_0000`+ (kseg0) / `0x0001_0000`+ | 2 MB. Programs load at `0x8001_0000`. |
31
+ | Scratchpad | `0x1F80_0000` | 1 KB fast on-chip RAM |
32
+ | Hardware I/O | `0x1F80_1000`+ | GPU (`…1810`), SPU (`…1C00`), SIO (`…1040`), DMA, timers |
33
+ | BIOS | `0xBFC0_0000` | HLE'd by default — you don't ship a BIOS |
34
+
35
+ Addresses are **little-endian**. `memory({op:'read', region:'system_ram'})` reads
36
+ main RAM.
37
+
38
+ ## Booting — PS-EXE (no BIOS, no disc)
39
+
40
+ PS1 boots from a **2048-byte PS-EXE header** (magic `PS-X EXE`, with `pc0` / `t_addr`
41
+ / `t_size` at fixed offsets). The HLE BIOS loads `t_size` bytes to `t_addr` and jumps
42
+ to `pc0`. romdev's `build`/`cart` wrap a valid PS-EXE around your code (`wrapPsExe`) —
43
+ no CD image, no real BIOS needed. (We never build discs; HLE + PS-EXE is enough for
44
+ homebrew.)
45
+
46
+ ## Input
47
+
48
+ Homebrew reads the pad from **hardware** — the SIO handshake at `0x1F80_1040`
49
+ (transfer `0x01`, `0x42`, read the button halfword). The host's `setInput`/`run`
50
+ holdInputs drive the emulated pad; the game polls SIO. The helper lib has a
51
+ `pad_read()`.
52
+
53
+ ## Sound
54
+
55
+ The SPU (24 ADPCM voices) at `0x1F80_1C00`. Samples are SPU-ADPCM in SPU RAM, keyed on
56
+ via the voice registers. The helper lib has a minimal tone path.
57
+
58
+ ## MCP debug & inspection tooling — current state
59
+
60
+ - **`memory({op:'read', region:'system_ram'})`** — main RAM. ✅
61
+ - **`disasm` / `decompile`** — MIPS via rizin/Ghidra. ✅ (R3000A `jal` targets are
62
+ absolute VAs — the analysis buffer is base-aligned so call-following works; see
63
+ TROUBLESHOOTING if a fixed-VA image misbehaves.)
64
+ - **`frame({op:'verify'})` / screenshot** — render-health + capture. ✅
65
+ - **`cart` extract/wrap** — PS-EXE. ✅
66
+ - **`cpu({op:'read'})`** — live R3000A register file (`romdev_mips_regs_get`: r0..r31,
67
+ LO, HI, PC). ✅
68
+ - **`audioDebug({op:'inspect', chip:'spu'})`** — the SPU register block at `0x1F80_1C00`
69
+ (`romdev_spu_get`, the 1 KB window). ✅
70
+ - **`breakpoint` / `watch`** — not yet (those need interpreter-step + memory-path hooks
71
+ patched into beetle; cpuState/audioDebug are plain reads and ARE wired).
72
+ - **`renderingContext`** is **N/A** (false) — 3D GPU machine, no 2D tile VDP to decode.
73
+
74
+ ## Build pipeline
75
+
76
+ `build({platform:'ps1'})` cross-compiles with the from-scratch **mips-elf-gcc → WASM**
77
+ toolchain (little-endian libs; a small `softint.c` supplies 64-bit divide helpers on
78
+ the LE side), then `wrapPsExe` produces the bootable PS-EXE. `#include` the bundled
79
+ `psx.h` for the software-3D front end + GP0 emit + SIO/SPU helpers.
80
+
81
+ ## What's NOT bundled / hardware limits
82
+
83
+ - No CD-ROM/XA/streaming or MDEC video.
84
+ - `breakpoint`/`watch` not yet (need interpreter-step hooks; cpuState/audioDebug ARE wired).
85
+ - `renderingContext` is N/A (3D GPU, no tile VDP).
@@ -0,0 +1,55 @@
1
+ # PlayStation (PS1) — troubleshooting
2
+
3
+ Read `platform({op:'doc', platform:'ps1', name:'mental_model'})` first — PS1 renders
4
+ by issuing **GPU primitives (GP0)** on the hardware renderer, not by writing a CPU
5
+ framebuffer.
6
+
7
+ ## "ROM builds + boots but the screen is BLACK"
8
+
9
+ 1. **You're trying to write a raw framebuffer.** PS1's renderer is the hardware GPU
10
+ (`beetle_psx_hw`, `hwRender:true`) — it rasterizes the **GP0 command stream**, not
11
+ a CPU-written framebuffer. Draw with GPU primitives (the bundled `psx.c` emits
12
+ them); a CPU memset of "VRAM" won't show.
13
+ 2. **You never set the display area / drawing environment.** GP1 sets the display
14
+ mode + display start; GP0 `0xE1..0xE6` set the drawing area/offset. Without a valid
15
+ draw env + display env the GPU has nowhere to put pixels. The helper lib's
16
+ `gpu_init` does this — call it before drawing.
17
+ 3. **Nothing was actually submitted to GP0.** Confirm with `frame({op:'verify'})`
18
+ (nearlyBlank) + check you're writing the GP0 port (`0x1F80_1810`) / running a DMA
19
+ to it, not just touching VRAM.
20
+
21
+ ## "Rectangles stretch to the edge of the screen / primitives are garbage"
22
+
23
+ `GP0 0x60` (and the rect family) is a **variable-size rectangle**: 3 words = color,
24
+ top-left corner, size. If you feed it 4 polygon vertices it reads corner+size+garbage
25
+ and stretches. Use the **triangle** family (`0x20`/`0x24`/`0x28`/`0x2C`) for polys and
26
+ the **rect** family (`0x60`/`0x64`/`0x68`/`0x6C`) for rects. Match the command to the
27
+ primitive and word count.
28
+
29
+ ## "Geometry is wrong / inside-out / clipped"
30
+
31
+ Software-3D front-end math (the helper lib transforms before emitting GP0): check
32
+ 16.16 fixed-point throughout, back-face winding matches your vertex order, perspective
33
+ divide before the GP0 vertex coords, and coords fit the 11-bit signed GPU range.
34
+
35
+ ## "disasm/decompile of a multi-function program returns junk (fcn.00000000)"
36
+
37
+ Known rizin trap: for **absolute-addressed `jal`** (PS1 `jal 0x80010518`), rizin
38
+ ignores `-B` on a raw `malloc://` buffer and addresses flat from 0, so cross-function
39
+ discovery dangles outside the image. romdev fixes this by left-padding `.text` so the
40
+ flat offset == the VA's low 20 bits, seeding analysis there, and rebasing the high bits
41
+ back — so reported addresses are real VAs. If you see a lone `fcn.00000000`, you're on
42
+ a path that bypassed the rebase; build a **multi-function** program to exercise it (a
43
+ single-instruction smoke test hides this).
44
+
45
+ ## "breakpoint / watch return N/A"
46
+
47
+ `cpu({op:'read'})` and `audioDebug({op:'inspect', chip:'spu'})` ARE wired on PS1 (the
48
+ core exports `romdev_mips_regs_get` + `romdev_spu_get`). `breakpoint`/`watch` are not
49
+ yet — those need interpreter-step + memory-path hooks patched into beetle (cpuState/
50
+ audioDebug are plain reads). Use `cpu`/`audioDebug` + `memory` + `disasm`/`decompile`
51
+ meanwhile.
52
+
53
+ ## "renderingContext returns N/A"
54
+
55
+ Correct — PS1 is a 3D GPU machine with no 2D tile/sprite VDP for that op. Not a bug.
@@ -0,0 +1,54 @@
1
+ # PlayStation (PS1) — source you can read
2
+
3
+ Trust hierarchy (try in order before filing a feedback round):
4
+
5
+ 1. **Bundled examples** (`examples/ps1/{shmup,platformer,puzzle,racing,sports}/main.c` +
6
+ `sprite_move/`) — verified building + rendering on the GPU. Start here.
7
+ 2. **Bundled helper lib source** (`src/platforms/ps1/lib/c/psx.c` + `psx.h`) — the GPU
8
+ front end. Read this when a primitive renders wrong: it shows how the software-3D
9
+ transform feeds **GP0 GPU primitives** (the real console path — the GPU rasterizes the
10
+ command stream; this is NOT a software framebuffer). The GP0 `0x60` rect-vs-poly gotcha
11
+ is in here + TROUBLESHOOTING.
12
+ 3. **The core source** (NOT bundled — fetch on demand):
13
+
14
+ | What | Upstream |
15
+ |---|---|
16
+ | beetle-psx (Mednafen PSX, libretro, HW renderer) | https://github.com/libretro/beetle-psx-libretro |
17
+ | R3000A CPU / SPU / GPU internals | (in the beetle tree: `mednafen/psx/cpu.c`, `spu.c`, `gpu.cpp`) |
18
+
19
+ The cpuState/audioDebug exports we patch in (`romdev_mips_regs_get`,
20
+ `romdev_spu_get`) live in `scripts/patches/romdev-snippets/beetle-psx-regsnap.c` +
21
+ `beetle-psx-spu.c`.
22
+
23
+ 4. **The toolchain** — a from-scratch `mips-elf-gcc` cross-compiler (little-endian) built
24
+ to WASM (`scripts/build-mips-toolchain.sh` — one toolchain emits both N64 BE + PS1 LE):
25
+
26
+ | Component | Upstream |
27
+ |---|---|
28
+ | binutils | https://ftp.gnu.org/gnu/binutils/ |
29
+ | gcc | https://ftp.gnu.org/gnu/gcc/ |
30
+ | newlib | https://sourceware.org/pub/newlib/ |
31
+
32
+ 5. **Reverse engineering** — Rizin + Ghidra MIPS (R3000A):
33
+
34
+ | What | Upstream |
35
+ |---|---|
36
+ | Rizin | https://github.com/rizinorg/rizin |
37
+ | rz-ghidra | https://github.com/rizinorg/rz-ghidra |
38
+
39
+ ## PS1 hardware docs
40
+
41
+ - **psx-spx** (Nocash, the canonical reference): https://psx-spx.consoledev.net/
42
+ - **PSn00bSDK** (a real open PS1 SDK — read its GPU/GTE/SPU code for the standard GP0/GP1
43
+ encodings + SPU register layout): https://github.com/Lameguy64/PSn00bSDK
44
+
45
+ ## When to use what
46
+
47
+ - "Why is my screen BLACK?" → MENTAL_MODEL + TROUBLESHOOTING (you must issue GP0
48
+ primitives + set the draw/display environment — a CPU memset won't show).
49
+ - "My rectangles stretch to the edge" → the GP0 `0x60` variable-size-rect gotcha
50
+ (TROUBLESHOOTING).
51
+ - "How does the helper emit a triangle?" → `psx.c` + psx-spx GPU section.
52
+ - "audioDebug SPU volumes look off" → the SPU volume/sweep registers read processed
53
+ values; the decode reads the raw mirror (see `psx.c` / beetle `spu.c`).
54
+ - "disasm returns a single `fcn.00000000`" → TROUBLESHOOTING (the absolute-`jal` rebase).
@@ -261,6 +261,16 @@ synthesizes a fallback `issues[]` entry with a hint. The idioms to avoid:
261
261
  block where the layout expects it. Use
262
262
  `examples({op:'snippets', platform:"snes", mode:"get", snippetName:"lorom_header.asm"})`
263
263
  for the canonical layout (and `lorom_multibank.asm` for multi-bank).
264
+ - **`readfile1`/`filesize`/`canreadfile` across MANY distinct files.** Converting
265
+ assets at assemble time (the commercial-disassembly idiom — e.g. an `incpal`
266
+ macro reading every `.pal` palette to emit 15-bit color) reads many *distinct*
267
+ files via the readfile API. asar-WASM can **abort with no diagnostic** above a
268
+ threshold of distinct files read that way (one file read 200× is fine — it's
269
+ the *count of distinct files*). `incbin` does NOT have this problem. The build
270
+ log carries an `[asar advisory]`/abort-hint when it sees a source over the
271
+ threshold. **Fix: pre-convert the assets to `.bin` blobs offline and `incbin`
272
+ them** — the output is byte-identical. (Put the converted blobs in a subdir;
273
+ `output:'project'` stages subdirectory assets recursively.)
264
274
 
265
275
  (This is the asar/asm path. The default PVSnesLib **C** path goes through
266
276
  tcc-65816 + wla-65816 and has its own C89 trap, above.)