perf(lighting): allocation-free nearest-N light selection (#133 FPS)
Tick built a new List<>(N) and ran an O(N log N) Sort every frame; in a dungeon N is thousands of torches, so it allocated a large list per frame (GC pressure -> FPS). Replace with an insertion partial-select that keeps the nearest maxPoint directly in the _active window — O(N * maxPoint), maxPoint<=8, zero allocation. Same selection result (nearest 8); lighting suite 20/20 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0fe479ba06
commit
5872bcf075
1 changed files with 51 additions and 38 deletions
|
|
@ -93,53 +93,66 @@ public sealed class LightManager
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Tick(Vector3 viewerWorldPos)
|
public void Tick(Vector3 viewerWorldPos)
|
||||||
{
|
{
|
||||||
// Pass 1: compute DistSq + filter out lights outside the slack radius.
|
// Retail D3D-style fixed-pipeline lighting takes the nearest (MaxActiveLights-1)
|
||||||
var candidates = new List<LightSource>(_all.Count);
|
// point lights (slot 0 is the sun) and applies each light's hard range cutoff
|
||||||
foreach (var light in _all)
|
// PER SURFACE in the shader (mesh_modern.frag: `if (d < range && range > 1e-3)`),
|
||||||
{
|
// NOT a viewer-range candidacy filter — a torch the viewer stands outside the
|
||||||
if (!light.IsLit) continue;
|
// range of must still light the wall it sits on.
|
||||||
if (light.Kind == LightKind.Directional)
|
//
|
||||||
{
|
// Allocation-free partial selection: the old path built `new List<>(N)` and
|
||||||
// Directional lights don't participate in this ranking —
|
// ran an O(N log N) Sort EVERY FRAME; in a dungeon N is thousands of torches,
|
||||||
// the sun is always slot 0.
|
// so that allocated a large list per frame (GC pressure → FPS). Instead keep
|
||||||
continue;
|
// the nearest maxPoint directly in the _active window, maintained sorted by
|
||||||
}
|
// insertion. O(N · maxPoint), maxPoint ≤ 8, zero allocation.
|
||||||
|
|
||||||
Vector3 delta = light.WorldPosition - viewerWorldPos;
|
|
||||||
light.DistSq = delta.LengthSquared();
|
|
||||||
|
|
||||||
// Retail D3D-style fixed-pipeline lighting picks the 8 NEAREST point
|
|
||||||
// lights and applies each light's hard range-cutoff PER SURFACE in the
|
|
||||||
// shader (mesh_modern.frag: `if (d < range && range > 1e-3)`). The
|
|
||||||
// previous viewer-range candidacy filter (skip when DistSq > Range²·slack²)
|
|
||||||
// was wrong — it dropped a torch whenever the VIEWER stood outside that
|
|
||||||
// torch's range, so a dungeon room with 2227 registered torches lit only
|
|
||||||
// the ~1 the player was standing inside (activeLights≈1, the rest of the
|
|
||||||
// room at flat 0.2 ambient — the "dungeon lighting off" report). Take the
|
|
||||||
// nearest 8 regardless of viewer range; the shader's per-fragment
|
|
||||||
// `d < range` does the actual hard cutoff.
|
|
||||||
candidates.Add(light);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pass 2: sort by DistSq ascending, take up to 7.
|
|
||||||
candidates.Sort((a, b) => a.DistSq.CompareTo(b.DistSq));
|
|
||||||
|
|
||||||
Array.Clear(_active);
|
Array.Clear(_active);
|
||||||
_activeCount = 0;
|
_activeCount = 0;
|
||||||
|
|
||||||
// Slot 0 = sun when present.
|
// Slot 0 = sun when present (directional; never ranked by distance).
|
||||||
|
int baseSlot = 0;
|
||||||
if (Sun is not null)
|
if (Sun is not null)
|
||||||
{
|
{
|
||||||
_active[0] = Sun;
|
_active[0] = Sun;
|
||||||
_activeCount = 1;
|
baseSlot = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
int maxPoint = MaxActiveLights - _activeCount;
|
int maxPoint = MaxActiveLights - baseSlot;
|
||||||
int pointCount = Math.Min(maxPoint, candidates.Count);
|
int filled = 0;
|
||||||
for (int i = 0; i < pointCount; i++)
|
if (maxPoint > 0)
|
||||||
{
|
{
|
||||||
_active[_activeCount + i] = candidates[i];
|
foreach (var light in _all)
|
||||||
|
{
|
||||||
|
if (!light.IsLit || light.Kind == LightKind.Directional) continue;
|
||||||
|
|
||||||
|
Vector3 delta = light.WorldPosition - viewerWorldPos;
|
||||||
|
light.DistSq = delta.LengthSquared();
|
||||||
|
|
||||||
|
// Maintain _active[baseSlot .. baseSlot+filled) sorted ascending by
|
||||||
|
// DistSq. Insert if there's room or this light is nearer than the
|
||||||
|
// current farthest (then the farthest falls off the end).
|
||||||
|
if (filled < maxPoint)
|
||||||
|
{
|
||||||
|
int j = baseSlot + filled;
|
||||||
|
while (j > baseSlot && _active[j - 1]!.DistSq > light.DistSq)
|
||||||
|
{
|
||||||
|
_active[j] = _active[j - 1];
|
||||||
|
j--;
|
||||||
|
}
|
||||||
|
_active[j] = light;
|
||||||
|
filled++;
|
||||||
|
}
|
||||||
|
else if (light.DistSq < _active[baseSlot + maxPoint - 1]!.DistSq)
|
||||||
|
{
|
||||||
|
int j = baseSlot + maxPoint - 1;
|
||||||
|
while (j > baseSlot && _active[j - 1]!.DistSq > light.DistSq)
|
||||||
|
{
|
||||||
|
_active[j] = _active[j - 1];
|
||||||
|
j--;
|
||||||
|
}
|
||||||
|
_active[j] = light;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_activeCount += pointCount;
|
|
||||||
|
_activeCount = baseSlot + filled;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue