fix #176: light pool tracked the camera via flood scoping - collect residents, anchor at player

The seam-floor purple flicker was NOT a draw z-fight. The in-engine
[seam-*] probe (ACDREAM_PROBE_SEAMDRAW - built because RenderDoc cannot
capture this pipeline: it hides GL_ARB_bindless_texture and the
mandatory-modern startup gate throws; AMD GPU rules out Nsight) killed
every double-draw suspect: ONE shell instance per seam cell at the
lifted z, no floor-coincident entity (portal entities sit at z=-12.05),
zero portal depth fans in the sealed Hub. What it caught instead: the
corridor floor's applied light set flipping wholesale with the flood.

Root cause: c500912b scoped BuildPointLightSnapshot by the per-frame
portal flood, on the research doc's gloss of CEnvCell::visible_cell_table
as "the portal-flood visible set". The named decomp refutes the gloss:
add_visible_cell (0x0052de40) DBObj-LOADS absent cells and inserts them;
a cell activation adds itself + its whole dat visible-cell list
(0x0052e228/0x0052e24a); entries leave only via the flush machinery.
It is the RESIDENT-cell registry - gaze can never remove a cell.
add_dynamic_lights (0x0052d410) walks the WHOLE table per frame
(caller 0x00452d30), and insert_light (0x0054d1b0) caps the pool by
distance to Render::player_pos (0x0054d1dd). Retail's pool is a function
of player position only. Ours followed the camera: turning changed the
flood (probe: 8..41 cells across one turn), the six intensity-100
under-room portal purples entered/left the pool, and the wedge blinked.

Fix: BuildPointLightSnapshot(playerWorldPos) collects ALL registered
(=resident) lit lights; over cap keeps dynamics FIRST (retail's separate
7-slot dynamic pool never competes with statics) then nearest-the-player;
the RebuildScopedLights callback is deleted. Live-verified with the probe:
full-circle turn, flood churning 8..41, the floor set held the same 8
identities on every post-spawn frame. The purple wedge SHAPE stays - it
is cdb-proven retail-faithful.

Residual deviation (AP-85 rewritten): single 128 pool vs retail's
7-dynamic/40-static degrade-scaled dual pools - the Hub now shows
7 purples + viewer where retail's cdb showed 4 + viewer + fixture slots;
if the gate reads the wedge as too purple, the A7 dual-pool cap is the
faithful trim.

Pins: PointSnapshot_HubScaleLightCount_ObjectSelectionIsCameraInvariant
(rewritten to the corrected model),
PointSnapshot_OverCap_DynamicsNeverEvictedByNearerStatics,
PointSnapshot_OverCap_KeepsNearestThePlayer,
PointSnapshot_ResidentCollection_CellTagDoesNotFilter.
Suites: Core 2599+2skip / App 726+2skip / UI 425 / Net 385.

The [seam-*] probes stay until the visual gate passes, then strip.
Correction banner added to 2026-07-06-a7-per-cell-lighting-pseudocode.md;
outcome banner on the z-fight handoff; ISSUES #176 updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-06 15:12:31 +02:00
parent 8cb3176daa
commit d8984e877f
12 changed files with 525 additions and 194 deletions

View file

@ -149,21 +149,37 @@ the connecting opening, and stays rendered through the full descent.
## #176 — Purple flashing on dungeon floors at cell seams, camera-angle dependent
**⚠️ UPDATE 2026-07-06 (final — cdb-confirmed, RenderDoc handoff) — it's a RUNTIME draw
z-fight, NOT lighting.** Full elimination table + plan:
`docs/research/2026-07-06-176-seam-floor-zfight-handoff.md`. The purple WEDGE SHAPE is
retail-FAITHFUL — cdb (`tools/cdb/issue176-floor-light.cdb`) proved retail applies the same
4 intensity-100 magenta portal lights + the viewer fill to EVERY Hub cell via D3D hardware,
so the faceted per-vertex purple is exactly what retail shows. The only defect is the
FLICKER, eliminated as: NOT lighting (per-light cap + all-dynamic-per-cell both no-change),
NOT membership (render cell `0x8A020164` stable on 100% of 188k frames / 526 angles), NOT dat
geometry (coplanar sweep empty at the z=6 corridor floor, incl. cell `0164`). → a RUNTIME
double-draw of the seam floor. Suspects: the `+0.02 m` shell lift (AP-32/#130), the #178
double-sided cell-shell stopgap, a portal depth-mask. **NEXT: RenderDoc pixel-history** on a
flickering floor pixel to name the two fighting draws. Shipped this arc (retail-faithful, NOT
the fix): scoping `c500912b` + `SelectForCell` (all-dynamic-lights-per-cell).
**🟡 FIX SHIPPED 2026-07-06 (pending user visual gate) — the flicker was the LIGHT POOL
tracking the CAMERA, not a draw z-fight.** The z-fight framing was refuted by the in-engine
`[seam-*]` probe (RenderDoc is infeasible on this pipeline — it hides
`GL_ARB_bindless_texture` and our mandatory-modern startup gate throws; AMD GPU rules out
Nsight): exactly ONE shell instance per seam cell at the lifted z (5.98), NO
floor-coincident entity (portal entities sit at z=12.05, six meters down), ZERO portal
depth fans (sealed dungeon). What the probe DID catch: the corridor floor's applied light
set flipping wholesale with flood composition. Root cause (named-decomp-verified): the
`c500912b` scoping port glossed retail's `CEnvCell::visible_cell_table` as "the portal-flood
visible set" — it is the **RESIDENT-cell registry** (`add_visible_cell` 0x0052de40
dat-loads absent cells; populated from each activated cell + its dat visible-cell list;
gaze can never remove entries), and `Render::insert_light` (0x0054d1b0) caps the pool
nearest **`Render::player_pos`**, not the camera. Our flood-scoped pool dropped/admitted
the six intensity-100 under-room portal purples as the camera turn changed the flood
(probe: flood churned 8→41 cells over a full turn) → the wedge blinked. **Fix:**
`BuildPointLightSnapshot(playerWorldPos)` — resident collection (all registered lit
lights), dynamics-first player-nearest cap; the `RebuildScopedLights` callback deleted.
Verified live: full-circle turn sweep, flood churning 8→41, the floor's set held the SAME
8 identities on every post-spawn frame. Pins: `PointSnapshot_HubScale…CameraInvariant` (rewritten),
`PointSnapshot_OverCap_DynamicsNeverEvictedByNearerStatics`, `PointSnapshot_ResidentCollection_CellTagDoesNotFilter`.
Register AP-85 rewritten; correction banner in
`docs/research/2026-07-06-a7-per-cell-lighting-pseudocode.md`. Residual parity note for the
gate: our single 128 pool admits 7 purples + viewer (8 dynamics, all 8 slots) where retail's
7-dynamic/40-static dual pool showed 4 purples + viewer + fixture slots — if the wedge reads
"too purple," the A7-arc dual-pool cap is the faithful trim. `[seam-*]` probes stay until the
gate passes, then strip.
**Status:** OPEN
**Gate:** stand in the `0x8A020164` corridor, turn back-and-forth across the seam — the
purple wedge must hold steady (its faceted SHAPE is retail-correct and stays).
**Status:** 🟡 FIX SHIPPED — pending user gate
**Severity:** MEDIUM (visible artifact along every corridor seam in the M1.5 dungeon)
**Filed:** 2026-07-06
**Component:** render — floor-portal polygons / portal surface state