diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.SharedTextures.cs b/Dalamud/Interface/Textures/Internal/TextureManager.SharedTextures.cs index 156ffa56f..d7e185b68 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.SharedTextures.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.SharedTextures.cs @@ -4,6 +4,8 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; using BitFaster.Caching.Lru; @@ -65,12 +67,21 @@ internal sealed partial class TextureManager private readonly ConcurrentDictionary<(Assembly, string), SharedImmediateTexture> manifestResourceDict = new(); private readonly HashSet invalidatedTextures = new(); + private readonly Thread sharedTextureReleaseThread; + + private readonly CancellationTokenSource disposingCancellationTokenSource = new(); + /// Initializes a new instance of the class. /// An instance of . public SharedTextureManager(TextureManager textureManager) { this.textureManager = textureManager; - this.textureManager.framework.Update += this.FrameworkOnUpdate; + + this.sharedTextureReleaseThread = new(this.ReleaseSharedTextures) + { + Priority = ThreadPriority.Lowest, + }; + this.sharedTextureReleaseThread.Start(); } /// Gets all the loaded textures from game resources. @@ -90,14 +101,20 @@ internal sealed partial class TextureManager Justification = "Debug use only; users are expected to lock around this")] public ICollection ForDebugInvalidatedTextures => this.invalidatedTextures; + private SharedTextureManager NonDisposed => + this.disposingCancellationTokenSource.IsCancellationRequested + ? throw new ObjectDisposedException(nameof(SharedTextureManager)) + : this; + /// public void Dispose() { - this.textureManager.framework.Update -= this.FrameworkOnUpdate; + this.disposingCancellationTokenSource.Cancel(); this.lookupCache.Clear(); ReleaseSelfReferences(this.gameDict); ReleaseSelfReferences(this.fileDict); ReleaseSelfReferences(this.manifestResourceDict); + this.sharedTextureReleaseThread.Join(); return; static void ReleaseSelfReferences(ConcurrentDictionary dict) @@ -111,12 +128,14 @@ internal sealed partial class TextureManager /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public SharedImmediateTexture.PureImpl GetFromGameIcon(in GameIconLookup lookup) => - this.GetFromGame(this.lookupCache.GetOrAdd(lookup, this.GetIconPathByValue)); + this.NonDisposed.GetFromGame(this.lookupCache.GetOrAdd(lookup, this.GetIconPathByValue)); /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool TryGetFromGameIcon(in GameIconLookup lookup, [NotNullWhen(true)] out SharedImmediateTexture.PureImpl? texture) { + ObjectDisposedException.ThrowIf(this.disposingCancellationTokenSource.IsCancellationRequested, this); + texture = null; if (!this.lookupCache.TryGet(lookup, out var path)) @@ -134,29 +153,29 @@ internal sealed partial class TextureManager /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public SharedImmediateTexture.PureImpl GetFromGame(string path) => - this.gameDict.GetOrAdd(path, GamePathSharedImmediateTexture.CreatePlaceholder) + this.NonDisposed.gameDict.GetOrAdd(path, GamePathSharedImmediateTexture.CreatePlaceholder) .PublicUseInstance; /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public SharedImmediateTexture.PureImpl GetFromFile(string path) => - this.GetFromFile(new FileInfo(path)); + this.NonDisposed.GetFromFile(new FileInfo(path)); /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public SharedImmediateTexture.PureImpl GetFromFile(FileInfo file) => - this.GetFromFileAbsolute(file.FullName); + this.NonDisposed.GetFromFileAbsolute(file.FullName); /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public SharedImmediateTexture.PureImpl GetFromFileAbsolute(string fullPath) => - this.fileDict.GetOrAdd(fullPath, FileSystemSharedImmediateTexture.CreatePlaceholder) + this.NonDisposed.fileDict.GetOrAdd(fullPath, FileSystemSharedImmediateTexture.CreatePlaceholder) .PublicUseInstance; /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public SharedImmediateTexture.PureImpl GetFromManifestResource(Assembly assembly, string name) => - this.manifestResourceDict.GetOrAdd( + this.NonDisposed.manifestResourceDict.GetOrAdd( (assembly, name), ManifestResourceSharedImmediateTexture.CreatePlaceholder) .PublicUseInstance; @@ -166,6 +185,9 @@ internal sealed partial class TextureManager /// The path to invalidate. public void FlushFromGameCache(string path) { + if (this.disposingCancellationTokenSource.IsCancellationRequested) + return; + if (this.gameDict.TryRemove(path, out var r)) { if (r.ReleaseSelfReference(true) != 0) @@ -178,19 +200,33 @@ internal sealed partial class TextureManager [MethodImpl(MethodImplOptions.AggressiveInlining)] private string GetIconPathByValue(GameIconLookup lookup) => - this.textureManager.TryGetIconPath(lookup, out var path) ? path : throw new IconNotFoundException(lookup); + this.NonDisposed.textureManager.TryGetIconPath(lookup, out var path) + ? path + : throw new IconNotFoundException(lookup); - private void FrameworkOnUpdate(IFramework unused) + private void ReleaseSharedTextures() { - RemoveFinalReleased(this.gameDict); - RemoveFinalReleased(this.fileDict); - RemoveFinalReleased(this.manifestResourceDict); - - // ReSharper disable once InconsistentlySynchronizedField - if (this.invalidatedTextures.Count != 0) + while (!this.disposingCancellationTokenSource.IsCancellationRequested) { - lock (this.invalidatedTextures) - this.invalidatedTextures.RemoveWhere(TextureFinalReleasePredicate); + RemoveFinalReleased(this.gameDict); + RemoveFinalReleased(this.fileDict); + RemoveFinalReleased(this.manifestResourceDict); + + // ReSharper disable once InconsistentlySynchronizedField + if (this.invalidatedTextures.Count != 0) + { + lock (this.invalidatedTextures) + this.invalidatedTextures.RemoveWhere(TextureFinalReleasePredicate); + } + + try + { + this.textureManager.framework.DelayTicks(60).Wait(this.disposingCancellationTokenSource.Token); + } + catch (Exception) + { + // who cares + } } return; @@ -198,13 +234,13 @@ internal sealed partial class TextureManager [MethodImpl(MethodImplOptions.AggressiveInlining)] static void RemoveFinalReleased(ConcurrentDictionary dict) { - if (!dict.IsEmpty) + if (dict.IsEmpty) + return; + + foreach (var (k, v) in dict) { - foreach (var (k, v) in dict) - { - if (TextureFinalReleasePredicate(v)) - _ = dict.TryRemove(k, out _); - } + if (TextureFinalReleasePredicate(v)) + _ = dict.TryRemove(k, out _); } }