using System; using System.Collections.Generic; using System.Collections.Concurrent; using System.Threading; namespace AcDream.Content; /// /// Thread-safe, byte-bounded residency for canonical decoded texture pixels. /// Mesh extraction may run on several workers, so cached arrays are immutable /// after admission and callers clone them before applying surface-local alpha. /// internal sealed class DecodedTextureCache { private sealed record Entry(DecodedTextureKey Key, byte[] Pixels); private readonly object _gate = new(); private readonly Dictionary> _entries = new(); private readonly LinkedList _lru = new(); private readonly ConcurrentDictionary> _inflight = new(); private readonly long _maximumBytes; private readonly int _maximumEntries; private long _residentBytes; // Hit/miss/eviction traffic counters (2026-07-24 measurement-tooling // review). Read via Stats without the _gate lock, so plain fields with // Interlocked increments rather than lock-protected longs. private long _hits; private long _misses; private long _evictions; /// Cumulative hit/miss/eviction counts since construction. public CacheStats Stats => new( Interlocked.Read(ref _hits), Interlocked.Read(ref _misses), Interlocked.Read(ref _evictions)); public DecodedTextureCache(long maximumBytes, int maximumEntries) { ArgumentOutOfRangeException.ThrowIfNegative(maximumBytes); ArgumentOutOfRangeException.ThrowIfNegative(maximumEntries); _maximumBytes = maximumBytes; _maximumEntries = maximumEntries; } public int Count { get { lock (_gate) { return _entries.Count; } } } public long ResidentBytes { get { lock (_gate) { return _residentBytes; } } } public bool TryGet(DecodedTextureKey key, out byte[] pixels) { lock (_gate) { if (!_entries.TryGetValue(key, out var node)) { Interlocked.Increment(ref _misses); pixels = null!; return false; } Interlocked.Increment(ref _hits); Touch(node); pixels = node.Value.Pixels; return true; } } /// /// Returns the canonical pixels for . If another /// worker admitted the same key first, its array wins. /// reports whether the returned array is retained and therefore immutable. /// public byte[] RetainOrUse(DecodedTextureKey key, byte[] pixels, out bool isCached) { ArgumentNullException.ThrowIfNull(pixels); lock (_gate) { if (_entries.TryGetValue(key, out var existing)) { Touch(existing); isCached = true; return existing.Value.Pixels; } if (_maximumEntries == 0 || pixels.LongLength > _maximumBytes) { isCached = false; return pixels; } while (_lru.First is { } oldest && (_entries.Count >= _maximumEntries || _residentBytes + pixels.LongLength > _maximumBytes)) { Remove(oldest); } var node = _lru.AddLast(new Entry(key, pixels)); _entries.Add(key, node); _residentBytes += pixels.LongLength; isCached = true; return pixels; } } /// /// Decode a missing key once across concurrent mesh workers. The expensive /// factory runs outside the LRU lock and unrelated keys proceed in parallel. /// A failed or oversized decode is not stranded as permanent in-flight state. /// public byte[] GetOrCreate( DecodedTextureKey key, Func factory, out bool isCached) { ArgumentNullException.ThrowIfNull(factory); if (TryGet(key, out byte[] cached)) { isCached = true; return cached; } var candidate = new Lazy( factory, LazyThreadSafetyMode.ExecutionAndPublication); Lazy shared = _inflight.GetOrAdd(key, candidate); try { byte[] decoded = shared.Value; return RetainOrUse(key, decoded, out isCached); } finally { _inflight.TryRemove( new KeyValuePair>(key, shared)); } } private void Touch(LinkedListNode node) { if (node != _lru.Last) { _lru.Remove(node); _lru.AddLast(node); } } private void Remove(LinkedListNode node) { _lru.Remove(node); _entries.Remove(node.Value.Key); _residentBytes -= node.Value.Pixels.LongLength; Interlocked.Increment(ref _evictions); } } /// /// Every input that can change decoded RGBA belongs in the cache identity. /// INDEX16/P8 clip-map conversion and A8 additive conversion are surface /// dependent even when they reference the same RenderSurface DID. /// internal readonly record struct DecodedTextureKey( uint RenderSurfaceId, bool IsClipMap, bool IsAdditive);