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

133 lines
8.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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-writes**`RetailPViewRenderer.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 **scoping**`LightSource.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.