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>
8.9 KiB
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"):
- Light-through-solid-floors. Retail registers lights per-CELL
(
insert_light0x0054d1b0); 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). - Fixture falloff curve misassignment. Stationary weenie fixtures
(ACE serves dungeon lanterns/braziers as CreateObject weenies) register via
the
isDynamic:truepath → 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_light0x0059c8b0, static_light_factor 1.3). 1/d is ~9× stronger at 3 m → every pool over-broad + over-saturated. TheisDynamicflag should be reserved for genuinely MOVING lights (portal swirls, projectiles); a stationary fixture — even server-spawned — is static. - 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_CoplanarOverlappingDrawnPolyPairssweep 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 plainBase1Imagestone 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)
- Port per-cell light registration (
insert_light0x0054d1b0 + 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. - Static curve for stationary fixtures. Decide
isDynamicby 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.) - Hunt the stripes with the full pool on (see #3 above).
- THEN uncap
MaxGlobalLights— un-skipLightManagerTests.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), andCorridorNeighborhood_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.