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

@ -1,5 +1,40 @@
# A7 dungeon lighting — retail per-cell light model (source-confirmed pseudocode)
> ## ⚠️ CORRECTION #2 (2026-07-06, later the same day — the #176 flicker root cause)
>
> **§1.3's gloss is WRONG: `CEnvCell::visible_cell_table` is NOT "the portal-flood
> visible set".** It is a **static class member** — `HashTable<uint, CEnvCell*>
> CEnvCell::visible_cell_table` (defined 0x0081dd28, ctor 0x006fea4a) — the
> **RESIDENT-EnvCell registry**. Proof from the named decomp:
> - `CEnvCell::add_visible_cell(id)` (0x0052de40) returns the cell if present,
> otherwise **loads it from the dat** (`DBObj::Get(QDID(id, 3))`) and inserts it.
> Entries leave only via the flush machinery (`cell_flush_table` /
> `flush_cells` 0x0052def0) — **camera gaze can never remove a cell**.
> - It is populated at cell activation: a cell adds **itself + its entire dat
> visible-cell list** (`add_visible_cell(this->m_DID.id)` 0x0052e228 +
> `add_visible_cell(this->stab_list[i])` loop 0x0052e24a).
> - `CEnvCell::add_dynamic_lights` (0x0052d410) walks the **whole table**
> (`begin(&visible_cell_table)`), per frame, from 0x00452d30 (SmartBox).
> - `Render::insert_light` (0x0054d1b0) sorts the pool by distance to
> **`Render::player_pos`** (0x0054d1dd0x0054d214; set from
> `player->m_position`, SmartBox 0x00453d3a, viewer fallback 0x00455ab6).
>
> **So retail's pool = all RESIDENT cells' lights, capped nearest-the-PLAYER —
> a function of player position only, camera-invariant.** The §3.1 "slice 1"
> shipped below (flood-scoped `BuildPointLightSnapshot(camPos, visibleCells)`,
> `c500912b`) implemented the wrong gloss and WAS the #176 seam-floor blink: the
> under-room portal purples entered/left the pool as the camera turned (probe
> `[seam-blk]`: flood churned 8→41 cells across a full turn). Fixed same day —
> the scoping + the `RebuildScopedLights` callback are deleted;
> `BuildPointLightSnapshot(playerWorldPos)` collects every registered lit light
> (registration lifecycle = residency) with dynamics-first player-nearest
> capping. §2's "Retail never has 366 candidates" and the through-floor
> paragraph are likewise wrong: retail's Hub pool DOES contain the under-room
> purples (the cdb capture showed them applied to every Hub cell — the wedge is
> faithful). Register row AP-85 rewritten. Lesson (feedback_bn_decomp_field_names
> class): a decomp identifier's NAME is not its semantics — verify what
> POPULATES a structure before adopting its name as its meaning.
**Date:** 2026-07-06 (continuation of the #176/#177 arc)
**Purpose:** the mandated `grep named → decompile → pseudocode → port` step 3 for
the A7 per-cell lighting fix. Captures the RETAIL light-selection model exactly as