diff --git a/Dalamud/Interface/Internal/SharableTextures/FileSystemSharableTexture.cs b/Dalamud/Interface/Internal/SharableTextures/FileSystemSharableTexture.cs index cae633a84..06184b5ec 100644 --- a/Dalamud/Interface/Internal/SharableTextures/FileSystemSharableTexture.cs +++ b/Dalamud/Interface/Internal/SharableTextures/FileSystemSharableTexture.cs @@ -21,12 +21,15 @@ internal sealed class FileSystemSharableTexture : SharableTexture this.UnderlyingWrap = this.CreateTextureAsync(); } + /// + public override string SourcePathForDebug => this.path; + /// public override string ToString() => $"{nameof(FileSystemSharableTexture)}#{this.InstanceIdForDebug}({this.path})"; /// - protected override void FinalRelease() + protected override void ReleaseResources() { this.DisposeSuppressingWrap = null; _ = this.UnderlyingWrap?.ToContentDisposedTask(true); @@ -34,7 +37,7 @@ internal sealed class FileSystemSharableTexture : SharableTexture } /// - protected override void Revive() => + protected override void ReviveResources() => this.UnderlyingWrap = this.CreateTextureAsync(); private Task CreateTextureAsync() => diff --git a/Dalamud/Interface/Internal/SharableTextures/GamePathSharableTexture.cs b/Dalamud/Interface/Internal/SharableTextures/GamePathSharableTexture.cs index 328377c1b..e58f21c26 100644 --- a/Dalamud/Interface/Internal/SharableTextures/GamePathSharableTexture.cs +++ b/Dalamud/Interface/Internal/SharableTextures/GamePathSharableTexture.cs @@ -25,11 +25,14 @@ internal sealed class GamePathSharableTexture : SharableTexture this.UnderlyingWrap = this.CreateTextureAsync(); } + /// + public override string SourcePathForDebug => this.path; + /// public override string ToString() => $"{nameof(GamePathSharableTexture)}#{this.InstanceIdForDebug}({this.path})"; /// - protected override void FinalRelease() + protected override void ReleaseResources() { this.DisposeSuppressingWrap = null; _ = this.UnderlyingWrap?.ToContentDisposedTask(true); @@ -37,7 +40,7 @@ internal sealed class GamePathSharableTexture : SharableTexture } /// - protected override void Revive() => + protected override void ReviveResources() => this.UnderlyingWrap = this.CreateTextureAsync(); private Task CreateTextureAsync() => diff --git a/Dalamud/Interface/Internal/SharableTextures/SharableTexture.cs b/Dalamud/Interface/Internal/SharableTextures/SharableTexture.cs index 9c4b43f66..c08cdb7e9 100644 --- a/Dalamud/Interface/Internal/SharableTextures/SharableTexture.cs +++ b/Dalamud/Interface/Internal/SharableTextures/SharableTexture.cs @@ -18,6 +18,7 @@ internal abstract class SharableTexture : IRefCountable private readonly object reviveLock = new(); + private bool resourceReleased; private int refCount; private long selfReferenceExpiry; private IDalamudTextureWrap? availableOnAccessWrapForApi9; @@ -30,21 +31,8 @@ internal abstract class SharableTexture : IRefCountable this.InstanceIdForDebug = Interlocked.Increment(ref instanceCounter); this.refCount = 1; this.selfReferenceExpiry = Environment.TickCount64 + SelfReferenceDurationTicks; - this.ReviveEnabled = true; } - /// - /// Gets a weak reference to an object that demands this objects to be alive. - /// - /// - /// TextureManager must keep references to all shared textures, regardless of whether textures' contents are - /// flushed, because API9 functions demand that the returned textures may be stored so that they can used anytime, - /// possibly reviving a dead-inside object. The object referenced by this property is given out to such use cases, - /// which gets created from . If this no longer points to an alive - /// object, and is null, then this object is not used from API9 use case. - /// - public WeakReference? RevivalPossibility { get; private set; } - /// /// Gets the instance ID. Debug use only. /// @@ -63,6 +51,16 @@ internal abstract class SharableTexture : IRefCountable /// public int RefCountForDebug => this.refCount; + /// + /// Gets the source path. Debug use only. + /// + public abstract string SourcePathForDebug { get; } + + /// + /// Gets a value indicating whether this instance of supports revival. + /// + public bool HasRevivalPossibility => this.RevivalPossibility?.TryGetTarget(out _) is true; + /// /// Gets or sets the underlying texture wrap. /// @@ -74,9 +72,16 @@ internal abstract class SharableTexture : IRefCountable protected DisposeSuppressingTextureWrap? DisposeSuppressingWrap { get; set; } /// - /// Gets a value indicating whether this instance of supports reviving. + /// Gets or sets a weak reference to an object that demands this objects to be alive. /// - protected bool ReviveEnabled { get; private set; } + /// + /// TextureManager must keep references to all shared textures, regardless of whether textures' contents are + /// flushed, because API9 functions demand that the returned textures may be stored so that they can used anytime, + /// possibly reviving a dead-inside object. The object referenced by this property is given out to such use cases, + /// which gets created from . If this no longer points to an alive + /// object, and is null, then this object is not used from API9 use case. + /// + private WeakReference? RevivalPossibility { get; set; } /// public int AddRef() => this.TryAddRef(out var newRefCount) switch @@ -96,8 +101,35 @@ internal abstract class SharableTexture : IRefCountable return newRefCount; case IRefCountable.RefCountResult.FinalRelease: - this.FinalRelease(); - return newRefCount; + // This case may not be entered while TryAddRef is in progress. + // Note that IRefCountable.AlterRefCount guarantees that either TAR or Release will be called for one + // generation of refCount; they never are called together for the same generation of refCount. + // If TAR is called when refCount >= 1, and then Release is called, case StillAlive will be run. + // If TAR is called when refCount == 0, and then Release is called: + // ... * if TAR was done: case FinalRelease will be run. + // ... * if TAR was not done: case AlreadyDisposed will be run. + // ... Because refCount will be altered as the last step of TAR. + // If Release is called when refCount == 1, and then TAR is called, + // ... the resource may be released yet, so TAR waits for resourceReleased inside reviveLock, + // ... while Release releases the underlying resource and then sets resourceReleased inside reviveLock. + // ... Once that's done, TAR may revive the object safely. + while (true) + { + lock (this.reviveLock) + { + if (this.resourceReleased) + { + // I cannot think of a case that the code entering this code block, but just in case. + Thread.Yield(); + continue; + } + + this.ReleaseResources(); + this.resourceReleased = true; + + return newRefCount; + } + } case IRefCountable.RefCountResult.AlreadyDisposed: throw new ObjectDisposedException(nameof(SharableTexture)); @@ -108,16 +140,22 @@ internal abstract class SharableTexture : IRefCountable } /// - /// Releases self-reference, if it should expire. + /// Releases self-reference, if conditions are met. /// + /// If set to true, the self-reference will be released immediately. /// Number of the new reference count that may or may not have changed. - public int ReleaseSelfReferenceIfExpired() + public int ReleaseSelfReference(bool immediate) { while (true) { var exp = this.selfReferenceExpiry; - if (exp > Environment.TickCount64) - return this.refCount; + switch (immediate) + { + case false when exp > Environment.TickCount64: + return this.refCount; + case true when exp == SelfReferenceExpiryExpired: + return this.refCount; + } if (exp != Interlocked.CompareExchange(ref this.selfReferenceExpiry, SelfReferenceExpiryExpired, exp)) continue; @@ -127,28 +165,6 @@ internal abstract class SharableTexture : IRefCountable } } - /// - /// Disables revival. - /// - public void DisableReviveAndReleaseSelfReference() - { - this.ReviveEnabled = false; - - while (true) - { - var exp = this.selfReferenceExpiry; - if (exp == SelfReferenceExpiryExpired) - return; - - if (exp != Interlocked.CompareExchange(ref this.selfReferenceExpiry, SelfReferenceExpiryExpired, exp)) - continue; - - this.availableOnAccessWrapForApi9 = null; - this.Release(); - break; - } - } - /// /// Gets the texture if immediately available. The texture is guarnateed to be available for the rest of the frame. /// Invocation from non-main thread will exhibit an undefined behavior. @@ -233,31 +249,41 @@ internal abstract class SharableTexture : IRefCountable /// /// Cleans up this instance of . /// - protected abstract void FinalRelease(); + protected abstract void ReleaseResources(); /// /// Attempts to restore the reference to this texture. /// - protected abstract void Revive(); + protected abstract void ReviveResources(); private IRefCountable.RefCountResult TryAddRef(out int newRefCount) { var alterResult = IRefCountable.AlterRefCount(1, ref this.refCount, out newRefCount); - if (alterResult != IRefCountable.RefCountResult.AlreadyDisposed || !this.ReviveEnabled) + if (alterResult != IRefCountable.RefCountResult.AlreadyDisposed) return alterResult; - lock (this.reviveLock) + + while (true) { - alterResult = IRefCountable.AlterRefCount(1, ref this.refCount, out newRefCount); - if (alterResult != IRefCountable.RefCountResult.AlreadyDisposed) - return alterResult; + lock (this.reviveLock) + { + if (!this.resourceReleased) + { + Thread.Yield(); + continue; + } - this.Revive(); - Interlocked.Increment(ref this.refCount); + alterResult = IRefCountable.AlterRefCount(1, ref this.refCount, out newRefCount); + if (alterResult != IRefCountable.RefCountResult.AlreadyDisposed) + return alterResult; - if (this.RevivalPossibility?.TryGetTarget(out var target) is true) - this.availableOnAccessWrapForApi9 = target; + this.ReviveResources(); + if (this.RevivalPossibility?.TryGetTarget(out var target) is true) + this.availableOnAccessWrapForApi9 = target; - return IRefCountable.RefCountResult.StillAlive; + Interlocked.Increment(ref this.refCount); + this.resourceReleased = false; + return IRefCountable.RefCountResult.StillAlive; + } } } diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index 9081cf14e..d1ab16a1d 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -1,5 +1,6 @@ using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Threading.Tasks; @@ -58,6 +59,7 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid private readonly ConcurrentLru lookupToPath = new(PathLookupLruCount); private readonly ConcurrentDictionary gamePathTextures = new(); private readonly ConcurrentDictionary fileSystemTextures = new(); + private readonly HashSet invalidatedTextures = new(); private bool disposing; @@ -73,12 +75,22 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid /// /// Gets all the loaded textures from the game resources. Debug use only. /// - public IReadOnlyDictionary GamePathTextures => this.gamePathTextures; + public ICollection GamePathTextures => this.gamePathTextures.Values; /// /// Gets all the loaded textures from the game resources. Debug use only. /// - public IReadOnlyDictionary FileSystemTextures => this.fileSystemTextures; + public ICollection FileSystemTextures => this.fileSystemTextures.Values; + + /// + /// Gets all the loaded textures that are invalidated from . Debug use only. + /// + /// lock on use of the value returned from this property. + [SuppressMessage( + "ReSharper", + "InconsistentlySynchronizedField", + Justification = "Debug use only; users are expected to lock around this")] + public ICollection InvalidatedTextures => this.invalidatedTextures; /// public void Dispose() @@ -88,9 +100,9 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid this.disposing = true; foreach (var v in this.gamePathTextures.Values) - v.DisableReviveAndReleaseSelfReference(); + v.ReleaseSelfReference(true); foreach (var v in this.fileSystemTextures.Values) - v.DisableReviveAndReleaseSelfReference(); + v.ReleaseSelfReference(true); this.lookupToPath.Clear(); this.gamePathTextures.Clear(); @@ -142,52 +154,61 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] [Obsolete("See interface definition.")] public IDalamudTextureWrap? GetTextureFromFile(FileInfo file, bool keepAlive = false) => - this.fileSystemTextures.GetOrAdd(file.FullName, CreateFileSystemSharableTexture).GetAvailableOnAccessWrapForApi9(); + this.fileSystemTextures.GetOrAdd(file.FullName, CreateFileSystemSharableTexture) + .GetAvailableOnAccessWrapForApi9(); #pragma warning restore CS0618 // Type or member is obsolete - /// - public bool ImmediateGetStateFromGameIcon(in GameIconLookup lookup, out Exception? exception) => - this.ImmediateGetStateFromGame(this.lookupToPath.GetOrAdd(lookup, this.GetIconPathByValue), out exception); - - /// - public bool ImmediateGetStateFromGame(string path, out Exception? exception) - { - if (!this.gamePathTextures.TryGetValue(path, out var texture)) - { - exception = null; - return false; - } - - exception = texture.UnderlyingWrap?.Exception; - return texture.UnderlyingWrap?.IsCompletedSuccessfully ?? false; - } - - /// - public bool ImmediateGetStateFromFile(string file, out Exception? exception) - { - if (!this.fileSystemTextures.TryGetValue(file, out var texture)) - { - exception = null; - return false; - } - - exception = texture.UnderlyingWrap?.Exception; - return texture.UnderlyingWrap?.IsCompletedSuccessfully ?? false; - } - /// public IDalamudTextureWrap ImmediateGetFromGameIcon(in GameIconLookup lookup) => this.ImmediateGetFromGame(this.lookupToPath.GetOrAdd(lookup, this.GetIconPathByValue)); /// public IDalamudTextureWrap ImmediateGetFromGame(string path) => - this.gamePathTextures.GetOrAdd(path, CreateGamePathSharableTexture).GetImmediate() - ?? this.dalamudAssetManager.Empty4X4; + this.ImmediateTryGetFromGame(path, out var texture, out _) + ? texture + : this.dalamudAssetManager.Empty4X4; /// public IDalamudTextureWrap ImmediateGetFromFile(string file) => - this.fileSystemTextures.GetOrAdd(file, CreateFileSystemSharableTexture).GetImmediate() - ?? this.dalamudAssetManager.Empty4X4; + this.ImmediateTryGetFromFile(file, out var texture, out _) + ? texture + : this.dalamudAssetManager.Empty4X4; + + /// + public bool ImmediateTryGetFromGameIcon( + in GameIconLookup lookup, + [NotNullWhen(true)] out IDalamudTextureWrap? texture, + out Exception? exception) => + this.ImmediateTryGetFromGame( + this.lookupToPath.GetOrAdd(lookup, this.GetIconPathByValue), + out texture, + out exception); + + /// + public bool ImmediateTryGetFromGame( + string path, + [NotNullWhen(true)] out IDalamudTextureWrap? texture, + out Exception? exception) + { + ThreadSafety.AssertMainThread(); + var t = this.gamePathTextures.GetOrAdd(path, CreateGamePathSharableTexture); + texture = t.GetImmediate(); + exception = t.UnderlyingWrap?.Exception; + return texture is not null; + } + + /// + public bool ImmediateTryGetFromFile( + string file, + [NotNullWhen(true)] out IDalamudTextureWrap? texture, + out Exception? exception) + { + ThreadSafety.AssertMainThread(); + var t = this.fileSystemTextures.GetOrAdd(file, CreateFileSystemSharableTexture); + texture = t.GetImmediate(); + exception = t.UnderlyingWrap?.Exception; + return texture is not null; + } /// public Task GetFromGameIconAsync(in GameIconLookup lookup) => @@ -336,7 +357,22 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid foreach (var path in paths) { if (this.gamePathTextures.TryRemove(path, out var r)) - r.DisableReviveAndReleaseSelfReference(); + { + if (r.ReleaseSelfReference(true) != 0 || r.HasRevivalPossibility) + { + lock (this.invalidatedTextures) + this.invalidatedTextures.Add(r); + } + } + + if (this.fileSystemTextures.TryRemove(path, out r)) + { + if (r.ReleaseSelfReference(true) != 0 || r.HasRevivalPossibility) + { + lock (this.invalidatedTextures) + this.invalidatedTextures.Add(r); + } + } } } @@ -359,17 +395,35 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid private void FrameworkOnUpdate(IFramework unused) { - foreach (var (k, v) in this.gamePathTextures) + if (!this.gamePathTextures.IsEmpty) { - if (v.ReleaseSelfReferenceIfExpired() == 0 && v.RevivalPossibility?.TryGetTarget(out _) is not true) - _ = this.gamePathTextures.TryRemove(k, out _); + foreach (var (k, v) in this.gamePathTextures) + { + if (TextureFinalReleasePredicate(v)) + _ = this.gamePathTextures.TryRemove(k, out _); + } } - foreach (var (k, v) in this.fileSystemTextures) + if (!this.fileSystemTextures.IsEmpty) { - if (v.ReleaseSelfReferenceIfExpired() == 0 && v.RevivalPossibility?.TryGetTarget(out _) is not true) - _ = this.fileSystemTextures.TryRemove(k, out _); + foreach (var (k, v) in this.fileSystemTextures) + { + if (TextureFinalReleasePredicate(v)) + _ = this.fileSystemTextures.TryRemove(k, out _); + } } + + // ReSharper disable once InconsistentlySynchronizedField + if (this.invalidatedTextures.Count != 0) + { + lock (this.invalidatedTextures) + this.invalidatedTextures.RemoveWhere(TextureFinalReleasePredicate); + } + + return; + + static bool TextureFinalReleasePredicate(SharableTexture v) => + v.ReleaseSelfReference(false) == 0 && !v.HasRevivalPossibility; } private string GetIconPathByValue(GameIconLookup lookup) => diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs index 37fc958af..f9886dd2c 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs @@ -117,9 +117,8 @@ public class IconBrowserWidget : IDataWindowWidget try { var cursor = ImGui.GetCursorScreenPos(); - var texture = texm.ImmediateGetFromGameIcon(new((uint)iconId)); - if (texm.ImmediateGetStateFromGameIcon(new((uint)iconId), out var exc) || exc is null) + if (texm.ImmediateTryGetFromGameIcon(new((uint)iconId), out var texture, out var exc)) { ImGui.Image(texture.ImGuiHandle, this.iconSize); @@ -145,9 +144,9 @@ public class IconBrowserWidget : IDataWindowWidget ImGui.SetTooltip(iconId.ToString()); } } - else + else if (exc is not null) { - // This texture was null, draw nothing, and prevent from trying to show it again. + // This texture failed to load; draw nothing, and prevent from trying to show it again. this.nullValues.Add(iconId); } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs index 351957974..f97fd040f 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -3,6 +3,7 @@ using System.IO; using System.Numerics; using System.Threading.Tasks; +using Dalamud.Interface.Components; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Internal.SharableTextures; using Dalamud.Interface.Utility; @@ -62,15 +63,26 @@ internal class TexWidget : IDataWindowWidget GC.Collect(); ImGui.PushID("loadedGameTextures"); - if (ImGui.CollapsingHeader($"Loaded Game Textures: {this.textureManager.GamePathTextures.Count}###header")) + if (ImGui.CollapsingHeader($"Loaded Game Textures: {this.textureManager.GamePathTextures.Count:g}###header")) this.DrawLoadedTextures(this.textureManager.GamePathTextures); ImGui.PopID(); ImGui.PushID("loadedFileTextures"); - if (ImGui.CollapsingHeader($"Loaded File Textures: {this.textureManager.FileSystemTextures.Count}###header")) + if (ImGui.CollapsingHeader($"Loaded File Textures: {this.textureManager.FileSystemTextures.Count:g}###header")) this.DrawLoadedTextures(this.textureManager.FileSystemTextures); ImGui.PopID(); + lock (this.textureManager.InvalidatedTextures) + { + ImGui.PushID("invalidatedTextures"); + if (ImGui.CollapsingHeader($"Invalidated: {this.textureManager.InvalidatedTextures.Count:g}###header")) + { + this.DrawLoadedTextures(this.textureManager.InvalidatedTextures); + } + + ImGui.PopID(); + } + if (ImGui.CollapsingHeader("Load Game File by Icon ID", ImGuiTreeNodeFlags.DefaultOpen)) this.DrawIconInput(); @@ -133,18 +145,33 @@ internal class TexWidget : IDataWindowWidget } } - private unsafe void DrawLoadedTextures(IReadOnlyDictionary textures) + private unsafe void DrawLoadedTextures(ICollection textures) { + var im = Service.Get(); if (!ImGui.BeginTable("##table", 6)) return; + const int numIcons = 3; + float iconWidths; + using (im.IconFontHandle?.Push()) + { + iconWidths = ImGui.CalcTextSize(FontAwesomeIcon.Image.ToIconString()).X; + iconWidths += ImGui.CalcTextSize(FontAwesomeIcon.Sync.ToIconString()).X; + iconWidths += ImGui.CalcTextSize(FontAwesomeIcon.Trash.ToIconString()).X; + } + ImGui.TableSetupScrollFreeze(0, 1); ImGui.TableSetupColumn("ID", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("000000").X); - ImGui.TableSetupColumn("Preview", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("Preview_").X); ImGui.TableSetupColumn("Source", ImGuiTableColumnFlags.WidthStretch); ImGui.TableSetupColumn("RefCount", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("RefCount__").X); ImGui.TableSetupColumn("SelfRef", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("00.000___").X); ImGui.TableSetupColumn("CanRevive", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("CanRevive__").X); + ImGui.TableSetupColumn( + "Actions", + ImGuiTableColumnFlags.WidthFixed, + iconWidths + + (ImGui.GetStyle().FramePadding.X * 2 * numIcons) + + (ImGui.GetStyle().ItemSpacing.X * 1 * numIcons)); ImGui.TableHeadersRow(); var clipper = new ImGuiListClipperPtr(ImGuiNative.ImGuiListClipper_ImGuiListClipper()); @@ -168,20 +195,37 @@ internal class TexWidget : IDataWindowWidget if (!valid) break; - var (key, texture) = enu.Current; ImGui.TableNextRow(); + if (enu.Current is not { } texture) + { + // Should not happen + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("?"); + continue; + } + + var remain = texture.SelfReferenceExpiresInForDebug; + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); this.TextRightAlign($"{texture.InstanceIdForDebug:n0}"); ImGui.TableNextColumn(); - ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero); - ImGui.PushStyleColor(ImGuiCol.Button, Vector4.Zero); - ImGui.PushStyleColor(ImGuiCol.ButtonActive, Vector4.Zero); - ImGui.PushStyleColor(ImGuiCol.ButtonHovered, Vector4.Zero); - ImGui.Button("Hover"); - ImGui.PopStyleColor(3); - ImGui.PopStyleVar(); + this.TextCopiable(texture.SourcePathForDebug, true); + + ImGui.TableNextColumn(); + this.TextRightAlign($"{texture.RefCountForDebug:n0}"); + + ImGui.TableNextColumn(); + this.TextRightAlign(remain <= 0 ? "-" : $"{remain:00.000}"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted(texture.HasRevivalPossibility ? "Yes" : "No"); + + ImGui.TableNextColumn(); + ImGuiComponents.IconButton(FontAwesomeIcon.Image); if (ImGui.IsItemHovered() && texture.GetImmediate() is { } immediate) { ImGui.BeginTooltip(); @@ -189,18 +233,21 @@ internal class TexWidget : IDataWindowWidget ImGui.EndTooltip(); } - ImGui.TableNextColumn(); - this.TextCopiable(key); + ImGui.SameLine(); + if (ImGuiComponents.IconButton(FontAwesomeIcon.Sync)) + this.textureManager.InvalidatePaths(new[] { texture.SourcePathForDebug }); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip($"Call {nameof(ITextureSubstitutionProvider.InvalidatePaths)}."); - ImGui.TableNextColumn(); - this.TextRightAlign($"{texture.RefCountForDebug:n0}"); - - ImGui.TableNextColumn(); - var remain = texture.SelfReferenceExpiresInForDebug; - this.TextRightAlign(remain <= 0 ? "-" : $"{remain:00.000}"); - - ImGui.TableNextColumn(); - ImGui.TextUnformatted(texture.RevivalPossibility?.TryGetTarget(out _) is true ? "Yes" : "No"); + ImGui.SameLine(); + if (remain <= 0) + ImGui.BeginDisabled(); + if (ImGuiComponents.IconButton(FontAwesomeIcon.Trash)) + texture.ReleaseSelfReference(true); + if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) + ImGui.SetTooltip("Release self-reference immediately."); + if (remain <= 0) + ImGui.EndDisabled(); } if (!valid) diff --git a/Dalamud/Plugin/Services/ITextureProvider.cs b/Dalamud/Plugin/Services/ITextureProvider.cs index 8ee724fd5..8441ca3dc 100644 --- a/Dalamud/Plugin/Services/ITextureProvider.cs +++ b/Dalamud/Plugin/Services/ITextureProvider.cs @@ -11,26 +11,23 @@ namespace Dalamud.Plugin.Services; /// /// Service that grants you access to textures you may render via ImGui. /// +/// +/// Immediate functions
+/// Immediate functions do not throw, unless they are called outside the UI thread.
+/// Instances of returned from Immediate functions do not have to be disposed. +/// Calling on them is a no-op; it is safe to call +/// on them, as it will do nothing.
+/// Use and alike if you don't care about the load state and dimensions of the +/// texture to be loaded. These functions will return a valid texture that is empty (fully transparent).
+/// Use and alike if you do. These functions will return the load state, +/// loaded texture if available, and the load exception on failure.
+///
+/// All other functions
+/// Instances of or <> +/// returned from all other functions must be d after use. +///
public partial interface ITextureProvider { - /// Gets the state of the background load task for . - /// The icon specifier. - /// The exception, if failed. - /// true if loaded; false if not fully loaded or failed. - public bool ImmediateGetStateFromGameIcon(in GameIconLookup lookup, out Exception? exception); - - /// Gets the state of the background load task for . - /// The game-internal path to a .tex, .atex, or an image file such as .png. - /// The exception, if failed. - /// true if loaded; false if not fully loaded or failed. - public bool ImmediateGetStateFromGame(string path, out Exception? exception); - - /// Gets the state of the background load task for . - /// The filesystem path to a .tex, .atex, or an image file such as .png. - /// The exception, if failed. - /// true if loaded; false if not fully loaded or failed. - public bool ImmediateGetStateFromFile(string file, out Exception? exception); - /// Gets the corresponding game icon for use with the current frame. /// The icon specifier. /// An instance of that is guaranteed to be available for the current @@ -61,6 +58,49 @@ public partial interface ITextureProvider /// empty texture instead. /// Thrown when called outside the UI thread. public IDalamudTextureWrap ImmediateGetFromFile(string file); + + /// Gets the corresponding game icon for use with the current frame. + /// The icon specifier. + /// An instance of that is guaranteed to be available for + /// the current frame being drawn, or null if texture is not loaded (yet). + /// The load exception, if any. + /// true if points to the loaded texture; false if the texture is + /// still being loaded, or the load has failed. + /// on the returned will be ignored. + /// Thrown when called outside the UI thread. + public bool ImmediateTryGetFromGameIcon( + in GameIconLookup lookup, + [NotNullWhen(true)] out IDalamudTextureWrap? texture, + out Exception? exception); + + /// Gets a texture from a file shipped as a part of the game resources for use with the current frame. + /// + /// The game-internal path to a .tex, .atex, or an image file such as .png. + /// An instance of that is guaranteed to be available for + /// the current frame being drawn, or null if texture is not loaded (yet). + /// The load exception, if any. + /// true if points to the loaded texture; false if the texture is + /// still being loaded, or the load has failed. + /// on the returned will be ignored. + /// Thrown when called outside the UI thread. + public bool ImmediateTryGetFromGame( + string path, + [NotNullWhen(true)] out IDalamudTextureWrap? texture, + out Exception? exception); + + /// Gets a texture from a file on the filesystem for use with the current frame. + /// The filesystem path to a .tex, .atex, or an image file such as .png. + /// An instance of that is guaranteed to be available for + /// the current frame being drawn, or null if texture is not loaded (yet). + /// The load exception, if any. + /// true if points to the loaded texture; false if the texture is + /// still being loaded, or the load has failed. + /// on the returned will be ignored. + /// Thrown when called outside the UI thread. + public bool ImmediateTryGetFromFile( + string file, + [NotNullWhen(true)] out IDalamudTextureWrap? texture, + out Exception? exception); /// Gets the corresponding game icon for use with the current frame. /// The icon specifier. diff --git a/Dalamud/Plugin/Services/ITextureSubstitutionProvider.cs b/Dalamud/Plugin/Services/ITextureSubstitutionProvider.cs index 3ddd7d13e..371fbaf0f 100644 --- a/Dalamud/Plugin/Services/ITextureSubstitutionProvider.cs +++ b/Dalamud/Plugin/Services/ITextureSubstitutionProvider.cs @@ -33,5 +33,8 @@ public interface ITextureSubstitutionProvider /// and paths that are newly substituted. ///
/// The paths with a changed substitution status. + /// + /// This function will not invalidate the copies of the textures loaded from plugins. + /// public void InvalidatePaths(IEnumerable paths); }