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>
154 lines
9.7 KiB
Markdown
154 lines
9.7 KiB
Markdown
# 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 3–6`,
|
||
`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.
|