fix(D233): fast3d trivial-reject drops visible triangles with a behind-camera vertex (#63)

gfx_sp_tri1's CPU-side trivial-reject AND-test trusted per-vertex outcodes
(x<-w / x>w / y<-w / y>w / z>w) that are only valid half-space tests when
w>0. A vertex that has crossed behind the camera plane (w<0 -- common when
the camera is close to a large room-structure polygon, e.g. walls/ceiling
trusses near a doorway) flips the comparison sense, so its outcode can come
out wrong. When that spurious bit happens to match the other two genuinely
correct vertices' bits, the AND-reduction fired and dropped the whole
triangle before it ever reached OpenGL -- even though GL's own homogeneous
clipper (which handles w<0 correctly) would have rendered the visible part
of it.

This reads exactly like D233 (indoor geometry intermittently vanishing near
doors while props/furniture keep drawing): large near-camera polygons are
far more likely to have a vertex cross behind the eye than small
self-contained prop meshes, and the backface-cull code six lines below
already special-cases this same w-sign hazard for its own cross-product
sign -- the trivial-reject block above it never got the same treatment.

Fix: skip the trivial-reject AND-test whenever any of the triangle's three
vertices has w<0, deferring to GL's clipper instead. This can only ever
send MORE triangles to the GPU than before, never fewer, so it cannot make
anything newly invisible.

Also investigated D234 (Dam guard towers, only the first renders) under
the same M-106 hypothesis that it shares a root cause with D233/D222 --
did not find fast3d-side evidence for it; left open in findings.md with
the ruled-out mechanisms and a next-step recommendation. D222 (FovScale
breaks culling) is a separate, already-characterised game-logic bug in
src/game/fr.c/bondview2.c, out of this fast3d-only fix's scope.

Build verified green (MSYS2 mingw64 cmake+ninja, full link succeeds).
Could not visually verify -- this worktree has no ROM/data and no
interactive display; see docs/dev/findings.md D233 entry for the exact
human verification steps (GE_PCDUMP on a doorway-heavy interior).

docs/dev/findings.md: D233 (FIXED, high confidence on mechanism/unverified
visually) + D234 (OPEN, not root-caused) entries added.
docs/porting-notes.md: generalised the w<0 trivial-reject hazard as a
recurring class for any future CPU-side "skip this triangle" heuristic.
docs/dev/findings-index.csv regenerated.

Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG
This commit is contained in:
James Dansereau authored and GitHub committed 2026-09-12 10:11:51 -04:00
1 parent 067c8384d0
commit 650ac57fc6
4 files changed
+42 -3

No files matched your search

+18
View File
@@ -592,6 +592,24 @@ through a converter or a runtime bswap fixup reads scrambled.
- Portal near-plane: z==0 clip points project to ±1e20; x86-64 float
garbage can come back `min>max` / non-finite and slip past a
degenerate-box check that clamps to full-screen on N64 (D106).
- fast3d CPU-side "trivial reject" (`v1->clip_rej & v2->clip_rej &
v3->clip_rej` in `gfx_sp_tri1`) trusts per-vertex outcodes (`x<-w`,
`x>w`, etc.) that are only valid half-space tests when `w>0`. A vertex
behind the camera (`w<0`) flips the comparison sense, so its outcode can
be wrong; if that spurious bit happens to match the other two (correct)
vertices', the AND-reduction drops a triangle GL's own clipper would
have rendered correctly. Bites large near-camera polygons (room
walls/ceilings close to a doorway) far more than small prop models —
reads as "background geometry flickers away, props keep drawing" (D233).
The backface-cull code right below already special-cases this same
w-sign hazard for its cross-product sign; the trivial-reject block
didn't. Fix pattern: skip the AND-test (don't trivially reject) whenever
any vertex has `w<0` — safe, since it only ever adds triangles, never
drops more. General lesson: any CPU-side "skip this triangle" heuristic
in fast3d needs the same w<0-behind-camera exemption the existing
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).
- **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