using AcDream.Content.Pak; using AcDream.Core.Physics; namespace AcDream.Content; public readonly record struct SharedPreparedCollisionCacheSnapshot( int Count, int Capacity, long Hits, long Misses, bool IsDisposed) { public bool IsBounded => Count >= 0 && Count <= Capacity; } /// /// Process-scoped bounded cache over immutable prepared collision payloads. /// The wrapped package remains owned by the caller; disposing this cache only /// retires its decoded references. Sessions retain separate /// instances and share only these immutable /// values. /// public sealed class SharedPreparedCollisionCache : IPreparedCollisionSource { public const int DefaultCapacity = 4096; private readonly object _gate = new(); private readonly IPreparedCollisionSource _source; private readonly int _capacity; private readonly Dictionary> _entries = new(); private readonly LinkedList _lru = new(); private long _probes; private long _reads; private long _loaded; private long _missing; private long _corrupt; private long _hits; private long _misses; private bool _disposed; public SharedPreparedCollisionCache( IPreparedCollisionSource source, int capacity = DefaultCapacity) { _source = source ?? throw new ArgumentNullException(nameof(source)); if (capacity <= 0) throw new ArgumentOutOfRangeException(nameof(capacity)); _capacity = capacity; } public PreparedCollisionSourceStats CollisionStats => new( Interlocked.Read(ref _probes), Interlocked.Read(ref _reads), Interlocked.Read(ref _loaded), Interlocked.Read(ref _missing), Interlocked.Read(ref _corrupt)); public SharedPreparedCollisionCacheSnapshot CaptureSnapshot() { lock (_gate) { return new SharedPreparedCollisionCacheSnapshot( _entries.Count, _capacity, Interlocked.Read(ref _hits), Interlocked.Read(ref _misses), _disposed); } } public PreparedAssetPresence ProbeCollision( PakAssetType type, uint sourceFileId) { PreparedCollisionTypeContract.Validate(type); Interlocked.Increment(ref _probes); lock (_gate) { EnsureNotDisposed(); if (_entries.TryGetValue( new CacheKey(type, sourceFileId), out LinkedListNode? node)) { Touch(node); return node.Value.Status switch { PreparedAssetReadStatus.Loaded => PreparedAssetPresence.Available, PreparedAssetReadStatus.Corrupt => PreparedAssetPresence.Corrupt, _ => PreparedAssetPresence.Missing, }; } return _source.ProbeCollision(type, sourceFileId); } } public PreparedCollisionReadResult ReadGfxObjCollision( uint sourceFileId, CancellationToken cancellationToken = default) => Read( PakAssetType.GfxObjCollision, sourceFileId, token => _source.ReadGfxObjCollision(sourceFileId, token), cancellationToken); public PreparedCollisionReadResult ReadSetupCollision( uint sourceFileId, CancellationToken cancellationToken = default) => Read( PakAssetType.SetupCollision, sourceFileId, token => _source.ReadSetupCollision(sourceFileId, token), cancellationToken); public PreparedCollisionReadResult ReadCellStructureCollision( uint sourceFileId, CancellationToken cancellationToken = default) => Read( PakAssetType.CellStructureCollision, sourceFileId, token => _source.ReadCellStructureCollision( sourceFileId, token), cancellationToken); public PreparedCollisionReadResult ReadEnvCellTopology( uint sourceFileId, CancellationToken cancellationToken = default) => Read( PakAssetType.EnvCellTopology, sourceFileId, token => _source.ReadEnvCellTopology(sourceFileId, token), cancellationToken); public void Dispose() { lock (_gate) { if (_disposed) return; _entries.Clear(); _lru.Clear(); _disposed = true; } } private PreparedCollisionReadResult Read( PakAssetType type, uint sourceFileId, Func> loader, CancellationToken cancellationToken) where T : class { cancellationToken.ThrowIfCancellationRequested(); Interlocked.Increment(ref _reads); lock (_gate) { EnsureNotDisposed(); var key = new CacheKey(type, sourceFileId); if (_entries.TryGetValue( key, out LinkedListNode? node)) { Touch(node); Interlocked.Increment(ref _hits); return CountAndConvert(node.Value); } Interlocked.Increment(ref _misses); PreparedCollisionReadResult loaded = loader(cancellationToken); cancellationToken.ThrowIfCancellationRequested(); var entry = new CacheEntry( key, loaded.Status, loaded.Data); LinkedListNode added = _lru.AddFirst(entry); _entries.Add(key, added); Trim(); return CountAndConvert(entry); } } private PreparedCollisionReadResult CountAndConvert( CacheEntry entry) where T : class { switch (entry.Status) { case PreparedAssetReadStatus.Loaded when entry.Data is T data: Interlocked.Increment(ref _loaded); return PreparedCollisionReadResult.Loaded(data); case PreparedAssetReadStatus.Missing: Interlocked.Increment(ref _missing); return PreparedCollisionReadResult.Missing; case PreparedAssetReadStatus.Corrupt: Interlocked.Increment(ref _corrupt); return PreparedCollisionReadResult.Corrupt; default: throw new InvalidDataException( $"Prepared collision cache entry {entry.Key} has an invalid payload."); } } private void Touch(LinkedListNode node) { if (ReferenceEquals(_lru.First, node)) return; _lru.Remove(node); _lru.AddFirst(node); } private void Trim() { while (_entries.Count > _capacity) { LinkedListNode tail = _lru.Last ?? throw new InvalidOperationException( "Prepared collision LRU lost its terminal entry."); _entries.Remove(tail.Value.Key); _lru.RemoveLast(); } } private void EnsureNotDisposed() => ObjectDisposedException.ThrowIf(_disposed, this); private readonly record struct CacheKey( PakAssetType Type, uint SourceFileId); private sealed record CacheEntry( CacheKey Key, PreparedAssetReadStatus Status, object? Data); }