Implement texture load throttling & cancellable async loads

This commit is contained in:
Soreepeong 2024-02-22 16:06:26 +09:00
parent e12b2f7803
commit ea633cd876
7 changed files with 595 additions and 154 deletions

View file

@ -1,3 +1,4 @@
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Utility;
@ -15,15 +16,40 @@ internal sealed class FileSystemSharableTexture : SharableTexture
/// Initializes a new instance of the <see cref="FileSystemSharableTexture"/> class.
/// </summary>
/// <param name="path">The path.</param>
public FileSystemSharableTexture(string path)
/// <param name="holdSelfReference">If set to <c>true</c>, this class will hold a reference to self.
/// Otherwise, it is expected that the caller to hold the reference.</param>
private FileSystemSharableTexture(string path, bool holdSelfReference)
: base(holdSelfReference)
{
this.path = path;
this.UnderlyingWrap = this.CreateTextureAsync();
if (holdSelfReference)
{
this.UnderlyingWrap = Service<TextureLoadThrottler>.Get().CreateLoader(
this,
this.CreateTextureAsync,
this.LoadCancellationToken);
}
}
/// <inheritdoc/>
public override string SourcePathForDebug => this.path;
/// <summary>
/// Creates a new instance of <see cref="GamePathSharableTexture"/>.
/// The new instance will hold a reference to itself.
/// </summary>
/// <param name="path">The path.</param>
/// <returns>The new instance.</returns>
public static SharableTexture CreateImmediate(string path) => new FileSystemSharableTexture(path, true);
/// <summary>
/// Creates a new instance of <see cref="GamePathSharableTexture"/>.
/// The caller is expected to manage ownership of the new instance.
/// </summary>
/// <param name="path">The path.</param>
/// <returns>The new instance.</returns>
public static SharableTexture CreateAsync(string path) => new FileSystemSharableTexture(path, false);
/// <inheritdoc/>
public override string ToString() =>
$"{nameof(FileSystemSharableTexture)}#{this.InstanceIdForDebug}({this.path})";
@ -38,15 +64,16 @@ internal sealed class FileSystemSharableTexture : SharableTexture
/// <inheritdoc/>
protected override void ReviveResources() =>
this.UnderlyingWrap = this.CreateTextureAsync();
this.UnderlyingWrap = Service<TextureLoadThrottler>.Get().CreateLoader(
this,
this.CreateTextureAsync,
this.LoadCancellationToken);
private Task<IDalamudTextureWrap> CreateTextureAsync() =>
Task.Run(
() =>
{
var w = (IDalamudTextureWrap)Service<InterfaceManager>.Get().LoadImage(this.path)
?? throw new("Failed to load image because of an unknown reason.");
this.DisposeSuppressingWrap = new(w);
return w;
});
private Task<IDalamudTextureWrap> CreateTextureAsync(CancellationToken cancellationToken)
{
var w = (IDalamudTextureWrap)Service<InterfaceManager>.Get().LoadImage(this.path)
?? throw new("Failed to load image because of an unknown reason.");
this.DisposeSuppressingWrap = new(w);
return Task.FromResult(w);
}
}

View file

@ -1,4 +1,5 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Data;
@ -19,15 +20,40 @@ internal sealed class GamePathSharableTexture : SharableTexture
/// Initializes a new instance of the <see cref="GamePathSharableTexture"/> class.
/// </summary>
/// <param name="path">The path.</param>
public GamePathSharableTexture(string path)
/// <param name="holdSelfReference">If set to <c>true</c>, this class will hold a reference to self.
/// Otherwise, it is expected that the caller to hold the reference.</param>
private GamePathSharableTexture(string path, bool holdSelfReference)
: base(holdSelfReference)
{
this.path = path;
this.UnderlyingWrap = this.CreateTextureAsync();
if (holdSelfReference)
{
this.UnderlyingWrap = Service<TextureLoadThrottler>.Get().CreateLoader(
this,
this.CreateTextureAsync,
this.LoadCancellationToken);
}
}
/// <inheritdoc/>
public override string SourcePathForDebug => this.path;
/// <summary>
/// Creates a new instance of <see cref="GamePathSharableTexture"/>.
/// The new instance will hold a reference to itself.
/// </summary>
/// <param name="path">The path.</param>
/// <returns>The new instance.</returns>
public static SharableTexture CreateImmediate(string path) => new GamePathSharableTexture(path, true);
/// <summary>
/// Creates a new instance of <see cref="GamePathSharableTexture"/>.
/// The caller is expected to manage ownership of the new instance.
/// </summary>
/// <param name="path">The path.</param>
/// <returns>The new instance.</returns>
public static SharableTexture CreateAsync(string path) => new GamePathSharableTexture(path, false);
/// <inheritdoc/>
public override string ToString() => $"{nameof(GamePathSharableTexture)}#{this.InstanceIdForDebug}({this.path})";
@ -41,17 +67,19 @@ internal sealed class GamePathSharableTexture : SharableTexture
/// <inheritdoc/>
protected override void ReviveResources() =>
this.UnderlyingWrap = this.CreateTextureAsync();
this.UnderlyingWrap = Service<TextureLoadThrottler>.Get().CreateLoader(
this,
this.CreateTextureAsync,
this.LoadCancellationToken);
private Task<IDalamudTextureWrap> CreateTextureAsync() =>
Task.Run(
async () =>
{
var dm = await Service<DataManager>.GetAsync();
var im = await Service<InterfaceManager>.GetAsync();
var file = dm.GetFile<TexFile>(this.path);
var t = (IDalamudTextureWrap)im.LoadImageFromTexFile(file ?? throw new FileNotFoundException());
this.DisposeSuppressingWrap = new(t);
return t;
});
private async Task<IDalamudTextureWrap> CreateTextureAsync(CancellationToken cancellationToken)
{
var dm = await Service<DataManager>.GetAsync();
var im = await Service<InterfaceManager>.GetAsync();
var file = dm.GetFile<TexFile>(this.path);
cancellationToken.ThrowIfCancellationRequested();
var t = (IDalamudTextureWrap)im.LoadImageFromTexFile(file ?? throw new FileNotFoundException());
this.DisposeSuppressingWrap = new(t);
return t;
}
}

View file

@ -9,9 +9,9 @@ namespace Dalamud.Interface.Internal.SharableTextures;
/// <summary>
/// Represents a texture that may have multiple reference holders (owners).
/// </summary>
internal abstract class SharableTexture : IRefCountable
internal abstract class SharableTexture : IRefCountable, TextureLoadThrottler.IThrottleBasisProvider
{
private const int SelfReferenceDurationTicks = 5000;
private const int SelfReferenceDurationTicks = 2000;
private const long SelfReferenceExpiryExpired = long.MaxValue;
private static long instanceCounter;
@ -22,15 +22,24 @@ internal abstract class SharableTexture : IRefCountable
private int refCount;
private long selfReferenceExpiry;
private IDalamudTextureWrap? availableOnAccessWrapForApi9;
private CancellationTokenSource? cancellationTokenSource;
/// <summary>
/// Initializes a new instance of the <see cref="SharableTexture"/> class.
/// </summary>
protected SharableTexture()
/// <param name="holdSelfReference">If set to <c>true</c>, this class will hold a reference to self.
/// Otherwise, it is expected that the caller to hold the reference.</param>
protected SharableTexture(bool holdSelfReference)
{
this.InstanceIdForDebug = Interlocked.Increment(ref instanceCounter);
this.refCount = 1;
this.selfReferenceExpiry = Environment.TickCount64 + SelfReferenceDurationTicks;
this.selfReferenceExpiry =
holdSelfReference
? Environment.TickCount64 + SelfReferenceDurationTicks
: SelfReferenceExpiryExpired;
this.IsOpportunistic = true;
this.FirstRequestedTick = this.LatestRequestedTick = Environment.TickCount64;
this.cancellationTokenSource = new();
}
/// <summary>
@ -66,11 +75,26 @@ internal abstract class SharableTexture : IRefCountable
/// </summary>
public Task<IDalamudTextureWrap>? UnderlyingWrap { get; set; }
/// <inheritdoc/>
public bool IsOpportunistic { get; private set; }
/// <inheritdoc/>
public long FirstRequestedTick { get; private set; }
/// <inheritdoc/>
public long LatestRequestedTick { get; private set; }
/// <summary>
/// Gets or sets the dispose-suppressing wrap for <see cref="UnderlyingWrap"/>.
/// </summary>
protected DisposeSuppressingTextureWrap? DisposeSuppressingWrap { get; set; }
/// <summary>
/// Gets a cancellation token for cancelling load.
/// Intended to be called from implementors' constructors and <see cref="ReviveResources"/>.
/// </summary>
protected CancellationToken LoadCancellationToken => this.cancellationTokenSource?.Token ?? default;
/// <summary>
/// Gets or sets a weak reference to an object that demands this objects to be alive.
/// </summary>
@ -124,6 +148,7 @@ internal abstract class SharableTexture : IRefCountable
continue;
}
this.cancellationTokenSource = null;
this.ReleaseResources();
this.resourceReleased = true;
@ -175,6 +200,7 @@ internal abstract class SharableTexture : IRefCountable
if (this.TryAddRef(out _) != IRefCountable.RefCountResult.StillAlive)
return null;
this.LatestRequestedTick = Environment.TickCount64;
var nexp = Environment.TickCount64 + SelfReferenceDurationTicks;
while (true)
{
@ -197,22 +223,45 @@ internal abstract class SharableTexture : IRefCountable
/// Creates a new reference to this texture. The texture is guaranteed to be available until
/// <see cref="IDisposable.Dispose"/> is called.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The task containing the texture.</returns>
public Task<IDalamudTextureWrap> CreateNewReference()
public async Task<IDalamudTextureWrap> CreateNewReference(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
this.AddRef();
if (this.UnderlyingWrap is null)
throw new InvalidOperationException("AddRef returned but UnderlyingWrap is null?");
return this.UnderlyingWrap.ContinueWith(
r =>
this.IsOpportunistic = false;
this.LatestRequestedTick = Environment.TickCount64;
var uw = this.UnderlyingWrap;
if (cancellationToken != default)
{
while (!uw.IsCompleted)
{
if (r.IsCompletedSuccessfully)
return Task.FromResult((IDalamudTextureWrap)new RefCountableWrappingTextureWrap(r.Result, this));
if (cancellationToken.IsCancellationRequested)
{
this.Release();
throw new OperationCanceledException(cancellationToken);
}
this.Release();
return r;
}).Unwrap();
await Task.WhenAny(uw, Task.Delay(1000000, cancellationToken));
}
}
IDalamudTextureWrap dtw;
try
{
dtw = await uw;
}
catch
{
this.Release();
throw;
}
return new RefCountableWrappingTextureWrap(dtw, this);
}
/// <summary>
@ -233,7 +282,7 @@ internal abstract class SharableTexture : IRefCountable
if (this.RevivalPossibility?.TryGetTarget(out this.availableOnAccessWrapForApi9) is true)
return this.availableOnAccessWrapForApi9;
var newRefTask = this.CreateNewReference();
var newRefTask = this.CreateNewReference(default);
newRefTask.Wait();
if (!newRefTask.IsCompletedSuccessfully)
return null;
@ -276,7 +325,17 @@ internal abstract class SharableTexture : IRefCountable
if (alterResult != IRefCountable.RefCountResult.AlreadyDisposed)
return alterResult;
this.ReviveResources();
this.cancellationTokenSource = new();
try
{
this.ReviveResources();
}
catch
{
this.cancellationTokenSource = null;
throw;
}
if (this.RevivalPossibility?.TryGetTarget(out var target) is true)
this.availableOnAccessWrapForApi9 = target;

View file

@ -0,0 +1,178 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
namespace Dalamud.Interface.Internal.SharableTextures;
/// <summary>
/// Service for managing texture loads.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal class TextureLoadThrottler : IServiceType
{
private readonly List<WorkItem> workList = new();
private readonly List<WorkItem> activeWorkList = new();
[ServiceManager.ServiceConstructor]
private TextureLoadThrottler() =>
this.MaxActiveWorkItems = Math.Min(64, Environment.ProcessorCount);
/// <summary>
/// Basis for throttling.
/// </summary>
internal interface IThrottleBasisProvider
{
/// <summary>
/// Gets a value indicating whether the resource is requested in an opportunistic way.
/// </summary>
bool IsOpportunistic { get; }
/// <summary>
/// Gets the first requested tick count from <see cref="Environment.TickCount64"/>.
/// </summary>
long FirstRequestedTick { get; }
/// <summary>
/// Gets the latest requested tick count from <see cref="Environment.TickCount64"/>.
/// </summary>
long LatestRequestedTick { get; }
}
private int MaxActiveWorkItems { get; }
/// <summary>
/// Creates a texture loader.
/// </summary>
/// <param name="basis">The throttle basis.</param>
/// <param name="immediateLoadFunction">The immediate load function.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The task.</returns>
public Task<IDalamudTextureWrap> CreateLoader(
IThrottleBasisProvider basis,
Func<CancellationToken, Task<IDalamudTextureWrap>> immediateLoadFunction,
CancellationToken cancellationToken)
{
var work = new WorkItem
{
TaskCompletionSource = new(),
Basis = basis,
CancellationToken = cancellationToken,
ImmediateLoadFunction = immediateLoadFunction,
};
_ = Task.Run(
() =>
{
lock (this.workList)
{
this.workList.Add(work);
if (this.activeWorkList.Count >= this.MaxActiveWorkItems)
return;
}
this.ContinueWork();
},
default);
return work.TaskCompletionSource.Task;
}
private void ContinueWork()
{
WorkItem minWork;
lock (this.workList)
{
if (this.workList.Count == 0)
return;
if (this.activeWorkList.Count >= this.MaxActiveWorkItems)
return;
var minIndex = 0;
for (var i = 1; i < this.workList.Count; i++)
{
if (this.workList[i].CompareTo(this.workList[minIndex]) < 0)
minIndex = i;
}
minWork = this.workList[minIndex];
// Avoid shifting; relocate the element to remove to the last
if (minIndex != this.workList.Count - 1)
(this.workList[^1], this.workList[minIndex]) = (this.workList[minIndex], this.workList[^1]);
this.workList.RemoveAt(this.workList.Count - 1);
this.activeWorkList.Add(minWork);
}
try
{
minWork.CancellationToken.ThrowIfCancellationRequested();
minWork.InnerTask = minWork.ImmediateLoadFunction(minWork.CancellationToken);
}
catch (Exception e)
{
minWork.InnerTask = Task.FromException<IDalamudTextureWrap>(e);
}
minWork.InnerTask.ContinueWith(
r =>
{
// Swallow exception, if any
_ = r.Exception;
lock (this.workList)
this.activeWorkList.Remove(minWork);
if (r.IsCompletedSuccessfully)
minWork.TaskCompletionSource.SetResult(r.Result);
else if (r.Exception is not null)
minWork.TaskCompletionSource.SetException(r.Exception);
else if (r.IsCanceled)
minWork.TaskCompletionSource.SetCanceled();
else
minWork.TaskCompletionSource.SetException(new Exception("??"));
this.ContinueWork();
});
}
/// <summary>
/// A read-only implementation of <see cref="IThrottleBasisProvider"/>.
/// </summary>
public class ReadOnlyThrottleBasisProvider : IThrottleBasisProvider
{
/// <inheritdoc/>
public bool IsOpportunistic { get; init; } = false;
/// <inheritdoc/>
public long FirstRequestedTick { get; init; } = Environment.TickCount64;
/// <inheritdoc/>
public long LatestRequestedTick { get; init; } = Environment.TickCount64;
}
[SuppressMessage(
"StyleCop.CSharp.OrderingRules",
"SA1206:Declaration keywords should follow order",
Justification = "no")]
private record WorkItem : IComparable<WorkItem>
{
public required TaskCompletionSource<IDalamudTextureWrap> TaskCompletionSource { get; init; }
public required IThrottleBasisProvider Basis { get; init; }
public required CancellationToken CancellationToken { get; init; }
public required Func<CancellationToken, Task<IDalamudTextureWrap>> ImmediateLoadFunction { get; init; }
public Task<IDalamudTextureWrap>? InnerTask { get; set; }
public int CompareTo(WorkItem other)
{
if (this.Basis.IsOpportunistic != other.Basis.IsOpportunistic)
return this.Basis.IsOpportunistic ? 1 : -1;
if (this.Basis.IsOpportunistic)
return -this.Basis.LatestRequestedTick.CompareTo(other.Basis.LatestRequestedTick);
return this.Basis.FirstRequestedTick.CompareTo(other.Basis.FirstRequestedTick);
}
}
}

View file

@ -2,6 +2,7 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using BitFaster.Caching.Lru;
@ -56,6 +57,9 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid
[ServiceManager.ServiceDependency]
private readonly InterfaceManager interfaceManager = Service<InterfaceManager>.Get();
[ServiceManager.ServiceDependency]
private readonly TextureLoadThrottler textureLoadThrottler = Service<TextureLoadThrottler>.Get();
private readonly ConcurrentLru<GameIconLookup, string> lookupToPath = new(PathLookupLruCount);
private readonly ConcurrentDictionary<string, SharableTexture> gamePathTextures = new();
private readonly ConcurrentDictionary<string, SharableTexture> fileSystemTextures = new();
@ -148,13 +152,14 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
[Obsolete("See interface definition.")]
public IDalamudTextureWrap? GetTextureFromGame(string path, bool keepAlive = false) =>
this.gamePathTextures.GetOrAdd(path, CreateGamePathSharableTexture).GetAvailableOnAccessWrapForApi9();
this.gamePathTextures.GetOrAdd(path, GamePathSharableTexture.CreateImmediate)
.GetAvailableOnAccessWrapForApi9();
/// <inheritdoc/>
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
[Obsolete("See interface definition.")]
public IDalamudTextureWrap? GetTextureFromFile(FileInfo file, bool keepAlive = false) =>
this.fileSystemTextures.GetOrAdd(file.FullName, CreateFileSystemSharableTexture)
this.fileSystemTextures.GetOrAdd(file.FullName, FileSystemSharableTexture.CreateImmediate)
.GetAvailableOnAccessWrapForApi9();
#pragma warning restore CS0618 // Type or member is obsolete
@ -191,7 +196,7 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid
out Exception? exception)
{
ThreadSafety.AssertMainThread();
var t = this.gamePathTextures.GetOrAdd(path, CreateGamePathSharableTexture);
var t = this.gamePathTextures.GetOrAdd(path, GamePathSharableTexture.CreateImmediate);
texture = t.GetImmediate();
exception = t.UnderlyingWrap?.Exception;
return texture is not null;
@ -204,41 +209,64 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid
out Exception? exception)
{
ThreadSafety.AssertMainThread();
var t = this.fileSystemTextures.GetOrAdd(file, CreateFileSystemSharableTexture);
var t = this.fileSystemTextures.GetOrAdd(file, FileSystemSharableTexture.CreateImmediate);
texture = t.GetImmediate();
exception = t.UnderlyingWrap?.Exception;
return texture is not null;
}
/// <inheritdoc/>
public Task<IDalamudTextureWrap> GetFromGameIconAsync(in GameIconLookup lookup) =>
this.GetFromGameAsync(this.lookupToPath.GetOrAdd(lookup, this.GetIconPathByValue));
public Task<IDalamudTextureWrap> GetFromGameIconAsync(
in GameIconLookup lookup,
CancellationToken cancellationToken = default) =>
this.GetFromGameAsync(this.lookupToPath.GetOrAdd(lookup, this.GetIconPathByValue), cancellationToken);
/// <inheritdoc/>
public Task<IDalamudTextureWrap> GetFromGameAsync(string path) =>
this.gamePathTextures.GetOrAdd(path, CreateGamePathSharableTexture).CreateNewReference();
public Task<IDalamudTextureWrap> GetFromGameAsync(
string path,
CancellationToken cancellationToken = default) =>
this.gamePathTextures.GetOrAdd(path, GamePathSharableTexture.CreateAsync)
.CreateNewReference(cancellationToken);
/// <inheritdoc/>
public Task<IDalamudTextureWrap> GetFromFileAsync(string file) =>
this.fileSystemTextures.GetOrAdd(file, CreateFileSystemSharableTexture).CreateNewReference();
public Task<IDalamudTextureWrap> GetFromFileAsync(
string file,
CancellationToken cancellationToken = default) =>
this.fileSystemTextures.GetOrAdd(file, FileSystemSharableTexture.CreateAsync)
.CreateNewReference(cancellationToken);
/// <inheritdoc/>
public Task<IDalamudTextureWrap> GetFromImageAsync(ReadOnlyMemory<byte> bytes) =>
Task.Run(
() => this.interfaceManager.LoadImage(bytes.ToArray())
?? throw new("Failed to load image because of an unknown reason."));
public Task<IDalamudTextureWrap> GetFromImageAsync(
ReadOnlyMemory<byte> bytes,
CancellationToken cancellationToken = default) =>
this.textureLoadThrottler.CreateLoader(
new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(),
_ => Task.FromResult(
this.interfaceManager.LoadImage(bytes.ToArray())
?? throw new("Failed to load image because of an unknown reason.")),
cancellationToken);
/// <inheritdoc/>
public async Task<IDalamudTextureWrap> GetFromImageAsync(Stream stream, bool leaveOpen = false)
{
await using var streamDispose = leaveOpen ? null : stream;
await using var ms = stream.CanSeek ? new MemoryStream((int)stream.Length) : new();
await stream.CopyToAsync(ms).ConfigureAwait(false);
return await this.GetFromImageAsync(ms.GetBuffer());
}
public Task<IDalamudTextureWrap> GetFromImageAsync(
Stream stream,
bool leaveOpen = false,
CancellationToken cancellationToken = default) =>
this.textureLoadThrottler.CreateLoader(
new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(),
async ct =>
{
await using var streamDispose = leaveOpen ? null : stream;
await using var ms = stream.CanSeek ? new MemoryStream((int)stream.Length) : new();
await stream.CopyToAsync(ms, ct).ConfigureAwait(false);
return await this.GetFromImageAsync(ms.GetBuffer(), ct);
},
cancellationToken);
/// <inheritdoc/>
public IDalamudTextureWrap GetFromRaw(RawImageSpecification specs, ReadOnlySpan<byte> bytes) =>
public IDalamudTextureWrap GetFromRaw(
RawImageSpecification specs,
ReadOnlySpan<byte> bytes,
CancellationToken cancellationToken = default) =>
this.interfaceManager.LoadImageFromDxgiFormat(
bytes,
specs.Pitch,
@ -247,23 +275,47 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid
(SharpDX.DXGI.Format)specs.DxgiFormat);
/// <inheritdoc/>
public Task<IDalamudTextureWrap> GetFromRawAsync(RawImageSpecification specs, ReadOnlyMemory<byte> bytes) =>
Task.Run(() => this.GetFromRaw(specs, bytes.Span));
public Task<IDalamudTextureWrap> GetFromRawAsync(
RawImageSpecification specs,
ReadOnlyMemory<byte> bytes,
CancellationToken cancellationToken = default) =>
this.textureLoadThrottler.CreateLoader(
new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(),
ct => Task.FromResult(this.GetFromRaw(specs, bytes.Span, ct)),
cancellationToken);
/// <inheritdoc/>
public async Task<IDalamudTextureWrap> GetFromRawAsync(
public Task<IDalamudTextureWrap> GetFromRawAsync(
RawImageSpecification specs,
Stream stream,
bool leaveOpen = false)
{
await using var streamDispose = leaveOpen ? null : stream;
await using var ms = stream.CanSeek ? new MemoryStream((int)stream.Length) : new();
await stream.CopyToAsync(ms).ConfigureAwait(false);
return await this.GetFromRawAsync(specs, ms.GetBuffer());
}
bool leaveOpen = false,
CancellationToken cancellationToken = default) =>
this.textureLoadThrottler.CreateLoader(
new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(),
async ct =>
{
await using var streamDispose = leaveOpen ? null : stream;
await using var ms = stream.CanSeek ? new MemoryStream((int)stream.Length) : new();
await stream.CopyToAsync(ms, ct).ConfigureAwait(false);
return await this.GetFromRawAsync(specs, ms.GetBuffer(), ct);
},
cancellationToken);
/// <inheritdoc/>
public IDalamudTextureWrap GetTexture(TexFile file) => this.interfaceManager.LoadImageFromTexFile(file);
public IDalamudTextureWrap GetTexture(TexFile file) => this.GetFromTexFileAsync(file).Result;
/// <inheritdoc/>
public Task<IDalamudTextureWrap> GetFromTexFileAsync(
TexFile file,
CancellationToken cancellationToken = default) =>
this.textureLoadThrottler.CreateLoader(
new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(),
_ => Task.FromResult<IDalamudTextureWrap>(this.interfaceManager.LoadImageFromTexFile(file)),
cancellationToken);
/// <inheritdoc/>
public bool SupportsDxgiFormat(int dxgiFormat) =>
this.interfaceManager.SupportsDxgiFormat((SharpDX.DXGI.Format)dxgiFormat);
/// <inheritdoc/>
public bool TryGetIconPath(in GameIconLookup lookup, out string path)
@ -376,12 +428,6 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid
}
}
private static SharableTexture CreateGamePathSharableTexture(string gamePath) =>
new GamePathSharableTexture(gamePath);
private static SharableTexture CreateFileSystemSharableTexture(string fileSystemPath) =>
new FileSystemSharableTexture(fileSystemPath);
private static string FormatIconPath(uint iconId, string? type, bool highResolution)
{
var format = highResolution ? HighResolutionIconFileFormat : IconFileFormat;

View file

@ -1,6 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Threading.Tasks;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility;
@ -14,15 +14,12 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
/// </summary>
public class IconBrowserWidget : IDataWindowWidget
{
// Remove range 170,000 -> 180,000 by default, this specific range causes exceptions.
private readonly HashSet<int> nullValues = Enumerable.Range(170000, 9999).ToHashSet();
private Vector2 iconSize = new(64.0f, 64.0f);
private Vector2 editIconSize = new(64.0f, 64.0f);
private List<int> valueRange = Enumerable.Range(0, 200000).ToList();
private List<int>? valueRange;
private Task<List<(int ItemId, string Path)>>? iconIdsTask;
private int lastNullValueCount;
private int startRange;
private int stopRange = 200000;
private bool showTooltipImage;
@ -48,25 +45,51 @@ public class IconBrowserWidget : IDataWindowWidget
/// <inheritdoc/>
public void Draw()
{
this.iconIdsTask ??= Task.Run(
() =>
{
var texm = Service<TextureManager>.Get();
var result = new List<(int ItemId, string Path)>(200000);
for (var iconId = 0; iconId < 200000; iconId++)
{
// // Remove range 170,000 -> 180,000 by default, this specific range causes exceptions.
// if (iconId is >= 170000 and < 180000)
// continue;
if (!texm.TryGetIconPath(new((uint)iconId), out var path))
continue;
result.Add((iconId, path));
}
return result;
});
this.DrawOptions();
if (ImGui.BeginChild("ScrollableSection", ImGui.GetContentRegionAvail(), false, ImGuiWindowFlags.NoMove))
if (!this.iconIdsTask.IsCompleted)
{
var itemsPerRow = (int)MathF.Floor(
ImGui.GetContentRegionMax().X / (this.iconSize.X + ImGui.GetStyle().ItemSpacing.X));
var itemHeight = this.iconSize.Y + ImGui.GetStyle().ItemSpacing.Y;
ImGuiClip.ClippedDraw(this.valueRange, this.DrawIcon, itemsPerRow, itemHeight);
ImGui.TextUnformatted("Loading...");
}
ImGui.EndChild();
this.ProcessMouseDragging();
if (this.lastNullValueCount != this.nullValues.Count)
else if (!this.iconIdsTask.IsCompletedSuccessfully)
{
ImGui.TextUnformatted(this.iconIdsTask.Exception?.ToString() ?? "Unknown error");
}
else
{
this.RecalculateIndexRange();
this.lastNullValueCount = this.nullValues.Count;
if (ImGui.BeginChild("ScrollableSection", ImGui.GetContentRegionAvail(), false, ImGuiWindowFlags.NoMove))
{
var itemsPerRow = (int)MathF.Floor(
ImGui.GetContentRegionMax().X / (this.iconSize.X + ImGui.GetStyle().ItemSpacing.X));
var itemHeight = this.iconSize.Y + ImGui.GetStyle().ItemSpacing.Y;
ImGuiClip.ClippedDraw(this.valueRange!, this.DrawIcon, itemsPerRow, itemHeight);
}
ImGui.EndChild();
this.ProcessMouseDragging();
}
}
@ -92,11 +115,13 @@ public class IconBrowserWidget : IDataWindowWidget
ImGui.Columns(2);
ImGui.PushItemWidth(ImGui.GetContentRegionAvail().X);
if (ImGui.InputInt("##StartRange", ref this.startRange, 0, 0)) this.RecalculateIndexRange();
if (ImGui.InputInt("##StartRange", ref this.startRange, 0, 0))
this.valueRange = null;
ImGui.NextColumn();
ImGui.PushItemWidth(ImGui.GetContentRegionAvail().X);
if (ImGui.InputInt("##StopRange", ref this.stopRange, 0, 0)) this.RecalculateIndexRange();
if (ImGui.InputInt("##StopRange", ref this.stopRange, 0, 0))
this.valueRange = null;
ImGui.NextColumn();
ImGui.Checkbox("Show Image in Tooltip", ref this.showTooltipImage);
@ -114,40 +139,32 @@ public class IconBrowserWidget : IDataWindowWidget
private void DrawIcon(int iconId)
{
var texm = Service<TextureManager>.Get();
try
var cursor = ImGui.GetCursorScreenPos();
if (texm.ImmediateTryGetFromGameIcon(new((uint)iconId), out var texture, out var exc))
{
var cursor = ImGui.GetCursorScreenPos();
ImGui.Image(texture.ImGuiHandle, this.iconSize);
if (texm.ImmediateTryGetFromGameIcon(new((uint)iconId), out var texture, out var exc))
// If we have the option to show a tooltip image, draw the image, but make sure it's not too big.
if (ImGui.IsItemHovered() && this.showTooltipImage)
{
ImGui.Image(texture.ImGuiHandle, this.iconSize);
ImGui.BeginTooltip();
// If we have the option to show a tooltip image, draw the image, but make sure it's not too big.
if (ImGui.IsItemHovered() && this.showTooltipImage)
{
ImGui.BeginTooltip();
var scale = GetImageScaleFactor(texture);
var scale = GetImageScaleFactor(texture);
var textSize = ImGui.CalcTextSize(iconId.ToString());
ImGui.SetCursorPosX(
texture.Size.X * scale / 2.0f - textSize.X / 2.0f + ImGui.GetStyle().FramePadding.X * 2.0f);
ImGui.Text(iconId.ToString());
var textSize = ImGui.CalcTextSize(iconId.ToString());
ImGui.SetCursorPosX(
texture.Size.X * scale / 2.0f - textSize.X / 2.0f + ImGui.GetStyle().FramePadding.X * 2.0f);
ImGui.Text(iconId.ToString());
ImGui.Image(texture.ImGuiHandle, texture.Size * scale);
ImGui.EndTooltip();
}
// else, just draw the iconId.
else if (ImGui.IsItemHovered())
{
ImGui.SetTooltip(iconId.ToString());
}
ImGui.Image(texture.ImGuiHandle, texture.Size * scale);
ImGui.EndTooltip();
}
else if (exc is not null)
// else, just draw the iconId.
else if (ImGui.IsItemHovered())
{
// This texture failed to load; draw nothing, and prevent from trying to show it again.
this.nullValues.Add(iconId);
ImGui.SetTooltip(iconId.ToString());
}
ImGui.GetWindowDrawList().AddRect(
@ -155,10 +172,46 @@ public class IconBrowserWidget : IDataWindowWidget
cursor + this.iconSize,
ImGui.GetColorU32(ImGuiColors.DalamudWhite));
}
catch (Exception)
else if (exc is not null)
{
// If something went wrong, prevent from trying to show this icon again.
this.nullValues.Add(iconId);
ImGui.Dummy(this.iconSize);
using (Service<InterfaceManager>.Get().IconFontHandle?.Push())
{
var iconText = FontAwesomeIcon.Ban.ToIconString();
var textSize = ImGui.CalcTextSize(iconText);
ImGui.GetWindowDrawList().AddText(
cursor + ((this.iconSize - textSize) / 2),
ImGui.GetColorU32(ImGuiColors.DalamudRed),
iconText);
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip($"{iconId}\n{exc}".Replace("%", "%%"));
ImGui.GetWindowDrawList().AddRect(
cursor,
cursor + this.iconSize,
ImGui.GetColorU32(ImGuiColors.DalamudRed));
}
else
{
const uint color = 0x50FFFFFFu;
const string text = "...";
ImGui.Dummy(this.iconSize);
var textSize = ImGui.CalcTextSize(text);
ImGui.GetWindowDrawList().AddText(
cursor + ((this.iconSize - textSize) / 2),
color,
text);
if (ImGui.IsItemHovered())
ImGui.SetTooltip(iconId.ToString());
ImGui.GetWindowDrawList().AddRect(
cursor,
cursor + this.iconSize,
color);
}
}
@ -195,14 +248,14 @@ public class IconBrowserWidget : IDataWindowWidget
private void RecalculateIndexRange()
{
if (this.stopRange <= this.startRange || this.stopRange <= 0 || this.startRange < 0)
if (this.valueRange is not null)
return;
this.valueRange = new();
foreach (var (id, _) in this.iconIdsTask!.Result)
{
this.valueRange = new List<int>();
}
else
{
this.valueRange = Enumerable.Range(this.startRange, this.stopRange - this.startRange).ToList();
this.valueRange.RemoveAll(value => this.nullValues.Contains(value));
if (this.startRange <= id && id < this.stopRange)
this.valueRange.Add(id);
}
}
}

View file

@ -1,5 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Interface.Internal;
@ -36,7 +37,7 @@ public partial interface ITextureProvider
/// If the file is unavailable, then the returned instance of <see cref="IDalamudTextureWrap"/> will point to an
/// empty texture instead.</remarks>
/// <exception cref="InvalidOperationException">Thrown when called outside the UI thread.</exception>
public IDalamudTextureWrap ImmediateGetFromGameIcon(in GameIconLookup lookup);
IDalamudTextureWrap ImmediateGetFromGameIcon(in GameIconLookup lookup);
/// <summary>Gets a texture from a file shipped as a part of the game resources for use with the current frame.
/// </summary>
@ -47,7 +48,7 @@ public partial interface ITextureProvider
/// If the file is unavailable, then the returned instance of <see cref="IDalamudTextureWrap"/> will point to an
/// empty texture instead.</remarks>
/// <exception cref="InvalidOperationException">Thrown when called outside the UI thread.</exception>
public IDalamudTextureWrap ImmediateGetFromGame(string path);
IDalamudTextureWrap ImmediateGetFromGame(string path);
/// <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>
@ -57,7 +58,7 @@ public partial interface ITextureProvider
/// If the file is unavailable, then the returned instance of <see cref="IDalamudTextureWrap"/> will point to an
/// empty texture instead.</remarks>
/// <exception cref="InvalidOperationException">Thrown when called outside the UI thread.</exception>
public IDalamudTextureWrap ImmediateGetFromFile(string file);
IDalamudTextureWrap ImmediateGetFromFile(string file);
/// <summary>Gets the corresponding game icon for use with the current frame.</summary>
/// <param name="lookup">The icon specifier.</param>
@ -68,7 +69,7 @@ public partial interface ITextureProvider
/// 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(
bool ImmediateTryGetFromGameIcon(
in GameIconLookup lookup,
[NotNullWhen(true)] out IDalamudTextureWrap? texture,
out Exception? exception);
@ -83,7 +84,7 @@ public partial interface ITextureProvider
/// 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(
bool ImmediateTryGetFromGame(
string path,
[NotNullWhen(true)] out IDalamudTextureWrap? texture,
out Exception? exception);
@ -97,60 +98,90 @@ public partial interface ITextureProvider
/// 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(
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>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A <see cref="Task{TResult}"/> containing the loaded texture on success. Dispose after use.</returns>
public Task<IDalamudTextureWrap> GetFromGameIconAsync(in GameIconLookup lookup);
Task<IDalamudTextureWrap> GetFromGameIconAsync(
in GameIconLookup lookup,
CancellationToken cancellationToken = default);
/// <summary>Gets a texture from a file shipped as a part of the game resources.</summary>
/// <param name="path">The game-internal path to a .tex, .atex, or an image file such as .png.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A <see cref="Task{TResult}"/> containing the loaded texture on success. Dispose after use.</returns>
public Task<IDalamudTextureWrap> GetFromGameAsync(string path);
Task<IDalamudTextureWrap> GetFromGameAsync(
string path,
CancellationToken cancellationToken = default);
/// <summary>Gets a texture from a file on the filesystem.</summary>
/// <param name="file">The filesystem path to a .tex, .atex, or an image file such as .png.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A <see cref="Task{TResult}"/> containing the loaded texture on success. Dispose after use.</returns>
public Task<IDalamudTextureWrap> GetFromFileAsync(string file);
Task<IDalamudTextureWrap> GetFromFileAsync(
string file,
CancellationToken cancellationToken = default);
/// <summary>Gets a texture from the given bytes, trying to interpret it as a .tex file or other well-known image
/// files, such as .png.</summary>
/// <param name="bytes">The bytes to load.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A <see cref="Task{TResult}"/> containing the loaded texture on success. Dispose after use.</returns>
public Task<IDalamudTextureWrap> GetFromImageAsync(ReadOnlyMemory<byte> bytes);
Task<IDalamudTextureWrap> GetFromImageAsync(
ReadOnlyMemory<byte> bytes,
CancellationToken cancellationToken = default);
/// <summary>Gets a texture from the given stream, trying to interpret it as a .tex file or other well-known image
/// files, such as .png.</summary>
/// <param name="stream">The stream to load data from.</param>
/// <param name="leaveOpen">Whether to leave the stream open once the task completes, sucessfully or not.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A <see cref="Task{TResult}"/> containing the loaded texture on success. Dispose after use.</returns>
public Task<IDalamudTextureWrap> GetFromImageAsync(Stream stream, bool leaveOpen = false);
/// <remarks><paramref name="stream"/> will be closed or not only according to <paramref name="leaveOpen"/>;
/// <paramref name="cancellationToken"/> is irrelevant in closing the stream.</remarks>
Task<IDalamudTextureWrap> GetFromImageAsync(
Stream stream,
bool leaveOpen = false,
CancellationToken cancellationToken = default);
/// <summary>Gets a texture from the given bytes, interpreting it as a raw bitmap.</summary>
/// <param name="specs">The specifications for the raw bitmap.</param>
/// <param name="bytes">The bytes to load.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The texture loaded from the supplied raw bitmap. Dispose after use.</returns>
public IDalamudTextureWrap GetFromRaw(RawImageSpecification specs, ReadOnlySpan<byte> bytes);
IDalamudTextureWrap GetFromRaw(
RawImageSpecification specs,
ReadOnlySpan<byte> bytes,
CancellationToken cancellationToken = default);
/// <summary>Gets a texture from the given bytes, interpreting it as a raw bitmap.</summary>
/// <param name="specs">The specifications for the raw bitmap.</param>
/// <param name="bytes">The bytes to load.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A <see cref="Task{TResult}"/> containing the loaded texture on success. Dispose after use.</returns>
public Task<IDalamudTextureWrap> GetFromRawAsync(RawImageSpecification specs, ReadOnlyMemory<byte> bytes);
Task<IDalamudTextureWrap> GetFromRawAsync(
RawImageSpecification specs,
ReadOnlyMemory<byte> bytes,
CancellationToken cancellationToken = default);
/// <summary>Gets a texture from the given stream, interpreting the read data as a raw bitmap.</summary>
/// <param name="specs">The specifications for the raw bitmap.</param>
/// <param name="stream">The stream to load data from.</param>
/// <param name="leaveOpen">Whether to leave the stream open once the task completes, sucessfully or not.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A <see cref="Task{TResult}"/> containing the loaded texture on success. Dispose after use.</returns>
public Task<IDalamudTextureWrap> GetFromRawAsync(
/// <remarks><paramref name="stream"/> will be closed or not only according to <paramref name="leaveOpen"/>;
/// <paramref name="cancellationToken"/> is irrelevant in closing the stream.</remarks>
Task<IDalamudTextureWrap> GetFromRawAsync(
RawImageSpecification specs,
Stream stream,
bool leaveOpen = false);
bool leaveOpen = false,
CancellationToken cancellationToken = default);
/// <summary>
/// Get a path for a specific icon's .tex file.
@ -158,7 +189,7 @@ public partial interface ITextureProvider
/// <param name="lookup">The icon lookup.</param>
/// <returns>The path to the icon.</returns>
/// <exception cref="FileNotFoundException">If a corresponding file could not be found.</exception>
public string GetIconPath(in GameIconLookup lookup);
string GetIconPath(in GameIconLookup lookup);
/// <summary>
/// Gets the path of an icon.
@ -166,12 +197,31 @@ public partial interface ITextureProvider
/// <param name="lookup">The icon lookup.</param>
/// <param name="path">The resolved path.</param>
/// <returns><c>true</c> if the corresponding file exists and <paramref name="path"/> has been set.</returns>
public bool TryGetIconPath(in GameIconLookup lookup, [NotNullWhen(true)] out string? path);
bool TryGetIconPath(in GameIconLookup lookup, [NotNullWhen(true)] out string? path);
/// <summary>
/// Get a texture handle for the specified Lumina <see cref="TexFile"/>.
/// Alias for fetching <see cref="Task{TResult}.Result"/> from <see cref="GetFromTexFileAsync"/>.
/// </summary>
/// <param name="file">The texture to obtain a handle to.</param>
/// <returns>A texture wrap that can be used to render the texture. Dispose after use.</returns>
IDalamudTextureWrap GetTexture(TexFile file);
/// <summary>
/// Get a texture handle for the specified Lumina <see cref="TexFile"/>.
/// </summary>
/// <param name="file">The texture to obtain a handle to.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A texture wrap that can be used to render the texture. Dispose after use.</returns>
public IDalamudTextureWrap GetTexture(TexFile file);
Task<IDalamudTextureWrap> GetFromTexFileAsync(
TexFile file,
CancellationToken cancellationToken = default);
/// <summary>
/// Determines whether the system supports the given DXGI format.
/// For use with <see cref="RawImageSpecification.DxgiFormat"/>.
/// </summary>
/// <param name="dxgiFormat">The DXGI format.</param>
/// <returns><c>true</c> if supported.</returns>
bool SupportsDxgiFormat(int dxgiFormat);
}