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

@ -146,11 +146,10 @@ public sealed class RetailPViewRenderer
prepareCells = _lookInPrepareScratch;
}
// A7 #176/#177: scope this frame's point-light pool to the cells actually being
// drawn, NOW that the flood has resolved the visible set (retail collects lights
// per-frame over visible_cell_table). Must run before the cell/entity draws below
// that select from LightManager.PointSnapshot.
ctx.RebuildScopedLights?.Invoke(prepareCells);
// (#176 correction, 2026-07-06: the flood-scoped light-pool rebuild that ran
// here was the seam-floor flicker mechanism — retail's visible_cell_table is
// the RESIDENT-cell registry, not the frame flood — and is deleted. The pool
// is built once per frame in GameWindow, player-anchored.)
_envCells.PrepareRenderBatches(
ctx.ViewProjection,
@ -1108,16 +1107,6 @@ public sealed class RetailPViewDrawContext : IRetailPViewCellDrawContext
public Action? DrawUnattachedSceneParticles { get; init; }
public Action<IReadOnlyList<WorldEntity>>? DrawDynamicsParticles { get; init; }
public Action<RetailPViewFrameResult>? EmitDiagnostics { get; init; }
/// <summary>A7 #176/#177: rebuild the point-light snapshot scoped to the cells
/// this frame actually draws — invoked AFTER the portal flood resolves the visible
/// set and BEFORE any cell/entity draw (the faithful port of retail's per-frame
/// light collection: <c>CObjCell::add_*_to_global_lights</c> walked over
/// <c>CEnvCell::visible_cell_table</c>). The argument is every cell drawn this frame
/// (main flood + interior-root look-ins). A cell-less light (viewer fill) is kept
/// regardless. Null-safe: outdoor/no-flood callers leave it unset and keep the
/// legacy full-pool snapshot.</summary>
public Action<IReadOnlySet<uint>>? RebuildScopedLights { get; init; }
}
public sealed class RetailPViewFrameResult