diff --git a/.gitignore b/.gitignore index e4cfe8dd..d008a355 100644 --- a/.gitignore +++ b/.gitignore @@ -125,6 +125,9 @@ ge007.crash.log .pi/ run_out.log ppm/ +screenshots/ +# local Mupen64 Plus checkout (N64 reference footage); binaries, do not commit +tools_pc/mupen64/ # PC port run/scratch logs run_*.log diff --git a/PORTING.md b/PORTING.md index f22ec01f..c3e2b07a 100644 --- a/PORTING.md +++ b/PORTING.md @@ -8,9 +8,10 @@ It follows the approach of the [Perfect Dark PC port](https://github.com/perfect-dark-pc-port/perfect_dark), which ports the same Rare "Indy" engine (a later revision). -> **Status: scaffolding.** The directory structure, build system, and port-layer -> interfaces are in place. The actual implementation is being brought in phase -> by phase. See below. +> **Status: Phase 2 (rendering) in progress.** The game boots on PC, plays the +> intro music, and renders the entire intro (logo โ†’ gun barrel โ†’ cast); +> the current blocker is first stage load (BG-file endianness, finding D69). +> Current session state and immediate tasks: **[docs/HANDOFF.md](docs/HANDOFF.md)**. ## Read this first @@ -45,23 +46,23 @@ docs/PCPortResearch.md The N64 build (`Makefile`, `tools/`, `rsp/`, etc.) is **untouched**. -## Building (once implementation lands) +## Building ```sh ./build-pc.sh ntsc-final # or pal-final / jpn-final ``` -Then put your ROM in `./data/` and run the binary. (The ROM is not -distributed; you must own the game.) +Then put your ROM in `./data/` and run the binary from the repo root. +(The ROM is not distributed; you must own the game.) ## Phased plan (summary) -| Phase | Goal | -|-------|------| -| 0 | Scaffolding (this change) | -| 1 | Boot to a window: system/fs/rom/config/video + OS shims | -| 2 | Rendering: bring in fast3d, add `G_SETTEX`, port scheduler | -| 3 | Audio + input | -| 4 | Saves + polish (widescreen, mouse-look, FPS, config) | +| Phase | Goal | Status | +|-------|------|--------| +| 0 | Scaffolding: build system + port-layer interfaces | done | +| 1 (+1.5) | Boot to window / first frame: ROM map, OS shims, ABI reconciliation | done | +| 2 | Rendering: fast3d software RSP, asset conversion/fixups | **in progress** (full intro renders; stage load = D69) | +| 3 | Audio + input (SDL device, mixer, gamepad) | not started | +| 4 | Saves + polish (file-backed EEPROM, widescreen, config) | not started | Details and risks: [docs/PCPortResearch.md ยง8โ€“9](docs/PCPortResearch.md). diff --git a/docs/reference/n64-footage-2026-08-24.jpg b/docs/reference/n64-footage-2026-08-24.jpg new file mode 100644 index 00000000..cdd16f36 Binary files /dev/null and b/docs/reference/n64-footage-2026-08-24.jpg differ diff --git a/docs/reference/n64-footage-gun-barrel.png b/docs/reference/n64-footage-gun-barrel.png new file mode 100644 index 00000000..7668de42 Binary files /dev/null and b/docs/reference/n64-footage-gun-barrel.png differ diff --git a/tools_pc/mtxtest.c b/tools_pc/mtxtest.c new file mode 100644 index 00000000..4eaebae0 --- /dev/null +++ b/tools_pc/mtxtest.c @@ -0,0 +1,212 @@ +/* Standalone numerical test of the fast3d matrix pipeline conventions. + * Replicates: GE guPerspectiveF (swapped), guLookAtF, guRotateF, guMtxF2L + * packing, gfx_sp_matrix unpacking + MUL order, and the row-vector vertex + * transform in gfx_pc.cpp. Compares against the standard column-vector + * pipeline P_std * MV * v for two scenes: gun barrel (known-good render) + * and rareware logo (D72.3 off-screen bug). + */ +#include +#include +#include + +typedef float f32; + +/* ---- GE libultra functions (verbatim math) ---- */ + +static void guMtxIdentF(float mf[4][4]) { + for (int i = 0; i < 4; i++) + for (int j = 0; j < 4; j++) mf[i][j] = (i == j); +} + +/* GE's SWAPPED perspective: mf[2][3]=-1, mf[3][2]=2nf/(n-f) */ +static void guPerspectiveF_GE(float mf[4][4], float fovy, float aspect, float near_, float far_) { + float cot; + guMtxIdentF(mf); + fovy *= 3.1415926f / 180.0f; + cot = cosf(fovy / 2) / sinf(fovy / 2); + mf[0][0] = cot / aspect; + mf[1][1] = cot; + mf[2][2] = (near_ + far_) / (near_ - far_); + mf[2][3] = -1; + mf[3][2] = (2 * near_ * far_) / (near_ - far_); + mf[3][3] = 0; +} + +/* standard libultra perspective (for the "true" pipeline) */ +static void guPerspectiveF_STD(float mf[4][4], float fovy, float aspect, float near_, float far_) { + float cot; + guMtxIdentF(mf); + fovy *= 3.1415926f / 180.0f; + cot = cosf(fovy / 2) / sinf(fovy / 2); + mf[0][0] = cot / aspect; + mf[1][1] = cot; + mf[2][2] = (near_ + far_) / (near_ - far_); + mf[2][3] = (2 * near_ * far_) / (near_ - far_); + mf[3][2] = -1; + mf[3][3] = 0; +} + +static void guLookAtF(float mf[4][4], float xEye, float yEye, float zEye, + float xAt, float yAt, float zAt, + float xUp, float yUp, float zUp) { + float len, xLook, yLook, zLook, xRight, yRight, zRight; + guMtxIdentF(mf); + xLook = xAt - xEye; yLook = yAt - yEye; zLook = zAt - zEye; + len = -1.0f / sqrtf(xLook*xLook + yLook*yLook + zLook*zLook); + xLook *= len; yLook *= len; zLook *= len; + xRight = yUp * zLook - zUp * yLook; + yRight = zUp * xLook - xUp * zLook; + zRight = xUp * yLook - yUp * xLook; + len = 1.0f / sqrtf(xRight*xRight + yRight*yRight + zRight*zRight); + xRight *= len; yRight *= len; zRight *= len; + xUp = yLook * zRight - zLook * yRight; + yUp = zLook * xRight - xLook * zRight; + zUp = xLook * yRight - yLook * xRight; + len = 1.0f / sqrtf(xUp*xUp + yUp*yUp + zUp*zUp); + xUp *= len; yUp *= len; zUp *= len; + mf[0][0] = xRight; mf[1][0] = yRight; mf[2][0] = zRight; + mf[3][0] = -(xEye * xRight + yEye * yRight + zEye * zRight); + mf[0][1] = xUp; mf[1][1] = yUp; mf[2][1] = zUp; + mf[3][1] = -(xEye * xUp + yEye * yUp + zEye * zUp); + mf[0][2] = xLook; mf[1][2] = yLook; mf[2][2] = zLook; + mf[3][2] = -(xEye * xLook + yEye * yLook + zEye * zLook); + mf[0][3] = 0; mf[1][3] = 0; mf[2][3] = 0; mf[3][3] = 1; +} + +static void guRotateF(float mf[4][4], float a, float x, float y, float z) { + static float dtor = 3.1415926f / 180.0f; + float sine, cosine, ab, bc, ca, t; + float l = sqrtf(x*x + y*y + z*z); + if (l != 0) { x /= l; y /= l; z /= l; } + a *= dtor; + sine = sinf(a); cosine = cosf(a); + t = (1 - cosine); + ab = x * y * t; bc = y * z * t; ca = z * x * t; + guMtxIdentF(mf); + t = x * x; + mf[0][0] = t + cosine * (1 - t); + mf[2][1] = bc - x * sine; + mf[1][2] = bc + x * sine; + t = y * y; + mf[1][1] = t + cosine * (1 - t); + mf[2][0] = ca + y * sine; + mf[0][2] = ca - y * sine; + t = z * z; + mf[2][2] = t + cosine * (1 - t); + mf[1][0] = ab - z * sine; + mf[0][1] = ab + z * sine; +} + +/* ---- guMtxF2L pack / gfx_sp_matrix unpack (bit-exact path) ---- */ + +static void mtxf2l(const float mf[4][4], int32_t m[16]) { + for (int i = 0; i < 4; i++) + for (int j = 0; j < 2; j++) { + int e1 = (int)(mf[i][j*2] * 65536.0f); + int e2 = (int)(mf[i][j*2+1] * 65536.0f); + /* real guMtxF2L uses *(ai++)/*(af++): slot = i*2+j */ + m[i*2 + j] = (e1 & 0xffff0000) | ((e2 >> 16) & 0xffff); + m[8 + i*2 + j] = ((e1 << 16) & 0xffff0000) | (e2 & 0xffff); + } +} + +static void sp_matrix_unpack(const int32_t addr[16], float matrix[4][4]) { + for (int i = 0; i < 4; i++) + for (int j = 0; j < 4; j += 2) { + int32_t int_part = addr[i * 2 + j / 2]; + uint32_t frac_part = addr[8 + i * 2 + j / 2]; + matrix[i][j] = (int32_t)((int_part & 0xffff0000) | (frac_part >> 16)) / 65536.0f; + matrix[i][j + 1] = (int32_t)((int_part << 16) | (frac_part & 0xffff)) / 65536.0f; + } +} + +/* ---- gfx_pc.cpp conventions ---- */ + +static void mmul(float res[4][4], const float a[4][4], const float b[4][4]) { + float tmp[4][4]; + for (int i = 0; i < 4; i++) + for (int j = 0; j < 4; j++) + tmp[i][j] = a[i][0]*b[0][j] + a[i][1]*b[1][j] + a[i][2]*b[2][j] + a[i][3]*b[3][j]; + for (int i = 0; i < 4; i++) for (int j = 0; j < 4; j++) res[i][j] = tmp[i][j]; +} + +/* row-vector vertex transform exactly as in gfx_sp_vertex */ +static void xform_row(const float MP[4][4], const float v[3], float c[4]) { + c[0] = v[0]*MP[0][0] + v[1]*MP[1][0] + v[2]*MP[2][0] + MP[3][0]; + c[1] = v[0]*MP[0][1] + v[1]*MP[1][1] + v[2]*MP[2][1] + MP[3][1]; + c[2] = v[0]*MP[0][2] + v[1]*MP[1][2] + v[2]*MP[2][2] + MP[3][2]; + c[3] = v[0]*MP[0][3] + v[1]*MP[1][3] + v[2]*MP[2][3] + MP[3][3]; +} + +/* standard column-vector transform c = A*B*v */ +static void xform_col(const float A[4][4], const float B[4][4], const float v[3], float c[4]) { + float M[4][4]; + mmul(M, A, B); + for (int i = 0; i < 4; i++) + c[i] = M[i][0]*v[0] + M[i][1]*v[1] + M[i][2]*v[2] + M[i][3]; +} + +static void report(const char *tag, const float c[4]) { + float xw = c[3] != 0 ? c[0]/c[3] : 1e9f; + float yw = c[3] != 0 ? c[1]/c[3] : 1e9f; + printf(" %-34s clip=(%12.1f,%12.1f,%12.1f,%12.1f) x/w=%8.4f y/w=%8.4f %s\n", + tag, c[0], c[1], c[2], c[3], xw, yw, + (fabsf(xw) <= 1 && fabsf(yw) <= 1) ? "ON-SCREEN" : "off-screen"); +} + +/* pack+unpack round-trip so we exercise the exact bit path */ +static void rt(const float mf[4][4], float out[4][4]) { + int32_t m[16]; + mtxf2l(mf, m); + sp_matrix_unpack(m, out); +} + +int main(void) { + float Pge[4][4], Pstd[4][4], MVla[4][4], Rot[4][4]; + float MPours[4][4], MPalt[4][4], Mtot[4][4]; + float c[4]; + + printf("== SCENE 1: gun barrel (eye 1758.3,220,684.3 dir -0.97,0,0.24 up 0,1,0; fov46 n10 f10000) ==\n"); + guPerspectiveF_GE(Pge, 46.0f, 320.0f/240.0f, 10.0f, 10000.0f); + guPerspectiveF_STD(Pstd, 46.0f, 320.0f/240.0f, 10.0f, 10000.0f); + guLookAtF(MVla, 1758.2957f, 220.0f, 684.28143f, + 1758.2957f - 0.97f, 220.0f, 684.28143f + 0.24f, 0, 1, 0); + rt(Pge, Pge); rt(MVla, MVla); + + float v[3] = { -57.3f, -20.0f, 16.3f }; /* sample point offset from target */ + mmul(MPours, MVla, Pge); /* our MP for LOAD-only */ + xform_row(MPours, v, c); + report("OURS row: v.MV.P_ge", c); + xform_col(Pstd, MVla, v, c); + report("TRUE col: P_std*MV*v", c); + + printf("\n== SCENE 2: rareware logo (eye 0,0,880 at 0,0,879 up 0,1,0; fov60 n100 f5000; rotY -40deg) ==\n"); + guPerspectiveF_GE(Pge, 60.0f, 320.0f/240.0f, 100.0f, 5000.0f); + guPerspectiveF_STD(Pstd, 60.0f, 320.0f/240.0f, 100.0f, 5000.0f); + guLookAtF(MVla, 0, 0, 880.0f, 0, 0, 879.0f, 0, 1, 0); + guRotateF(Rot, -40.0f, 0, 1, 0); + rt(Pge, Pge); rt(MVla, MVla); rt(Rot, Rot); + + float lv[3] = { 55.0f, 61.0f, 15.0f }; /* verts4258[0].ob */ + mmul(MPours, Rot, MVla); /* our code: new = matrix*old */ + mmul(MPours, MPours, Pge); + xform_row(MPours, lv, c); + report("OURS MUL (Rot*MV) then P", c); + + mmul(MPalt, MVla, Rot); /* alt: new = old*matrix */ + mmul(MPalt, MPalt, Pge); + xform_row(MPalt, lv, c); + report("ALT MUL (MV*Rot) then P", c); + + /* true standard pipeline: P_std * (MVla*Rot) * v [Rot applied first] */ + mmul(Mtot, MVla, Rot); + xform_col(Pstd, Mtot, lv, c); + report("TRUE col: P_std*MV*Rot*v", c); + + /* and the other hardware-MUL interpretation: P_std * (Rot*MV) * v */ + mmul(Mtot, Rot, MVla); + xform_col(Pstd, Mtot, lv, c); + report("TRUE col: P_std*Rot*MV*v", c); + + return 0; +}