using System;
using System.Collections.Generic;
namespace AcDream.App.Audio;
///
/// Pure LRU/byte-budget bookkeeping for 's
/// native AL buffer cache (_bufferByWaveId).
///
///
/// Deliberately has no AL/Silk dependency.
/// supplies an "is this buffer id still attached to a live source"
/// predicate — native AL per-source state is the only thing that actually
/// determines whether alDeleteBuffers would fail, so the engine
/// queries it live via AL.GetSourceProperty rather than this class
/// tracking a second, easily-stale copy. This class only decides WHICH
/// resident buffer is the least-recently-used *evictable* one and keeps
/// the running byte total, which keeps that decision unit-testable without
/// a live OpenAL device.
///
internal sealed class AlBufferBudgetTracker
{
private sealed class Entry
{
public required uint WaveId { get; init; }
public required uint BufferId { get; init; }
public required long Bytes { get; init; }
public long LastUseTick { get; set; }
}
private readonly Dictionary _byWaveId = new();
private long _tick;
public AlBufferBudgetTracker(long maxBytes)
{
ArgumentOutOfRangeException.ThrowIfLessThan(maxBytes, 1);
MaxBytes = maxBytes;
}
public long MaxBytes { get; }
public long ResidentBytes { get; private set; }
public int Count => _byWaveId.Count;
public bool TryGetBufferId(uint waveId, out uint bufferId)
{
if (_byWaveId.TryGetValue(waveId, out var entry))
{
bufferId = entry.BufferId;
return true;
}
bufferId = 0;
return false;
}
///
/// Record a freshly-allocated, freshly-uploaded buffer as resident and
/// most-recently-used.
///
public void RecordCreated(uint waveId, uint bufferId, long bytes)
{
long charged = Math.Max(1L, bytes);
_byWaveId[waveId] = new Entry
{
WaveId = waveId,
BufferId = bufferId,
Bytes = charged,
LastUseTick = ++_tick,
};
ResidentBytes += charged;
}
///
/// Bump an already-resident buffer's LRU order — call on every replay
/// (cache hit), not just on first creation.
///
public void Touch(uint waveId)
{
if (_byWaveId.TryGetValue(waveId, out var entry))
entry.LastUseTick = ++_tick;
}
///
/// Evict the single least-recently-used resident entry whose buffer id
/// is NOT reported as protected by
/// (still attached to a live AL source, or otherwise off-limits for
/// this call — e.g. a buffer just created and not yet handed to the
/// caller). Returns false when every resident entry is currently
/// protected, meaning the caller should stop evicting and accept a
/// transient budget overage rather than loop forever.
///
public bool TryEvictOldestUnprotected(
Func isProtected,
out uint evictedWaveId,
out uint evictedBufferId)
{
ArgumentNullException.ThrowIfNull(isProtected);
Entry? victim = null;
foreach (Entry candidate in _byWaveId.Values)
{
if (isProtected(candidate.BufferId))
continue;
if (victim is null || candidate.LastUseTick < victim.LastUseTick)
victim = candidate;
}
if (victim is null)
{
evictedWaveId = 0;
evictedBufferId = 0;
return false;
}
_byWaveId.Remove(victim.WaveId);
ResidentBytes -= victim.Bytes;
evictedWaveId = victim.WaveId;
evictedBufferId = victim.BufferId;
return true;
}
}