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>
181 lines
12 KiB
Markdown
181 lines
12 KiB
Markdown
# #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.33–0.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 `(1−d/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).
|