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>
This commit is contained in:
Erik 2026-07-05 21:55:43 +02:00
parent b8e9e204ad
commit 4d25e04d83
5 changed files with 151 additions and 28 deletions

View file

@ -176,9 +176,18 @@ public sealed class LightManager
public const int MaxLightsPerObject = 8;
/// <summary>Hard cap on the per-frame global point-light snapshot the shader
/// indexes. AC scenes rarely exceed a few dozen lit point lights in view; 128
/// is generous. If exceeded, the nearest-to-camera are kept (cold path).</summary>
public const int MaxGlobalLights = 128;
/// indexes. #176/#177 (2026-07-06): the old value of 128 BIT in the Facility
/// Hub — 366 registered fixtures, so 238 were evicted per frame by camera
/// distance. An in-range torch of a VISIBLE cell that ranked past the cap
/// dropped out of the cell's 8-light set, so per-cell Gouraud lighting popped
/// as the camera moved (the purple seam flash / the stair-room pop-in).
/// Retail's <c>minimize_object_lighting</c> (0x0054d480) has NO global
/// camera-nearest cap — every registered light reaching an object is a
/// candidate. 1024 is a pure safety valve (the whole Facility Hub registers
/// 366; GlobalLightPacker grows to fit, 64 B/light on the GPU) and must never
/// bite at real content scale — if it ever does, raise it, don't tune it.
/// Pin: LightManagerTests.PointSnapshot_HubScaleLightCount_ObjectSelectionIsCameraInvariant.</summary>
public const int MaxGlobalLights = 1024;
private readonly List<LightSource> _pointSnapshot = new();