diff --git a/docs/dev/GE-ENV-PROBES.md b/docs/dev/GE-ENV-PROBES.md index fad172aa..98423a8f 100644 --- a/docs/dev/GE-ENV-PROBES.md +++ b/docs/dev/GE-ENV-PROBES.md @@ -68,6 +68,7 @@ Two classes: | `GE_D245_FIXEDSHIFT` | `src/game/sky.c:946` (`#ifdef PORT`) | D245 test override: pins the water tc shift `k` to the given integer (0..SKY_TC_MAX_SHIFT) instead of the computed per-vertex value, to A/B the allowShift=TRUE behavior. Off by default (unset = current behavior). | **live, test-only** (D245 OPEN) | | `GE_D252` | `port/fast3d/gfx_pc.cpp:733` (`import_texture`, cached `s_d252`) | D252 rainbow-particle diag: one-shot per (address,size) import log of the tile state (tmem/fmt/siz/addr/size/line) for small textures so a suspect particle tile can be checked against its source bytes by hand. Remove once D252 is resolved. | **live** (D252 OPEN — non-reproducible) | | `GE_D252POOL` | `src/game/image.c` (`texLoad`), `src/game/initmttex.c`, `port/src/gimgfixup.c` | **TEMP** (M-201): one line per new texture load (pool main/other, free bytes) + `POOL-FULL` events + stage pool range; `D252SYNC: N slots re-synced, M changed` per stage load (the D252 fix at work: M>0 from the 2nd stage on). Load-time only, no fps cost. | **TEMP** (D252 fixed; remove after release) | +| `GE_D245_OLDVTX=1` / `GE_D245_OLDTEX=1` | `src/game/sky.c` (`skyPortRenderPoly`) / `port/fast3d/gfx_pc.cpp` (CI8 import + tri-path dims) | **A/B** (M-201): restore the pre-fix s16 sky/water vertex path / the pre-fix 32x43, 16-wide water texture geometry. | **TEMP** (D245 fixed; remove post-release) | | `GE_D207S` | `src/game/gunfire.c` (player shot-sound block, `#ifdef PORT`) | **TEMP** (M-201): one line per player shot-sound decision: `D207S t= hand item rate -> H1|H2|SKIP` (SKIP = both per-hand sound handles still held). Read-only, uncapped, cheap; use instead of `GE_AUDIOTRACE` for live captures (that probe does file I/O on the audio path). | **TEMP** (D207/D241) | | `GE_D306C="f0 f1 [wmax]"` | `port/fast3d/gfx_pc.cpp` (after `zf_note_emit`) | **TEMP** (M-201): depth-state census of every emitted triangle in frames f0..f1 (GL depth mode, geometry_mode, other_mode_l, tile-0 texture, NDC centroid, z/w range, min w); optional `wmax` keeps near geometry only. Cap 60000 lines. | **TEMP** (D306 closed; remove post-release) | | `GE_INTROCAM=` | `src/game/bondview_r.c` (`#ifdef PORT`, intro camera pick) | **TEMP/test-only**: force the level intro's first camera index (Dam truck shot = 5). The PRNG draw is still consumed. Logs `D306: intro camera index` when set (or with `GE_D306C`). | **TEMP** (useful repro tool) | diff --git a/docs/dev/findings-index.csv b/docs/dev/findings-index.csv index 1628e71d..6b264335 100644 --- a/docs/dev/findings-index.csv +++ b/docs/dev/findings-index.csv @@ -139,9 +139,9 @@ D238,FIXED,"QoL ask (M-106, user QA):",FIXED (2026-09-26: covered by D337/D338 a D239,CLOSED,"QoL ask (M-106, user QA):",CLOSED — executed in the v0.2.0 prep pass (port-only)… D240,OPEN,"Player gunshot SFX timing/cadence reportedly does not match the N64 original's rate (M-106, user QA report).","OPEN — not root-caused; static call-site tracing only, no runtime verification possible in this environment." D241,OPEN,Player gunshot SFX goes silent under some conditions —,"OPEN — not newly root-caused; high confidence this is D207 reproduced, not an independent bug." -D242,OPEN,Player-death sequence UX regression vs N64 original:,"OPEN — N64 reference obtained (2026-09-26, user on 1964:" +D242,FIXED,Player-death sequence UX regression vs N64 original:,"FIXED/CLOSED (2026-09-26, user:" D243,FIXED,"Dam end-of-level cutscene (the abseil/bungee sequence, D148/D160's subject):","FIXED, live-confirmed by the user (M-189/M-190, 2026-09-18)." -D245,OPEN,IsWater water: two competing patterns with a visible seam that moves as the player looks around (mouse/aim) —,"OPEN — CONFIRMED PORT BUG (2026-09-26, user vs 1964:" +D245,FIXED,IsWater water: two competing patterns with a visible seam that moves as the player looks around (mouse/aim) —,"FIXED (M-201, 2026-09-26, user-verified vs 1964):" D246,FIXED,"Tiny pixel strips at the left and right edges of the screen, ""like overscan"" —","FIXED (2026-09-18 session, port-only, Video.SafeAreaCrop)." D247,FIXED,Black bars top and bottom of screen —,"FIXED (2026-09-18 session, port-only, Video.SafeAreaCrop)." D248,FIXED,"Port runs a stable 30 FPS instead of 60 FPS game-wide (M-117, user asked to verify FPS after the D209 timing-keystone fixes).","FIXED, root-caused to a genuine pointer-width ABI bug in src/sched.c (compiled for the PC build, not excluded)." diff --git a/docs/dev/findings.md b/docs/dev/findings.md index e7144793..3e0ca0b8 100644 --- a/docs/dev/findings.md +++ b/docs/dev/findings.md @@ -546,9 +546,9 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D239 | **QoL ask (M-106, user QA): click-to-lock vs always-grab parity.** — full `## D239` entry at file tail | CLOSED — executed in the v0.2.0 prep pass (port-only)… | | D240 | **Player gunshot SFX timing/cadence reportedly does not match the N64 original's rate (M-106, user QA report).** — full `## D240` entry at file tail | **OPEN — not root-caused; static call-site tracing only, no runtime verification possible in this environment.** Confidence LOW-MEDIUM that this is a *new* bug distinct from the D155/D186/D193/D204/D209 timing-keystone family (all of which gate the identical `deltaFrames→speedgraphframes→g_ClockTimer→g_GlobalTimer` chain this SFX trigger consumes).… | | D241 | **Player gunshot SFX goes silent under some conditions — reported on Bunker with the alarm sounding, "or some other factor" — but only for PP7 and AK47, Klobb…** — full `## D241` entry at file tail | **OPEN — not newly root-caused; high confidence this is D207 reproduced, not an independent bug.** Recommend folding this repro into D207 (or closing D241 as a duplicate once D207 is reopened) rather than tracking separately.… | -| D242 | **Player-death sequence UX regression vs N64 original: PC requires an LMB click to proceed to the mission-failed/file-select screen; user recalls N64 auto-plays…** — full `## D242` entry at file tail | OPEN — N64 reference obtained (2026-09-26, user on 1964: after death the game returns to mission select and shows the "killed in action" screen); PC behaviour still to validate. Earlier: OPEN — 1964 comparison owed (2026-09-26, user). Earlier: **NOT A PORT-LOGIC BUG as far as static analysis can tell.** Confidence HIGH that the LMB-click skip path is faithful original behavior (Z_TRIG-mapped, exists unmodified in `bondview2.c`), not a defect to remove.… | +| D242 | **Player-death sequence UX regression vs N64 original: PC requires an LMB click to proceed to the mission-failed/file-select screen; user recalls N64 auto-plays…** — full `## D242` entry at file tail | FIXED/CLOSED (2026-09-26, user: death -> mission failed flow works without input, matches N64). Earlier: OPEN — N64 reference obtained (2026-09-26, user on 1964: after death the game returns to mission select and shows the "killed in action" screen); PC behaviour still to validate. Earlier: OPEN — 1964 comparison owed (2026-09-26, user). Earlier: **NOT A PORT-LOGIC BUG as far as static analysis can tell.** Confidence HIGH that the LMB-click skip path is faithful original behavior (Z_TRIG-mapped, exists unmodified in `bondview2.c`), not a defect to remove.… | | D243 | **Dam end-of-level cutscene (the abseil/bungee sequence, D148/D160's subject): the camera violently shakes/"spazzes" during each of the scripted camera-angle…** — full `## D243` entry at file tail | **FIXED, live-confirmed by the user (M-189/M-190, 2026-09-18).** Root cause found: `bondview2.c:598-599`'s manual buffer-layout math for Bond's third-person cutscene body model reserved space using a hardcoded 32-bit `sizeof(Model)` literal (`0xfb`) that never accounted for pointer widening — real 64-bit `sizeof(Model)` is 232 bytes, 44 more than reserved, so the animation-data buffer placed right after it overlapped the model's own `playspeed`/`animrate`/`unkac`/`unkb0`/`unkb4` fields, corrupting them (this is what M-183/M-185's clamps were band-aiding). Fixed by computing the real `sizeof(Model)` instead of the stale literal, matching the identical idiom already used one line above for `ModelFileHeader`. **M-190 follow-up:** M-170's camera-shake freeze was replaced with a filter re-seed-on-teleport (camera now tracks Bond continuously within each shot instead of holding a frozen angle), and an unrelated bug in the same review pass — mouse-look never gated during scripted/no-control camera states (D300's own documented-but-unimplemented safety gate) — was found and fixed in `port/src/input.c`. User confirmed both fixes live, full intro-flyover + Dam abseil playthrough: "ok yeah looks good." | -| D245 | **IsWater water: two competing patterns with a visible seam that moves as the player looks around (mouse/aim) — "looks almost exactly like skybox" (M-116, user…** — full `## D245` entry at file tail | OPEN — CONFIRMED PORT BUG (2026-09-26, user vs 1964: PC water displays glitched with a weird yellow line; view-dependent shift is subtle; animation otherwise matches). Earlier: OPEN — REOPENED, M-142 headless probe found the tc-shift `k` measurably flips during ordinary view rotation (new root-cause evidence); an opt-in `GE_D245_FIXEDSHIFT` candidate is build-verified but needs a live A/B, not shipped as default.… | +| D245 | **IsWater water: two competing patterns with a visible seam that moves as the player looks around (mouse/aim) — "looks almost exactly like skybox" (M-116, user…** — full `## D245` entry at file tail | FIXED (M-201, 2026-09-26, user-verified vs 1964): water texture imported/normalised at 32x43/16-wide instead of the 32x32 CI8 mask period (resets, 2x speed) + s16 sky/water vertices replaced by G_FLOATVTX_EXT floats (jitter). Earlier: OPEN — CONFIRMED PORT BUG (2026-09-26, user vs 1964: PC water displays glitched with a weird yellow line; view-dependent shift is subtle; animation otherwise matches). Earlier: OPEN — REOPENED, M-142 headless probe found the tc-shift `k` measurably flips during ordinary view rotation (new root-cause evidence); an opt-in `GE_D245_FIXEDSHIFT` candidate is build-verified but needs a live A/B, not shipped as default.… | | D246 | **Tiny pixel strips at the left and right edges of the screen, "like overscan" — look like raw pixels, not 3D graphics (M-116, user QA report).** — full `## D246` entry at file tail | **FIXED (2026-09-18 session, port-only, `Video.SafeAreaCrop`).** Measured precisely (exactly 1.0 logical unit missing on both edges, every resolution); underlying clip-precision root cause not isolated, but `gfx_adjust_viewport_or_scissor` now trims a fixed 1-unit margin from both edges — same toggle/mechanism as D247. Live user playtest confirmed clean edges. | | D247 | **Black bars top and bottom of screen — possible aspect-ratio defect (M-116, user QA report).** — full `## D247` entry at file tail | **FIXED (2026-09-18 session, port-only, `Video.SafeAreaCrop`).** Root cause: faithful, unmodified N64 TV-safe-area letterboxing that real CRT overscan used to hide; this port showed the full VI frame. Per user request, given a real fix (not left as "correct original behaviour"): `gfx_adjust_viewport_or_scissor` now remaps against the active gameplay viewport bounds so it fills the window edge-to-edge. New `Video.SafeAreaCrop` toggle (default on). Live user playtest confirmed clean edges. | | D248 | **Port runs a stable 30 FPS instead of 60 FPS game-wide (M-117, user asked to verify FPS after the D209 timing-keystone fixes).** — full `## D248` entry at file tail | **FIXED, root-caused to a genuine pointer-width ABI bug in `src/sched.c` (compiled for the PC build, not excluded).** `osScAddClient()` (`src/sched.c:187-199`) stashes a per-client "present every retrace vs.… | @@ -11613,6 +11613,17 @@ D173 (level-start floating), D292 (death sinking), and the Cradle spin (also und **Untested candidate, explicitly NOT shipped as the default** (per this row's own "don't repeat the same mistake" lesson from the superseded `allowShift=TRUE` fix): `skyPortForceFanShift()` + `GE_D245_FIXEDSHIFT=` env var (`sky.c`, `#ifdef PORT`) lets a live session force a single constant `k` for the whole water draw instead of the per-frame adaptive pick, eliminating the transition this session found — but at the cost of coarser tc quantization *always* (a fixed `k=5` collapses to whole-texel granularity, which could itself look worse on the shimmer cross-fade; that trade-off can only be judged by eyes on the actual water, not another headless capture). Build-verified crash-free with `GE_D245_FIXEDSHIFT=5` on Frigate (300+ frames); `verify.sh frigate`/`verify.sh "surface 2"` both PASS with the env var unset (default path byte-identical to the current reopened behaviour — nothing changes for players until someone opts in). **Next step for whoever has live Frigate/Surface-2 access:** play with `GE_D245_FIXEDSHIFT` unset (current behaviour, confirm the glitch still reproduces) vs. set to 3, 4, and 5 in turn, and report which (if any) looks better — that's the missing data point, not more static tracing. +**M-201 (2026-09-26, v0.4.0): FIXED — two port-layer causes; user-verified against 1964 ("looks accurate").** + +User vs 1964: PC water jittered with player movement, "reset" even standing still, animated too fast (the earlier yellow line was not present on the current build). + +1. **Texture geometry (the resets + speed).** The IsWater image is a 32x32 CI8 texture (mask 5 = 32-texel period) loaded with its mip chain (1400 B) and drawn through an RGBA16 tile (the D229 case). fast3d imported it CI8 as **32x43** (mip rows included) while the triangle path normalised UVs with the RGBA16 reading, **width 16** (line bytes / 2) and height 43 (`GE_D229` probe: `texw=16 texh=43`). So S ran at 2x frequency and T repeated every 43 rows, not 32; the port's per-fan S/T fold (multiples of 64 texels) is not a multiple of 43, so every fold step jumped the pattern. Fix (`gfx_pc.cpp`, D229 case only): import only the `line_size * (1< #include #include @@ -1336,7 +1337,23 @@ static void import_texture(int i, int tile, bool importReplacement) { if (siz_eff == G_IM_SIZ_4b) { import_texture_ci4(tile, loaded_texture, rdp.tex_lod); } else if (siz_eff == G_IM_SIZ_8b) { - import_texture_ci8(tile, loaded_texture, rdp.tex_lod); + /* D245 (M-201): for the D229 case (RGBA16 tile over a CI8 load -- + * the IsWater sky water), upload only the base level: the load + * carries the whole mip chain (32x32 base + mips = 1400 B), which + * made a 32x43 image whose GL REPEAT period (43 rows) differs from + * the N64's mask period (32). See the tri-path counterpart. */ + static int d245_oldtex = -1; + if (d245_oldtex < 0) d245_oldtex = getenv("GE_D245_OLDTEX") != NULL; + const uint8_t maskt = rdp.texture_tile[tile].maskt; + if (!d245_oldtex && fmt == G_IM_FMT_RGBA && siz == G_IM_SIZ_16b && maskt > 0 && maskt < 12 && + loaded_texture.line_size_bytes > 0 && + loaded_texture.size_bytes > loaded_texture.line_size_bytes * (1u << maskt)) { + LoadedTexture lt = loaded_texture; + lt.size_bytes = loaded_texture.line_size_bytes * (1u << maskt); + import_texture_ci8(tile, lt, rdp.tex_lod); + } else { + import_texture_ci8(tile, loaded_texture, rdp.tex_lod); + } } else { sysFatalError("Bad size for CI texture in tile %d: %02x", tile, siz); } @@ -2415,6 +2432,22 @@ static void gfx_sp_tri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx, bo break; } tex_width[i] = line_size; + /* D245 (M-201): D229 water (RGBA16 tile over a CI8 load) is + * imported as CI8 -- one byte per texel, base level only -- so the + * UV normalisation must use that geometry: width = line bytes + * (32, not 32/2 = 16, which doubled the S frequency) and height = + * the mask period (32, not 1400/32 = 43 rows of mip chain). */ + { + static int d245_oldtex2 = -1; + if (d245_oldtex2 < 0) d245_oldtex2 = getenv("GE_D245_OLDTEX") != NULL; + const auto& ltx = rdp.loaded_texture[rdp.texture_tile[tile].tmem]; + if (!d245_oldtex2 && rdp.texture_tile[tile].fmt == G_IM_FMT_RGBA && + rdp.texture_tile[tile].siz == G_IM_SIZ_16b && ltx.src_fmt == G_IM_FMT_CI) { + const uint8_t mkt = rdp.texture_tile[tile].maskt; + tex_width[i] = rdp.texture_tile[tile].line_size_bytes; + if (mkt > 0 && mkt < 12 && tex_height[i] > (1u << mkt)) tex_height[i] = 1u << mkt; + } + } tex_width2[i] = (rdp.texture_tile[tile].lrs - rdp.texture_tile[tile].uls + 4) / 4; tex_height2[i] = (rdp.texture_tile[tile].lrt - rdp.texture_tile[tile].ult + 4) / 4; @@ -3662,6 +3695,38 @@ static void gfx_dp_set_other_mode(uint32_t h, uint32_t l) { rdp.other_mode_l = l; } +/* D245 (M-201): G_FLOATVTX_EXT loader -- gfx_sp_vertex for pre-transformed + * clip-space float vertices with float S/T (see port/include/floatvtx.h). + * Only the unlit, unfogged path the sky/water fans use is supported. */ +static void gfx_sp_vertex_float(size_t n_vertices, size_t dest_index, const PortFloatVtx* vertices) { + if (!fast3d_ptr_ok(vertices) || dest_index + n_vertices > MAX_VERTICES) { + return; + } + for (size_t i = 0; i < n_vertices; i++, dest_index++) { + const PortFloatVtx* v = &vertices[i]; + struct LoadedVertex* d = &rsp.loaded_vertices[dest_index]; + float x = gfx_adjust_x_for_aspect_ratio(v->x, v->w); + float y = v->y, z = v->z, w = v->w; + d->u = v->s * (float)rsp.texture_scaling_factor.s / 65536.0f; + d->v = v->t * (float)rsp.texture_scaling_factor.t / 65536.0f; + d->color.r = v->r; + d->color.g = v->g; + d->color.b = v->b; + d->color.a = v->a; + d->clip_rej = 0; + if (x < -w) d->clip_rej |= 1; + if (x > w) d->clip_rej |= 2; + if (y < -w) d->clip_rej |= 4; + if (y > w) d->clip_rej |= 8; + if (z > w) d->clip_rej |= 32; + d->x = x; + d->y = y; + d->z = z; + d->w = w; + d->fog = rdp.fog_color.a; + } +} + static inline void *seg_addr(uintptr_t w1) { // GE model files reference GDLs (gSPDisplayList) and vertex arrays by raw // VMA 0x05xxxxxx WITHOUT the LSB set; segment 5 is set per-render to the @@ -3780,6 +3845,9 @@ static void gfx_run_dl(Gfx* cmd) { case (uint8_t)G_CLEARGEOMETRYMODE: gfx_sp_geometry_mode(cmd->words.w1, 0); break; + case G_FLOATVTX_EXT: /* D245 */ + gfx_sp_vertex_float(C0(0, 16), C0(16, 8), (const PortFloatVtx*)seg_addr(cmd->words.w1)); + break; case 0x46: /* G_HUDSCALE_EXT (D226): w0 low16 = scale*256 (0/256 = off), w1 = ax4<<16 | ay4 */ { const uint32_t sc = C0(0, 16); diff --git a/port/include/floatvtx.h b/port/include/floatvtx.h new file mode 100644 index 00000000..ffff7658 --- /dev/null +++ b/port/include/floatvtx.h @@ -0,0 +1,33 @@ +#ifndef PORT_FLOATVTX_H +#define PORT_FLOATVTX_H + +/* D245 (M-201): port-only full-precision vertex load for the sky/water fans. + * + * On N64, sky.c builds these triangles directly as RDP edge/texture + * coefficients with 32-bit S/T, so a horizon-scale quad (the IsWater ocean + * spans ~300,000 S10.5 units, ~9,400 texels) is exact. Routed through an + * ordinary gSPVertex, both its texture coords (s16 tc: +-1,024 texels) and its + * positions (s16 ob, divided by a fan-wide wScale to fit camera-space w up to + * ~2e5) had to be quantised -- visible as water/sky jitter that slides with + * the view. G_FLOATVTX_EXT hands fast3d the clip-space position and S/T as + * floats instead; fast3d already stores every loaded vertex as floats. + * + * w0 = G_FLOATVTX_EXT << 24 | dest_index << 16 | count, w1 = PortFloatVtx *. + * s/t are in Vtx.tc units (S10.5, before gSPTexture scaling). */ +#define G_FLOATVTX_EXT 0x47 + +typedef struct PortFloatVtx { + float x, y, z, w; /* clip space */ + float s, t; /* tc units */ + unsigned char r, g, b, a; +} PortFloatVtx; + +#define gSPFloatVertexExt(pkt, ptr, n, v0) \ + do { \ + Gfx *_g = (Gfx *)(pkt); \ + _g->words.w0 = ((uintptr_t)G_FLOATVTX_EXT << 24) | \ + (((uintptr_t)(v0) & 0xff) << 16) | ((uintptr_t)(n) & 0xffff); \ + _g->words.w1 = (uintptr_t)(ptr); \ + } while (0) + +#endif diff --git a/src/game/sky.c b/src/game/sky.c index fe553a0b..94d56766 100644 --- a/src/game/sky.c +++ b/src/game/sky.c @@ -5,6 +5,10 @@ #include #endif #include "sky.h" +#ifdef PORT +#include "floatvtx.h" /* D245: G_FLOATVTX_EXT */ +#include "envflag.h" +#endif #include "player.h" #include "unk_092E50.h" #include "bondview.h" @@ -1996,6 +2000,53 @@ static Gfx *skyPortRenderPoly(Gfx *gdl, SkyRelated38 **v, s32 nverts) wScale = (maxAbsW > 30000.0f) ? (maxAbsW / 30000.0f) : 1.0f; } + /* D245 (M-201): full-precision path. Hand fast3d the clip-space position + * (ndcX*w, ndcY*w, 0, w) and the folded S/T as floats (G_FLOATVTX_EXT, + * port/include/floatvtx.h) instead of squeezing them through s16 Vtx + * ob/tc: no wScale position quantisation, no 2^k tc shift. The fold is + * still applied (a multiple of the repeat period, exact under wrapping) + * to keep the float magnitudes small. GE_D245_OLDVTX=1 restores the old + * s16 route for A/B. */ + if (!GE_ENVFLAG("GE_D245_OLDVTX")) + { + PortFloatVtx *fv = (PortFloatVtx *) dynAllocateVertices(2 * (nverts < 3 ? 3 : nverts)); + + for (i = 0; i < nverts; i++) + { + f32 screenX = v[i]->unk28 * 0.25f; + f32 screenY = v[i]->unk2c * 0.25f; + f32 ndcX = 2.0f * ((screenX - l) / (r - l)) - 1.0f; + f32 ndcY = 1.0f - 2.0f * ((screenY - t) / (b - t)); + f32 w = v[i]->unk0c; + + fv[i].x = ndcX * w; + fv[i].y = ndcY * w; + fv[i].z = 0.0f; + fv[i].w = w; + fv[i].s = v[i]->unk20 - foldS; + fv[i].t = v[i]->unk24 - foldT; + fv[i].r = (u8) v[i]->r; + fv[i].g = (u8) v[i]->g; + fv[i].b = (u8) v[i]->b; + fv[i].a = (u8) v[i]->a; + } + + gSPClearGeometryMode(gdl++, G_LIGHTING | G_CULL_BOTH | G_FOG); + gSPSetGeometryMode(gdl++, G_SHADE | G_SHADING_SMOOTH); + gSPFloatVertexExt(gdl++, fv, nverts, 0); + + if (nverts >= 4) + { + gSP2Triangles(gdl++, 0, 1, 3, 0, 3, 2, 0, 0); + } + else + { + gSP1Triangle(gdl++, 0, 1, 2, 0); + } + + return gdl; + } + guMtxIdentF(projf.m); /* row = input axis (matches this codebase's row-vector * M convention, * e.g. sub_GAME_7F097388 / gfx_pc.cpp's own MP_matrix use):