using DatReaderWriter.DBObjs; using DatReaderWriter.Lib.IO; using System.Diagnostics.CodeAnalysis; namespace AcDream.Content; /// /// Thread-safe LRU for unpacked DAT objects retained by /// . /// /// /// The underlying remains the sole /// reader and owner of the DAT databases. Entries here are managed object /// references only; eviction therefore releases the adapter's reference and /// never disposes or mutates an object. /// /// DatReaderWriter objects do not expose an exact retained-size contract. The /// byte budget is consequently conservative accounting rather than a hard /// managed-heap limit: known raw texture payloads are charged exactly, while /// other object graphs receive a fixed floor. The independent entry ceiling /// provides the hard bound even when a generated DAT type grows an unmeasured /// child graph. /// internal sealed class BoundedDatObjectCache { internal const int DefaultEntryLimit = 256; internal const long DefaultEstimatedByteLimit = 64L * 1024L * 1024L; private const long UnknownObjectEstimate = 128L * 1024L; private readonly record struct CacheKey(Type ObjectType, uint FileId); private sealed record CacheEntry( CacheKey Key, IDBObj Value, long EstimatedBytes); private readonly int _entryLimit; private readonly long _estimatedByteLimit; private readonly Func _estimateRetainedBytes; private readonly Dictionary> _byKey = new(); private readonly LinkedList _leastRecentlyUsed = new(); private readonly object _gate = new(); private long _estimatedBytes; internal BoundedDatObjectCache( int entryLimit = DefaultEntryLimit, long estimatedByteLimit = DefaultEstimatedByteLimit, Func? estimateRetainedBytes = null) { ArgumentOutOfRangeException.ThrowIfLessThan(entryLimit, 1); ArgumentOutOfRangeException.ThrowIfLessThan(estimatedByteLimit, 1); _entryLimit = entryLimit; _estimatedByteLimit = estimatedByteLimit; _estimateRetainedBytes = estimateRetainedBytes ?? EstimateRetainedBytes; } internal int Count { get { lock (_gate) return _byKey.Count; } } internal long EstimatedBytes { get { lock (_gate) return _estimatedBytes; } } internal bool TryGet(uint fileId, [MaybeNullWhen(false)] out T value) where T : IDBObj { lock (_gate) { if (!_byKey.TryGetValue(new CacheKey(typeof(T), fileId), out var node)) { value = default; return false; } MarkMostRecentlyUsed(node); value = (T)node.Value.Value; return true; } } /// /// Returns the existing canonical object for a key, or admits /// and returns it. An object larger than the /// complete byte budget is served to the caller but is not retained. /// internal T GetOrAdd(uint fileId, T candidate) where T : IDBObj { ArgumentNullException.ThrowIfNull(candidate); var key = new CacheKey(typeof(T), fileId); long estimatedBytes = Math.Max(1L, _estimateRetainedBytes(candidate)); lock (_gate) { if (_byKey.TryGetValue(key, out var existing)) { MarkMostRecentlyUsed(existing); return (T)existing.Value.Value; } if (estimatedBytes > _estimatedByteLimit) return candidate; while (_byKey.Count >= _entryLimit || _estimatedBytes > _estimatedByteLimit - estimatedBytes) { EvictLeastRecentlyUsed(); } var entry = new CacheEntry(key, candidate, estimatedBytes); var node = _leastRecentlyUsed.AddLast(entry); _byKey.Add(key, node); _estimatedBytes += estimatedBytes; return candidate; } } private void MarkMostRecentlyUsed(LinkedListNode node) { if (!ReferenceEquals(node, _leastRecentlyUsed.Last)) { _leastRecentlyUsed.Remove(node); _leastRecentlyUsed.AddLast(node); } } private void EvictLeastRecentlyUsed() { LinkedListNode? node = _leastRecentlyUsed.First; if (node is null) throw new InvalidOperationException("DAT object cache accounting is inconsistent."); _leastRecentlyUsed.RemoveFirst(); _byKey.Remove(node.Value.Key); _estimatedBytes -= node.Value.EstimatedBytes; } private static long EstimateRetainedBytes(IDBObj value) { // RenderSurface is the dominant byte-bearing object on the mesh path. // Charge its unpacked source payload exactly, plus the conservative // floor for the generated object and array overhead. if (value is RenderSurface renderSurface) return Math.Max(UnknownObjectEstimate, renderSurface.SourceData.LongLength + 256L); return UnknownObjectEstimate; } }