acdream/docs/research/2026-07-06-176-177-render-pair-investigation.md
Erik 4d25e04d83 fix #176 #177: the camera-capped light snapshot evicted visible cells' torches — per-cell lighting popped at seams
The probe launch discriminated it: the user reproduced the purple floor
flash while [light] (ambient branch) and [pv-input] (portal flood) read
provably healthy — eliminating the last CPU-side theories and exposing
the one channel the probes could not see: per-cell 8-light set
composition.

BuildPointLightSnapshot kept the MaxGlobalLights=128 point lights
nearest THE CAMERA; the Facility Hub registers 366 fixtures, so 238
were evicted per frame by camera distance. SelectForObject (faithfully
camera-independent, and unit-pinned as such) could only choose from the
surviving 128 — an in-range torch of a visible cell that ranked past
the cap dropped out of that cell's 8-set, so per-cell Gouraud lighting
flipped as the chase boom swung the camera:

- #176: the flipping unit is a CELL -> discontinuity lines at exactly
  cell-seam granularity; a torch-losing floor drops to dim blue-grey
  stone at 0.2 ambient (the perceived purple), camera-angle dependent.
- #177: a stair room whose torches all ranked past the cap rendered at
  bare 0.2 ambient (near-black = 'not visible'); approach re-admitted
  them ('pops into existence'); the sweeping boundary dropped the
  ramp's lights mid-descent ('disappears on the last step'). The
  geometry never vanished - its lights did.

Retail's minimize_object_lighting (0x0054d480) has NO global
camera-nearest pool cap (lights register per cell, insert_light
0x0054d1b0). Fix: MaxGlobalLights 128 -> 1024, a non-biting safety
valve (GlobalLightPacker grows to fit; 64 B/light). Register row AP-85.

TDD pin: PointSnapshot_HubScaleLightCount_ObjectSelectionIsCameraInvariant
(RED at 128 with a Hub-scale 401-light layout, GREEN at 1024). The
pre-existing camera-independence pin covered the SELECTOR but not the
SNAPSHOT it selects from - the pop re-entered one stage upstream.

Suites: Core 2588 / App 719 / UI 425 / Net 385 green. Pending user gate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 21:55:43 +02:00

181 lines
12 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.

# #176/#177 render pair — investigation ledger (2026-07-06, session 2)
## ✅ OUTCOME (same day, after the probe launch): ROOT CAUSE FOUND + FIX SHIPPED
**The probe run discriminated it.** The user reproduced the purple floor
flash while BOTH surviving CPU theories read provably healthy in the log —
`[light]` insideCell/ambient rock-stable (one pre-spawn outdoor line, then
flat 0.2 grey through 36 transits), `[pv-input]` flood stable (54k frames,
zero collapses). That eliminated T-A and T-B and exposed the one channel
the probes were structurally blind to: **per-cell 8-light SET COMPOSITION.**
The log's own headline number told the story: `registeredLights=366`
against `MaxGlobalLights = 128`. `BuildPointLightSnapshot` kept the 128
lights nearest THE CAMERA and evicted 238 every frame; `SelectForObject`
(camera-independent, faithfully retail — and unit-PINNED as such) could
only choose from the surviving 128. An in-range torch of a VISIBLE cell
that ranked past the cap dropped out of that cell's 8-set → the cell's
per-vertex Gouraud lighting flipped as the camera moved (the chase boom
swings the camera position by meters, re-ranking the 128):
- **#176:** the flipping unit is A CELL → discontinuity lines at exactly
cell-seam granularity; a torch-losing floor drops to dim blue-grey
stone (0.2 ambient × stone = the perceived purple); camera-angle
dependent by construction.
- **#177:** a stair room whose torches ALL ranked past the cap rendered
at bare 0.2 ambient — near-black in a dungeon = "not visible from the
corridor"; approaching re-admitted them = "pops into existence"; the
boundary sweeping during the descent dropped the ramp's lights =
"disappears on the last step". The geometry never vanished — its
LIGHTS did.
Retail anchor: `minimize_object_lighting` (0x0054d480) selects from the
cell-registered reaching set (`insert_light` 0x0054d1b0) — **no global
camera-nearest pool cap exists in retail.**
**Fix:** `MaxGlobalLights` 128 → 1024 (a non-biting safety valve; the
GPU packer grows to fit — 64 B/light). Register row **AP-85**. TDD pin:
`LightManagerTests.PointSnapshot_HubScaleLightCount_ObjectSelectionIsCameraInvariant`
(RED at 128 with a Hub-scale 401-light layout, GREEN at 1024). All four
suites green. **Pending the user visual gate.**
Process note: the pre-existing test
`SelectForObject_CameraIndependent_DependsOnlyOnObjectCentre` was written
to pin "the property that kills the lights-up-as-I-approach popping" — it
proved the SELECTOR camera-independent while the SNAPSHOT it selects from
was camera-capped. The pop re-entered one stage upstream of the pin.
---
**Pre-launch status below (kept as the audit trail): mechanism NOT yet pinned — but the hypothesis space is now razor-thin.**
Twelve candidate mechanisms refuted by direct evidence (dat dumps, headless
replays, production-log analysis, code reads). Every layer that can be checked
offline is verified HEALTHY at the anchor cells. The surviving discriminator
requires ONE live probe launch (protocol at the bottom — piggyback on the
pending #175 door gate).
## The issues
- **#176** — purple flashing on dungeon floors at cell seams, camera-angle
dependent (Facility Hub).
- **#177** — stairs pop in/out across levels: (a) vanish on the last step
running down, (b) invisible looking into the stair room from the corridor,
(c) pop into existence on entering.
## Anchor-cell dat truth (Issue176177DungeonSeamInspectionTests)
- Corridor `0x8A02016E` floor = three abutting TEXTURED drawn portal polys
(polys 1/3/5, surface 0x08000377 → DXT1 tex 0x050026F7, `[PortalSide]`) →
under-hall `0x011E` (z=12). Reciprocal ceiling poly = NoPos (not drawn).
- The "stairs" = a RAMP owned by `0x8A020182` (inclined drawn polys, z 9…6
floor + 6…3 ceiling); `0x0183` = flat lower cell. Connections are
VERTICAL wall portals (NOT floor-portals). PortalSide flag asymmetry:
0x0182→0x0183 carries PortalSide; the back-portal does not.
- **Zero StaticObjects in all five anchor cells** (no #119-class statics, no
torch-bearing stabs → no registered point lights except the viewer fill).
- CellBSP volumes partition EXACTLY at the portal planes (no overlap zone).
- All surfaces resolve; DXT1 textures contain **zero** transparent-mode
texels (all 3-color-mode blocks, index 3 never used).
## REFUTED mechanisms (each by direct evidence — do NOT retry)
| # | Hypothesis | Killed by |
|---|---|---|
| 1 | Placeholder/missing texture (magenta class) | All surfaces resolve; drawn-poly sweep 0 misses |
| 2 | Reciprocal portal-poly z-fight | Reciprocal is NoPos (never drawn) |
| 3 | Seal depth-stamp z-fights the drawn floor-portal | Seals fire ONLY for `OtherCellId==0xFFFF` (GameWindow:11437); a sealed dungeon draws zero seals |
| 4 | Root/eye incoherence (viewer root lags the eye across portal planes) | Production camera sweep publishes coherent pairs; out-cell flips at x=85.001/88.335 — mm-exact at the planes (gate2 log). The flood DOES collapse to 1 cell under artificially incoherent inputs (ScenarioE pin) — but production inputs are coherent |
| 5 | Membership transit lag (0.330.47 m in [cell-transit]) as the render-root lag | The resolver flips within one tick-step of the plane in the harness (Issue176177SeamTransitLagTests); the logged "lag" is speed×tick quantization of the PLAYER probe, and the camera root (probe out-cell) is plane-exact |
| 6 | Flood bistability at the anchors | ScenarioC gaze sweep (2° steps, 4 pitches): 0 one-step drops; ScenarioA stair approach: ramp+lower admitted at all tested eyes/pitches |
| 7 | Staircase = EnvCell static culled by viewcone (#119 class) | Zero statics in the anchor cells; the stairs are shell geometry |
| 8 | Undefined DXT mip levels (compressed arrays skip GenerateMipmap) | Both ObjectMeshManager texture paths DECODE DXT→RGBA8 (BcDecoder) — the compressed-array branch of ManagedGLTextureArray is dead WB-heritage code; RGBA8 arrays get real mips |
| 9 | DXT1 3-color-mode alpha=0 texels + opaque-pass `discard a<0.05` / A2C | Block histogram: 0 transparent texels in all Hub floor/wall textures |
| 10 | Fog mix toward purple FogColor at distance | Fog ramp starts at `_nearRadius×192×0.7 ≈ 538 m` (radii stay 4/12 in dungeon mode); Hub sightlines ≤ ~100 m → fog term ≡ 0 |
| 11 | Lightning-flash additive (`uFogParams.z × (0.6,0.6,0.75)`) leaking indoors | `WeatherState._flashLevel` is 0 in production ("Production never TriggerFlashes") — dormant test hook. (The missing indoor gate is still real debt for when storm strobes ship.) |
| 12 | Viewer-light per-cell/per-vertex pops (hard range edge or 8-set membership flips) | The point ramp is `(1d/range)` — smoothly ZERO at the range boundary; set-membership beyond range is a zero-contribution no-op. No torches exist in the Hub cells to churn the 8-cap |
## VERIFIED-HEALTHY layers (offline pins, keep as regression assets)
- `PortalVisibilityBuilder` at the anchors: approach/descent/gaze-sweep/walk
scenarios (`Issue176177FacilityHubFloodReplayTests`) — admissions correct
and stable with coherent inputs; the ScenarioE incoherent-input collapse is
the sensitivity pin (1-cell flood when root≠eye side).
- Membership: `ResolveWithTransition` flips cells within one tick of the
portal plane, both directions (`Issue176177SeamTransitLagTests`).
- Mesh path: `CellMesh.Build` (production, GameWindow:7013) draws the
textured floor-portal strips; snapshot frustum gate (WbFrustum) is the
standard conservative p-vertex test; per-cell AABBs are vertex-derived,
8-corner transformed.
- Per-instance light sets are truly per-instance (EnvCellRenderer MDI).
## SURVIVING theories (need the live discriminator)
- **T-A (ambient flip):** any frame where `playerRoot` resolves null (or
`playerSeenOutside` defaults true) runs the OUTDOOR lighting branch —
purple sky ambient + full sun. Sun is directional-from-above → floors
(N·L≈1) catch it, walls (N·L≈0) barely → a FLOOR-selective purple-bright
flash, temporally lockable to whatever gaps CurrCell/TryGetCell. Desk
analysis found no per-frame gap trigger during plain corridor runs
(UpdatePlayerCurrCell is stale-beats-null; the registry is stable in
AP-36 dungeon mode) — but the branch inputs are exactly probed by
`[light] insideCell=` lines, so one run settles it.
- **T-B (flood with REAL production inputs):** my harness feeds synthetic
viewProj/eye. If the real per-frame inputs at the artifact moments differ
(collided camera pressed into walls near seams, damped-forward gaze), the
flood could still misbehave in configurations the sweeps missed.
`[pv-input]` prints the exact inputs + flood count per frame.
- **T-C:** unknown-unknown (GPU state, driver, MSAA resolve…). If T-A/T-B
both read clean at a flash moment → RenderDoc frame capture next.
## Also found (real, filed, not these bugs)
- **A8 double-sided shells stopgap still live**: `EnvCellRenderer.cs`
RenderModernMDIInternal maps `CullMode.Landblock → CullMode.None`
("while the architectural cause is isolated") — all cell shells draw
two-sided. Perf + correctness debt; retire under the A7/A8 lighting arc.
- **Lightning indoor gate missing** (dormant): when weather strobes ship,
`SceneLightingUbo.Build` needs the `playerInsideCell` gate or dungeons
will strobe blue-violet.
- **Flood follows floor-portals DOWNWARD from above** (ScenarioC: under-hall
network admitted at down-pitches). Retail's `portal_side` side test
(0x005a59a0: portal_side≠0 → NEGATIVE side only; ==0 → POSITIVE only;
IN_PLANE(±0.0002) → refused) *appears* to refuse this direction, but my
plane-sign reading had unresolved contradictions (CCellPortal::UnPack
normalizes portal_side at load, 0x0053ba1c/0x0053bc6a). OPEN QUESTION —
harmless-looking (extra admitted cells draw below the opaque floor,
z-buffer wins) but worth settling when the flood is next touched.
## The live probe protocol (piggyback on the #175 door gate launch)
Env (add to the standard launch block):
```
ACDREAM_PROBE_LIGHT=1 # [light] insideCell/ambient/sun — rate-limited
ACDREAM_PROBE_PVINPUT=1 # [pv-input] one line/frame: exact flood inputs + count
ACDREAM_PROBE_CELL=1 # [cell-transit] timeline anchors
```
User reproduces in Facility Hub (~2 min): run the corridor across several
seams until the purple flash shows; approach the stair room from the
corridor, walk in, run down. Then close. Discrimination:
- Flash moments + `[light]` shows `insideCell=False` blips or ambient jumps
**T-A confirmed** (then root-cause the gap trigger from the same log).
- `[pv-input]` flood count drops (e.g. 12→1) at flash/pop moments while
`[light]` stays clean → **T-B confirmed** (the line carries the exact
inputs to replay in `Issue176177FacilityHubFloodReplayTests`).
- Stairs invisible while the flood contains 0x0182/0x0183 → draw-side hunt
(RenderDoc next); absent → flood-side with the printed inputs.
- All clean → T-C: RenderDoc frame capture of a flash frame.
## Apparatus shipped this session
- `tests/AcDream.Core.Tests/Rendering/Issue176177DungeonSeamInspectionTests.cs`
— dat truth: portal polys/surfaces/draw verdicts, reciprocal coincidence,
stair geometry owner, CellBSP containment, under-hall surface colors,
DXT1 alpha histograms.
- `tests/AcDream.App.Tests/Rendering/Issue176177FacilityHubFloodReplayTests.cs`
— production-matched flood replays: stair approach, descent, gaze sweep,
corridor walk, and the ScenarioE incoherent-root sensitivity pin.
- `tests/AcDream.Core.Tests/Physics/Issue176177SeamTransitLagTests.cs`
— resolver cell-flip position at the seam (plane-exact pin).