From fbc2a533ae7e504af3e81c63b9becc8375cd3e24 Mon Sep 17 00:00:00 2001 From: James Dansereau <106631954+jkdansereau@users.noreply.github.com> Date: Wed, 16 Sep 2026 16:13:33 -0400 Subject: [PATCH] D288 M-152: static investigation, add GE_D288 near-plane probe Read gfx_sp_tri1 against D233/D106/D271's existing guards. D233's trivial-reject fix is ruled out as a direct cause -- GL clips well-formed clip-space geometry correctly downstream of it. Found a new, previously uncatalogued unguarded site: the G_CULL_BOTH backface-cull cross-product divides raw vertex w with no epsilon guard, which can NaN/Inf near w=0 and silently skip both cull branches. Not yet confirmed against a live repro. Added GE_D288 (cached-getenv, D250-safe) to catch the culprit triangle's w/bbox at draw time on the next -level_20 run. No game-logic change; port/fast3d/ diagnostic only, inert unless GE_D288 is set. --- docs/dev/GE-ENV-PROBES.md | 1 + docs/dev/findings.md | 4 +++- docs/porting-notes.md | 9 +++++++++ port/fast3d/gfx_pc.cpp | 22 ++++++++++++++++++++++ 4 files changed, 35 insertions(+), 1 deletion(-) diff --git a/docs/dev/GE-ENV-PROBES.md b/docs/dev/GE-ENV-PROBES.md index 880ed6af..6e7d806d 100644 --- a/docs/dev/GE-ENV-PROBES.md +++ b/docs/dev/GE-ENV-PROBES.md @@ -55,6 +55,7 @@ Two classes: | `GE_DTEX` | `port/fast3d/gfx_pc.cpp:1001` | Per-`import_texture` param log (dims / line / size / lod / gen_mipmaps). Used to root-cause RC2 and D159. Kept as an inert diagnostic. | **live** (RC2/RC3 not finished) | | `GE_TEXDUMP` | `port/fast3d/gfx_pc.cpp:1020`, `port/fast3d/gfx_opengl.cpp:700` | PPM dump of every uploaded texture + a `fmt/siz/palfmt/palidx/pal[0..3]` line. Root-caused D161 (Depot ceiling). Kept as an inert diagnostic. | **live** (RC3 not finished) | | `GE_D116` | `port/fast3d/gfx_pc.cpp:1710,2362`, `src/game/textrelated.c:265,541` (cached `ge_d116`) | HUD/text "X-mirror" (D114/D116) vbo/uv trace. **D114/D116 CLOSED — NOT A BUG (M-33/D168): the captures were upside-down, not mirrored.** Probe is now dead scaffolding — strip on the next cleanup pass. | dead (D114/D116 closed) | +| `GE_D288` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_tri1`, right after the D233 trivial-reject guard; cached `ge_d288`) | M-152 diag for the Silo intro stray triangle: logs any triangle whose clip-space NDC bbox covers >15% of the screen or goes non-finite, with each vertex's `w` and `any_behind_camera`. **Investigation in progress** — narrowed to two candidates (an unguarded `w`-division NaN leak in the backface-cull test just below, or the D106/D271 portal-BFS family), not yet root-caused; needs a live/headless `-level_20` run to disambiguate. | **live** (D288) | ## Dxx diagnostic probes — finding closed unless noted diff --git a/docs/dev/findings.md b/docs/dev/findings.md index 4b1f0bc0..28cb36e0 100644 --- a/docs/dev/findings.md +++ b/docs/dev/findings.md @@ -10875,7 +10875,9 @@ crash class C2 (Facility `-level_34`, Runway `-level_35`): `import_texture_i8` A **Silo intro: a huge, flat-shaded, untextured brown/tan triangle sweeps across the frame during the camera pan down the missile shaft (M-141, user report — "weird triangle drawn on silo intro randomly when the camera pans around the rocket"; previously catalogued as `GRAPHICS-BACKLOG.md` D171, "observed, not investigated" since M-34, never given a full findings.md entry until now).** Reproduced headlessly this session: `GE_PCDUMP=1-2000:10 ./build-pc/ge007.x86_64.exe -level_20` (run backgrounded, not foregrounded — see the environment note below), captured to `./ppm/`. Visual sweep (contact-sheet montages, `docs/img/bugs/D288-silo-stray-triangle-sheet.png` covers frames 571-751; `-closeup.png` is a full-res crop of frame 691) shows: **clean at frames 481-561** (no triangle), **first appears between frame ~562-601**, grows continuously in screen size through frame ~711 (by which point it covers roughly a third of the frame), then **shrinks and is gone by ~731-741** as the scene cuts to the first-person shaft-corridor view. Sampled pixel color is **exactly uniform** across the whole triangle (`RGB(83,62,44)` / `#533E2C` at two different sample points) — no gradient, no texture detail — i.e. this is a flat-shaded (no per-vertex color variance, no texture bound) single triangle, not garbage/noisy vertex-color data. **Reading of the evidence:** the grow-then-shrink-then-vanish arc across ~15 real seconds of camera motion is the classic signature of a genuine 3D triangle in world space that the camera flies close to/through during the scripted pan — not a 2D HUD/UI element (which would stay roughly fixed in screen size) and not obviously a `D146`-class freed-memory read (which reads as noisy garbage bytes, not a clean uniform flat color). The flat uniform color argues this is a REAL, correctly-colored asset triangle whose position/transform is wrong, which points at the same neighborhood as D106/D233 (near-plane / w-clip mishandling in `port/fast3d/gfx_pc.cpp`) rather than a game-code or converter data bug — but this is a read of the visual evidence, not a code-level confirmation; no probe has been run yet. **Not yet identified:** which model/prop this triangle belongs to (nothing in `UsetupsiloZ.c` was grepped for a matching solid-brown material this session), and no `port/fast3d/` tracing was done to catch the actual triangle at draw time. **Environment note (new, useful beyond this bug):** launching `ge007.x86_64.exe` in the **foreground** under this session's Bash tool with `timeout N ...` produced **zero** `ppm/` output despite `GE_PCDUMP` being set and verified exported — the `ppm/` directory was never even created (`GE_MKDIR` never ran or `frames`-gated capture never fired). Backgrounding the same invocation (`... &`, `sleep N`, `kill`) worked immediately and reliably. Not root-caused (didn't block this investigation), but worth remembering for the next session that needs `GE_PCDUMP` from this same tool. **Next, concrete:** (1) grep `assets/obseg/setup/UsetupsiloZ.c` / the missile-shaft model's material table for a solid `RGB(83,62,44)`-ish prim/vertex color to identify the source asset; (2) add a temporary `#ifdef PORT` probe in `gfx_sp_tri1` logging any triangle whose post-transform screen-space bounding box exceeds some fraction of the viewport together with each vertex's `w`, gated to `-level_20` frames ~560-740, to catch the culprit triangle's `w` sign/magnitude at the moment it blows up — same style as the existing `GE_D271`/`GE_D104` probes; (3) cross-check against D233's `w<0` trivial-reject fix and D106/D271's near-plane portal-bound clamp to see if this is a third site with the same underlying class of bug, or a genuinely new mechanism. -**Status (from §F table):** OPEN — visually confirmed and precisely bounded (frames ~562-741 of a bare `-level_20` boot), root cause not yet identified; promoted from the long-parked `GRAPHICS-BACKLOG.md` D171 entry with real evidence. +**M-152 (2026-09-16, static-only pass, no game execution this session — user was mid-playtest):** read `gfx_sp_tri1` (`port/fast3d/gfx_pc.cpp:1586-1651`) against D233/D106/D271/D234 to see what's already guarded. **D233's trivial-reject fix is ruled out as the direct cause** — it only ever adds triangles GL's own hardware clipper would legitimately draw from well-formed clip-space geometry (`gfx_opengl.cpp:320` hands GL raw `x,y,z,w` and lets GL clip natively), so a well-formed w<0/straddling triangle can't come out screen-filling from that guard alone. **New unguarded site found, not previously catalogued:** the backface-cull test right below it (`gfx_pc.cpp:1618-1629`) divides raw vertex `w` with no epsilon guard when computing the cull cross-product; a vertex at/near `w≈0` during a near-plane crossing can NaN/Inf the cross product, which silently fails both `<=0`/`>=0` cull branches and lets a triangle through that should have been backface-culled. Asset search (`UsetupsiloZ.c`, `bg_silo_all_p.c` — no `SetPrimColor`/`SetEnvColor`/`SetFillColor` at all, so the flat color must be vertex-RGBA or lighting-derived, not a literal stored triple; a byte-triple grep for `0x53,0x3e,0x2c` ±3 fuzzy across all of `assets/` found zero matches) did not identify the source model — GE's binary-included model blobs aren't literal hex in the `.c` sources, so this needs a runtime probe, not more grepping. **Confidence: LOW-MEDIUM, still not root-caused.** Two live candidates: (1) the new backface-cull NaN/Inf leak above, (2) the D106/D271 portal/room-visibility BFS family in `bg.c` (less likely given the smooth 150-frame grow/shrink arc, which doesn't match that family's usual abrupt full-screen-clamp signature, but not traced through the silo intro's specific camera/portal path so not ruled out). A `GE_D288=1` env-gated probe (cached-getenv, D250-safe) is now in tree at `gfx_pc.cpp` right after the D233 guard (~line 1616): logs any triangle whose clip-space NDC bbox covers >15% of the screen or goes non-finite, together with each vertex's `w` and `any_behind_camera`. **Next:** `GE_D288=1 GE_PCDUMP=560-745:5 ./build-pc/ge007.x86_64.exe -level_20 2> scratch/d288_probe.log` (background it, not foreground — see the environment note above), grep around frames 562-741 for `color≈(83,62,44)`. `any_behind=1` with one `w` very close to (not deeply past) 0 → candidate 1; `any_behind=0` with comfortably-positive `w`s but still a large bbox → a genuine asset/transform bug outside fast3d entirely, rules out both candidates above. + +**Status (from §F table):** OPEN — visually confirmed and precisely bounded (frames ~562-741 of a bare `-level_20` boot); M-152 narrowed to two live candidates (fast3d backface-cull NaN leak, or the D106/D271 portal-BFS family) via static review, root cause still not identified; a `GE_D288` probe is in tree, needs a live/headless run to disambiguate. Promoted from the long-parked `GRAPHICS-BACKLOG.md` D171 entry with real evidence. ## D289 — Dam: a crash in the computer room, not reproducible on a second attempt (M-141, Steam Deck user playtest). diff --git a/docs/porting-notes.md b/docs/porting-notes.md index 66bcb0e4..03a59a40 100644 --- a/docs/porting-notes.md +++ b/docs/porting-notes.md @@ -680,6 +680,15 @@ through a converter or a runtime bswap fixup reads scrambled. backface culling already has, or it'll silently eat near-camera geometry exactly where a port is most likely to get scrutinized (doorways, tight corridors). +- `gfx_sp_tri1`'s `G_CULL_BOTH` backface-cull cross-product (`gfx_pc.cpp`, + right below the D233 trivial-reject guard) divides raw vertex `w` with + no epsilon guard. A vertex at/near `w≈0` during a near-plane crossing + can NaN/Inf the cross product, which silently fails both the + `cross<=0` and `cross>=0` cull branches and lets a triangle draw that + should have been backface-culled — a candidate for D288 (Silo intro + stray triangle), not yet confirmed against a live repro (M-152). Unlike + D233's fix, this one hasn't been shown to actually fire yet; it's a + code-review lead, not a confirmed mechanism. - **D74 wrap-block is DEAD CODE** (`gfx_pc.cpp:1546/1551`): guard is `cms & G_TX_WRAP` but `G_TX_WRAP == 0`, so always false (line 1887 does it right with `cms == G_TX_WRAP`). And if it did run it indexes diff --git a/port/fast3d/gfx_pc.cpp b/port/fast3d/gfx_pc.cpp index 2d31d6cc..68dbd920 100644 --- a/port/fast3d/gfx_pc.cpp +++ b/port/fast3d/gfx_pc.cpp @@ -1613,6 +1613,28 @@ static void gfx_sp_tri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx, bo // The whole triangle lies outside the visible area return; } + + /* D288 diag (M-152): the silo intro's screen-filling stray triangle + * (bounded to level_20 frames ~562-741) isn't explained by this + * function's existing D233/D106 guards on static review -- log any + * triangle whose clip-space NDC bbox covers an unreasonable chunk + * of the screen, or whose bbox math goes non-finite, together with + * each vertex's w and any_behind_camera, to catch it at draw time. + * Remove once D288 is root-caused. */ + static int ge_d288 = -1; + if (ge_d288 < 0) ge_d288 = getenv("GE_D288") != NULL; + if (ge_d288) { + float minx = std::fmin(std::fmin(v1->x / v1->w, v2->x / v2->w), v3->x / v3->w); + float maxx = std::fmax(std::fmax(v1->x / v1->w, v2->x / v2->w), v3->x / v3->w); + float miny = std::fmin(std::fmin(v1->y / v1->w, v2->y / v2->w), v3->y / v3->w); + float maxy = std::fmax(std::fmax(v1->y / v1->w, v2->y / v2->w), v3->y / v3->w); + float bbox_frac = (maxx - minx) * (maxy - miny) / 4.0f; // NDC quad is [-1,1]^2 + if (!std::isfinite(bbox_frac) || bbox_frac > 0.15f) { + fprintf(stderr, "D288: tri w=(%.6f,%.6f,%.6f) bbox_frac=%.3f any_behind=%d color=(%d,%d,%d)\n", + v1->w, v2->w, v3->w, bbox_frac, (int)any_behind_camera, + v1->color.r, v1->color.g, v1->color.b); + } + } } if ((rsp.geometry_mode & G_CULL_BOTH) != 0) {