From 3fe2920e924ffce77f3152627d115709963dae8d Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 22 Feb 2024 00:53:07 +0900 Subject: [PATCH] Update ITextureProvider --- Dalamud/Dalamud.csproj | 1 + .../Internal/DisposeSuppressingTextureWrap.cs | 30 + .../Internal/DisposedDalamudTextureWrap.cs | 31 + .../Interface/Internal/InterfaceManager.cs | 47 +- .../FileSystemSharableTexture.cs | 54 ++ .../GamePathSharableTexture.cs | 59 ++ .../SharableTextures/SharableTexture.cs | 333 ++++++++++ Dalamud/Interface/Internal/TextureManager.cs | 629 +++++++----------- Dalamud/Interface/UiBuilder.cs | 13 + Dalamud/Plugin/Services/GameIconLookup.cs | 20 + .../Plugin/Services/ITextureProvider.Api9.cs | 98 +++ Dalamud/Plugin/Services/ITextureProvider.cs | 165 +++-- .../Plugin/Services/RawImageSpecification.cs | 216 ++++++ Dalamud/Storage/Assets/DalamudAssetManager.cs | 24 +- 14 files changed, 1222 insertions(+), 498 deletions(-) create mode 100644 Dalamud/Interface/Internal/DisposeSuppressingTextureWrap.cs create mode 100644 Dalamud/Interface/Internal/DisposedDalamudTextureWrap.cs create mode 100644 Dalamud/Interface/Internal/SharableTextures/FileSystemSharableTexture.cs create mode 100644 Dalamud/Interface/Internal/SharableTextures/GamePathSharableTexture.cs create mode 100644 Dalamud/Interface/Internal/SharableTextures/SharableTexture.cs create mode 100644 Dalamud/Plugin/Services/GameIconLookup.cs create mode 100644 Dalamud/Plugin/Services/ITextureProvider.Api9.cs create mode 100644 Dalamud/Plugin/Services/RawImageSpecification.cs diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 205681cb8..d2ad7e4ad 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -64,6 +64,7 @@ + diff --git a/Dalamud/Interface/Internal/DisposeSuppressingTextureWrap.cs b/Dalamud/Interface/Internal/DisposeSuppressingTextureWrap.cs new file mode 100644 index 000000000..d099bae69 --- /dev/null +++ b/Dalamud/Interface/Internal/DisposeSuppressingTextureWrap.cs @@ -0,0 +1,30 @@ +namespace Dalamud.Interface.Internal; + +/// +/// A texture wrap that ignores calls. +/// +internal sealed class DisposeSuppressingTextureWrap : IDalamudTextureWrap +{ + private readonly IDalamudTextureWrap innerWrap; + + /// + /// Initializes a new instance of the class. + /// + /// The inner wrap. + public DisposeSuppressingTextureWrap(IDalamudTextureWrap wrap) => this.innerWrap = wrap; + + /// + public IntPtr ImGuiHandle => this.innerWrap.ImGuiHandle; + + /// + public int Width => this.innerWrap.Width; + + /// + public int Height => this.innerWrap.Height; + + /// + public void Dispose() + { + // suppressed + } +} diff --git a/Dalamud/Interface/Internal/DisposedDalamudTextureWrap.cs b/Dalamud/Interface/Internal/DisposedDalamudTextureWrap.cs new file mode 100644 index 000000000..904a2ccb8 --- /dev/null +++ b/Dalamud/Interface/Internal/DisposedDalamudTextureWrap.cs @@ -0,0 +1,31 @@ +namespace Dalamud.Interface.Internal; + +/// +/// A disposed texture wrap. +/// +internal sealed class DisposedDalamudTextureWrap : IDalamudTextureWrap +{ + /// + /// Gets the singleton instance. + /// + public static readonly DisposedDalamudTextureWrap Instance = new(); + + private DisposedDalamudTextureWrap() + { + } + + /// + public IntPtr ImGuiHandle => throw new ObjectDisposedException(nameof(DisposedDalamudTextureWrap)); + + /// + public int Width => throw new ObjectDisposedException(nameof(DisposedDalamudTextureWrap)); + + /// + public int Height => throw new ObjectDisposedException(nameof(DisposedDalamudTextureWrap)); + + /// + public void Dispose() + { + // suppressed + } +} diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 3db799be0..5bafc4ff3 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -25,6 +25,10 @@ using Dalamud.Utility; using Dalamud.Utility.Timing; using ImGuiNET; using ImGuiScene; + +using Lumina.Data.Files; +using Lumina.Data.Parsing.Tex.Buffers; + using PInvoke; using Serilog; using SharpDX; @@ -63,7 +67,7 @@ internal class InterfaceManager : IDisposable, IServiceType public const float DefaultFontSizePx = (DefaultFontSizePt * 4.0f) / 3.0f; private readonly ConcurrentBag deferredDisposeTextures = new(); - private readonly ConcurrentBag deferredDisposeImFontLockeds = new(); + private readonly ConcurrentBag deferredDisposeDisposables = new(); [ServiceManager.ServiceDependency] private readonly WndProcHookManager wndProcHookManager = Service.Get(); @@ -349,7 +353,7 @@ internal class InterfaceManager : IDisposable, IServiceType /// The height in pixels. /// Format of the texture. /// A texture, ready to use in ImGui. - public DalamudTextureWrap LoadImageFromDxgiFormat(Span data, int pitch, int width, int height, Format dxgiFormat) + public DalamudTextureWrap LoadImageFromDxgiFormat(ReadOnlySpan data, int pitch, int width, int height, Format dxgiFormat) { if (this.scene == null) throw new InvalidOperationException("Scene isn't ready."); @@ -389,6 +393,37 @@ internal class InterfaceManager : IDisposable, IServiceType #nullable restore + /// + /// Get a texture handle for the specified Lumina TexFile. + /// + /// The texture to obtain a handle to. + /// A texture wrap that can be used to render the texture. + /// Thrown when the graphics system is not available yet. Relevant for plugins when LoadRequiredState is set to 0 or 1. + /// Thrown when the given is not supported. Most likely is that the file is corrupt. + public DalamudTextureWrap LoadImageFromTexFile(TexFile file) + { + if (!this.IsReady) + throw new InvalidOperationException("Cannot create textures before scene is ready"); + + var buffer = file.TextureBuffer; + var bpp = 1 << (((int)file.Header.Format & (int)TexFile.TextureFormat.BppMask) >> + (int)TexFile.TextureFormat.BppShift); + + var (dxgiFormat, conversion) = TexFile.GetDxgiFormatFromTextureFormat(file.Header.Format, false); + if (conversion != TexFile.DxgiFormatConversion.NoConversion || !this.SupportsDxgiFormat((Format)dxgiFormat)) + { + dxgiFormat = (int)Format.B8G8R8A8_UNorm; + buffer = buffer.Filter(0, 0, TexFile.TextureFormat.B8G8R8A8); + bpp = 32; + } + + var pitch = buffer is BlockCompressionTextureBuffer + ? Math.Max(1, (buffer.Width + 3) / 4) * 2 * bpp + : ((buffer.Width * bpp) + 7) / 8; + + return this.LoadImageFromDxgiFormat(buffer.RawData, pitch, buffer.Width, buffer.Height, (Format)dxgiFormat); + } + /// /// Sets up a deferred invocation of font rebuilding, before the next render frame. /// @@ -411,9 +446,9 @@ internal class InterfaceManager : IDisposable, IServiceType /// Enqueue an to be disposed at the end of the frame. /// /// The disposable. - public void EnqueueDeferredDispose(in ILockedImFont locked) + public void EnqueueDeferredDispose(IDisposable locked) { - this.deferredDisposeImFontLockeds.Add(locked); + this.deferredDisposeDisposables.Add(locked); } /// @@ -683,12 +718,12 @@ internal class InterfaceManager : IDisposable, IServiceType Log.Verbose("[IM] Disposing {Count} textures", count); } - if (!this.deferredDisposeImFontLockeds.IsEmpty) + if (!this.deferredDisposeDisposables.IsEmpty) { // Not logging; the main purpose of this is to keep resources used for rendering the frame to be kept // referenced until the resources are actually done being used, and it is expected that this will be // frequent. - while (this.deferredDisposeImFontLockeds.TryTake(out var d)) + while (this.deferredDisposeDisposables.TryTake(out var d)) d.Dispose(); } } diff --git a/Dalamud/Interface/Internal/SharableTextures/FileSystemSharableTexture.cs b/Dalamud/Interface/Internal/SharableTextures/FileSystemSharableTexture.cs new file mode 100644 index 000000000..0eea2a5f7 --- /dev/null +++ b/Dalamud/Interface/Internal/SharableTextures/FileSystemSharableTexture.cs @@ -0,0 +1,54 @@ +using System.Threading.Tasks; + +using Dalamud.Utility; + +namespace Dalamud.Interface.Internal.SharableTextures; + +/// +/// Represents a sharable texture, based on a file on the system filesystem. +/// +internal sealed class FileSystemSharableTexture : SharableTexture +{ + private readonly string path; + + /// + /// Initializes a new instance of the class. + /// + /// The path. + public FileSystemSharableTexture(string path) + { + this.path = path; + this.UnderlyingWrap = this.CreateTextureAsync(); + } + + /// + protected override Task UnderlyingWrap { get; set; } + + /// + public override string ToString() => + $"{nameof(FileSystemSharableTexture)}#{this.InstanceIdForDebug}({this.path})"; + + /// + protected override void FinalRelease() + { + this.DisposeSuppressingWrap = null; + _ = this.UnderlyingWrap.ToContentDisposedTask(true); + this.UnderlyingWrap = + Task.FromException(new ObjectDisposedException(nameof(GamePathSharableTexture))); + _ = this.UnderlyingWrap.Exception; + } + + /// + protected override void Revive() => + this.UnderlyingWrap = this.CreateTextureAsync(); + + 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; + }); +} diff --git a/Dalamud/Interface/Internal/SharableTextures/GamePathSharableTexture.cs b/Dalamud/Interface/Internal/SharableTextures/GamePathSharableTexture.cs new file mode 100644 index 000000000..e2198b651 --- /dev/null +++ b/Dalamud/Interface/Internal/SharableTextures/GamePathSharableTexture.cs @@ -0,0 +1,59 @@ +using System.IO; +using System.Threading.Tasks; + +using Dalamud.Data; +using Dalamud.Utility; + +using Lumina.Data.Files; + +namespace Dalamud.Interface.Internal.SharableTextures; + +/// +/// Represents a sharable texture, based on a file in game resources. +/// +internal sealed class GamePathSharableTexture : SharableTexture +{ + private readonly string path; + + /// + /// Initializes a new instance of the class. + /// + /// The path. + public GamePathSharableTexture(string path) + { + this.path = path; + this.UnderlyingWrap = this.CreateTextureAsync(); + } + + /// + protected override Task UnderlyingWrap { get; set; } + + /// + public override string ToString() => $"{nameof(GamePathSharableTexture)}#{this.InstanceIdForDebug}({this.path})"; + + /// + protected override void FinalRelease() + { + this.DisposeSuppressingWrap = null; + _ = this.UnderlyingWrap.ToContentDisposedTask(true); + this.UnderlyingWrap = + Task.FromException(new ObjectDisposedException(nameof(GamePathSharableTexture))); + _ = this.UnderlyingWrap.Exception; + } + + /// + protected override void Revive() => + this.UnderlyingWrap = this.CreateTextureAsync(); + + 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; + }); +} diff --git a/Dalamud/Interface/Internal/SharableTextures/SharableTexture.cs b/Dalamud/Interface/Internal/SharableTextures/SharableTexture.cs new file mode 100644 index 000000000..b0c1a373a --- /dev/null +++ b/Dalamud/Interface/Internal/SharableTextures/SharableTexture.cs @@ -0,0 +1,333 @@ +using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Storage.Assets; +using Dalamud.Utility; + +namespace Dalamud.Interface.Internal.SharableTextures; + +/// +/// Represents a texture that may have multiple reference holders (owners). +/// +internal abstract class SharableTexture : IRefCountable +{ + private const int SelfReferenceDurationTicks = 5000; + private const long SelfReferenceExpiryExpired = long.MaxValue; + + private static long instanceCounter; + + private readonly object reviveLock = new(); + + private int refCount; + private long selfReferenceExpiry; + private IDalamudTextureWrap? availableOnAccessWrapForApi9; + + /// + /// Initializes a new instance of the class. + /// + protected SharableTexture() + { + this.InstanceIdForDebug = Interlocked.Increment(ref instanceCounter); + this.refCount = 1; + this.selfReferenceExpiry = Environment.TickCount64 + SelfReferenceDurationTicks; + this.ReviveEnabled = true; + } + + /// + /// Gets a weak reference to an object that demands this objects to be alive. + /// + /// + /// TextureManager must keep references to all shared textures, regardless of whether textures' contents are + /// flushed, because API9 functions demand that the returned textures may be stored so that they can used anytime, + /// possibly reviving a dead-inside object. The object referenced by this property is given out to such use cases, + /// which gets created from . If this no longer points to an alive + /// object, and is null, then this object is not used from API9 use case. + /// + public WeakReference? RevivalPossibility { get; private set; } + + /// + /// Gets the instance ID. Debug use only. + /// + public long InstanceIdForDebug { get; } + + /// + /// Gets the remaining time for self reference in milliseconds. Debug use only. + /// + public long SelfReferenceExpiresInForDebug => + this.selfReferenceExpiry == SelfReferenceExpiryExpired + ? 0 + : Math.Max(0, this.selfReferenceExpiry - Environment.TickCount64); + + /// + /// Gets the reference count. Debug use only. + /// + public int RefCountForDebug => this.refCount; + + /// + /// Gets or sets the underlying texture wrap. + /// + protected abstract Task UnderlyingWrap { get; set; } + + /// + /// Gets or sets the dispose-suppressing wrap for . + /// + protected DisposeSuppressingTextureWrap? DisposeSuppressingWrap { get; set; } + + /// + /// Gets a value indicating whether this instance of supports reviving. + /// + protected bool ReviveEnabled { get; private set; } + + /// + public int AddRef() => this.TryAddRef(out var newRefCount) switch + { + IRefCountable.RefCountResult.StillAlive => newRefCount, + IRefCountable.RefCountResult.AlreadyDisposed => throw new ObjectDisposedException(nameof(SharableTexture)), + IRefCountable.RefCountResult.FinalRelease => throw new InvalidOperationException(), + _ => throw new InvalidOperationException(), + }; + + /// + public int Release() + { + switch (IRefCountable.AlterRefCount(-1, ref this.refCount, out var newRefCount)) + { + case IRefCountable.RefCountResult.StillAlive: + return newRefCount; + + case IRefCountable.RefCountResult.FinalRelease: + this.FinalRelease(); + return newRefCount; + + case IRefCountable.RefCountResult.AlreadyDisposed: + throw new ObjectDisposedException(nameof(SharableTexture)); + + default: + throw new InvalidOperationException(); + } + } + + /// + /// Releases self-reference, if it should expire. + /// + /// Number of the new reference count that may or may not have changed. + public int ReleaseSelfReferenceIfExpired() + { + while (true) + { + var exp = this.selfReferenceExpiry; + if (exp > Environment.TickCount64) + return this.refCount; + + if (exp != Interlocked.CompareExchange(ref this.selfReferenceExpiry, SelfReferenceExpiryExpired, exp)) + continue; + + this.availableOnAccessWrapForApi9 = null; + return this.Release(); + } + } + + /// + /// Disables revival. + /// + public void DisableReviveAndReleaseSelfReference() + { + this.ReviveEnabled = false; + + while (true) + { + var exp = this.selfReferenceExpiry; + if (exp == SelfReferenceExpiryExpired) + return; + + if (exp != Interlocked.CompareExchange(ref this.selfReferenceExpiry, SelfReferenceExpiryExpired, exp)) + continue; + + this.availableOnAccessWrapForApi9 = null; + this.Release(); + break; + } + } + + /// + /// Gets the texture if immediately available. The texture is guarnateed to be available for the rest of the frame. + /// Invocation from non-main thread will exhibit an undefined behavior. + /// + /// The texture if available; null if not. + public IDalamudTextureWrap? GetImmediate() + { + if (this.TryAddRef(out _) != IRefCountable.RefCountResult.StillAlive) + return null; + + var nexp = Environment.TickCount64 + SelfReferenceDurationTicks; + while (true) + { + var exp = this.selfReferenceExpiry; + if (exp != Interlocked.CompareExchange(ref this.selfReferenceExpiry, nexp, exp)) + continue; + + // If below condition is met, the additional reference from above is for the self-reference. + if (exp == SelfReferenceExpiryExpired) + _ = this.AddRef(); + + // Release the reference for rendering, after rendering ImGui. + Service.Get().EnqueueDeferredDispose(this); + + return this.DisposeSuppressingWrap; + } + } + + /// + /// Creates a new reference to this texture. The texture is guaranteed to be available until + /// is called. + /// + /// The task containing the texture. + public Task CreateNewReference() + { + this.AddRef(); + return this.UnderlyingWrap.ContinueWith( + r => (IDalamudTextureWrap)new RefCountableWrappingTextureWrap(r.Result, this)); + } + + /// + /// Gets a texture wrap which ensures that the values will be populated on access. + /// + /// The texture wrap, or null if failed. + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + public IDalamudTextureWrap? GetAvailableOnAccessWrapForApi9() + { + if (this.availableOnAccessWrapForApi9 is not null) + return this.availableOnAccessWrapForApi9; + + lock (this.reviveLock) + { + if (this.availableOnAccessWrapForApi9 is not null) + return this.availableOnAccessWrapForApi9; + + if (this.RevivalPossibility?.TryGetTarget(out this.availableOnAccessWrapForApi9) is true) + return this.availableOnAccessWrapForApi9; + + this.UnderlyingWrap.Wait(); + if (this.UnderlyingWrap.Exception is not null) + return null; + this.availableOnAccessWrapForApi9 = new AvailableOnAccessTextureWrap(this); + this.RevivalPossibility = new(this.availableOnAccessWrapForApi9); + } + + return this.availableOnAccessWrapForApi9; + } + + /// + /// Cleans up this instance of . + /// + protected abstract void FinalRelease(); + + /// + /// Attempts to restore the reference to this texture. + /// + protected abstract void Revive(); + + private IRefCountable.RefCountResult TryAddRef(out int newRefCount) + { + var alterResult = IRefCountable.AlterRefCount(1, ref this.refCount, out newRefCount); + if (alterResult != IRefCountable.RefCountResult.AlreadyDisposed || !this.ReviveEnabled) + return alterResult; + lock (this.reviveLock) + { + alterResult = IRefCountable.AlterRefCount(1, ref this.refCount, out newRefCount); + if (alterResult != IRefCountable.RefCountResult.AlreadyDisposed) + return alterResult; + + this.Revive(); + Interlocked.Increment(ref this.refCount); + + if (this.RevivalPossibility?.TryGetTarget(out var target) is true) + this.availableOnAccessWrapForApi9 = target; + + return IRefCountable.RefCountResult.StillAlive; + } + } + + private sealed class RefCountableWrappingTextureWrap : IDalamudTextureWrap + { + private IDalamudTextureWrap? innerWrap; + private IRefCountable? owner; + + /// + /// Initializes a new instance of the class. + /// + /// The inner wrap. + /// The reference counting owner. + public RefCountableWrappingTextureWrap(IDalamudTextureWrap wrap, IRefCountable owner) + { + this.innerWrap = wrap; + this.owner = owner; + } + + ~RefCountableWrappingTextureWrap() => this.Dispose(); + + /// + public IntPtr ImGuiHandle => this.InnerWrapNonDisposed.ImGuiHandle; + + /// + public int Width => this.InnerWrapNonDisposed.Width; + + /// + public int Height => this.InnerWrapNonDisposed.Height; + + private IDalamudTextureWrap InnerWrapNonDisposed => + this.innerWrap ?? throw new ObjectDisposedException(nameof(RefCountableWrappingTextureWrap)); + + /// + public void Dispose() + { + while (true) + { + if (this.owner is not { } ownerCopy) + return; + if (ownerCopy != Interlocked.CompareExchange(ref this.owner, null, ownerCopy)) + continue; + this.innerWrap = null; + ownerCopy.Release(); + GC.SuppressFinalize(this); + } + } + + /// + public override string ToString() => $"{nameof(RefCountableWrappingTextureWrap)}({this.owner})"; + } + + private sealed class AvailableOnAccessTextureWrap : IDalamudTextureWrap + { + private readonly SharableTexture inner; + + public AvailableOnAccessTextureWrap(SharableTexture inner) => this.inner = inner; + + /// + public IntPtr ImGuiHandle => this.GetActualTexture().ImGuiHandle; + + /// + public int Width => this.GetActualTexture().Width; + + /// + public int Height => this.GetActualTexture().Height; + + /// + public void Dispose() + { + // ignore + } + + /// + public override string ToString() => $"{nameof(AvailableOnAccessTextureWrap)}({this.inner})"; + + private IDalamudTextureWrap GetActualTexture() + { + if (this.inner.GetImmediate() is { } t) + return t; + + this.inner.UnderlyingWrap.Wait(); + return this.inner.DisposeSuppressingWrap ?? Service.Get().Empty4X4; + } + } +} diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index 9f90ea1ad..cd0447b6b 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -1,17 +1,21 @@ +using System.Collections.Concurrent; using System.Collections.Generic; -using System.Diagnostics; using System.IO; -using System.Numerics; +using System.Threading.Tasks; + +using BitFaster.Caching.Lru; using Dalamud.Data; using Dalamud.Game; +using Dalamud.Interface.Internal.SharableTextures; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; using Dalamud.Plugin.Services; +using Dalamud.Storage.Assets; +using Dalamud.Utility; + using Lumina.Data.Files; -using Lumina.Data.Parsing.Tex.Buffers; -using SharpDX.DXGI; namespace Dalamud.Interface.Internal; @@ -27,218 +31,263 @@ namespace Dalamud.Interface.Internal; [ResolveVia] [ResolveVia] #pragma warning restore SA1015 -internal class TextureManager : IDisposable, IServiceType, ITextureProvider, ITextureSubstitutionProvider +internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvider, ITextureSubstitutionProvider { + private const int PathLookupLruCount = 8192; + private const string IconFileFormat = "ui/icon/{0:D3}000/{1}{2:D6}.tex"; private const string HighResolutionIconFileFormat = "ui/icon/{0:D3}000/{1}{2:D6}_hr1.tex"; - private const uint MillisecondsEvictionTime = 2000; - private static readonly ModuleLog Log = new("TEXM"); - private readonly Framework framework; - private readonly DataManager dataManager; - private readonly InterfaceManager im; + [ServiceManager.ServiceDependency] + private readonly Dalamud dalamud = Service.Get(); - private readonly ClientLanguage language; - - private readonly Dictionary activeTextures = new(); + [ServiceManager.ServiceDependency] + private readonly DalamudAssetManager dalamudAssetManager = Service.Get(); - private IDalamudTextureWrap? fallbackTextureWrap; + [ServiceManager.ServiceDependency] + private readonly DataManager dataManager = Service.Get(); + + [ServiceManager.ServiceDependency] + private readonly Framework framework = Service.Get(); + + [ServiceManager.ServiceDependency] + private readonly InterfaceManager interfaceManager = Service.Get(); + + private readonly ConcurrentLru lookupToPath = new(PathLookupLruCount); + private readonly ConcurrentDictionary gamePathTextures = new(); + private readonly ConcurrentDictionary fileSystemTextures = new(); + + private bool disposing; - /// - /// Initializes a new instance of the class. - /// - /// Dalamud instance. - /// Framework instance. - /// DataManager instance. - /// InterfaceManager instance. [ServiceManager.ServiceConstructor] - public TextureManager(Dalamud dalamud, Framework framework, DataManager dataManager, InterfaceManager im) + private TextureManager() { - this.framework = framework; - this.dataManager = dataManager; - this.im = im; - - this.language = (ClientLanguage)dalamud.StartInfo.Language; - this.framework.Update += this.FrameworkOnUpdate; - - Service.GetAsync().ContinueWith(_ => this.CreateFallbackTexture()); } /// public event ITextureSubstitutionProvider.TextureDataInterceptorDelegate? InterceptTexDataLoad; - - /// - /// Get a texture handle for a specific icon. - /// - /// The ID of the icon to load. - /// Options to be considered when loading the icon. - /// - /// The language to be considered when loading the icon, if the icon has versions for multiple languages. - /// If null, default to the game's current language. - /// - /// - /// Not used. This parameter is ignored. - /// - /// - /// Null, if the icon does not exist in the specified configuration, or a texture wrap that can be used - /// to render the icon. - /// - public IDalamudTextureWrap? GetIcon(uint iconId, ITextureProvider.IconFlags flags = ITextureProvider.IconFlags.HiRes, ClientLanguage? language = null, bool keepAlive = false) - { - var path = this.GetIconPath(iconId, flags, language); - return path == null ? null : this.CreateWrap(path); - } /// - /// Get a path for a specific icon's .tex file. + /// Gets all the loaded textures from the game resources. Debug use only. /// - /// The ID of the icon to look up. - /// Options to be considered when loading the icon. - /// - /// The language to be considered when loading the icon, if the icon has versions for multiple languages. - /// If null, default to the game's current language. - /// - /// - /// Null, if the icon does not exist in the specified configuration, or the path to the texture's .tex file, - /// which can be loaded via IDataManager. - /// - public string? GetIconPath(uint iconId, ITextureProvider.IconFlags flags = ITextureProvider.IconFlags.HiRes, ClientLanguage? language = null) + public IReadOnlyDictionary GamePathTextures => this.gamePathTextures; + + /// + /// Gets all the loaded textures from the game resources. Debug use only. + /// + public IReadOnlyDictionary FileSystemTextures => this.fileSystemTextures; + + /// + public void Dispose() + { + if (this.disposing) + return; + + this.disposing = true; + foreach (var v in this.gamePathTextures.Values) + v.DisableReviveAndReleaseSelfReference(); + foreach (var v in this.fileSystemTextures.Values) + v.DisableReviveAndReleaseSelfReference(); + + this.lookupToPath.Clear(); + this.gamePathTextures.Clear(); + this.fileSystemTextures.Clear(); + } + +#pragma warning disable CS0618 // Type or member is obsolete + /// + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + [Obsolete("See interface definition.")] + public string? GetIconPath( + uint iconId, + ITextureProvider.IconFlags flags = ITextureProvider.IconFlags.HiRes, + ClientLanguage? language = null) + => this.TryGetIconPath( + new( + iconId, + (flags & ITextureProvider.IconFlags.ItemHighQuality) != 0, + (flags & ITextureProvider.IconFlags.HiRes) != 0, + language), + out var path) + ? path + : null; + + /// + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + [Obsolete("See interface definition.")] + public IDalamudTextureWrap? GetIcon( + uint iconId, + ITextureProvider.IconFlags flags = ITextureProvider.IconFlags.HiRes, + ClientLanguage? language = null, + bool keepAlive = false) => + this.GetTextureFromGame( + this.lookupToPath.GetOrAdd( + new( + iconId, + (flags & ITextureProvider.IconFlags.ItemHighQuality) != 0, + (flags & ITextureProvider.IconFlags.HiRes) != 0, + language), + this.GetIconPathByValue)); + + /// + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + [Obsolete("See interface definition.")] + public IDalamudTextureWrap? GetTextureFromGame(string path, bool keepAlive = false) => + this.gamePathTextures.GetOrAdd(path, CreateGamePathSharableTexture).GetAvailableOnAccessWrapForApi9(); + + /// + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + [Obsolete("See interface definition.")] + public IDalamudTextureWrap? GetTextureFromFile(FileInfo file, bool keepAlive = false) => + this.fileSystemTextures.GetOrAdd(file.FullName, CreateFileSystemSharableTexture).GetAvailableOnAccessWrapForApi9(); +#pragma warning restore CS0618 // Type or member is obsolete + + /// + public IDalamudTextureWrap ImmediateGetFromGameIcon(in GameIconLookup lookup) => + this.ImmediateGetFromGame(this.lookupToPath.GetOrAdd(lookup, this.GetIconPathByValue)); + + /// + public IDalamudTextureWrap ImmediateGetFromGame(string path) => + this.gamePathTextures.GetOrAdd(path, CreateGamePathSharableTexture).GetImmediate() + ?? this.dalamudAssetManager.Empty4X4; + + /// + public IDalamudTextureWrap ImmediateGetFromFile(string file) => + this.fileSystemTextures.GetOrAdd(file, CreateFileSystemSharableTexture).GetImmediate() + ?? this.dalamudAssetManager.Empty4X4; + + /// + public Task GetFromGameIconAsync(in GameIconLookup lookup) => + this.GetFromGameAsync(this.lookupToPath.GetOrAdd(lookup, this.GetIconPathByValue)); + + /// + public Task GetFromGameAsync(string path) => + this.gamePathTextures.GetOrAdd(path, CreateGamePathSharableTexture).CreateNewReference(); + + /// + public Task GetFromFileAsync(string file) => + this.fileSystemTextures.GetOrAdd(file, CreateFileSystemSharableTexture).CreateNewReference(); + + /// + public Task GetFromImageAsync(ReadOnlyMemory bytes) => + Task.Run( + () => this.interfaceManager.LoadImage(bytes.ToArray()) + ?? throw new("Failed to load image because of an unknown reason.")); + + /// + 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 IDalamudTextureWrap GetFromRaw(RawImageSpecification specs, ReadOnlySpan bytes) => + this.interfaceManager.LoadImageFromDxgiFormat( + bytes, + specs.Pitch, + specs.Width, + specs.Height, + (SharpDX.DXGI.Format)specs.DxgiFormat); + + /// + public Task GetFromRawAsync(RawImageSpecification specs, ReadOnlyMemory bytes) => + Task.Run(() => this.GetFromRaw(specs, bytes.Span)); + + /// + public async 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()); + } + + /// + public IDalamudTextureWrap GetTexture(TexFile file) => this.interfaceManager.LoadImageFromTexFile(file); + + /// + public bool TryGetIconPath(in GameIconLookup lookup, out string path) { - var hiRes = flags.HasFlag(ITextureProvider.IconFlags.HiRes); - // 1. Item - var path = FormatIconPath( - iconId, - flags.HasFlag(ITextureProvider.IconFlags.ItemHighQuality) ? "hq/" : string.Empty, - hiRes); + path = FormatIconPath( + lookup.IconId, + lookup.ItemHq ? "hq/" : string.Empty, + lookup.HiRes); if (this.dataManager.FileExists(path)) - return path; - - language ??= this.language; - var languageFolder = language switch + return true; + + var languageFolder = (lookup.Language ?? (ClientLanguage)(int)this.dalamud.StartInfo.Language) switch { ClientLanguage.Japanese => "ja/", ClientLanguage.English => "en/", ClientLanguage.German => "de/", ClientLanguage.French => "fr/", - _ => throw new ArgumentOutOfRangeException(nameof(language), $"Unknown Language: {language}"), + _ => null, }; - - // 2. Regular icon, with language, hi-res - path = FormatIconPath( - iconId, - languageFolder, - hiRes); - if (this.dataManager.FileExists(path)) - return path; - if (hiRes) + if (languageFolder is not null) { - // 3. Regular icon, with language, no hi-res + // 2. Regular icon, with language, hi-res path = FormatIconPath( - iconId, + lookup.IconId, languageFolder, - false); + lookup.HiRes); if (this.dataManager.FileExists(path)) - return path; + return true; + + if (lookup.HiRes) + { + // 3. Regular icon, with language, no hi-res + path = FormatIconPath( + lookup.IconId, + languageFolder, + false); + if (this.dataManager.FileExists(path)) + return true; + } } // 4. Regular icon, without language, hi-res path = FormatIconPath( - iconId, + lookup.IconId, null, - hiRes); + lookup.HiRes); if (this.dataManager.FileExists(path)) - return path; - + return true; + // 4. Regular icon, without language, no hi-res - if (hiRes) + if (lookup.HiRes) { path = FormatIconPath( - iconId, + lookup.IconId, null, false); if (this.dataManager.FileExists(path)) - return path; + return true; } - return null; + return false; } - /// - /// Get a texture handle for the texture at the specified path. - /// You may only specify paths in the game's VFS. - /// - /// The path to the texture in the game's VFS. - /// Not used. This parameter is ignored. - /// Null, if the icon does not exist, or a texture wrap that can be used to render the texture. - public IDalamudTextureWrap? GetTextureFromGame(string path, bool keepAlive = false) - { - ArgumentException.ThrowIfNullOrEmpty(path); - - if (Path.IsPathRooted(path)) - throw new ArgumentException("Use GetTextureFromFile() to load textures directly from a file.", nameof(path)); - - return !this.dataManager.FileExists(path) ? null : this.CreateWrap(path); - } - - /// - /// Get a texture handle for the image or texture, specified by the passed FileInfo. - /// You may only specify paths on the native file system. - /// - /// This API can load .png and .tex files. - /// - /// The FileInfo describing the image or texture file. - /// Not used. This parameter is ignored. - /// Null, if the file does not exist, or a texture wrap that can be used to render the texture. - public IDalamudTextureWrap? GetTextureFromFile(FileInfo file, bool keepAlive = false) - { - ArgumentNullException.ThrowIfNull(file); - return !file.Exists ? null : this.CreateWrap(file.FullName); - } - - /// - /// Get a texture handle for the specified Lumina TexFile. - /// - /// The texture to obtain a handle to. - /// A texture wrap that can be used to render the texture. - /// Thrown when the graphics system is not available yet. Relevant for plugins when LoadRequiredState is set to 0 or 1. - /// Thrown when the given is not supported. Most likely is that the file is corrupt. - public IDalamudTextureWrap GetTexture(TexFile file) - { - ArgumentNullException.ThrowIfNull(file); - - if (!this.im.IsReady) - throw new InvalidOperationException("Cannot create textures before scene is ready"); - - var buffer = file.TextureBuffer; - var bpp = 1 << (((int)file.Header.Format & (int)TexFile.TextureFormat.BppMask) >> - (int)TexFile.TextureFormat.BppShift); - - var (dxgiFormat, conversion) = TexFile.GetDxgiFormatFromTextureFormat(file.Header.Format, false); - if (conversion != TexFile.DxgiFormatConversion.NoConversion || !this.im.SupportsDxgiFormat((Format)dxgiFormat)) - { - dxgiFormat = (int)Format.B8G8R8A8_UNorm; - buffer = buffer.Filter(0, 0, TexFile.TextureFormat.B8G8R8A8); - bpp = 32; - } - - var pitch = buffer is BlockCompressionTextureBuffer - ? Math.Max(1, (buffer.Width + 3) / 4) * 2 * bpp - : ((buffer.Width * bpp) + 7) / 8; - - return this.im.LoadImageFromDxgiFormat(buffer.RawData, pitch, buffer.Width, buffer.Height, (Format)dxgiFormat); - } + /// + public string GetIconPath(in GameIconLookup lookup) => + this.TryGetIconPath(lookup, out var path) ? path : throw new FileNotFoundException(); /// public string GetSubstitutedPath(string originalPath) { if (this.InterceptTexDataLoad == null) return originalPath; - + string? interceptPath = null; this.InterceptTexDataLoad.Invoke(originalPath, ref interceptPath); @@ -254,261 +303,45 @@ internal class TextureManager : IDisposable, IServiceType, ITextureProvider, ITe /// public void InvalidatePaths(IEnumerable paths) { - lock (this.activeTextures) + foreach (var path in paths) { - foreach (var path in paths) - { - if (!this.activeTextures.TryGetValue(path, out var info) || info == null) - continue; - - info.Wrap?.Dispose(); - info.Wrap = null; - } + if (this.gamePathTextures.TryRemove(path, out var r)) + r.DisableReviveAndReleaseSelfReference(); } } - - /// - public void Dispose() - { - this.fallbackTextureWrap?.Dispose(); - this.framework.Update -= this.FrameworkOnUpdate; - - if (this.activeTextures.Count == 0) - return; - Log.Verbose("Disposing {Num} left behind textures.", this.activeTextures.Count); - - foreach (var activeTexture in this.activeTextures) - { - activeTexture.Value.Wrap?.Dispose(); - } - - this.activeTextures.Clear(); - } + private static SharableTexture CreateGamePathSharableTexture(string gamePath) => + new GamePathSharableTexture(gamePath); - /// - /// Get texture info. - /// - /// Path to the texture. - /// - /// If true, exceptions caused by texture load will not be caught. - /// If false, exceptions will be caught and a dummy texture will be returned to prevent plugins from using invalid texture handles. - /// - /// Info object storing texture metadata. - internal TextureInfo GetInfo(string path, bool rethrow = false) - { - TextureInfo? info; - lock (this.activeTextures) - { - // This either is a new texture, or it had been evicted and now wants to be drawn again. - if (!this.activeTextures.TryGetValue(path, out info)) - { - info = new TextureInfo(); - this.activeTextures.Add(path, info); - } - - if (info == null) - throw new Exception("null info in activeTextures"); - - info.LastAccess = DateTime.UtcNow; - - if (info is { Wrap: not null }) - return info; - } - - if (!this.im.IsReady) - throw new InvalidOperationException("Cannot create textures before scene is ready"); - - // Substitute the path here for loading, instead of when getting the respective TextureInfo - path = this.GetSubstitutedPath(path); - - IDalamudTextureWrap? wrap; - try - { - // We want to load this from the disk, probably, if the path has a root - // Not sure if this can cause issues with e.g. network drives, might have to rethink - // and add a flag instead if it does. - if (Path.IsPathRooted(path)) - { - if (Path.GetExtension(path) == ".tex") - { - // Attempt to load via Lumina - var file = this.dataManager.GameData.GetFileFromDisk(path); - wrap = this.GetTexture(file); - Log.Verbose("Texture {Path} loaded FS via Lumina", path); - } - else - { - // Attempt to load image - wrap = this.im.LoadImage(path); - Log.Verbose("Texture {Path} loaded FS via LoadImage", path); - } - } - else - { - // Load regularly from dats - var file = this.dataManager.GetFile(path); - if (file == null) - throw new Exception("Could not load TexFile from dat."); - - wrap = this.GetTexture(file); - Log.Verbose("Texture {Path} loaded from SqPack", path); - } - - if (wrap == null) - throw new Exception("Could not create texture"); - - // TODO: We could support this, but I don't think it's worth it at the moment. - var extents = new Vector2(wrap.Width, wrap.Height); - if (info.Extents != Vector2.Zero && info.Extents != extents) - Log.Warning("Texture at {Path} changed size between reloads, this is currently not supported.", path); - - info.Extents = extents; - } - catch (Exception e) - { - Log.Error(e, "Could not load texture from {Path}", path); - - // When creating the texture initially, we want to be able to pass errors back to the plugin - if (rethrow) - throw; - - // This means that the load failed due to circumstances outside of our control, - // and we can't do anything about it. Return a dummy texture so that the plugin still - // has something to draw. - wrap = this.fallbackTextureWrap; - - // Prevent divide-by-zero - if (info.Extents == Vector2.Zero) - info.Extents = Vector2.One; - } - - info.Wrap = wrap; - return info; - } + private static SharableTexture CreateFileSystemSharableTexture(string fileSystemPath) => + new FileSystemSharableTexture(fileSystemPath); private static string FormatIconPath(uint iconId, string? type, bool highResolution) { var format = highResolution ? HighResolutionIconFileFormat : IconFileFormat; - + type ??= string.Empty; if (type.Length > 0 && !type.EndsWith("/")) type += "/"; return string.Format(format, iconId / 1000, type, iconId); } - - private TextureManagerTextureWrap? CreateWrap(string path) + + private void FrameworkOnUpdate(IFramework unused) { - lock (this.activeTextures) + foreach (var (k, v) in this.gamePathTextures) { - // This will create the texture. - // That's fine, it's probably used immediately and this will let the plugin catch load errors. - var info = this.GetInfo(path, rethrow: true); - - return new TextureManagerTextureWrap(path, info.Extents, this); + if (v.ReleaseSelfReferenceIfExpired() == 0 && v.RevivalPossibility?.TryGetTarget(out _) is not true) + _ = this.gamePathTextures.TryRemove(k, out _); + } + + foreach (var (k, v) in this.fileSystemTextures) + { + if (v.ReleaseSelfReferenceIfExpired() == 0 && v.RevivalPossibility?.TryGetTarget(out _) is not true) + _ = this.fileSystemTextures.TryRemove(k, out _); } } - private void FrameworkOnUpdate(IFramework fw) - { - lock (this.activeTextures) - { - var toRemove = new List(); - - foreach (var texInfo in this.activeTextures) - { - if (texInfo.Value.Wrap == null) - continue; - - if (DateTime.UtcNow - texInfo.Value.LastAccess > TimeSpan.FromMilliseconds(MillisecondsEvictionTime)) - { - Log.Verbose("Evicting {Path} since too old", texInfo.Key); - texInfo.Value.Wrap.Dispose(); - texInfo.Value.Wrap = null; - toRemove.Add(texInfo.Key); - } - } - - foreach (var path in toRemove) - { - this.activeTextures.Remove(path); - } - } - } - - private void CreateFallbackTexture() - { - var fallbackTexBytes = new byte[] { 0xFF, 0x00, 0xDC, 0xFF }; - this.fallbackTextureWrap = this.im.LoadImageRaw(fallbackTexBytes, 1, 1, 4); - Debug.Assert(this.fallbackTextureWrap != null, "this.fallbackTextureWrap != null"); - } - - /// - /// Internal representation of a managed texture. - /// - internal class TextureInfo - { - /// - /// Gets or sets the actual texture wrap. May be unpopulated. - /// - public IDalamudTextureWrap? Wrap { get; set; } - - /// - /// Gets or sets the time the texture was last accessed. - /// - public DateTime LastAccess { get; set; } - - /// - /// Gets or sets the extents of the texture. - /// - public Vector2 Extents { get; set; } - } -} - -/// -/// Wrap. -/// -internal class TextureManagerTextureWrap : IDalamudTextureWrap -{ - private readonly TextureManager manager; - private readonly string path; - - /// - /// Initializes a new instance of the class. - /// - /// The path to the texture. - /// The extents of the texture. - /// Manager that we obtained this from. - internal TextureManagerTextureWrap(string path, Vector2 extents, TextureManager manager) - { - this.path = path; - this.manager = manager; - this.Width = (int)extents.X; - this.Height = (int)extents.Y; - } - - /// - public IntPtr ImGuiHandle => !this.IsDisposed ? - this.manager.GetInfo(this.path).Wrap!.ImGuiHandle : - throw new InvalidOperationException("Texture already disposed. You may not render it."); - - /// - public int Width { get; private set; } - - /// - public int Height { get; private set; } - - /// - /// Gets a value indicating whether or not this wrap has already been disposed. - /// If true, the handle may be invalid. - /// - internal bool IsDisposed { get; private set; } - - /// - public void Dispose() - { - this.IsDisposed = true; - // This is a no-op. The manager cleans up textures that are not being drawn. - } + private string GetIconPathByValue(GameIconLookup lookup) => + this.TryGetIconPath(lookup, out var path) ? path : throw new FileNotFoundException(); } diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index d260868a0..ca0ecb71c 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -15,6 +15,7 @@ using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Plugin.Internal.Types; +using Dalamud.Plugin.Services; using Dalamud.Utility; using ImGuiNET; using ImGuiScene; @@ -383,6 +384,8 @@ public sealed class UiBuilder : IDisposable /// /// The full filepath to the image. /// A object wrapping the created image. Use inside ImGui.Image(). + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + [Obsolete($"Use {nameof(ITextureProvider.GetFromFileAsync)}.")] public IDalamudTextureWrap LoadImage(string filePath) => this.InterfaceManagerWithScene?.LoadImage(filePath) ?? throw new InvalidOperationException("Load failed."); @@ -392,6 +395,8 @@ public sealed class UiBuilder : IDisposable /// /// A byte array containing the raw image data. /// A object wrapping the created image. Use inside ImGui.Image(). + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + [Obsolete($"Use {nameof(ITextureProvider.GetFromImageAsync)}.")] public IDalamudTextureWrap LoadImage(byte[] imageData) => this.InterfaceManagerWithScene?.LoadImage(imageData) ?? throw new InvalidOperationException("Load failed."); @@ -404,6 +409,8 @@ public sealed class UiBuilder : IDisposable /// The height of the image contained in . /// The number of channels (bytes per pixel) of the image contained in . This should usually be 4. /// A object wrapping the created image. Use inside ImGui.Image(). + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + [Obsolete($"Use {nameof(ITextureProvider.GetFromRaw)} or {nameof(ITextureProvider.GetFromRawAsync)}.")] public IDalamudTextureWrap LoadImageRaw(byte[] imageData, int width, int height, int numChannels) => this.InterfaceManagerWithScene?.LoadImageRaw(imageData, width, height, numChannels) ?? throw new InvalidOperationException("Load failed."); @@ -421,6 +428,8 @@ public sealed class UiBuilder : IDisposable /// /// The full filepath to the image. /// A object wrapping the created image. Use inside ImGui.Image(). + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + [Obsolete($"Use {nameof(ITextureProvider.GetFromFileAsync)}.")] public Task LoadImageAsync(string filePath) => Task.Run( async () => (await this.InterfaceManagerWithSceneAsync).LoadImage(filePath) @@ -431,6 +440,8 @@ public sealed class UiBuilder : IDisposable /// /// A byte array containing the raw image data. /// A object wrapping the created image. Use inside ImGui.Image(). + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + [Obsolete($"Use {nameof(ITextureProvider.GetFromImageAsync)}.")] public Task LoadImageAsync(byte[] imageData) => Task.Run( async () => (await this.InterfaceManagerWithSceneAsync).LoadImage(imageData) @@ -444,6 +455,8 @@ public sealed class UiBuilder : IDisposable /// The height of the image contained in . /// The number of channels (bytes per pixel) of the image contained in . This should usually be 4. /// A object wrapping the created image. Use inside ImGui.Image(). + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + [Obsolete($"Use {nameof(ITextureProvider.GetFromRawAsync)}.")] public Task LoadImageRawAsync(byte[] imageData, int width, int height, int numChannels) => Task.Run( async () => (await this.InterfaceManagerWithSceneAsync).LoadImageRaw(imageData, width, height, numChannels) diff --git a/Dalamud/Plugin/Services/GameIconLookup.cs b/Dalamud/Plugin/Services/GameIconLookup.cs new file mode 100644 index 000000000..4ad710ab1 --- /dev/null +++ b/Dalamud/Plugin/Services/GameIconLookup.cs @@ -0,0 +1,20 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Dalamud.Plugin.Services; + +/// +/// Represents a lookup for a game icon. +/// +/// The icon ID. +/// Whether the HQ icon is requested, where HQ is in the context of items. +/// Whether the high-resolution icon is requested. +/// The language of the icon to load. +[SuppressMessage( + "StyleCop.CSharp.NamingRules", + "SA1313:Parameter names should begin with lower-case letter", + Justification = "no")] +public record struct GameIconLookup( + uint IconId, + bool ItemHq = false, + bool HiRes = true, + ClientLanguage? Language = null); diff --git a/Dalamud/Plugin/Services/ITextureProvider.Api9.cs b/Dalamud/Plugin/Services/ITextureProvider.Api9.cs new file mode 100644 index 000000000..9592da711 --- /dev/null +++ b/Dalamud/Plugin/Services/ITextureProvider.Api9.cs @@ -0,0 +1,98 @@ +using System.IO; + +using Dalamud.Interface.Internal; +using Dalamud.Utility; + +namespace Dalamud.Plugin.Services; + +/// +/// Service that grants you access to textures you may render via ImGui. +/// +public partial interface ITextureProvider +{ + /// + /// Flags describing the icon you wish to receive. + /// + [Obsolete($"Use {nameof(GameIconLookup)}.")] + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + [Flags] + public enum IconFlags + { + /// + /// Low-resolution, standard quality icon. + /// + None = 0, + + /// + /// If this icon is an item icon, and it has a high-quality variant, receive the high-quality version. + /// Null if the item does not have a high-quality variant. + /// + ItemHighQuality = 1 << 0, + + /// + /// Get the hi-resolution version of the icon, if it exists. + /// + HiRes = 1 << 1, + } + + /// + /// Get a texture handle for a specific icon. + /// + /// The ID of the icon to load. + /// Options to be considered when loading the icon. + /// + /// The language to be considered when loading the icon, if the icon has versions for multiple languages. + /// If null, default to the game's current language. + /// + /// + /// Not used. This parameter is ignored. + /// + /// + /// Null, if the icon does not exist in the specified configuration, or a texture wrap that can be used + /// to render the icon. + /// + [Obsolete($"Use {nameof(ImmediateGetFromGameIcon)} or {nameof(GetFromGameIconAsync)}.")] + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + public IDalamudTextureWrap? GetIcon(uint iconId, IconFlags flags = IconFlags.HiRes, ClientLanguage? language = null, bool keepAlive = false); + + /// + /// Get a path for a specific icon's .tex file. + /// + /// The ID of the icon to look up. + /// Options to be considered when loading the icon. + /// + /// The language to be considered when loading the icon, if the icon has versions for multiple languages. + /// If null, default to the game's current language. + /// + /// + /// Null, if the icon does not exist in the specified configuration, or the path to the texture's .tex file, + /// which can be loaded via IDataManager. + /// + [Obsolete($"Use {nameof(TryGetIconPath)} or {nameof(GetIconPath)}({nameof(GameIconLookup)}).")] + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + public string? GetIconPath(uint iconId, IconFlags flags = IconFlags.HiRes, ClientLanguage? language = null); + + /// + /// Get a texture handle for the texture at the specified path. + /// You may only specify paths in the game's VFS. + /// + /// The path to the texture in the game's VFS. + /// Not used. This parameter is ignored. + /// Null, if the icon does not exist, or a texture wrap that can be used to render the texture. + [Obsolete($"Use {nameof(ImmediateGetFromGame)} or {nameof(GetFromGameAsync)}.")] + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + public IDalamudTextureWrap? GetTextureFromGame(string path, bool keepAlive = false); + + /// + /// Get a texture handle for the image or texture, specified by the passed FileInfo. + /// You may only specify paths on the native file system. + /// + /// This API can load .png and .tex files. + /// + /// The FileInfo describing the image or texture file. + /// Not used. This parameter is ignored. + /// Null, if the file does not exist, or a texture wrap that can be used to render the texture. + [Obsolete($"Use {nameof(ImmediateGetFromFile)} or {nameof(GetFromFileAsync)}.")] + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + public IDalamudTextureWrap? GetTextureFromFile(FileInfo file, bool keepAlive = false); +} diff --git a/Dalamud/Plugin/Services/ITextureProvider.cs b/Dalamud/Plugin/Services/ITextureProvider.cs index f91d4ee8e..e004c8be7 100644 --- a/Dalamud/Plugin/Services/ITextureProvider.cs +++ b/Dalamud/Plugin/Services/ITextureProvider.cs @@ -1,7 +1,9 @@ -using System; +using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Threading.Tasks; using Dalamud.Interface.Internal; + using Lumina.Data.Files; namespace Dalamud.Plugin.Services; @@ -9,86 +11,107 @@ namespace Dalamud.Plugin.Services; /// /// Service that grants you access to textures you may render via ImGui. /// -public interface ITextureProvider +public partial interface ITextureProvider { - /// - /// Flags describing the icon you wish to receive. + /// Gets the corresponding game icon for use with the current frame. + /// The icon specifier. + /// An instance of that is guaranteed to be available for the current + /// frame being drawn. + /// will be ignored.
+ /// 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); + + /// Gets a texture from a file shipped as a part of the game resources for use with the current frame. /// - [Flags] - public enum IconFlags - { - /// - /// Low-resolution, standard quality icon. - /// - None = 0, - - /// - /// If this icon is an item icon, and it has a high-quality variant, receive the high-quality version. - /// Null if the item does not have a high-quality variant. - /// - ItemHighQuality = 1 << 0, - - /// - /// Get the hi-resolution version of the icon, if it exists. - /// - HiRes = 1 << 1, - } - - /// - /// Get a texture handle for a specific icon. - /// - /// The ID of the icon to load. - /// Options to be considered when loading the icon. - /// - /// The language to be considered when loading the icon, if the icon has versions for multiple languages. - /// If null, default to the game's current language. - /// - /// - /// Not used. This parameter is ignored. - /// - /// - /// Null, if the icon does not exist in the specified configuration, or a texture wrap that can be used - /// to render the icon. - /// - public IDalamudTextureWrap? GetIcon(uint iconId, IconFlags flags = IconFlags.HiRes, ClientLanguage? language = null, bool keepAlive = false); + /// The game-internal path to a .tex, .atex, or an image file such as .png. + /// An instance of that is guaranteed to be available for the current + /// frame being drawn. + /// will be ignored.
+ /// 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); + + /// Gets a texture from a file on the filesystem for use with the current frame. + /// The filesystem path to a .tex, .atex, or an image file such as .png. + /// An instance of that is guaranteed to be available for the current + /// frame being drawn. + /// will be ignored.
+ /// 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); + + /// Gets the corresponding game icon for use with the current frame. + /// The icon specifier. + /// A containing the loaded texture on success. Dispose after use. + public Task GetFromGameIconAsync(in GameIconLookup lookup); + + /// 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. + /// A containing the loaded texture on success. Dispose after use. + public Task GetFromGameAsync(string path); + + /// Gets a texture from a file on the filesystem. + /// The filesystem path to a .tex, .atex, or an image file such as .png. + /// A containing the loaded texture on success. Dispose after use. + public Task GetFromFileAsync(string file); + + /// 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. + /// A containing the loaded texture on success. Dispose after use. + public Task GetFromImageAsync(ReadOnlyMemory bytes); + + /// 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. + /// A containing the loaded texture on success. Dispose after use. + public Task GetFromImageAsync(Stream stream, bool leaveOpen = false); + + /// Gets a texture from the given bytes, interpreting it as a raw bitmap. + /// The specifications for the raw bitmap. + /// The bytes to load. + /// The texture loaded from the supplied raw bitmap. Dispose after use. + public IDalamudTextureWrap GetFromRaw(RawImageSpecification specs, ReadOnlySpan bytes); + + /// Gets a texture from the given bytes, interpreting it as a raw bitmap. + /// The specifications for the raw bitmap. + /// The bytes to load. + /// A containing the loaded texture on success. Dispose after use. + public Task GetFromRawAsync(RawImageSpecification specs, ReadOnlyMemory bytes); + + /// 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. + /// A containing the loaded texture on success. Dispose after use. + public Task GetFromRawAsync( + RawImageSpecification specs, + Stream stream, + bool leaveOpen = false); /// /// Get a path for a specific icon's .tex file. /// - /// The ID of the icon to look up. - /// Options to be considered when loading the icon. - /// - /// The language to be considered when loading the icon, if the icon has versions for multiple languages. - /// If null, default to the game's current language. - /// - /// - /// Null, if the icon does not exist in the specified configuration, or the path to the texture's .tex file, - /// which can be loaded via IDataManager. - /// - public string? GetIconPath(uint iconId, IconFlags flags = IconFlags.HiRes, ClientLanguage? language = null); - + /// The icon lookup. + /// The path to the icon. + /// If a corresponding file could not be found. + public string GetIconPath(in GameIconLookup lookup); + /// - /// Get a texture handle for the texture at the specified path. - /// You may only specify paths in the game's VFS. + /// Gets the path of an icon. /// - /// The path to the texture in the game's VFS. - /// Not used. This parameter is ignored. - /// Null, if the icon does not exist, or a texture wrap that can be used to render the texture. - public IDalamudTextureWrap? GetTextureFromGame(string path, bool keepAlive = false); + /// 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); /// - /// Get a texture handle for the image or texture, specified by the passed FileInfo. - /// You may only specify paths on the native file system. - /// - /// This API can load .png and .tex files. - /// - /// The FileInfo describing the image or texture file. - /// Not used. This parameter is ignored. - /// Null, if the file does not exist, or a texture wrap that can be used to render the texture. - public IDalamudTextureWrap? GetTextureFromFile(FileInfo file, bool keepAlive = false); - - /// - /// Get a texture handle for the specified Lumina TexFile. + /// Get a texture handle for the specified Lumina . /// /// The texture to obtain a handle to. /// A texture wrap that can be used to render the texture. diff --git a/Dalamud/Plugin/Services/RawImageSpecification.cs b/Dalamud/Plugin/Services/RawImageSpecification.cs new file mode 100644 index 000000000..696b3d6b6 --- /dev/null +++ b/Dalamud/Plugin/Services/RawImageSpecification.cs @@ -0,0 +1,216 @@ +using System.Diagnostics.CodeAnalysis; + +using TerraFX.Interop.DirectX; + +namespace Dalamud.Plugin.Services; + +/// +/// Describes a raw image. +/// +/// The width of the image. +/// The height of the image. +/// The pitch of the image. +/// The format of the image. See DXGI_FORMAT. +[SuppressMessage( + "StyleCop.CSharp.NamingRules", + "SA1313:Parameter names should begin with lower-case letter", + Justification = "no")] +public record struct RawImageSpecification(int Width, int Height, int Pitch, int DxgiFormat) +{ + /// + /// Creates a new instance of record using the given resolution and pixel + /// format. Pitch will be automatically calculated. + /// + /// The width. + /// The height. + /// The format. + /// The new instance. + public static RawImageSpecification From(int width, int height, int format) + { + int bitsPerPixel; + var isBlockCompression = false; + switch ((DXGI_FORMAT)format) + { + case DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_TYPELESS: + case DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_FLOAT: + case DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_UINT: + case DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_SINT: + bitsPerPixel = 128; + break; + case DXGI_FORMAT.DXGI_FORMAT_R32G32B32_TYPELESS: + case DXGI_FORMAT.DXGI_FORMAT_R32G32B32_FLOAT: + case DXGI_FORMAT.DXGI_FORMAT_R32G32B32_UINT: + case DXGI_FORMAT.DXGI_FORMAT_R32G32B32_SINT: + bitsPerPixel = 96; + break; + case DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_TYPELESS: + case DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_FLOAT: + case DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_UNORM: + case DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_UINT: + case DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_SNORM: + case DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_SINT: + case DXGI_FORMAT.DXGI_FORMAT_R32G32_TYPELESS: + case DXGI_FORMAT.DXGI_FORMAT_R32G32_FLOAT: + case DXGI_FORMAT.DXGI_FORMAT_R32G32_UINT: + case DXGI_FORMAT.DXGI_FORMAT_R32G32_SINT: + case DXGI_FORMAT.DXGI_FORMAT_R32G8X24_TYPELESS: + case DXGI_FORMAT.DXGI_FORMAT_D32_FLOAT_S8X24_UINT: + case DXGI_FORMAT.DXGI_FORMAT_R32_FLOAT_X8X24_TYPELESS: + case DXGI_FORMAT.DXGI_FORMAT_X32_TYPELESS_G8X24_UINT: + bitsPerPixel = 64; + break; + case DXGI_FORMAT.DXGI_FORMAT_R10G10B10A2_TYPELESS: + case DXGI_FORMAT.DXGI_FORMAT_R10G10B10A2_UNORM: + case DXGI_FORMAT.DXGI_FORMAT_R10G10B10A2_UINT: + case DXGI_FORMAT.DXGI_FORMAT_R11G11B10_FLOAT: + case DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_TYPELESS: + case DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM: + case DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM_SRGB: + case DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UINT: + case DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_SNORM: + case DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_SINT: + case DXGI_FORMAT.DXGI_FORMAT_R16G16_TYPELESS: + case DXGI_FORMAT.DXGI_FORMAT_R16G16_FLOAT: + case DXGI_FORMAT.DXGI_FORMAT_R16G16_UNORM: + case DXGI_FORMAT.DXGI_FORMAT_R16G16_UINT: + case DXGI_FORMAT.DXGI_FORMAT_R16G16_SNORM: + case DXGI_FORMAT.DXGI_FORMAT_R16G16_SINT: + case DXGI_FORMAT.DXGI_FORMAT_R32_TYPELESS: + case DXGI_FORMAT.DXGI_FORMAT_D32_FLOAT: + case DXGI_FORMAT.DXGI_FORMAT_R32_FLOAT: + case DXGI_FORMAT.DXGI_FORMAT_R32_UINT: + case DXGI_FORMAT.DXGI_FORMAT_R32_SINT: + case DXGI_FORMAT.DXGI_FORMAT_R24G8_TYPELESS: + case DXGI_FORMAT.DXGI_FORMAT_D24_UNORM_S8_UINT: + case DXGI_FORMAT.DXGI_FORMAT_R24_UNORM_X8_TYPELESS: + case DXGI_FORMAT.DXGI_FORMAT_X24_TYPELESS_G8_UINT: + bitsPerPixel = 32; + break; + case DXGI_FORMAT.DXGI_FORMAT_R8G8_TYPELESS: + case DXGI_FORMAT.DXGI_FORMAT_R8G8_UNORM: + case DXGI_FORMAT.DXGI_FORMAT_R8G8_UINT: + case DXGI_FORMAT.DXGI_FORMAT_R8G8_SNORM: + case DXGI_FORMAT.DXGI_FORMAT_R8G8_SINT: + case DXGI_FORMAT.DXGI_FORMAT_R16_TYPELESS: + case DXGI_FORMAT.DXGI_FORMAT_R16_FLOAT: + case DXGI_FORMAT.DXGI_FORMAT_D16_UNORM: + case DXGI_FORMAT.DXGI_FORMAT_R16_UNORM: + case DXGI_FORMAT.DXGI_FORMAT_R16_UINT: + case DXGI_FORMAT.DXGI_FORMAT_R16_SNORM: + case DXGI_FORMAT.DXGI_FORMAT_R16_SINT: + bitsPerPixel = 16; + break; + case DXGI_FORMAT.DXGI_FORMAT_R8_TYPELESS: + case DXGI_FORMAT.DXGI_FORMAT_R8_UNORM: + case DXGI_FORMAT.DXGI_FORMAT_R8_UINT: + case DXGI_FORMAT.DXGI_FORMAT_R8_SNORM: + case DXGI_FORMAT.DXGI_FORMAT_R8_SINT: + case DXGI_FORMAT.DXGI_FORMAT_A8_UNORM: + bitsPerPixel = 8; + break; + case DXGI_FORMAT.DXGI_FORMAT_R1_UNORM: + bitsPerPixel = 1; + break; + case DXGI_FORMAT.DXGI_FORMAT_R9G9B9E5_SHAREDEXP: + case DXGI_FORMAT.DXGI_FORMAT_R8G8_B8G8_UNORM: + case DXGI_FORMAT.DXGI_FORMAT_G8R8_G8B8_UNORM: + throw new NotSupportedException(); + case DXGI_FORMAT.DXGI_FORMAT_BC1_TYPELESS: + case DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM: + case DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM_SRGB: + bitsPerPixel = 4; + isBlockCompression = true; + break; + case DXGI_FORMAT.DXGI_FORMAT_BC2_TYPELESS: + case DXGI_FORMAT.DXGI_FORMAT_BC2_UNORM: + case DXGI_FORMAT.DXGI_FORMAT_BC2_UNORM_SRGB: + case DXGI_FORMAT.DXGI_FORMAT_BC3_TYPELESS: + case DXGI_FORMAT.DXGI_FORMAT_BC3_UNORM: + case DXGI_FORMAT.DXGI_FORMAT_BC3_UNORM_SRGB: + bitsPerPixel = 8; + isBlockCompression = true; + break; + case DXGI_FORMAT.DXGI_FORMAT_BC4_TYPELESS: + case DXGI_FORMAT.DXGI_FORMAT_BC4_UNORM: + case DXGI_FORMAT.DXGI_FORMAT_BC4_SNORM: + bitsPerPixel = 4; + isBlockCompression = true; + break; + case DXGI_FORMAT.DXGI_FORMAT_BC5_TYPELESS: + case DXGI_FORMAT.DXGI_FORMAT_BC5_UNORM: + case DXGI_FORMAT.DXGI_FORMAT_BC5_SNORM: + bitsPerPixel = 8; + isBlockCompression = true; + break; + case DXGI_FORMAT.DXGI_FORMAT_B5G6R5_UNORM: + case DXGI_FORMAT.DXGI_FORMAT_B5G5R5A1_UNORM: + bitsPerPixel = 16; + break; + case DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM: + case DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM: + case DXGI_FORMAT.DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM: + case DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_TYPELESS: + case DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM_SRGB: + case DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_TYPELESS: + case DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM_SRGB: + bitsPerPixel = 32; + break; + case DXGI_FORMAT.DXGI_FORMAT_BC6H_TYPELESS: + case DXGI_FORMAT.DXGI_FORMAT_BC6H_UF16: + case DXGI_FORMAT.DXGI_FORMAT_BC6H_SF16: + case DXGI_FORMAT.DXGI_FORMAT_BC7_TYPELESS: + case DXGI_FORMAT.DXGI_FORMAT_BC7_UNORM: + case DXGI_FORMAT.DXGI_FORMAT_BC7_UNORM_SRGB: + bitsPerPixel = 8; + isBlockCompression = true; + break; + case DXGI_FORMAT.DXGI_FORMAT_AYUV: + case DXGI_FORMAT.DXGI_FORMAT_Y410: + case DXGI_FORMAT.DXGI_FORMAT_Y416: + case DXGI_FORMAT.DXGI_FORMAT_NV12: + case DXGI_FORMAT.DXGI_FORMAT_P010: + case DXGI_FORMAT.DXGI_FORMAT_P016: + case DXGI_FORMAT.DXGI_FORMAT_420_OPAQUE: + case DXGI_FORMAT.DXGI_FORMAT_YUY2: + case DXGI_FORMAT.DXGI_FORMAT_Y210: + case DXGI_FORMAT.DXGI_FORMAT_Y216: + case DXGI_FORMAT.DXGI_FORMAT_NV11: + case DXGI_FORMAT.DXGI_FORMAT_AI44: + case DXGI_FORMAT.DXGI_FORMAT_IA44: + case DXGI_FORMAT.DXGI_FORMAT_P8: + case DXGI_FORMAT.DXGI_FORMAT_A8P8: + throw new NotSupportedException(); + case DXGI_FORMAT.DXGI_FORMAT_B4G4R4A4_UNORM: + bitsPerPixel = 16; + break; + default: + throw new NotSupportedException(); + } + + var pitch = isBlockCompression + ? Math.Max(1, (width + 3) / 4) * 2 * bitsPerPixel + : ((width * bitsPerPixel) + 7) / 8; + + return new(width, height, pitch, format); + } + + /// + /// Creates a new instance of record using the given resolution, + /// in B8G8R8A8(BGRA32) UNorm pixel format. + /// + /// The width. + /// The height. + /// The new instance. + public static RawImageSpecification Bgra32(int width, int height) => + new(width, height, width * 4, (int)DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM); + + /// + /// Creates a new instance of record using the given resolution, + /// in R8G8B8A8(RGBA32) UNorm pixel format. + /// + /// The width. + /// The height. + /// The new instance. + public static RawImageSpecification Rgba32(int width, int height) => + new(width, height, width * 4, (int)DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM); +} diff --git a/Dalamud/Storage/Assets/DalamudAssetManager.cs b/Dalamud/Storage/Assets/DalamudAssetManager.cs index 69c7c32e8..8abf42e5c 100644 --- a/Dalamud/Storage/Assets/DalamudAssetManager.cs +++ b/Dalamud/Storage/Assets/DalamudAssetManager.cs @@ -320,7 +320,7 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA var disposeDeferred = this.scopedFinalizer.Add(image) ?? throw new InvalidOperationException("Something went wrong very badly"); - return new DisposeSuppressingDalamudTextureWrap(disposeDeferred); + return new DisposeSuppressingTextureWrap(disposeDeferred); } catch (Exception e) { @@ -342,26 +342,4 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA return Task.FromException(exc); return task.ContinueWith(_ => this.TransformImmediate(task, transformer)).Unwrap(); } - - private class DisposeSuppressingDalamudTextureWrap : IDalamudTextureWrap - { - private readonly IDalamudTextureWrap innerWrap; - - public DisposeSuppressingDalamudTextureWrap(IDalamudTextureWrap wrap) => this.innerWrap = wrap; - - /// - public IntPtr ImGuiHandle => this.innerWrap.ImGuiHandle; - - /// - public int Width => this.innerWrap.Width; - - /// - public int Height => this.innerWrap.Height; - - /// - public void Dispose() - { - // suppressed - } - } }