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.
- package/CHANGELOG.md +59 -0
- package/examples/dreamcast/platformer/main.c +31 -0
- package/examples/dreamcast/puzzle/main.c +44 -0
- package/examples/dreamcast/racing/main.c +39 -0
- package/examples/dreamcast/shmup/main.c +50 -0
- package/examples/dreamcast/sports/main.c +39 -0
- package/package.json +3 -3
- package/src/cores/capabilities.js +13 -9
- package/src/host/LibretroHost.js +242 -23
- package/src/host/callbacks.js +68 -1
- package/src/host/cpu-state.js +32 -0
- package/src/host/dc-aica-state.js +67 -0
- package/src/mcp/tools/audio.js +1 -1
- package/src/mcp/tools/disasm.js +1 -1
- package/src/mcp/tools/index.js +1 -1
- package/src/mcp/tools/platform-docs.js +1 -1
- package/src/mcp/tools/platform-tools.js +9 -1
- package/src/mcp/tools/toolchain.js +115 -10
- package/src/platforms/dreamcast/MENTAL_MODEL.md +87 -0
- package/src/platforms/dreamcast/TROUBLESHOOTING.md +55 -0
- package/src/platforms/dreamcast/UPSTREAM_SOURCES.md +57 -0
- package/src/platforms/n64/MENTAL_MODEL.md +84 -0
- package/src/platforms/n64/TROUBLESHOOTING.md +60 -0
- package/src/platforms/n64/UPSTREAM_SOURCES.md +52 -0
- package/src/platforms/n64/lib/c/n64.c +181 -80
- package/src/platforms/ps1/MENTAL_MODEL.md +85 -0
- package/src/platforms/ps1/TROUBLESHOOTING.md +55 -0
- package/src/platforms/ps1/UPSTREAM_SOURCES.md +54 -0
- package/src/platforms/snes/TROUBLESHOOTING.md +10 -0
- package/src/toolchains/asar/asar.js +84 -14
- package/src/toolchains/mips-c/mips-c.js +35 -1
- package/src/toolchains/sh-c/lib/dc.h +65 -15
- package/src/toolchains/sh-c/sh-c.js +6 -3
|
@@ -1,117 +1,218 @@
|
|
|
1
|
-
/* n64.c — N64 helpers
|
|
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
|
-
/*
|
|
5
|
-
#define
|
|
6
|
-
#define
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
#define
|
|
12
|
-
#define
|
|
13
|
-
#define
|
|
14
|
-
|
|
15
|
-
#define
|
|
16
|
-
#define
|
|
17
|
-
#define
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
VI(
|
|
28
|
-
VI(
|
|
29
|
-
VI(
|
|
30
|
-
VI(
|
|
31
|
-
VI(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
57
|
-
|
|
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
|
-
/*
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
if (
|
|
69
|
-
if (y1
|
|
70
|
-
if (
|
|
71
|
-
if (
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (
|
|
81
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
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;
|
|
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;
|
|
207
|
+
si[1] = 0x1FC007C0;
|
|
107
208
|
{ volatile int t; for (t = 0; t < 5000; t++) { } }
|
|
108
|
-
buttons = pif[1] >> 16;
|
|
109
|
-
return (~0u) & buttons;
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.)
|