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);
}