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>
|
||||
public void Tick(Vector3 viewerWorldPos)
|
||||
{
|
||||
// Pass 1: compute DistSq + filter out lights outside the slack radius.
|
||||
var candidates = new List<LightSource>(_all.Count);
|
||||
foreach (var light in _all)
|
||||
{
|
||||
if (!light.IsLit) continue;
|
||||
if (light.Kind == LightKind.Directional)
|
||||
{
|
||||
// Directional lights don't participate in this ranking —
|
||||
// the sun is always slot 0.
|
||||
continue;
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
// Retail D3D-style fixed-pipeline lighting takes the nearest (MaxActiveLights-1)
|
||||
// point lights (slot 0 is the sun) and applies each light's hard range cutoff
|
||||
// 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
|
||||
// range of must still light the wall it sits on.
|
||||
//
|
||||
// Allocation-free partial selection: the old path built `new List<>(N)` and
|
||||
// ran an O(N log N) Sort EVERY FRAME; in a dungeon N is thousands of torches,
|
||||
// so that allocated a large list per frame (GC pressure → FPS). Instead keep
|
||||
// the nearest maxPoint directly in the _active window, maintained sorted by
|
||||
// insertion. O(N · maxPoint), maxPoint ≤ 8, zero allocation.
|
||||
Array.Clear(_active);
|
||||
_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)
|
||||
{
|
||||
_active[0] = Sun;
|
||||
_activeCount = 1;
|
||||
baseSlot = 1;
|
||||
}
|
||||
|
||||
int maxPoint = MaxActiveLights - _activeCount;
|
||||
int pointCount = Math.Min(maxPoint, candidates.Count);
|
||||
for (int i = 0; i < pointCount; i++)
|
||||
int maxPoint = MaxActiveLights - baseSlot;
|
||||
int filled = 0;
|
||||
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