diff --git a/Dalamud/Interface/Internal/SharableTextures/FileSystemSharableTexture.cs b/Dalamud/Interface/Internal/SharableTextures/FileSystemSharableTexture.cs
index 06184b5ec..ff4a6adbf 100644
--- a/Dalamud/Interface/Internal/SharableTextures/FileSystemSharableTexture.cs
+++ b/Dalamud/Interface/Internal/SharableTextures/FileSystemSharableTexture.cs
@@ -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 class.
///
/// The path.
- public FileSystemSharableTexture(string path)
+ /// If set to true, this class will hold a reference to self.
+ /// Otherwise, it is expected that the caller to hold the reference.
+ private FileSystemSharableTexture(string path, bool holdSelfReference)
+ : base(holdSelfReference)
{
this.path = path;
- this.UnderlyingWrap = this.CreateTextureAsync();
+ if (holdSelfReference)
+ {
+ this.UnderlyingWrap = Service.Get().CreateLoader(
+ this,
+ this.CreateTextureAsync,
+ this.LoadCancellationToken);
+ }
}
///
public override string SourcePathForDebug => this.path;
+ ///
+ /// Creates a new instance of .
+ /// The new instance will hold a reference to itself.
+ ///
+ /// The path.
+ /// The new instance.
+ public static SharableTexture CreateImmediate(string path) => new FileSystemSharableTexture(path, true);
+
+ ///
+ /// Creates a new instance of .
+ /// The caller is expected to manage ownership of the new instance.
+ ///
+ /// The path.
+ /// The new instance.
+ public static SharableTexture CreateAsync(string path) => new FileSystemSharableTexture(path, false);
+
///
public override string ToString() =>
$"{nameof(FileSystemSharableTexture)}#{this.InstanceIdForDebug}({this.path})";
@@ -38,15 +64,16 @@ internal sealed class FileSystemSharableTexture : SharableTexture
///
protected override void ReviveResources() =>
- this.UnderlyingWrap = this.CreateTextureAsync();
+ this.UnderlyingWrap = Service.Get().CreateLoader(
+ this,
+ this.CreateTextureAsync,
+ this.LoadCancellationToken);
- private Task CreateTextureAsync() =>
- Task.Run(
- () =>
- {
- var w = (IDalamudTextureWrap)Service.Get().LoadImage(this.path)
- ?? throw new("Failed to load image because of an unknown reason.");
- this.DisposeSuppressingWrap = new(w);
- return w;
- });
+ private Task CreateTextureAsync(CancellationToken cancellationToken)
+ {
+ var w = (IDalamudTextureWrap)Service.Get().LoadImage(this.path)
+ ?? throw new("Failed to load image because of an unknown reason.");
+ this.DisposeSuppressingWrap = new(w);
+ return Task.FromResult(w);
+ }
}
diff --git a/Dalamud/Interface/Internal/SharableTextures/GamePathSharableTexture.cs b/Dalamud/Interface/Internal/SharableTextures/GamePathSharableTexture.cs
index e58f21c26..ad026aff7 100644
--- a/Dalamud/Interface/Internal/SharableTextures/GamePathSharableTexture.cs
+++ b/Dalamud/Interface/Internal/SharableTextures/GamePathSharableTexture.cs
@@ -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 class.
///
/// The path.
- public GamePathSharableTexture(string path)
+ /// If set to true, this class will hold a reference to self.
+ /// Otherwise, it is expected that the caller to hold the reference.
+ private GamePathSharableTexture(string path, bool holdSelfReference)
+ : base(holdSelfReference)
{
this.path = path;
- this.UnderlyingWrap = this.CreateTextureAsync();
+ if (holdSelfReference)
+ {
+ this.UnderlyingWrap = Service.Get().CreateLoader(
+ this,
+ this.CreateTextureAsync,
+ this.LoadCancellationToken);
+ }
}
///
public override string SourcePathForDebug => this.path;
+ ///
+ /// Creates a new instance of .
+ /// The new instance will hold a reference to itself.
+ ///
+ /// The path.
+ /// The new instance.
+ public static SharableTexture CreateImmediate(string path) => new GamePathSharableTexture(path, true);
+
+ ///
+ /// Creates a new instance of .
+ /// The caller is expected to manage ownership of the new instance.
+ ///
+ /// The path.
+ /// The new instance.
+ public static SharableTexture CreateAsync(string path) => new GamePathSharableTexture(path, false);
+
///
public override string ToString() => $"{nameof(GamePathSharableTexture)}#{this.InstanceIdForDebug}({this.path})";
@@ -41,17 +67,19 @@ internal sealed class GamePathSharableTexture : SharableTexture
///
protected override void ReviveResources() =>
- this.UnderlyingWrap = this.CreateTextureAsync();
+ this.UnderlyingWrap = Service.Get().CreateLoader(
+ this,
+ this.CreateTextureAsync,
+ this.LoadCancellationToken);
- private Task CreateTextureAsync() =>
- Task.Run(
- async () =>
- {
- var dm = await Service.GetAsync();
- var im = await Service.GetAsync();
- var file = dm.GetFile(this.path);
- var t = (IDalamudTextureWrap)im.LoadImageFromTexFile(file ?? throw new FileNotFoundException());
- this.DisposeSuppressingWrap = new(t);
- return t;
- });
+ private async Task CreateTextureAsync(CancellationToken cancellationToken)
+ {
+ var dm = await Service.GetAsync();
+ var im = await Service.GetAsync();
+ var file = dm.GetFile(this.path);
+ cancellationToken.ThrowIfCancellationRequested();
+ var t = (IDalamudTextureWrap)im.LoadImageFromTexFile(file ?? throw new FileNotFoundException());
+ this.DisposeSuppressingWrap = new(t);
+ return t;
+ }
}
diff --git a/Dalamud/Interface/Internal/SharableTextures/SharableTexture.cs b/Dalamud/Interface/Internal/SharableTextures/SharableTexture.cs
index c08cdb7e9..057589ee7 100644
--- a/Dalamud/Interface/Internal/SharableTextures/SharableTexture.cs
+++ b/Dalamud/Interface/Internal/SharableTextures/SharableTexture.cs
@@ -9,9 +9,9 @@ namespace Dalamud.Interface.Internal.SharableTextures;
///
/// Represents a texture that may have multiple reference holders (owners).
///
-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;
///
/// Initializes a new instance of the class.
///
- protected SharableTexture()
+ /// If set to true, this class will hold a reference to self.
+ /// Otherwise, it is expected that the caller to hold the reference.
+ 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();
}
///
@@ -66,11 +75,26 @@ internal abstract class SharableTexture : IRefCountable
///
public Task? UnderlyingWrap { get; set; }
+ ///
+ public bool IsOpportunistic { get; private set; }
+
+ ///
+ public long FirstRequestedTick { get; private set; }
+
+ ///
+ public long LatestRequestedTick { get; private set; }
+
///
/// Gets or sets the dispose-suppressing wrap for .
///
protected DisposeSuppressingTextureWrap? DisposeSuppressingWrap { get; set; }
+ ///
+ /// Gets a cancellation token for cancelling load.
+ /// Intended to be called from implementors' constructors and .
+ ///
+ protected CancellationToken LoadCancellationToken => this.cancellationTokenSource?.Token ?? default;
+
///
/// Gets or sets a weak reference to an object that demands this objects to be alive.
///
@@ -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
/// is called.
///
+ /// The cancellation token.
/// The task containing the texture.
- public Task CreateNewReference()
+ public async Task 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);
}
///
@@ -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;
diff --git a/Dalamud/Interface/Internal/SharableTextures/TextureLoadThrottler.cs b/Dalamud/Interface/Internal/SharableTextures/TextureLoadThrottler.cs
new file mode 100644
index 000000000..65fee34d6
--- /dev/null
+++ b/Dalamud/Interface/Internal/SharableTextures/TextureLoadThrottler.cs
@@ -0,0 +1,178 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Dalamud.Interface.Internal.SharableTextures;
+
+///
+/// Service for managing texture loads.
+///
+[ServiceManager.EarlyLoadedService]
+internal class TextureLoadThrottler : IServiceType
+{
+ private readonly List workList = new();
+ private readonly List activeWorkList = new();
+
+ [ServiceManager.ServiceConstructor]
+ private TextureLoadThrottler() =>
+ this.MaxActiveWorkItems = Math.Min(64, Environment.ProcessorCount);
+
+ ///
+ /// Basis for throttling.
+ ///
+ internal interface IThrottleBasisProvider
+ {
+ ///
+ /// Gets a value indicating whether the resource is requested in an opportunistic way.
+ ///
+ bool IsOpportunistic { get; }
+
+ ///
+ /// Gets the first requested tick count from .
+ ///
+ long FirstRequestedTick { get; }
+
+ ///
+ /// Gets the latest requested tick count from .
+ ///
+ long LatestRequestedTick { get; }
+ }
+
+ private int MaxActiveWorkItems { get; }
+
+ ///
+ /// Creates a texture loader.
+ ///
+ /// The throttle basis.
+ /// The immediate load function.
+ /// The cancellation token.
+ /// The task.
+ public Task CreateLoader(
+ IThrottleBasisProvider basis,
+ Func> 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(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();
+ });
+ }
+
+ ///
+ /// A read-only implementation of .
+ ///
+ public class ReadOnlyThrottleBasisProvider : IThrottleBasisProvider
+ {
+ ///
+ public bool IsOpportunistic { get; init; } = false;
+
+ ///
+ public long FirstRequestedTick { get; init; } = Environment.TickCount64;
+
+ ///
+ public long LatestRequestedTick { get; init; } = Environment.TickCount64;
+ }
+
+ [SuppressMessage(
+ "StyleCop.CSharp.OrderingRules",
+ "SA1206:Declaration keywords should follow order",
+ Justification = "no")]
+ private record WorkItem : IComparable
+ {
+ public required TaskCompletionSource TaskCompletionSource { get; init; }
+
+ public required IThrottleBasisProvider Basis { get; init; }
+
+ public required CancellationToken CancellationToken { get; init; }
+
+ public required Func> ImmediateLoadFunction { get; init; }
+
+ public Task? 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);
+ }
+ }
+}
diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs
index d1ab16a1d..0e6686025 100644
--- a/Dalamud/Interface/Internal/TextureManager.cs
+++ b/Dalamud/Interface/Internal/TextureManager.cs
@@ -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.Get();
+ [ServiceManager.ServiceDependency]
+ private readonly TextureLoadThrottler textureLoadThrottler = Service.Get();
+
private readonly ConcurrentLru lookupToPath = new(PathLookupLruCount);
private readonly ConcurrentDictionary gamePathTextures = new();
private readonly ConcurrentDictionary 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();
///
[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;
}
///
- public Task GetFromGameIconAsync(in GameIconLookup lookup) =>
- this.GetFromGameAsync(this.lookupToPath.GetOrAdd(lookup, this.GetIconPathByValue));
+ public Task GetFromGameIconAsync(
+ in GameIconLookup lookup,
+ CancellationToken cancellationToken = default) =>
+ this.GetFromGameAsync(this.lookupToPath.GetOrAdd(lookup, this.GetIconPathByValue), cancellationToken);
///
- public Task GetFromGameAsync(string path) =>
- this.gamePathTextures.GetOrAdd(path, CreateGamePathSharableTexture).CreateNewReference();
+ public Task GetFromGameAsync(
+ string path,
+ CancellationToken cancellationToken = default) =>
+ this.gamePathTextures.GetOrAdd(path, GamePathSharableTexture.CreateAsync)
+ .CreateNewReference(cancellationToken);
///
- public Task GetFromFileAsync(string file) =>
- this.fileSystemTextures.GetOrAdd(file, CreateFileSystemSharableTexture).CreateNewReference();
+ public Task GetFromFileAsync(
+ string file,
+ CancellationToken cancellationToken = default) =>
+ this.fileSystemTextures.GetOrAdd(file, FileSystemSharableTexture.CreateAsync)
+ .CreateNewReference(cancellationToken);
///
- public Task GetFromImageAsync(ReadOnlyMemory bytes) =>
- Task.Run(
- () => this.interfaceManager.LoadImage(bytes.ToArray())
- ?? throw new("Failed to load image because of an unknown reason."));
+ public Task GetFromImageAsync(
+ ReadOnlyMemory 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);
///
- public async Task 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 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);
///
- public IDalamudTextureWrap GetFromRaw(RawImageSpecification specs, ReadOnlySpan bytes) =>
+ public IDalamudTextureWrap GetFromRaw(
+ RawImageSpecification specs,
+ ReadOnlySpan 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);
///
- public Task GetFromRawAsync(RawImageSpecification specs, ReadOnlyMemory bytes) =>
- Task.Run(() => this.GetFromRaw(specs, bytes.Span));
+ public Task GetFromRawAsync(
+ RawImageSpecification specs,
+ ReadOnlyMemory bytes,
+ CancellationToken cancellationToken = default) =>
+ this.textureLoadThrottler.CreateLoader(
+ new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(),
+ ct => Task.FromResult(this.GetFromRaw(specs, bytes.Span, ct)),
+ cancellationToken);
///
- public async Task GetFromRawAsync(
+ public Task 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);
///
- public IDalamudTextureWrap GetTexture(TexFile file) => this.interfaceManager.LoadImageFromTexFile(file);
+ public IDalamudTextureWrap GetTexture(TexFile file) => this.GetFromTexFileAsync(file).Result;
+
+ ///
+ public Task GetFromTexFileAsync(
+ TexFile file,
+ CancellationToken cancellationToken = default) =>
+ this.textureLoadThrottler.CreateLoader(
+ new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(),
+ _ => Task.FromResult(this.interfaceManager.LoadImageFromTexFile(file)),
+ cancellationToken);
+
+ ///
+ public bool SupportsDxgiFormat(int dxgiFormat) =>
+ this.interfaceManager.SupportsDxgiFormat((SharpDX.DXGI.Format)dxgiFormat);
///
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;
diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs
index f9886dd2c..4a5bd89cf 100644
--- a/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs
@@ -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;
///
public class IconBrowserWidget : IDataWindowWidget
{
- // Remove range 170,000 -> 180,000 by default, this specific range causes exceptions.
- private readonly HashSet nullValues = Enumerable.Range(170000, 9999).ToHashSet();
-
private Vector2 iconSize = new(64.0f, 64.0f);
private Vector2 editIconSize = new(64.0f, 64.0f);
- private List valueRange = Enumerable.Range(0, 200000).ToList();
+ private List? valueRange;
+ private Task>? iconIdsTask;
- private int lastNullValueCount;
private int startRange;
private int stopRange = 200000;
private bool showTooltipImage;
@@ -48,25 +45,51 @@ public class IconBrowserWidget : IDataWindowWidget
///
public void Draw()
{
+ this.iconIdsTask ??= Task.Run(
+ () =>
+ {
+ var texm = Service.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.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.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();
- }
- 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);
}
}
}
diff --git a/Dalamud/Plugin/Services/ITextureProvider.cs b/Dalamud/Plugin/Services/ITextureProvider.cs
index 8441ca3dc..c314d7392 100644
--- a/Dalamud/Plugin/Services/ITextureProvider.cs
+++ b/Dalamud/Plugin/Services/ITextureProvider.cs
@@ -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 will point to an
/// empty texture instead.
/// Thrown when called outside the UI thread.
- public IDalamudTextureWrap ImmediateGetFromGameIcon(in GameIconLookup lookup);
+ IDalamudTextureWrap ImmediateGetFromGameIcon(in GameIconLookup lookup);
/// Gets a texture from a file shipped as a part of the game resources for use with the current frame.
///
@@ -47,7 +48,7 @@ public partial interface ITextureProvider
/// If the file is unavailable, then the returned instance of will point to an
/// empty texture instead.
/// Thrown when called outside the UI thread.
- public IDalamudTextureWrap ImmediateGetFromGame(string path);
+ IDalamudTextureWrap ImmediateGetFromGame(string path);
/// 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.
@@ -57,7 +58,7 @@ public partial interface ITextureProvider
/// If the file is unavailable, then the returned instance of will point to an
/// empty texture instead.
/// Thrown when called outside the UI thread.
- public IDalamudTextureWrap ImmediateGetFromFile(string file);
+ IDalamudTextureWrap ImmediateGetFromFile(string file);
/// Gets the corresponding game icon for use with the current frame.
/// The icon specifier.
@@ -68,7 +69,7 @@ public partial interface ITextureProvider
/// still being loaded, or the load has failed.
/// on the returned will be ignored.
/// Thrown when called outside the UI thread.
- 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.
/// on the returned will be ignored.
/// Thrown when called outside the UI thread.
- 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.
/// on the returned will be ignored.
/// Thrown when called outside the UI thread.
- public bool ImmediateTryGetFromFile(
+ 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.
+ /// The cancellation token.
/// A containing the loaded texture on success. Dispose after use.
- public Task GetFromGameIconAsync(in GameIconLookup lookup);
+ Task GetFromGameIconAsync(
+ in GameIconLookup lookup,
+ CancellationToken cancellationToken = default);
/// Gets a texture from a file shipped as a part of the game resources.
/// The game-internal path to a .tex, .atex, or an image file such as .png.
+ /// The cancellation token.
/// A containing the loaded texture on success. Dispose after use.
- public Task GetFromGameAsync(string path);
+ Task GetFromGameAsync(
+ string path,
+ CancellationToken cancellationToken = default);
/// Gets a texture from a file on the filesystem.
/// The filesystem path to a .tex, .atex, or an image file such as .png.
+ /// The cancellation token.
/// A containing the loaded texture on success. Dispose after use.
- public Task GetFromFileAsync(string file);
+ Task GetFromFileAsync(
+ string file,
+ CancellationToken cancellationToken = default);
/// Gets a texture from the given bytes, trying to interpret it as a .tex file or other well-known image
/// files, such as .png.
/// The bytes to load.
+ /// The cancellation token.
/// A containing the loaded texture on success. Dispose after use.
- public Task GetFromImageAsync(ReadOnlyMemory bytes);
+ Task GetFromImageAsync(
+ ReadOnlyMemory bytes,
+ CancellationToken cancellationToken = default);
/// Gets a texture from the given stream, trying to interpret it as a .tex file or other well-known image
/// files, such as .png.
/// The stream to load data from.
/// Whether to leave the stream open once the task completes, sucessfully or not.
+ /// The cancellation token.
/// A containing the loaded texture on success. Dispose after use.
- public Task GetFromImageAsync(Stream stream, bool leaveOpen = false);
+ /// will be closed or not only according to ;
+ /// is irrelevant in closing the stream.
+ Task GetFromImageAsync(
+ Stream stream,
+ bool leaveOpen = false,
+ CancellationToken cancellationToken = default);
/// Gets a texture from the given bytes, interpreting it as a raw bitmap.
/// The specifications for the raw bitmap.
/// The bytes to load.
+ /// The cancellation token.
/// The texture loaded from the supplied raw bitmap. Dispose after use.
- public IDalamudTextureWrap GetFromRaw(RawImageSpecification specs, ReadOnlySpan bytes);
+ IDalamudTextureWrap GetFromRaw(
+ RawImageSpecification specs,
+ ReadOnlySpan bytes,
+ CancellationToken cancellationToken = default);
/// Gets a texture from the given bytes, interpreting it as a raw bitmap.
/// The specifications for the raw bitmap.
/// The bytes to load.
+ /// The cancellation token.
/// A containing the loaded texture on success. Dispose after use.
- public Task GetFromRawAsync(RawImageSpecification specs, ReadOnlyMemory bytes);
+ Task GetFromRawAsync(
+ RawImageSpecification specs,
+ ReadOnlyMemory bytes,
+ CancellationToken cancellationToken = default);
/// Gets a texture from the given stream, interpreting the read data as a raw bitmap.
/// The specifications for the raw bitmap.
/// The stream to load data from.
/// Whether to leave the stream open once the task completes, sucessfully or not.
+ /// The cancellation token.
/// A containing the loaded texture on success. Dispose after use.
- public Task GetFromRawAsync(
+ /// will be closed or not only according to ;
+ /// is irrelevant in closing the stream.
+ Task GetFromRawAsync(
RawImageSpecification specs,
Stream stream,
- bool leaveOpen = false);
+ bool leaveOpen = false,
+ CancellationToken cancellationToken = default);
///
/// Get a path for a specific icon's .tex file.
@@ -158,7 +189,7 @@ public partial interface ITextureProvider
/// The icon lookup.
/// The path to the icon.
/// If a corresponding file could not be found.
- public string GetIconPath(in GameIconLookup lookup);
+ string GetIconPath(in GameIconLookup lookup);
///
/// Gets the path of an icon.
@@ -166,12 +197,31 @@ public partial interface ITextureProvider
/// The icon lookup.
/// The resolved path.
/// true if the corresponding file exists and has been set.
- public bool TryGetIconPath(in GameIconLookup lookup, [NotNullWhen(true)] out string? path);
+ bool TryGetIconPath(in GameIconLookup lookup, [NotNullWhen(true)] out string? path);
+
+ ///
+ /// Get a texture handle for the specified Lumina .
+ /// Alias for fetching from .
+ ///
+ /// The texture to obtain a handle to.
+ /// A texture wrap that can be used to render the texture. Dispose after use.
+ IDalamudTextureWrap GetTexture(TexFile file);
///
/// Get a texture handle for the specified Lumina .
///
/// The texture to obtain a handle to.
+ /// The cancellation token.
/// A texture wrap that can be used to render the texture. Dispose after use.
- public IDalamudTextureWrap GetTexture(TexFile file);
+ Task GetFromTexFileAsync(
+ TexFile file,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Determines whether the system supports the given DXGI format.
+ /// For use with .
+ ///
+ /// The DXGI format.
+ /// true if supported.
+ bool SupportsDxgiFormat(int dxgiFormat);
}