Make state+texture retrieval done with one API call

This commit is contained in:
Soreepeong 2024-02-22 14:14:52 +09:00
parent 71bb02347f
commit e12b2f7803
8 changed files with 325 additions and 150 deletions

View file

@ -21,12 +21,15 @@ internal sealed class FileSystemSharableTexture : SharableTexture
this.UnderlyingWrap = this.CreateTextureAsync();
}
/// <inheritdoc/>
public override string SourcePathForDebug => this.path;
/// <inheritdoc/>
public override string ToString() =>
$"{nameof(FileSystemSharableTexture)}#{this.InstanceIdForDebug}({this.path})";
/// <inheritdoc/>
protected override void FinalRelease()
protected override void ReleaseResources()
{
this.DisposeSuppressingWrap = null;
_ = this.UnderlyingWrap?.ToContentDisposedTask(true);
@ -34,7 +37,7 @@ internal sealed class FileSystemSharableTexture : SharableTexture
}
/// <inheritdoc/>
protected override void Revive() =>
protected override void ReviveResources() =>
this.UnderlyingWrap = this.CreateTextureAsync();
private Task<IDalamudTextureWrap> CreateTextureAsync() =>

View file

@ -25,11 +25,14 @@ internal sealed class GamePathSharableTexture : SharableTexture
this.UnderlyingWrap = this.CreateTextureAsync();
}
/// <inheritdoc/>
public override string SourcePathForDebug => this.path;
/// <inheritdoc/>
public override string ToString() => $"{nameof(GamePathSharableTexture)}#{this.InstanceIdForDebug}({this.path})";
/// <inheritdoc/>
protected override void FinalRelease()
protected override void ReleaseResources()
{
this.DisposeSuppressingWrap = null;
_ = this.UnderlyingWrap?.ToContentDisposedTask(true);
@ -37,7 +40,7 @@ internal sealed class GamePathSharableTexture : SharableTexture
}
/// <inheritdoc/>
protected override void Revive() =>
protected override void ReviveResources() =>
this.UnderlyingWrap = this.CreateTextureAsync();
private Task<IDalamudTextureWrap> CreateTextureAsync() =>

View file

@ -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;
}
/// <summary>
/// Gets a weak reference to an object that demands this objects to be alive.
/// </summary>
/// <remarks>
/// 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 <see cref="GetAvailableOnAccessWrapForApi9"/>. If this no longer points to an alive
/// object, and <see cref="availableOnAccessWrapForApi9"/> is null, then this object is not used from API9 use case.
/// </remarks>
public WeakReference<IDalamudTextureWrap>? RevivalPossibility { get; private set; }
/// <summary>
/// Gets the instance ID. Debug use only.
/// </summary>
@ -63,6 +51,16 @@ internal abstract class SharableTexture : IRefCountable
/// </summary>
public int RefCountForDebug => this.refCount;
/// <summary>
/// Gets the source path. Debug use only.
/// </summary>
public abstract string SourcePathForDebug { get; }
/// <summary>
/// Gets a value indicating whether this instance of <see cref="SharableTexture"/> supports revival.
/// </summary>
public bool HasRevivalPossibility => this.RevivalPossibility?.TryGetTarget(out _) is true;
/// <summary>
/// Gets or sets the underlying texture wrap.
/// </summary>
@ -74,9 +72,16 @@ internal abstract class SharableTexture : IRefCountable
protected DisposeSuppressingTextureWrap? DisposeSuppressingWrap { get; set; }
/// <summary>
/// Gets a value indicating whether this instance of <see cref="SharableTexture"/> supports reviving.
/// Gets or sets a weak reference to an object that demands this objects to be alive.
/// </summary>
protected bool ReviveEnabled { get; private set; }
/// <remarks>
/// 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 <see cref="GetAvailableOnAccessWrapForApi9"/>. If this no longer points to an alive
/// object, and <see cref="availableOnAccessWrapForApi9"/> is null, then this object is not used from API9 use case.
/// </remarks>
private WeakReference<IDalamudTextureWrap>? RevivalPossibility { get; set; }
/// <inheritdoc/>
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
}
/// <summary>
/// Releases self-reference, if it should expire.
/// Releases self-reference, if conditions are met.
/// </summary>
/// <param name="immediate">If set to <c>true</c>, the self-reference will be released immediately.</param>
/// <returns>Number of the new reference count that may or may not have changed.</returns>
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
}
}
/// <summary>
/// Disables revival.
/// </summary>
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;
}
}
/// <summary>
/// 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
/// <summary>
/// Cleans up this instance of <see cref="SharableTexture"/>.
/// </summary>
protected abstract void FinalRelease();
protected abstract void ReleaseResources();
/// <summary>
/// Attempts to restore the reference to this texture.
/// </summary>
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;
}
}
}

View file

@ -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<GameIconLookup, string> lookupToPath = new(PathLookupLruCount);
private readonly ConcurrentDictionary<string, SharableTexture> gamePathTextures = new();
private readonly ConcurrentDictionary<string, SharableTexture> fileSystemTextures = new();
private readonly HashSet<SharableTexture> invalidatedTextures = new();
private bool disposing;
@ -73,12 +75,22 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid
/// <summary>
/// Gets all the loaded textures from the game resources. Debug use only.
/// </summary>
public IReadOnlyDictionary<string, SharableTexture> GamePathTextures => this.gamePathTextures;
public ICollection<SharableTexture> GamePathTextures => this.gamePathTextures.Values;
/// <summary>
/// Gets all the loaded textures from the game resources. Debug use only.
/// </summary>
public IReadOnlyDictionary<string, SharableTexture> FileSystemTextures => this.fileSystemTextures;
public ICollection<SharableTexture> FileSystemTextures => this.fileSystemTextures.Values;
/// <summary>
/// Gets all the loaded textures that are invalidated from <see cref="InvalidatePaths"/>. Debug use only.
/// </summary>
/// <remarks><c>lock</c> on use of the value returned from this property.</remarks>
[SuppressMessage(
"ReSharper",
"InconsistentlySynchronizedField",
Justification = "Debug use only; users are expected to lock around this")]
public ICollection<SharableTexture> InvalidatedTextures => this.invalidatedTextures;
/// <inheritdoc/>
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
/// <inheritdoc/>
public bool ImmediateGetStateFromGameIcon(in GameIconLookup lookup, out Exception? exception) =>
this.ImmediateGetStateFromGame(this.lookupToPath.GetOrAdd(lookup, this.GetIconPathByValue), out exception);
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
public IDalamudTextureWrap ImmediateGetFromGameIcon(in GameIconLookup lookup) =>
this.ImmediateGetFromGame(this.lookupToPath.GetOrAdd(lookup, this.GetIconPathByValue));
/// <inheritdoc/>
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;
/// <inheritdoc/>
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;
/// <inheritdoc/>
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);
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
public Task<IDalamudTextureWrap> 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) =>

View file

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

View file

@ -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<string, SharableTexture> textures)
private unsafe void DrawLoadedTextures(ICollection<SharableTexture> textures)
{
var im = Service<InterfaceManager>.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)

View file

@ -11,26 +11,23 @@ namespace Dalamud.Plugin.Services;
/// <summary>
/// Service that grants you access to textures you may render via ImGui.
/// </summary>
/// <remarks>
/// <b>Immediate functions</b><br />
/// Immediate functions do not throw, unless they are called outside the UI thread.<br />
/// Instances of <see cref="IDalamudTextureWrap"/> returned from Immediate functions do not have to be disposed.
/// Calling <see cref="IDisposable.Dispose"/> on them is a no-op; it is safe to call <see cref="IDisposable.Dispose"/>
/// on them, as it will do nothing.<br />
/// Use <see cref="ImmediateGetFromGameIcon"/> 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).<br />
/// Use <see cref="ImmediateTryGetFromGameIcon"/> and alike if you do. These functions will return the load state,
/// loaded texture if available, and the load exception on failure.<br />
/// <br />
/// <b>All other functions</b><br />
/// Instances of <see cref="IDalamudTextureWrap"/> or <see cref="Task"/>&lt;<see cref="IDalamudTextureWrap"/>&gt;
/// returned from all other functions must be <see cref="IDisposable.Dispose"/>d after use.
/// </remarks>
public partial interface ITextureProvider
{
/// <summary>Gets the state of the background load task for <see cref="ImmediateGetFromGameIcon"/>.</summary>
/// <param name="lookup">The icon specifier.</param>
/// <param name="exception">The exception, if failed.</param>
/// <returns><c>true</c> if loaded; <c>false</c> if not fully loaded or failed.</returns>
public bool ImmediateGetStateFromGameIcon(in GameIconLookup lookup, out Exception? exception);
/// <summary>Gets the state of the background load task for <see cref="ImmediateGetFromGameIcon"/>.</summary>
/// <param name="path">The game-internal path to a .tex, .atex, or an image file such as .png.</param>
/// <param name="exception">The exception, if failed.</param>
/// <returns><c>true</c> if loaded; <c>false</c> if not fully loaded or failed.</returns>
public bool ImmediateGetStateFromGame(string path, out Exception? exception);
/// <summary>Gets the state of the background load task for <see cref="ImmediateGetFromGameIcon"/>.</summary>
/// <param name="file">The filesystem path to a .tex, .atex, or an image file such as .png.</param>
/// <param name="exception">The exception, if failed.</param>
/// <returns><c>true</c> if loaded; <c>false</c> if not fully loaded or failed.</returns>
public bool ImmediateGetStateFromFile(string file, out Exception? exception);
/// <summary>Gets the corresponding game icon for use with the current frame.</summary>
/// <param name="lookup">The icon specifier.</param>
/// <returns>An instance of <see cref="IDalamudTextureWrap"/> that is guaranteed to be available for the current
@ -61,6 +58,49 @@ public partial interface ITextureProvider
/// empty texture instead.</remarks>
/// <exception cref="InvalidOperationException">Thrown when called outside the UI thread.</exception>
public IDalamudTextureWrap ImmediateGetFromFile(string file);
/// <summary>Gets the corresponding game icon for use with the current frame.</summary>
/// <param name="lookup">The icon specifier.</param>
/// <param name="texture">An instance of <see cref="IDalamudTextureWrap"/> that is guaranteed to be available for
/// the current frame being drawn, or <c>null</c> if texture is not loaded (yet).</param>
/// <param name="exception">The load exception, if any.</param>
/// <returns><c>true</c> if <paramref name="texture"/> points to the loaded texture; <c>false</c> if the texture is
/// still being loaded, or the load has failed.</returns>
/// <remarks><see cref="IDisposable.Dispose"/> on the returned <paramref name="texture"/> will be ignored.</remarks>
/// <exception cref="InvalidOperationException">Thrown when called outside the UI thread.</exception>
public bool ImmediateTryGetFromGameIcon(
in GameIconLookup lookup,
[NotNullWhen(true)] out IDalamudTextureWrap? texture,
out Exception? exception);
/// <summary>Gets a texture from a file shipped as a part of the game resources for use with the current frame.
/// </summary>
/// <param name="path">The game-internal path to a .tex, .atex, or an image file such as .png.</param>
/// <param name="texture">An instance of <see cref="IDalamudTextureWrap"/> that is guaranteed to be available for
/// the current frame being drawn, or <c>null</c> if texture is not loaded (yet).</param>
/// <param name="exception">The load exception, if any.</param>
/// <returns><c>true</c> if <paramref name="texture"/> points to the loaded texture; <c>false</c> if the texture is
/// still being loaded, or the load has failed.</returns>
/// <remarks><see cref="IDisposable.Dispose"/> on the returned <paramref name="texture"/> will be ignored.</remarks>
/// <exception cref="InvalidOperationException">Thrown when called outside the UI thread.</exception>
public bool ImmediateTryGetFromGame(
string path,
[NotNullWhen(true)] out IDalamudTextureWrap? texture,
out Exception? exception);
/// <summary>Gets a texture from a file on the filesystem for use with the current frame.</summary>
/// <param name="file">The filesystem path to a .tex, .atex, or an image file such as .png.</param>
/// <param name="texture">An instance of <see cref="IDalamudTextureWrap"/> that is guaranteed to be available for
/// the current frame being drawn, or <c>null</c> if texture is not loaded (yet).</param>
/// <param name="exception">The load exception, if any.</param>
/// <returns><c>true</c> if <paramref name="texture"/> points to the loaded texture; <c>false</c> if the texture is
/// still being loaded, or the load has failed.</returns>
/// <remarks><see cref="IDisposable.Dispose"/> on the returned <paramref name="texture"/> will be ignored.</remarks>
/// <exception cref="InvalidOperationException">Thrown when called outside the UI thread.</exception>
public bool ImmediateTryGetFromFile(
string file,
[NotNullWhen(true)] out IDalamudTextureWrap? texture,
out Exception? exception);
/// <summary>Gets the corresponding game icon for use with the current frame.</summary>
/// <param name="lookup">The icon specifier.</param>

View file

@ -33,5 +33,8 @@ public interface ITextureSubstitutionProvider
/// and paths that are newly substituted.
/// </summary>
/// <param name="paths">The paths with a changed substitution status.</param>
/// <remarks>
/// This function will not invalidate the copies of the textures loaded from plugins.
/// </remarks>
public void InvalidatePaths(IEnumerable<string> paths);
}