diff --git a/docs/dev/GE-ENV-PROBES.md b/docs/dev/GE-ENV-PROBES.md index 057fdf79..c9259f5a 100644 --- a/docs/dev/GE-ENV-PROBES.md +++ b/docs/dev/GE-ENV-PROBES.md @@ -3,7 +3,7 @@ Every `getenv("GE_…")` in the tree. The table prose is hand-curated; **`tools_pc/gen_env_probes.py`** re-greps the live sites and reports drift (NEW / GONE vars + a fresh file:line map) — run it before trusting the -File:line cells. Last reconciled 2026-09-21 (GE_D235 added, D235 fix wired; GE_D288 refined per M-155 follow-up). All are **env-gated**: unset = zero behavior +File:line cells. Last reconciled 2026-09-21 (GE_D235 added, D235 fix wired; GE_D288 refined per M-155 follow-up; GE_D75V added for the Nintendo-logo vertex-slot investigation, M-195). All are **env-gated**: unset = zero behavior change, so they are safe to leave in a release build (they only cost a `getenv` on the guarded path). "Dead" below means *the finding it was built for is closed* — the probe is strip-candidate scaffolding, not that it does @@ -72,6 +72,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_D235` | `src/boss.c` (per-stage `while (!done)` block, after `memaReset`; `#ifdef PORT`) | D235 evidence probe: logs how many GL-texture-cache entries the per-stage `videoResetTextureCache()` call purges at each level load. A cold boot into a level should log 0; a death/restart (or any transition from a level that uploaded textures) logs >0 — direct evidence of the stale-entry mechanism. The clear itself is **not** gated on this var (it's the fix, see below); the var only controls the log line. | **live** (D235) | +| `GE_D75V` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_vertex`, end of the copy/transform loop; cached env check) | D75 (Nintendo logo) per-batch vertex probe: for each `G_VTX` batch in frames `lo..hi`, logs frame#, source pointer, n_vertices, original dest_index, raw ob[0..2] of the first vertex, min/max of the transformed x/y/z/w across the batch, and the MP translation row. Used to prove the transform is correct (M-195) and that every batch encodes v0=0 (slot-overwrite hypothesis). Value form `"lo-hi"` (e.g. `300-900` = the logo screen); unset = off. | **live** (D75) | ## Dxx diagnostic probes — finding closed unless noted diff --git a/docs/dev/findings.md b/docs/dev/findings.md index 324c6c0d..17f218b4 100644 --- a/docs/dev/findings.md +++ b/docs/dev/findings.md @@ -2426,6 +2426,8 @@ Confidence overall: **medium** (triage solid, no runtime confirmation). > renders as degenerate white blobs" plus the unverified cast roll; the > gun-barrel half is retired. The M-32b probe text below is left for history. +> **M-195 UPDATE (v0.4.0 Phase 1, 2026-09-21) — Nintendo logo: vertex transform proven CORRECT; leading hypothesis now = G_VTX slot overwrite.** Three-pass investigation this session: **(1) Emission is fine.** Fresh `GE_D63=1` bare-front-end run (log `/c/tmp/d75_d63.log`, 32k lines): all 23 nintendologo Primary GDLs are emitted **every frame** of the logo screen (~1002 hits each, BaseAddr=0x70157a98), and the log contains **zero** `D146: unknown GBI opcode` lines — the software RSP consumes every DL to completion. PC-side GDLs verified byte-faithful to the N64 source via the d43 toolchain (same custom GE opcode sequences, same vertex counts). **(2) Transform is fine.** New `GE_D75V="lo-hi"` probe in `gfx_sp_vertex` (per-batch transformed-vertex min/max + MP translation row; uses new `videoGetFrameCount()` accessor) shows the logo's vertices land exactly where the wordmark geometry says they should: x∈[−2500,+2500], y∈[±644], w≈3.9k–5k, MP translation (0,0,3878.7) matching `lookat(0,0,4000)` — this eliminates basematrix, the f32→s32 conversion loop, and the matrix-list suspects. **(3) Leading hypothesis: vertex-slot overwrite.** Every `G_VTX` batch in these GDLs encodes **v0=0** (fast3d decodes `n=C0(0,16)/sizeof(Vtx)`, `dest_index=C0(16,4)`; probe confirms di=0 for every batch), so a node whose vertices span multiple 16-vertex batches writes all of them to slots 0..n−1 of the shared buffer — each later batch **overwrites** the earlier ones and only the last batch survives. Triangles referencing indices beyond the last batch's count read stale slots from prior nodes (likely clipped, not visible as junk — consistent with the observed single ~25×6px orbiting fragment matching the smallest node's 4-vert quad at x≈1020 → ~155px orbit radius vs observed ~200px). The N64 RSP must handle multi-batch nodes differently (per-batch ring or relative indexing) — GE's own ucode is not in this repo, so the next step is empirical: dump one full node's GDL stream from the ROM and cross-reference its `G_TRI4`/`G_TRI1` index ranges against per-batch vertex counts. If confirmed, the fix is port-layer only (fast3d vertex-slot management) — no rule-2 sign-off needed. Probe registered in `GE-ENV-PROBES.md` as `GE_D75V`. + **D75 Bug 2 — RUNTIME PROBE (session M-32b, `GE_D75=1` in `title.c` `sub_GAME_7F007F30`, kept env-gated).** Booted the bare front end (`GE_D75=1 GE_PCDUMP="1-1200:120"`, no `-level`) and captured through the diff --git a/port/fast3d/gfx_pc.cpp b/port/fast3d/gfx_pc.cpp index f06fec94..892a8743 100644 --- a/port/fast3d/gfx_pc.cpp +++ b/port/fast3d/gfx_pc.cpp @@ -1442,7 +1442,10 @@ static inline bool fast3d_ptr_ok(const void *p) { return v >= 0x10000 && v < 0x0000800000000000ULL; } +extern "C" u32 videoGetFrameCount(void); /* port/src/video.c (D75 probe) */ + static void gfx_sp_vertex(size_t n_vertices, size_t dest_index, const Vtx* vertices) { + const size_t d75_di0 = dest_index; /* D75 probe: loop below mutates dest_index */ SUPPORT_CHECK(n_vertices <= MAX_VERTICES); if (!fast3d_ptr_ok(vertices) || dest_index + n_vertices > MAX_VERTICES) { @@ -1699,6 +1702,47 @@ static void gfx_sp_vertex(size_t n_vertices, size_t dest_index, const Vtx* verti d->color.a = vcn->a; // can be required for SHADE_ALPHA even if fog is enabled } + +#ifdef PORT + /* TEMP D75 (M-19x): per-batch transformed-vertex range probe. Static + * analysis of the nintendologo path exhausted itself clean (all 23 op=4 + * nodes visited every frame, GDLs byte-faithful to N64, every opcode + * handled, G_VTX counts sum exactly to nv, G_TRI4 decode matches the + * gSP4Triangles macro) -- so log what the pipeline actually computes: + * for frames in [lo,hi] (env GE_D75V="lo-hi"), one line per G_VTX batch + * with the resolved vertex ptr, raw first-vertex ob[], transformed + * x/y/z/w ranges and the MP translation row. The logo's batches are + * identifiable offline by vtx ptr == BaseAddr(0x70157a98)+file offset. + * Remove once D75 is root-caused. */ + { + static int d75v_lo = -1, d75v_hi = 0; + if (d75v_lo < 0) { + const char* v = getenv("GE_D75V"); + d75v_lo = 1; d75v_hi = 0x7fffffff; + if (!v || sscanf(v, "%d-%d", &d75v_lo, &d75v_hi) != 2) + d75v_lo = -1; + } + if (d75v_lo >= 0 && (int)videoGetFrameCount() >= d75v_lo && + (int)videoGetFrameCount() <= d75v_hi) { + float mnx = 1e30f, mxx = -1e30f, mny = 1e30f, mxy = -1e30f, + mnz = 1e30f, mxz = -1e30f, mnw = 1e30f, mxw = -1e30f; + for (size_t i = 0; i < n_vertices; i++) { + const struct LoadedVertex* d2 = &rsp.loaded_vertices[d75_di0 + i]; + if (d2->x < mnx) mnx = d2->x; if (d2->x > mxx) mxx = d2->x; + if (d2->y < mny) mny = d2->y; if (d2->y > mxy) mxy = d2->y; + if (d2->z < mnz) mnz = d2->z; if (d2->z > mxz) mxz = d2->z; + if (d2->w < mnw) mnw = d2->w; if (d2->w > mxw) mxw = d2->w; + } + const Vtx* v0 = &vertices[0]; + sysLogPrintf(LOG_NOTE, + "D75V: f=%u vtx=%p n=%zu di=%zu raw0=(%d,%d,%d) x[%.0f,%.0f] y[%.0f,%.0f] z[%.0f,%.0f] w[%.0f,%.0f] MPt=(%.1f,%.1f,%.1f)", + videoGetFrameCount(), (const void*)vertices, n_vertices, d75_di0, + (int)v0->v.ob[0], (int)v0->v.ob[1], (int)v0->v.ob[2], + mnx, mxx, mny, mxy, mnz, mxz, mnw, mxw, + rsp.MP_matrix[3][0], rsp.MP_matrix[3][1], rsp.MP_matrix[3][2]); + } + } +#endif } static void gfx_sp_modify_vertex(uint16_t vtx_idx, uint8_t where, uint32_t val) { diff --git a/port/include/video.h b/port/include/video.h index f49971e8..de57d5fa 100644 --- a/port/include/video.h +++ b/port/include/video.h @@ -40,6 +40,7 @@ void videoPumpEvents(void); /* The game's native video mode (NTSC 640x480, PAL 640x400). fast3d scales * N64 screen coordinates into window pixels using this. */ void videoUpdateNativeResolution(s32 w, s32 h); +u32 videoGetFrameCount(void); s32 videoGetNativeWidth(void); s32 videoGetNativeHeight(void); diff --git a/port/src/video.c b/port/src/video.c index b70fa827..85b2701d 100644 --- a/port/src/video.c +++ b/port/src/video.c @@ -732,6 +732,7 @@ void videoUpdateNativeResolution(s32 w, s32 h) gfx_current_native_aspect = (float)w / (float)h; } +u32 videoGetFrameCount(void) { return frames; } s32 videoGetNativeWidth(void) { return gfx_current_native_viewport.width; } s32 videoGetNativeHeight(void) { return gfx_current_native_viewport.height; }