acdream/docs/research/2026-07-06-176-seam-floor-zfight-handoff.md
Erik 8cb3176daa feat(lighting): SelectForCell (all dynamic lights per EnvCell) + #176 handoff — residual is a runtime z-fight
cdb trace of LIVE retail (tools/cdb/issue176-floor-light.cdb, binary<->PDB MATCH) PROVED retail
applies ALL its dynamic lights — 4 intensity-100 magenta portal lights (d3dIdx 3-6, falloff 6) +
the viewer fill (d3dIdx 1, 2.25) — as D3D hardware lights to EVERY Facility Hub cell, every frame,
stable. So the faceted purple wedges on the floor are retail-FAITHFUL. acdream did a per-cell
SelectForObject sphere-overlap 8-cap for cells, so the portal set could differ/flip per cell.

- LightManager.SelectForCell (retail minimize_envcell_lighting 0x0054c170): ALL dynamic lights
  applied unconditionally (shader range cutoff zeroes non-reaching = D3D hardware range), then
  nearest static torches fill remaining slots. Wired into EnvCellRenderer.GetCellLightSet.
  Objects keep SelectForObject (minimize_object_lighting). Pins:
  SelectForCell_AppliesAllDynamicLights_EvenOutOfReach + _SameDynamicSet_ForCellsFarApart_NoFlap.
- Apparatus: [light-detail] gains owner/cell/dyn (pinned the culprit = 2 portal weenies
  0x000F4247/48 in 0x8A020118/19, intensity=100 magenta); CellVertexNormals_SmoothOrFaceted_Dump
  (corridor floor uses SMOOTH per-vertex dat normals, not flat); tools/cdb/issue176-floor-light.cdb.

#176 RESIDUAL is NOT this fix. It's a RUNTIME draw z-fight in the seam floor. Eliminated (evidence):
NOT lighting (per-light cap + this both no-change), NOT membership (render cell 0x8A020164 stable
100% of 188k frames / 526 angles, res=None), NOT dat geometry (coplanar sweep empty at z=-6 floor
incl. cell 0164). NEXT = RenderDoc pixel-history. Full handoff + DO-NOT-RETRY:
docs/research/2026-07-06-176-seam-floor-zfight-handoff.md. Suites green: Core 2599 + 2 skip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 13:55:16 +02:00

8.2 KiB
Raw Blame History

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

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.