From e1746ca10d60cbc656fb5bada684298cfe906b9a Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 5 Jul 2026 23:16:17 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20#176/#177=20handoff=20=E2=80=94=20root-?= =?UTF-8?q?caused,=20folded=20into=20A7=20dungeon=20lighting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Session handoff for the next pickup. #176 (purple seam flash) + #177 (stair-room light pop-in) are ONE bug, root-caused to the camera-nearest MaxGlobalLights=128 snapshot cap evicting in-range lights of visible cells (Hub = 366 fixtures). Fix deferred to A7 because uncapping exposes unported per-cell light-reach (through-floor light) + the fixture falloff-curve misassignment + an unattributed striped-floor artifact. - New handoff: docs/research/2026-07-06-176-177-handoff-A7-lighting.md (root cause, why-deferred, the A7 fix order, tooling inventory). - Roadmap A7 updated: now owns #176/#177; the 'light visibility culling' hypothesis layer is CONFIRMED (no per-cell insert_light registration); fixture-curve layer added. Analysis pre-paid. Co-Authored-By: Claude Fable 5 --- docs/plans/2026-04-11-roadmap.md | 20 ++- .../2026-07-06-176-177-handoff-A7-lighting.md | 152 ++++++++++++++++++ 2 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 docs/research/2026-07-06-176-177-handoff-A7-lighting.md diff --git a/docs/plans/2026-04-11-roadmap.md b/docs/plans/2026-04-11-roadmap.md index fcc681de..e427c217 100644 --- a/docs/plans/2026-04-11-roadmap.md +++ b/docs/plans/2026-04-11-roadmap.md @@ -293,14 +293,32 @@ successfully 2026-04-30 for the steep-roof case. Matching binaries #### Phase A7 — Indoor lighting fidelity (RenderDoc + retail-decomp driven) +**Now also owns #176/#177 (2026-07-06):** the Facility Hub purple seam +flash + stair-room light pop-in are ROOT-CAUSED to this phase's "light +visibility culling" layer — a camera-nearest `MaxGlobalLights=128` +snapshot cap evicts in-range lights of visible cells (Hub has 366 +fixtures), so per-cell 8-light sets churn as the camera moves. +Uncapping was live-tested and reverted because the full pool exposes the +per-cell-reach + fixture-curve defects below (through-floor light, +1/d-vs-1/d³). Analysis PRE-PAID — see +`docs/research/2026-07-06-176-177-handoff-A7-lighting.md` (the fix order: +per-cell `insert_light` registration → static fixture curve → stripe +hunt → uncap) + register AP-85. + **Hypothesis layers (less mapped than physics):** - Per-cell environment-light tag association — indoor cells should inherit only their own env lights, not outdoor day-cycle. - Light visibility culling — what lights actually contribute to each - cell's render. + cell's render. **CONFIRMED bug here (#176/#177): no per-cell light + registration — lights are a flat world-space sphere-overlap pool that + reaches through solid floors; retail's `insert_light` 0x0054d1b0 + scopes each light to its cell.** - Per-entity light direction transform — held-item-spotlight bug (#L-spotlight) is per-entity attribution gone wrong. - Static-stab atmospheric inheritance (#81). +- **Fixture falloff curve (#176/#177): stationary server-spawned + fixtures ride the DYNAMIC 1/d path (#143 `isDynamic`); should be the + static 1/d³ bake (`calc_point_light` 0x0059c8b0).** **Investigation methodology:** less existing infrastructure than physics. Requires: diff --git a/docs/research/2026-07-06-176-177-handoff-A7-lighting.md b/docs/research/2026-07-06-176-177-handoff-A7-lighting.md new file mode 100644 index 00000000..54dceac5 --- /dev/null +++ b/docs/research/2026-07-06-176-177-handoff-A7-lighting.md @@ -0,0 +1,152 @@ +# Pickup prompt — #176/#177 root-caused, deferred to A7 dungeon lighting (paste into a fresh session) + +**Read `claude-memory/project_render_pipeline_digest.md` FIRST** (top banner +is the #176/#177 outcome + DO-NOT-RETRY), then **ISSUES #176 and #177**, then +this file. Then read **roadmap Phase A7** (`docs/plans/2026-04-11-roadmap.md` +§"Phase A7 — Indoor lighting fidelity") — this session effectively pre-paid +A7's analysis for the light-cap slice. + +## Where we are (2026-07-06 end of session) + +**Currently working toward: M1.5 — Indoor world feels right.** Critical path: +#137 dungeon collision (DONE, gated), #138 teleport-OUT, **A7 dungeon lighting +(#79/#93 + now #176/#177)**. + +HEAD = `d591e3bb` on `main` AND branch `claude/vigorous-joliot-f0c3ad` +(fast-forwarded, in sync). Working tree clean. All suites green +(Core 2591 + 3 skip / App 719 + 2 skip / UI 425 / Net 385). + +Three commits this session: + +| Commit | What | +|---|---| +| `b8e9e204` | #176/#177 investigation: 12 mechanisms refuted, apparatus shipped, probe protocol staged | +| `4d25e04d` | fix attempt: `MaxGlobalLights` 128→1024 (stops the pops) — **REVERTED** | +| `d591e3bb` | revert to 128 + full deferral docs (register AP-85 rewritten, ISSUES back to OPEN, digest DO-NOT-RETRY) | + +A client may still be running (`launch-176-revert-check.log`) — the user +manages lifecycle. It is on the reverted 128 baseline (rooms normal, seam +flashes present = the still-open issue). + +## THE ROOT CAUSE (confirmed, not a hypothesis) + +**#176 and #177 are ONE bug: per-cell 8-light SET COMPOSITION churning under a +camera-nearest snapshot cap.** `LightManager.BuildPointLightSnapshot` keeps only +the `MaxGlobalLights=128` point lights nearest THE CAMERA; the Facility Hub +registers **366** fixtures, so 238 are evicted per frame by camera distance. +`SelectForObject` (the faithful per-object 8-cap, retail +`minimize_object_lighting` 0x0054d480) can only choose from the surviving 128 — +so an in-range torch of a VISIBLE cell that ranks past the cap drops out of that +cell's 8-set, and the cell's per-vertex Gouraud lighting flips as the camera +moves. + +- **#176** — the flipping unit is a CELL → discontinuities at exactly cell-seam + granularity; camera-angle dependent (the chase boom swings the camera position, + re-ranking the 128); the dominant flipping light is the under-room PORTALS' + purple → purple flashes on the floor at seams. +- **#177** — a stair room whose fixtures ALL rank past the cap renders at bare + 0.2 ambient (near-black = "not visible from the corridor"); approach + re-admits them ("pops into existence"); the eviction boundary sweeping during + the descent strips the ramp's lights ("disappears on the last step"). **The + geometry never vanished — its LIGHTS did.** + +**How it was confirmed (the discriminator):** the user reproduced the flash +while `[light]` (ambient branch — stable 0.2 grey) AND `[pv-input]` (portal +flood — zero drops in 54k frames) read provably healthy in the probe log. That +eliminated every CPU signal the probes COULD see and left the one they can't: +set composition (`[light]` prints counts, not membership). The log's headline +number — `registeredLights=366` vs cap 128 — closed it. + +## WHY THE FIX IS DEFERRED (do not re-raise the cap alone) + +Uncapping (128→1024, `4d25e04d`) stopped the pops but the full 366-fixture pool +exposed three UNPORTED retail lighting semantics that then dominated the Hub — +this is why it was reverted (user: "rooms have no textures" → actually a magenta +light wash over intact textures; then "purple stripes… something fighting to +draw the purple lightning over the floor"): + +1. **Light-through-solid-floors.** Retail registers lights per-CELL + (`insert_light` 0x0054d1b0); a light belongs to a cell and only lights that + cell's geometry. Our snapshot is a flat world-space sphere-overlap with NO + reach/occlusion notion → the under-room portals' purple light washed the + corridors ABOVE them. **This is the big one.** The 128 cap accidentally + MASKED it by keeping the pool camera-local (far under-room lights fell off + the list before they could reach up). +2. **Fixture falloff curve misassignment.** Stationary weenie fixtures + (ACE serves dungeon lanterns/braziers as CreateObject weenies) register via + the `isDynamic:true` path → D3D 1/d falloff (`LightInfoLoader.cs:89`, + GameWindow weenie-light block ~3688). Retail bakes STATIONARY fixture light + with the static 1/d³ curve (`calc_point_light` 0x0059c8b0, static_light_factor + 1.3). 1/d is ~9× stronger at 3 m → every pool over-broad + over-saturated. + The `isDynamic` flag should be reserved for genuinely MOVING lights (portal + swirls, projectiles); a stationary fixture — even server-spawned — is static. +3. **Striped floor z-fight-like artifact.** User's 2nd screenshot: regular + magenta bands across one floor region, "like something is fighting to draw + the purple over the floor." **NOT attributed.** Ruled out: not coincident dat + geometry (the `CorridorNeighborhood_CoplanarOverlappingDrawnPolyPairs` sweep + found only the legit z=−12 under-hall floor quad-fan, nothing near the −6 + corridor floor); not a striped texture (all corridor surfaces are plain + `Base1Image` stone 0x08000375/6/7/8). Leading guess: two draws of the same + floor with DIFFERENT light sets (the per-cell-vs-per-something set assignment + splitting), or an MDI instance-order/light-set-index desync exposed only when + the purple light is stably present. **Hunt this in A7 with the full pool + temporarily on** — it's invisible at cap 128. + +## THE A7 FIX SHAPE (the real fix, in order) + +1. **Port per-cell light registration** (`insert_light` 0x0054d1b0 + the + per-cell light list retail keeps). A light lights its OWN cell's geometry + + cells reachable through portals — NOT arbitrary world-space overlap. This + kills #1 (through-floor) and makes the global pool cap irrelevant (per-cell + sets are naturally bounded), which is what actually lets #176/#177 close. +2. **Static curve for stationary fixtures.** Decide `isDynamic` by whether the + light MOVES, not by dat-static-vs-weenie origin. A server-spawned wall lantern + is stationary → static 1/d³. (Register AP-67/AP-44 are the weenie-light path; + AP-85 is the pool cap; #143 is the curve-by-path decision to revisit.) +3. **Hunt the stripes** with the full pool on (see #3 above). +4. **THEN uncap** `MaxGlobalLights` — un-skip + `LightManagerTests.PointSnapshot_HubScaleLightCount_ObjectSelectionIsCameraInvariant` + (it asserts the retail end-state: an in-range light of a cell is never + camera-evicted). + +## Tooling built this session (reuse, don't rebuild) + +- **`tests/AcDream.Core.Tests/Rendering/Issue176177DungeonSeamInspectionTests.cs`** + — dat truth for the Hub: portal-poly draw verdicts, reciprocal coincidence, + stair geometry owner (`0x8A020182`'s ramp shell, vertical portals, ZERO + statics), CellBSP containment (partitions exactly at portal planes), + under-hall + corridor drawn-poly surface colors, DXT1 alpha histograms (0 + transparent texels), and `CorridorNeighborhood_CoplanarOverlappingDrawnPolyPairs` + (the stripe-geometry sweep — came back empty for the −6 floor). +- **`tests/AcDream.App.Tests/Rendering/Issue176177FacilityHubFloodReplayTests.cs`** + — production-matched portal-flood replays (approach/descent/gaze-sweep/walk + + the ScenarioE incoherent-root sensitivity pin). Flood is HEALTHY — do not + re-investigate it for these issues. +- **`tests/AcDream.Core.Tests/Physics/Issue176177SeamTransitLagTests.cs`** + — resolver cell-flip is plane-exact (membership is NOT the bug). +- **`LightManagerTests.PointSnapshot_HubScaleLightCount_*`** — Skip'd + end-state pin (RED@128, GREEN@1024). +- **Ledger**: `docs/research/2026-07-06-176-177-render-pair-investigation.md` + (13 refuted mechanisms + the probe-run discriminator + the OUTCOME banner). + +## Live probe env (all zero-cost off; use for the A7 spike) + +``` +ACDREAM_PROBE_LIGHT=1 # [light] insideCell/ambient/sun/registeredLights/activeLights — rate-limited +ACDREAM_PROBE_PVINPUT=1 # [pv-input] one line/frame: exact flood inputs + count +ACDREAM_PROBE_CELL=1 # [cell-transit] timeline anchors +ACDREAM_PROBE_TEXFLUSH=1 # [tex-flush] staged-upload drain (proves #105 healthy: after=0) +``` + +A7.L1's planned `[indoor-light]` probe (per-cell active-light dump: position, +color, attenuation, direction) is the natural next apparatus — it prints exactly +the SET COMPOSITION the current `[light]` counts can't. Build it FIRST. + +## Launch protocol (unchanged) + +`dotnet build` green first; PowerShell launch with the CLAUDE.md env block +(+ the probes above), background + Tee to `launch-*.log`. User manages client +lifecycle (graceful close → ACE clears in ~5 s; hard kill → ~3 min). Strip +`\000` before grep (PowerShell Tee = UTF-16): `tr -d '\000' < launch.log | grep ...`. +Test char spawns near `0x8A020179` (the ramp corridor); the 015E↔017A corridor +loop is the #176 repro; look-into + descend the 0178→0182→0183 stairs for #177.