From 2572f24e088ea3824b69f4afe8fea9adb445c3df Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Mon, 4 Mar 2024 22:46:38 +0900 Subject: [PATCH] More cleanup --- .../FileSystemSharedImmediateTexture.cs | 22 +- .../GamePathSharedImmediateTexture.cs | 22 +- .../ManifestResourceSharedImmediateTexture.cs | 28 +- .../SharedImmediateTexture.cs | 34 +- .../Internal/TextureManager.Drawer.cs | 8 - .../TextureManager.FromExistingTexture.cs | 76 ++-- .../Textures/Internal/TextureManager.cs | 181 +++++---- .../TextureManagerPluginScoped.Api9.cs | 56 +++ .../Internal/TextureManagerPluginScoped.cs | 350 ++++++++++++++++++ .../Textures/Internal/ViewportTextureWrap.cs | 2 - .../DynamicPriorityQueueLoader.cs} | 109 +++--- 11 files changed, 635 insertions(+), 253 deletions(-) create mode 100644 Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.Api9.cs create mode 100644 Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs rename Dalamud/{Interface/Textures/Internal/TextureLoadThrottler.cs => Utility/DynamicPriorityQueueLoader.cs} (67%) diff --git a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs index a98f5c940..4735c1af7 100644 --- a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs +++ b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs @@ -2,7 +2,6 @@ using System.Threading; using System.Threading.Tasks; using Dalamud.Interface.Internal; -using Dalamud.Utility; namespace Dalamud.Interface.Textures.Internal.SharedImmediateTextures; @@ -13,10 +12,8 @@ internal sealed class FileSystemSharedImmediateTexture : SharedImmediateTexture /// Initializes a new instance of the class. /// The path. - private FileSystemSharedImmediateTexture(string path) => this.path = path; - - /// - public override string SourcePathForDebug => this.path; + private FileSystemSharedImmediateTexture(string path) + : base(path) => this.path = path; /// Creates a new placeholder instance of . /// The path. @@ -28,20 +25,7 @@ internal sealed class FileSystemSharedImmediateTexture : SharedImmediateTexture $"{nameof(FileSystemSharedImmediateTexture)}#{this.InstanceIdForDebug}({this.path})"; /// - protected override void ReleaseResources() - { - _ = this.UnderlyingWrap?.ToContentDisposedTask(true); - this.UnderlyingWrap = null; - } - - /// - protected override void ReviveResources() => - this.UnderlyingWrap = Service.Get().LoadTextureAsync( - this, - this.CreateTextureAsync, - this.LoadCancellationToken); - - private async Task CreateTextureAsync(CancellationToken cancellationToken) + protected override async Task CreateTextureAsync(CancellationToken cancellationToken) { var tm = await Service.GetAsync(); return await tm.NoThrottleCreateFromFileAsync(this.path, cancellationToken); diff --git a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs index 0af66c457..da3cc1a8d 100644 --- a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs +++ b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs @@ -4,7 +4,6 @@ using System.Threading.Tasks; using Dalamud.Data; using Dalamud.Interface.Internal; -using Dalamud.Utility; using Lumina.Data.Files; @@ -17,10 +16,8 @@ internal sealed class GamePathSharedImmediateTexture : SharedImmediateTexture /// Initializes a new instance of the class. /// The path. - private GamePathSharedImmediateTexture(string path) => this.path = path; - - /// - public override string SourcePathForDebug => this.path; + private GamePathSharedImmediateTexture(string path) + : base(path) => this.path = path; /// Creates a new placeholder instance of . /// The path. @@ -32,20 +29,7 @@ internal sealed class GamePathSharedImmediateTexture : SharedImmediateTexture $"{nameof(GamePathSharedImmediateTexture)}#{this.InstanceIdForDebug}({this.path})"; /// - protected override void ReleaseResources() - { - _ = this.UnderlyingWrap?.ToContentDisposedTask(true); - this.UnderlyingWrap = null; - } - - /// - protected override void ReviveResources() => - this.UnderlyingWrap = Service.Get().LoadTextureAsync( - this, - this.CreateTextureAsync, - this.LoadCancellationToken); - - private async Task CreateTextureAsync(CancellationToken cancellationToken) + protected override async Task CreateTextureAsync(CancellationToken cancellationToken) { var dm = await Service.GetAsync(); var tm = await Service.GetAsync(); diff --git a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs index 62f315809..00e2f34d5 100644 --- a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs +++ b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs @@ -4,7 +4,6 @@ using System.Threading; using System.Threading.Tasks; using Dalamud.Interface.Internal; -using Dalamud.Utility; namespace Dalamud.Interface.Textures.Internal.SharedImmediateTextures; @@ -19,14 +18,12 @@ internal sealed class ManifestResourceSharedImmediateTexture : SharedImmediateTe /// The assembly containing manifest resources. /// The case-sensitive name of the manifest resource being requested. private ManifestResourceSharedImmediateTexture(Assembly assembly, string name) + : base($"{assembly.GetName().FullName}:{name}") { this.assembly = assembly; this.name = name; } - /// - public override string SourcePathForDebug => $"{this.assembly.GetName().FullName}:{this.name}"; - /// Creates a new placeholder instance of . /// The arguments to pass to the constructor. /// The new instance. @@ -34,32 +31,15 @@ internal sealed class ManifestResourceSharedImmediateTexture : SharedImmediateTe new ManifestResourceSharedImmediateTexture(args.Assembly, args.Name); /// - public override string ToString() => - $"{nameof(ManifestResourceSharedImmediateTexture)}#{this.InstanceIdForDebug}({this.SourcePathForDebug})"; - - /// - protected override void ReleaseResources() - { - _ = this.UnderlyingWrap?.ToContentDisposedTask(true); - this.UnderlyingWrap = null; - } - - /// - protected override void ReviveResources() => - this.UnderlyingWrap = Service.Get().LoadTextureAsync( - this, - this.CreateTextureAsync, - this.LoadCancellationToken); - - private async Task CreateTextureAsync(CancellationToken cancellationToken) + protected override async Task CreateTextureAsync(CancellationToken cancellationToken) { await using var stream = this.assembly.GetManifestResourceStream(this.name); if (stream is null) throw new FileNotFoundException("The resource file could not be found."); var tm = await Service.GetAsync(); - var ms = new MemoryStream(stream.CanSeek ? (int)stream.Length : 0); + var ms = new MemoryStream(stream.CanSeek ? checked((int)stream.Length) : 0); await stream.CopyToAsync(ms, cancellationToken); - return tm.NoThrottleCreateFromImage(ms.GetBuffer().AsMemory(0, (int)ms.Length), cancellationToken); + return tm.NoThrottleCreateFromImage(ms.GetBuffer().AsMemory(0, checked((int)ms.Length)), cancellationToken); } } diff --git a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/SharedImmediateTexture.cs b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/SharedImmediateTexture.cs index 4a6289ceb..07eb52500 100644 --- a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/SharedImmediateTexture.cs +++ b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/SharedImmediateTexture.cs @@ -11,7 +11,7 @@ namespace Dalamud.Interface.Textures.Internal.SharedImmediateTextures; /// Represents a texture that may have multiple reference holders (owners). internal abstract class SharedImmediateTexture - : ISharedImmediateTexture, IRefCountable, TextureLoadThrottler.IThrottleBasisProvider + : ISharedImmediateTexture, IRefCountable, DynamicPriorityQueueLoader.IThrottleBasisProvider { private const int SelfReferenceDurationTicks = 2000; private const long SelfReferenceExpiryExpired = long.MaxValue; @@ -28,9 +28,11 @@ internal abstract class SharedImmediateTexture private NotOwnedTextureWrap? nonOwningWrap; /// Initializes a new instance of the class. + /// Name of the underlying resource. /// The new instance is a placeholder instance. - protected SharedImmediateTexture() + protected SharedImmediateTexture(string sourcePathForDebug) { + this.SourcePathForDebug = sourcePathForDebug; this.InstanceIdForDebug = Interlocked.Increment(ref instanceCounter); this.refCount = 0; this.selfReferenceExpiry = SelfReferenceExpiryExpired; @@ -53,7 +55,7 @@ internal abstract class SharedImmediateTexture public int RefCountForDebug => this.refCount; /// Gets the source path. Debug use only. - public abstract string SourcePathForDebug { get; } + public string SourcePathForDebug { get; } /// Gets a value indicating whether this instance of supports revival. /// @@ -76,7 +78,7 @@ internal abstract class SharedImmediateTexture public bool ContentQueried { get; private set; } /// Gets a cancellation token for cancelling load. - /// Intended to be called from implementors' constructors and . + /// 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. @@ -134,7 +136,7 @@ internal abstract class SharedImmediateTexture this.cancellationTokenSource?.Cancel(); this.cancellationTokenSource = null; this.nonOwningWrap = null; - this.ReleaseResources(); + this.ClearUnderlyingWrap(); this.resourceReleased = true; return newRefCount; @@ -272,11 +274,27 @@ internal abstract class SharedImmediateTexture return this.availableOnAccessWrapForApi9; } + /// + public override string ToString() => $"{this.GetType().Name}#{this.InstanceIdForDebug}({this.SourcePathForDebug})"; + /// Cleans up this instance of . - protected abstract void ReleaseResources(); + protected void ClearUnderlyingWrap() + { + _ = this.UnderlyingWrap?.ToContentDisposedTask(true); + this.UnderlyingWrap = null; + } /// Attempts to restore the reference to this texture. - protected abstract void ReviveResources(); + protected void LoadUnderlyingWrap() => + this.UnderlyingWrap = Service.Get().DynamicPriorityTextureLoader.LoadAsync( + this, + this.CreateTextureAsync, + this.LoadCancellationToken); + + /// Creates the texture. + /// The cancellation token. + /// The task resulting in a loaded texture. + protected abstract Task CreateTextureAsync(CancellationToken cancellationToken); private IRefCountable.RefCountResult TryAddRef(out int newRefCount) { @@ -301,7 +319,7 @@ internal abstract class SharedImmediateTexture this.cancellationTokenSource = new(); try { - this.ReviveResources(); + this.LoadUnderlyingWrap(); } catch { diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.Drawer.cs b/Dalamud/Interface/Textures/Internal/TextureManager.Drawer.cs index c1043e914..7fb79311a 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.Drawer.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.Drawer.cs @@ -2,7 +2,6 @@ using System.Buffers; using System.Diagnostics.CodeAnalysis; using System.Numerics; -using Dalamud.Interface.Internal; using Dalamud.Storage.Assets; using Dalamud.Utility; @@ -18,13 +17,6 @@ internal sealed partial class TextureManager { private SimpleDrawerImpl? simpleDrawer; - [ServiceManager.CallWhenServicesReady("Need device")] - private unsafe void ContinueConstructionFromExistingTextures(InterfaceManager.InterfaceManagerWithScene withScene) - { - this.simpleDrawer = new(); - this.simpleDrawer.Setup(this.Device.Get()); - } - /// A class for drawing simple stuff. [SuppressMessage( "StyleCop.CSharp.LayoutRules", diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs b/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs index c9ec65502..3f0b69b96 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs @@ -34,7 +34,7 @@ internal sealed partial class TextureManager } D3D11_FORMAT_SUPPORT supported; - if (this.Device.Get()->CheckFormatSupport(dxgiFormat, (uint*)&supported).FAILED) + if (this.device.Get()->CheckFormatSupport(dxgiFormat, (uint*)&supported).FAILED) return false; const D3D11_FORMAT_SUPPORT required = @@ -48,52 +48,48 @@ internal sealed partial class TextureManager IDalamudTextureWrap wrap, TextureModificationArgs args = default, bool leaveWrapOpen = false, - CancellationToken cancellationToken = default) - { - return this.textureLoadThrottler.LoadTextureAsync( - new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), - ImmediateLoadFunction, + CancellationToken cancellationToken = default) => + this.DynamicPriorityTextureLoader.LoadAsync( + null, + async _ => + { + // leaveWrapOpen is taken care from calling LoadTextureAsync + using var wrapAux = new WrapAux(wrap, true); + using var tex = await this.NoThrottleCreateFromExistingTextureAsync(wrapAux, args); + + unsafe + { + var srvDesc = new D3D11_SHADER_RESOURCE_VIEW_DESC( + tex, + D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE2D); + using var srv = default(ComPtr); + this.device.Get()->CreateShaderResourceView( + (ID3D11Resource*)tex.Get(), + &srvDesc, + srv.GetAddressOf()) + .ThrowOnError(); + + var desc = default(D3D11_TEXTURE2D_DESC); + tex.Get()->GetDesc(&desc); + return new UnknownTextureWrap( + (IUnknown*)srv.Get(), + (int)desc.Width, + (int)desc.Height, + true); + } + }, cancellationToken, leaveWrapOpen ? null : wrap); - async Task ImmediateLoadFunction(CancellationToken ct) - { - // leaveWrapOpen is taken care from calling LoadTextureAsync - using var wrapAux = new WrapAux(wrap, true); - using var tex = await this.NoThrottleCreateFromExistingTextureAsync(wrapAux, args); - - unsafe - { - var srvDesc = new D3D11_SHADER_RESOURCE_VIEW_DESC( - tex, - D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE2D); - using var srv = default(ComPtr); - this.Device.Get()->CreateShaderResourceView( - (ID3D11Resource*)tex.Get(), - &srvDesc, - srv.GetAddressOf()) - .ThrowOnError(); - - var desc = default(D3D11_TEXTURE2D_DESC); - tex.Get()->GetDesc(&desc); - return new UnknownTextureWrap( - (IUnknown*)srv.Get(), - (int)desc.Width, - (int)desc.Height, - true); - } - } - } - /// - public async Task CreateFromImGuiViewportAsync( + public Task CreateFromImGuiViewportAsync( ImGuiViewportTextureArgs args, CancellationToken cancellationToken = default) { - // This constructor may throw; keep the function "async", to wrap the exception as a Task. + args.ThrowOnInvalidValues(); var t = new ViewportTextureWrap(args, cancellationToken); t.QueueUpdate(); - return await t.FirstUpdateTask; + return t.FirstUpdateTask; } /// @@ -210,7 +206,7 @@ internal sealed partial class TextureManager CPUAccessFlags = 0u, MiscFlags = 0u, }; - this.Device.Get()->CreateTexture2D(&tex2DCopyTempDesc, null, tex2DCopyTemp.GetAddressOf()).ThrowOnError(); + this.device.Get()->CreateTexture2D(&tex2DCopyTempDesc, null, tex2DCopyTemp.GetAddressOf()).ThrowOnError(); } await this.interfaceManager.RunBeforeImGuiRender( @@ -222,7 +218,7 @@ internal sealed partial class TextureManager var rtvCopyTempDesc = new D3D11_RENDER_TARGET_VIEW_DESC( tex2DCopyTemp, D3D11_RTV_DIMENSION.D3D11_RTV_DIMENSION_TEXTURE2D); - this.Device.Get()->CreateRenderTargetView( + this.device.Get()->CreateRenderTargetView( (ID3D11Resource*)tex2DCopyTemp.Get(), &rtvCopyTempDesc, rtvCopyTemp.GetAddressOf()).ThrowOnError(); diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.cs b/Dalamud/Interface/Textures/Internal/TextureManager.cs index f4f8e62d9..25f3c634e 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.cs @@ -1,15 +1,17 @@ -using System.Diagnostics.CodeAnalysis; +using System.Collections.Generic; +using System.Diagnostics; using System.IO; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; +using Dalamud.Configuration.Internal; using Dalamud.Data; using Dalamud.Game; using Dalamud.Interface.Internal; using Dalamud.Interface.Textures.Internal.SharedImmediateTextures; -using Dalamud.IoC; -using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; +using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; using Dalamud.Utility; @@ -22,22 +24,22 @@ using TerraFX.Interop.Windows; namespace Dalamud.Interface.Textures.Internal; /// Service responsible for loading and disposing ImGui texture wraps. -[PluginInterface] -[InterfaceVersion("1.0")] [ServiceManager.EarlyLoadedService] -#pragma warning disable SA1015 -[ResolveVia] -[ResolveVia] -[ResolveVia] -#pragma warning restore SA1015 internal sealed partial class TextureManager - : IServiceType, IDisposable, ITextureProvider, ITextureSubstitutionProvider, ITextureReadbackProvider + : IServiceType, + IDisposable, + ITextureProvider, + ITextureSubstitutionProvider, + ITextureReadbackProvider { private static readonly ModuleLog Log = new(nameof(TextureManager)); [ServiceManager.ServiceDependency] private readonly Dalamud dalamud = Service.Get(); + [ServiceManager.ServiceDependency] + private readonly DalamudConfiguration dalamudConfiguration = Service.Get(); + [ServiceManager.ServiceDependency] private readonly DataManager dataManager = Service.Get(); @@ -47,54 +49,56 @@ internal sealed partial class TextureManager [ServiceManager.ServiceDependency] private readonly InterfaceManager interfaceManager = Service.Get(); - [ServiceManager.ServiceDependency] - private readonly TextureLoadThrottler textureLoadThrottler = Service.Get(); - + private DynamicPriorityQueueLoader? dynamicPriorityTextureLoader; private SharedTextureManager? sharedTextureManager; private WicManager? wicManager; private bool disposing; + private ComPtr device; - [SuppressMessage( - "StyleCop.CSharp.LayoutRules", - "SA1519:Braces should not be omitted from multi-line child statement", - Justification = "Multiple fixed blocks")] [ServiceManager.ServiceConstructor] - private TextureManager() + private unsafe TextureManager(InterfaceManager.InterfaceManagerWithScene withScene) { - this.sharedTextureManager = new(this); - this.wicManager = new(this); + using var failsafe = new DisposeSafety.ScopedFinalizer(); + failsafe.Add(this.device = new((ID3D11Device*)withScene.Manager.Device!.NativePointer)); + failsafe.Add(this.dynamicPriorityTextureLoader = new(Math.Max(1, Environment.ProcessorCount - 1))); + failsafe.Add(this.sharedTextureManager = new(this)); + failsafe.Add(this.wicManager = new(this)); + failsafe.Add(this.simpleDrawer = new()); + this.simpleDrawer.Setup(this.device.Get()); + + failsafe.Cancel(); } - /// Gets the D3D11 Device used to create textures. Ownership is not transferred. - public unsafe ComPtr Device - { - get - { - if (this.interfaceManager.Scene is not { } scene) - { - _ = Service.Get(); - scene = this.interfaceManager.Scene ?? throw new InvalidOperationException(); - } + /// Finalizes an instance of the class. + ~TextureManager() => this.ReleaseUnmanagedResources(); - var device = default(ComPtr); - device.Attach((ID3D11Device*)scene.Device.NativePointer); - return device; - } + /// Gets the dynamic-priority queue texture loader. + public DynamicPriorityQueueLoader DynamicPriorityTextureLoader + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.dynamicPriorityTextureLoader ?? throw new ObjectDisposedException(nameof(TextureManager)); } /// Gets a simpler drawer. - public SimpleDrawerImpl SimpleDrawer => - this.simpleDrawer ?? throw new ObjectDisposedException(nameof(TextureManager)); + public SimpleDrawerImpl SimpleDrawer + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.simpleDrawer ?? throw new ObjectDisposedException(nameof(TextureManager)); + } /// Gets the shared texture manager. - public SharedTextureManager Shared => - this.sharedTextureManager ?? - throw new ObjectDisposedException(nameof(TextureManager)); + public SharedTextureManager Shared + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.sharedTextureManager ?? throw new ObjectDisposedException(nameof(TextureManager)); + } /// Gets the WIC manager. - public WicManager Wic => - this.wicManager ?? - throw new ObjectDisposedException(nameof(TextureManager)); + public WicManager Wic + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.wicManager ?? throw new ObjectDisposedException(nameof(TextureManager)); + } /// public void Dispose() @@ -104,17 +108,28 @@ internal sealed partial class TextureManager this.disposing = true; + Interlocked.Exchange(ref this.dynamicPriorityTextureLoader, null)?.Dispose(); Interlocked.Exchange(ref this.simpleDrawer, null)?.Dispose(); Interlocked.Exchange(ref this.sharedTextureManager, null)?.Dispose(); Interlocked.Exchange(ref this.wicManager, null)?.Dispose(); + this.ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + + /// Puts a plugin on blame for a texture. + /// The texture. + /// The plugin. + public void Blame(IDalamudTextureWrap textureWrap, LocalPlugin ownerPlugin) + { + // nop for now } /// public Task CreateFromImageAsync( ReadOnlyMemory bytes, CancellationToken cancellationToken = default) => - this.textureLoadThrottler.LoadTextureAsync( - new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), + this.DynamicPriorityTextureLoader.LoadAsync( + null, ct => Task.Run(() => this.NoThrottleCreateFromImage(bytes.ToArray(), ct), ct), cancellationToken); @@ -123,24 +138,16 @@ internal sealed partial class TextureManager Stream stream, bool leaveOpen = false, CancellationToken cancellationToken = default) => - this.textureLoadThrottler.LoadTextureAsync( - new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), - async ct => - { - await using var ms = stream.CanSeek ? new MemoryStream((int)stream.Length) : new(); - await stream.CopyToAsync(ms, ct).ConfigureAwait(false); - return this.NoThrottleCreateFromImage(ms.GetBuffer(), ct); - }, - cancellationToken) - .ContinueWith( - r => - { - if (!leaveOpen) - stream.Dispose(); - return r; - }, - default(CancellationToken)) - .Unwrap(); + this.DynamicPriorityTextureLoader.LoadAsync( + null, + async ct => + { + await using var ms = stream.CanSeek ? new MemoryStream((int)stream.Length) : new(); + await stream.CopyToAsync(ms, ct).ConfigureAwait(false); + return this.NoThrottleCreateFromImage(ms.GetBuffer(), ct); + }, + cancellationToken, + leaveOpen ? null : stream); /// // It probably doesn't make sense to throttle this, as it copies the passed bytes to GPU without any transformation. @@ -153,8 +160,8 @@ internal sealed partial class TextureManager RawImageSpecification specs, ReadOnlyMemory bytes, CancellationToken cancellationToken = default) => - this.textureLoadThrottler.LoadTextureAsync( - new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), + this.DynamicPriorityTextureLoader.LoadAsync( + null, _ => Task.FromResult(this.NoThrottleCreateFromRaw(specs, bytes.Span)), cancellationToken); @@ -164,24 +171,16 @@ internal sealed partial class TextureManager Stream stream, bool leaveOpen = false, CancellationToken cancellationToken = default) => - this.textureLoadThrottler.LoadTextureAsync( - new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), - async ct => - { - await using var ms = stream.CanSeek ? new MemoryStream((int)stream.Length) : new(); - await stream.CopyToAsync(ms, ct).ConfigureAwait(false); - return this.NoThrottleCreateFromRaw(specs, ms.GetBuffer().AsSpan(0, (int)ms.Length)); - }, - cancellationToken) - .ContinueWith( - r => - { - if (!leaveOpen) - stream.Dispose(); - return r; - }, - default(CancellationToken)) - .Unwrap(); + this.DynamicPriorityTextureLoader.LoadAsync( + null, + async ct => + { + await using var ms = stream.CanSeek ? new MemoryStream((int)stream.Length) : new(); + await stream.CopyToAsync(ms, ct).ConfigureAwait(false); + return this.NoThrottleCreateFromRaw(specs, ms.GetBuffer().AsSpan(0, (int)ms.Length)); + }, + cancellationToken, + leaveOpen ? null : stream); /// public IDalamudTextureWrap CreateFromTexFile(TexFile file) => this.CreateFromTexFileAsync(file).Result; @@ -190,9 +189,9 @@ internal sealed partial class TextureManager public Task CreateFromTexFileAsync( TexFile file, CancellationToken cancellationToken = default) => - this.textureLoadThrottler.LoadTextureAsync( - new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), - ct => Task.Run(() => this.NoThrottleCreateFromTexFile(file), ct), + this.DynamicPriorityTextureLoader.LoadAsync( + null, + _ => Task.FromResult(this.NoThrottleCreateFromTexFile(file)), cancellationToken); /// @@ -203,7 +202,7 @@ internal sealed partial class TextureManager public unsafe bool IsDxgiFormatSupported(DXGI_FORMAT dxgiFormat) { D3D11_FORMAT_SUPPORT supported; - if (this.Device.Get()->CheckFormatSupport(dxgiFormat, (uint*)&supported).FAILED) + if (this.device.Get()->CheckFormatSupport(dxgiFormat, (uint*)&supported).FAILED) return false; const D3D11_FORMAT_SUPPORT required = D3D11_FORMAT_SUPPORT.D3D11_FORMAT_SUPPORT_TEXTURE2D; @@ -215,8 +214,6 @@ internal sealed partial class TextureManager RawImageSpecification specs, ReadOnlySpan bytes) { - var device = this.Device; - var texd = new D3D11_TEXTURE2D_DESC { Width = (uint)specs.Width, @@ -234,7 +231,7 @@ internal sealed partial class TextureManager fixed (void* dataPtr = bytes) { var subrdata = new D3D11_SUBRESOURCE_DATA { pSysMem = dataPtr, SysMemPitch = (uint)specs.Pitch }; - device.Get()->CreateTexture2D(&texd, &subrdata, texture.GetAddressOf()).ThrowOnError(); + this.device.Get()->CreateTexture2D(&texd, &subrdata, texture.GetAddressOf()).ThrowOnError(); } var viewDesc = new D3D11_SHADER_RESOURCE_VIEW_DESC @@ -244,7 +241,7 @@ internal sealed partial class TextureManager Texture2D = new() { MipLevels = texd.MipLevels }, }; using var view = default(ComPtr); - device.Get()->CreateShaderResourceView((ID3D11Resource*)texture.Get(), &viewDesc, view.GetAddressOf()) + this.device.Get()->CreateShaderResourceView((ID3D11Resource*)texture.Get(), &viewDesc, view.GetAddressOf()) .ThrowOnError(); return new UnknownTextureWrap((IUnknown*)view.Get(), specs.Width, specs.Height, true); @@ -294,4 +291,6 @@ internal sealed partial class TextureManager // Note: FileInfo and FilePath are not used from TexFile; skip it. return this.NoThrottleCreateFromTexFile(tf); } + + private void ReleaseUnmanagedResources() => this.device.Reset(); } diff --git a/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.Api9.cs b/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.Api9.cs new file mode 100644 index 000000000..21ee1291c --- /dev/null +++ b/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.Api9.cs @@ -0,0 +1,56 @@ +using System.IO; + +using Dalamud.Interface.Internal; +using Dalamud.Plugin.Services; +using Dalamud.Utility; + +namespace Dalamud.Interface.Textures.Internal; + +#pragma warning disable CS0618 // Type or member is obsolete + +/// Plugin-scoped version of . +internal sealed partial class TextureManagerPluginScoped +{ + /// + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + [Obsolete("See interface definition.")] + public IDalamudTextureWrap? GetIcon( + uint iconId, + ITextureProvider.IconFlags flags = ITextureProvider.IconFlags.HiRes, + ClientLanguage? language = null, + bool keepAlive = false) + { + throw new NotImplementedException(); + } + + /// + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + [Obsolete("See interface definition.")] + public string? GetIconPath( + uint iconId, + ITextureProvider.IconFlags flags = ITextureProvider.IconFlags.HiRes, + ClientLanguage? language = null) + { + throw new NotImplementedException(); + } + + /// + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + [Obsolete("See interface definition.")] + public IDalamudTextureWrap? GetTextureFromGame( + string path, + bool keepAlive = false) + { + throw new NotImplementedException(); + } + + /// + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + [Obsolete("See interface definition.")] + public IDalamudTextureWrap? GetTextureFromFile( + FileInfo file, + bool keepAlive = false) + { + throw new NotImplementedException(); + } +} diff --git a/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs b/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs new file mode 100644 index 000000000..6ec346c30 --- /dev/null +++ b/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs @@ -0,0 +1,350 @@ +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Interface.Internal; +using Dalamud.IoC; +using Dalamud.IoC.Internal; +using Dalamud.Plugin.Internal.Types; +using Dalamud.Plugin.Internal.Types.Manifest; +using Dalamud.Plugin.Services; +using Dalamud.Utility; + +using Lumina.Data.Files; + +using TerraFX.Interop.DirectX; + +namespace Dalamud.Interface.Textures.Internal; + +/// Plugin-scoped version of . +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.ScopedService] +#pragma warning disable SA1015 +[ResolveVia] +[ResolveVia] +[ResolveVia] +#pragma warning restore SA1015 +internal sealed partial class TextureManagerPluginScoped + : IServiceType, + IDisposable, + ITextureProvider, + ITextureSubstitutionProvider, + ITextureReadbackProvider +{ + private readonly LocalPlugin plugin; + private readonly bool nonAsyncFunctionAccessDuringLoadIsError; + + private Task? managerTaskNullable; + + [ServiceManager.ServiceConstructor] + private TextureManagerPluginScoped(LocalPlugin plugin) + { + this.plugin = plugin; + if (plugin.Manifest is LocalPluginManifest lpm) + this.nonAsyncFunctionAccessDuringLoadIsError = lpm.LoadSync && lpm.LoadRequiredState != 0; + + this.managerTaskNullable = + Service + .GetAsync() + .ContinueWith( + r => + { + if (r.IsCompletedSuccessfully) + r.Result.InterceptTexDataLoad += this.ResultOnInterceptTexDataLoad; + return r; + }) + .Unwrap(); + } + + /// + public event ITextureSubstitutionProvider.TextureDataInterceptorDelegate? InterceptTexDataLoad; + + /// Gets the task resulting in an instance of . + /// Thrown if disposed. + private Task ManagerTask + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.managerTaskNullable ?? throw new ObjectDisposedException(this.ToString()); + } + + /// Gets an instance of . + /// Thrown if disposed. + /// Thrown if called at an unfortune time. + private TextureManager ManagerOrThrow + { + get + { + var task = this.ManagerTask; + + // Check for IMWS too, as TextureManager is constructed after IMWS, and UiBuilder.RunWhenUiPrepared gets + // resolved when IMWS is constructed. + if (!task.IsCompleted && Service.GetNullable() is null) + { + if (this.nonAsyncFunctionAccessDuringLoadIsError && this.plugin.State != PluginState.Loaded) + { + throw new InvalidOperationException( + "The function you've called will wait for the drawing facilities to be available, and as " + + "Dalamud is already waiting for your plugin to be fully constructed before even attempting " + + "to initialize the drawing facilities, calling this function will stall the game until and " + + "is forbidden until your plugin has been fully loaded.\n" + + $"Consider using {nameof(UiBuilder.RunWhenUiPrepared)} to wait for the right moment.\n" + + "\n" + + $"Note that your plugin has {nameof(LocalPluginManifest.LoadSync)} set and " + + $"{nameof(LocalPluginManifest.LoadRequiredState)} that is nonzero."); + } + + if (ThreadSafety.IsMainThread) + { + throw new InvalidOperationException( + "The function you've called will wait for the drawing facilities to be available, and as " + + "the drawing facilities are initialized from the main thread, calling this function will " + + "stall the game until and is forbidden until your plugin has been fully loaded.\n" + + $"Consider using {nameof(UiBuilder.RunWhenUiPrepared)} to wait for the right moment."); + } + } + + return task.Result; + } + } + + /// + public void Dispose() + { + if (Interlocked.Exchange(ref this.managerTaskNullable, null) is not { } task) + return; + task.ContinueWith( + r => + { + if (r.IsCompletedSuccessfully) + r.Result.InterceptTexDataLoad -= this.ResultOnInterceptTexDataLoad; + }); + } + + /// + public override string ToString() + { + return this.managerTaskNullable is null + ? $"{nameof(TextureManagerPluginScoped)}({this.plugin.Name}, disposed)" + : $"{nameof(TextureManagerPluginScoped)}({this.plugin.Name})"; + } + + /// + public async Task CreateFromExistingTextureAsync( + IDalamudTextureWrap wrap, + TextureModificationArgs args = default, + bool leaveWrapOpen = false, + CancellationToken cancellationToken = default) + { + var manager = await this.ManagerTask; + var textureWrap = await manager.CreateFromExistingTextureAsync( + wrap, + args, + leaveWrapOpen, + cancellationToken); + manager.Blame(textureWrap, this.plugin); + return textureWrap; + } + + /// + public async Task CreateFromImGuiViewportAsync( + ImGuiViewportTextureArgs args, + CancellationToken cancellationToken = default) + { + var manager = await this.ManagerTask; + var textureWrap = await manager.CreateFromImGuiViewportAsync(args, cancellationToken); + manager.Blame(textureWrap, this.plugin); + return textureWrap; + } + + /// + public async Task CreateFromImageAsync( + ReadOnlyMemory bytes, + CancellationToken cancellationToken = default) + { + var manager = await this.ManagerTask; + var textureWrap = await manager.CreateFromImageAsync(bytes, cancellationToken); + manager.Blame(textureWrap, this.plugin); + return textureWrap; + } + + /// + public async Task CreateFromImageAsync( + Stream stream, + bool leaveOpen = false, + CancellationToken cancellationToken = default) + { + var manager = await this.ManagerTask; + var textureWrap = await manager.CreateFromImageAsync(stream, leaveOpen, cancellationToken); + manager.Blame(textureWrap, this.plugin); + return textureWrap; + } + + /// + public IDalamudTextureWrap CreateFromRaw( + RawImageSpecification specs, + ReadOnlySpan bytes) + { + var manager = this.ManagerOrThrow; + var textureWrap = manager.CreateFromRaw(specs, bytes); + manager.Blame(textureWrap, this.plugin); + return textureWrap; + } + + /// + public async Task CreateFromRawAsync( + RawImageSpecification specs, + ReadOnlyMemory bytes, + CancellationToken cancellationToken = default) + { + var manager = await this.ManagerTask; + var textureWrap = await manager.CreateFromRawAsync(specs, bytes, cancellationToken); + manager.Blame(textureWrap, this.plugin); + return textureWrap; + } + + /// + public async Task CreateFromRawAsync( + RawImageSpecification specs, + Stream stream, + bool leaveOpen = false, + CancellationToken cancellationToken = default) + { + var manager = await this.ManagerTask; + var textureWrap = await manager.CreateFromRawAsync(specs, stream, leaveOpen, cancellationToken); + manager.Blame(textureWrap, this.plugin); + return textureWrap; + } + + /// + public IDalamudTextureWrap CreateFromTexFile(TexFile file) + { + var manager = this.ManagerOrThrow; + var textureWrap = manager.CreateFromTexFile(file); + manager.Blame(textureWrap, this.plugin); + return textureWrap; + } + + /// + public async Task CreateFromTexFileAsync( + TexFile file, + CancellationToken cancellationToken = default) + { + var manager = await this.ManagerTask; + var textureWrap = await manager.CreateFromTexFileAsync(file, cancellationToken); + manager.Blame(textureWrap, this.plugin); + return textureWrap; + } + + /// + public IEnumerable GetSupportedImageDecoderInfos() => + this.ManagerOrThrow.Wic.GetSupportedDecoderInfos(); + + /// + public ISharedImmediateTexture GetFromGameIcon(in GameIconLookup lookup) + { + return this.ManagerOrThrow.Shared.GetFromGameIcon(lookup); + } + + /// + public ISharedImmediateTexture GetFromGame(string path) + { + return this.ManagerOrThrow.Shared.GetFromGame(path); + } + + /// + public ISharedImmediateTexture GetFromFile(string path) + { + return this.ManagerOrThrow.Shared.GetFromFile(path); + } + + /// + public ISharedImmediateTexture GetFromManifestResource(Assembly assembly, string name) + { + return this.ManagerOrThrow.Shared.GetFromManifestResource(assembly, name); + } + + /// + public string GetIconPath(in GameIconLookup lookup) => this.ManagerOrThrow.GetIconPath(lookup); + + /// + public bool TryGetIconPath(in GameIconLookup lookup, out string? path) => + this.ManagerOrThrow.TryGetIconPath(lookup, out path); + + /// + public bool IsDxgiFormatSupported(int dxgiFormat) => + this.ManagerOrThrow.IsDxgiFormatSupported((DXGI_FORMAT)dxgiFormat); + + /// + public bool IsDxgiFormatSupportedForCreateFromExistingTextureAsync(int dxgiFormat) => + this.ManagerOrThrow.IsDxgiFormatSupportedForCreateFromExistingTextureAsync((DXGI_FORMAT)dxgiFormat); + + /// + public string GetSubstitutedPath(string originalPath) => + this.ManagerOrThrow.GetSubstitutedPath(originalPath); + + /// + public void InvalidatePaths(IEnumerable paths) => + this.ManagerOrThrow.InvalidatePaths(paths); + + /// + public async Task<(RawImageSpecification Specification, byte[] RawData)> GetRawImageAsync( + IDalamudTextureWrap wrap, + TextureModificationArgs args = default, + bool leaveWrapOpen = false, + CancellationToken cancellationToken = default) + { + var manager = await this.ManagerTask; + return await manager.GetRawImageAsync(wrap, args, leaveWrapOpen, cancellationToken); + } + + /// + public IEnumerable GetSupportedImageEncoderInfos() => + this.ManagerOrThrow.Wic.GetSupportedEncoderInfos(); + + /// + public async Task SaveToStreamAsync( + IDalamudTextureWrap wrap, + Guid containerGuid, + Stream stream, + IReadOnlyDictionary? props = null, + bool leaveWrapOpen = false, + bool leaveStreamOpen = false, + CancellationToken cancellationToken = default) + { + var manager = await this.ManagerTask; + await manager.SaveToStreamAsync( + wrap, + containerGuid, + stream, + props, + leaveWrapOpen, + leaveStreamOpen, + cancellationToken); + } + + /// + public async Task SaveToFileAsync( + IDalamudTextureWrap wrap, + Guid containerGuid, + string path, + IReadOnlyDictionary? props = null, + bool leaveWrapOpen = false, + CancellationToken cancellationToken = default) + { + var manager = await this.ManagerTask; + await manager.SaveToFileAsync( + wrap, + containerGuid, + path, + props, + leaveWrapOpen, + cancellationToken); + } + + private void ResultOnInterceptTexDataLoad(string path, ref string? replacementPath) => + this.InterceptTexDataLoad?.Invoke(path, ref replacementPath); +} diff --git a/Dalamud/Interface/Textures/Internal/ViewportTextureWrap.cs b/Dalamud/Interface/Textures/Internal/ViewportTextureWrap.cs index 272a2d8ee..a8692b323 100644 --- a/Dalamud/Interface/Textures/Internal/ViewportTextureWrap.cs +++ b/Dalamud/Interface/Textures/Internal/ViewportTextureWrap.cs @@ -34,8 +34,6 @@ internal sealed class ViewportTextureWrap : IDalamudTextureWrap, IDeferredDispos /// The cancellation token. public ViewportTextureWrap(ImGuiViewportTextureArgs args, CancellationToken cancellationToken) { - args.ThrowOnInvalidValues(); - this.args = args; this.cancellationToken = cancellationToken; } diff --git a/Dalamud/Interface/Textures/Internal/TextureLoadThrottler.cs b/Dalamud/Utility/DynamicPriorityQueueLoader.cs similarity index 67% rename from Dalamud/Interface/Textures/Internal/TextureLoadThrottler.cs rename to Dalamud/Utility/DynamicPriorityQueueLoader.cs index 1e7db4659..8109d2e94 100644 --- a/Dalamud/Interface/Textures/Internal/TextureLoadThrottler.cs +++ b/Dalamud/Utility/DynamicPriorityQueueLoader.cs @@ -4,15 +4,10 @@ using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; -using Dalamud.Interface.Internal; +namespace Dalamud.Utility; -namespace Dalamud.Interface.Textures.Internal; - -/// -/// Service for managing texture loads. -/// -[ServiceManager.EarlyLoadedService] -internal class TextureLoadThrottler : IServiceType, IDisposable +/// Base class for loading resources in dynamic order. +internal class DynamicPriorityQueueLoader : IDisposable { private readonly CancellationTokenSource disposeCancellationTokenSource = new(); private readonly Task adderTask; @@ -24,19 +19,20 @@ internal class TextureLoadThrottler : IServiceType, IDisposable private bool disposing; - [ServiceManager.ServiceConstructor] - private TextureLoadThrottler() + /// Initializes a new instance of the class. + /// Maximum number of concurrent load tasks. + public DynamicPriorityQueueLoader(int concurrency) { this.newItemChannel = Channel.CreateUnbounded(new() { SingleReader = true }); this.workTokenChannel = Channel.CreateUnbounded(new() { SingleWriter = true }); this.adderTask = Task.Run(this.LoopAddWorkItemAsync); - this.workerTasks = new Task[Math.Max(1, Environment.ProcessorCount - 1)]; + this.workerTasks = new Task[concurrency]; foreach (ref var task in this.workerTasks.AsSpan()) task = Task.Run(this.LoopProcessWorkItemAsync); } - /// Basis for throttling. Values may be changed anytime. + /// Provider for priority metrics. internal interface IThrottleBasisProvider { /// Gets a value indicating whether the resource is requested in an opportunistic way. @@ -68,27 +64,36 @@ internal class TextureLoadThrottler : IServiceType, IDisposable _ = t.Exception; } - /// Loads a texture according to some order. - /// The throttle basis. + /// Loads a resource according to some order. + /// The type of resource. + /// The throttle basis. null may be used to create a new instance of + /// that is not opportunistic with time values of now. /// The immediate load function. /// The cancellation token. /// Disposables to dispose when the task completes. /// The task. - public Task LoadTextureAsync( - IThrottleBasisProvider basis, - Func> immediateLoadFunction, + /// + /// may throw immediately without returning anything, or the returned + /// may complete in failure. + /// + public Task LoadAsync( + IThrottleBasisProvider? basis, + Func> immediateLoadFunction, CancellationToken cancellationToken, params IDisposable?[] disposables) { - var work = new WorkItem(basis, immediateLoadFunction, cancellationToken, disposables); + basis ??= new ReadOnlyThrottleBasisProvider(); + var work = new WorkItem(basis, immediateLoadFunction, cancellationToken, disposables); if (this.newItemChannel.Writer.TryWrite(work)) return work.Task; work.Dispose(); - return Task.FromException(new ObjectDisposedException(nameof(TextureLoadThrottler))); + return Task.FromException(new ObjectDisposedException(this.GetType().Name)); } + /// Continuously transfers work items added from to + /// , until all items are transferred and is called. private async Task LoopAddWorkItemAsync() { const int batchAddSize = 64; @@ -109,6 +114,8 @@ internal class TextureLoadThrottler : IServiceType, IDisposable } } + /// Continuously processes work items in , until all items are processed and + /// is called. private async Task LoopProcessWorkItemAsync() { var reader = this.workTokenChannel.Reader; @@ -167,10 +174,8 @@ internal class TextureLoadThrottler : IServiceType, IDisposable } } - /// - /// A read-only implementation of . - /// - public class ReadOnlyThrottleBasisProvider : IThrottleBasisProvider + /// A read-only implementation of . + private class ReadOnlyThrottleBasisProvider : IThrottleBasisProvider { /// public bool IsOpportunistic { get; init; } = false; @@ -182,27 +187,23 @@ internal class TextureLoadThrottler : IServiceType, IDisposable public long LatestRequestedTick { get; init; } = Environment.TickCount64; } - private sealed class WorkItem : IComparable, IDisposable + /// Represents a work item added from . + private abstract class WorkItem : IComparable, IDisposable { - private readonly TaskCompletionSource taskCompletionSource; private readonly IThrottleBasisProvider basis; - private readonly CancellationToken cancellationToken; - private readonly Func> immediateLoadFunction; private readonly IDisposable?[] disposables; - public WorkItem( + protected WorkItem( IThrottleBasisProvider basis, - Func> immediateLoadFunction, - CancellationToken cancellationToken, IDisposable?[] disposables) + CancellationToken cancellationToken, + params IDisposable?[] disposables) { - this.taskCompletionSource = new(); this.basis = basis; - this.cancellationToken = cancellationToken; + this.CancellationToken = cancellationToken; this.disposables = disposables; - this.immediateLoadFunction = immediateLoadFunction; } - public Task Task => this.taskCompletionSource.Task; + protected CancellationToken CancellationToken { get; } public void Dispose() { @@ -219,13 +220,37 @@ internal class TextureLoadThrottler : IServiceType, IDisposable return this.basis.FirstRequestedTick.CompareTo(other.basis.FirstRequestedTick); } - public bool CancelAsRequested() + public abstract bool CancelAsRequested(); + + public abstract ValueTask Process(CancellationToken serviceDisposeToken); + } + + /// Typed version of . + private sealed class WorkItem : WorkItem + { + private readonly TaskCompletionSource taskCompletionSource; + private readonly Func> immediateLoadFunction; + + public WorkItem( + IThrottleBasisProvider basis, + Func> immediateLoadFunction, + CancellationToken cancellationToken, + params IDisposable?[] disposables) + : base(basis, cancellationToken, disposables) { - if (!this.cancellationToken.IsCancellationRequested) + this.taskCompletionSource = new(); + this.immediateLoadFunction = immediateLoadFunction; + } + + public Task Task => this.taskCompletionSource.Task; + + public override bool CancelAsRequested() + { + if (!this.CancellationToken.IsCancellationRequested) return false; // Cancel the load task and move on. - this.taskCompletionSource.TrySetCanceled(this.cancellationToken); + this.taskCompletionSource.TrySetCanceled(this.CancellationToken); // Suppress the OperationCanceledException caused from the above. _ = this.taskCompletionSource.Task.Exception; @@ -233,16 +258,16 @@ internal class TextureLoadThrottler : IServiceType, IDisposable return true; } - public async ValueTask Process(CancellationToken serviceDisposeToken) + public override async ValueTask Process(CancellationToken serviceDisposeToken) { try { - IDalamudTextureWrap wrap; - if (this.cancellationToken.CanBeCanceled) + T wrap; + if (this.CancellationToken.CanBeCanceled) { using var cts = CancellationTokenSource.CreateLinkedTokenSource( serviceDisposeToken, - this.cancellationToken); + this.CancellationToken); wrap = await this.immediateLoadFunction(cts.Token); } else @@ -251,7 +276,7 @@ internal class TextureLoadThrottler : IServiceType, IDisposable } if (!this.taskCompletionSource.TrySetResult(wrap)) - wrap.Dispose(); + (wrap as IDisposable)?.Dispose(); } catch (Exception e) {