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

154 lines
9.7 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)
> ## ✅ 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-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.