acdream/docs/research/2026-07-06-176-177-handoff-A7-lighting.md
Erik e1746ca10d docs: #176/#177 handoff — root-caused, folded into A7 dungeon lighting
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 <noreply@anthropic.com>
2026-07-05 23:16:17 +02:00

152 lines
8.9 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 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.