using System;
using System.Collections.Generic;
using System.Numerics;
namespace AcDream.Core.Lighting;
///
/// Manages the registered dynamic lights in the world and picks the 8
/// most relevant ones each frame for the shader to consume. Matches
/// retail's fixed-function-era "8 hardware lights" constraint (r13
/// §12.2).
///
///
/// Active-light selection algorithm (r13 §12.2 "Tick" steps):
///
/// -
/// Recompute DistSq from viewer to every registered
/// point/spot light.
///
/// -
/// Drop lights outside Range² * 1.1 (10% slack prevents
/// pop as we walk across the boundary).
///
/// -
/// Rank remaining lights by DistSq ascending. Pick top 7.
///
/// -
/// Reserve slot 0 for the sun (directional, infinite range).
///
///
///
///
///
/// Not thread-safe — the render thread owns the light list.
///
///
public sealed class LightManager
{
public const int MaxActiveLights = 8; // D3D parity
private const float RangeSlack = 1.1f; // 10% hysteresis around hard cutoff
private readonly List _all = new();
private readonly LightSource?[] _active = new LightSource?[MaxActiveLights];
private int _activeCount;
/// Current cell ambient state applied to everything.
public CellAmbientState CurrentAmbient { get; set; }
///
/// The sun (or "global directional") — always slot 0 of the active
/// list. Set this from the
/// each frame.
///
public LightSource? Sun { get; set; }
/// Snapshot of the currently-active lights (up to 8).
public ReadOnlySpan Active => _active.AsSpan(0, _activeCount);
public int ActiveCount => _activeCount;
public int RegisteredCount => _all.Count;
/// Add a light. Idempotent — adding the same instance twice is a no-op.
public void Register(LightSource light)
{
ArgumentNullException.ThrowIfNull(light);
foreach (var existing in _all)
if (ReferenceEquals(existing, light)) return;
_all.Add(light);
}
/// Remove by reference.
public void Unregister(LightSource light)
{
_all.Remove(light);
}
/// Remove every light attached to a specific entity.
public void UnregisterByOwner(uint ownerId)
{
_all.RemoveAll(l => l.OwnerId == ownerId);
}
public void Clear()
{
_all.Clear();
Array.Clear(_active);
_activeCount = 0;
}
///
/// Refresh the active-light list for the current viewer position.
/// Called once per render frame from the render thread; the shader
/// reads and uploads to the light UBO.
///
public void Tick(Vector3 viewerWorldPos)
{
// Pass 1: compute DistSq + filter out lights outside the slack radius.
var candidates = new List(_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();
float rangeSq = light.Range * light.Range * RangeSlack * RangeSlack;
if (light.DistSq > rangeSq) continue;
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);
_activeCount = 0;
// Slot 0 = sun when present.
if (Sun is not null)
{
_active[0] = Sun;
_activeCount = 1;
}
int maxPoint = MaxActiveLights - _activeCount;
int pointCount = Math.Min(maxPoint, candidates.Count);
for (int i = 0; i < pointCount; i++)
{
_active[_activeCount + i] = candidates[i];
}
_activeCount += pointCount;
}
}