feat(A7.L1): scope the point-light pool by last frame's visible cells (#79/#93/#176/#177)

The Town Network hub (498 registered fixtures) starved the player's own
room: BuildPointLightSnapshot's player-nearest-128 cap sorts by raw
Euclidean distance, which isn't a reliable proxy for "same room" in a
dense maze — a fixture on the other side of a wall can be geometrically
closer than the room's own torches and win the cap. LightSource.CellId
tagging and the [indoor-light] membership probe already existed from the
c500912b/#176 arc; the missing piece was a candidacy filter.

BuildPointLightSnapshot(playerWorldPos, visibleCells) now narrows
candidates to the frame's actual visible cells before the existing
dynamics-first player-nearest cap runs (cell-less lights, e.g. the viewer
fill, always included). GameWindow feeds LAST FRAME's already-rendered
RetailPViewFrameResult.DrawableCells — one frame of latency instead of
re-threading a mid-DrawInside callback, which was the exact mechanism
(c500912b) that caused the earlier #176 seam-floor flicker regression.
The distance-sort anchor stays the player, unchanged.

AP-85 updated in place (third revision) rather than adding a new row —
same underlying divergence, now with the render-visibility approximation
of retail's true DBObj-load/flush-bounded resident registry documented
alongside its residual risk (one unscoped frame on portal re-entry).

Core 2652+2skip / App 741+2skip / UI 425 / Net 385 green. Pending: user
visual gate at the Town Network fountain, and a #176 corridor-seam
non-regression recheck (Facility Hub, different landblock).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-09 10:34:15 +02:00
parent 388f3ed307
commit d275ed554e
5 changed files with 166 additions and 23 deletions

View file

@ -794,6 +794,22 @@ public sealed class GameWindow : IDisposable
new AcDream.Core.World.WorldTimeService(
AcDream.Core.World.SkyStateProvider.Default());
public readonly AcDream.Core.Lighting.LightManager Lighting = new();
// A7.L1 (2026-07-09) — last frame's rendered visible-cell set, fed into next
// frame's Lighting.BuildPointLightSnapshot scoping. One frame of latency
// (~16 ms) instead of a mid-DrawInside callback: DrawableCells only becomes
// known partway through DrawInside (after the flood + look-in merge), and the
// #176 correction already showed that rebuilding the light pool FROM a
// freshly-recomputed frame flood (the deleted RebuildScopedLights callback,
// c500912b) is a flicker mechanism. Reusing last frame's already-drawn set
// (MP-Alloc: reused HashSet, no per-frame alloc) decouples the light-scoping
// change from DrawInside's internals entirely. _lightPoolVisibleCellsValid is
// false until the first indoor frame completes, and is cleared on any
// outdoor-only frame so scoping fails open (unscoped) for one frame on
// re-entry rather than filtering by a stale dungeon's cell ids.
private readonly HashSet<uint> _lightPoolVisibleCells = new();
private bool _lightPoolVisibleCellsValid;
public readonly AcDream.Core.World.WeatherSystem Weather = new();
// Wired into the hook router in OnLoad so SetLightHook fires
// from the animation pipeline flip the matching LightSource.IsLit.
@ -9255,7 +9271,13 @@ public sealed class GameWindow : IDisposable
// collected from ALL registered (=resident-cell) lights — it is a
// function of player position only, so camera rotation cannot change
// it. playerViewPos carries retail's no-player fallback (camPos).
Lighting.BuildPointLightSnapshot(playerViewPos);
// A7.L1: candidacy is additionally scoped to last frame's rendered
// visible-cell set (see _lightPoolVisibleCells) when available — the
// Town Network starvation fix. Unscoped (null) until the first indoor
// frame completes or after any outdoor-only frame.
Lighting.BuildPointLightSnapshot(
playerViewPos,
_lightPoolVisibleCellsValid ? _lightPoolVisibleCells : null);
_wbDrawDispatcher?.SetSceneLights(Lighting.PointSnapshot);
_envCellRenderer?.SetPointSnapshot(Lighting.PointSnapshot); // A7 Fix D (D-2)
@ -9682,6 +9704,14 @@ public sealed class GameWindow : IDisposable
envCellShellFilter = pviewResult.DrawableCells;
_interiorPartition = pviewResult.Partition;
// A7.L1: snapshot this frame's drawn cell set for NEXT frame's light-
// pool scoping. DrawableCells is the renderer's own reused scratch set
// (cleared/rebuilt every DrawInside call) — copy it, don't hold a
// reference to it.
_lightPoolVisibleCells.Clear();
_lightPoolVisibleCells.UnionWith(pviewResult.DrawableCells);
_lightPoolVisibleCellsValid = true;
// Flap root-cause apparatus (2026-06-07): per-frame, the EXACT Build inputs at 6 dp +
// the resulting flood count. The live flap shows flood flipping 2↔6 at an eye/player
// identical to cm; this answers whether the inputs differ sub-cm (jitter) or are
@ -9742,6 +9772,10 @@ public sealed class GameWindow : IDisposable
neverCullLandblockId: playerLb,
visibleCellIds: null,
animatedEntityIds: animatedIds);
// A7.L1: no indoor draw this frame — fail open (unscoped) rather than
// scoping next frame's light pool by a stale dungeon's cell ids.
_lightPoolVisibleCellsValid = false;
}
// Phase U.3: close the world-geometry clip bracket opened above. From here down the