feat(lighting): A7 visible-cell light scoping + [indoor-light] probe (NOT the #176/#177 fix)
Port retail's per-frame light collection: the point-light pool is built from ONLY the currently-visible cells' lights, matching CObjCell::add_*_to_global_lights (0x0052b350/0x0052b390) walked over CEnvCell::visible_cell_table (0x0052d410) — not a flat world-space set capped at 128-nearest-camera. - LightSource.CellId (retail insert_light arg6 -> RenderLight +0x6c); tagged at both registration sites from entity.ParentCellId (live weenie fixtures + dat EnvCell statics). - LightManager.BuildPointLightSnapshot(camPos, visibleCells): a light joins the pool iff CellId==0 (viewer/global) or its cell is in the flood. 128 cap kept as a now-non-biting backstop (retail's is 40 static + 7 dynamic, 0x0081ec94/8). - Threaded via RetailPViewDrawContext.RebuildScopedLights, invoked in DrawInside after the flood resolves prepareCells and before the draws (renderers select from the same in-place-rebuilt PointSnapshot; EnvCellRenderer clears its per-cell cache each pass). - [indoor-light] probe (ACDREAM_PROBE_INDOOR_LIGHT=1) dumps the scoped-pool SET COMPOSITION. Un-skips LightManagerTests.PointSnapshot_HubScaleLightCount_ObjectSelectionIsCameraInvariant. CORRECTION: the handoff called the camera-cap the "confirmed" #176/#177 mechanism. The probe PROVES scoping works (291 Hub fixtures -> pool of 1-9, ~285 through-floor lights dropped/frame, CellIds match the flood), but the user's VISUAL GATE showed BOTH symptoms unchanged. So pool composition is NOT the cause. #176 real cause = an over-bright purple point light (intensity=100, color 0.784,0,0.784 -- from [light-detail]); #177 = a portal-visibility miss (stairs not drawn looking back). Both stay OPEN. This change is retail-faithful and retires the camera-eviction latent bug; kept as such, not as the symptom fix. Register AP-85 corrected; ISSUES #176/#177 re-diagnosed; render digest banner updated. Decomp: insert_light 0x0054d1b0, minimize_object_lighting 0x0054d480, calc_point_light 0x0059c8b0; pseudocode docs/research/2026-07-06-a7-per-cell-lighting-pseudocode.md. Suites green: Core 2595 + 2 skip, App 719 + 2 skip. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
e1746ca10d
commit
c500912bf8
10 changed files with 516 additions and 35 deletions
|
|
@ -218,11 +218,33 @@ public sealed class LightManager
|
|||
/// per-object selection.
|
||||
/// </summary>
|
||||
public void BuildPointLightSnapshot(Vector3 cameraWorldPos)
|
||||
=> BuildPointLightSnapshot(cameraWorldPos, visibleCells: null);
|
||||
|
||||
/// <summary>
|
||||
/// Visible-cell-scoped snapshot build — the faithful port of retail's per-frame
|
||||
/// light collection (<c>CObjCell::add_*_to_global_lights</c> 0x0052b350/0x0052b390
|
||||
/// walked over <c>CEnvCell::visible_cell_table</c> 0x0052d410). When
|
||||
/// <paramref name="visibleCells"/> is non-null (an indoor root with a portal
|
||||
/// flood), a cell-tagged light is a candidate ONLY when its <see cref="LightSource.CellId"/>
|
||||
/// is in the visible set — a cell-less light (<c>CellId == 0</c>: the viewer fill,
|
||||
/// global lights) is always included. This is what (a) stops an under-room light
|
||||
/// washing THROUGH a solid floor (its cell isn't visibly flooded → excluded) and
|
||||
/// (b) bounds the pool to the handful of visible cells so <see cref="MaxGlobalLights"/>
|
||||
/// never evicts a visible cell's in-range light (the #176/#177 mechanism). When
|
||||
/// <paramref name="visibleCells"/> is null (outdoor root / no flood) the behaviour
|
||||
/// is unchanged from the legacy full-pool path.
|
||||
/// </summary>
|
||||
public void BuildPointLightSnapshot(Vector3 cameraWorldPos, IReadOnlySet<uint>? visibleCells)
|
||||
{
|
||||
_pointSnapshot.Clear();
|
||||
foreach (var light in _all)
|
||||
{
|
||||
if (!light.IsLit || light.Kind == LightKind.Directional) continue;
|
||||
// Visible-cell scoping (retail add_*_lights over visible_cell_table). A
|
||||
// cell-less light (CellId == 0: viewer fill / global) is always a candidate;
|
||||
// a cell-tagged light joins the pool ONLY when its cell is visibly flooded.
|
||||
if (visibleCells is not null && light.CellId != 0 && !visibleCells.Contains(light.CellId))
|
||||
continue;
|
||||
light.DistSq = (light.WorldPosition - cameraWorldPos).LengthSquared();
|
||||
_pointSnapshot.Add(light);
|
||||
}
|
||||
|
|
@ -231,6 +253,11 @@ public sealed class LightManager
|
|||
_pointSnapshot.Sort(static (a, b) => a.DistSq.CompareTo(b.DistSq));
|
||||
_pointSnapshot.RemoveRange(MaxGlobalLights, _pointSnapshot.Count - MaxGlobalLights);
|
||||
}
|
||||
|
||||
// A7.L1 SET-COMPOSITION probe — only meaningful on the scoped (indoor) path.
|
||||
// Inert unless ACDREAM_PROBE_INDOOR_LIGHT=1; the flag check keeps it zero-cost off.
|
||||
if (visibleCells is not null && AcDream.Core.Rendering.RenderingDiagnostics.ProbeIndoorLightEnabled)
|
||||
AcDream.Core.Rendering.RenderingDiagnostics.EmitIndoorLight(visibleCells.Count, _all, _pointSnapshot);
|
||||
}
|
||||
|
||||
// ── Viewer light — retail SmartBox::set_viewer (0x00452c40) ──────────────
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue