acdream/docs/research/2026-07-06-176-seam-floor-zfight-handoff.md
Erik d8984e877f 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>
2026-07-06 15:12:31 +02:00

9.7 KiB
Raw Blame History

Pickup handoff — #176 seam-floor lighting flicker = a RUNTIME draw z-fight (RenderDoc next)

OUTCOME (2026-07-06, the pickup session) — z-fight REFUTED; it was the light POOL tracking the camera; FIX SHIPPED

RenderDoc was infeasible (it does not support GL_ARB_bindless_texture and hides it → our mandatory-modern startup gate throws NotSupportedException; the AMD RX 9070 XT rules out Nsight). The equivalent evidence came from the in-engine ACDREAM_PROBE_SEAMDRAW probe ([seam-blk]/[seam-cell]/[seam-snap]/[seam-ent]/ [seam-mask]): one shell instance per seam cell at the lifted z (suspect 1 dead), no floor-coincident entity (suspect-class dead), zero portal depth fans (suspect 3 dead) — but the corridor floor's applied LIGHT SET flipped wholesale with flood composition. Root cause: the c500912b visible-cell scoping was built on a wrong gloss — retail's CEnvCell::visible_cell_table is the RESIDENT-cell registry (add_visible_cell 0x0052de40 dat-loads absent cells), not the frame flood, and insert_light (0x0054d1b0) anchors the pool at Render::player_pos. Fix: BuildPointLightSnapshot(playerWorldPos) — resident collection, dynamics-first player-nearest cap; RebuildScopedLights deleted. Live-verified: flood churned 8→41 cells over a full-circle turn while the floor's set held the same 8 identities. Details: ISSUES #176, register AP-85, the correction banner in 2026-07-06-a7-per-cell-lighting-pseudocode.md. The document below is kept as the historical elimination record — its DO-NOT-RETRY table remains valid (all those channels stay dead).

Paste the companion prompt below into a fresh session. Read this file, then claude-memory/project_render_pipeline_digest.md (top banner), then ISSUES #176.

TL;DR — what #176 actually is now

The Facility Hub corridor floor shows hard-edged purple triangular facets that flicker as the camera turns, at cell seams. After a long investigation (11 hypotheses), the domain is pinned by evidence:

#176's residual is a RUNTIME draw-level depth-fight (z-fight) in the seam FLOOR geometry — two draws the renderer emits at ~the same depth, one carrying the purple portal light and one not, whose winner flips as the camera moves. It is NOT lighting, NOT membership, NOT dat geometry.

The wedge SHAPE itself is retail-faithful (retail shows the same faceted purple — cdb-proven, below). The only defect is the flicker, and it is a render/draw problem. The next step is a RenderDoc pixel-history capture — do NOT throw more lighting fixes at it (two already did nothing).

The elimination table (evidence — this is the DO-NOT-RETRY list)

Ruled OUT How (hard evidence)
Lighting brightness per-light cap on the dynamic branch (mesh_modern.vert) → no visual change, reverted
Lighting selection SelectForCell (all dynamic lights per cell, retail-exact) → no visual change
Light-set camera-cap churn (the OLD "confirmed" #176 theory) visible-cell scoping shipped (c500912b); probe-proven ~285 through-floor lights dropped/frame — symptom unchanged. REFUTED.
Membership / the "flap" ACDREAM_PROBE_FLAP: render cell = 0x8A020164 on 100% of 188,732 frames across 526 distinct camera angles, res=None always; ACDREAM_PROBE_PVINPUT flood is stable-per-angle (17/10/8…), never oscillates at a fixed view; 1 [cell-transit] (the spawn teleport) total
Dat-geometry z-fight Issue176177DungeonSeamInspectionTests.CorridorNeighborhood_CoplanarOverlappingDrawnPolyPairs seeded on the ACTUAL cell 0x8A020164 + neighbors → zero coplanar drawn pairs at the z=6 corridor floor (only the benign same-cell z=12 under-hall floor tiling in 0x011E)
A2C / alpha-hole see-through corridor floor surface 0x08000377 is fully opaque (alpha0Texels=0, transl=0.00)
Translucent under-surface blend the one translucent colored surface 0x08000034 is NoPos (not drawn)
Flat (per-face) normals corridor floor uses smooth per-vertex dat normals (center (0,0,1), corners tilted ~27° — retail-style edge smoothing), NOT flat (CellVertexNormals_SmoothOrFaceted_Dump)

What retail actually does (cdb-proven — the wedge is faithful)

tools/cdb/issue176-floor-light.cdb traced live retail's config_hardware_light standing in the Hub (binary↔PDB MATCH, GUID 9e847e2f…):

  • Retail applies 4 intensity-100 magenta portal lights (d3dIdx 36, color=(0.784,0,0.784), falloff=6) + the viewer fill (d3dIdx 1, intensity=2.25 falloff=10) as D3D hardware lights to EVERY Hub cell (0102,0103,0109,0111,0112,0115,0118,0119,011a,011e,011f,0121,0122,0123…), every frame, stable.
  • So retail's cell floors ARE lit by the portals via per-vertex hardware T&L over the same smoothed fan-corner normals → the same faceted purple wedges. → the wedge SHAPE is retail-faithful; only the flicker is ours.

The intensity-100 magenta light is the portal (2 portal weenies 0x000F4247/0x000F4248 in cells 0x8A020118/0x8A020119, dyn=1). Confirmed via the [light-detail] owner=/cell=/dyn= probe fields.

The residual bug — a runtime double-draw of the seam floor

Since it's not dat geometry, the renderer is emitting the corridor floor (or a coincident surface) twice at ~the same depth. Prime suspects to check in RenderDoc (do NOT assume — capture and read):

  1. The +0.02 m shell render lift (PortalVisibilityBuilder.ShellDrawLiftZ, register AP-32, the #130 saga). If the shell floor draws lifted while some other pass draws the same floor unlifted, 0.02 m z-fights at grazing angles.
  2. The A8 double-sided cell-shell stopgap (#178, LIVE): EnvCellRenderer.RenderModernMDIInternal if (cullMode == CullMode.Landblock) cullMode = CullMode.None; — every cell shell draws two-sided. Retiring this is already an open issue; test whether it's implicated.
  3. Portal depth-writesRetailPViewRenderer.DrawExitPortalMasks / the interior-root depth-clear+seal path. Seals are supposed to fire only for OtherCellId==0xFFFF (a sealed dungeon draws zero), but VERIFY in the capture that no depth-mask lands on the floor-portal plane (the corridor floor IS a floor-portal poly to the under-room, surf 0x08000377, z=6).

RenderDoc plan (the definitive next step)

  1. Launch acdream under RenderDoc (renderdoc → capture AcDream.App, or inject). Standard ACDREAM_LIVE env from CLAUDE.md.
  2. Stand in cell 0x8A020164 corridor, at an angle where the floor flickers. Capture a frame (F12).
  3. In RenderDoc: right-click a flickering floor pixel → "History" (pixel history). It lists every draw that touched that pixel + its depth + pass/fail.
  4. Two draws at ~equal depth writing that pixel = the z-fight. Identify which two passes they are (shell vs shell-lift vs portal-mask). Report the draw names + depths back — that names the fix.
  5. Fix at the source (make the two draws not coincide: drop the redundant draw, or reconcile the lift), then re-gate. No band-aid depth-bias (that's a workaround — see CLAUDE.md).

What's committed / the state

  • c500912b (in main): visible-cell light scopingLightSource.CellId
    • BuildPointLightSnapshot(camPos, visibleCells) (retail add_*_lights over visible_cell_table) + the [indoor-light] probe. Retail-faithful; fixed the camera-eviction latent bug. Register AP-85 corrected. Corrected the old handoff's imprecise "insert_light = per-cell" framing — the real model is visible-cell-scoped collection into a small player-nearest pool; see docs/research/2026-07-06-a7-per-cell-lighting-pseudocode.md.
  • This commit: LightManager.SelectForCell (all dynamic lights per EnvCell, retail minimize_envcell_lighting) wired into EnvCellRenderer.GetCellLightSet; the [light-detail] owner/cell/dyn probe; CellVertexNormals_* dump; tools/cdb/issue176-floor-light.cdb. Retail-faithful, NOT the flicker fix.
  • Suites green: Core 2599 + 2 skip. All on main.

Apparatus (reuse, don't rebuild)

  • ACDREAM_PROBE_FLAP=1 — per-frame [flap-cam] render cell + [flap] portal decisions (proved membership stable).
  • ACDREAM_PROBE_PVINPUT=1 — per-frame [pv-input] flood=N eye= yaw=.
  • ACDREAM_PROBE_LIGHT=1[light] + [light-detail] (now with owner/cell/dyn).
  • ACDREAM_PROBE_INDOOR_LIGHT=1[indoor-light] scoped-pool SET composition.
  • tools/cdb/issue176-floor-light.cdb — retail light-setup trace.
  • Issue176177DungeonSeamInspectionTests — dat truth (coplanar sweep, floor surfaces, vertex normals); CorridorNeighborhood_CoplanarOverlappingDrawnPolyPairs seed = {0164,0165,016E,017A}+neighbors.

Repro + launch protocol

dotnet build green FIRST (close any running client — a client may still be up; the user manages lifecycle, do NOT kill it, ASK). Standard ACDREAM_LIVE PowerShell launch. Test char spawns in the Facility Hub; the flickering corridor is cell 0x8A020164 (player z≈5.9). Turn back-and-forth across a seam to reproduce. Strip \000 before grep (PowerShell Tee = UTF-16).

Process lesson (paid for in full this session)

Get a screenshot AND a retail cdb trace EARLY for a render bug — before theorizing. This session chased 11 inference-based hypotheses over many launches; the screenshot (faceted triangles) + the cdb trace (retail applies the same lights → faithful shape) would each have redirected the whole thing in one step. The project's own rules already say this; follow them.

Also open

  • #177 — dungeon stairs pop in/out (0178/0182/0183) — a SEPARATE portal-visibility miss, not lighting. Untouched this arc.