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:
parent
388f3ed307
commit
d275ed554e
5 changed files with 166 additions and 23 deletions
|
|
@ -1,3 +1,4 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using AcDream.Core.Lighting;
|
||||
using Xunit;
|
||||
|
|
@ -256,6 +257,80 @@ public sealed class LightManagerTests
|
|||
Assert.Contains(torch, mgr.PointSnapshot);
|
||||
}
|
||||
|
||||
// ── Visible-cell scoping (A7.L1, 2026-07-09 — the Town Network starvation fix) ──
|
||||
// BuildPointLightSnapshot's player-nearest cap sorts by raw Euclidean distance,
|
||||
// which is not a reliable proxy for "same room" in a dense, maze-like hub: a
|
||||
// fixture on the other side of a wall can be geometrically closer than the
|
||||
// player's own room's torches. The Town Network fountain room (463 registered
|
||||
// fixtures, cap 128) went dark because far-denser, closer-in-a-straight-line
|
||||
// corridor fixtures won the cap over the room's own lights. Filtering candidacy
|
||||
// by the frame's actual visible-cell set (the render already computes this)
|
||||
// fixes it without touching the distance-sort anchor (still the PLAYER, per the
|
||||
// #176 correction — camera anchoring is what caused the earlier flicker).
|
||||
|
||||
[Fact]
|
||||
public void BuildPointLightSnapshot_VisibleCellScoping_RoomLightsSurviveOverEuclideanCloserInvisibleCell()
|
||||
{
|
||||
var mgr = new LightManager();
|
||||
|
||||
// A different, NOT-visible cell packed with fixtures that are, in raw
|
||||
// straight-line distance, closer to the player than the room's own
|
||||
// torches (e.g. a corridor on the other side of a wall).
|
||||
const uint otherCellId = 0xAAAA0102u;
|
||||
for (int i = 0; i < LightManager.MaxGlobalLights + 50; i++)
|
||||
mgr.Register(MakePoint(new Vector3(1f + i * 0.001f, 1f, 0), range: 5f, ownerId: (uint)(i + 1), cellId: otherCellId));
|
||||
|
||||
// The player's own room: a handful of torches, each FARTHER in raw
|
||||
// distance than every "other cell" fixture above, but the only cell
|
||||
// actually visible from the player's viewpoint this frame.
|
||||
const uint roomCellId = 0xAAAA0101u;
|
||||
var roomTorches = new LightSource[5];
|
||||
for (int i = 0; i < roomTorches.Length; i++)
|
||||
{
|
||||
roomTorches[i] = MakePoint(new Vector3(50f + i, 0, 0), range: 15f, cellId: roomCellId);
|
||||
mgr.Register(roomTorches[i]);
|
||||
}
|
||||
|
||||
var visibleCells = new HashSet<uint> { roomCellId };
|
||||
mgr.BuildPointLightSnapshot(playerWorldPos: Vector3.Zero, visibleCells);
|
||||
|
||||
foreach (var torch in roomTorches)
|
||||
Assert.Contains(torch, mgr.PointSnapshot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildPointLightSnapshot_VisibleCellScoping_CellLessLightAlwaysIncluded()
|
||||
{
|
||||
// The viewer fill light (CellId==0) must survive scoping unconditionally —
|
||||
// retail's per-frame add_dynamic_light(&viewer_light, ...) is unconditional
|
||||
// (LightManager.UpdateViewerLight's doc comment).
|
||||
var mgr = new LightManager();
|
||||
var viewerFill = MakePoint(new Vector3(0, 0, 2), range: 15f, cellId: 0u);
|
||||
mgr.Register(viewerFill);
|
||||
var otherRoom = MakePoint(new Vector3(2, 0, 0), range: 5f, cellId: 0xBEEFu);
|
||||
mgr.Register(otherRoom);
|
||||
|
||||
var visibleCells = new HashSet<uint> { 0xF00Du }; // neither light's cell
|
||||
mgr.BuildPointLightSnapshot(Vector3.Zero, visibleCells);
|
||||
|
||||
Assert.Contains(viewerFill, mgr.PointSnapshot);
|
||||
Assert.DoesNotContain(otherRoom, mgr.PointSnapshot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildPointLightSnapshot_NoVisibleCellsArg_UnscopedLegacyBehavior()
|
||||
{
|
||||
// Outdoor / no-clipRoot callers omit visibleCells — every registered lit
|
||||
// light stays a candidate, exactly the pre-A7.L1 behavior.
|
||||
var mgr = new LightManager();
|
||||
mgr.Register(MakePoint(new Vector3(1, 0, 0), 5f, cellId: 0xAAAAu));
|
||||
mgr.Register(MakePoint(new Vector3(2, 0, 0), 5f, cellId: 0xBBBBu));
|
||||
|
||||
mgr.BuildPointLightSnapshot(Vector3.Zero);
|
||||
|
||||
Assert.Equal(2, mgr.PointSnapshot.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectForObject_EmptySnapshot_ReturnsZero()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue