namespace AcDream.App.Rendering;
///
/// Least-recently-used set for resources which currently have no logical
/// owner but remain resident for inexpensive reuse. The byte budget is a
/// cache policy, not a lifetime fence: callers remove one key here and then
/// retire its physical GPU storage through .
/// Render-thread only.
///
internal sealed class BoundedUnownedResourceCache where TKey : notnull
{
private readonly long _budgetBytes;
private readonly int _maximumCount;
private readonly LinkedList _lru = new();
private readonly Dictionary _entries = new();
private long _residentBytes;
private readonly record struct Entry(long Bytes, LinkedListNode Node);
public BoundedUnownedResourceCache(long budgetBytes, int maximumCount = int.MaxValue)
{
ArgumentOutOfRangeException.ThrowIfNegative(budgetBytes);
ArgumentOutOfRangeException.ThrowIfLessThan(maximumCount, 1);
_budgetBytes = budgetBytes;
_maximumCount = maximumCount;
}
public int Count => _entries.Count;
public long ResidentBytes => _residentBytes;
public long BudgetBytes => _budgetBytes;
public int MaximumCount => _maximumCount;
public bool Contains(TKey key) => _entries.ContainsKey(key);
public void MarkUnowned(TKey key, long bytes)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(bytes);
if (_entries.TryGetValue(key, out Entry existing))
{
if (existing.Bytes != bytes)
throw new InvalidOperationException(
$"Cached resource {key} changed size from {existing.Bytes} to {bytes} bytes.");
_lru.Remove(existing.Node);
_lru.AddLast(existing.Node);
return;
}
LinkedListNode node = _lru.AddLast(key);
_entries.Add(key, new Entry(bytes, node));
_residentBytes = checked(_residentBytes + bytes);
}
public bool MarkOwned(TKey key)
{
if (!_entries.Remove(key, out Entry entry))
return false;
_lru.Remove(entry.Node);
_residentBytes -= entry.Bytes;
return true;
}
///
/// Removes the oldest unowned key only while the cache is above budget.
/// Calling once per frame gives the GPU driver explicit destruction
/// backpressure instead of turning a portal transition into one large
/// resource-release burst.
///
public bool TryTakeOldestOverBudget(out TKey key)
{
if ((_residentBytes <= _budgetBytes && _entries.Count <= _maximumCount)
|| _lru.First is null)
{
key = default!;
return false;
}
LinkedListNode node = _lru.First;
key = node.Value;
Entry entry = _entries[key];
_lru.RemoveFirst();
_entries.Remove(key);
_residentBytes -= entry.Bytes;
return true;
}
public bool TryTakeOldest(out TKey key)
{
if (_lru.First is null)
{
key = default!;
return false;
}
LinkedListNode node = _lru.First;
key = node.Value;
Entry entry = _entries[key];
_lru.RemoveFirst();
_entries.Remove(key);
_residentBytes -= entry.Bytes;
return true;
}
public bool TryTake(TKey key)
{
if (!_entries.Remove(key, out Entry entry))
return false;
_lru.Remove(entry.Node);
_residentBytes -= entry.Bytes;
return true;
}
public void Clear()
{
_lru.Clear();
_entries.Clear();
_residentBytes = 0;
}
}