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; } }