From 3fe2920e924ffce77f3152627d115709963dae8d Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 22 Feb 2024 00:53:07 +0900 Subject: [PATCH 01/57] 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 - } - } } From eb4d9aba7e080eac47c1b6f8dcb0f4c540eca757 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 22 Feb 2024 03:04:42 +0900 Subject: [PATCH 02/57] TexWidget: add test/examples for new APIs --- .../Windows/Data/Widgets/TexWidget.cs | 441 ++++++++++++++---- 1 file changed, 361 insertions(+), 80 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs index 8d6879ac1..351957974 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -1,12 +1,16 @@ using System.Collections.Generic; using System.IO; using System.Numerics; +using System.Threading.Tasks; +using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.Internal.SharableTextures; using Dalamud.Interface.Utility; using Dalamud.Plugin.Services; +using Dalamud.Storage.Assets; +using Dalamud.Utility; + using ImGuiNET; -using ImGuiScene; -using Serilog; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; @@ -15,23 +19,24 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; ///
internal class TexWidget : IDataWindowWidget { - private readonly List addedTextures = new(); - + private readonly List addedTextures = new(); + private string iconId = "18"; private bool hiRes = true; private bool hq = false; - private bool keepAlive = false; private string inputTexPath = string.Empty; + private string inputFilePath = string.Empty; private Vector2 inputTexUv0 = Vector2.Zero; private Vector2 inputTexUv1 = Vector2.One; private Vector4 inputTintCol = Vector4.One; private Vector2 inputTexScale = Vector2.Zero; + private TextureManager textureManager = null!; /// public string[]? CommandShortcuts { get; init; } = { "tex", "texture" }; - + /// - public string DisplayName { get; init; } = "Tex"; + public string DisplayName { get; init; } = "Tex"; /// public bool Ready { get; set; } @@ -39,91 +44,81 @@ internal class TexWidget : IDataWindowWidget /// public void Load() { + this.addedTextures.AggregateToDisposable().Dispose(); + this.addedTextures.Clear(); + this.inputTexPath = "ui/loadingimage/-nowloading_base25_hr1.tex"; + this.inputFilePath = Path.Join( + Service.Get().StartInfo.AssetDirectory!, + DalamudAsset.Logo.GetAttribute()!.FileName); this.Ready = true; } /// public void Draw() { - var texManager = Service.Get(); + this.textureManager = Service.Get(); - ImGui.InputText("Icon ID", ref this.iconId, 32); - ImGui.Checkbox("HQ Item", ref this.hq); - ImGui.Checkbox("Hi-Res", ref this.hiRes); - ImGui.Checkbox("Keep alive", ref this.keepAlive); - if (ImGui.Button("Load Icon")) + if (ImGui.Button("GC")) + GC.Collect(); + + ImGui.PushID("loadedGameTextures"); + if (ImGui.CollapsingHeader($"Loaded Game Textures: {this.textureManager.GamePathTextures.Count}###header")) + this.DrawLoadedTextures(this.textureManager.GamePathTextures); + ImGui.PopID(); + + ImGui.PushID("loadedFileTextures"); + if (ImGui.CollapsingHeader($"Loaded File Textures: {this.textureManager.FileSystemTextures.Count}###header")) + this.DrawLoadedTextures(this.textureManager.FileSystemTextures); + ImGui.PopID(); + + if (ImGui.CollapsingHeader("Load Game File by Icon ID", ImGuiTreeNodeFlags.DefaultOpen)) + this.DrawIconInput(); + + if (ImGui.CollapsingHeader("Load Game File by Path", ImGuiTreeNodeFlags.DefaultOpen)) + this.DrawGamePathInput(); + + if (ImGui.CollapsingHeader("Load File", ImGuiTreeNodeFlags.DefaultOpen)) + this.DrawFileInput(); + + if (ImGui.CollapsingHeader("UV")) + this.DrawUvInput(); + + TextureEntry? toRemove = null; + TextureEntry? toCopy = null; + foreach (var t in this.addedTextures) { - try + ImGui.PushID(t.Id); + if (ImGui.CollapsingHeader($"Tex #{t.Id} {t}###header", ImGuiTreeNodeFlags.DefaultOpen)) { - var flags = ITextureProvider.IconFlags.None; - if (this.hq) - flags |= ITextureProvider.IconFlags.ItemHighQuality; - - if (this.hiRes) - flags |= ITextureProvider.IconFlags.HiRes; - - this.addedTextures.Add(texManager.GetIcon(uint.Parse(this.iconId), flags, keepAlive: this.keepAlive)); - } - catch (Exception ex) - { - Log.Error(ex, "Could not load tex"); - } - } - - ImGui.Separator(); - ImGui.InputText("Tex Path", ref this.inputTexPath, 255); - if (ImGui.Button("Load Tex")) - { - try - { - this.addedTextures.Add(texManager.GetTextureFromGame(this.inputTexPath, this.keepAlive)); - } - catch (Exception ex) - { - Log.Error(ex, "Could not load tex"); - } - } - - if (ImGui.Button("Load File")) - { - try - { - this.addedTextures.Add(texManager.GetTextureFromFile(new FileInfo(this.inputTexPath), this.keepAlive)); - } - catch (Exception ex) - { - Log.Error(ex, "Could not load tex"); - } - } - - ImGui.Separator(); - ImGui.InputFloat2("UV0", ref this.inputTexUv0); - ImGui.InputFloat2("UV1", ref this.inputTexUv1); - ImGui.InputFloat4("Tint", ref this.inputTintCol); - ImGui.InputFloat2("Scale", ref this.inputTexScale); - - ImGuiHelpers.ScaledDummy(10); - - IDalamudTextureWrap? toRemove = null; - for (var i = 0; i < this.addedTextures.Count; i++) - { - if (ImGui.CollapsingHeader($"Tex #{i}")) - { - var tex = this.addedTextures[i]; - - var scale = new Vector2(tex.Width, tex.Height); - if (this.inputTexScale != Vector2.Zero) - scale = this.inputTexScale; - - ImGui.Image(tex.ImGuiHandle, scale, this.inputTexUv0, this.inputTexUv1, this.inputTintCol); - - if (ImGui.Button($"X##{i}")) - toRemove = tex; + if (ImGui.Button("X")) + toRemove = t; ImGui.SameLine(); - if (ImGui.Button($"Clone##{i}")) - this.addedTextures.Add(tex.CreateWrapSharingLowLevelResource()); + if (ImGui.Button("Copy")) + toCopy = t; + + try + { + if (t.GetTexture(this.textureManager) is { } tex) + { + var scale = new Vector2(tex.Width, tex.Height); + if (this.inputTexScale != Vector2.Zero) + scale = this.inputTexScale; + + ImGui.Image(tex.ImGuiHandle, scale, this.inputTexUv0, this.inputTexUv1, this.inputTintCol); + } + else + { + ImGui.TextUnformatted(t.DescribeError()); + } + } + catch (Exception e) + { + ImGui.TextUnformatted(e.ToString()); + } } + + ImGui.PopID(); } if (toRemove != null) @@ -131,5 +126,291 @@ internal class TexWidget : IDataWindowWidget toRemove.Dispose(); this.addedTextures.Remove(toRemove); } + + if (toCopy != null) + { + this.addedTextures.Add(toCopy.CreateFromSharedLowLevelResource(this.textureManager)); + } + } + + private unsafe void DrawLoadedTextures(IReadOnlyDictionary textures) + { + if (!ImGui.BeginTable("##table", 6)) + return; + + ImGui.TableSetupScrollFreeze(0, 1); + ImGui.TableSetupColumn("ID", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("000000").X); + ImGui.TableSetupColumn("Preview", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("Preview_").X); + ImGui.TableSetupColumn("Source", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("RefCount", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("RefCount__").X); + ImGui.TableSetupColumn("SelfRef", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("00.000___").X); + ImGui.TableSetupColumn("CanRevive", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("CanRevive__").X); + ImGui.TableHeadersRow(); + + var clipper = new ImGuiListClipperPtr(ImGuiNative.ImGuiListClipper_ImGuiListClipper()); + clipper.Begin(textures.Count); + + using (var enu = textures.GetEnumerator()) + { + var row = 0; + while (clipper.Step()) + { + var valid = true; + for (; row < clipper.DisplayStart && valid; row++) + valid = enu.MoveNext(); + + if (!valid) + break; + + for (; row < clipper.DisplayEnd; row++) + { + valid = enu.MoveNext(); + if (!valid) + break; + + var (key, texture) = enu.Current; + ImGui.TableNextRow(); + + ImGui.TableNextColumn(); + this.TextRightAlign($"{texture.InstanceIdForDebug:n0}"); + + ImGui.TableNextColumn(); + ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero); + ImGui.PushStyleColor(ImGuiCol.Button, Vector4.Zero); + ImGui.PushStyleColor(ImGuiCol.ButtonActive, Vector4.Zero); + ImGui.PushStyleColor(ImGuiCol.ButtonHovered, Vector4.Zero); + ImGui.Button("Hover"); + ImGui.PopStyleColor(3); + ImGui.PopStyleVar(); + if (ImGui.IsItemHovered() && texture.GetImmediate() is { } immediate) + { + ImGui.BeginTooltip(); + ImGui.Image(immediate.ImGuiHandle, immediate.Size); + ImGui.EndTooltip(); + } + + ImGui.TableNextColumn(); + this.TextCopiable(key); + + ImGui.TableNextColumn(); + this.TextRightAlign($"{texture.RefCountForDebug:n0}"); + + ImGui.TableNextColumn(); + var remain = texture.SelfReferenceExpiresInForDebug; + this.TextRightAlign(remain <= 0 ? "-" : $"{remain:00.000}"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted(texture.RevivalPossibility?.TryGetTarget(out _) is true ? "Yes" : "No"); + } + + if (!valid) + break; + } + } + + clipper.Destroy(); + ImGui.EndTable(); + + ImGuiHelpers.ScaledDummy(10); + } + + private void DrawIconInput() + { + ImGui.InputText("Icon ID", ref this.iconId, 32); + ImGui.Checkbox("HQ Item", ref this.hq); + ImGui.Checkbox("Hi-Res", ref this.hiRes); +#pragma warning disable CS0618 // Type or member is obsolete + if (ImGui.Button("Load Icon (API9)")) + { + var flags = ITextureProvider.IconFlags.None; + if (this.hq) + flags |= ITextureProvider.IconFlags.ItemHighQuality; + if (this.hiRes) + flags |= ITextureProvider.IconFlags.HiRes; + this.addedTextures.Add(new(Api9: this.textureManager.GetIcon(uint.Parse(this.iconId), flags))); + } +#pragma warning restore CS0618 // Type or member is obsolete + + ImGui.SameLine(); + if (ImGui.Button("Load Icon (Async)")) + { + this.addedTextures.Add( + new( + Api10: this.textureManager.GetFromGameIconAsync( + new(uint.Parse(this.iconId), this.hq, this.hiRes)))); + } + + ImGui.SameLine(); + if (ImGui.Button("Load Icon (Immediate)")) + this.addedTextures.Add(new(Api10ImmGameIcon: new(uint.Parse(this.iconId), this.hq, this.hiRes))); + + ImGuiHelpers.ScaledDummy(10); + } + + private void DrawGamePathInput() + { + ImGui.InputText("Tex Path", ref this.inputTexPath, 255); + +#pragma warning disable CS0618 // Type or member is obsolete + if (ImGui.Button("Load Tex (API9)")) + this.addedTextures.Add(new(Api9: this.textureManager.GetTextureFromGame(this.inputTexPath))); +#pragma warning restore CS0618 // Type or member is obsolete + + ImGui.SameLine(); + if (ImGui.Button("Load Tex (Async)")) + this.addedTextures.Add(new(Api10: this.textureManager.GetFromGameAsync(this.inputTexPath))); + + ImGui.SameLine(); + if (ImGui.Button("Load Tex (Immediate)")) + this.addedTextures.Add(new(Api10ImmGamePath: this.inputTexPath)); + + ImGuiHelpers.ScaledDummy(10); + } + + private void DrawFileInput() + { + ImGui.InputText("File Path", ref this.inputFilePath, 255); + +#pragma warning disable CS0618 // Type or member is obsolete + if (ImGui.Button("Load File (API9)")) + this.addedTextures.Add(new(Api9: this.textureManager.GetTextureFromFile(new(this.inputFilePath)))); +#pragma warning restore CS0618 // Type or member is obsolete + + ImGui.SameLine(); + if (ImGui.Button("Load File (Async)")) + this.addedTextures.Add(new(Api10: this.textureManager.GetFromFileAsync(this.inputFilePath))); + + ImGui.SameLine(); + if (ImGui.Button("Load File (Immediate)")) + this.addedTextures.Add(new(Api10ImmFile: this.inputFilePath)); + + ImGuiHelpers.ScaledDummy(10); + } + + private void DrawUvInput() + { + ImGui.InputFloat2("UV0", ref this.inputTexUv0); + ImGui.InputFloat2("UV1", ref this.inputTexUv1); + ImGui.InputFloat4("Tint", ref this.inputTintCol); + ImGui.InputFloat2("Scale", ref this.inputTexScale); + + ImGuiHelpers.ScaledDummy(10); + } + + private void TextRightAlign(string s) + { + var width = ImGui.CalcTextSize(s).X; + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetColumnWidth() - width); + ImGui.TextUnformatted(s); + } + + private void TextCopiable(string s, bool framepad = false) + { + var offset = ImGui.GetCursorScreenPos() + new Vector2(0, framepad ? ImGui.GetStyle().FramePadding.Y : 0); + if (framepad) + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(s); + if (ImGui.IsItemHovered()) + { + ImGui.SetNextWindowPos(offset - ImGui.GetStyle().WindowPadding); + var vp = ImGui.GetWindowViewport(); + var wrx = (vp.WorkPos.X + vp.WorkSize.X) - offset.X; + ImGui.SetNextWindowSizeConstraints(Vector2.One, new(wrx, float.MaxValue)); + ImGui.BeginTooltip(); + ImGui.PushTextWrapPos(wrx); + ImGui.TextWrapped(s.Replace("%", "%%")); + ImGui.PopTextWrapPos(); + ImGui.EndTooltip(); + } + + if (ImGui.IsItemClicked()) + { + ImGui.SetClipboardText(s); + Service.Get().AddNotification( + $"Copied {ImGui.TableGetColumnName()} to clipboard.", + this.DisplayName, + NotificationType.Success); + } + } + + private record TextureEntry( + IDalamudTextureWrap? SharedResource = null, + IDalamudTextureWrap? Api9 = null, + Task? Api10 = null, + GameIconLookup? Api10ImmGameIcon = null, + string? Api10ImmGamePath = null, + string? Api10ImmFile = null) : IDisposable + { + private static int idCounter; + + public int Id { get; } = idCounter++; + + public void Dispose() + { + this.SharedResource?.Dispose(); + this.Api9?.Dispose(); + _ = this.Api10?.ToContentDisposedTask(); + } + + public string DescribeError() + { + if (this.SharedResource is not null) + return "Unknown error"; + if (this.Api9 is not null) + return "Unknown error"; + if (this.Api10 is not null) + { + return !this.Api10.IsCompleted + ? "Loading" + : this.Api10.Exception?.ToString() ?? (this.Api10.IsCanceled ? "Canceled" : "Unknown error"); + } + + if (this.Api10ImmGameIcon is not null) + return "Must not happen"; + if (this.Api10ImmGamePath is not null) + return "Must not happen"; + if (this.Api10ImmFile is not null) + return "Must not happen"; + return "Not implemented"; + } + + public IDalamudTextureWrap? GetTexture(ITextureProvider tp) + { + if (this.SharedResource is not null) + return this.SharedResource; + if (this.Api9 is not null) + return this.Api9; + if (this.Api10 is not null) + return this.Api10.IsCompletedSuccessfully ? this.Api10.Result : null; + if (this.Api10ImmGameIcon is not null) + return tp.ImmediateGetFromGameIcon(this.Api10ImmGameIcon.Value); + if (this.Api10ImmGamePath is not null) + return tp.ImmediateGetFromGame(this.Api10ImmGamePath); + if (this.Api10ImmFile is not null) + return tp.ImmediateGetFromFile(this.Api10ImmFile); + return null; + } + + public TextureEntry CreateFromSharedLowLevelResource(ITextureProvider tp) => + new() { SharedResource = this.GetTexture(tp)?.CreateWrapSharingLowLevelResource() }; + + public override string ToString() + { + if (this.SharedResource is not null) + return $"{nameof(this.SharedResource)}: {this.SharedResource}"; + if (this.Api9 is not null) + return $"{nameof(this.Api9)}: {this.Api9}"; + if (this.Api10 is { IsCompletedSuccessfully: true }) + return $"{nameof(this.Api10)}: {this.Api10.Result}"; + if (this.Api10 is not null) + return $"{nameof(this.Api10)}: {this.Api10}"; + if (this.Api10ImmGameIcon is not null) + return $"{nameof(this.Api10ImmGameIcon)}: {this.Api10ImmGameIcon}"; + if (this.Api10ImmGamePath is not null) + return $"{nameof(this.Api10ImmGamePath)}: {this.Api10ImmGamePath}"; + if (this.Api10ImmFile is not null) + return $"{nameof(this.Api10ImmFile)}: {this.Api10ImmFile}"; + return "Not implemented"; + } } } From 5eadfc1b4da13f3c8f909bdd0e6a9fe3e318dde5 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 22 Feb 2024 03:32:13 +0900 Subject: [PATCH 03/57] Add state fetchers --- .../Internal}/GameIconLookup.cs | 2 +- .../FileSystemSharableTexture.cs | 9 ++---- .../GamePathSharableTexture.cs | 9 ++---- .../SharableTextures/SharableTexture.cs | 23 ++++++++++---- Dalamud/Interface/Internal/TextureManager.cs | 30 +++++++++++++++++++ .../Plugin/Services/ITextureProvider.Api9.cs | 1 + Dalamud/Plugin/Services/ITextureProvider.cs | 18 +++++++++++ 7 files changed, 72 insertions(+), 20 deletions(-) rename Dalamud/{Plugin/Services => Interface/Internal}/GameIconLookup.cs (94%) diff --git a/Dalamud/Plugin/Services/GameIconLookup.cs b/Dalamud/Interface/Internal/GameIconLookup.cs similarity index 94% rename from Dalamud/Plugin/Services/GameIconLookup.cs rename to Dalamud/Interface/Internal/GameIconLookup.cs index 4ad710ab1..b34db9d59 100644 --- a/Dalamud/Plugin/Services/GameIconLookup.cs +++ b/Dalamud/Interface/Internal/GameIconLookup.cs @@ -1,6 +1,6 @@ using System.Diagnostics.CodeAnalysis; -namespace Dalamud.Plugin.Services; +namespace Dalamud.Interface.Internal; /// /// Represents a lookup for a game icon. diff --git a/Dalamud/Interface/Internal/SharableTextures/FileSystemSharableTexture.cs b/Dalamud/Interface/Internal/SharableTextures/FileSystemSharableTexture.cs index 0eea2a5f7..cae633a84 100644 --- a/Dalamud/Interface/Internal/SharableTextures/FileSystemSharableTexture.cs +++ b/Dalamud/Interface/Internal/SharableTextures/FileSystemSharableTexture.cs @@ -21,9 +21,6 @@ internal sealed class FileSystemSharableTexture : SharableTexture this.UnderlyingWrap = this.CreateTextureAsync(); } - /// - protected override Task UnderlyingWrap { get; set; } - /// public override string ToString() => $"{nameof(FileSystemSharableTexture)}#{this.InstanceIdForDebug}({this.path})"; @@ -32,10 +29,8 @@ internal sealed class FileSystemSharableTexture : SharableTexture protected override void FinalRelease() { this.DisposeSuppressingWrap = null; - _ = this.UnderlyingWrap.ToContentDisposedTask(true); - this.UnderlyingWrap = - Task.FromException(new ObjectDisposedException(nameof(GamePathSharableTexture))); - _ = this.UnderlyingWrap.Exception; + _ = this.UnderlyingWrap?.ToContentDisposedTask(true); + this.UnderlyingWrap = null; } /// diff --git a/Dalamud/Interface/Internal/SharableTextures/GamePathSharableTexture.cs b/Dalamud/Interface/Internal/SharableTextures/GamePathSharableTexture.cs index e2198b651..328377c1b 100644 --- a/Dalamud/Interface/Internal/SharableTextures/GamePathSharableTexture.cs +++ b/Dalamud/Interface/Internal/SharableTextures/GamePathSharableTexture.cs @@ -25,9 +25,6 @@ internal sealed class GamePathSharableTexture : SharableTexture this.UnderlyingWrap = this.CreateTextureAsync(); } - /// - protected override Task UnderlyingWrap { get; set; } - /// public override string ToString() => $"{nameof(GamePathSharableTexture)}#{this.InstanceIdForDebug}({this.path})"; @@ -35,10 +32,8 @@ internal sealed class GamePathSharableTexture : SharableTexture protected override void FinalRelease() { this.DisposeSuppressingWrap = null; - _ = this.UnderlyingWrap.ToContentDisposedTask(true); - this.UnderlyingWrap = - Task.FromException(new ObjectDisposedException(nameof(GamePathSharableTexture))); - _ = this.UnderlyingWrap.Exception; + _ = this.UnderlyingWrap?.ToContentDisposedTask(true); + this.UnderlyingWrap = null; } /// diff --git a/Dalamud/Interface/Internal/SharableTextures/SharableTexture.cs b/Dalamud/Interface/Internal/SharableTextures/SharableTexture.cs index b0c1a373a..9c4b43f66 100644 --- a/Dalamud/Interface/Internal/SharableTextures/SharableTexture.cs +++ b/Dalamud/Interface/Internal/SharableTextures/SharableTexture.cs @@ -66,7 +66,7 @@ internal abstract class SharableTexture : IRefCountable /// /// Gets or sets the underlying texture wrap. /// - protected abstract Task UnderlyingWrap { get; set; } + public Task? UnderlyingWrap { get; set; } /// /// Gets or sets the dispose-suppressing wrap for . @@ -185,8 +185,18 @@ internal abstract class SharableTexture : IRefCountable public Task CreateNewReference() { this.AddRef(); + if (this.UnderlyingWrap is null) + throw new InvalidOperationException("AddRef returned but UnderlyingWrap is null?"); + return this.UnderlyingWrap.ContinueWith( - r => (IDalamudTextureWrap)new RefCountableWrappingTextureWrap(r.Result, this)); + r => + { + if (r.IsCompletedSuccessfully) + return Task.FromResult((IDalamudTextureWrap)new RefCountableWrappingTextureWrap(r.Result, this)); + + this.Release(); + return r; + }).Unwrap(); } /// @@ -207,9 +217,12 @@ internal abstract class SharableTexture : IRefCountable if (this.RevivalPossibility?.TryGetTarget(out this.availableOnAccessWrapForApi9) is true) return this.availableOnAccessWrapForApi9; - this.UnderlyingWrap.Wait(); - if (this.UnderlyingWrap.Exception is not null) + var newRefTask = this.CreateNewReference(); + newRefTask.Wait(); + if (!newRefTask.IsCompletedSuccessfully) return null; + newRefTask.Result.Dispose(); + this.availableOnAccessWrapForApi9 = new AvailableOnAccessTextureWrap(this); this.RevivalPossibility = new(this.availableOnAccessWrapForApi9); } @@ -326,7 +339,7 @@ internal abstract class SharableTexture : IRefCountable if (this.inner.GetImmediate() is { } t) return t; - this.inner.UnderlyingWrap.Wait(); + 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 cd0447b6b..9081cf14e 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -145,6 +145,36 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid this.fileSystemTextures.GetOrAdd(file.FullName, CreateFileSystemSharableTexture).GetAvailableOnAccessWrapForApi9(); #pragma warning restore CS0618 // Type or member is obsolete + /// + public bool ImmediateGetStateFromGameIcon(in GameIconLookup lookup, out Exception? exception) => + this.ImmediateGetStateFromGame(this.lookupToPath.GetOrAdd(lookup, this.GetIconPathByValue), out exception); + + /// + public bool ImmediateGetStateFromGame(string path, out Exception? exception) + { + if (!this.gamePathTextures.TryGetValue(path, out var texture)) + { + exception = null; + return false; + } + + exception = texture.UnderlyingWrap?.Exception; + return texture.UnderlyingWrap?.IsCompletedSuccessfully ?? false; + } + + /// + public bool ImmediateGetStateFromFile(string file, out Exception? exception) + { + if (!this.fileSystemTextures.TryGetValue(file, out var texture)) + { + exception = null; + return false; + } + + exception = texture.UnderlyingWrap?.Exception; + return texture.UnderlyingWrap?.IsCompletedSuccessfully ?? false; + } + /// public IDalamudTextureWrap ImmediateGetFromGameIcon(in GameIconLookup lookup) => this.ImmediateGetFromGame(this.lookupToPath.GetOrAdd(lookup, this.GetIconPathByValue)); diff --git a/Dalamud/Plugin/Services/ITextureProvider.Api9.cs b/Dalamud/Plugin/Services/ITextureProvider.Api9.cs index 9592da711..db033778e 100644 --- a/Dalamud/Plugin/Services/ITextureProvider.Api9.cs +++ b/Dalamud/Plugin/Services/ITextureProvider.Api9.cs @@ -1,5 +1,6 @@ using System.IO; +using Dalamud.Interface; using Dalamud.Interface.Internal; using Dalamud.Utility; diff --git a/Dalamud/Plugin/Services/ITextureProvider.cs b/Dalamud/Plugin/Services/ITextureProvider.cs index e004c8be7..52037a449 100644 --- a/Dalamud/Plugin/Services/ITextureProvider.cs +++ b/Dalamud/Plugin/Services/ITextureProvider.cs @@ -13,6 +13,24 @@ namespace Dalamud.Plugin.Services; /// public partial interface ITextureProvider { + /// Gets the state of the background load task for . + /// The icon specifier. + /// The exception, if failed. + /// true if loaded; false if not fully loaded or failed. + public bool ImmediateGetStateFromGameIcon(in GameIconLookup lookup, out Exception? exception); + + /// Gets the state of the background load task for . + /// The game-internal path to a .tex, .atex, or an image file such as .png. + /// The exception, if failed. + /// true if loaded; false if not fully loaded or failed. + public bool ImmediateGetStateFromGame(string path, out Exception? exception); + + /// Gets the state of the background load task for . + /// The filesystem path to a .tex, .atex, or an image file such as .png. + /// The exception, if failed. + /// true if loaded; false if not fully loaded or failed. + public bool ImmediateGetStateFromFile(string file, out Exception? exception); + /// 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 From 2920d18afa8dd37a3b2281b0e888337500eb48d3 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 22 Feb 2024 03:36:55 +0900 Subject: [PATCH 04/57] Update IconBrowserWidget --- .../Windows/Data/Widgets/IconBrowserWidget.cs | 78 +++++++++---------- 1 file changed, 35 insertions(+), 43 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs index 06c691cc9..37fc958af 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs @@ -1,12 +1,10 @@ using System.Collections.Generic; -using System.Drawing; using System.Linq; using System.Numerics; -using Dalamud.Data; using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; -using Dalamud.Utility; + using ImGuiNET; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; @@ -17,22 +15,22 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; public class IconBrowserWidget : IDataWindowWidget { // Remove range 170,000 -> 180,000 by default, this specific range causes exceptions. - private readonly HashSet nullValues = Enumerable.Range(170000, 9999).ToHashSet(); - + private readonly HashSet nullValues = Enumerable.Range(170000, 9999).ToHashSet(); + private Vector2 iconSize = new(64.0f, 64.0f); private Vector2 editIconSize = new(64.0f, 64.0f); - + private List valueRange = Enumerable.Range(0, 200000).ToList(); - + private int lastNullValueCount; private int startRange; private int stopRange = 200000; private bool showTooltipImage; - + private Vector2 mouseDragStart; private bool dragStarted; private Vector2 lastWindowSize = Vector2.Zero; - + /// public string[]? CommandShortcuts { get; init; } = { "icon", "icons" }; @@ -46,7 +44,7 @@ public class IconBrowserWidget : IDataWindowWidget public void Load() { } - + /// public void Draw() { @@ -54,23 +52,24 @@ public class IconBrowserWidget : IDataWindowWidget if (ImGui.BeginChild("ScrollableSection", ImGui.GetContentRegionAvail(), false, ImGuiWindowFlags.NoMove)) { - var itemsPerRow = (int)MathF.Floor(ImGui.GetContentRegionMax().X / (this.iconSize.X + ImGui.GetStyle().ItemSpacing.X)); + var itemsPerRow = (int)MathF.Floor( + ImGui.GetContentRegionMax().X / (this.iconSize.X + ImGui.GetStyle().ItemSpacing.X)); var itemHeight = this.iconSize.Y + ImGui.GetStyle().ItemSpacing.Y; ImGuiClip.ClippedDraw(this.valueRange, this.DrawIcon, itemsPerRow, itemHeight); } - + ImGui.EndChild(); - + this.ProcessMouseDragging(); - + if (this.lastNullValueCount != this.nullValues.Count) { this.RecalculateIndexRange(); this.lastNullValueCount = this.nullValues.Count; } } - + // Limit the popup image to half our screen size. private static float GetImageScaleFactor(IDalamudTextureWrap texture) { @@ -84,10 +83,10 @@ public class IconBrowserWidget : IDataWindowWidget scale = MathF.Min(widthRatio, heightRatio); } - + return scale; } - + private void DrawOptions() { ImGui.Columns(2); @@ -101,48 +100,45 @@ public class IconBrowserWidget : IDataWindowWidget ImGui.NextColumn(); ImGui.Checkbox("Show Image in Tooltip", ref this.showTooltipImage); - + ImGui.NextColumn(); ImGui.InputFloat2("Icon Size", ref this.editIconSize); if (ImGui.IsItemDeactivatedAfterEdit()) { this.iconSize = this.editIconSize; } - + ImGui.Columns(1); } - + private void DrawIcon(int iconId) { + var texm = Service.Get(); try { var cursor = ImGui.GetCursorScreenPos(); - - if (!this.IsIconValid(iconId)) - { - this.nullValues.Add(iconId); - return; - } - - if (Service.Get().GetIcon((uint)iconId) is { } texture) + var texture = texm.ImmediateGetFromGameIcon(new((uint)iconId)); + + if (texm.ImmediateGetStateFromGameIcon(new((uint)iconId), out var exc) || exc is null) { ImGui.Image(texture.ImGuiHandle, this.iconSize); - + // If we have the option to show a tooltip image, draw the image, but make sure it's not too big. if (ImGui.IsItemHovered() && this.showTooltipImage) { ImGui.BeginTooltip(); - + var scale = GetImageScaleFactor(texture); - + var textSize = ImGui.CalcTextSize(iconId.ToString()); - ImGui.SetCursorPosX(texture.Size.X * scale / 2.0f - textSize.X / 2.0f + ImGui.GetStyle().FramePadding.X * 2.0f); + ImGui.SetCursorPosX( + texture.Size.X * scale / 2.0f - textSize.X / 2.0f + ImGui.GetStyle().FramePadding.X * 2.0f); ImGui.Text(iconId.ToString()); - + ImGui.Image(texture.ImGuiHandle, texture.Size * scale); ImGui.EndTooltip(); } - + // else, just draw the iconId. else if (ImGui.IsItemHovered()) { @@ -155,7 +151,10 @@ public class IconBrowserWidget : IDataWindowWidget this.nullValues.Add(iconId); } - ImGui.GetWindowDrawList().AddRect(cursor, cursor + this.iconSize, ImGui.GetColorU32(ImGuiColors.DalamudWhite)); + ImGui.GetWindowDrawList().AddRect( + cursor, + cursor + this.iconSize, + ImGui.GetColorU32(ImGuiColors.DalamudWhite)); } catch (Exception) { @@ -182,7 +181,7 @@ public class IconBrowserWidget : IDataWindowWidget this.dragStarted = false; } } - + if (ImGui.IsMouseDragging(ImGuiMouseButton.Left) && this.dragStarted) { var delta = this.mouseDragStart - ImGui.GetMousePos(); @@ -194,13 +193,6 @@ public class IconBrowserWidget : IDataWindowWidget this.dragStarted = false; } } - - // Check if the icon has a valid filepath, and exists in the game data. - private bool IsIconValid(int iconId) - { - var filePath = Service.Get().GetIconPath((uint)iconId); - return !filePath.IsNullOrEmpty() && Service.Get().FileExists(filePath); - } private void RecalculateIndexRange() { From 71bb02347f5ac8254d30abf0d3ecee754006d0ed Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 22 Feb 2024 03:38:23 +0900 Subject: [PATCH 05/57] Update docs --- Dalamud/Plugin/Services/ITextureProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Plugin/Services/ITextureProvider.cs b/Dalamud/Plugin/Services/ITextureProvider.cs index 52037a449..8ee724fd5 100644 --- a/Dalamud/Plugin/Services/ITextureProvider.cs +++ b/Dalamud/Plugin/Services/ITextureProvider.cs @@ -132,6 +132,6 @@ public partial interface ITextureProvider /// 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. + /// A texture wrap that can be used to render the texture. Dispose after use. public IDalamudTextureWrap GetTexture(TexFile file); } From e12b2f7803bb14abb4f2772e9fe4f21d784881b4 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 22 Feb 2024 14:14:52 +0900 Subject: [PATCH 06/57] Make state+texture retrieval done with one API call --- .../FileSystemSharableTexture.cs | 7 +- .../GamePathSharableTexture.cs | 7 +- .../SharableTextures/SharableTexture.cs | 136 +++++++++------- Dalamud/Interface/Internal/TextureManager.cs | 146 ++++++++++++------ .../Windows/Data/Widgets/IconBrowserWidget.cs | 7 +- .../Windows/Data/Widgets/TexWidget.cs | 93 ++++++++--- Dalamud/Plugin/Services/ITextureProvider.cs | 76 ++++++--- .../Services/ITextureSubstitutionProvider.cs | 3 + 8 files changed, 325 insertions(+), 150 deletions(-) diff --git a/Dalamud/Interface/Internal/SharableTextures/FileSystemSharableTexture.cs b/Dalamud/Interface/Internal/SharableTextures/FileSystemSharableTexture.cs index cae633a84..06184b5ec 100644 --- a/Dalamud/Interface/Internal/SharableTextures/FileSystemSharableTexture.cs +++ b/Dalamud/Interface/Internal/SharableTextures/FileSystemSharableTexture.cs @@ -21,12 +21,15 @@ internal sealed class FileSystemSharableTexture : SharableTexture this.UnderlyingWrap = this.CreateTextureAsync(); } + /// + public override string SourcePathForDebug => this.path; + /// public override string ToString() => $"{nameof(FileSystemSharableTexture)}#{this.InstanceIdForDebug}({this.path})"; /// - protected override void FinalRelease() + protected override void ReleaseResources() { this.DisposeSuppressingWrap = null; _ = this.UnderlyingWrap?.ToContentDisposedTask(true); @@ -34,7 +37,7 @@ internal sealed class FileSystemSharableTexture : SharableTexture } /// - protected override void Revive() => + protected override void ReviveResources() => this.UnderlyingWrap = this.CreateTextureAsync(); private Task CreateTextureAsync() => diff --git a/Dalamud/Interface/Internal/SharableTextures/GamePathSharableTexture.cs b/Dalamud/Interface/Internal/SharableTextures/GamePathSharableTexture.cs index 328377c1b..e58f21c26 100644 --- a/Dalamud/Interface/Internal/SharableTextures/GamePathSharableTexture.cs +++ b/Dalamud/Interface/Internal/SharableTextures/GamePathSharableTexture.cs @@ -25,11 +25,14 @@ internal sealed class GamePathSharableTexture : SharableTexture this.UnderlyingWrap = this.CreateTextureAsync(); } + /// + public override string SourcePathForDebug => this.path; + /// public override string ToString() => $"{nameof(GamePathSharableTexture)}#{this.InstanceIdForDebug}({this.path})"; /// - protected override void FinalRelease() + protected override void ReleaseResources() { this.DisposeSuppressingWrap = null; _ = this.UnderlyingWrap?.ToContentDisposedTask(true); @@ -37,7 +40,7 @@ internal sealed class GamePathSharableTexture : SharableTexture } /// - protected override void Revive() => + protected override void ReviveResources() => this.UnderlyingWrap = this.CreateTextureAsync(); private Task CreateTextureAsync() => diff --git a/Dalamud/Interface/Internal/SharableTextures/SharableTexture.cs b/Dalamud/Interface/Internal/SharableTextures/SharableTexture.cs index 9c4b43f66..c08cdb7e9 100644 --- a/Dalamud/Interface/Internal/SharableTextures/SharableTexture.cs +++ b/Dalamud/Interface/Internal/SharableTextures/SharableTexture.cs @@ -18,6 +18,7 @@ internal abstract class SharableTexture : IRefCountable private readonly object reviveLock = new(); + private bool resourceReleased; private int refCount; private long selfReferenceExpiry; private IDalamudTextureWrap? availableOnAccessWrapForApi9; @@ -30,21 +31,8 @@ internal abstract class SharableTexture : IRefCountable this.InstanceIdForDebug = Interlocked.Increment(ref instanceCounter); this.refCount = 1; this.selfReferenceExpiry = Environment.TickCount64 + SelfReferenceDurationTicks; - this.ReviveEnabled = true; } - /// - /// 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. /// @@ -63,6 +51,16 @@ internal abstract class SharableTexture : IRefCountable /// public int RefCountForDebug => this.refCount; + /// + /// Gets the source path. Debug use only. + /// + public abstract string SourcePathForDebug { get; } + + /// + /// Gets a value indicating whether this instance of supports revival. + /// + public bool HasRevivalPossibility => this.RevivalPossibility?.TryGetTarget(out _) is true; + /// /// Gets or sets the underlying texture wrap. /// @@ -74,9 +72,16 @@ internal abstract class SharableTexture : IRefCountable protected DisposeSuppressingTextureWrap? DisposeSuppressingWrap { get; set; } /// - /// Gets a value indicating whether this instance of supports reviving. + /// Gets or sets a weak reference to an object that demands this objects to be alive. /// - protected bool ReviveEnabled { get; private set; } + /// + /// 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. + /// + private WeakReference? RevivalPossibility { get; set; } /// public int AddRef() => this.TryAddRef(out var newRefCount) switch @@ -96,8 +101,35 @@ internal abstract class SharableTexture : IRefCountable return newRefCount; case IRefCountable.RefCountResult.FinalRelease: - this.FinalRelease(); - return newRefCount; + // This case may not be entered while TryAddRef is in progress. + // Note that IRefCountable.AlterRefCount guarantees that either TAR or Release will be called for one + // generation of refCount; they never are called together for the same generation of refCount. + // If TAR is called when refCount >= 1, and then Release is called, case StillAlive will be run. + // If TAR is called when refCount == 0, and then Release is called: + // ... * if TAR was done: case FinalRelease will be run. + // ... * if TAR was not done: case AlreadyDisposed will be run. + // ... Because refCount will be altered as the last step of TAR. + // If Release is called when refCount == 1, and then TAR is called, + // ... the resource may be released yet, so TAR waits for resourceReleased inside reviveLock, + // ... while Release releases the underlying resource and then sets resourceReleased inside reviveLock. + // ... Once that's done, TAR may revive the object safely. + while (true) + { + lock (this.reviveLock) + { + if (this.resourceReleased) + { + // I cannot think of a case that the code entering this code block, but just in case. + Thread.Yield(); + continue; + } + + this.ReleaseResources(); + this.resourceReleased = true; + + return newRefCount; + } + } case IRefCountable.RefCountResult.AlreadyDisposed: throw new ObjectDisposedException(nameof(SharableTexture)); @@ -108,16 +140,22 @@ internal abstract class SharableTexture : IRefCountable } /// - /// Releases self-reference, if it should expire. + /// Releases self-reference, if conditions are met. /// + /// If set to true, the self-reference will be released immediately. /// Number of the new reference count that may or may not have changed. - public int ReleaseSelfReferenceIfExpired() + public int ReleaseSelfReference(bool immediate) { while (true) { var exp = this.selfReferenceExpiry; - if (exp > Environment.TickCount64) - return this.refCount; + switch (immediate) + { + case false when exp > Environment.TickCount64: + return this.refCount; + case true when exp == SelfReferenceExpiryExpired: + return this.refCount; + } if (exp != Interlocked.CompareExchange(ref this.selfReferenceExpiry, SelfReferenceExpiryExpired, exp)) continue; @@ -127,28 +165,6 @@ internal abstract class SharableTexture : IRefCountable } } - /// - /// 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. @@ -233,31 +249,41 @@ internal abstract class SharableTexture : IRefCountable /// /// Cleans up this instance of . /// - protected abstract void FinalRelease(); + protected abstract void ReleaseResources(); /// /// Attempts to restore the reference to this texture. /// - protected abstract void Revive(); + protected abstract void ReviveResources(); private IRefCountable.RefCountResult TryAddRef(out int newRefCount) { var alterResult = IRefCountable.AlterRefCount(1, ref this.refCount, out newRefCount); - if (alterResult != IRefCountable.RefCountResult.AlreadyDisposed || !this.ReviveEnabled) + if (alterResult != IRefCountable.RefCountResult.AlreadyDisposed) return alterResult; - lock (this.reviveLock) + + while (true) { - alterResult = IRefCountable.AlterRefCount(1, ref this.refCount, out newRefCount); - if (alterResult != IRefCountable.RefCountResult.AlreadyDisposed) - return alterResult; + lock (this.reviveLock) + { + if (!this.resourceReleased) + { + Thread.Yield(); + continue; + } - this.Revive(); - Interlocked.Increment(ref this.refCount); + alterResult = IRefCountable.AlterRefCount(1, ref this.refCount, out newRefCount); + if (alterResult != IRefCountable.RefCountResult.AlreadyDisposed) + return alterResult; - if (this.RevivalPossibility?.TryGetTarget(out var target) is true) - this.availableOnAccessWrapForApi9 = target; + this.ReviveResources(); + if (this.RevivalPossibility?.TryGetTarget(out var target) is true) + this.availableOnAccessWrapForApi9 = target; - return IRefCountable.RefCountResult.StillAlive; + Interlocked.Increment(ref this.refCount); + this.resourceReleased = false; + return IRefCountable.RefCountResult.StillAlive; + } } } diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index 9081cf14e..d1ab16a1d 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -1,5 +1,6 @@ using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Threading.Tasks; @@ -58,6 +59,7 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid private readonly ConcurrentLru lookupToPath = new(PathLookupLruCount); private readonly ConcurrentDictionary gamePathTextures = new(); private readonly ConcurrentDictionary fileSystemTextures = new(); + private readonly HashSet invalidatedTextures = new(); private bool disposing; @@ -73,12 +75,22 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid /// /// Gets all the loaded textures from the game resources. Debug use only. /// - public IReadOnlyDictionary GamePathTextures => this.gamePathTextures; + public ICollection GamePathTextures => this.gamePathTextures.Values; /// /// Gets all the loaded textures from the game resources. Debug use only. /// - public IReadOnlyDictionary FileSystemTextures => this.fileSystemTextures; + public ICollection FileSystemTextures => this.fileSystemTextures.Values; + + /// + /// Gets all the loaded textures that are invalidated from . Debug use only. + /// + /// lock on use of the value returned from this property. + [SuppressMessage( + "ReSharper", + "InconsistentlySynchronizedField", + Justification = "Debug use only; users are expected to lock around this")] + public ICollection InvalidatedTextures => this.invalidatedTextures; /// public void Dispose() @@ -88,9 +100,9 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid this.disposing = true; foreach (var v in this.gamePathTextures.Values) - v.DisableReviveAndReleaseSelfReference(); + v.ReleaseSelfReference(true); foreach (var v in this.fileSystemTextures.Values) - v.DisableReviveAndReleaseSelfReference(); + v.ReleaseSelfReference(true); this.lookupToPath.Clear(); this.gamePathTextures.Clear(); @@ -142,52 +154,61 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] [Obsolete("See interface definition.")] public IDalamudTextureWrap? GetTextureFromFile(FileInfo file, bool keepAlive = false) => - this.fileSystemTextures.GetOrAdd(file.FullName, CreateFileSystemSharableTexture).GetAvailableOnAccessWrapForApi9(); + this.fileSystemTextures.GetOrAdd(file.FullName, CreateFileSystemSharableTexture) + .GetAvailableOnAccessWrapForApi9(); #pragma warning restore CS0618 // Type or member is obsolete - /// - public bool ImmediateGetStateFromGameIcon(in GameIconLookup lookup, out Exception? exception) => - this.ImmediateGetStateFromGame(this.lookupToPath.GetOrAdd(lookup, this.GetIconPathByValue), out exception); - - /// - public bool ImmediateGetStateFromGame(string path, out Exception? exception) - { - if (!this.gamePathTextures.TryGetValue(path, out var texture)) - { - exception = null; - return false; - } - - exception = texture.UnderlyingWrap?.Exception; - return texture.UnderlyingWrap?.IsCompletedSuccessfully ?? false; - } - - /// - public bool ImmediateGetStateFromFile(string file, out Exception? exception) - { - if (!this.fileSystemTextures.TryGetValue(file, out var texture)) - { - exception = null; - return false; - } - - exception = texture.UnderlyingWrap?.Exception; - return texture.UnderlyingWrap?.IsCompletedSuccessfully ?? false; - } - /// 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; + this.ImmediateTryGetFromGame(path, out var texture, out _) + ? texture + : this.dalamudAssetManager.Empty4X4; /// public IDalamudTextureWrap ImmediateGetFromFile(string file) => - this.fileSystemTextures.GetOrAdd(file, CreateFileSystemSharableTexture).GetImmediate() - ?? this.dalamudAssetManager.Empty4X4; + this.ImmediateTryGetFromFile(file, out var texture, out _) + ? texture + : this.dalamudAssetManager.Empty4X4; + + /// + public bool ImmediateTryGetFromGameIcon( + in GameIconLookup lookup, + [NotNullWhen(true)] out IDalamudTextureWrap? texture, + out Exception? exception) => + this.ImmediateTryGetFromGame( + this.lookupToPath.GetOrAdd(lookup, this.GetIconPathByValue), + out texture, + out exception); + + /// + public bool ImmediateTryGetFromGame( + string path, + [NotNullWhen(true)] out IDalamudTextureWrap? texture, + out Exception? exception) + { + ThreadSafety.AssertMainThread(); + var t = this.gamePathTextures.GetOrAdd(path, CreateGamePathSharableTexture); + texture = t.GetImmediate(); + exception = t.UnderlyingWrap?.Exception; + return texture is not null; + } + + /// + public bool ImmediateTryGetFromFile( + string file, + [NotNullWhen(true)] out IDalamudTextureWrap? texture, + out Exception? exception) + { + ThreadSafety.AssertMainThread(); + var t = this.fileSystemTextures.GetOrAdd(file, CreateFileSystemSharableTexture); + texture = t.GetImmediate(); + exception = t.UnderlyingWrap?.Exception; + return texture is not null; + } /// public Task GetFromGameIconAsync(in GameIconLookup lookup) => @@ -336,7 +357,22 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid foreach (var path in paths) { if (this.gamePathTextures.TryRemove(path, out var r)) - r.DisableReviveAndReleaseSelfReference(); + { + if (r.ReleaseSelfReference(true) != 0 || r.HasRevivalPossibility) + { + lock (this.invalidatedTextures) + this.invalidatedTextures.Add(r); + } + } + + if (this.fileSystemTextures.TryRemove(path, out r)) + { + if (r.ReleaseSelfReference(true) != 0 || r.HasRevivalPossibility) + { + lock (this.invalidatedTextures) + this.invalidatedTextures.Add(r); + } + } } } @@ -359,17 +395,35 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid private void FrameworkOnUpdate(IFramework unused) { - foreach (var (k, v) in this.gamePathTextures) + if (!this.gamePathTextures.IsEmpty) { - if (v.ReleaseSelfReferenceIfExpired() == 0 && v.RevivalPossibility?.TryGetTarget(out _) is not true) - _ = this.gamePathTextures.TryRemove(k, out _); + foreach (var (k, v) in this.gamePathTextures) + { + if (TextureFinalReleasePredicate(v)) + _ = this.gamePathTextures.TryRemove(k, out _); + } } - foreach (var (k, v) in this.fileSystemTextures) + if (!this.fileSystemTextures.IsEmpty) { - if (v.ReleaseSelfReferenceIfExpired() == 0 && v.RevivalPossibility?.TryGetTarget(out _) is not true) - _ = this.fileSystemTextures.TryRemove(k, out _); + foreach (var (k, v) in this.fileSystemTextures) + { + if (TextureFinalReleasePredicate(v)) + _ = this.fileSystemTextures.TryRemove(k, out _); + } } + + // ReSharper disable once InconsistentlySynchronizedField + if (this.invalidatedTextures.Count != 0) + { + lock (this.invalidatedTextures) + this.invalidatedTextures.RemoveWhere(TextureFinalReleasePredicate); + } + + return; + + static bool TextureFinalReleasePredicate(SharableTexture v) => + v.ReleaseSelfReference(false) == 0 && !v.HasRevivalPossibility; } private string GetIconPathByValue(GameIconLookup lookup) => diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs index 37fc958af..f9886dd2c 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs @@ -117,9 +117,8 @@ public class IconBrowserWidget : IDataWindowWidget try { var cursor = ImGui.GetCursorScreenPos(); - var texture = texm.ImmediateGetFromGameIcon(new((uint)iconId)); - if (texm.ImmediateGetStateFromGameIcon(new((uint)iconId), out var exc) || exc is null) + if (texm.ImmediateTryGetFromGameIcon(new((uint)iconId), out var texture, out var exc)) { ImGui.Image(texture.ImGuiHandle, this.iconSize); @@ -145,9 +144,9 @@ public class IconBrowserWidget : IDataWindowWidget ImGui.SetTooltip(iconId.ToString()); } } - else + else if (exc is not null) { - // This texture was null, draw nothing, and prevent from trying to show it again. + // This texture failed to load; draw nothing, and prevent from trying to show it again. this.nullValues.Add(iconId); } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs index 351957974..f97fd040f 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -3,6 +3,7 @@ using System.IO; using System.Numerics; using System.Threading.Tasks; +using Dalamud.Interface.Components; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Internal.SharableTextures; using Dalamud.Interface.Utility; @@ -62,15 +63,26 @@ internal class TexWidget : IDataWindowWidget GC.Collect(); ImGui.PushID("loadedGameTextures"); - if (ImGui.CollapsingHeader($"Loaded Game Textures: {this.textureManager.GamePathTextures.Count}###header")) + if (ImGui.CollapsingHeader($"Loaded Game Textures: {this.textureManager.GamePathTextures.Count:g}###header")) this.DrawLoadedTextures(this.textureManager.GamePathTextures); ImGui.PopID(); ImGui.PushID("loadedFileTextures"); - if (ImGui.CollapsingHeader($"Loaded File Textures: {this.textureManager.FileSystemTextures.Count}###header")) + if (ImGui.CollapsingHeader($"Loaded File Textures: {this.textureManager.FileSystemTextures.Count:g}###header")) this.DrawLoadedTextures(this.textureManager.FileSystemTextures); ImGui.PopID(); + lock (this.textureManager.InvalidatedTextures) + { + ImGui.PushID("invalidatedTextures"); + if (ImGui.CollapsingHeader($"Invalidated: {this.textureManager.InvalidatedTextures.Count:g}###header")) + { + this.DrawLoadedTextures(this.textureManager.InvalidatedTextures); + } + + ImGui.PopID(); + } + if (ImGui.CollapsingHeader("Load Game File by Icon ID", ImGuiTreeNodeFlags.DefaultOpen)) this.DrawIconInput(); @@ -133,18 +145,33 @@ internal class TexWidget : IDataWindowWidget } } - private unsafe void DrawLoadedTextures(IReadOnlyDictionary textures) + private unsafe void DrawLoadedTextures(ICollection textures) { + var im = Service.Get(); if (!ImGui.BeginTable("##table", 6)) return; + const int numIcons = 3; + float iconWidths; + using (im.IconFontHandle?.Push()) + { + iconWidths = ImGui.CalcTextSize(FontAwesomeIcon.Image.ToIconString()).X; + iconWidths += ImGui.CalcTextSize(FontAwesomeIcon.Sync.ToIconString()).X; + iconWidths += ImGui.CalcTextSize(FontAwesomeIcon.Trash.ToIconString()).X; + } + ImGui.TableSetupScrollFreeze(0, 1); ImGui.TableSetupColumn("ID", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("000000").X); - ImGui.TableSetupColumn("Preview", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("Preview_").X); ImGui.TableSetupColumn("Source", ImGuiTableColumnFlags.WidthStretch); ImGui.TableSetupColumn("RefCount", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("RefCount__").X); ImGui.TableSetupColumn("SelfRef", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("00.000___").X); ImGui.TableSetupColumn("CanRevive", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("CanRevive__").X); + ImGui.TableSetupColumn( + "Actions", + ImGuiTableColumnFlags.WidthFixed, + iconWidths + + (ImGui.GetStyle().FramePadding.X * 2 * numIcons) + + (ImGui.GetStyle().ItemSpacing.X * 1 * numIcons)); ImGui.TableHeadersRow(); var clipper = new ImGuiListClipperPtr(ImGuiNative.ImGuiListClipper_ImGuiListClipper()); @@ -168,20 +195,37 @@ internal class TexWidget : IDataWindowWidget if (!valid) break; - var (key, texture) = enu.Current; ImGui.TableNextRow(); + if (enu.Current is not { } texture) + { + // Should not happen + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("?"); + continue; + } + + var remain = texture.SelfReferenceExpiresInForDebug; + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); this.TextRightAlign($"{texture.InstanceIdForDebug:n0}"); ImGui.TableNextColumn(); - ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero); - ImGui.PushStyleColor(ImGuiCol.Button, Vector4.Zero); - ImGui.PushStyleColor(ImGuiCol.ButtonActive, Vector4.Zero); - ImGui.PushStyleColor(ImGuiCol.ButtonHovered, Vector4.Zero); - ImGui.Button("Hover"); - ImGui.PopStyleColor(3); - ImGui.PopStyleVar(); + this.TextCopiable(texture.SourcePathForDebug, true); + + ImGui.TableNextColumn(); + this.TextRightAlign($"{texture.RefCountForDebug:n0}"); + + ImGui.TableNextColumn(); + this.TextRightAlign(remain <= 0 ? "-" : $"{remain:00.000}"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted(texture.HasRevivalPossibility ? "Yes" : "No"); + + ImGui.TableNextColumn(); + ImGuiComponents.IconButton(FontAwesomeIcon.Image); if (ImGui.IsItemHovered() && texture.GetImmediate() is { } immediate) { ImGui.BeginTooltip(); @@ -189,18 +233,21 @@ internal class TexWidget : IDataWindowWidget ImGui.EndTooltip(); } - ImGui.TableNextColumn(); - this.TextCopiable(key); + ImGui.SameLine(); + if (ImGuiComponents.IconButton(FontAwesomeIcon.Sync)) + this.textureManager.InvalidatePaths(new[] { texture.SourcePathForDebug }); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip($"Call {nameof(ITextureSubstitutionProvider.InvalidatePaths)}."); - ImGui.TableNextColumn(); - this.TextRightAlign($"{texture.RefCountForDebug:n0}"); - - ImGui.TableNextColumn(); - var remain = texture.SelfReferenceExpiresInForDebug; - this.TextRightAlign(remain <= 0 ? "-" : $"{remain:00.000}"); - - ImGui.TableNextColumn(); - ImGui.TextUnformatted(texture.RevivalPossibility?.TryGetTarget(out _) is true ? "Yes" : "No"); + ImGui.SameLine(); + if (remain <= 0) + ImGui.BeginDisabled(); + if (ImGuiComponents.IconButton(FontAwesomeIcon.Trash)) + texture.ReleaseSelfReference(true); + if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) + ImGui.SetTooltip("Release self-reference immediately."); + if (remain <= 0) + ImGui.EndDisabled(); } if (!valid) diff --git a/Dalamud/Plugin/Services/ITextureProvider.cs b/Dalamud/Plugin/Services/ITextureProvider.cs index 8ee724fd5..8441ca3dc 100644 --- a/Dalamud/Plugin/Services/ITextureProvider.cs +++ b/Dalamud/Plugin/Services/ITextureProvider.cs @@ -11,26 +11,23 @@ namespace Dalamud.Plugin.Services; /// /// Service that grants you access to textures you may render via ImGui. /// +/// +/// Immediate functions
+/// Immediate functions do not throw, unless they are called outside the UI thread.
+/// Instances of returned from Immediate functions do not have to be disposed. +/// Calling on them is a no-op; it is safe to call +/// on them, as it will do nothing.
+/// Use and alike if you don't care about the load state and dimensions of the +/// texture to be loaded. These functions will return a valid texture that is empty (fully transparent).
+/// Use and alike if you do. These functions will return the load state, +/// loaded texture if available, and the load exception on failure.
+///
+/// All other functions
+/// Instances of or <> +/// returned from all other functions must be d after use. +///
public partial interface ITextureProvider { - /// Gets the state of the background load task for . - /// The icon specifier. - /// The exception, if failed. - /// true if loaded; false if not fully loaded or failed. - public bool ImmediateGetStateFromGameIcon(in GameIconLookup lookup, out Exception? exception); - - /// Gets the state of the background load task for . - /// The game-internal path to a .tex, .atex, or an image file such as .png. - /// The exception, if failed. - /// true if loaded; false if not fully loaded or failed. - public bool ImmediateGetStateFromGame(string path, out Exception? exception); - - /// Gets the state of the background load task for . - /// The filesystem path to a .tex, .atex, or an image file such as .png. - /// The exception, if failed. - /// true if loaded; false if not fully loaded or failed. - public bool ImmediateGetStateFromFile(string file, out Exception? exception); - /// 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 @@ -61,6 +58,49 @@ public partial interface ITextureProvider /// 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. + /// An instance of that is guaranteed to be available for + /// the current frame being drawn, or null if texture is not loaded (yet). + /// The load exception, if any. + /// true if points to the loaded texture; false if the texture is + /// still being loaded, or the load has failed. + /// on the returned will be ignored. + /// Thrown when called outside the UI thread. + public bool ImmediateTryGetFromGameIcon( + in GameIconLookup lookup, + [NotNullWhen(true)] out IDalamudTextureWrap? texture, + out Exception? exception); + + /// Gets a texture from a file shipped as a part of the game resources for use with the current frame. + /// + /// 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, or null if texture is not loaded (yet). + /// The load exception, if any. + /// true if points to the loaded texture; false if the texture is + /// still being loaded, or the load has failed. + /// on the returned will be ignored. + /// Thrown when called outside the UI thread. + public bool ImmediateTryGetFromGame( + string path, + [NotNullWhen(true)] out IDalamudTextureWrap? texture, + out Exception? exception); + + /// 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, or null if texture is not loaded (yet). + /// The load exception, if any. + /// true if points to the loaded texture; false if the texture is + /// still being loaded, or the load has failed. + /// on the returned will be ignored. + /// Thrown when called outside the UI thread. + public bool ImmediateTryGetFromFile( + string file, + [NotNullWhen(true)] out IDalamudTextureWrap? texture, + out Exception? exception); /// Gets the corresponding game icon for use with the current frame. /// The icon specifier. diff --git a/Dalamud/Plugin/Services/ITextureSubstitutionProvider.cs b/Dalamud/Plugin/Services/ITextureSubstitutionProvider.cs index 3ddd7d13e..371fbaf0f 100644 --- a/Dalamud/Plugin/Services/ITextureSubstitutionProvider.cs +++ b/Dalamud/Plugin/Services/ITextureSubstitutionProvider.cs @@ -33,5 +33,8 @@ public interface ITextureSubstitutionProvider /// and paths that are newly substituted. ///
/// The paths with a changed substitution status. + /// + /// This function will not invalidate the copies of the textures loaded from plugins. + /// public void InvalidatePaths(IEnumerable paths); } From ea633cd876fb3c38733900a1512e76975db1e4a5 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 22 Feb 2024 16:06:26 +0900 Subject: [PATCH 07/57] Implement texture load throttling & cancellable async loads --- .../FileSystemSharableTexture.cs | 51 +++-- .../GamePathSharableTexture.cs | 56 ++++-- .../SharableTextures/SharableTexture.cs | 87 +++++++-- .../SharableTextures/TextureLoadThrottler.cs | 178 ++++++++++++++++++ Dalamud/Interface/Internal/TextureManager.cs | 124 ++++++++---- .../Windows/Data/Widgets/IconBrowserWidget.cs | 167 ++++++++++------ Dalamud/Plugin/Services/ITextureProvider.cs | 86 +++++++-- 7 files changed, 595 insertions(+), 154 deletions(-) create mode 100644 Dalamud/Interface/Internal/SharableTextures/TextureLoadThrottler.cs diff --git a/Dalamud/Interface/Internal/SharableTextures/FileSystemSharableTexture.cs b/Dalamud/Interface/Internal/SharableTextures/FileSystemSharableTexture.cs index 06184b5ec..ff4a6adbf 100644 --- a/Dalamud/Interface/Internal/SharableTextures/FileSystemSharableTexture.cs +++ b/Dalamud/Interface/Internal/SharableTextures/FileSystemSharableTexture.cs @@ -1,3 +1,4 @@ +using System.Threading; using System.Threading.Tasks; using Dalamud.Utility; @@ -15,15 +16,40 @@ internal sealed class FileSystemSharableTexture : SharableTexture /// Initializes a new instance of the class. /// /// The path. - public FileSystemSharableTexture(string path) + /// If set to true, this class will hold a reference to self. + /// Otherwise, it is expected that the caller to hold the reference. + private FileSystemSharableTexture(string path, bool holdSelfReference) + : base(holdSelfReference) { this.path = path; - this.UnderlyingWrap = this.CreateTextureAsync(); + if (holdSelfReference) + { + this.UnderlyingWrap = Service.Get().CreateLoader( + this, + this.CreateTextureAsync, + this.LoadCancellationToken); + } } /// public override string SourcePathForDebug => this.path; + /// + /// Creates a new instance of . + /// The new instance will hold a reference to itself. + /// + /// The path. + /// The new instance. + public static SharableTexture CreateImmediate(string path) => new FileSystemSharableTexture(path, true); + + /// + /// Creates a new instance of . + /// The caller is expected to manage ownership of the new instance. + /// + /// The path. + /// The new instance. + public static SharableTexture CreateAsync(string path) => new FileSystemSharableTexture(path, false); + /// public override string ToString() => $"{nameof(FileSystemSharableTexture)}#{this.InstanceIdForDebug}({this.path})"; @@ -38,15 +64,16 @@ internal sealed class FileSystemSharableTexture : SharableTexture /// protected override void ReviveResources() => - this.UnderlyingWrap = this.CreateTextureAsync(); + this.UnderlyingWrap = Service.Get().CreateLoader( + this, + this.CreateTextureAsync, + this.LoadCancellationToken); - private Task CreateTextureAsync() => - Task.Run( - () => - { - var w = (IDalamudTextureWrap)Service.Get().LoadImage(this.path) - ?? throw new("Failed to load image because of an unknown reason."); - this.DisposeSuppressingWrap = new(w); - return w; - }); + private Task CreateTextureAsync(CancellationToken cancellationToken) + { + var w = (IDalamudTextureWrap)Service.Get().LoadImage(this.path) + ?? throw new("Failed to load image because of an unknown reason."); + this.DisposeSuppressingWrap = new(w); + return Task.FromResult(w); + } } diff --git a/Dalamud/Interface/Internal/SharableTextures/GamePathSharableTexture.cs b/Dalamud/Interface/Internal/SharableTextures/GamePathSharableTexture.cs index e58f21c26..ad026aff7 100644 --- a/Dalamud/Interface/Internal/SharableTextures/GamePathSharableTexture.cs +++ b/Dalamud/Interface/Internal/SharableTextures/GamePathSharableTexture.cs @@ -1,4 +1,5 @@ using System.IO; +using System.Threading; using System.Threading.Tasks; using Dalamud.Data; @@ -19,15 +20,40 @@ internal sealed class GamePathSharableTexture : SharableTexture /// Initializes a new instance of the class. /// /// The path. - public GamePathSharableTexture(string path) + /// If set to true, this class will hold a reference to self. + /// Otherwise, it is expected that the caller to hold the reference. + private GamePathSharableTexture(string path, bool holdSelfReference) + : base(holdSelfReference) { this.path = path; - this.UnderlyingWrap = this.CreateTextureAsync(); + if (holdSelfReference) + { + this.UnderlyingWrap = Service.Get().CreateLoader( + this, + this.CreateTextureAsync, + this.LoadCancellationToken); + } } /// public override string SourcePathForDebug => this.path; + /// + /// Creates a new instance of . + /// The new instance will hold a reference to itself. + /// + /// The path. + /// The new instance. + public static SharableTexture CreateImmediate(string path) => new GamePathSharableTexture(path, true); + + /// + /// Creates a new instance of . + /// The caller is expected to manage ownership of the new instance. + /// + /// The path. + /// The new instance. + public static SharableTexture CreateAsync(string path) => new GamePathSharableTexture(path, false); + /// public override string ToString() => $"{nameof(GamePathSharableTexture)}#{this.InstanceIdForDebug}({this.path})"; @@ -41,17 +67,19 @@ internal sealed class GamePathSharableTexture : SharableTexture /// protected override void ReviveResources() => - this.UnderlyingWrap = this.CreateTextureAsync(); + this.UnderlyingWrap = Service.Get().CreateLoader( + this, + this.CreateTextureAsync, + this.LoadCancellationToken); - private Task CreateTextureAsync() => - Task.Run( - async () => - { - var dm = await Service.GetAsync(); - var im = await Service.GetAsync(); - var file = dm.GetFile(this.path); - var t = (IDalamudTextureWrap)im.LoadImageFromTexFile(file ?? throw new FileNotFoundException()); - this.DisposeSuppressingWrap = new(t); - return t; - }); + private async Task CreateTextureAsync(CancellationToken cancellationToken) + { + var dm = await Service.GetAsync(); + var im = await Service.GetAsync(); + var file = dm.GetFile(this.path); + cancellationToken.ThrowIfCancellationRequested(); + var t = (IDalamudTextureWrap)im.LoadImageFromTexFile(file ?? throw new FileNotFoundException()); + this.DisposeSuppressingWrap = new(t); + return t; + } } diff --git a/Dalamud/Interface/Internal/SharableTextures/SharableTexture.cs b/Dalamud/Interface/Internal/SharableTextures/SharableTexture.cs index c08cdb7e9..057589ee7 100644 --- a/Dalamud/Interface/Internal/SharableTextures/SharableTexture.cs +++ b/Dalamud/Interface/Internal/SharableTextures/SharableTexture.cs @@ -9,9 +9,9 @@ namespace Dalamud.Interface.Internal.SharableTextures; /// /// Represents a texture that may have multiple reference holders (owners). /// -internal abstract class SharableTexture : IRefCountable +internal abstract class SharableTexture : IRefCountable, TextureLoadThrottler.IThrottleBasisProvider { - private const int SelfReferenceDurationTicks = 5000; + private const int SelfReferenceDurationTicks = 2000; private const long SelfReferenceExpiryExpired = long.MaxValue; private static long instanceCounter; @@ -22,15 +22,24 @@ internal abstract class SharableTexture : IRefCountable private int refCount; private long selfReferenceExpiry; private IDalamudTextureWrap? availableOnAccessWrapForApi9; + private CancellationTokenSource? cancellationTokenSource; /// /// Initializes a new instance of the class. /// - protected SharableTexture() + /// If set to true, this class will hold a reference to self. + /// Otherwise, it is expected that the caller to hold the reference. + protected SharableTexture(bool holdSelfReference) { this.InstanceIdForDebug = Interlocked.Increment(ref instanceCounter); this.refCount = 1; - this.selfReferenceExpiry = Environment.TickCount64 + SelfReferenceDurationTicks; + this.selfReferenceExpiry = + holdSelfReference + ? Environment.TickCount64 + SelfReferenceDurationTicks + : SelfReferenceExpiryExpired; + this.IsOpportunistic = true; + this.FirstRequestedTick = this.LatestRequestedTick = Environment.TickCount64; + this.cancellationTokenSource = new(); } /// @@ -66,11 +75,26 @@ internal abstract class SharableTexture : IRefCountable /// public Task? UnderlyingWrap { get; set; } + /// + public bool IsOpportunistic { get; private set; } + + /// + public long FirstRequestedTick { get; private set; } + + /// + public long LatestRequestedTick { get; private set; } + /// /// Gets or sets the dispose-suppressing wrap for . /// protected DisposeSuppressingTextureWrap? DisposeSuppressingWrap { get; set; } + /// + /// Gets a cancellation token for cancelling load. + /// Intended to be called from implementors' constructors and . + /// + protected CancellationToken LoadCancellationToken => this.cancellationTokenSource?.Token ?? default; + /// /// Gets or sets a weak reference to an object that demands this objects to be alive. /// @@ -124,6 +148,7 @@ internal abstract class SharableTexture : IRefCountable continue; } + this.cancellationTokenSource = null; this.ReleaseResources(); this.resourceReleased = true; @@ -175,6 +200,7 @@ internal abstract class SharableTexture : IRefCountable if (this.TryAddRef(out _) != IRefCountable.RefCountResult.StillAlive) return null; + this.LatestRequestedTick = Environment.TickCount64; var nexp = Environment.TickCount64 + SelfReferenceDurationTicks; while (true) { @@ -197,22 +223,45 @@ internal abstract class SharableTexture : IRefCountable /// Creates a new reference to this texture. The texture is guaranteed to be available until /// is called. /// + /// The cancellation token. /// The task containing the texture. - public Task CreateNewReference() + public async Task CreateNewReference(CancellationToken cancellationToken) { + cancellationToken.ThrowIfCancellationRequested(); + this.AddRef(); if (this.UnderlyingWrap is null) throw new InvalidOperationException("AddRef returned but UnderlyingWrap is null?"); - return this.UnderlyingWrap.ContinueWith( - r => + this.IsOpportunistic = false; + this.LatestRequestedTick = Environment.TickCount64; + var uw = this.UnderlyingWrap; + if (cancellationToken != default) + { + while (!uw.IsCompleted) { - if (r.IsCompletedSuccessfully) - return Task.FromResult((IDalamudTextureWrap)new RefCountableWrappingTextureWrap(r.Result, this)); + if (cancellationToken.IsCancellationRequested) + { + this.Release(); + throw new OperationCanceledException(cancellationToken); + } - this.Release(); - return r; - }).Unwrap(); + await Task.WhenAny(uw, Task.Delay(1000000, cancellationToken)); + } + } + + IDalamudTextureWrap dtw; + try + { + dtw = await uw; + } + catch + { + this.Release(); + throw; + } + + return new RefCountableWrappingTextureWrap(dtw, this); } /// @@ -233,7 +282,7 @@ internal abstract class SharableTexture : IRefCountable if (this.RevivalPossibility?.TryGetTarget(out this.availableOnAccessWrapForApi9) is true) return this.availableOnAccessWrapForApi9; - var newRefTask = this.CreateNewReference(); + var newRefTask = this.CreateNewReference(default); newRefTask.Wait(); if (!newRefTask.IsCompletedSuccessfully) return null; @@ -276,7 +325,17 @@ internal abstract class SharableTexture : IRefCountable if (alterResult != IRefCountable.RefCountResult.AlreadyDisposed) return alterResult; - this.ReviveResources(); + this.cancellationTokenSource = new(); + try + { + this.ReviveResources(); + } + catch + { + this.cancellationTokenSource = null; + throw; + } + if (this.RevivalPossibility?.TryGetTarget(out var target) is true) this.availableOnAccessWrapForApi9 = target; diff --git a/Dalamud/Interface/Internal/SharableTextures/TextureLoadThrottler.cs b/Dalamud/Interface/Internal/SharableTextures/TextureLoadThrottler.cs new file mode 100644 index 000000000..65fee34d6 --- /dev/null +++ b/Dalamud/Interface/Internal/SharableTextures/TextureLoadThrottler.cs @@ -0,0 +1,178 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; + +namespace Dalamud.Interface.Internal.SharableTextures; + +/// +/// Service for managing texture loads. +/// +[ServiceManager.EarlyLoadedService] +internal class TextureLoadThrottler : IServiceType +{ + private readonly List workList = new(); + private readonly List activeWorkList = new(); + + [ServiceManager.ServiceConstructor] + private TextureLoadThrottler() => + this.MaxActiveWorkItems = Math.Min(64, Environment.ProcessorCount); + + /// + /// Basis for throttling. + /// + internal interface IThrottleBasisProvider + { + /// + /// Gets a value indicating whether the resource is requested in an opportunistic way. + /// + bool IsOpportunistic { get; } + + /// + /// Gets the first requested tick count from . + /// + long FirstRequestedTick { get; } + + /// + /// Gets the latest requested tick count from . + /// + long LatestRequestedTick { get; } + } + + private int MaxActiveWorkItems { get; } + + /// + /// Creates a texture loader. + /// + /// The throttle basis. + /// The immediate load function. + /// The cancellation token. + /// The task. + public Task CreateLoader( + IThrottleBasisProvider basis, + Func> immediateLoadFunction, + CancellationToken cancellationToken) + { + var work = new WorkItem + { + TaskCompletionSource = new(), + Basis = basis, + CancellationToken = cancellationToken, + ImmediateLoadFunction = immediateLoadFunction, + }; + + _ = Task.Run( + () => + { + lock (this.workList) + { + this.workList.Add(work); + if (this.activeWorkList.Count >= this.MaxActiveWorkItems) + return; + } + + this.ContinueWork(); + }, + default); + + return work.TaskCompletionSource.Task; + } + + private void ContinueWork() + { + WorkItem minWork; + lock (this.workList) + { + if (this.workList.Count == 0) + return; + + if (this.activeWorkList.Count >= this.MaxActiveWorkItems) + return; + + var minIndex = 0; + for (var i = 1; i < this.workList.Count; i++) + { + if (this.workList[i].CompareTo(this.workList[minIndex]) < 0) + minIndex = i; + } + + minWork = this.workList[minIndex]; + // Avoid shifting; relocate the element to remove to the last + if (minIndex != this.workList.Count - 1) + (this.workList[^1], this.workList[minIndex]) = (this.workList[minIndex], this.workList[^1]); + this.workList.RemoveAt(this.workList.Count - 1); + + this.activeWorkList.Add(minWork); + } + + try + { + minWork.CancellationToken.ThrowIfCancellationRequested(); + minWork.InnerTask = minWork.ImmediateLoadFunction(minWork.CancellationToken); + } + catch (Exception e) + { + minWork.InnerTask = Task.FromException(e); + } + + minWork.InnerTask.ContinueWith( + r => + { + // Swallow exception, if any + _ = r.Exception; + + lock (this.workList) + this.activeWorkList.Remove(minWork); + if (r.IsCompletedSuccessfully) + minWork.TaskCompletionSource.SetResult(r.Result); + else if (r.Exception is not null) + minWork.TaskCompletionSource.SetException(r.Exception); + else if (r.IsCanceled) + minWork.TaskCompletionSource.SetCanceled(); + else + minWork.TaskCompletionSource.SetException(new Exception("??")); + this.ContinueWork(); + }); + } + + /// + /// A read-only implementation of . + /// + public class ReadOnlyThrottleBasisProvider : IThrottleBasisProvider + { + /// + public bool IsOpportunistic { get; init; } = false; + + /// + public long FirstRequestedTick { get; init; } = Environment.TickCount64; + + /// + public long LatestRequestedTick { get; init; } = Environment.TickCount64; + } + + [SuppressMessage( + "StyleCop.CSharp.OrderingRules", + "SA1206:Declaration keywords should follow order", + Justification = "no")] + private record WorkItem : IComparable + { + public required TaskCompletionSource TaskCompletionSource { get; init; } + + public required IThrottleBasisProvider Basis { get; init; } + + public required CancellationToken CancellationToken { get; init; } + + public required Func> ImmediateLoadFunction { get; init; } + + public Task? InnerTask { get; set; } + + public int CompareTo(WorkItem other) + { + if (this.Basis.IsOpportunistic != other.Basis.IsOpportunistic) + return this.Basis.IsOpportunistic ? 1 : -1; + if (this.Basis.IsOpportunistic) + return -this.Basis.LatestRequestedTick.CompareTo(other.Basis.LatestRequestedTick); + return this.Basis.FirstRequestedTick.CompareTo(other.Basis.FirstRequestedTick); + } + } +} diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index d1ab16a1d..0e6686025 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -2,6 +2,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Threading; using System.Threading.Tasks; using BitFaster.Caching.Lru; @@ -56,6 +57,9 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid [ServiceManager.ServiceDependency] private readonly InterfaceManager interfaceManager = Service.Get(); + [ServiceManager.ServiceDependency] + private readonly TextureLoadThrottler textureLoadThrottler = Service.Get(); + private readonly ConcurrentLru lookupToPath = new(PathLookupLruCount); private readonly ConcurrentDictionary gamePathTextures = new(); private readonly ConcurrentDictionary fileSystemTextures = new(); @@ -148,13 +152,14 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] [Obsolete("See interface definition.")] public IDalamudTextureWrap? GetTextureFromGame(string path, bool keepAlive = false) => - this.gamePathTextures.GetOrAdd(path, CreateGamePathSharableTexture).GetAvailableOnAccessWrapForApi9(); + this.gamePathTextures.GetOrAdd(path, GamePathSharableTexture.CreateImmediate) + .GetAvailableOnAccessWrapForApi9(); /// [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] [Obsolete("See interface definition.")] public IDalamudTextureWrap? GetTextureFromFile(FileInfo file, bool keepAlive = false) => - this.fileSystemTextures.GetOrAdd(file.FullName, CreateFileSystemSharableTexture) + this.fileSystemTextures.GetOrAdd(file.FullName, FileSystemSharableTexture.CreateImmediate) .GetAvailableOnAccessWrapForApi9(); #pragma warning restore CS0618 // Type or member is obsolete @@ -191,7 +196,7 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid out Exception? exception) { ThreadSafety.AssertMainThread(); - var t = this.gamePathTextures.GetOrAdd(path, CreateGamePathSharableTexture); + var t = this.gamePathTextures.GetOrAdd(path, GamePathSharableTexture.CreateImmediate); texture = t.GetImmediate(); exception = t.UnderlyingWrap?.Exception; return texture is not null; @@ -204,41 +209,64 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid out Exception? exception) { ThreadSafety.AssertMainThread(); - var t = this.fileSystemTextures.GetOrAdd(file, CreateFileSystemSharableTexture); + var t = this.fileSystemTextures.GetOrAdd(file, FileSystemSharableTexture.CreateImmediate); texture = t.GetImmediate(); exception = t.UnderlyingWrap?.Exception; return texture is not null; } /// - public Task GetFromGameIconAsync(in GameIconLookup lookup) => - this.GetFromGameAsync(this.lookupToPath.GetOrAdd(lookup, this.GetIconPathByValue)); + public Task GetFromGameIconAsync( + in GameIconLookup lookup, + CancellationToken cancellationToken = default) => + this.GetFromGameAsync(this.lookupToPath.GetOrAdd(lookup, this.GetIconPathByValue), cancellationToken); /// - public Task GetFromGameAsync(string path) => - this.gamePathTextures.GetOrAdd(path, CreateGamePathSharableTexture).CreateNewReference(); + public Task GetFromGameAsync( + string path, + CancellationToken cancellationToken = default) => + this.gamePathTextures.GetOrAdd(path, GamePathSharableTexture.CreateAsync) + .CreateNewReference(cancellationToken); /// - public Task GetFromFileAsync(string file) => - this.fileSystemTextures.GetOrAdd(file, CreateFileSystemSharableTexture).CreateNewReference(); + public Task GetFromFileAsync( + string file, + CancellationToken cancellationToken = default) => + this.fileSystemTextures.GetOrAdd(file, FileSystemSharableTexture.CreateAsync) + .CreateNewReference(cancellationToken); /// - public Task GetFromImageAsync(ReadOnlyMemory bytes) => - Task.Run( - () => this.interfaceManager.LoadImage(bytes.ToArray()) - ?? throw new("Failed to load image because of an unknown reason.")); + public Task GetFromImageAsync( + ReadOnlyMemory bytes, + CancellationToken cancellationToken = default) => + this.textureLoadThrottler.CreateLoader( + new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), + _ => Task.FromResult( + this.interfaceManager.LoadImage(bytes.ToArray()) + ?? throw new("Failed to load image because of an unknown reason.")), + cancellationToken); /// - public async Task GetFromImageAsync(Stream stream, bool leaveOpen = false) - { - await using var streamDispose = leaveOpen ? null : stream; - await using var ms = stream.CanSeek ? new MemoryStream((int)stream.Length) : new(); - await stream.CopyToAsync(ms).ConfigureAwait(false); - return await this.GetFromImageAsync(ms.GetBuffer()); - } + public Task GetFromImageAsync( + Stream stream, + bool leaveOpen = false, + CancellationToken cancellationToken = default) => + this.textureLoadThrottler.CreateLoader( + new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), + async ct => + { + await using var streamDispose = leaveOpen ? null : stream; + await using var ms = stream.CanSeek ? new MemoryStream((int)stream.Length) : new(); + await stream.CopyToAsync(ms, ct).ConfigureAwait(false); + return await this.GetFromImageAsync(ms.GetBuffer(), ct); + }, + cancellationToken); /// - public IDalamudTextureWrap GetFromRaw(RawImageSpecification specs, ReadOnlySpan bytes) => + public IDalamudTextureWrap GetFromRaw( + RawImageSpecification specs, + ReadOnlySpan bytes, + CancellationToken cancellationToken = default) => this.interfaceManager.LoadImageFromDxgiFormat( bytes, specs.Pitch, @@ -247,23 +275,47 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid (SharpDX.DXGI.Format)specs.DxgiFormat); /// - public Task GetFromRawAsync(RawImageSpecification specs, ReadOnlyMemory bytes) => - Task.Run(() => this.GetFromRaw(specs, bytes.Span)); + public Task GetFromRawAsync( + RawImageSpecification specs, + ReadOnlyMemory bytes, + CancellationToken cancellationToken = default) => + this.textureLoadThrottler.CreateLoader( + new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), + ct => Task.FromResult(this.GetFromRaw(specs, bytes.Span, ct)), + cancellationToken); /// - public async Task GetFromRawAsync( + public Task GetFromRawAsync( RawImageSpecification specs, Stream stream, - bool leaveOpen = false) - { - await using var streamDispose = leaveOpen ? null : stream; - await using var ms = stream.CanSeek ? new MemoryStream((int)stream.Length) : new(); - await stream.CopyToAsync(ms).ConfigureAwait(false); - return await this.GetFromRawAsync(specs, ms.GetBuffer()); - } + bool leaveOpen = false, + CancellationToken cancellationToken = default) => + this.textureLoadThrottler.CreateLoader( + new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), + async ct => + { + await using var streamDispose = leaveOpen ? null : stream; + await using var ms = stream.CanSeek ? new MemoryStream((int)stream.Length) : new(); + await stream.CopyToAsync(ms, ct).ConfigureAwait(false); + return await this.GetFromRawAsync(specs, ms.GetBuffer(), ct); + }, + cancellationToken); /// - public IDalamudTextureWrap GetTexture(TexFile file) => this.interfaceManager.LoadImageFromTexFile(file); + public IDalamudTextureWrap GetTexture(TexFile file) => this.GetFromTexFileAsync(file).Result; + + /// + public Task GetFromTexFileAsync( + TexFile file, + CancellationToken cancellationToken = default) => + this.textureLoadThrottler.CreateLoader( + new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), + _ => Task.FromResult(this.interfaceManager.LoadImageFromTexFile(file)), + cancellationToken); + + /// + public bool SupportsDxgiFormat(int dxgiFormat) => + this.interfaceManager.SupportsDxgiFormat((SharpDX.DXGI.Format)dxgiFormat); /// public bool TryGetIconPath(in GameIconLookup lookup, out string path) @@ -376,12 +428,6 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid } } - private static SharableTexture CreateGamePathSharableTexture(string gamePath) => - new GamePathSharableTexture(gamePath); - - private static SharableTexture CreateFileSystemSharableTexture(string fileSystemPath) => - new FileSystemSharableTexture(fileSystemPath); - private static string FormatIconPath(uint iconId, string? type, bool highResolution) { var format = highResolution ? HighResolutionIconFileFormat : IconFileFormat; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs index f9886dd2c..4a5bd89cf 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -using System.Linq; using System.Numerics; +using System.Threading.Tasks; using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; @@ -14,15 +14,12 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// public class IconBrowserWidget : IDataWindowWidget { - // Remove range 170,000 -> 180,000 by default, this specific range causes exceptions. - private readonly HashSet nullValues = Enumerable.Range(170000, 9999).ToHashSet(); - private Vector2 iconSize = new(64.0f, 64.0f); private Vector2 editIconSize = new(64.0f, 64.0f); - private List valueRange = Enumerable.Range(0, 200000).ToList(); + private List? valueRange; + private Task>? iconIdsTask; - private int lastNullValueCount; private int startRange; private int stopRange = 200000; private bool showTooltipImage; @@ -48,25 +45,51 @@ public class IconBrowserWidget : IDataWindowWidget /// public void Draw() { + this.iconIdsTask ??= Task.Run( + () => + { + var texm = Service.Get(); + + var result = new List<(int ItemId, string Path)>(200000); + for (var iconId = 0; iconId < 200000; iconId++) + { + // // Remove range 170,000 -> 180,000 by default, this specific range causes exceptions. + // if (iconId is >= 170000 and < 180000) + // continue; + if (!texm.TryGetIconPath(new((uint)iconId), out var path)) + continue; + result.Add((iconId, path)); + } + + return result; + }); + this.DrawOptions(); - if (ImGui.BeginChild("ScrollableSection", ImGui.GetContentRegionAvail(), false, ImGuiWindowFlags.NoMove)) + if (!this.iconIdsTask.IsCompleted) { - var itemsPerRow = (int)MathF.Floor( - ImGui.GetContentRegionMax().X / (this.iconSize.X + ImGui.GetStyle().ItemSpacing.X)); - var itemHeight = this.iconSize.Y + ImGui.GetStyle().ItemSpacing.Y; - - ImGuiClip.ClippedDraw(this.valueRange, this.DrawIcon, itemsPerRow, itemHeight); + ImGui.TextUnformatted("Loading..."); } - - ImGui.EndChild(); - - this.ProcessMouseDragging(); - - if (this.lastNullValueCount != this.nullValues.Count) + else if (!this.iconIdsTask.IsCompletedSuccessfully) + { + ImGui.TextUnformatted(this.iconIdsTask.Exception?.ToString() ?? "Unknown error"); + } + else { this.RecalculateIndexRange(); - this.lastNullValueCount = this.nullValues.Count; + + if (ImGui.BeginChild("ScrollableSection", ImGui.GetContentRegionAvail(), false, ImGuiWindowFlags.NoMove)) + { + var itemsPerRow = (int)MathF.Floor( + ImGui.GetContentRegionMax().X / (this.iconSize.X + ImGui.GetStyle().ItemSpacing.X)); + var itemHeight = this.iconSize.Y + ImGui.GetStyle().ItemSpacing.Y; + + ImGuiClip.ClippedDraw(this.valueRange!, this.DrawIcon, itemsPerRow, itemHeight); + } + + ImGui.EndChild(); + + this.ProcessMouseDragging(); } } @@ -92,11 +115,13 @@ public class IconBrowserWidget : IDataWindowWidget ImGui.Columns(2); ImGui.PushItemWidth(ImGui.GetContentRegionAvail().X); - if (ImGui.InputInt("##StartRange", ref this.startRange, 0, 0)) this.RecalculateIndexRange(); + if (ImGui.InputInt("##StartRange", ref this.startRange, 0, 0)) + this.valueRange = null; ImGui.NextColumn(); ImGui.PushItemWidth(ImGui.GetContentRegionAvail().X); - if (ImGui.InputInt("##StopRange", ref this.stopRange, 0, 0)) this.RecalculateIndexRange(); + if (ImGui.InputInt("##StopRange", ref this.stopRange, 0, 0)) + this.valueRange = null; ImGui.NextColumn(); ImGui.Checkbox("Show Image in Tooltip", ref this.showTooltipImage); @@ -114,40 +139,32 @@ public class IconBrowserWidget : IDataWindowWidget private void DrawIcon(int iconId) { var texm = Service.Get(); - try + var cursor = ImGui.GetCursorScreenPos(); + + if (texm.ImmediateTryGetFromGameIcon(new((uint)iconId), out var texture, out var exc)) { - var cursor = ImGui.GetCursorScreenPos(); + ImGui.Image(texture.ImGuiHandle, this.iconSize); - if (texm.ImmediateTryGetFromGameIcon(new((uint)iconId), out var texture, out var exc)) + // If we have the option to show a tooltip image, draw the image, but make sure it's not too big. + if (ImGui.IsItemHovered() && this.showTooltipImage) { - ImGui.Image(texture.ImGuiHandle, this.iconSize); + ImGui.BeginTooltip(); - // If we have the option to show a tooltip image, draw the image, but make sure it's not too big. - if (ImGui.IsItemHovered() && this.showTooltipImage) - { - ImGui.BeginTooltip(); + var scale = GetImageScaleFactor(texture); - var scale = GetImageScaleFactor(texture); + var textSize = ImGui.CalcTextSize(iconId.ToString()); + ImGui.SetCursorPosX( + texture.Size.X * scale / 2.0f - textSize.X / 2.0f + ImGui.GetStyle().FramePadding.X * 2.0f); + ImGui.Text(iconId.ToString()); - var textSize = ImGui.CalcTextSize(iconId.ToString()); - ImGui.SetCursorPosX( - texture.Size.X * scale / 2.0f - textSize.X / 2.0f + ImGui.GetStyle().FramePadding.X * 2.0f); - ImGui.Text(iconId.ToString()); - - ImGui.Image(texture.ImGuiHandle, texture.Size * scale); - ImGui.EndTooltip(); - } - - // else, just draw the iconId. - else if (ImGui.IsItemHovered()) - { - ImGui.SetTooltip(iconId.ToString()); - } + ImGui.Image(texture.ImGuiHandle, texture.Size * scale); + ImGui.EndTooltip(); } - else if (exc is not null) + + // else, just draw the iconId. + else if (ImGui.IsItemHovered()) { - // This texture failed to load; draw nothing, and prevent from trying to show it again. - this.nullValues.Add(iconId); + ImGui.SetTooltip(iconId.ToString()); } ImGui.GetWindowDrawList().AddRect( @@ -155,10 +172,46 @@ public class IconBrowserWidget : IDataWindowWidget cursor + this.iconSize, ImGui.GetColorU32(ImGuiColors.DalamudWhite)); } - catch (Exception) + else if (exc is not null) { - // If something went wrong, prevent from trying to show this icon again. - this.nullValues.Add(iconId); + ImGui.Dummy(this.iconSize); + using (Service.Get().IconFontHandle?.Push()) + { + var iconText = FontAwesomeIcon.Ban.ToIconString(); + var textSize = ImGui.CalcTextSize(iconText); + ImGui.GetWindowDrawList().AddText( + cursor + ((this.iconSize - textSize) / 2), + ImGui.GetColorU32(ImGuiColors.DalamudRed), + iconText); + } + + if (ImGui.IsItemHovered()) + ImGui.SetTooltip($"{iconId}\n{exc}".Replace("%", "%%")); + + ImGui.GetWindowDrawList().AddRect( + cursor, + cursor + this.iconSize, + ImGui.GetColorU32(ImGuiColors.DalamudRed)); + } + else + { + const uint color = 0x50FFFFFFu; + const string text = "..."; + + ImGui.Dummy(this.iconSize); + var textSize = ImGui.CalcTextSize(text); + ImGui.GetWindowDrawList().AddText( + cursor + ((this.iconSize - textSize) / 2), + color, + text); + + if (ImGui.IsItemHovered()) + ImGui.SetTooltip(iconId.ToString()); + + ImGui.GetWindowDrawList().AddRect( + cursor, + cursor + this.iconSize, + color); } } @@ -195,14 +248,14 @@ public class IconBrowserWidget : IDataWindowWidget private void RecalculateIndexRange() { - if (this.stopRange <= this.startRange || this.stopRange <= 0 || this.startRange < 0) + if (this.valueRange is not null) + return; + + this.valueRange = new(); + foreach (var (id, _) in this.iconIdsTask!.Result) { - this.valueRange = new List(); - } - else - { - this.valueRange = Enumerable.Range(this.startRange, this.stopRange - this.startRange).ToList(); - this.valueRange.RemoveAll(value => this.nullValues.Contains(value)); + if (this.startRange <= id && id < this.stopRange) + this.valueRange.Add(id); } } } diff --git a/Dalamud/Plugin/Services/ITextureProvider.cs b/Dalamud/Plugin/Services/ITextureProvider.cs index 8441ca3dc..c314d7392 100644 --- a/Dalamud/Plugin/Services/ITextureProvider.cs +++ b/Dalamud/Plugin/Services/ITextureProvider.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Threading; using System.Threading.Tasks; using Dalamud.Interface.Internal; @@ -36,7 +37,7 @@ public partial interface ITextureProvider /// If the file is unavailable, then the returned instance of will point to an /// empty texture instead. /// Thrown when called outside the UI thread. - public IDalamudTextureWrap ImmediateGetFromGameIcon(in GameIconLookup lookup); + IDalamudTextureWrap ImmediateGetFromGameIcon(in GameIconLookup lookup); /// Gets a texture from a file shipped as a part of the game resources for use with the current frame. /// @@ -47,7 +48,7 @@ public partial interface ITextureProvider /// If the file is unavailable, then the returned instance of will point to an /// empty texture instead. /// Thrown when called outside the UI thread. - public IDalamudTextureWrap ImmediateGetFromGame(string path); + IDalamudTextureWrap ImmediateGetFromGame(string path); /// Gets a texture from a file on the filesystem for use with the current frame. /// The filesystem path to a .tex, .atex, or an image file such as .png. @@ -57,7 +58,7 @@ public partial interface ITextureProvider /// If the file is unavailable, then the returned instance of will point to an /// empty texture instead. /// Thrown when called outside the UI thread. - public IDalamudTextureWrap ImmediateGetFromFile(string file); + IDalamudTextureWrap ImmediateGetFromFile(string file); /// Gets the corresponding game icon for use with the current frame. /// The icon specifier. @@ -68,7 +69,7 @@ public partial interface ITextureProvider /// still being loaded, or the load has failed. /// on the returned will be ignored. /// Thrown when called outside the UI thread. - public bool ImmediateTryGetFromGameIcon( + bool ImmediateTryGetFromGameIcon( in GameIconLookup lookup, [NotNullWhen(true)] out IDalamudTextureWrap? texture, out Exception? exception); @@ -83,7 +84,7 @@ public partial interface ITextureProvider /// still being loaded, or the load has failed. /// on the returned will be ignored. /// Thrown when called outside the UI thread. - public bool ImmediateTryGetFromGame( + bool ImmediateTryGetFromGame( string path, [NotNullWhen(true)] out IDalamudTextureWrap? texture, out Exception? exception); @@ -97,60 +98,90 @@ public partial interface ITextureProvider /// still being loaded, or the load has failed. /// on the returned will be ignored. /// Thrown when called outside the UI thread. - public bool ImmediateTryGetFromFile( + bool ImmediateTryGetFromFile( string file, [NotNullWhen(true)] out IDalamudTextureWrap? texture, out Exception? exception); /// Gets the corresponding game icon for use with the current frame. /// The icon specifier. + /// The cancellation token. /// A containing the loaded texture on success. Dispose after use. - public Task GetFromGameIconAsync(in GameIconLookup lookup); + Task GetFromGameIconAsync( + in GameIconLookup lookup, + CancellationToken cancellationToken = default); /// Gets a texture from a file shipped as a part of the game resources. /// The game-internal path to a .tex, .atex, or an image file such as .png. + /// The cancellation token. /// A containing the loaded texture on success. Dispose after use. - public Task GetFromGameAsync(string path); + Task GetFromGameAsync( + string path, + CancellationToken cancellationToken = default); /// Gets a texture from a file on the filesystem. /// The filesystem path to a .tex, .atex, or an image file such as .png. + /// The cancellation token. /// A containing the loaded texture on success. Dispose after use. - public Task GetFromFileAsync(string file); + Task GetFromFileAsync( + string file, + CancellationToken cancellationToken = default); /// Gets a texture from the given bytes, trying to interpret it as a .tex file or other well-known image /// files, such as .png. /// The bytes to load. + /// The cancellation token. /// A containing the loaded texture on success. Dispose after use. - public Task GetFromImageAsync(ReadOnlyMemory bytes); + Task GetFromImageAsync( + ReadOnlyMemory bytes, + CancellationToken cancellationToken = default); /// Gets a texture from the given stream, trying to interpret it as a .tex file or other well-known image /// files, such as .png. /// The stream to load data from. /// Whether to leave the stream open once the task completes, sucessfully or not. + /// The cancellation token. /// A containing the loaded texture on success. Dispose after use. - public Task GetFromImageAsync(Stream stream, bool leaveOpen = false); + /// will be closed or not only according to ; + /// is irrelevant in closing the stream. + Task GetFromImageAsync( + Stream stream, + bool leaveOpen = false, + CancellationToken cancellationToken = default); /// Gets a texture from the given bytes, interpreting it as a raw bitmap. /// The specifications for the raw bitmap. /// The bytes to load. + /// The cancellation token. /// The texture loaded from the supplied raw bitmap. Dispose after use. - public IDalamudTextureWrap GetFromRaw(RawImageSpecification specs, ReadOnlySpan bytes); + IDalamudTextureWrap GetFromRaw( + RawImageSpecification specs, + ReadOnlySpan bytes, + CancellationToken cancellationToken = default); /// Gets a texture from the given bytes, interpreting it as a raw bitmap. /// The specifications for the raw bitmap. /// The bytes to load. + /// The cancellation token. /// A containing the loaded texture on success. Dispose after use. - public Task GetFromRawAsync(RawImageSpecification specs, ReadOnlyMemory bytes); + Task GetFromRawAsync( + RawImageSpecification specs, + ReadOnlyMemory bytes, + CancellationToken cancellationToken = default); /// Gets a texture from the given stream, interpreting the read data as a raw bitmap. /// The specifications for the raw bitmap. /// The stream to load data from. /// Whether to leave the stream open once the task completes, sucessfully or not. + /// The cancellation token. /// A containing the loaded texture on success. Dispose after use. - public Task GetFromRawAsync( + /// will be closed or not only according to ; + /// is irrelevant in closing the stream. + Task GetFromRawAsync( RawImageSpecification specs, Stream stream, - bool leaveOpen = false); + bool leaveOpen = false, + CancellationToken cancellationToken = default); /// /// Get a path for a specific icon's .tex file. @@ -158,7 +189,7 @@ public partial interface ITextureProvider /// The icon lookup. /// The path to the icon. /// If a corresponding file could not be found. - public string GetIconPath(in GameIconLookup lookup); + string GetIconPath(in GameIconLookup lookup); /// /// Gets the path of an icon. @@ -166,12 +197,31 @@ public partial interface ITextureProvider /// The icon lookup. /// The resolved path. /// true if the corresponding file exists and has been set. - public bool TryGetIconPath(in GameIconLookup lookup, [NotNullWhen(true)] out string? path); + bool TryGetIconPath(in GameIconLookup lookup, [NotNullWhen(true)] out string? path); + + /// + /// Get a texture handle for the specified Lumina . + /// Alias for fetching from . + /// + /// The texture to obtain a handle to. + /// A texture wrap that can be used to render the texture. Dispose after use. + IDalamudTextureWrap GetTexture(TexFile file); /// /// Get a texture handle for the specified Lumina . /// /// The texture to obtain a handle to. + /// The cancellation token. /// A texture wrap that can be used to render the texture. Dispose after use. - public IDalamudTextureWrap GetTexture(TexFile file); + Task GetFromTexFileAsync( + TexFile file, + CancellationToken cancellationToken = default); + + /// + /// Determines whether the system supports the given DXGI format. + /// For use with . + /// + /// The DXGI format. + /// true if supported. + bool SupportsDxgiFormat(int dxgiFormat); } From 517abb0c71a9e21c36e93d8de449ac207c845129 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 22 Feb 2024 16:08:00 +0900 Subject: [PATCH 08/57] Suppress hint --- Dalamud/Interface/Internal/SharableTextures/SharableTexture.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dalamud/Interface/Internal/SharableTextures/SharableTexture.cs b/Dalamud/Interface/Internal/SharableTextures/SharableTexture.cs index 057589ee7..807dafc18 100644 --- a/Dalamud/Interface/Internal/SharableTextures/SharableTexture.cs +++ b/Dalamud/Interface/Internal/SharableTextures/SharableTexture.cs @@ -283,6 +283,8 @@ internal abstract class SharableTexture : IRefCountable, TextureLoadThrottler.IT return this.availableOnAccessWrapForApi9; var newRefTask = this.CreateNewReference(default); + // Cancellation is not expected for this API + // ReSharper disable once MethodSupportsCancellation newRefTask.Wait(); if (!newRefTask.IsCompletedSuccessfully) return null; From ba51ec52f5bc9f50fd5bcb6e31cce89556c46496 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 22 Feb 2024 16:32:58 +0900 Subject: [PATCH 09/57] Better tex load cancellation handling --- .../SharableTextures/SharableTexture.cs | 1 + .../SharableTextures/TextureLoadThrottler.cs | 131 +++++++++--------- Dalamud/Logging/Internal/TaskTracker.cs | 3 +- 3 files changed, 70 insertions(+), 65 deletions(-) diff --git a/Dalamud/Interface/Internal/SharableTextures/SharableTexture.cs b/Dalamud/Interface/Internal/SharableTextures/SharableTexture.cs index 807dafc18..0a5faffc2 100644 --- a/Dalamud/Interface/Internal/SharableTextures/SharableTexture.cs +++ b/Dalamud/Interface/Internal/SharableTextures/SharableTexture.cs @@ -148,6 +148,7 @@ internal abstract class SharableTexture : IRefCountable, TextureLoadThrottler.IT continue; } + this.cancellationTokenSource?.Cancel(); this.cancellationTokenSource = null; this.ReleaseResources(); this.resourceReleased = true; diff --git a/Dalamud/Interface/Internal/SharableTextures/TextureLoadThrottler.cs b/Dalamud/Interface/Internal/SharableTextures/TextureLoadThrottler.cs index 65fee34d6..f540ebe87 100644 --- a/Dalamud/Interface/Internal/SharableTextures/TextureLoadThrottler.cs +++ b/Dalamud/Interface/Internal/SharableTextures/TextureLoadThrottler.cs @@ -11,7 +11,8 @@ namespace Dalamud.Interface.Internal.SharableTextures; [ServiceManager.EarlyLoadedService] internal class TextureLoadThrottler : IServiceType { - private readonly List workList = new(); + private readonly object workListLock = new(); + private readonly List pendingWorkList = new(); private readonly List activeWorkList = new(); [ServiceManager.ServiceConstructor] @@ -61,78 +62,82 @@ internal class TextureLoadThrottler : IServiceType ImmediateLoadFunction = immediateLoadFunction, }; - _ = Task.Run( - () => - { - lock (this.workList) - { - this.workList.Add(work); - if (this.activeWorkList.Count >= this.MaxActiveWorkItems) - return; - } - - this.ContinueWork(); - }, - default); + _ = Task.Run(() => this.ContinueWork(work), default); return work.TaskCompletionSource.Task; } - private void ContinueWork() + private async Task ContinueWork(WorkItem? newItem) { - WorkItem minWork; - lock (this.workList) + while (true) { - if (this.workList.Count == 0) - return; - - if (this.activeWorkList.Count >= this.MaxActiveWorkItems) - return; - - var minIndex = 0; - for (var i = 1; i < this.workList.Count; i++) + WorkItem? minWork = null; + lock (this.workListLock) { - if (this.workList[i].CompareTo(this.workList[minIndex]) < 0) - minIndex = i; + if (newItem is not null) + { + this.pendingWorkList.Add(newItem); + newItem = null; + } + + if (this.activeWorkList.Count >= this.MaxActiveWorkItems) + return; + + var minIndex = -1; + for (var i = 0; i < this.pendingWorkList.Count; i++) + { + var work = this.pendingWorkList[i]; + if (work.CancellationToken.IsCancellationRequested) + { + work.TaskCompletionSource.SetCanceled(work.CancellationToken); + this.RelocatePendingWorkItemToEndAndEraseUnsafe(i--); + continue; + } + + if (minIndex == -1 || work.CompareTo(this.pendingWorkList[minIndex]) < 0) + { + minIndex = i; + minWork = work; + } + } + + if (minWork is null) + return; + + this.RelocatePendingWorkItemToEndAndEraseUnsafe(minIndex); + + this.activeWorkList.Add(minWork); } - minWork = this.workList[minIndex]; - // Avoid shifting; relocate the element to remove to the last - if (minIndex != this.workList.Count - 1) - (this.workList[^1], this.workList[minIndex]) = (this.workList[minIndex], this.workList[^1]); - this.workList.RemoveAt(this.workList.Count - 1); - - this.activeWorkList.Add(minWork); - } - - try - { - minWork.CancellationToken.ThrowIfCancellationRequested(); - minWork.InnerTask = minWork.ImmediateLoadFunction(minWork.CancellationToken); - } - catch (Exception e) - { - minWork.InnerTask = Task.FromException(e); - } - - minWork.InnerTask.ContinueWith( - r => + try { - // Swallow exception, if any - _ = r.Exception; + var r = await minWork.ImmediateLoadFunction(minWork.CancellationToken); + minWork.TaskCompletionSource.SetResult(r); + } + catch (Exception e) + { + minWork.TaskCompletionSource.SetException(e); + } - lock (this.workList) - this.activeWorkList.Remove(minWork); - if (r.IsCompletedSuccessfully) - minWork.TaskCompletionSource.SetResult(r.Result); - else if (r.Exception is not null) - minWork.TaskCompletionSource.SetException(r.Exception); - else if (r.IsCanceled) - minWork.TaskCompletionSource.SetCanceled(); - else - minWork.TaskCompletionSource.SetException(new Exception("??")); - this.ContinueWork(); - }); + lock (this.workListLock) + this.activeWorkList.Remove(minWork); + } + } + + /// + /// Remove an item in , avoiding shifting. + /// + /// Index of the item to remove. + private void RelocatePendingWorkItemToEndAndEraseUnsafe(int index) + { + // Relocate the element to remove to the last. + if (index != this.pendingWorkList.Count - 1) + { + (this.pendingWorkList[^1], this.pendingWorkList[index]) = + (this.pendingWorkList[index], this.pendingWorkList[^1]); + } + + this.pendingWorkList.RemoveAt(this.pendingWorkList.Count - 1); } /// @@ -164,8 +169,6 @@ internal class TextureLoadThrottler : IServiceType public required Func> ImmediateLoadFunction { get; init; } - public Task? InnerTask { get; set; } - public int CompareTo(WorkItem other) { if (this.Basis.IsOpportunistic != other.Basis.IsOpportunistic) diff --git a/Dalamud/Logging/Internal/TaskTracker.cs b/Dalamud/Logging/Internal/TaskTracker.cs index b65f0efa7..407b16642 100644 --- a/Dalamud/Logging/Internal/TaskTracker.cs +++ b/Dalamud/Logging/Internal/TaskTracker.cs @@ -36,8 +36,9 @@ internal class TaskTracker : IDisposable, IServiceType /// /// Gets a read-only list of tracked tasks. + /// Intended for use only from UI thread. /// - public static IReadOnlyList Tasks => TrackedTasksInternal.ToArray(); + public static IReadOnlyList Tasks => TrackedTasksInternal; /// /// Clear the list of tracked tasks. From 80875247b6dd233a7908da7f21c892cfd113e943 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 22 Feb 2024 16:52:07 +0900 Subject: [PATCH 10/57] Correct Async refcount handling --- .../FileSystemSharableTexture.cs | 7 +-- .../GamePathSharableTexture.cs | 7 +-- .../SharableTextures/SharableTexture.cs | 46 +++++++++++++++---- Dalamud/Interface/Internal/TextureManager.cs | 2 +- 4 files changed, 41 insertions(+), 21 deletions(-) diff --git a/Dalamud/Interface/Internal/SharableTextures/FileSystemSharableTexture.cs b/Dalamud/Interface/Internal/SharableTextures/FileSystemSharableTexture.cs index ff4a6adbf..bd867d6a3 100644 --- a/Dalamud/Interface/Internal/SharableTextures/FileSystemSharableTexture.cs +++ b/Dalamud/Interface/Internal/SharableTextures/FileSystemSharableTexture.cs @@ -23,12 +23,7 @@ internal sealed class FileSystemSharableTexture : SharableTexture { this.path = path; if (holdSelfReference) - { - this.UnderlyingWrap = Service.Get().CreateLoader( - this, - this.CreateTextureAsync, - this.LoadCancellationToken); - } + this.ReviveResources(); } /// diff --git a/Dalamud/Interface/Internal/SharableTextures/GamePathSharableTexture.cs b/Dalamud/Interface/Internal/SharableTextures/GamePathSharableTexture.cs index ad026aff7..82f2d1b48 100644 --- a/Dalamud/Interface/Internal/SharableTextures/GamePathSharableTexture.cs +++ b/Dalamud/Interface/Internal/SharableTextures/GamePathSharableTexture.cs @@ -27,12 +27,7 @@ internal sealed class GamePathSharableTexture : SharableTexture { this.path = path; if (holdSelfReference) - { - this.UnderlyingWrap = Service.Get().CreateLoader( - this, - this.CreateTextureAsync, - this.LoadCancellationToken); - } + this.ReviveResources(); } /// diff --git a/Dalamud/Interface/Internal/SharableTextures/SharableTexture.cs b/Dalamud/Interface/Internal/SharableTextures/SharableTexture.cs index 0a5faffc2..0bdd4ed4a 100644 --- a/Dalamud/Interface/Internal/SharableTextures/SharableTexture.cs +++ b/Dalamud/Interface/Internal/SharableTextures/SharableTexture.cs @@ -32,14 +32,26 @@ internal abstract class SharableTexture : IRefCountable, TextureLoadThrottler.IT protected SharableTexture(bool holdSelfReference) { this.InstanceIdForDebug = Interlocked.Increment(ref instanceCounter); - this.refCount = 1; - this.selfReferenceExpiry = - holdSelfReference - ? Environment.TickCount64 + SelfReferenceDurationTicks - : SelfReferenceExpiryExpired; - this.IsOpportunistic = true; + + if (holdSelfReference) + { + this.refCount = 1; + this.selfReferenceExpiry = Environment.TickCount64 + SelfReferenceDurationTicks; + this.ContentQueried = true; + this.IsOpportunistic = true; + this.resourceReleased = false; + this.cancellationTokenSource = new(); + } + else + { + this.refCount = 0; + this.selfReferenceExpiry = SelfReferenceExpiryExpired; + this.ContentQueried = false; + this.IsOpportunistic = false; + this.resourceReleased = true; + } + this.FirstRequestedTick = this.LatestRequestedTick = Environment.TickCount64; - this.cancellationTokenSource = new(); } /// @@ -84,6 +96,12 @@ internal abstract class SharableTexture : IRefCountable, TextureLoadThrottler.IT /// public long LatestRequestedTick { get; private set; } + /// + /// Gets a value indicating whether the content has been queried, + /// i.e. or is called. + /// + public bool ContentQueried { get; private set; } + /// /// Gets or sets the dispose-suppressing wrap for . /// @@ -199,8 +217,12 @@ internal abstract class SharableTexture : IRefCountable, TextureLoadThrottler.IT public IDalamudTextureWrap? GetImmediate() { if (this.TryAddRef(out _) != IRefCountable.RefCountResult.StillAlive) + { + this.ContentQueried = true; return null; + } + this.ContentQueried = true; this.LatestRequestedTick = Environment.TickCount64; var nexp = Environment.TickCount64 + SelfReferenceDurationTicks; while (true) @@ -230,7 +252,15 @@ internal abstract class SharableTexture : IRefCountable, TextureLoadThrottler.IT { cancellationToken.ThrowIfCancellationRequested(); - this.AddRef(); + try + { + this.AddRef(); + } + finally + { + this.ContentQueried = true; + } + if (this.UnderlyingWrap is null) throw new InvalidOperationException("AddRef returned but UnderlyingWrap is null?"); diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index 0e6686025..7789e9e76 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -469,7 +469,7 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid return; static bool TextureFinalReleasePredicate(SharableTexture v) => - v.ReleaseSelfReference(false) == 0 && !v.HasRevivalPossibility; + v.ContentQueried && v.ReleaseSelfReference(false) == 0 && !v.HasRevivalPossibility; } private string GetIconPathByValue(GameIconLookup lookup) => From 3bf907002f9fc09d73a67c583e0c120cca296f4e Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 22 Feb 2024 16:53:13 +0900 Subject: [PATCH 11/57] Remove CancellationToken from GetFromRaw(Sync) --- Dalamud/Interface/Internal/TextureManager.cs | 5 ++--- Dalamud/Plugin/Services/ITextureProvider.cs | 4 +--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index 7789e9e76..462a5ec76 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -265,8 +265,7 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid /// public IDalamudTextureWrap GetFromRaw( RawImageSpecification specs, - ReadOnlySpan bytes, - CancellationToken cancellationToken = default) => + ReadOnlySpan bytes) => this.interfaceManager.LoadImageFromDxgiFormat( bytes, specs.Pitch, @@ -281,7 +280,7 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid CancellationToken cancellationToken = default) => this.textureLoadThrottler.CreateLoader( new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), - ct => Task.FromResult(this.GetFromRaw(specs, bytes.Span, ct)), + ct => Task.FromResult(this.GetFromRaw(specs, bytes.Span)), cancellationToken); /// diff --git a/Dalamud/Plugin/Services/ITextureProvider.cs b/Dalamud/Plugin/Services/ITextureProvider.cs index c314d7392..b6c7e2cbd 100644 --- a/Dalamud/Plugin/Services/ITextureProvider.cs +++ b/Dalamud/Plugin/Services/ITextureProvider.cs @@ -152,12 +152,10 @@ public partial interface ITextureProvider /// Gets a texture from the given bytes, interpreting it as a raw bitmap. /// The specifications for the raw bitmap. /// The bytes to load. - /// The cancellation token. /// The texture loaded from the supplied raw bitmap. Dispose after use. IDalamudTextureWrap GetFromRaw( RawImageSpecification specs, - ReadOnlySpan bytes, - CancellationToken cancellationToken = default); + ReadOnlySpan bytes); /// Gets a texture from the given bytes, interpreting it as a raw bitmap. /// The specifications for the raw bitmap. From 248c7911a0ce2339a61cedf45430e1d29f3a6d23 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 22 Feb 2024 17:05:17 +0900 Subject: [PATCH 12/57] Ensure leaveOpen takes effect even on cancellations --- Dalamud/Interface/Internal/TextureManager.cs | 54 +++++++++++++------- 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index 462a5ec76..378697e88 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -252,15 +252,23 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid bool leaveOpen = false, CancellationToken cancellationToken = default) => this.textureLoadThrottler.CreateLoader( - new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), - async ct => - { - await using var streamDispose = leaveOpen ? null : stream; - await using var ms = stream.CanSeek ? new MemoryStream((int)stream.Length) : new(); - await stream.CopyToAsync(ms, ct).ConfigureAwait(false); - return await this.GetFromImageAsync(ms.GetBuffer(), ct); - }, - cancellationToken); + 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 await this.GetFromImageAsync(ms.GetBuffer(), ct); + }, + cancellationToken) + .ContinueWith( + r => + { + if (!leaveOpen) + stream.Dispose(); + return r; + }, + default(CancellationToken)) + .Unwrap(); /// public IDalamudTextureWrap GetFromRaw( @@ -280,7 +288,7 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid CancellationToken cancellationToken = default) => this.textureLoadThrottler.CreateLoader( new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), - ct => Task.FromResult(this.GetFromRaw(specs, bytes.Span)), + _ => Task.FromResult(this.GetFromRaw(specs, bytes.Span)), cancellationToken); /// @@ -290,15 +298,23 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid bool leaveOpen = false, CancellationToken cancellationToken = default) => this.textureLoadThrottler.CreateLoader( - new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), - async ct => - { - await using var streamDispose = leaveOpen ? null : stream; - await using var ms = stream.CanSeek ? new MemoryStream((int)stream.Length) : new(); - await stream.CopyToAsync(ms, ct).ConfigureAwait(false); - return await this.GetFromRawAsync(specs, ms.GetBuffer(), ct); - }, - cancellationToken); + 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 await this.GetFromRawAsync(specs, ms.GetBuffer(), ct); + }, + cancellationToken) + .ContinueWith( + r => + { + if (!leaveOpen) + stream.Dispose(); + return r; + }, + default(CancellationToken)) + .Unwrap(); /// public IDalamudTextureWrap GetTexture(TexFile file) => this.GetFromTexFileAsync(file).Result; From 71b84bcf404662ba29a50e0f473f8a41820d8e15 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 22 Feb 2024 17:57:24 +0900 Subject: [PATCH 13/57] Move all texture loading functionalities from IM to TM --- .../Interface/Internal/InterfaceManager.cs | 168 ------------------ .../FileSystemSharableTexture.cs | 10 +- .../GamePathSharableTexture.cs | 7 +- Dalamud/Interface/Internal/TextureManager.cs | 112 ++++++++++-- .../Internal/Windows/PluginImageCache.cs | 22 +-- .../PluginInstaller/PluginInstallerWindow.cs | 76 +++++--- .../FontAtlasFactory.BuildToolkit.cs | 25 ++- .../Internals/FontAtlasFactory.cs | 24 ++- Dalamud/Interface/UiBuilder.cs | 40 ++--- .../Plugin/Services/RawImageSpecification.cs | 10 ++ Dalamud/Storage/Assets/DalamudAssetManager.cs | 6 +- .../Assets/DalamudAssetRawTextureAttribute.cs | 28 +-- 12 files changed, 230 insertions(+), 298 deletions(-) diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 5bafc4ff3..7e9c8eed0 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -26,14 +26,9 @@ using Dalamud.Utility.Timing; using ImGuiNET; using ImGuiScene; -using Lumina.Data.Files; -using Lumina.Data.Parsing.Tex.Buffers; - using PInvoke; using Serilog; using SharpDX; -using SharpDX.Direct3D; -using SharpDX.Direct3D11; using SharpDX.DXGI; // general dev notes, here because it's easiest @@ -261,169 +256,6 @@ internal class InterfaceManager : IDisposable, IServiceType } } -#nullable enable - - /// - /// Load an image from disk. - /// - /// The filepath to load. - /// A texture, ready to use in ImGui. - public IDalamudTextureWrap? LoadImage(string filePath) - { - if (this.scene == null) - throw new InvalidOperationException("Scene isn't ready."); - - try - { - var wrap = this.scene?.LoadImage(filePath); - return wrap != null ? new DalamudTextureWrap(wrap) : null; - } - catch (Exception ex) - { - Log.Error(ex, $"Failed to load image from {filePath}"); - } - - return null; - } - - /// - /// Load an image from an array of bytes. - /// - /// The data to load. - /// A texture, ready to use in ImGui. - public IDalamudTextureWrap? LoadImage(byte[] imageData) - { - if (this.scene == null) - throw new InvalidOperationException("Scene isn't ready."); - - try - { - var wrap = this.scene?.LoadImage(imageData); - return wrap != null ? new DalamudTextureWrap(wrap) : null; - } - catch (Exception ex) - { - Log.Error(ex, "Failed to load image from memory"); - } - - return null; - } - - /// - /// Load an image from an array of bytes. - /// - /// The data to load. - /// The width in pixels. - /// The height in pixels. - /// The number of channels. - /// A texture, ready to use in ImGui. - public IDalamudTextureWrap? LoadImageRaw(byte[] imageData, int width, int height, int numChannels) - { - if (this.scene == null) - throw new InvalidOperationException("Scene isn't ready."); - - try - { - var wrap = this.scene?.LoadImageRaw(imageData, width, height, numChannels); - return wrap != null ? new DalamudTextureWrap(wrap) : null; - } - catch (Exception ex) - { - Log.Error(ex, "Failed to load image from raw data"); - } - - return null; - } - - /// - /// Check whether the current D3D11 Device supports the given DXGI format. - /// - /// DXGI format to check. - /// Whether it is supported. - public bool SupportsDxgiFormat(Format dxgiFormat) => this.scene is null - ? throw new InvalidOperationException("Scene isn't ready.") - : this.scene.Device.CheckFormatSupport(dxgiFormat).HasFlag(FormatSupport.Texture2D); - - /// - /// Load an image from a span of bytes of specified format. - /// - /// The data to load. - /// The pitch(stride) in bytes. - /// The width in pixels. - /// The height in pixels. - /// Format of the texture. - /// A texture, ready to use in ImGui. - public DalamudTextureWrap LoadImageFromDxgiFormat(ReadOnlySpan data, int pitch, int width, int height, Format dxgiFormat) - { - if (this.scene == null) - throw new InvalidOperationException("Scene isn't ready."); - - ShaderResourceView resView; - unsafe - { - fixed (void* pData = data) - { - var texDesc = new Texture2DDescription - { - Width = width, - Height = height, - MipLevels = 1, - ArraySize = 1, - Format = dxgiFormat, - SampleDescription = new(1, 0), - Usage = ResourceUsage.Immutable, - BindFlags = BindFlags.ShaderResource, - CpuAccessFlags = CpuAccessFlags.None, - OptionFlags = ResourceOptionFlags.None, - }; - - using var texture = new Texture2D(this.Device, texDesc, new DataRectangle(new(pData), pitch)); - resView = new(this.Device, texture, new() - { - Format = texDesc.Format, - Dimension = ShaderResourceViewDimension.Texture2D, - Texture2D = { MipLevels = texDesc.MipLevels }, - }); - } - } - - // no sampler for now because the ImGui implementation we copied doesn't allow for changing it - return new DalamudTextureWrap(new D3DTextureWrap(resView, width, height)); - } - -#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. /// diff --git a/Dalamud/Interface/Internal/SharableTextures/FileSystemSharableTexture.cs b/Dalamud/Interface/Internal/SharableTextures/FileSystemSharableTexture.cs index bd867d6a3..fe1b16de8 100644 --- a/Dalamud/Interface/Internal/SharableTextures/FileSystemSharableTexture.cs +++ b/Dalamud/Interface/Internal/SharableTextures/FileSystemSharableTexture.cs @@ -1,3 +1,5 @@ +using System.Buffers; +using System.IO; using System.Threading; using System.Threading.Tasks; @@ -64,11 +66,9 @@ internal sealed class FileSystemSharableTexture : SharableTexture this.CreateTextureAsync, this.LoadCancellationToken); - private Task CreateTextureAsync(CancellationToken cancellationToken) + private async Task CreateTextureAsync(CancellationToken cancellationToken) { - var w = (IDalamudTextureWrap)Service.Get().LoadImage(this.path) - ?? throw new("Failed to load image because of an unknown reason."); - this.DisposeSuppressingWrap = new(w); - return Task.FromResult(w); + var tm = await Service.GetAsync(); + return tm.NoThrottleGetFromImage(await File.ReadAllBytesAsync(this.path, cancellationToken)); } } diff --git a/Dalamud/Interface/Internal/SharableTextures/GamePathSharableTexture.cs b/Dalamud/Interface/Internal/SharableTextures/GamePathSharableTexture.cs index 82f2d1b48..d7c478187 100644 --- a/Dalamud/Interface/Internal/SharableTextures/GamePathSharableTexture.cs +++ b/Dalamud/Interface/Internal/SharableTextures/GamePathSharableTexture.cs @@ -70,10 +70,11 @@ internal sealed class GamePathSharableTexture : SharableTexture private async Task CreateTextureAsync(CancellationToken cancellationToken) { var dm = await Service.GetAsync(); - var im = await Service.GetAsync(); - var file = dm.GetFile(this.path); + var tm = await Service.GetAsync(); + if (dm.GetFile(this.path) is not { } file) + throw new FileNotFoundException(); cancellationToken.ThrowIfCancellationRequested(); - var t = (IDalamudTextureWrap)im.LoadImageFromTexFile(file ?? throw new FileNotFoundException()); + var t = tm.NoThrottleGetFromTexFile(file); this.DisposeSuppressingWrap = new(t); return t; } diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index 378697e88..a4edb4449 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -19,6 +19,11 @@ using Dalamud.Utility; using Lumina.Data.Files; +using SharpDX; +using SharpDX.Direct3D; +using SharpDX.Direct3D11; +using SharpDX.DXGI; + namespace Dalamud.Interface.Internal; // TODO API10: Remove keepAlive from public APIs @@ -241,9 +246,7 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid CancellationToken cancellationToken = default) => this.textureLoadThrottler.CreateLoader( new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), - _ => Task.FromResult( - this.interfaceManager.LoadImage(bytes.ToArray()) - ?? throw new("Failed to load image because of an unknown reason.")), + ct => Task.Run(() => this.NoThrottleGetFromImage(bytes.ToArray()), ct), cancellationToken); /// @@ -273,13 +276,46 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid /// public IDalamudTextureWrap GetFromRaw( RawImageSpecification specs, - ReadOnlySpan bytes) => - this.interfaceManager.LoadImageFromDxgiFormat( - bytes, - specs.Pitch, - specs.Width, - specs.Height, - (SharpDX.DXGI.Format)specs.DxgiFormat); + ReadOnlySpan bytes) + { + if (this.interfaceManager.Scene is not { } scene) + { + _ = Service.Get(); + scene = this.interfaceManager.Scene ?? throw new InvalidOperationException(); + } + + ShaderResourceView resView; + unsafe + { + fixed (void* pData = bytes) + { + var texDesc = new Texture2DDescription + { + Width = specs.Width, + Height = specs.Height, + MipLevels = 1, + ArraySize = 1, + Format = (Format)specs.DxgiFormat, + SampleDescription = new(1, 0), + Usage = ResourceUsage.Immutable, + BindFlags = BindFlags.ShaderResource, + CpuAccessFlags = CpuAccessFlags.None, + OptionFlags = ResourceOptionFlags.None, + }; + + using var texture = new Texture2D(scene.Device, texDesc, new DataRectangle(new(pData), specs.Pitch)); + resView = new(scene.Device, texture, new() + { + Format = texDesc.Format, + Dimension = ShaderResourceViewDimension.Texture2D, + Texture2D = { MipLevels = texDesc.MipLevels }, + }); + } + } + + // no sampler for now because the ImGui implementation we copied doesn't allow for changing it + return new DalamudTextureWrap(new D3DTextureWrap(resView, specs.Width, specs.Height)); + } /// public Task GetFromRawAsync( @@ -325,12 +361,20 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid CancellationToken cancellationToken = default) => this.textureLoadThrottler.CreateLoader( new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), - _ => Task.FromResult(this.interfaceManager.LoadImageFromTexFile(file)), + ct => Task.Run(() => this.NoThrottleGetFromTexFile(file), ct), cancellationToken); - + /// - public bool SupportsDxgiFormat(int dxgiFormat) => - this.interfaceManager.SupportsDxgiFormat((SharpDX.DXGI.Format)dxgiFormat); + public bool SupportsDxgiFormat(int dxgiFormat) + { + if (this.interfaceManager.Scene is not { } scene) + { + _ = Service.Get(); + scene = this.interfaceManager.Scene ?? throw new InvalidOperationException(); + } + + return scene.Device.CheckFormatSupport((Format)dxgiFormat).HasFlag(FormatSupport.Texture2D); + } /// public bool TryGetIconPath(in GameIconLookup lookup, out string path) @@ -443,6 +487,46 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid } } + /// + /// Gets a texture from the given image. Skips the load throttler; intended to be used from implementation of + /// s. + /// + /// The data. + /// The loaded texture. + internal IDalamudTextureWrap NoThrottleGetFromImage(ReadOnlyMemory bytes) + { + if (this.interfaceManager.Scene is not { } scene) + { + _ = Service.Get(); + scene = this.interfaceManager.Scene ?? throw new InvalidOperationException(); + } + + return new DalamudTextureWrap( + scene.LoadImage(bytes.ToArray()) + ?? throw new("Failed to load image because of an unknown reason.")); + } + + /// + /// Gets a texture from the given . Skips the load throttler; intended to be used from + /// implementation of s. + /// + /// The data. + /// The loaded texture. + internal IDalamudTextureWrap NoThrottleGetFromTexFile(TexFile file) + { + var buffer = file.TextureBuffer; + var (dxgiFormat, conversion) = TexFile.GetDxgiFormatFromTextureFormat(file.Header.Format, false); + if (conversion != TexFile.DxgiFormatConversion.NoConversion || !this.SupportsDxgiFormat(dxgiFormat)) + { + dxgiFormat = (int)Format.B8G8R8A8_UNorm; + buffer = buffer.Filter(0, 0, TexFile.TextureFormat.B8G8R8A8); + } + + return this.GetFromRaw( + RawImageSpecification.From(buffer.Width, buffer.Height, dxgiFormat), + buffer.RawData); + } + private static string FormatIconPath(uint iconId, string? type, bool highResolution) { var format = highResolution ? HighResolutionIconFileFormat : IconFileFormat; diff --git a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs index 29adbb3e5..6ae45c962 100644 --- a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs +++ b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs @@ -269,33 +269,17 @@ internal class PluginImageCache : IDisposable, IServiceType if (bytes == null) return null; - var interfaceManager = (await Service.GetAsync()).Manager; - var framework = await Service.GetAsync(); + var textureManager = await Service.GetAsync(); IDalamudTextureWrap? image; // FIXME(goat): This is a hack around this call failing randomly in certain situations. Might be related to not being called on the main thread. try { - image = interfaceManager.LoadImage(bytes); + image = await textureManager.GetFromImageAsync(bytes); } catch (Exception ex) { - Log.Error(ex, "Access violation during load plugin {name} from {Loc} (Async Thread)", name, loc); - - try - { - image = await framework.RunOnFrameworkThread(() => interfaceManager.LoadImage(bytes)); - } - catch (Exception ex2) - { - Log.Error(ex2, "Access violation during load plugin {name} from {Loc} (Framework Thread)", name, loc); - return null; - } - } - - if (image == null) - { - Log.Error($"Could not load {name} for {manifest.InternalName} at {loc}"); + Log.Error(ex, $"Could not load {name} for {manifest.InternalName} at {loc}"); return null; } diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 95c227662..29e76434b 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -69,8 +69,8 @@ internal class PluginInstallerWindow : Window, IDisposable private string[] testerImagePaths = new string[5]; private string testerIconPath = string.Empty; - private IDalamudTextureWrap?[]? testerImages; - private IDalamudTextureWrap? testerIcon; + private Task?[]? testerImages; + private Task? testerIcon; private bool testerError = false; private bool testerUpdateAvailable = false; @@ -1510,10 +1510,10 @@ internal class PluginInstallerWindow : Window, IDisposable ImGui.SetCursorPos(startCursor); - var hasIcon = this.testerIcon != null; + var hasIcon = this.testerIcon?.IsCompletedSuccessfully is true; var iconTex = this.imageCache.DefaultIcon; - if (hasIcon) iconTex = this.testerIcon; + if (hasIcon) iconTex = this.testerIcon.Result; var iconSize = ImGuiHelpers.ScaledVector2(64, 64); @@ -1607,10 +1607,24 @@ internal class PluginInstallerWindow : Window, IDisposable for (var i = 0; i < this.testerImages.Length; i++) { var popupId = $"pluginTestingImage{i}"; - var image = this.testerImages[i]; - if (image == null) + var imageTask = this.testerImages[i]; + if (imageTask == null) continue; + if (!imageTask.IsCompleted) + { + ImGui.TextUnformatted("Loading..."); + continue; + } + + if (imageTask.Exception is not null) + { + ImGui.TextUnformatted(imageTask.Exception.ToString()); + continue; + } + + var image = imageTask.Result; + ImGui.PushStyleVar(ImGuiStyleVar.PopupBorderSize, 0); ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero); ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero); @@ -1666,14 +1680,37 @@ internal class PluginInstallerWindow : Window, IDisposable ImGuiHelpers.ScaledDummy(20); - static void CheckImageSize(IDalamudTextureWrap? image, int maxWidth, int maxHeight, bool requireSquare) + static void CheckImageSize(Task? imageTask, int maxWidth, int maxHeight, bool requireSquare) { - if (image == null) + if (imageTask == null) return; - if (image.Width > maxWidth || image.Height > maxHeight) - ImGui.TextColored(ImGuiColors.DalamudRed, $"Image is larger than the maximum allowed resolution ({image.Width}x{image.Height} > {maxWidth}x{maxHeight})"); - if (requireSquare && image.Width != image.Height) - ImGui.TextColored(ImGuiColors.DalamudRed, $"Image must be square! Current size: {image.Width}x{image.Height}"); + + if (!imageTask.IsCompleted) + { + ImGui.Text("Loading..."); + return; + } + + ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed); + + if (imageTask.Exception is { } exc) + { + ImGui.TextUnformatted(exc.ToString()); + } + else + { + var image = imageTask.Result; + if (image.Width > maxWidth || image.Height > maxHeight) + { + ImGui.TextUnformatted( + $"Image is larger than the maximum allowed resolution ({image.Width}x{image.Height} > {maxWidth}x{maxHeight})"); + } + + if (requireSquare && image.Width != image.Height) + ImGui.TextUnformatted($"Image must be square! Current size: {image.Width}x{image.Height}"); + } + + ImGui.PopStyleColor(); } ImGui.InputText("Icon Path", ref this.testerIconPath, 1000); @@ -1695,7 +1732,7 @@ internal class PluginInstallerWindow : Window, IDisposable if (this.testerImages?.Length > 4) CheckImageSize(this.testerImages[4], PluginImageCache.PluginImageWidth, PluginImageCache.PluginImageHeight, false); - var im = Service.Get(); + var tm = Service.Get(); if (ImGui.Button("Load")) { try @@ -1708,23 +1745,18 @@ internal class PluginInstallerWindow : Window, IDisposable if (!this.testerIconPath.IsNullOrEmpty()) { - this.testerIcon = im.LoadImage(this.testerIconPath); + this.testerIcon = tm.GetFromFileAsync(this.testerIconPath); } - this.testerImages = new IDalamudTextureWrap[this.testerImagePaths.Length]; + this.testerImages = new Task?[this.testerImagePaths.Length]; for (var i = 0; i < this.testerImagePaths.Length; i++) { if (this.testerImagePaths[i].IsNullOrEmpty()) continue; - if (this.testerImages[i] != null) - { - this.testerImages[i].Dispose(); - this.testerImages[i] = null; - } - - this.testerImages[i] = im.LoadImage(this.testerImagePaths[i]); + _ = this.testerImages[i]?.ToContentDisposedTask(); + this.testerImages[i] = tm.GetFromFileAsync(this.testerImagePaths[i]); } } catch (Exception ex) diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs index 55af20329..0148e80dd 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs @@ -10,6 +10,7 @@ using Dalamud.Interface.FontIdentifier; using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal; using Dalamud.Interface.Utility; +using Dalamud.Plugin.Services; using Dalamud.Storage.Assets; using Dalamud.Utility; @@ -579,7 +580,7 @@ internal sealed partial class FontAtlasFactory var buf = Array.Empty(); try { - var use4 = this.factory.InterfaceManager.SupportsDxgiFormat(Format.B4G4R4A4_UNorm); + var use4 = this.factory.TextureManager.SupportsDxgiFormat((int)Format.B4G4R4A4_UNorm); var bpp = use4 ? 2 : 4; var width = this.NewImAtlas.TexWidth; var height = this.NewImAtlas.TexHeight; @@ -591,12 +592,9 @@ internal sealed partial class FontAtlasFactory } else if (texture.TexPixelsRGBA32 is not null) { - var wrap = this.factory.InterfaceManager.LoadImageFromDxgiFormat( - new(texture.TexPixelsRGBA32, width * height * 4), - width * 4, - width, - height, - use4 ? Format.B4G4R4A4_UNorm : Format.R8G8B8A8_UNorm); + var wrap = this.factory.TextureManager.GetFromRaw( + RawImageSpecification.Rgba32(width, height), + new(texture.TexPixelsRGBA32, width * height * 4)); this.data.AddExistingTexture(wrap); texture.TexID = wrap.ImGuiHandle; } @@ -634,12 +632,13 @@ internal sealed partial class FontAtlasFactory } } - var wrap = this.factory.InterfaceManager.LoadImageFromDxgiFormat( - buf, - width * bpp, - width, - height, - use4 ? Format.B4G4R4A4_UNorm : Format.B8G8R8A8_UNorm); + var wrap = this.factory.TextureManager.GetFromRaw( + new( + width, + height, + width * bpp, + (int)(use4 ? Format.B4G4R4A4_UNorm : Format.B8G8R8A8_UNorm)), + buf); this.data.AddExistingTexture(wrap); texture.TexID = wrap.ImGuiHandle; continue; diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs index 3e0fd1394..ffddcc272 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs @@ -46,11 +46,13 @@ internal sealed partial class FontAtlasFactory DataManager dataManager, Framework framework, InterfaceManager interfaceManager, - DalamudAssetManager dalamudAssetManager) + DalamudAssetManager dalamudAssetManager, + TextureManager textureManager) { this.Framework = framework; this.InterfaceManager = interfaceManager; this.dalamudAssetManager = dalamudAssetManager; + this.TextureManager = textureManager; this.SceneTask = Service .GetAsync() .ContinueWith(r => r.Result.Manager.Scene); @@ -144,6 +146,11 @@ internal sealed partial class FontAtlasFactory /// public InterfaceManager InterfaceManager { get; } + /// + /// Gets the service instance of . + /// + public TextureManager TextureManager { get; } + /// /// Gets the async task for inside . /// @@ -346,7 +353,7 @@ internal sealed partial class FontAtlasFactory var numPixels = texFile.Header.Width * texFile.Header.Height; _ = Service.Get(); - var targetIsB4G4R4A4 = this.InterfaceManager.SupportsDxgiFormat(Format.B4G4R4A4_UNorm); + var targetIsB4G4R4A4 = this.TextureManager.SupportsDxgiFormat((int)Format.B4G4R4A4_UNorm); var bpp = targetIsB4G4R4A4 ? 2 : 4; var buffer = ArrayPool.Shared.Rent(numPixels * bpp); try @@ -369,12 +376,13 @@ internal sealed partial class FontAtlasFactory } return this.scopedFinalizer.Add( - this.InterfaceManager.LoadImageFromDxgiFormat( - buffer, - texFile.Header.Width * bpp, - texFile.Header.Width, - texFile.Header.Height, - targetIsB4G4R4A4 ? Format.B4G4R4A4_UNorm : Format.B8G8R8A8_UNorm)); + this.TextureManager.GetFromRaw( + new( + texFile.Header.Width, + texFile.Header.Height, + texFile.Header.Width * bpp, + (int)(targetIsB4G4R4A4 ? Format.B4G4R4A4_UNorm : Format.B8G8R8A8_UNorm)), + buffer)); } finally { diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index ca0ecb71c..74a718507 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -379,6 +379,8 @@ public sealed class UiBuilder : IDisposable private Task InterfaceManagerWithSceneAsync => Service.GetAsync().ContinueWith(task => task.Result.Manager); + private ITextureProvider TextureProvider => Service.Get(); + /// /// Loads an image from the specified file. /// @@ -386,9 +388,7 @@ public sealed class UiBuilder : IDisposable /// 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."); + public IDalamudTextureWrap LoadImage(string filePath) => this.TextureProvider.GetFromFileAsync(filePath).Result; /// /// Loads an image from a byte stream, such as a png downloaded into memory. @@ -397,9 +397,7 @@ public sealed class UiBuilder : IDisposable /// 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."); + public IDalamudTextureWrap LoadImage(byte[] imageData) => this.TextureProvider.GetFromImageAsync(imageData).Result; /// /// Loads an image from raw unformatted pixel data, with no type or header information. To load formatted data, use . @@ -411,9 +409,12 @@ public sealed class UiBuilder : IDisposable /// 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."); + public IDalamudTextureWrap LoadImageRaw(byte[] imageData, int width, int height, int numChannels) => + numChannels switch + { + 4 => this.TextureProvider.GetFromRaw(RawImageSpecification.Rgba32(width, height), imageData), + _ => throw new NotSupportedException(), + }; /// /// Loads an ULD file that can load textures containing multiple icons in a single texture. @@ -430,10 +431,7 @@ public sealed class UiBuilder : IDisposable /// 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) - ?? throw new InvalidOperationException("Load failed.")); + public Task LoadImageAsync(string filePath) => this.TextureProvider.GetFromFileAsync(filePath); /// /// Asynchronously loads an image from a byte stream, such as a png downloaded into memory, when it's possible to do so. @@ -442,10 +440,8 @@ public sealed class UiBuilder : IDisposable /// 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) - ?? throw new InvalidOperationException("Load failed.")); + public Task LoadImageAsync(byte[] imageData) => + this.TextureProvider.GetFromImageAsync(imageData); /// /// Asynchronously loads an image from raw unformatted pixel data, with no type or header information, when it's possible to do so. To load formatted data, use . @@ -457,10 +453,12 @@ public sealed class UiBuilder : IDisposable /// 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) - ?? throw new InvalidOperationException("Load failed.")); + public Task LoadImageRawAsync(byte[] imageData, int width, int height, int numChannels) => + numChannels switch + { + 4 => this.TextureProvider.GetFromRawAsync(RawImageSpecification.Rgba32(width, height), imageData), + _ => Task.FromException(new NotSupportedException()), + }; /// /// Waits for UI to become available for use. diff --git a/Dalamud/Plugin/Services/RawImageSpecification.cs b/Dalamud/Plugin/Services/RawImageSpecification.cs index 696b3d6b6..206ce578e 100644 --- a/Dalamud/Plugin/Services/RawImageSpecification.cs +++ b/Dalamud/Plugin/Services/RawImageSpecification.cs @@ -213,4 +213,14 @@ public record struct RawImageSpecification(int Width, int Height, int Pitch, int /// The new instance. public static RawImageSpecification Rgba32(int width, int height) => new(width, height, width * 4, (int)DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM); + + /// + /// Creates a new instance of record using the given resolution, + /// in A8 UNorm pixel format. + /// + /// The width. + /// The height. + /// The new instance. + public static RawImageSpecification A8(int width, int height) => + new(width, height, width, (int)DXGI_FORMAT.DXGI_FORMAT_A8_UNORM); } diff --git a/Dalamud/Storage/Assets/DalamudAssetManager.cs b/Dalamud/Storage/Assets/DalamudAssetManager.cs index 8abf42e5c..83f03e274 100644 --- a/Dalamud/Storage/Assets/DalamudAssetManager.cs +++ b/Dalamud/Storage/Assets/DalamudAssetManager.cs @@ -302,17 +302,17 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA var buf = Array.Empty(); try { - var im = (await Service.GetAsync()).Manager; + var tm = await Service.GetAsync(); await using var stream = await this.CreateStreamAsync(asset); var length = checked((int)stream.Length); buf = ArrayPool.Shared.Rent(length); stream.ReadExactly(buf, 0, length); var image = purpose switch { - DalamudAssetPurpose.TextureFromPng => im.LoadImage(buf), + DalamudAssetPurpose.TextureFromPng => await tm.GetFromImageAsync(buf), DalamudAssetPurpose.TextureFromRaw => asset.GetAttribute() is { } raw - ? im.LoadImageFromDxgiFormat(buf, raw.Pitch, raw.Width, raw.Height, raw.Format) + ? await tm.GetFromRawAsync(raw.Specification, buf) : throw new InvalidOperationException( "TextureFromRaw must accompany a DalamudAssetRawTextureAttribute."), _ => null, diff --git a/Dalamud/Storage/Assets/DalamudAssetRawTextureAttribute.cs b/Dalamud/Storage/Assets/DalamudAssetRawTextureAttribute.cs index b79abb7d7..99253411b 100644 --- a/Dalamud/Storage/Assets/DalamudAssetRawTextureAttribute.cs +++ b/Dalamud/Storage/Assets/DalamudAssetRawTextureAttribute.cs @@ -1,4 +1,6 @@ -using SharpDX.DXGI; +using Dalamud.Plugin.Services; + +using SharpDX.DXGI; namespace Dalamud.Storage.Assets; @@ -17,29 +19,11 @@ internal class DalamudAssetRawTextureAttribute : Attribute /// The format. public DalamudAssetRawTextureAttribute(int width, int pitch, int height, Format format) { - this.Width = width; - this.Pitch = pitch; - this.Height = height; - this.Format = format; + this.Specification = new(width, height, pitch, (int)format); } /// - /// Gets the width. + /// Gets the specification. /// - public int Width { get; } - - /// - /// Gets the pitch. - /// - public int Pitch { get; } - - /// - /// Gets the height. - /// - public int Height { get; } - - /// - /// Gets the format. - /// - public Format Format { get; } + public RawImageSpecification Specification { get; } } From 2c013f39cad3a5afa170e6101bde06fef914816c Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 22 Feb 2024 18:07:42 +0900 Subject: [PATCH 14/57] fix --- .../SharableTextures/FileSystemSharableTexture.cs | 2 -- .../SharableTextures/GamePathSharableTexture.cs | 5 +---- .../Internal/SharableTextures/SharableTexture.cs | 13 ++++++------- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/Dalamud/Interface/Internal/SharableTextures/FileSystemSharableTexture.cs b/Dalamud/Interface/Internal/SharableTextures/FileSystemSharableTexture.cs index fe1b16de8..df4d2ca0a 100644 --- a/Dalamud/Interface/Internal/SharableTextures/FileSystemSharableTexture.cs +++ b/Dalamud/Interface/Internal/SharableTextures/FileSystemSharableTexture.cs @@ -1,4 +1,3 @@ -using System.Buffers; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -54,7 +53,6 @@ internal sealed class FileSystemSharableTexture : SharableTexture /// protected override void ReleaseResources() { - this.DisposeSuppressingWrap = null; _ = this.UnderlyingWrap?.ToContentDisposedTask(true); this.UnderlyingWrap = null; } diff --git a/Dalamud/Interface/Internal/SharableTextures/GamePathSharableTexture.cs b/Dalamud/Interface/Internal/SharableTextures/GamePathSharableTexture.cs index d7c478187..db51159bb 100644 --- a/Dalamud/Interface/Internal/SharableTextures/GamePathSharableTexture.cs +++ b/Dalamud/Interface/Internal/SharableTextures/GamePathSharableTexture.cs @@ -55,7 +55,6 @@ internal sealed class GamePathSharableTexture : SharableTexture /// protected override void ReleaseResources() { - this.DisposeSuppressingWrap = null; _ = this.UnderlyingWrap?.ToContentDisposedTask(true); this.UnderlyingWrap = null; } @@ -74,8 +73,6 @@ internal sealed class GamePathSharableTexture : SharableTexture if (dm.GetFile(this.path) is not { } file) throw new FileNotFoundException(); cancellationToken.ThrowIfCancellationRequested(); - var t = tm.NoThrottleGetFromTexFile(file); - this.DisposeSuppressingWrap = new(t); - return t; + return tm.NoThrottleGetFromTexFile(file); } } diff --git a/Dalamud/Interface/Internal/SharableTextures/SharableTexture.cs b/Dalamud/Interface/Internal/SharableTextures/SharableTexture.cs index 0bdd4ed4a..cb12c1894 100644 --- a/Dalamud/Interface/Internal/SharableTextures/SharableTexture.cs +++ b/Dalamud/Interface/Internal/SharableTextures/SharableTexture.cs @@ -23,6 +23,7 @@ internal abstract class SharableTexture : IRefCountable, TextureLoadThrottler.IT private long selfReferenceExpiry; private IDalamudTextureWrap? availableOnAccessWrapForApi9; private CancellationTokenSource? cancellationTokenSource; + private DisposeSuppressingTextureWrap? disposeSuppressingWrap; /// /// Initializes a new instance of the class. @@ -102,11 +103,6 @@ internal abstract class SharableTexture : IRefCountable, TextureLoadThrottler.IT /// public bool ContentQueried { get; private set; } - /// - /// Gets or sets the dispose-suppressing wrap for . - /// - protected DisposeSuppressingTextureWrap? DisposeSuppressingWrap { get; set; } - /// /// Gets a cancellation token for cancelling load. /// Intended to be called from implementors' constructors and . @@ -168,6 +164,7 @@ internal abstract class SharableTexture : IRefCountable, TextureLoadThrottler.IT this.cancellationTokenSource?.Cancel(); this.cancellationTokenSource = null; + this.disposeSuppressingWrap = null; this.ReleaseResources(); this.resourceReleased = true; @@ -238,7 +235,9 @@ internal abstract class SharableTexture : IRefCountable, TextureLoadThrottler.IT // Release the reference for rendering, after rendering ImGui. Service.Get().EnqueueDeferredDispose(this); - return this.DisposeSuppressingWrap; + return this.UnderlyingWrap?.IsCompletedSuccessfully is true + ? this.disposeSuppressingWrap ??= new(this.UnderlyingWrap.Result) + : null; } } @@ -458,7 +457,7 @@ internal abstract class SharableTexture : IRefCountable, TextureLoadThrottler.IT return t; this.inner.UnderlyingWrap?.Wait(); - return this.inner.DisposeSuppressingWrap ?? Service.Get().Empty4X4; + return this.inner.disposeSuppressingWrap ?? Service.Get().Empty4X4; } } } From e12563d20ff756de7bf5fecde6a10d89e0c19de0 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 22 Feb 2024 18:31:04 +0900 Subject: [PATCH 15/57] Suppress cancelled task exception from bubbling --- .../Interface/Internal/SharableTextures/TextureLoadThrottler.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dalamud/Interface/Internal/SharableTextures/TextureLoadThrottler.cs b/Dalamud/Interface/Internal/SharableTextures/TextureLoadThrottler.cs index f540ebe87..47e3ab90f 100644 --- a/Dalamud/Interface/Internal/SharableTextures/TextureLoadThrottler.cs +++ b/Dalamud/Interface/Internal/SharableTextures/TextureLoadThrottler.cs @@ -90,6 +90,7 @@ internal class TextureLoadThrottler : IServiceType if (work.CancellationToken.IsCancellationRequested) { work.TaskCompletionSource.SetCanceled(work.CancellationToken); + _ = work.TaskCompletionSource.Task.Exception; this.RelocatePendingWorkItemToEndAndEraseUnsafe(i--); continue; } @@ -117,6 +118,7 @@ internal class TextureLoadThrottler : IServiceType catch (Exception e) { minWork.TaskCompletionSource.SetException(e); + _ = minWork.TaskCompletionSource.Task.Exception; } lock (this.workListLock) From aa35052a15ffcce1344a1342e115c22fe337993b Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 22 Feb 2024 18:31:08 +0900 Subject: [PATCH 16/57] update UldWrapper --- Dalamud/Interface/UldWrapper.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Dalamud/Interface/UldWrapper.cs b/Dalamud/Interface/UldWrapper.cs index dd8986bed..4b0c9cf4f 100644 --- a/Dalamud/Interface/UldWrapper.cs +++ b/Dalamud/Interface/UldWrapper.cs @@ -4,6 +4,7 @@ using System.Linq; using Dalamud.Data; using Dalamud.Interface.Internal; +using Dalamud.Plugin.Services; using Dalamud.Utility; using Lumina.Data.Files; using Lumina.Data.Parsing.Uld; @@ -14,16 +15,17 @@ namespace Dalamud.Interface; public class UldWrapper : IDisposable { private readonly DataManager data; - private readonly UiBuilder uiBuilder; + private readonly TextureManager textureManager; private readonly Dictionary textures = new(); /// Initializes a new instance of the class, wrapping an ULD file. /// The UiBuilder used to load textures. /// The requested ULD file. - internal UldWrapper(UiBuilder uiBuilder, string uldPath) + internal UldWrapper(UiBuilder? uiBuilder, string uldPath) { - this.uiBuilder = uiBuilder; + _ = uiBuilder; this.data = Service.Get(); + this.textureManager = Service.Get(); this.Uld = this.data.GetFile(uldPath); } @@ -123,7 +125,7 @@ public class UldWrapper : IDisposable inputSlice.CopyTo(outputSlice); } - return this.uiBuilder.LoadImageRaw(imageData, part.W, part.H, 4); + return this.textureManager.GetFromRaw(RawImageSpecification.Rgba32(part.W, part.H), imageData); } private (uint Id, int Width, int Height, bool HD, byte[] RgbaData)? GetTexture(string texturePath) From f8492dc06b5cdbdf841d34df10a023522dc28506 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 28 Feb 2024 19:16:14 +0900 Subject: [PATCH 17/57] changes --- .../{Internal => }/GameIconLookup.cs | 2 +- Dalamud/Interface/ISharedImmediateTexture.cs | 69 ++++++ .../FileSystemSharedImmediateTexture.cs} | 18 +- .../GamePathSharedImmediateTexture.cs} | 18 +- .../SharedImmediateTexture.cs} | 233 +++++++++++++----- .../TextureLoadThrottler.cs | 2 +- Dalamud/Interface/Internal/TextureManager.cs | 180 +++++--------- .../Windows/Data/Widgets/IconBrowserWidget.cs | 2 +- .../Windows/Data/Widgets/TexWidget.cs | 29 ++- .../Internal/Windows/PluginImageCache.cs | 2 +- .../PluginInstaller/PluginInstallerWindow.cs | 4 +- .../FontAtlasFactory.BuildToolkit.cs | 4 +- .../Internals/FontAtlasFactory.cs | 2 +- Dalamud/Interface/UiBuilder.cs | 27 +- Dalamud/Interface/UldWrapper.cs | 2 +- .../Plugin/Services/ITextureProvider.Api9.cs | 6 +- Dalamud/Plugin/Services/ITextureProvider.cs | 167 ++++--------- Dalamud/Storage/Assets/DalamudAssetManager.cs | 4 +- 18 files changed, 410 insertions(+), 361 deletions(-) rename Dalamud/Interface/{Internal => }/GameIconLookup.cs (94%) create mode 100644 Dalamud/Interface/ISharedImmediateTexture.cs rename Dalamud/Interface/Internal/{SharableTextures/FileSystemSharableTexture.cs => SharedImmediateTextures/FileSystemSharedImmediateTexture.cs} (72%) rename Dalamud/Interface/Internal/{SharableTextures/GamePathSharableTexture.cs => SharedImmediateTextures/GamePathSharedImmediateTexture.cs} (73%) rename Dalamud/Interface/Internal/{SharableTextures/SharableTexture.cs => SharedImmediateTextures/SharedImmediateTexture.cs} (71%) rename Dalamud/Interface/Internal/{SharableTextures => }/TextureLoadThrottler.cs (99%) diff --git a/Dalamud/Interface/Internal/GameIconLookup.cs b/Dalamud/Interface/GameIconLookup.cs similarity index 94% rename from Dalamud/Interface/Internal/GameIconLookup.cs rename to Dalamud/Interface/GameIconLookup.cs index b34db9d59..001f519aa 100644 --- a/Dalamud/Interface/Internal/GameIconLookup.cs +++ b/Dalamud/Interface/GameIconLookup.cs @@ -1,6 +1,6 @@ using System.Diagnostics.CodeAnalysis; -namespace Dalamud.Interface.Internal; +namespace Dalamud.Interface; /// /// Represents a lookup for a game icon. diff --git a/Dalamud/Interface/ISharedImmediateTexture.cs b/Dalamud/Interface/ISharedImmediateTexture.cs new file mode 100644 index 000000000..d2b22b877 --- /dev/null +++ b/Dalamud/Interface/ISharedImmediateTexture.cs @@ -0,0 +1,69 @@ +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Interface.Internal; +using Dalamud.Utility; + +namespace Dalamud.Interface; + +/// A texture with a backing instance of that is shared across multiple +/// requesters. +/// +/// Calling on this interface is a no-op. +/// and may stop returning the intended texture at any point. +/// Use to lock the texture for use in any thread for any duration. +/// +public interface ISharedImmediateTexture +{ + /// Gets the texture for use with the current frame. + /// An instance of that is guaranteed to be available for the current + /// frame being drawn. + /// + /// Calling outside the main thread will fail. + /// This function does not throw. + /// will be ignored. + /// If the texture is unavailable for any reason, then the returned instance of + /// will point to an empty texture instead. + /// + IDalamudTextureWrap GetWrap(); + + /// Gets the texture for use with the current frame. + /// The default wrap to return if the requested texture was not immediately available. + /// + /// An instance of that is guaranteed to be available for the current + /// frame being drawn. + /// + /// Calling outside the main thread will fail. + /// This function does not throw. + /// will be ignored. + /// If the texture is unavailable for any reason, then will be returned. + /// + [return: NotNullIfNotNull(nameof(defaultWrap))] + IDalamudTextureWrap? GetWrap(IDalamudTextureWrap? defaultWrap); + + /// Attempts to get the texture for use with the current frame. + /// An instance of that is guaranteed to be available for + /// the current frame being drawn, or null if texture is not loaded (yet). + /// The load exception, if any. + /// true if points to the loaded texture; false if the texture is + /// still being loaded, or the load has failed. + /// + /// Calling outside the main thread will fail. + /// This function does not throw. + /// on the returned will be ignored. + /// + /// Thrown when called outside the UI thread. + bool TryGetWrap([NotNullWhen(true)] out IDalamudTextureWrap? texture, out Exception? exception); + + /// Creates a new instance of holding a new reference to this texture. + /// The returned texture is guaranteed to be available until is called. + /// The cancellation token. + /// A containing the loaded texture on success. + /// + /// must be called on the resulting instance of + /// from the returned after use. Consider using + /// to dispose the result automatically according to the state + /// of the task. + Task RentAsync(CancellationToken cancellationToken = default); +} diff --git a/Dalamud/Interface/Internal/SharableTextures/FileSystemSharableTexture.cs b/Dalamud/Interface/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs similarity index 72% rename from Dalamud/Interface/Internal/SharableTextures/FileSystemSharableTexture.cs rename to Dalamud/Interface/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs index df4d2ca0a..aac2169b9 100644 --- a/Dalamud/Interface/Internal/SharableTextures/FileSystemSharableTexture.cs +++ b/Dalamud/Interface/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs @@ -4,22 +4,22 @@ using System.Threading.Tasks; using Dalamud.Utility; -namespace Dalamud.Interface.Internal.SharableTextures; +namespace Dalamud.Interface.Internal.SharedImmediateTextures; /// /// Represents a sharable texture, based on a file on the system filesystem. /// -internal sealed class FileSystemSharableTexture : SharableTexture +internal sealed class FileSystemSharedImmediateTexture : SharedImmediateTexture { private readonly string path; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The path. /// If set to true, this class will hold a reference to self. /// Otherwise, it is expected that the caller to hold the reference. - private FileSystemSharableTexture(string path, bool holdSelfReference) + private FileSystemSharedImmediateTexture(string path, bool holdSelfReference) : base(holdSelfReference) { this.path = path; @@ -31,24 +31,24 @@ internal sealed class FileSystemSharableTexture : SharableTexture public override string SourcePathForDebug => this.path; /// - /// Creates a new instance of . + /// Creates a new instance of . /// The new instance will hold a reference to itself. /// /// The path. /// The new instance. - public static SharableTexture CreateImmediate(string path) => new FileSystemSharableTexture(path, true); + public static SharedImmediateTexture CreateImmediate(string path) => new FileSystemSharedImmediateTexture(path, true); /// - /// Creates a new instance of . + /// Creates a new instance of . /// The caller is expected to manage ownership of the new instance. /// /// The path. /// The new instance. - public static SharableTexture CreateAsync(string path) => new FileSystemSharableTexture(path, false); + public static SharedImmediateTexture CreateAsync(string path) => new FileSystemSharedImmediateTexture(path, false); /// public override string ToString() => - $"{nameof(FileSystemSharableTexture)}#{this.InstanceIdForDebug}({this.path})"; + $"{nameof(FileSystemSharedImmediateTexture)}#{this.InstanceIdForDebug}({this.path})"; /// protected override void ReleaseResources() diff --git a/Dalamud/Interface/Internal/SharableTextures/GamePathSharableTexture.cs b/Dalamud/Interface/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs similarity index 73% rename from Dalamud/Interface/Internal/SharableTextures/GamePathSharableTexture.cs rename to Dalamud/Interface/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs index db51159bb..6c709321f 100644 --- a/Dalamud/Interface/Internal/SharableTextures/GamePathSharableTexture.cs +++ b/Dalamud/Interface/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs @@ -7,22 +7,22 @@ using Dalamud.Utility; using Lumina.Data.Files; -namespace Dalamud.Interface.Internal.SharableTextures; +namespace Dalamud.Interface.Internal.SharedImmediateTextures; /// /// Represents a sharable texture, based on a file in game resources. /// -internal sealed class GamePathSharableTexture : SharableTexture +internal sealed class GamePathSharedImmediateTexture : SharedImmediateTexture { private readonly string path; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The path. /// If set to true, this class will hold a reference to self. /// Otherwise, it is expected that the caller to hold the reference. - private GamePathSharableTexture(string path, bool holdSelfReference) + private GamePathSharedImmediateTexture(string path, bool holdSelfReference) : base(holdSelfReference) { this.path = path; @@ -34,23 +34,23 @@ internal sealed class GamePathSharableTexture : SharableTexture public override string SourcePathForDebug => this.path; /// - /// Creates a new instance of . + /// Creates a new instance of . /// The new instance will hold a reference to itself. /// /// The path. /// The new instance. - public static SharableTexture CreateImmediate(string path) => new GamePathSharableTexture(path, true); + public static SharedImmediateTexture CreateImmediate(string path) => new GamePathSharedImmediateTexture(path, true); /// - /// Creates a new instance of . + /// Creates a new instance of . /// The caller is expected to manage ownership of the new instance. /// /// The path. /// The new instance. - public static SharableTexture CreateAsync(string path) => new GamePathSharableTexture(path, false); + public static SharedImmediateTexture CreateAsync(string path) => new GamePathSharedImmediateTexture(path, false); /// - public override string ToString() => $"{nameof(GamePathSharableTexture)}#{this.InstanceIdForDebug}({this.path})"; + public override string ToString() => $"{nameof(GamePathSharedImmediateTexture)}#{this.InstanceIdForDebug}({this.path})"; /// protected override void ReleaseResources() diff --git a/Dalamud/Interface/Internal/SharableTextures/SharableTexture.cs b/Dalamud/Interface/Internal/SharedImmediateTextures/SharedImmediateTexture.cs similarity index 71% rename from Dalamud/Interface/Internal/SharableTextures/SharableTexture.cs rename to Dalamud/Interface/Internal/SharedImmediateTextures/SharedImmediateTexture.cs index cb12c1894..e75f8d038 100644 --- a/Dalamud/Interface/Internal/SharableTextures/SharableTexture.cs +++ b/Dalamud/Interface/Internal/SharedImmediateTextures/SharedImmediateTexture.cs @@ -1,15 +1,17 @@ +using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; using Dalamud.Storage.Assets; using Dalamud.Utility; -namespace Dalamud.Interface.Internal.SharableTextures; +namespace Dalamud.Interface.Internal.SharedImmediateTextures; /// /// Represents a texture that may have multiple reference holders (owners). /// -internal abstract class SharableTexture : IRefCountable, TextureLoadThrottler.IThrottleBasisProvider +internal abstract class SharedImmediateTexture + : ISharedImmediateTexture, IRefCountable, TextureLoadThrottler.IThrottleBasisProvider { private const int SelfReferenceDurationTicks = 2000; private const long SelfReferenceExpiryExpired = long.MaxValue; @@ -23,14 +25,14 @@ internal abstract class SharableTexture : IRefCountable, TextureLoadThrottler.IT private long selfReferenceExpiry; private IDalamudTextureWrap? availableOnAccessWrapForApi9; private CancellationTokenSource? cancellationTokenSource; - private DisposeSuppressingTextureWrap? disposeSuppressingWrap; + private NotOwnedTextureWrap? nonOwningWrap; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// If set to true, this class will hold a reference to self. /// Otherwise, it is expected that the caller to hold the reference. - protected SharableTexture(bool holdSelfReference) + protected SharedImmediateTexture(bool holdSelfReference) { this.InstanceIdForDebug = Interlocked.Increment(ref instanceCounter); @@ -79,7 +81,7 @@ internal abstract class SharableTexture : IRefCountable, TextureLoadThrottler.IT public abstract string SourcePathForDebug { get; } /// - /// Gets a value indicating whether this instance of supports revival. + /// Gets a value indicating whether this instance of supports revival. /// public bool HasRevivalPossibility => this.RevivalPossibility?.TryGetTarget(out _) is true; @@ -99,7 +101,7 @@ internal abstract class SharableTexture : IRefCountable, TextureLoadThrottler.IT /// /// Gets a value indicating whether the content has been queried, - /// i.e. or is called. + /// i.e. or is called. /// public bool ContentQueried { get; private set; } @@ -125,7 +127,8 @@ internal abstract class SharableTexture : IRefCountable, TextureLoadThrottler.IT public int AddRef() => this.TryAddRef(out var newRefCount) switch { IRefCountable.RefCountResult.StillAlive => newRefCount, - IRefCountable.RefCountResult.AlreadyDisposed => throw new ObjectDisposedException(nameof(SharableTexture)), + IRefCountable.RefCountResult.AlreadyDisposed => throw new ObjectDisposedException( + nameof(SharedImmediateTexture)), IRefCountable.RefCountResult.FinalRelease => throw new InvalidOperationException(), _ => throw new InvalidOperationException(), }; @@ -164,7 +167,7 @@ internal abstract class SharableTexture : IRefCountable, TextureLoadThrottler.IT this.cancellationTokenSource?.Cancel(); this.cancellationTokenSource = null; - this.disposeSuppressingWrap = null; + this.nonOwningWrap = null; this.ReleaseResources(); this.resourceReleased = true; @@ -173,7 +176,7 @@ internal abstract class SharableTexture : IRefCountable, TextureLoadThrottler.IT } case IRefCountable.RefCountResult.AlreadyDisposed: - throw new ObjectDisposedException(nameof(SharableTexture)); + throw new ObjectDisposedException(nameof(SharedImmediateTexture)); default: throw new InvalidOperationException(); @@ -206,48 +209,27 @@ internal abstract class SharableTexture : IRefCountable, TextureLoadThrottler.IT } } - /// - /// 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() + /// + public IDalamudTextureWrap GetWrap() => this.GetWrap(Service.Get().Empty4X4); + + /// + [return: NotNullIfNotNull(nameof(defaultWrap))] + public IDalamudTextureWrap? GetWrap(IDalamudTextureWrap? defaultWrap) { - if (this.TryAddRef(out _) != IRefCountable.RefCountResult.StillAlive) - { - this.ContentQueried = true; - return null; - } - - this.ContentQueried = true; - this.LatestRequestedTick = Environment.TickCount64; - 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.UnderlyingWrap?.IsCompletedSuccessfully is true - ? this.disposeSuppressingWrap ??= new(this.UnderlyingWrap.Result) - : null; - } + if (!this.TryGetWrap(out var texture, out _)) + texture = null; + return texture ?? defaultWrap; } - /// - /// Creates a new reference to this texture. The texture is guaranteed to be available until - /// is called. - /// - /// The cancellation token. - /// The task containing the texture. - public async Task CreateNewReference(CancellationToken cancellationToken) + /// + public bool TryGetWrap([NotNullWhen(true)] out IDalamudTextureWrap? texture, out Exception? exception) + { + ThreadSafety.AssertMainThread(); + return this.TryGetWrapCore(out texture, out exception); + } + + /// + public async Task RentAsync(CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); @@ -312,10 +294,8 @@ internal abstract class SharableTexture : IRefCountable, TextureLoadThrottler.IT if (this.RevivalPossibility?.TryGetTarget(out this.availableOnAccessWrapForApi9) is true) return this.availableOnAccessWrapForApi9; - var newRefTask = this.CreateNewReference(default); - // Cancellation is not expected for this API - // ReSharper disable once MethodSupportsCancellation - newRefTask.Wait(); + var newRefTask = this.RentAsync(this.LoadCancellationToken); + newRefTask.Wait(this.LoadCancellationToken); if (!newRefTask.IsCompletedSuccessfully) return null; newRefTask.Result.Dispose(); @@ -328,7 +308,7 @@ internal abstract class SharableTexture : IRefCountable, TextureLoadThrottler.IT } /// - /// Cleans up this instance of . + /// Cleans up this instance of . /// protected abstract void ReleaseResources(); @@ -378,14 +358,95 @@ internal abstract class SharableTexture : IRefCountable, TextureLoadThrottler.IT } } + /// , but without checking for thread. + private bool TryGetWrapCore( + [NotNullWhen(true)] out IDalamudTextureWrap? texture, + out Exception? exception) + { + if (this.TryAddRef(out _) != IRefCountable.RefCountResult.StillAlive) + { + this.ContentQueried = true; + texture = null; + exception = new ObjectDisposedException(this.GetType().Name); + return false; + } + + this.ContentQueried = true; + this.LatestRequestedTick = Environment.TickCount64; + + 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); + + var uw = this.UnderlyingWrap; + if (uw?.IsCompletedSuccessfully is true) + { + texture = this.nonOwningWrap ??= new(uw.Result, this); + exception = null; + return true; + } + + texture = null; + exception = uw?.Exception; + return false; + } + } + + private sealed class NotOwnedTextureWrap : IDalamudTextureWrap + { + private readonly IDalamudTextureWrap innerWrap; + private readonly IRefCountable owner; + + /// Initializes a new instance of the class. + /// The inner wrap. + /// The reference counting owner. + public NotOwnedTextureWrap(IDalamudTextureWrap wrap, IRefCountable owner) + { + this.innerWrap = wrap; + this.owner = owner; + } + + /// + public IntPtr ImGuiHandle => this.innerWrap.ImGuiHandle; + + /// + public int Width => this.innerWrap.Width; + + /// + public int Height => this.innerWrap.Height; + + /// + public IDalamudTextureWrap CreateWrapSharingLowLevelResource() + { + this.owner.AddRef(); + return new RefCountableWrappingTextureWrap(this.innerWrap, this.owner); + } + + /// + public void Dispose() + { + } + + /// + public override string ToString() => $"{nameof(NotOwnedTextureWrap)}({this.owner})"; + } + private sealed class RefCountableWrappingTextureWrap : IDalamudTextureWrap { private IDalamudTextureWrap? innerWrap; private IRefCountable? owner; - /// - /// Initializes a new instance of the class. - /// + /// Initializes a new instance of the class. /// The inner wrap. /// The reference counting owner. public RefCountableWrappingTextureWrap(IDalamudTextureWrap wrap, IRefCountable owner) @@ -408,6 +469,18 @@ internal abstract class SharableTexture : IRefCountable, TextureLoadThrottler.IT private IDalamudTextureWrap InnerWrapNonDisposed => this.innerWrap ?? throw new ObjectDisposedException(nameof(RefCountableWrappingTextureWrap)); + /// + public IDalamudTextureWrap CreateWrapSharingLowLevelResource() + { + var ownerCopy = this.owner; + var wrapCopy = this.innerWrap; + if (ownerCopy is null || wrapCopy is null) + throw new ObjectDisposedException(nameof(RefCountableWrappingTextureWrap)); + + ownerCopy.AddRef(); + return new RefCountableWrappingTextureWrap(wrapCopy, ownerCopy); + } + /// public void Dispose() { @@ -417,6 +490,8 @@ internal abstract class SharableTexture : IRefCountable, TextureLoadThrottler.IT return; if (ownerCopy != Interlocked.CompareExchange(ref this.owner, null, ownerCopy)) continue; + + // Note: do not dispose this; life of the wrap is managed by the owner. this.innerWrap = null; ownerCopy.Release(); GC.SuppressFinalize(this); @@ -427,20 +502,48 @@ internal abstract class SharableTexture : IRefCountable, TextureLoadThrottler.IT public override string ToString() => $"{nameof(RefCountableWrappingTextureWrap)}({this.owner})"; } + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] private sealed class AvailableOnAccessTextureWrap : IDalamudTextureWrap { - private readonly SharableTexture inner; + private readonly SharedImmediateTexture inner; - public AvailableOnAccessTextureWrap(SharableTexture inner) => this.inner = inner; + public AvailableOnAccessTextureWrap(SharedImmediateTexture inner) => this.inner = inner; /// - public IntPtr ImGuiHandle => this.GetActualTexture().ImGuiHandle; + public IntPtr ImGuiHandle => this.WaitGet().ImGuiHandle; /// - public int Width => this.GetActualTexture().Width; + public int Width => this.WaitGet().Width; /// - public int Height => this.GetActualTexture().Height; + public int Height => this.WaitGet().Height; + + /// + public IDalamudTextureWrap CreateWrapSharingLowLevelResource() + { + this.inner.AddRef(); + try + { + if (!this.inner.TryGetWrapCore(out var wrap, out _)) + { + this.inner.UnderlyingWrap?.Wait(); + + if (!this.inner.TryGetWrapCore(out wrap, out _)) + { + // Calling dispose on Empty4x4 is a no-op, so we can just return that. + this.inner.Release(); + return Service.Get().Empty4X4; + } + } + + return new RefCountableWrappingTextureWrap(wrap, this.inner); + } + catch + { + this.inner.Release(); + throw; + } + } /// public void Dispose() @@ -451,13 +554,13 @@ internal abstract class SharableTexture : IRefCountable, TextureLoadThrottler.IT /// public override string ToString() => $"{nameof(AvailableOnAccessTextureWrap)}({this.inner})"; - private IDalamudTextureWrap GetActualTexture() + private IDalamudTextureWrap WaitGet() { - if (this.inner.GetImmediate() is { } t) + if (this.inner.TryGetWrapCore(out var t, out _)) return t; this.inner.UnderlyingWrap?.Wait(); - return this.inner.disposeSuppressingWrap ?? Service.Get().Empty4X4; + return this.inner.nonOwningWrap ?? Service.Get().Empty4X4; } } } diff --git a/Dalamud/Interface/Internal/SharableTextures/TextureLoadThrottler.cs b/Dalamud/Interface/Internal/TextureLoadThrottler.cs similarity index 99% rename from Dalamud/Interface/Internal/SharableTextures/TextureLoadThrottler.cs rename to Dalamud/Interface/Internal/TextureLoadThrottler.cs index 47e3ab90f..cb8274f0a 100644 --- a/Dalamud/Interface/Internal/SharableTextures/TextureLoadThrottler.cs +++ b/Dalamud/Interface/Internal/TextureLoadThrottler.cs @@ -3,7 +3,7 @@ using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; -namespace Dalamud.Interface.Internal.SharableTextures; +namespace Dalamud.Interface.Internal; /// /// Service for managing texture loads. diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index a4edb4449..a906a214b 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -9,7 +9,7 @@ using BitFaster.Caching.Lru; using Dalamud.Data; using Dalamud.Game; -using Dalamud.Interface.Internal.SharableTextures; +using Dalamud.Interface.Internal.SharedImmediateTextures; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; @@ -66,9 +66,9 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid private readonly TextureLoadThrottler textureLoadThrottler = Service.Get(); private readonly ConcurrentLru lookupToPath = new(PathLookupLruCount); - private readonly ConcurrentDictionary gamePathTextures = new(); - private readonly ConcurrentDictionary fileSystemTextures = new(); - private readonly HashSet invalidatedTextures = new(); + private readonly ConcurrentDictionary gamePathTextures = new(); + private readonly ConcurrentDictionary fileSystemTextures = new(); + private readonly HashSet invalidatedTextures = new(); private bool disposing; @@ -84,12 +84,12 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid /// /// Gets all the loaded textures from the game resources. Debug use only. /// - public ICollection GamePathTextures => this.gamePathTextures.Values; + public ICollection GamePathTextures => this.gamePathTextures.Values; /// /// Gets all the loaded textures from the game resources. Debug use only. /// - public ICollection FileSystemTextures => this.fileSystemTextures.Values; + public ICollection FileSystemTextures => this.fileSystemTextures.Values; /// /// Gets all the loaded textures that are invalidated from . Debug use only. @@ -99,7 +99,7 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid "ReSharper", "InconsistentlySynchronizedField", Justification = "Debug use only; users are expected to lock around this")] - public ICollection InvalidatedTextures => this.invalidatedTextures; + public ICollection InvalidatedTextures => this.invalidatedTextures; /// public void Dispose() @@ -118,14 +118,12 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid this.fileSystemTextures.Clear(); } +#region API9 compat #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) + string? ITextureProvider.GetIconPath(uint iconId, ITextureProvider.IconFlags flags, ClientLanguage? language) => this.TryGetIconPath( new( iconId, @@ -139,109 +137,56 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid /// [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] [Obsolete("See interface definition.")] - public IDalamudTextureWrap? GetIcon( + IDalamudTextureWrap? ITextureProvider.GetIcon( uint iconId, - ITextureProvider.IconFlags flags = ITextureProvider.IconFlags.HiRes, - ClientLanguage? language = null, - bool keepAlive = false) => - this.GetTextureFromGame( - this.lookupToPath.GetOrAdd( + ITextureProvider.IconFlags flags, + ClientLanguage? language, + bool keepAlive) => + this.GetFromGameIcon( 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, GamePathSharableTexture.CreateImmediate) + language)) .GetAvailableOnAccessWrapForApi9(); /// [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] [Obsolete("See interface definition.")] - public IDalamudTextureWrap? GetTextureFromFile(FileInfo file, bool keepAlive = false) => - this.fileSystemTextures.GetOrAdd(file.FullName, FileSystemSharableTexture.CreateImmediate) - .GetAvailableOnAccessWrapForApi9(); + IDalamudTextureWrap? ITextureProvider.GetTextureFromGame(string path, bool keepAlive) => + this.GetFromGame(path).GetAvailableOnAccessWrapForApi9(); + + /// + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + [Obsolete("See interface definition.")] + IDalamudTextureWrap? ITextureProvider.GetTextureFromFile(FileInfo file, bool keepAlive) => + this.GetFromFile(file.FullName).GetAvailableOnAccessWrapForApi9(); #pragma warning restore CS0618 // Type or member is obsolete +#endregion + + /// + public SharedImmediateTexture GetFromGameIcon(in GameIconLookup lookup) => + this.GetFromGame(this.lookupToPath.GetOrAdd(lookup, this.GetIconPathByValue)); + + /// + public SharedImmediateTexture GetFromGame(string path) => + this.gamePathTextures.GetOrAdd(path, GamePathSharedImmediateTexture.CreateImmediate); + + /// + public SharedImmediateTexture GetFromFile(string path) => + this.fileSystemTextures.GetOrAdd(path, FileSystemSharedImmediateTexture.CreateImmediate); /// - public IDalamudTextureWrap ImmediateGetFromGameIcon(in GameIconLookup lookup) => - this.ImmediateGetFromGame(this.lookupToPath.GetOrAdd(lookup, this.GetIconPathByValue)); + ISharedImmediateTexture ITextureProvider.GetFromGameIcon(in GameIconLookup lookup) => this.GetFromGameIcon(lookup); /// - public IDalamudTextureWrap ImmediateGetFromGame(string path) => - this.ImmediateTryGetFromGame(path, out var texture, out _) - ? texture - : this.dalamudAssetManager.Empty4X4; + ISharedImmediateTexture ITextureProvider.GetFromGame(string path) => this.GetFromGame(path); /// - public IDalamudTextureWrap ImmediateGetFromFile(string file) => - this.ImmediateTryGetFromFile(file, out var texture, out _) - ? texture - : this.dalamudAssetManager.Empty4X4; + ISharedImmediateTexture ITextureProvider.GetFromFile(string path) => this.GetFromFile(path); /// - public bool ImmediateTryGetFromGameIcon( - in GameIconLookup lookup, - [NotNullWhen(true)] out IDalamudTextureWrap? texture, - out Exception? exception) => - this.ImmediateTryGetFromGame( - this.lookupToPath.GetOrAdd(lookup, this.GetIconPathByValue), - out texture, - out exception); - - /// - public bool ImmediateTryGetFromGame( - string path, - [NotNullWhen(true)] out IDalamudTextureWrap? texture, - out Exception? exception) - { - ThreadSafety.AssertMainThread(); - var t = this.gamePathTextures.GetOrAdd(path, GamePathSharableTexture.CreateImmediate); - texture = t.GetImmediate(); - exception = t.UnderlyingWrap?.Exception; - return texture is not null; - } - - /// - public bool ImmediateTryGetFromFile( - string file, - [NotNullWhen(true)] out IDalamudTextureWrap? texture, - out Exception? exception) - { - ThreadSafety.AssertMainThread(); - var t = this.fileSystemTextures.GetOrAdd(file, FileSystemSharableTexture.CreateImmediate); - texture = t.GetImmediate(); - exception = t.UnderlyingWrap?.Exception; - return texture is not null; - } - - /// - public Task GetFromGameIconAsync( - in GameIconLookup lookup, - CancellationToken cancellationToken = default) => - this.GetFromGameAsync(this.lookupToPath.GetOrAdd(lookup, this.GetIconPathByValue), cancellationToken); - - /// - public Task GetFromGameAsync( - string path, - CancellationToken cancellationToken = default) => - this.gamePathTextures.GetOrAdd(path, GamePathSharableTexture.CreateAsync) - .CreateNewReference(cancellationToken); - - /// - public Task GetFromFileAsync( - string file, - CancellationToken cancellationToken = default) => - this.fileSystemTextures.GetOrAdd(file, FileSystemSharableTexture.CreateAsync) - .CreateNewReference(cancellationToken); - - /// - public Task GetFromImageAsync( + public Task CreateFromImageAsync( ReadOnlyMemory bytes, CancellationToken cancellationToken = default) => this.textureLoadThrottler.CreateLoader( @@ -250,7 +195,7 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid cancellationToken); /// - public Task GetFromImageAsync( + public Task CreateFromImageAsync( Stream stream, bool leaveOpen = false, CancellationToken cancellationToken = default) => @@ -260,7 +205,7 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid { await using var ms = stream.CanSeek ? new MemoryStream((int)stream.Length) : new(); await stream.CopyToAsync(ms, ct).ConfigureAwait(false); - return await this.GetFromImageAsync(ms.GetBuffer(), ct); + return await this.CreateFromImageAsync(ms.GetBuffer(), ct); }, cancellationToken) .ContinueWith( @@ -274,7 +219,7 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid .Unwrap(); /// - public IDalamudTextureWrap GetFromRaw( + public IDalamudTextureWrap CreateFromRaw( RawImageSpecification specs, ReadOnlySpan bytes) { @@ -304,31 +249,34 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid }; using var texture = new Texture2D(scene.Device, texDesc, new DataRectangle(new(pData), specs.Pitch)); - resView = new(scene.Device, texture, new() - { - Format = texDesc.Format, - Dimension = ShaderResourceViewDimension.Texture2D, - Texture2D = { MipLevels = texDesc.MipLevels }, - }); + resView = new( + scene.Device, + texture, + new() + { + Format = texDesc.Format, + Dimension = ShaderResourceViewDimension.Texture2D, + Texture2D = { MipLevels = texDesc.MipLevels }, + }); } } - + // no sampler for now because the ImGui implementation we copied doesn't allow for changing it return new DalamudTextureWrap(new D3DTextureWrap(resView, specs.Width, specs.Height)); } /// - public Task GetFromRawAsync( + public Task CreateFromRawAsync( RawImageSpecification specs, ReadOnlyMemory bytes, CancellationToken cancellationToken = default) => this.textureLoadThrottler.CreateLoader( new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), - _ => Task.FromResult(this.GetFromRaw(specs, bytes.Span)), + _ => Task.FromResult(this.CreateFromRaw(specs, bytes.Span)), cancellationToken); /// - public Task GetFromRawAsync( + public Task CreateFromRawAsync( RawImageSpecification specs, Stream stream, bool leaveOpen = false, @@ -339,7 +287,7 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid { await using var ms = stream.CanSeek ? new MemoryStream((int)stream.Length) : new(); await stream.CopyToAsync(ms, ct).ConfigureAwait(false); - return await this.GetFromRawAsync(specs, ms.GetBuffer(), ct); + return await this.CreateFromRawAsync(specs, ms.GetBuffer(), ct); }, cancellationToken) .ContinueWith( @@ -353,17 +301,17 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid .Unwrap(); /// - public IDalamudTextureWrap GetTexture(TexFile file) => this.GetFromTexFileAsync(file).Result; + public IDalamudTextureWrap CreateFromTexFile(TexFile file) => this.CreateFromTexFileAsync(file).Result; /// - public Task GetFromTexFileAsync( + public Task CreateFromTexFileAsync( TexFile file, CancellationToken cancellationToken = default) => this.textureLoadThrottler.CreateLoader( new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), ct => Task.Run(() => this.NoThrottleGetFromTexFile(file), ct), cancellationToken); - + /// public bool SupportsDxgiFormat(int dxgiFormat) { @@ -489,7 +437,7 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid /// /// Gets a texture from the given image. Skips the load throttler; intended to be used from implementation of - /// s. + /// s. /// /// The data. /// The loaded texture. @@ -508,7 +456,7 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid /// /// Gets a texture from the given . Skips the load throttler; intended to be used from - /// implementation of s. + /// implementation of s. /// /// The data. /// The loaded texture. @@ -522,7 +470,7 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid buffer = buffer.Filter(0, 0, TexFile.TextureFormat.B8G8R8A8); } - return this.GetFromRaw( + return this.CreateFromRaw( RawImageSpecification.From(buffer.Width, buffer.Height, dxgiFormat), buffer.RawData); } @@ -567,7 +515,7 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid return; - static bool TextureFinalReleasePredicate(SharableTexture v) => + static bool TextureFinalReleasePredicate(SharedImmediateTexture v) => v.ContentQueried && v.ReleaseSelfReference(false) == 0 && !v.HasRevivalPossibility; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs index 4a5bd89cf..1f4e5f29d 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs @@ -141,7 +141,7 @@ public class IconBrowserWidget : IDataWindowWidget var texm = Service.Get(); var cursor = ImGui.GetCursorScreenPos(); - if (texm.ImmediateTryGetFromGameIcon(new((uint)iconId), out var texture, out var exc)) + if (texm.GetFromGameIcon(new((uint)iconId)).TryGetWrap(out var texture, out var exc)) { ImGui.Image(texture.ImGuiHandle, this.iconSize); diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs index f97fd040f..2a4222c5b 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; using Dalamud.Interface.Components; using Dalamud.Interface.Internal.Notifications; -using Dalamud.Interface.Internal.SharableTextures; +using Dalamud.Interface.Internal.SharedImmediateTextures; using Dalamud.Interface.Utility; using Dalamud.Plugin.Services; using Dalamud.Storage.Assets; @@ -42,6 +42,8 @@ internal class TexWidget : IDataWindowWidget /// public bool Ready { get; set; } + private ITextureProvider TextureManagerForApi9 => this.textureManager!; + /// public void Load() { @@ -145,7 +147,7 @@ internal class TexWidget : IDataWindowWidget } } - private unsafe void DrawLoadedTextures(ICollection textures) + private unsafe void DrawLoadedTextures(ICollection textures) { var im = Service.Get(); if (!ImGui.BeginTable("##table", 6)) @@ -226,7 +228,7 @@ internal class TexWidget : IDataWindowWidget ImGui.TableNextColumn(); ImGuiComponents.IconButton(FontAwesomeIcon.Image); - if (ImGui.IsItemHovered() && texture.GetImmediate() is { } immediate) + if (ImGui.IsItemHovered() && texture.GetWrap(null) is { } immediate) { ImGui.BeginTooltip(); ImGui.Image(immediate.ImGuiHandle, immediate.Size); @@ -274,7 +276,7 @@ internal class TexWidget : IDataWindowWidget flags |= ITextureProvider.IconFlags.ItemHighQuality; if (this.hiRes) flags |= ITextureProvider.IconFlags.HiRes; - this.addedTextures.Add(new(Api9: this.textureManager.GetIcon(uint.Parse(this.iconId), flags))); + this.addedTextures.Add(new(Api9: this.TextureManagerForApi9.GetIcon(uint.Parse(this.iconId), flags))); } #pragma warning restore CS0618 // Type or member is obsolete @@ -283,8 +285,9 @@ internal class TexWidget : IDataWindowWidget { this.addedTextures.Add( new( - Api10: this.textureManager.GetFromGameIconAsync( - new(uint.Parse(this.iconId), this.hq, this.hiRes)))); + Api10: this.textureManager + .GetFromGameIcon(new(uint.Parse(this.iconId), this.hq, this.hiRes)) + .RentAsync())); } ImGui.SameLine(); @@ -300,12 +303,12 @@ internal class TexWidget : IDataWindowWidget #pragma warning disable CS0618 // Type or member is obsolete if (ImGui.Button("Load Tex (API9)")) - this.addedTextures.Add(new(Api9: this.textureManager.GetTextureFromGame(this.inputTexPath))); + this.addedTextures.Add(new(Api9: this.TextureManagerForApi9.GetTextureFromGame(this.inputTexPath))); #pragma warning restore CS0618 // Type or member is obsolete ImGui.SameLine(); if (ImGui.Button("Load Tex (Async)")) - this.addedTextures.Add(new(Api10: this.textureManager.GetFromGameAsync(this.inputTexPath))); + this.addedTextures.Add(new(Api10: this.textureManager.GetFromGame(this.inputTexPath).RentAsync())); ImGui.SameLine(); if (ImGui.Button("Load Tex (Immediate)")) @@ -320,12 +323,12 @@ internal class TexWidget : IDataWindowWidget #pragma warning disable CS0618 // Type or member is obsolete if (ImGui.Button("Load File (API9)")) - this.addedTextures.Add(new(Api9: this.textureManager.GetTextureFromFile(new(this.inputFilePath)))); + this.addedTextures.Add(new(Api9: this.TextureManagerForApi9.GetTextureFromFile(new(this.inputFilePath)))); #pragma warning restore CS0618 // Type or member is obsolete ImGui.SameLine(); if (ImGui.Button("Load File (Async)")) - this.addedTextures.Add(new(Api10: this.textureManager.GetFromFileAsync(this.inputFilePath))); + this.addedTextures.Add(new(Api10: this.textureManager.GetFromFile(this.inputFilePath).RentAsync())); ImGui.SameLine(); if (ImGui.Button("Load File (Immediate)")) @@ -430,11 +433,11 @@ internal class TexWidget : IDataWindowWidget if (this.Api10 is not null) return this.Api10.IsCompletedSuccessfully ? this.Api10.Result : null; if (this.Api10ImmGameIcon is not null) - return tp.ImmediateGetFromGameIcon(this.Api10ImmGameIcon.Value); + return tp.GetFromGameIcon(this.Api10ImmGameIcon.Value).GetWrap(); if (this.Api10ImmGamePath is not null) - return tp.ImmediateGetFromGame(this.Api10ImmGamePath); + return tp.GetFromGame(this.Api10ImmGamePath).GetWrap(); if (this.Api10ImmFile is not null) - return tp.ImmediateGetFromFile(this.Api10ImmFile); + return tp.GetFromFile(this.Api10ImmFile).GetWrap(); return null; } diff --git a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs index 6ae45c962..50450aaae 100644 --- a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs +++ b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs @@ -275,7 +275,7 @@ internal class PluginImageCache : IDisposable, IServiceType // FIXME(goat): This is a hack around this call failing randomly in certain situations. Might be related to not being called on the main thread. try { - image = await textureManager.GetFromImageAsync(bytes); + image = await textureManager.CreateFromImageAsync(bytes); } catch (Exception ex) { diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 29e76434b..0e30658ef 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -1745,7 +1745,7 @@ internal class PluginInstallerWindow : Window, IDisposable if (!this.testerIconPath.IsNullOrEmpty()) { - this.testerIcon = tm.GetFromFileAsync(this.testerIconPath); + this.testerIcon = tm.GetFromFile(this.testerIconPath).RentAsync(); } this.testerImages = new Task?[this.testerImagePaths.Length]; @@ -1756,7 +1756,7 @@ internal class PluginInstallerWindow : Window, IDisposable continue; _ = this.testerImages[i]?.ToContentDisposedTask(); - this.testerImages[i] = tm.GetFromFileAsync(this.testerImagePaths[i]); + this.testerImages[i] = tm.GetFromFile(this.testerImagePaths[i]).RentAsync(); } } catch (Exception ex) diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs index 0148e80dd..0e344f450 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs @@ -592,7 +592,7 @@ internal sealed partial class FontAtlasFactory } else if (texture.TexPixelsRGBA32 is not null) { - var wrap = this.factory.TextureManager.GetFromRaw( + var wrap = this.factory.TextureManager.CreateFromRaw( RawImageSpecification.Rgba32(width, height), new(texture.TexPixelsRGBA32, width * height * 4)); this.data.AddExistingTexture(wrap); @@ -632,7 +632,7 @@ internal sealed partial class FontAtlasFactory } } - var wrap = this.factory.TextureManager.GetFromRaw( + var wrap = this.factory.TextureManager.CreateFromRaw( new( width, height, diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs index ffddcc272..b3edcc9b2 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs @@ -376,7 +376,7 @@ internal sealed partial class FontAtlasFactory } return this.scopedFinalizer.Add( - this.TextureManager.GetFromRaw( + this.TextureManager.CreateFromRaw( new( texFile.Header.Width, texFile.Header.Height, diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 74a718507..3a718ef4c 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -387,8 +387,9 @@ 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.TextureProvider.GetFromFileAsync(filePath).Result; + [Obsolete($"Use {nameof(ITextureProvider.GetFromFile)}.")] + public IDalamudTextureWrap LoadImage(string filePath) => + this.TextureProvider.GetFromFile(filePath).RentAsync().Result; /// /// Loads an image from a byte stream, such as a png downloaded into memory. @@ -396,8 +397,9 @@ 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.TextureProvider.GetFromImageAsync(imageData).Result; + [Obsolete($"Use {nameof(ITextureProvider.CreateFromImageAsync)}.")] + public IDalamudTextureWrap LoadImage(byte[] imageData) => + this.TextureProvider.CreateFromImageAsync(imageData).Result; /// /// Loads an image from raw unformatted pixel data, with no type or header information. To load formatted data, use . @@ -408,11 +410,11 @@ public sealed class UiBuilder : IDisposable /// 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)}.")] + [Obsolete($"Use {nameof(ITextureProvider.CreateFromRaw)} or {nameof(ITextureProvider.CreateFromRawAsync)}.")] public IDalamudTextureWrap LoadImageRaw(byte[] imageData, int width, int height, int numChannels) => numChannels switch { - 4 => this.TextureProvider.GetFromRaw(RawImageSpecification.Rgba32(width, height), imageData), + 4 => this.TextureProvider.CreateFromRaw(RawImageSpecification.Rgba32(width, height), imageData), _ => throw new NotSupportedException(), }; @@ -430,8 +432,9 @@ 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) => this.TextureProvider.GetFromFileAsync(filePath); + [Obsolete($"Use {nameof(ITextureProvider.GetFromFile)}.")] + public Task LoadImageAsync(string filePath) => + this.TextureProvider.GetFromFile(filePath).RentAsync(); /// /// Asynchronously loads an image from a byte stream, such as a png downloaded into memory, when it's possible to do so. @@ -439,9 +442,9 @@ 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)}.")] + [Obsolete($"Use {nameof(ITextureProvider.CreateFromImageAsync)}.")] public Task LoadImageAsync(byte[] imageData) => - this.TextureProvider.GetFromImageAsync(imageData); + this.TextureProvider.CreateFromImageAsync(imageData); /// /// Asynchronously loads an image from raw unformatted pixel data, with no type or header information, when it's possible to do so. To load formatted data, use . @@ -452,11 +455,11 @@ public sealed class UiBuilder : IDisposable /// 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)}.")] + [Obsolete($"Use {nameof(ITextureProvider.CreateFromRawAsync)}.")] public Task LoadImageRawAsync(byte[] imageData, int width, int height, int numChannels) => numChannels switch { - 4 => this.TextureProvider.GetFromRawAsync(RawImageSpecification.Rgba32(width, height), imageData), + 4 => this.TextureProvider.CreateFromRawAsync(RawImageSpecification.Rgba32(width, height), imageData), _ => Task.FromException(new NotSupportedException()), }; diff --git a/Dalamud/Interface/UldWrapper.cs b/Dalamud/Interface/UldWrapper.cs index 4b0c9cf4f..289db6faf 100644 --- a/Dalamud/Interface/UldWrapper.cs +++ b/Dalamud/Interface/UldWrapper.cs @@ -125,7 +125,7 @@ public class UldWrapper : IDisposable inputSlice.CopyTo(outputSlice); } - return this.textureManager.GetFromRaw(RawImageSpecification.Rgba32(part.W, part.H), imageData); + return this.textureManager.CreateFromRaw(RawImageSpecification.Rgba32(part.W, part.H), imageData); } private (uint Id, int Width, int Height, bool HD, byte[] RgbaData)? GetTexture(string texturePath) diff --git a/Dalamud/Plugin/Services/ITextureProvider.Api9.cs b/Dalamud/Plugin/Services/ITextureProvider.Api9.cs index db033778e..2a1a3a9a5 100644 --- a/Dalamud/Plugin/Services/ITextureProvider.Api9.cs +++ b/Dalamud/Plugin/Services/ITextureProvider.Api9.cs @@ -52,7 +52,7 @@ public partial interface ITextureProvider /// 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)}.")] + [Obsolete($"Use {nameof(GetFromGameIcon)}.")] [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] public IDalamudTextureWrap? GetIcon(uint iconId, IconFlags flags = IconFlags.HiRes, ClientLanguage? language = null, bool keepAlive = false); @@ -80,7 +80,7 @@ public partial interface ITextureProvider /// 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)}.")] + [Obsolete($"Use {nameof(GetFromGame)}.")] [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] public IDalamudTextureWrap? GetTextureFromGame(string path, bool keepAlive = false); @@ -93,7 +93,7 @@ public partial interface ITextureProvider /// 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)}.")] + [Obsolete($"Use {nameof(GetFromFile)}.")] [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 b6c7e2cbd..c63e7ae4f 100644 --- a/Dalamud/Plugin/Services/ITextureProvider.cs +++ b/Dalamud/Plugin/Services/ITextureProvider.cs @@ -1,138 +1,52 @@ -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Threading; using System.Threading.Tasks; +using Dalamud.Interface; using Dalamud.Interface.Internal; using Lumina.Data.Files; namespace Dalamud.Plugin.Services; -/// -/// Service that grants you access to textures you may render via ImGui. -/// +/// Service that grants you access to textures you may render via ImGui. /// -/// Immediate functions
-/// Immediate functions do not throw, unless they are called outside the UI thread.
-/// Instances of returned from Immediate functions do not have to be disposed. -/// Calling on them is a no-op; it is safe to call -/// on them, as it will do nothing.
-/// Use and alike if you don't care about the load state and dimensions of the -/// texture to be loaded. These functions will return a valid texture that is empty (fully transparent).
-/// Use and alike if you do. These functions will return the load state, -/// loaded texture if available, and the load exception on failure.
-///
-/// All other functions
-/// Instances of or <> -/// returned from all other functions must be d after use. +/// +/// Get functions will return a shared texture, and the returnd instance of +/// do not require calling , unless a new reference has been created by calling +/// .
+/// Use and alike to obtain a reference of +/// that will stay valid for the rest of the frame. +///
+/// +/// Create functions will return a new texture, and the returned instance of +/// must be disposed after use. +/// ///
public partial interface ITextureProvider { - /// 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. - IDalamudTextureWrap ImmediateGetFromGameIcon(in GameIconLookup lookup); + /// Gets a shared texture corresponding to the given game resource icon specifier. + /// A game icon specifier. + /// The shared texture that you may use to obtain the loaded texture wrap and load states. + ISharedImmediateTexture GetFromGameIcon(in GameIconLookup lookup); - /// Gets a texture from a file shipped as a part of the game resources for use with the current frame. - /// - /// 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. - IDalamudTextureWrap ImmediateGetFromGame(string path); + /// Gets a shared texture corresponding to the given path to a game resource. + /// A path to a game resource. + /// The shared texture that you may use to obtain the loaded texture wrap and load states. + ISharedImmediateTexture GetFromGame(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. - IDalamudTextureWrap ImmediateGetFromFile(string file); - - /// 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, or null if texture is not loaded (yet). - /// The load exception, if any. - /// true if points to the loaded texture; false if the texture is - /// still being loaded, or the load has failed. - /// on the returned will be ignored. - /// Thrown when called outside the UI thread. - bool ImmediateTryGetFromGameIcon( - in GameIconLookup lookup, - [NotNullWhen(true)] out IDalamudTextureWrap? texture, - out Exception? exception); - - /// Gets a texture from a file shipped as a part of the game resources for use with the current frame. - /// - /// 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, or null if texture is not loaded (yet). - /// The load exception, if any. - /// true if points to the loaded texture; false if the texture is - /// still being loaded, or the load has failed. - /// on the returned will be ignored. - /// Thrown when called outside the UI thread. - bool ImmediateTryGetFromGame( - string path, - [NotNullWhen(true)] out IDalamudTextureWrap? texture, - out Exception? exception); - - /// 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, or null if texture is not loaded (yet). - /// The load exception, if any. - /// true if points to the loaded texture; false if the texture is - /// still being loaded, or the load has failed. - /// on the returned will be ignored. - /// Thrown when called outside the UI thread. - bool ImmediateTryGetFromFile( - string file, - [NotNullWhen(true)] out IDalamudTextureWrap? texture, - out Exception? exception); - - /// Gets the corresponding game icon for use with the current frame. - /// The icon specifier. - /// The cancellation token. - /// A containing the loaded texture on success. Dispose after use. - Task GetFromGameIconAsync( - in GameIconLookup lookup, - CancellationToken cancellationToken = default); - - /// Gets a texture from a file shipped as a part of the game resources. - /// The game-internal path to a .tex, .atex, or an image file such as .png. - /// The cancellation token. - /// A containing the loaded texture on success. Dispose after use. - Task GetFromGameAsync( - string path, - CancellationToken cancellationToken = default); - - /// Gets a texture from a file on the filesystem. - /// The filesystem path to a .tex, .atex, or an image file such as .png. - /// The cancellation token. - /// A containing the loaded texture on success. Dispose after use. - Task GetFromFileAsync( - string file, - CancellationToken cancellationToken = default); + /// Gets a shared texture corresponding to the given file on the filesystem. + /// A path to a file on the filesystem. + /// The shared texture that you may use to obtain the loaded texture wrap and load states. + ISharedImmediateTexture GetFromFile(string path); /// Gets a texture from the given bytes, trying to interpret it as a .tex file or other well-known image /// files, such as .png. /// The bytes to load. /// The cancellation token. /// A containing the loaded texture on success. Dispose after use. - Task GetFromImageAsync( + Task CreateFromImageAsync( ReadOnlyMemory bytes, CancellationToken cancellationToken = default); @@ -144,7 +58,7 @@ public partial interface ITextureProvider /// A containing the loaded texture on success. Dispose after use. /// will be closed or not only according to ; /// is irrelevant in closing the stream. - Task GetFromImageAsync( + Task CreateFromImageAsync( Stream stream, bool leaveOpen = false, CancellationToken cancellationToken = default); @@ -153,7 +67,7 @@ public partial interface ITextureProvider /// The specifications for the raw bitmap. /// The bytes to load. /// The texture loaded from the supplied raw bitmap. Dispose after use. - IDalamudTextureWrap GetFromRaw( + IDalamudTextureWrap CreateFromRaw( RawImageSpecification specs, ReadOnlySpan bytes); @@ -162,7 +76,7 @@ public partial interface ITextureProvider /// The bytes to load. /// The cancellation token. /// A containing the loaded texture on success. Dispose after use. - Task GetFromRawAsync( + Task CreateFromRawAsync( RawImageSpecification specs, ReadOnlyMemory bytes, CancellationToken cancellationToken = default); @@ -175,7 +89,7 @@ public partial interface ITextureProvider /// A containing the loaded texture on success. Dispose after use. /// will be closed or not only according to ; /// is irrelevant in closing the stream. - Task GetFromRawAsync( + Task CreateFromRawAsync( RawImageSpecification specs, Stream stream, bool leaveOpen = false, @@ -196,22 +110,31 @@ public partial interface ITextureProvider /// The resolved path. /// true if the corresponding file exists and has been set. bool TryGetIconPath(in GameIconLookup lookup, [NotNullWhen(true)] out string? path); - + /// /// Get a texture handle for the specified Lumina . - /// Alias for fetching from . + /// Alias for fetching from . /// /// The texture to obtain a handle to. /// A texture wrap that can be used to render the texture. Dispose after use. - IDalamudTextureWrap GetTexture(TexFile file); - + /// Alias for . + IDalamudTextureWrap GetTexture(TexFile file) => this.CreateFromTexFile(file); + + /// + /// Get a texture handle for the specified Lumina . + /// Alias for fetching from . + /// + /// The texture to obtain a handle to. + /// A texture wrap that can be used to render the texture. Dispose after use. + IDalamudTextureWrap CreateFromTexFile(TexFile file); + /// /// Get a texture handle for the specified Lumina . /// /// The texture to obtain a handle to. /// The cancellation token. /// A texture wrap that can be used to render the texture. Dispose after use. - Task GetFromTexFileAsync( + Task CreateFromTexFileAsync( TexFile file, CancellationToken cancellationToken = default); diff --git a/Dalamud/Storage/Assets/DalamudAssetManager.cs b/Dalamud/Storage/Assets/DalamudAssetManager.cs index 83f03e274..9db6d55a4 100644 --- a/Dalamud/Storage/Assets/DalamudAssetManager.cs +++ b/Dalamud/Storage/Assets/DalamudAssetManager.cs @@ -309,10 +309,10 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA stream.ReadExactly(buf, 0, length); var image = purpose switch { - DalamudAssetPurpose.TextureFromPng => await tm.GetFromImageAsync(buf), + DalamudAssetPurpose.TextureFromPng => await tm.CreateFromImageAsync(buf), DalamudAssetPurpose.TextureFromRaw => asset.GetAttribute() is { } raw - ? await tm.GetFromRawAsync(raw.Specification, buf) + ? await tm.CreateFromRawAsync(raw.Specification, buf) : throw new InvalidOperationException( "TextureFromRaw must accompany a DalamudAssetRawTextureAttribute."), _ => null, From 55b5c5094ac4030a6384e08bcd938c920f465ea1 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 28 Feb 2024 19:59:36 +0900 Subject: [PATCH 18/57] TextureLoadThrottler: make CreateLoader never block --- .../Internal/TextureLoadThrottler.cs | 167 ++++++++++++------ 1 file changed, 110 insertions(+), 57 deletions(-) diff --git a/Dalamud/Interface/Internal/TextureLoadThrottler.cs b/Dalamud/Interface/Internal/TextureLoadThrottler.cs index cb8274f0a..2b5bbb797 100644 --- a/Dalamud/Interface/Internal/TextureLoadThrottler.cs +++ b/Dalamud/Interface/Internal/TextureLoadThrottler.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Threading; +using System.Threading.Channels; using System.Threading.Tasks; namespace Dalamud.Interface.Internal; @@ -9,15 +10,25 @@ namespace Dalamud.Interface.Internal; /// Service for managing texture loads. ///
[ServiceManager.EarlyLoadedService] -internal class TextureLoadThrottler : IServiceType +internal class TextureLoadThrottler : IServiceType, IDisposable { + private readonly CancellationTokenSource disposeCancellationTokenSource = new(); + private readonly Task adderTask; + private readonly Task[] workerTasks; + private readonly object workListLock = new(); - private readonly List pendingWorkList = new(); - private readonly List activeWorkList = new(); + private readonly Channel newItemChannel = Channel.CreateUnbounded(); + private readonly Channel workTokenChannel = Channel.CreateUnbounded(); + private readonly List workItemPending = new(); [ServiceManager.ServiceConstructor] - private TextureLoadThrottler() => - this.MaxActiveWorkItems = Math.Min(64, Environment.ProcessorCount); + private TextureLoadThrottler() + { + this.adderTask = Task.Run(this.LoopAddWorkItemAsync); + this.workerTasks = new Task[Math.Min(64, Environment.ProcessorCount)]; + foreach (ref var task in this.workerTasks.AsSpan()) + task = Task.Run(this.LoopProcessWorkItemAsync); + } /// /// Basis for throttling. @@ -40,7 +51,20 @@ internal class TextureLoadThrottler : IServiceType long LatestRequestedTick { get; } } - private int MaxActiveWorkItems { get; } + /// + public void Dispose() + { + this.newItemChannel.Writer.Complete(); + this.workTokenChannel.Writer.Complete(); + this.disposeCancellationTokenSource.Cancel(); + + this.adderTask.Wait(); + Task.WaitAll(this.workerTasks); + + _ = this.adderTask.Exception; + foreach (var t in this.workerTasks) + _ = t.Exception; + } /// /// Creates a texture loader. @@ -62,84 +86,113 @@ internal class TextureLoadThrottler : IServiceType ImmediateLoadFunction = immediateLoadFunction, }; - _ = Task.Run(() => this.ContinueWork(work), default); - - return work.TaskCompletionSource.Task; + return + this.newItemChannel.Writer.TryWrite(work) + ? work.TaskCompletionSource.Task + : Task.FromException(new ObjectDisposedException(nameof(TextureLoadThrottler))); } - private async Task ContinueWork(WorkItem? newItem) + private async Task LoopAddWorkItemAsync() { - while (true) + var newWorkTemp = new List(); + var reader = this.newItemChannel.Reader; + while (!reader.Completion.IsCompleted) { - WorkItem? minWork = null; + await reader.WaitToReadAsync(); + + newWorkTemp.EnsureCapacity(reader.Count); + while (newWorkTemp.Count < newWorkTemp.Capacity && reader.TryRead(out var newWork)) + newWorkTemp.Add(newWork); lock (this.workListLock) - { - if (newItem is not null) - { - this.pendingWorkList.Add(newItem); - newItem = null; - } + this.workItemPending.AddRange(newWorkTemp); + for (var i = newWorkTemp.Count; i > 0; i--) + this.workTokenChannel.Writer.TryWrite(null); + newWorkTemp.Clear(); + } + } - if (this.activeWorkList.Count >= this.MaxActiveWorkItems) - return; + private async Task LoopProcessWorkItemAsync() + { + var reader = this.workTokenChannel.Reader; + while (!reader.Completion.IsCompleted) + { + _ = await reader.ReadAsync(); - var minIndex = -1; - for (var i = 0; i < this.pendingWorkList.Count; i++) - { - var work = this.pendingWorkList[i]; - if (work.CancellationToken.IsCancellationRequested) - { - work.TaskCompletionSource.SetCanceled(work.CancellationToken); - _ = work.TaskCompletionSource.Task.Exception; - this.RelocatePendingWorkItemToEndAndEraseUnsafe(i--); - continue; - } - - if (minIndex == -1 || work.CompareTo(this.pendingWorkList[minIndex]) < 0) - { - minIndex = i; - minWork = work; - } - } - - if (minWork is null) - return; - - this.RelocatePendingWorkItemToEndAndEraseUnsafe(minIndex); - - this.activeWorkList.Add(minWork); - } + if (this.ExtractHighestPriorityWorkItem() is not { } work) + continue; try { - var r = await minWork.ImmediateLoadFunction(minWork.CancellationToken); - minWork.TaskCompletionSource.SetResult(r); + IDalamudTextureWrap wrap; + if (work.CancellationToken.CanBeCanceled) + { + using var cts = CancellationTokenSource.CreateLinkedTokenSource( + this.disposeCancellationTokenSource.Token, + work.CancellationToken); + wrap = await work.ImmediateLoadFunction(cts.Token); + } + else + { + wrap = await work.ImmediateLoadFunction(this.disposeCancellationTokenSource.Token); + } + + work.TaskCompletionSource.SetResult(wrap); } catch (Exception e) { - minWork.TaskCompletionSource.SetException(e); - _ = minWork.TaskCompletionSource.Task.Exception; + work.TaskCompletionSource.SetException(e); + _ = work.TaskCompletionSource.Task.Exception; + } + } + } + + private WorkItem? ExtractHighestPriorityWorkItem() + { + lock (this.workListLock) + { + WorkItem? highestPriorityWork = null; + var highestPriorityIndex = -1; + for (var i = 0; i < this.workItemPending.Count; i++) + { + var work = this.workItemPending[i]; + if (work.CancellationToken.IsCancellationRequested) + { + work.TaskCompletionSource.SetCanceled(work.CancellationToken); + _ = work.TaskCompletionSource.Task.Exception; + this.RelocatePendingWorkItemToEndAndEraseUnsafe(i--); + continue; + } + + if (highestPriorityIndex == -1 || + work.CompareTo(this.workItemPending[highestPriorityIndex]) < 0) + { + highestPriorityIndex = i; + highestPriorityWork = work; + } } - lock (this.workListLock) - this.activeWorkList.Remove(minWork); + if (highestPriorityWork is null) + return null; + + this.RelocatePendingWorkItemToEndAndEraseUnsafe(highestPriorityIndex); + return highestPriorityWork; } } /// - /// Remove an item in , avoiding shifting. + /// Remove an item in , avoiding shifting. /// /// Index of the item to remove. private void RelocatePendingWorkItemToEndAndEraseUnsafe(int index) { // Relocate the element to remove to the last. - if (index != this.pendingWorkList.Count - 1) + if (index != this.workItemPending.Count - 1) { - (this.pendingWorkList[^1], this.pendingWorkList[index]) = - (this.pendingWorkList[index], this.pendingWorkList[^1]); + (this.workItemPending[^1], this.workItemPending[index]) = + (this.workItemPending[index], this.workItemPending[^1]); } - this.pendingWorkList.RemoveAt(this.pendingWorkList.Count - 1); + this.workItemPending.RemoveAt(this.workItemPending.Count - 1); } /// From b16fa5cb463408d4d5bed7637f6630ad485ebd12 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 28 Feb 2024 20:07:13 +0900 Subject: [PATCH 19/57] Inline as seen fit --- .../SharedImmediateTexture.cs | 4 +++ .../Internal/TextureLoadThrottler.cs | 6 +++++ Dalamud/Interface/Internal/TextureManager.cs | 27 ++++++++++--------- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/Dalamud/Interface/Internal/SharedImmediateTextures/SharedImmediateTexture.cs b/Dalamud/Interface/Internal/SharedImmediateTextures/SharedImmediateTexture.cs index e75f8d038..88316b135 100644 --- a/Dalamud/Interface/Internal/SharedImmediateTextures/SharedImmediateTexture.cs +++ b/Dalamud/Interface/Internal/SharedImmediateTextures/SharedImmediateTexture.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -210,10 +211,12 @@ internal abstract class SharedImmediateTexture } /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public IDalamudTextureWrap GetWrap() => this.GetWrap(Service.Get().Empty4X4); /// [return: NotNullIfNotNull(nameof(defaultWrap))] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public IDalamudTextureWrap? GetWrap(IDalamudTextureWrap? defaultWrap) { if (!this.TryGetWrap(out var texture, out _)) @@ -222,6 +225,7 @@ internal abstract class SharedImmediateTexture } /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool TryGetWrap([NotNullWhen(true)] out IDalamudTextureWrap? texture, out Exception? exception) { ThreadSafety.AssertMainThread(); diff --git a/Dalamud/Interface/Internal/TextureLoadThrottler.cs b/Dalamud/Interface/Internal/TextureLoadThrottler.cs index 2b5bbb797..894e5308e 100644 --- a/Dalamud/Interface/Internal/TextureLoadThrottler.cs +++ b/Dalamud/Interface/Internal/TextureLoadThrottler.cs @@ -21,6 +21,8 @@ internal class TextureLoadThrottler : IServiceType, IDisposable private readonly Channel workTokenChannel = Channel.CreateUnbounded(); private readonly List workItemPending = new(); + private bool disposing; + [ServiceManager.ServiceConstructor] private TextureLoadThrottler() { @@ -54,6 +56,10 @@ internal class TextureLoadThrottler : IServiceType, IDisposable /// public void Dispose() { + if (this.disposing) + return; + + this.disposing = true; this.newItemChannel.Writer.Complete(); this.workTokenChannel.Writer.Complete(); this.disposeCancellationTokenSource.Cancel(); diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index a906a214b..65b290076 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -2,6 +2,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -14,7 +15,6 @@ 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; @@ -26,8 +26,6 @@ using SharpDX.DXGI; namespace Dalamud.Interface.Internal; -// TODO API10: Remove keepAlive from public APIs - /// /// Service responsible for loading and disposing ImGui texture wraps. /// @@ -45,14 +43,11 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid 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 static readonly ModuleLog Log = new("TEXM"); + private static readonly ModuleLog Log = new(nameof(TextureManager)); [ServiceManager.ServiceDependency] private readonly Dalamud dalamud = Service.Get(); - [ServiceManager.ServiceDependency] - private readonly DalamudAssetManager dalamudAssetManager = Service.Get(); - [ServiceManager.ServiceDependency] private readonly DataManager dataManager = Service.Get(); @@ -73,10 +68,7 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid private bool disposing; [ServiceManager.ServiceConstructor] - private TextureManager() - { - this.framework.Update += this.FrameworkOnUpdate; - } + private TextureManager() => this.framework.Update += this.FrameworkOnUpdate; /// public event ITextureSubstitutionProvider.TextureDataInterceptorDelegate? InterceptTexDataLoad; @@ -118,7 +110,8 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid this.fileSystemTextures.Clear(); } -#region API9 compat + #region API9 compat + #pragma warning disable CS0618 // Type or member is obsolete /// [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] @@ -162,27 +155,34 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid IDalamudTextureWrap? ITextureProvider.GetTextureFromFile(FileInfo file, bool keepAlive) => this.GetFromFile(file.FullName).GetAvailableOnAccessWrapForApi9(); #pragma warning restore CS0618 // Type or member is obsolete -#endregion + + #endregion /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public SharedImmediateTexture GetFromGameIcon(in GameIconLookup lookup) => this.GetFromGame(this.lookupToPath.GetOrAdd(lookup, this.GetIconPathByValue)); /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public SharedImmediateTexture GetFromGame(string path) => this.gamePathTextures.GetOrAdd(path, GamePathSharedImmediateTexture.CreateImmediate); /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public SharedImmediateTexture GetFromFile(string path) => this.fileSystemTextures.GetOrAdd(path, FileSystemSharedImmediateTexture.CreateImmediate); /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] ISharedImmediateTexture ITextureProvider.GetFromGameIcon(in GameIconLookup lookup) => this.GetFromGameIcon(lookup); /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] ISharedImmediateTexture ITextureProvider.GetFromGame(string path) => this.GetFromGame(path); /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] ISharedImmediateTexture ITextureProvider.GetFromFile(string path) => this.GetFromFile(path); /// @@ -519,6 +519,7 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid v.ContentQueried && v.ReleaseSelfReference(false) == 0 && !v.HasRevivalPossibility; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private string GetIconPathByValue(GameIconLookup lookup) => this.TryGetIconPath(lookup, out var path) ? path : throw new FileNotFoundException(); } From 35f38024711fa34e59d418b8617a8dc38900f75b Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 28 Feb 2024 20:10:31 +0900 Subject: [PATCH 20/57] Format --- .../FileSystemSharedImmediateTexture.cs | 20 ++---- .../GamePathSharedImmediateTexture.cs | 20 ++---- .../SharedImmediateTexture.cs | 63 +++++-------------- Dalamud/Interface/Internal/TextureManager.cs | 34 ++++------ .../Windows/Data/Widgets/TexWidget.cs | 14 ++--- 5 files changed, 47 insertions(+), 104 deletions(-) diff --git a/Dalamud/Interface/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs b/Dalamud/Interface/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs index aac2169b9..f74379c28 100644 --- a/Dalamud/Interface/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs +++ b/Dalamud/Interface/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs @@ -6,16 +6,12 @@ using Dalamud.Utility; namespace Dalamud.Interface.Internal.SharedImmediateTextures; -/// -/// Represents a sharable texture, based on a file on the system filesystem. -/// +/// Represents a sharable texture, based on a file on the system filesystem. internal sealed class FileSystemSharedImmediateTexture : SharedImmediateTexture { private readonly string path; - /// - /// Initializes a new instance of the class. - /// + /// Initializes a new instance of the class. /// The path. /// If set to true, this class will hold a reference to self. /// Otherwise, it is expected that the caller to hold the reference. @@ -30,18 +26,14 @@ internal sealed class FileSystemSharedImmediateTexture : SharedImmediateTexture /// public override string SourcePathForDebug => this.path; - /// - /// Creates a new instance of . - /// The new instance will hold a reference to itself. - /// + /// Creates a new instance of . + /// The new instance will hold a reference to itself. /// The path. /// The new instance. public static SharedImmediateTexture CreateImmediate(string path) => new FileSystemSharedImmediateTexture(path, true); - /// - /// Creates a new instance of . - /// The caller is expected to manage ownership of the new instance. - /// + /// Creates a new instance of . + /// The caller is expected to manage ownership of the new instance. /// The path. /// The new instance. public static SharedImmediateTexture CreateAsync(string path) => new FileSystemSharedImmediateTexture(path, false); diff --git a/Dalamud/Interface/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs b/Dalamud/Interface/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs index 6c709321f..c06669372 100644 --- a/Dalamud/Interface/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs +++ b/Dalamud/Interface/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs @@ -9,16 +9,12 @@ using Lumina.Data.Files; namespace Dalamud.Interface.Internal.SharedImmediateTextures; -/// -/// Represents a sharable texture, based on a file in game resources. -/// +/// Represents a sharable texture, based on a file in game resources. internal sealed class GamePathSharedImmediateTexture : SharedImmediateTexture { private readonly string path; - /// - /// Initializes a new instance of the class. - /// + /// Initializes a new instance of the class. /// The path. /// If set to true, this class will hold a reference to self. /// Otherwise, it is expected that the caller to hold the reference. @@ -33,18 +29,14 @@ internal sealed class GamePathSharedImmediateTexture : SharedImmediateTexture /// public override string SourcePathForDebug => this.path; - /// - /// Creates a new instance of . - /// The new instance will hold a reference to itself. - /// + /// Creates a new instance of . + /// The new instance will hold a reference to itself. /// The path. /// The new instance. public static SharedImmediateTexture CreateImmediate(string path) => new GamePathSharedImmediateTexture(path, true); - /// - /// Creates a new instance of . - /// The caller is expected to manage ownership of the new instance. - /// + /// Creates a new instance of . + /// The caller is expected to manage ownership of the new instance. /// The path. /// The new instance. public static SharedImmediateTexture CreateAsync(string path) => new GamePathSharedImmediateTexture(path, false); diff --git a/Dalamud/Interface/Internal/SharedImmediateTextures/SharedImmediateTexture.cs b/Dalamud/Interface/Internal/SharedImmediateTextures/SharedImmediateTexture.cs index 88316b135..b929a6faf 100644 --- a/Dalamud/Interface/Internal/SharedImmediateTextures/SharedImmediateTexture.cs +++ b/Dalamud/Interface/Internal/SharedImmediateTextures/SharedImmediateTexture.cs @@ -8,9 +8,7 @@ using Dalamud.Utility; namespace Dalamud.Interface.Internal.SharedImmediateTextures; -/// -/// Represents a texture that may have multiple reference holders (owners). -/// +/// Represents a texture that may have multiple reference holders (owners). internal abstract class SharedImmediateTexture : ISharedImmediateTexture, IRefCountable, TextureLoadThrottler.IThrottleBasisProvider { @@ -28,9 +26,7 @@ internal abstract class SharedImmediateTexture private CancellationTokenSource? cancellationTokenSource; private NotOwnedTextureWrap? nonOwningWrap; - /// - /// Initializes a new instance of the class. - /// + /// Initializes a new instance of the class. /// If set to true, this class will hold a reference to self. /// Otherwise, it is expected that the caller to hold the reference. protected SharedImmediateTexture(bool holdSelfReference) @@ -58,37 +54,26 @@ internal abstract class SharedImmediateTexture this.FirstRequestedTick = this.LatestRequestedTick = Environment.TickCount64; } - /// - /// Gets the instance ID. Debug use only. - /// + /// Gets the instance ID. Debug use only. public long InstanceIdForDebug { get; } - /// - /// Gets the remaining time for self reference in milliseconds. Debug use only. - /// + /// 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. - /// + /// Gets the reference count. Debug use only. public int RefCountForDebug => this.refCount; - /// - /// Gets the source path. Debug use only. - /// + /// Gets the source path. Debug use only. public abstract string SourcePathForDebug { get; } - /// - /// Gets a value indicating whether this instance of supports revival. + /// Gets a value indicating whether this instance of supports revival. /// public bool HasRevivalPossibility => this.RevivalPossibility?.TryGetTarget(out _) is true; - /// - /// Gets or sets the underlying texture wrap. - /// + /// Gets or sets the underlying texture wrap. public Task? UnderlyingWrap { get; set; } /// @@ -100,21 +85,15 @@ internal abstract class SharedImmediateTexture /// public long LatestRequestedTick { get; private set; } - /// - /// Gets a value indicating whether the content has been queried, - /// i.e. or is called. - /// + /// Gets a value indicating whether the content has been queried, + /// i.e. or is called. public bool ContentQueried { get; private set; } - /// - /// Gets a cancellation token for cancelling load. - /// Intended to be called from implementors' constructors and . - /// + /// Gets a cancellation token for cancelling load. + /// Intended to be called from implementors' constructors and . protected CancellationToken LoadCancellationToken => this.cancellationTokenSource?.Token ?? default; - /// - /// Gets or sets a weak reference to an object that demands this objects to be alive. - /// + /// Gets or sets 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, @@ -184,9 +163,7 @@ internal abstract class SharedImmediateTexture } } - /// - /// Releases self-reference, if conditions are met. - /// + /// Releases self-reference, if conditions are met. /// If set to true, the self-reference will be released immediately. /// Number of the new reference count that may or may not have changed. public int ReleaseSelfReference(bool immediate) @@ -280,9 +257,7 @@ internal abstract class SharedImmediateTexture return new RefCountableWrappingTextureWrap(dtw, this); } - /// - /// Gets a texture wrap which ensures that the values will be populated on access. - /// + /// 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() @@ -311,14 +286,10 @@ internal abstract class SharedImmediateTexture return this.availableOnAccessWrapForApi9; } - /// - /// Cleans up this instance of . - /// + /// Cleans up this instance of . protected abstract void ReleaseResources(); - /// - /// Attempts to restore the reference to this texture. - /// + /// Attempts to restore the reference to this texture. protected abstract void ReviveResources(); private IRefCountable.RefCountResult TryAddRef(out int newRefCount) diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index 65b290076..bd35ab97b 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -26,9 +26,7 @@ using SharpDX.DXGI; namespace Dalamud.Interface.Internal; -/// -/// Service responsible for loading and disposing ImGui texture wraps. -/// +/// Service responsible for loading and disposing ImGui texture wraps. [PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] @@ -73,25 +71,19 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid /// public event ITextureSubstitutionProvider.TextureDataInterceptorDelegate? InterceptTexDataLoad; - /// - /// Gets all the loaded textures from the game resources. Debug use only. - /// - public ICollection GamePathTextures => this.gamePathTextures.Values; + /// Gets all the loaded textures from the game resources. + public ICollection GamePathTexturesForDebug => this.gamePathTextures.Values; - /// - /// Gets all the loaded textures from the game resources. Debug use only. - /// - public ICollection FileSystemTextures => this.fileSystemTextures.Values; + /// Gets all the loaded textures from the game resources. + public ICollection FileSystemTexturesForDebug => this.fileSystemTextures.Values; - /// - /// Gets all the loaded textures that are invalidated from . Debug use only. - /// + /// Gets all the loaded textures that are invalidated from . /// lock on use of the value returned from this property. [SuppressMessage( "ReSharper", "InconsistentlySynchronizedField", Justification = "Debug use only; users are expected to lock around this")] - public ICollection InvalidatedTextures => this.invalidatedTextures; + public ICollection InvalidatedTexturesForDebug => this.invalidatedTextures; /// public void Dispose() @@ -435,10 +427,8 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid } } - /// - /// Gets a texture from the given image. Skips the load throttler; intended to be used from implementation of - /// s. - /// + /// Gets a texture from the given image. Skips the load throttler; intended to be used from implementation + /// of s. /// The data. /// The loaded texture. internal IDalamudTextureWrap NoThrottleGetFromImage(ReadOnlyMemory bytes) @@ -454,10 +444,8 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid ?? throw new("Failed to load image because of an unknown reason.")); } - /// - /// Gets a texture from the given . Skips the load throttler; intended to be used from - /// implementation of s. - /// + /// Gets a texture from the given . Skips the load throttler; intended to be used from + /// implementation of s. /// The data. /// The loaded texture. internal IDalamudTextureWrap NoThrottleGetFromTexFile(TexFile file) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs index 2a4222c5b..c75a0c629 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -65,21 +65,21 @@ internal class TexWidget : IDataWindowWidget GC.Collect(); ImGui.PushID("loadedGameTextures"); - if (ImGui.CollapsingHeader($"Loaded Game Textures: {this.textureManager.GamePathTextures.Count:g}###header")) - this.DrawLoadedTextures(this.textureManager.GamePathTextures); + if (ImGui.CollapsingHeader($"Loaded Game Textures: {this.textureManager.GamePathTexturesForDebug.Count:g}###header")) + this.DrawLoadedTextures(this.textureManager.GamePathTexturesForDebug); ImGui.PopID(); ImGui.PushID("loadedFileTextures"); - if (ImGui.CollapsingHeader($"Loaded File Textures: {this.textureManager.FileSystemTextures.Count:g}###header")) - this.DrawLoadedTextures(this.textureManager.FileSystemTextures); + if (ImGui.CollapsingHeader($"Loaded File Textures: {this.textureManager.FileSystemTexturesForDebug.Count:g}###header")) + this.DrawLoadedTextures(this.textureManager.FileSystemTexturesForDebug); ImGui.PopID(); - lock (this.textureManager.InvalidatedTextures) + lock (this.textureManager.InvalidatedTexturesForDebug) { ImGui.PushID("invalidatedTextures"); - if (ImGui.CollapsingHeader($"Invalidated: {this.textureManager.InvalidatedTextures.Count:g}###header")) + if (ImGui.CollapsingHeader($"Invalidated: {this.textureManager.InvalidatedTexturesForDebug.Count:g}###header")) { - this.DrawLoadedTextures(this.textureManager.InvalidatedTextures); + this.DrawLoadedTextures(this.textureManager.InvalidatedTexturesForDebug); } ImGui.PopID(); From b52d4724e936390b85d3107176ce7b5c025a9be8 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 28 Feb 2024 20:15:31 +0900 Subject: [PATCH 21/57] cleanup --- .../FileSystemSharedImmediateTexture.cs | 21 ++------------ .../GamePathSharedImmediateTexture.cs | 21 ++------------ .../SharedImmediateTexture.cs | 29 +++++-------------- Dalamud/Interface/Internal/TextureManager.cs | 4 +-- 4 files changed, 15 insertions(+), 60 deletions(-) diff --git a/Dalamud/Interface/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs b/Dalamud/Interface/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs index f74379c28..6504c72c0 100644 --- a/Dalamud/Interface/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs +++ b/Dalamud/Interface/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs @@ -13,30 +13,15 @@ internal sealed class FileSystemSharedImmediateTexture : SharedImmediateTexture /// Initializes a new instance of the class. /// The path. - /// If set to true, this class will hold a reference to self. - /// Otherwise, it is expected that the caller to hold the reference. - private FileSystemSharedImmediateTexture(string path, bool holdSelfReference) - : base(holdSelfReference) - { - this.path = path; - if (holdSelfReference) - this.ReviveResources(); - } + private FileSystemSharedImmediateTexture(string path) => this.path = path; /// public override string SourcePathForDebug => this.path; - /// Creates a new instance of . - /// The new instance will hold a reference to itself. + /// Creates a new placeholder instance of . /// The path. /// The new instance. - public static SharedImmediateTexture CreateImmediate(string path) => new FileSystemSharedImmediateTexture(path, true); - - /// Creates a new instance of . - /// The caller is expected to manage ownership of the new instance. - /// The path. - /// The new instance. - public static SharedImmediateTexture CreateAsync(string path) => new FileSystemSharedImmediateTexture(path, false); + public static SharedImmediateTexture CreatePlaceholder(string path) => new FileSystemSharedImmediateTexture(path); /// public override string ToString() => diff --git a/Dalamud/Interface/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs b/Dalamud/Interface/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs index c06669372..f1f35677e 100644 --- a/Dalamud/Interface/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs +++ b/Dalamud/Interface/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs @@ -16,30 +16,15 @@ internal sealed class GamePathSharedImmediateTexture : SharedImmediateTexture /// Initializes a new instance of the class. /// The path. - /// If set to true, this class will hold a reference to self. - /// Otherwise, it is expected that the caller to hold the reference. - private GamePathSharedImmediateTexture(string path, bool holdSelfReference) - : base(holdSelfReference) - { - this.path = path; - if (holdSelfReference) - this.ReviveResources(); - } + private GamePathSharedImmediateTexture(string path) => this.path = path; /// public override string SourcePathForDebug => this.path; - /// Creates a new instance of . - /// The new instance will hold a reference to itself. + /// Creates a new placeholder instance of . /// The path. /// The new instance. - public static SharedImmediateTexture CreateImmediate(string path) => new GamePathSharedImmediateTexture(path, true); - - /// Creates a new instance of . - /// The caller is expected to manage ownership of the new instance. - /// The path. - /// The new instance. - public static SharedImmediateTexture CreateAsync(string path) => new GamePathSharedImmediateTexture(path, false); + public static SharedImmediateTexture CreatePlaceholder(string path) => new GamePathSharedImmediateTexture(path); /// public override string ToString() => $"{nameof(GamePathSharedImmediateTexture)}#{this.InstanceIdForDebug}({this.path})"; diff --git a/Dalamud/Interface/Internal/SharedImmediateTextures/SharedImmediateTexture.cs b/Dalamud/Interface/Internal/SharedImmediateTextures/SharedImmediateTexture.cs index b929a6faf..d4df6a85b 100644 --- a/Dalamud/Interface/Internal/SharedImmediateTextures/SharedImmediateTexture.cs +++ b/Dalamud/Interface/Internal/SharedImmediateTextures/SharedImmediateTexture.cs @@ -27,30 +27,15 @@ internal abstract class SharedImmediateTexture private NotOwnedTextureWrap? nonOwningWrap; /// Initializes a new instance of the class. - /// If set to true, this class will hold a reference to self. - /// Otherwise, it is expected that the caller to hold the reference. - protected SharedImmediateTexture(bool holdSelfReference) + /// The new instance is a placeholder instance. + protected SharedImmediateTexture() { this.InstanceIdForDebug = Interlocked.Increment(ref instanceCounter); - - if (holdSelfReference) - { - this.refCount = 1; - this.selfReferenceExpiry = Environment.TickCount64 + SelfReferenceDurationTicks; - this.ContentQueried = true; - this.IsOpportunistic = true; - this.resourceReleased = false; - this.cancellationTokenSource = new(); - } - else - { - this.refCount = 0; - this.selfReferenceExpiry = SelfReferenceExpiryExpired; - this.ContentQueried = false; - this.IsOpportunistic = false; - this.resourceReleased = true; - } - + this.refCount = 0; + this.selfReferenceExpiry = SelfReferenceExpiryExpired; + this.ContentQueried = false; + this.IsOpportunistic = true; + this.resourceReleased = true; this.FirstRequestedTick = this.LatestRequestedTick = Environment.TickCount64; } diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index bd35ab97b..3f44bcf20 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -158,12 +158,12 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public SharedImmediateTexture GetFromGame(string path) => - this.gamePathTextures.GetOrAdd(path, GamePathSharedImmediateTexture.CreateImmediate); + this.gamePathTextures.GetOrAdd(path, GamePathSharedImmediateTexture.CreatePlaceholder); /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public SharedImmediateTexture GetFromFile(string path) => - this.fileSystemTextures.GetOrAdd(path, FileSystemSharedImmediateTexture.CreateImmediate); + this.fileSystemTextures.GetOrAdd(path, FileSystemSharedImmediateTexture.CreatePlaceholder); /// [MethodImpl(MethodImplOptions.AggressiveInlining)] From cc756c243cfd9adeec02c4842a650a9bbd9137ac Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 28 Feb 2024 21:04:57 +0900 Subject: [PATCH 22/57] Add ITextureProvider.GetFromManifestResource(Assembly,string) --- .../ManifestResourceSharedImmediateTexture.cs | 64 ++++++++ Dalamud/Interface/Internal/TextureManager.cs | 126 ++++++++++++---- .../Windows/Data/Widgets/TexWidget.cs | 140 +++++++++++++++++- Dalamud/Plugin/Services/ITextureProvider.cs | 7 + Dalamud/Utility/TexFileExtensions.cs | 25 ++++ 5 files changed, 327 insertions(+), 35 deletions(-) create mode 100644 Dalamud/Interface/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs diff --git a/Dalamud/Interface/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs b/Dalamud/Interface/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs new file mode 100644 index 000000000..de55a18f5 --- /dev/null +++ b/Dalamud/Interface/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs @@ -0,0 +1,64 @@ +using System.IO; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Utility; + +namespace Dalamud.Interface.Internal.SharedImmediateTextures; + +/// Represents a sharable texture, based on a manifest texture obtained from +/// . +internal sealed class ManifestResourceSharedImmediateTexture : SharedImmediateTexture +{ + private readonly Assembly assembly; + private readonly string name; + + /// Initializes a new instance of the class. + /// The assembly containing manifest resources. + /// The case-sensitive name of the manifest resource being requested. + private ManifestResourceSharedImmediateTexture(Assembly assembly, string 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. + public static SharedImmediateTexture CreatePlaceholder((Assembly Assembly, string Name) args) => + 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().CreateLoader( + this, + this.CreateTextureAsync, + this.LoadCancellationToken); + + private 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); + await stream.CopyToAsync(ms, cancellationToken); + return tm.NoThrottleGetFromImage(ms.GetBuffer().AsMemory(0, (int)ms.Length)); + } +} diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index 3f44bcf20..018e55d8b 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -2,6 +2,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Reflection; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -17,6 +18,7 @@ using Dalamud.Logging.Internal; using Dalamud.Plugin.Services; using Dalamud.Utility; +using Lumina.Data; using Lumina.Data.Files; using SharpDX; @@ -59,8 +61,14 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid private readonly TextureLoadThrottler textureLoadThrottler = Service.Get(); private readonly ConcurrentLru lookupToPath = new(PathLookupLruCount); + private readonly ConcurrentDictionary gamePathTextures = new(); + private readonly ConcurrentDictionary fileSystemTextures = new(); + + private readonly ConcurrentDictionary<(Assembly Assembly, string Name), SharedImmediateTexture> + manifestResourceTextures = new(); + private readonly HashSet invalidatedTextures = new(); private bool disposing; @@ -71,12 +79,15 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid /// public event ITextureSubstitutionProvider.TextureDataInterceptorDelegate? InterceptTexDataLoad; - /// Gets all the loaded textures from the game resources. + /// Gets all the loaded textures from game resources. public ICollection GamePathTexturesForDebug => this.gamePathTextures.Values; - /// Gets all the loaded textures from the game resources. + /// Gets all the loaded textures from filesystem. public ICollection FileSystemTexturesForDebug => this.fileSystemTextures.Values; + /// Gets all the loaded textures from assembly manifest resources. + public ICollection ManifestResourceTexturesForDebug => this.manifestResourceTextures.Values; + /// Gets all the loaded textures that are invalidated from . /// lock on use of the value returned from this property. [SuppressMessage( @@ -92,14 +103,20 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid return; this.disposing = true; - foreach (var v in this.gamePathTextures.Values) - v.ReleaseSelfReference(true); - foreach (var v in this.fileSystemTextures.Values) - v.ReleaseSelfReference(true); + ReleaseSelfReferences(this.gamePathTextures); + ReleaseSelfReferences(this.fileSystemTextures); + ReleaseSelfReferences(this.manifestResourceTextures); this.lookupToPath.Clear(); - this.gamePathTextures.Clear(); - this.fileSystemTextures.Clear(); + + return; + + static void ReleaseSelfReferences(ConcurrentDictionary dict) + { + foreach (var v in dict.Values) + v.ReleaseSelfReference(true); + dict.Clear(); + } } #region API9 compat @@ -157,13 +174,29 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public SharedImmediateTexture GetFromGame(string path) => - this.gamePathTextures.GetOrAdd(path, GamePathSharedImmediateTexture.CreatePlaceholder); + public SharedImmediateTexture GetFromGame(string path) + { + ObjectDisposedException.ThrowIf(this.disposing, this); + return this.gamePathTextures.GetOrAdd(path, GamePathSharedImmediateTexture.CreatePlaceholder); + } /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public SharedImmediateTexture GetFromFile(string path) => - this.fileSystemTextures.GetOrAdd(path, FileSystemSharedImmediateTexture.CreatePlaceholder); + public SharedImmediateTexture GetFromFile(string path) + { + ObjectDisposedException.ThrowIf(this.disposing, this); + return this.fileSystemTextures.GetOrAdd(path, FileSystemSharedImmediateTexture.CreatePlaceholder); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public SharedImmediateTexture GetFromManifestResource(Assembly assembly, string name) + { + ObjectDisposedException.ThrowIf(this.disposing, this); + return this.manifestResourceTextures.GetOrAdd( + (assembly, name), + ManifestResourceSharedImmediateTexture.CreatePlaceholder); + } /// [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -177,6 +210,11 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid [MethodImpl(MethodImplOptions.AggressiveInlining)] ISharedImmediateTexture ITextureProvider.GetFromFile(string path) => this.GetFromFile(path); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + ISharedImmediateTexture ITextureProvider.GetFromManifestResource(Assembly assembly, string name) => + this.GetFromManifestResource(assembly, name); + /// public Task CreateFromImageAsync( ReadOnlyMemory bytes, @@ -433,15 +471,39 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid /// The loaded texture. internal IDalamudTextureWrap NoThrottleGetFromImage(ReadOnlyMemory bytes) { + ObjectDisposedException.ThrowIf(this.disposing, this); + if (this.interfaceManager.Scene is not { } scene) { _ = Service.Get(); scene = this.interfaceManager.Scene ?? throw new InvalidOperationException(); } + var bytesArray = bytes.ToArray(); + var texFileAttemptException = default(Exception); + if (TexFileExtensions.IsPossiblyTexFile2D(bytesArray)) + { + var tf = new TexFile(); + typeof(TexFile).GetProperty(nameof(tf.Data))!.GetSetMethod(true)!.Invoke( + tf, + new object?[] { bytesArray }); + typeof(TexFile).GetProperty(nameof(tf.Reader))!.GetSetMethod(true)!.Invoke( + tf, + new object?[] { new LuminaBinaryReader(bytesArray) }); + // Note: FileInfo and FilePath are not used from TexFile; skip it. + try + { + return this.NoThrottleGetFromTexFile(tf); + } + catch (Exception e) + { + texFileAttemptException = e; + } + } + return new DalamudTextureWrap( - scene.LoadImage(bytes.ToArray()) - ?? throw new("Failed to load image because of an unknown reason.")); + scene.LoadImage(bytesArray) + ?? throw texFileAttemptException ?? new("Failed to load image because of an unknown reason.")); } /// Gets a texture from the given . Skips the load throttler; intended to be used from @@ -450,6 +512,8 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid /// The loaded texture. internal IDalamudTextureWrap NoThrottleGetFromTexFile(TexFile file) { + ObjectDisposedException.ThrowIf(this.disposing, this); + var buffer = file.TextureBuffer; var (dxgiFormat, conversion) = TexFile.GetDxgiFormatFromTextureFormat(file.Header.Format, false); if (conversion != TexFile.DxgiFormatConversion.NoConversion || !this.SupportsDxgiFormat(dxgiFormat)) @@ -476,23 +540,9 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid private void FrameworkOnUpdate(IFramework unused) { - if (!this.gamePathTextures.IsEmpty) - { - foreach (var (k, v) in this.gamePathTextures) - { - if (TextureFinalReleasePredicate(v)) - _ = this.gamePathTextures.TryRemove(k, out _); - } - } - - if (!this.fileSystemTextures.IsEmpty) - { - foreach (var (k, v) in this.fileSystemTextures) - { - if (TextureFinalReleasePredicate(v)) - _ = this.fileSystemTextures.TryRemove(k, out _); - } - } + RemoveFinalReleased(this.gamePathTextures); + RemoveFinalReleased(this.fileSystemTextures); + RemoveFinalReleased(this.manifestResourceTextures); // ReSharper disable once InconsistentlySynchronizedField if (this.invalidatedTextures.Count != 0) @@ -503,6 +553,20 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid return; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static void RemoveFinalReleased(ConcurrentDictionary dict) + { + if (!dict.IsEmpty) + { + foreach (var (k, v) in dict) + { + if (TextureFinalReleasePredicate(v)) + _ = dict.TryRemove(k, out _); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] static bool TextureFinalReleasePredicate(SharedImmediateTexture v) => v.ContentQueried && v.ReleaseSelfReference(false) == 0 && !v.HasRevivalPossibility; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs index c75a0c629..980f32f3c 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -1,6 +1,9 @@ using System.Collections.Generic; using System.IO; +using System.Linq; using System.Numerics; +using System.Reflection; +using System.Runtime.Loader; using System.Threading.Tasks; using Dalamud.Interface.Components; @@ -27,6 +30,11 @@ internal class TexWidget : IDataWindowWidget private bool hq = false; private string inputTexPath = string.Empty; private string inputFilePath = string.Empty; + private Assembly[]? inputManifestResourceAssemblyCandidates; + private string[]? inputManifestResourceAssemblyCandidateNames; + private int inputManifestResourceAssemblyIndex; + private string[]? inputManifestResourceNameCandidates; + private int inputManifestResourceNameIndex; private Vector2 inputTexUv0 = Vector2.Zero; private Vector2 inputTexUv1 = Vector2.One; private Vector4 inputTintCol = Vector4.One; @@ -53,6 +61,11 @@ internal class TexWidget : IDataWindowWidget this.inputFilePath = Path.Join( Service.Get().StartInfo.AssetDirectory!, DalamudAsset.Logo.GetAttribute()!.FileName); + this.inputManifestResourceAssemblyCandidates = null; + this.inputManifestResourceAssemblyCandidateNames = null; + this.inputManifestResourceAssemblyIndex = 0; + this.inputManifestResourceNameCandidates = null; + this.inputManifestResourceNameIndex = 0; this.Ready = true; } @@ -65,19 +78,28 @@ internal class TexWidget : IDataWindowWidget GC.Collect(); ImGui.PushID("loadedGameTextures"); - if (ImGui.CollapsingHeader($"Loaded Game Textures: {this.textureManager.GamePathTexturesForDebug.Count:g}###header")) + if (ImGui.CollapsingHeader( + $"Loaded Game Textures: {this.textureManager.GamePathTexturesForDebug.Count:g}###header")) this.DrawLoadedTextures(this.textureManager.GamePathTexturesForDebug); ImGui.PopID(); ImGui.PushID("loadedFileTextures"); - if (ImGui.CollapsingHeader($"Loaded File Textures: {this.textureManager.FileSystemTexturesForDebug.Count:g}###header")) + if (ImGui.CollapsingHeader( + $"Loaded File Textures: {this.textureManager.FileSystemTexturesForDebug.Count:g}###header")) this.DrawLoadedTextures(this.textureManager.FileSystemTexturesForDebug); ImGui.PopID(); + ImGui.PushID("loadedManifestResourceTextures"); + if (ImGui.CollapsingHeader( + $"Loaded Manifest Resource Textures: {this.textureManager.ManifestResourceTexturesForDebug.Count:g}###header")) + this.DrawLoadedTextures(this.textureManager.ManifestResourceTexturesForDebug); + ImGui.PopID(); + lock (this.textureManager.InvalidatedTexturesForDebug) { ImGui.PushID("invalidatedTextures"); - if (ImGui.CollapsingHeader($"Invalidated: {this.textureManager.InvalidatedTexturesForDebug.Count:g}###header")) + if (ImGui.CollapsingHeader( + $"Invalidated: {this.textureManager.InvalidatedTexturesForDebug.Count:g}###header")) { this.DrawLoadedTextures(this.textureManager.InvalidatedTexturesForDebug); } @@ -86,16 +108,39 @@ internal class TexWidget : IDataWindowWidget } if (ImGui.CollapsingHeader("Load Game File by Icon ID", ImGuiTreeNodeFlags.DefaultOpen)) + { + ImGui.PushID(nameof(this.DrawIconInput)); this.DrawIconInput(); + ImGui.PopID(); + } if (ImGui.CollapsingHeader("Load Game File by Path", ImGuiTreeNodeFlags.DefaultOpen)) + { + ImGui.PushID(nameof(this.DrawGamePathInput)); this.DrawGamePathInput(); + ImGui.PopID(); + } if (ImGui.CollapsingHeader("Load File", ImGuiTreeNodeFlags.DefaultOpen)) + { + ImGui.PushID(nameof(this.DrawFileInput)); this.DrawFileInput(); + ImGui.PopID(); + } + + if (ImGui.CollapsingHeader("Load Assembly Manifest Resource", ImGuiTreeNodeFlags.DefaultOpen)) + { + ImGui.PushID(nameof(this.DrawAssemblyManifestResourceInput)); + this.DrawAssemblyManifestResourceInput(); + ImGui.PopID(); + } if (ImGui.CollapsingHeader("UV")) + { + ImGui.PushID(nameof(this.DrawUvInput)); this.DrawUvInput(); + ImGui.PopID(); + } TextureEntry? toRemove = null; TextureEntry? toCopy = null; @@ -337,6 +382,81 @@ internal class TexWidget : IDataWindowWidget ImGuiHelpers.ScaledDummy(10); } + private void DrawAssemblyManifestResourceInput() + { + if (this.inputManifestResourceAssemblyCandidateNames is null || + this.inputManifestResourceAssemblyCandidates is null) + { + this.inputManifestResourceAssemblyIndex = 0; + this.inputManifestResourceAssemblyCandidates = + AssemblyLoadContext + .All + .SelectMany(x => x.Assemblies) + .Distinct() + .OrderBy(x => x.GetName().FullName) + .ToArray(); + this.inputManifestResourceAssemblyCandidateNames = + this.inputManifestResourceAssemblyCandidates + .Select(x => x.GetName().FullName) + .ToArray(); + } + + if (ImGui.Combo( + "Assembly", + ref this.inputManifestResourceAssemblyIndex, + this.inputManifestResourceAssemblyCandidateNames, + this.inputManifestResourceAssemblyCandidateNames.Length)) + { + this.inputManifestResourceNameIndex = 0; + this.inputManifestResourceNameCandidates = null; + } + + var assembly = + this.inputManifestResourceAssemblyIndex >= 0 + && this.inputManifestResourceAssemblyIndex < this.inputManifestResourceAssemblyCandidates.Length + ? this.inputManifestResourceAssemblyCandidates[this.inputManifestResourceAssemblyIndex] + : null; + + this.inputManifestResourceNameCandidates ??= assembly?.GetManifestResourceNames() ?? Array.Empty(); + + ImGui.Combo( + "Name", + ref this.inputManifestResourceNameIndex, + this.inputManifestResourceNameCandidates, + this.inputManifestResourceNameCandidates.Length); + + var name = + this.inputManifestResourceNameIndex >= 0 + && this.inputManifestResourceNameIndex < this.inputManifestResourceNameCandidates.Length + ? this.inputManifestResourceNameCandidates[this.inputManifestResourceNameIndex] + : null; + + if (ImGui.Button("Refresh Assemblies")) + { + this.inputManifestResourceAssemblyIndex = 0; + this.inputManifestResourceAssemblyCandidates = null; + this.inputManifestResourceAssemblyCandidateNames = null; + this.inputManifestResourceNameIndex = 0; + this.inputManifestResourceNameCandidates = null; + } + + if (assembly is not null && name is not null) + { + ImGui.SameLine(); + if (ImGui.Button("Load File (Async)")) + { + this.addedTextures.Add( + new(Api10: this.textureManager.GetFromManifestResource(assembly, name).RentAsync())); + } + + ImGui.SameLine(); + if (ImGui.Button("Load File (Immediate)")) + this.addedTextures.Add(new(Api10ImmManifestResource: (assembly, name))); + } + + ImGuiHelpers.ScaledDummy(10); + } + private void DrawUvInput() { ImGui.InputFloat2("UV0", ref this.inputTexUv0); @@ -389,7 +509,8 @@ internal class TexWidget : IDataWindowWidget Task? Api10 = null, GameIconLookup? Api10ImmGameIcon = null, string? Api10ImmGamePath = null, - string? Api10ImmFile = null) : IDisposable + string? Api10ImmFile = null, + (Assembly Assembly, string Name)? Api10ImmManifestResource = null) : IDisposable { private static int idCounter; @@ -421,6 +542,8 @@ internal class TexWidget : IDataWindowWidget return "Must not happen"; if (this.Api10ImmFile is not null) return "Must not happen"; + if (this.Api10ImmManifestResource is not null) + return "Must not happen"; return "Not implemented"; } @@ -438,6 +561,13 @@ internal class TexWidget : IDataWindowWidget return tp.GetFromGame(this.Api10ImmGamePath).GetWrap(); if (this.Api10ImmFile is not null) return tp.GetFromFile(this.Api10ImmFile).GetWrap(); + if (this.Api10ImmManifestResource is not null) + { + return tp.GetFromManifestResource( + this.Api10ImmManifestResource.Value.Assembly, + this.Api10ImmManifestResource.Value.Name).GetWrap(); + } + return null; } @@ -460,6 +590,8 @@ internal class TexWidget : IDataWindowWidget return $"{nameof(this.Api10ImmGamePath)}: {this.Api10ImmGamePath}"; if (this.Api10ImmFile is not null) return $"{nameof(this.Api10ImmFile)}: {this.Api10ImmFile}"; + if (this.Api10ImmManifestResource is not null) + return $"{nameof(this.Api10ImmManifestResource)}: {this.Api10ImmManifestResource}"; return "Not implemented"; } } diff --git a/Dalamud/Plugin/Services/ITextureProvider.cs b/Dalamud/Plugin/Services/ITextureProvider.cs index c63e7ae4f..031825379 100644 --- a/Dalamud/Plugin/Services/ITextureProvider.cs +++ b/Dalamud/Plugin/Services/ITextureProvider.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Reflection; using System.Threading; using System.Threading.Tasks; @@ -41,6 +42,12 @@ public partial interface ITextureProvider /// The shared texture that you may use to obtain the loaded texture wrap and load states. ISharedImmediateTexture GetFromFile(string path); + /// Gets a shared texture corresponding to the given file of the assembly manifest resources. + /// The assembly containing manifest resources. + /// The case-sensitive name of the manifest resource being requested. + /// The shared texture that you may use to obtain the loaded texture wrap and load states. + ISharedImmediateTexture GetFromManifestResource(Assembly assembly, string name); + /// 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. diff --git a/Dalamud/Utility/TexFileExtensions.cs b/Dalamud/Utility/TexFileExtensions.cs index 5abea692a..ec8e10b3c 100644 --- a/Dalamud/Utility/TexFileExtensions.cs +++ b/Dalamud/Utility/TexFileExtensions.cs @@ -1,3 +1,7 @@ +using System.Runtime.CompilerServices; + +using Dalamud.Memory; + using ImGuiScene; using Lumina.Data.Files; @@ -28,4 +32,25 @@ public static class TexFileExtensions return dst; } + + /// Determines if the given data is possibly a . + /// The data. + /// true if it should be attempted to be interpreted as a . + internal static unsafe bool IsPossiblyTexFile2D(ReadOnlySpan data) + { + if (data.Length < Unsafe.SizeOf()) + return false; + fixed (byte* ptr = data) + { + ref readonly var texHeader = ref MemoryHelper.Cast((nint)ptr); + if ((texHeader.Type & TexFile.Attribute.TextureTypeMask) != TexFile.Attribute.TextureType2D) + return false; + if (!Enum.IsDefined(texHeader.Format)) + return false; + if (texHeader.Width == 0 || texHeader.Height == 0) + return false; + } + + return true; + } } From ca986b46a297f9fb7d97245b41200559054683bc Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 28 Feb 2024 21:17:50 +0900 Subject: [PATCH 23/57] cleanup --- Dalamud/Interface/Internal/TextureManager.cs | 4 +- .../Windows/Data/Widgets/TexWidget.cs | 32 +++---- .../FontAtlasFactory.BuildToolkit.cs | 2 +- .../Internals/FontAtlasFactory.cs | 2 +- .../Plugin/Services/ITextureProvider.Api9.cs | 11 +++ Dalamud/Plugin/Services/ITextureProvider.cs | 85 +++++++++---------- 6 files changed, 69 insertions(+), 67 deletions(-) diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index 018e55d8b..b0341f36c 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -343,7 +343,7 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid cancellationToken); /// - public bool SupportsDxgiFormat(int dxgiFormat) + public bool IsDxgiFormatSupported(int dxgiFormat) { if (this.interfaceManager.Scene is not { } scene) { @@ -516,7 +516,7 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid var buffer = file.TextureBuffer; var (dxgiFormat, conversion) = TexFile.GetDxgiFormatFromTextureFormat(file.Header.Format, false); - if (conversion != TexFile.DxgiFormatConversion.NoConversion || !this.SupportsDxgiFormat(dxgiFormat)) + if (conversion != TexFile.DxgiFormatConversion.NoConversion || !this.IsDxgiFormatSupported(dxgiFormat)) { dxgiFormat = (int)Format.B8G8R8A8_UNorm; buffer = buffer.Filter(0, 0, TexFile.TextureFormat.B8G8R8A8); diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs index 980f32f3c..3fdf72cda 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -107,31 +107,31 @@ internal class TexWidget : IDataWindowWidget ImGui.PopID(); } - if (ImGui.CollapsingHeader("Load Game File by Icon ID", ImGuiTreeNodeFlags.DefaultOpen)) + if (ImGui.CollapsingHeader(nameof(ITextureProvider.GetFromGameIcon), ImGuiTreeNodeFlags.DefaultOpen)) { - ImGui.PushID(nameof(this.DrawIconInput)); - this.DrawIconInput(); + ImGui.PushID(nameof(this.DrawGetFromGameIcon)); + this.DrawGetFromGameIcon(); ImGui.PopID(); } - if (ImGui.CollapsingHeader("Load Game File by Path", ImGuiTreeNodeFlags.DefaultOpen)) + if (ImGui.CollapsingHeader(nameof(ITextureProvider.GetFromGame), ImGuiTreeNodeFlags.DefaultOpen)) { - ImGui.PushID(nameof(this.DrawGamePathInput)); - this.DrawGamePathInput(); + ImGui.PushID(nameof(this.DrawGetFromGame)); + this.DrawGetFromGame(); ImGui.PopID(); } - if (ImGui.CollapsingHeader("Load File", ImGuiTreeNodeFlags.DefaultOpen)) + if (ImGui.CollapsingHeader(nameof(ITextureProvider.GetFromFile), ImGuiTreeNodeFlags.DefaultOpen)) { - ImGui.PushID(nameof(this.DrawFileInput)); - this.DrawFileInput(); + ImGui.PushID(nameof(this.DrawGetFromFile)); + this.DrawGetFromFile(); ImGui.PopID(); } - if (ImGui.CollapsingHeader("Load Assembly Manifest Resource", ImGuiTreeNodeFlags.DefaultOpen)) + if (ImGui.CollapsingHeader(nameof(ITextureProvider.GetFromManifestResource), ImGuiTreeNodeFlags.DefaultOpen)) { - ImGui.PushID(nameof(this.DrawAssemblyManifestResourceInput)); - this.DrawAssemblyManifestResourceInput(); + ImGui.PushID(nameof(this.DrawGetFromManifestResource)); + this.DrawGetFromManifestResource(); ImGui.PopID(); } @@ -308,7 +308,7 @@ internal class TexWidget : IDataWindowWidget ImGuiHelpers.ScaledDummy(10); } - private void DrawIconInput() + private void DrawGetFromGameIcon() { ImGui.InputText("Icon ID", ref this.iconId, 32); ImGui.Checkbox("HQ Item", ref this.hq); @@ -342,7 +342,7 @@ internal class TexWidget : IDataWindowWidget ImGuiHelpers.ScaledDummy(10); } - private void DrawGamePathInput() + private void DrawGetFromGame() { ImGui.InputText("Tex Path", ref this.inputTexPath, 255); @@ -362,7 +362,7 @@ internal class TexWidget : IDataWindowWidget ImGuiHelpers.ScaledDummy(10); } - private void DrawFileInput() + private void DrawGetFromFile() { ImGui.InputText("File Path", ref this.inputFilePath, 255); @@ -382,7 +382,7 @@ internal class TexWidget : IDataWindowWidget ImGuiHelpers.ScaledDummy(10); } - private void DrawAssemblyManifestResourceInput() + private void DrawGetFromManifestResource() { if (this.inputManifestResourceAssemblyCandidateNames is null || this.inputManifestResourceAssemblyCandidates is null) diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs index 0e344f450..89a47c552 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs @@ -580,7 +580,7 @@ internal sealed partial class FontAtlasFactory var buf = Array.Empty(); try { - var use4 = this.factory.TextureManager.SupportsDxgiFormat((int)Format.B4G4R4A4_UNorm); + var use4 = this.factory.TextureManager.IsDxgiFormatSupported((int)Format.B4G4R4A4_UNorm); var bpp = use4 ? 2 : 4; var width = this.NewImAtlas.TexWidth; var height = this.NewImAtlas.TexHeight; diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs index b3edcc9b2..532e223fc 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs @@ -353,7 +353,7 @@ internal sealed partial class FontAtlasFactory var numPixels = texFile.Header.Width * texFile.Header.Height; _ = Service.Get(); - var targetIsB4G4R4A4 = this.TextureManager.SupportsDxgiFormat((int)Format.B4G4R4A4_UNorm); + var targetIsB4G4R4A4 = this.TextureManager.IsDxgiFormatSupported((int)Format.B4G4R4A4_UNorm); var bpp = targetIsB4G4R4A4 ? 2 : 4; var buffer = ArrayPool.Shared.Rent(numPixels * bpp); try diff --git a/Dalamud/Plugin/Services/ITextureProvider.Api9.cs b/Dalamud/Plugin/Services/ITextureProvider.Api9.cs index 2a1a3a9a5..71d1bf928 100644 --- a/Dalamud/Plugin/Services/ITextureProvider.Api9.cs +++ b/Dalamud/Plugin/Services/ITextureProvider.Api9.cs @@ -4,6 +4,8 @@ using Dalamud.Interface; using Dalamud.Interface.Internal; using Dalamud.Utility; +using Lumina.Data.Files; + namespace Dalamud.Plugin.Services; /// @@ -96,4 +98,13 @@ public partial interface ITextureProvider [Obsolete($"Use {nameof(GetFromFile)}.")] [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] public IDalamudTextureWrap? GetTextureFromFile(FileInfo file, bool keepAlive = false); + + /// + /// 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. Dispose after use. + [Obsolete($"Use {nameof(CreateFromTexFile)}.")] + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + IDalamudTextureWrap GetTexture(TexFile file) => this.CreateFromTexFile(file); } diff --git a/Dalamud/Plugin/Services/ITextureProvider.cs b/Dalamud/Plugin/Services/ITextureProvider.cs index 031825379..46ad0fb46 100644 --- a/Dalamud/Plugin/Services/ITextureProvider.cs +++ b/Dalamud/Plugin/Services/ITextureProvider.cs @@ -27,27 +27,6 @@ namespace Dalamud.Plugin.Services; /// public partial interface ITextureProvider { - /// Gets a shared texture corresponding to the given game resource icon specifier. - /// A game icon specifier. - /// The shared texture that you may use to obtain the loaded texture wrap and load states. - ISharedImmediateTexture GetFromGameIcon(in GameIconLookup lookup); - - /// Gets a shared texture corresponding to the given path to a game resource. - /// A path to a game resource. - /// The shared texture that you may use to obtain the loaded texture wrap and load states. - ISharedImmediateTexture GetFromGame(string path); - - /// Gets a shared texture corresponding to the given file on the filesystem. - /// A path to a file on the filesystem. - /// The shared texture that you may use to obtain the loaded texture wrap and load states. - ISharedImmediateTexture GetFromFile(string path); - - /// Gets a shared texture corresponding to the given file of the assembly manifest resources. - /// The assembly containing manifest resources. - /// The case-sensitive name of the manifest resource being requested. - /// The shared texture that you may use to obtain the loaded texture wrap and load states. - ISharedImmediateTexture GetFromManifestResource(Assembly assembly, string name); - /// 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. @@ -102,31 +81,6 @@ public partial interface ITextureProvider bool leaveOpen = false, CancellationToken cancellationToken = default); - /// - /// Get a path for a specific icon's .tex file. - /// - /// The icon lookup. - /// The path to the icon. - /// If a corresponding file could not be found. - string GetIconPath(in GameIconLookup lookup); - - /// - /// Gets the path of an icon. - /// - /// The icon lookup. - /// The resolved path. - /// true if the corresponding file exists and has been set. - bool TryGetIconPath(in GameIconLookup lookup, [NotNullWhen(true)] out string? path); - - /// - /// Get a texture handle for the specified Lumina . - /// Alias for fetching from . - /// - /// The texture to obtain a handle to. - /// A texture wrap that can be used to render the texture. Dispose after use. - /// Alias for . - IDalamudTextureWrap GetTexture(TexFile file) => this.CreateFromTexFile(file); - /// /// Get a texture handle for the specified Lumina . /// Alias for fetching from . @@ -145,11 +99,48 @@ public partial interface ITextureProvider TexFile file, CancellationToken cancellationToken = default); + /// Gets a shared texture corresponding to the given game resource icon specifier. + /// A game icon specifier. + /// The shared texture that you may use to obtain the loaded texture wrap and load states. + ISharedImmediateTexture GetFromGameIcon(in GameIconLookup lookup); + + /// Gets a shared texture corresponding to the given path to a game resource. + /// A path to a game resource. + /// The shared texture that you may use to obtain the loaded texture wrap and load states. + ISharedImmediateTexture GetFromGame(string path); + + /// Gets a shared texture corresponding to the given file on the filesystem. + /// A path to a file on the filesystem. + /// The shared texture that you may use to obtain the loaded texture wrap and load states. + ISharedImmediateTexture GetFromFile(string path); + + /// Gets a shared texture corresponding to the given file of the assembly manifest resources. + /// The assembly containing manifest resources. + /// The case-sensitive name of the manifest resource being requested. + /// The shared texture that you may use to obtain the loaded texture wrap and load states. + ISharedImmediateTexture GetFromManifestResource(Assembly assembly, string name); + + /// + /// Get a path for a specific icon's .tex file. + /// + /// The icon lookup. + /// The path to the icon. + /// If a corresponding file could not be found. + string GetIconPath(in GameIconLookup lookup); + + /// + /// Gets the path of an icon. + /// + /// The icon lookup. + /// The resolved path. + /// true if the corresponding file exists and has been set. + bool TryGetIconPath(in GameIconLookup lookup, [NotNullWhen(true)] out string? path); + /// /// Determines whether the system supports the given DXGI format. /// For use with . /// /// The DXGI format. /// true if supported. - bool SupportsDxgiFormat(int dxgiFormat); + bool IsDxgiFormatSupported(int dxgiFormat); } From b34a9017029605c15958c93087d8c15d23e2c2c1 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 28 Feb 2024 22:16:40 +0900 Subject: [PATCH 24/57] make it consistent --- .../FileSystemSharedImmediateTexture.cs | 2 +- .../GamePathSharedImmediateTexture.cs | 2 +- .../ManifestResourceSharedImmediateTexture.cs | 2 +- Dalamud/Interface/Internal/TextureManager.cs | 18 +++++++++--------- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Dalamud/Interface/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs b/Dalamud/Interface/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs index 6504c72c0..734f2d0f4 100644 --- a/Dalamud/Interface/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs +++ b/Dalamud/Interface/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs @@ -44,6 +44,6 @@ internal sealed class FileSystemSharedImmediateTexture : SharedImmediateTexture private async Task CreateTextureAsync(CancellationToken cancellationToken) { var tm = await Service.GetAsync(); - return tm.NoThrottleGetFromImage(await File.ReadAllBytesAsync(this.path, cancellationToken)); + return tm.NoThrottleCreateFromImage(await File.ReadAllBytesAsync(this.path, cancellationToken)); } } diff --git a/Dalamud/Interface/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs b/Dalamud/Interface/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs index f1f35677e..8b97d04d2 100644 --- a/Dalamud/Interface/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs +++ b/Dalamud/Interface/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs @@ -50,6 +50,6 @@ internal sealed class GamePathSharedImmediateTexture : SharedImmediateTexture if (dm.GetFile(this.path) is not { } file) throw new FileNotFoundException(); cancellationToken.ThrowIfCancellationRequested(); - return tm.NoThrottleGetFromTexFile(file); + return tm.NoThrottleCreateFromTexFile(file); } } diff --git a/Dalamud/Interface/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs b/Dalamud/Interface/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs index de55a18f5..a249be80e 100644 --- a/Dalamud/Interface/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs +++ b/Dalamud/Interface/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs @@ -59,6 +59,6 @@ internal sealed class ManifestResourceSharedImmediateTexture : SharedImmediateTe var tm = await Service.GetAsync(); var ms = new MemoryStream(stream.CanSeek ? (int)stream.Length : 0); await stream.CopyToAsync(ms, cancellationToken); - return tm.NoThrottleGetFromImage(ms.GetBuffer().AsMemory(0, (int)ms.Length)); + return tm.NoThrottleCreateFromImage(ms.GetBuffer().AsMemory(0, (int)ms.Length)); } } diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index b0341f36c..6ad768e76 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -221,7 +221,7 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid CancellationToken cancellationToken = default) => this.textureLoadThrottler.CreateLoader( new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), - ct => Task.Run(() => this.NoThrottleGetFromImage(bytes.ToArray()), ct), + ct => Task.Run(() => this.NoThrottleCreateFromImage(bytes.ToArray()), ct), cancellationToken); /// @@ -339,7 +339,7 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid CancellationToken cancellationToken = default) => this.textureLoadThrottler.CreateLoader( new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), - ct => Task.Run(() => this.NoThrottleGetFromTexFile(file), ct), + ct => Task.Run(() => this.NoThrottleCreateFromTexFile(file), ct), cancellationToken); /// @@ -465,11 +465,11 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid } } - /// Gets a texture from the given image. Skips the load throttler; intended to be used from implementation - /// of s. + /// Creates a texture from the given bytes of an image file. Skips the load throttler; intended to be used + /// from implementation of s. /// The data. /// The loaded texture. - internal IDalamudTextureWrap NoThrottleGetFromImage(ReadOnlyMemory bytes) + internal IDalamudTextureWrap NoThrottleCreateFromImage(ReadOnlyMemory bytes) { ObjectDisposedException.ThrowIf(this.disposing, this); @@ -493,7 +493,7 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid // Note: FileInfo and FilePath are not used from TexFile; skip it. try { - return this.NoThrottleGetFromTexFile(tf); + return this.NoThrottleCreateFromTexFile(tf); } catch (Exception e) { @@ -506,11 +506,11 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid ?? throw texFileAttemptException ?? new("Failed to load image because of an unknown reason.")); } - /// Gets a texture from the given . Skips the load throttler; intended to be used from - /// implementation of s. + /// Creates a texture from the given . Skips the load throttler; intended to be used + /// from implementation of s. /// The data. /// The loaded texture. - internal IDalamudTextureWrap NoThrottleGetFromTexFile(TexFile file) + internal IDalamudTextureWrap NoThrottleCreateFromTexFile(TexFile file) { ObjectDisposedException.ThrowIf(this.disposing, this); From e2ed5258ebfd80b084677233e4ceea4afab713ee Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 1 Mar 2024 00:10:58 +0900 Subject: [PATCH 25/57] Cleanup TextureLoadThrottler --- .../FileSystemSharedImmediateTexture.cs | 2 +- .../GamePathSharedImmediateTexture.cs | 2 +- .../ManifestResourceSharedImmediateTexture.cs | 2 +- .../Internal/TextureLoadThrottler.cs | 219 +++++++++--------- Dalamud/Interface/Internal/TextureManager.cs | 10 +- 5 files changed, 116 insertions(+), 119 deletions(-) diff --git a/Dalamud/Interface/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs b/Dalamud/Interface/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs index 734f2d0f4..702a83f52 100644 --- a/Dalamud/Interface/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs +++ b/Dalamud/Interface/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs @@ -36,7 +36,7 @@ internal sealed class FileSystemSharedImmediateTexture : SharedImmediateTexture /// protected override void ReviveResources() => - this.UnderlyingWrap = Service.Get().CreateLoader( + this.UnderlyingWrap = Service.Get().LoadTextureAsync( this, this.CreateTextureAsync, this.LoadCancellationToken); diff --git a/Dalamud/Interface/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs b/Dalamud/Interface/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs index 8b97d04d2..e22998813 100644 --- a/Dalamud/Interface/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs +++ b/Dalamud/Interface/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs @@ -38,7 +38,7 @@ internal sealed class GamePathSharedImmediateTexture : SharedImmediateTexture /// protected override void ReviveResources() => - this.UnderlyingWrap = Service.Get().CreateLoader( + this.UnderlyingWrap = Service.Get().LoadTextureAsync( this, this.CreateTextureAsync, this.LoadCancellationToken); diff --git a/Dalamud/Interface/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs b/Dalamud/Interface/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs index a249be80e..a75a7cb68 100644 --- a/Dalamud/Interface/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs +++ b/Dalamud/Interface/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs @@ -45,7 +45,7 @@ internal sealed class ManifestResourceSharedImmediateTexture : SharedImmediateTe /// protected override void ReviveResources() => - this.UnderlyingWrap = Service.Get().CreateLoader( + this.UnderlyingWrap = Service.Get().LoadTextureAsync( this, this.CreateTextureAsync, this.LoadCancellationToken); diff --git a/Dalamud/Interface/Internal/TextureLoadThrottler.cs b/Dalamud/Interface/Internal/TextureLoadThrottler.cs index 894e5308e..978d7b9b7 100644 --- a/Dalamud/Interface/Internal/TextureLoadThrottler.cs +++ b/Dalamud/Interface/Internal/TextureLoadThrottler.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; @@ -16,7 +16,6 @@ internal class TextureLoadThrottler : IServiceType, IDisposable private readonly Task adderTask; private readonly Task[] workerTasks; - private readonly object workListLock = new(); private readonly Channel newItemChannel = Channel.CreateUnbounded(); private readonly Channel workTokenChannel = Channel.CreateUnbounded(); private readonly List workItemPending = new(); @@ -27,29 +26,21 @@ internal class TextureLoadThrottler : IServiceType, IDisposable private TextureLoadThrottler() { this.adderTask = Task.Run(this.LoopAddWorkItemAsync); - this.workerTasks = new Task[Math.Min(64, Environment.ProcessorCount)]; + this.workerTasks = new Task[Math.Max(1, Environment.ProcessorCount - 1)]; foreach (ref var task in this.workerTasks.AsSpan()) task = Task.Run(this.LoopProcessWorkItemAsync); } - /// - /// Basis for throttling. - /// + /// Basis for throttling. Values may be changed anytime. internal interface IThrottleBasisProvider { - /// - /// Gets a value indicating whether the resource is requested in an opportunistic way. - /// + /// Gets a value indicating whether the resource is requested in an opportunistic way. bool IsOpportunistic { get; } - /// - /// Gets the first requested tick count from . - /// + /// Gets the first requested tick count from . long FirstRequestedTick { get; } - /// - /// Gets the latest requested tick count from . - /// + /// Gets the latest requested tick count from . long LatestRequestedTick { get; } } @@ -72,135 +63,94 @@ internal class TextureLoadThrottler : IServiceType, IDisposable _ = t.Exception; } - /// - /// Creates a texture loader. - /// + /// Loads a texture according to some order. /// The throttle basis. /// The immediate load function. /// The cancellation token. /// The task. - public Task CreateLoader( + public Task LoadTextureAsync( IThrottleBasisProvider basis, Func> immediateLoadFunction, CancellationToken cancellationToken) { - var work = new WorkItem - { - TaskCompletionSource = new(), - Basis = basis, - CancellationToken = cancellationToken, - ImmediateLoadFunction = immediateLoadFunction, - }; + var work = new WorkItem(basis, immediateLoadFunction, cancellationToken); return this.newItemChannel.Writer.TryWrite(work) - ? work.TaskCompletionSource.Task + ? work.Task : Task.FromException(new ObjectDisposedException(nameof(TextureLoadThrottler))); } private async Task LoopAddWorkItemAsync() { - var newWorkTemp = new List(); + const int batchAddSize = 64; + var newWorks = new List(batchAddSize); var reader = this.newItemChannel.Reader; - while (!reader.Completion.IsCompleted) + while (await reader.WaitToReadAsync()) { - await reader.WaitToReadAsync(); + while (newWorks.Count < batchAddSize && reader.TryRead(out var newWork)) + newWorks.Add(newWork); - newWorkTemp.EnsureCapacity(reader.Count); - while (newWorkTemp.Count < newWorkTemp.Capacity && reader.TryRead(out var newWork)) - newWorkTemp.Add(newWork); - lock (this.workListLock) - this.workItemPending.AddRange(newWorkTemp); - for (var i = newWorkTemp.Count; i > 0; i--) + lock (this.workItemPending) + this.workItemPending.AddRange(newWorks); + + for (var i = newWorks.Count; i > 0; i--) this.workTokenChannel.Writer.TryWrite(null); - newWorkTemp.Clear(); + + newWorks.Clear(); } } private async Task LoopProcessWorkItemAsync() { var reader = this.workTokenChannel.Reader; - while (!reader.Completion.IsCompleted) + while (await reader.WaitToReadAsync()) { - _ = await reader.ReadAsync(); + if (!reader.TryRead(out _)) + continue; if (this.ExtractHighestPriorityWorkItem() is not { } work) continue; - try - { - IDalamudTextureWrap wrap; - if (work.CancellationToken.CanBeCanceled) - { - using var cts = CancellationTokenSource.CreateLinkedTokenSource( - this.disposeCancellationTokenSource.Token, - work.CancellationToken); - wrap = await work.ImmediateLoadFunction(cts.Token); - } - else - { - wrap = await work.ImmediateLoadFunction(this.disposeCancellationTokenSource.Token); - } - - work.TaskCompletionSource.SetResult(wrap); - } - catch (Exception e) - { - work.TaskCompletionSource.SetException(e); - _ = work.TaskCompletionSource.Task.Exception; - } + await work.Process(this.disposeCancellationTokenSource.Token); } } + /// Extracts the work item with the highest priority from , + /// and removes cancelled items, if any. + /// The order of items of is undefined after this function. private WorkItem? ExtractHighestPriorityWorkItem() { - lock (this.workListLock) + lock (this.workItemPending) { - WorkItem? highestPriorityWork = null; - var highestPriorityIndex = -1; - for (var i = 0; i < this.workItemPending.Count; i++) + for (var startIndex = 0; startIndex < this.workItemPending.Count - 1;) { - var work = this.workItemPending[i]; - if (work.CancellationToken.IsCancellationRequested) + var span = CollectionsMarshal.AsSpan(this.workItemPending)[startIndex..]; + ref var lastRef = ref span[^1]; + foreach (ref var itemRef in span[..^1]) { - work.TaskCompletionSource.SetCanceled(work.CancellationToken); - _ = work.TaskCompletionSource.Task.Exception; - this.RelocatePendingWorkItemToEndAndEraseUnsafe(i--); - continue; - } + if (itemRef.CancelAsRequested()) + { + itemRef = lastRef; + this.workItemPending.RemoveAt(this.workItemPending.Count - 1); + break; + } - if (highestPriorityIndex == -1 || - work.CompareTo(this.workItemPending[highestPriorityIndex]) < 0) - { - highestPriorityIndex = i; - highestPriorityWork = work; + if (itemRef.CompareTo(lastRef) < 0) + (itemRef, lastRef) = (lastRef, itemRef); + startIndex++; } } - if (highestPriorityWork is null) + if (this.workItemPending.Count == 0) return null; - this.RelocatePendingWorkItemToEndAndEraseUnsafe(highestPriorityIndex); - return highestPriorityWork; + var last = this.workItemPending[^1]; + this.workItemPending.RemoveAt(this.workItemPending.Count - 1); + return last.CancelAsRequested() ? null : last; } } - /// - /// Remove an item in , avoiding shifting. - /// - /// Index of the item to remove. - private void RelocatePendingWorkItemToEndAndEraseUnsafe(int index) - { - // Relocate the element to remove to the last. - if (index != this.workItemPending.Count - 1) - { - (this.workItemPending[^1], this.workItemPending[index]) = - (this.workItemPending[index], this.workItemPending[^1]); - } - - this.workItemPending.RemoveAt(this.workItemPending.Count - 1); - } - /// /// A read-only implementation of . /// @@ -216,27 +166,74 @@ internal class TextureLoadThrottler : IServiceType, IDisposable public long LatestRequestedTick { get; init; } = Environment.TickCount64; } - [SuppressMessage( - "StyleCop.CSharp.OrderingRules", - "SA1206:Declaration keywords should follow order", - Justification = "no")] - private record WorkItem : IComparable + private class WorkItem : IComparable { - public required TaskCompletionSource TaskCompletionSource { get; init; } + private readonly TaskCompletionSource taskCompletionSource; + private readonly IThrottleBasisProvider basis; + private readonly CancellationToken cancellationToken; + private readonly Func> immediateLoadFunction; - public required IThrottleBasisProvider Basis { get; init; } + public WorkItem( + IThrottleBasisProvider basis, + Func> immediateLoadFunction, + CancellationToken cancellationToken) + { + this.taskCompletionSource = new(); + this.basis = basis; + this.cancellationToken = cancellationToken; + this.immediateLoadFunction = immediateLoadFunction; + } - public required CancellationToken CancellationToken { get; init; } - - public required Func> ImmediateLoadFunction { get; init; } + public Task Task => this.taskCompletionSource.Task; public int CompareTo(WorkItem other) { - if (this.Basis.IsOpportunistic != other.Basis.IsOpportunistic) - return this.Basis.IsOpportunistic ? 1 : -1; - if (this.Basis.IsOpportunistic) - return -this.Basis.LatestRequestedTick.CompareTo(other.Basis.LatestRequestedTick); - return this.Basis.FirstRequestedTick.CompareTo(other.Basis.FirstRequestedTick); + if (this.basis.IsOpportunistic != other.basis.IsOpportunistic) + return this.basis.IsOpportunistic ? 1 : -1; + if (this.basis.IsOpportunistic) + return -this.basis.LatestRequestedTick.CompareTo(other.basis.LatestRequestedTick); + return this.basis.FirstRequestedTick.CompareTo(other.basis.FirstRequestedTick); + } + + public bool CancelAsRequested() + { + if (!this.cancellationToken.IsCancellationRequested) + return false; + + // Cancel the load task and move on. + this.taskCompletionSource.TrySetCanceled(this.cancellationToken); + + // Suppress the OperationCanceledException caused from the above. + _ = this.taskCompletionSource.Task.Exception; + + return true; + } + + public async ValueTask Process(CancellationToken serviceDisposeToken) + { + try + { + IDalamudTextureWrap wrap; + if (this.cancellationToken.CanBeCanceled) + { + using var cts = CancellationTokenSource.CreateLinkedTokenSource( + serviceDisposeToken, + this.cancellationToken); + wrap = await this.immediateLoadFunction(cts.Token); + } + else + { + wrap = await this.immediateLoadFunction(serviceDisposeToken); + } + + if (!this.taskCompletionSource.TrySetResult(wrap)) + wrap.Dispose(); + } + catch (Exception e) + { + this.taskCompletionSource.TrySetException(e); + _ = this.taskCompletionSource.Task.Exception; + } } } } diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index 6ad768e76..0d0d3a835 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -219,7 +219,7 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid public Task CreateFromImageAsync( ReadOnlyMemory bytes, CancellationToken cancellationToken = default) => - this.textureLoadThrottler.CreateLoader( + this.textureLoadThrottler.LoadTextureAsync( new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), ct => Task.Run(() => this.NoThrottleCreateFromImage(bytes.ToArray()), ct), cancellationToken); @@ -229,7 +229,7 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid Stream stream, bool leaveOpen = false, CancellationToken cancellationToken = default) => - this.textureLoadThrottler.CreateLoader( + this.textureLoadThrottler.LoadTextureAsync( new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), async ct => { @@ -300,7 +300,7 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid RawImageSpecification specs, ReadOnlyMemory bytes, CancellationToken cancellationToken = default) => - this.textureLoadThrottler.CreateLoader( + this.textureLoadThrottler.LoadTextureAsync( new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), _ => Task.FromResult(this.CreateFromRaw(specs, bytes.Span)), cancellationToken); @@ -311,7 +311,7 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid Stream stream, bool leaveOpen = false, CancellationToken cancellationToken = default) => - this.textureLoadThrottler.CreateLoader( + this.textureLoadThrottler.LoadTextureAsync( new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), async ct => { @@ -337,7 +337,7 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid public Task CreateFromTexFileAsync( TexFile file, CancellationToken cancellationToken = default) => - this.textureLoadThrottler.CreateLoader( + this.textureLoadThrottler.LoadTextureAsync( new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), ct => Task.Run(() => this.NoThrottleCreateFromTexFile(file), ct), cancellationToken); From c9f613931ff4f95b082ec99005da458b3fdcb97f Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 1 Mar 2024 01:49:32 +0900 Subject: [PATCH 26/57] supply Channel create params --- Dalamud/Interface/Internal/TextureLoadThrottler.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Dalamud/Interface/Internal/TextureLoadThrottler.cs b/Dalamud/Interface/Internal/TextureLoadThrottler.cs index 978d7b9b7..043906782 100644 --- a/Dalamud/Interface/Internal/TextureLoadThrottler.cs +++ b/Dalamud/Interface/Internal/TextureLoadThrottler.cs @@ -16,8 +16,8 @@ internal class TextureLoadThrottler : IServiceType, IDisposable private readonly Task adderTask; private readonly Task[] workerTasks; - private readonly Channel newItemChannel = Channel.CreateUnbounded(); - private readonly Channel workTokenChannel = Channel.CreateUnbounded(); + private readonly Channel newItemChannel; + private readonly Channel workTokenChannel; private readonly List workItemPending = new(); private bool disposing; @@ -25,6 +25,9 @@ internal class TextureLoadThrottler : IServiceType, IDisposable [ServiceManager.ServiceConstructor] private TextureLoadThrottler() { + 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)]; foreach (ref var task in this.workerTasks.AsSpan()) From 8fa8ca43b67fc0530784ead5819aef775881231f Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 1 Mar 2024 09:47:27 +0900 Subject: [PATCH 27/57] Rename ISharedImmediateTexture methods --- Dalamud/Interface/ISharedImmediateTexture.cs | 11 ++++--- .../Internal/DisposedDalamudTextureWrap.cs | 31 ------------------- .../SharedImmediateTexture.cs | 4 +-- .../Windows/Data/Widgets/TexWidget.cs | 10 +++--- 4 files changed, 13 insertions(+), 43 deletions(-) delete mode 100644 Dalamud/Interface/Internal/DisposedDalamudTextureWrap.cs diff --git a/Dalamud/Interface/ISharedImmediateTexture.cs b/Dalamud/Interface/ISharedImmediateTexture.cs index d2b22b877..f6c63ee10 100644 --- a/Dalamud/Interface/ISharedImmediateTexture.cs +++ b/Dalamud/Interface/ISharedImmediateTexture.cs @@ -11,12 +11,12 @@ namespace Dalamud.Interface; /// requesters. /// /// Calling on this interface is a no-op. -/// and may stop returning the intended texture at any point. +/// and may stop returning the intended texture at any point. /// Use to lock the texture for use in any thread for any duration. /// public interface ISharedImmediateTexture { - /// Gets the texture for use with the current frame. + /// Gets the texture for use with the current frame, or an empty texture if unavailable. /// An instance of that is guaranteed to be available for the current /// frame being drawn. /// @@ -26,9 +26,10 @@ public interface ISharedImmediateTexture /// If the texture is unavailable for any reason, then the returned instance of /// will point to an empty texture instead. /// - IDalamudTextureWrap GetWrap(); + IDalamudTextureWrap GetWrapOrEmpty(); - /// Gets the texture for use with the current frame. + /// Gets the texture for use with the current frame, or a default value specified via + /// if unavailable. /// The default wrap to return if the requested texture was not immediately available. /// /// An instance of that is guaranteed to be available for the current @@ -40,7 +41,7 @@ public interface ISharedImmediateTexture /// If the texture is unavailable for any reason, then will be returned. /// [return: NotNullIfNotNull(nameof(defaultWrap))] - IDalamudTextureWrap? GetWrap(IDalamudTextureWrap? defaultWrap); + IDalamudTextureWrap? GetWrapOrDefault(IDalamudTextureWrap? defaultWrap = null); /// Attempts to get the texture for use with the current frame. /// An instance of that is guaranteed to be available for diff --git a/Dalamud/Interface/Internal/DisposedDalamudTextureWrap.cs b/Dalamud/Interface/Internal/DisposedDalamudTextureWrap.cs deleted file mode 100644 index 904a2ccb8..000000000 --- a/Dalamud/Interface/Internal/DisposedDalamudTextureWrap.cs +++ /dev/null @@ -1,31 +0,0 @@ -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/SharedImmediateTextures/SharedImmediateTexture.cs b/Dalamud/Interface/Internal/SharedImmediateTextures/SharedImmediateTexture.cs index d4df6a85b..426c61b2c 100644 --- a/Dalamud/Interface/Internal/SharedImmediateTextures/SharedImmediateTexture.cs +++ b/Dalamud/Interface/Internal/SharedImmediateTextures/SharedImmediateTexture.cs @@ -174,12 +174,12 @@ internal abstract class SharedImmediateTexture /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public IDalamudTextureWrap GetWrap() => this.GetWrap(Service.Get().Empty4X4); + public IDalamudTextureWrap GetWrapOrEmpty() => this.GetWrapOrDefault(Service.Get().Empty4X4); /// [return: NotNullIfNotNull(nameof(defaultWrap))] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public IDalamudTextureWrap? GetWrap(IDalamudTextureWrap? defaultWrap) + public IDalamudTextureWrap? GetWrapOrDefault(IDalamudTextureWrap? defaultWrap) { if (!this.TryGetWrap(out var texture, out _)) texture = null; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs index 3fdf72cda..4254b9082 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -273,7 +273,7 @@ internal class TexWidget : IDataWindowWidget ImGui.TableNextColumn(); ImGuiComponents.IconButton(FontAwesomeIcon.Image); - if (ImGui.IsItemHovered() && texture.GetWrap(null) is { } immediate) + if (ImGui.IsItemHovered() && texture.GetWrapOrDefault(null) is { } immediate) { ImGui.BeginTooltip(); ImGui.Image(immediate.ImGuiHandle, immediate.Size); @@ -556,16 +556,16 @@ internal class TexWidget : IDataWindowWidget if (this.Api10 is not null) return this.Api10.IsCompletedSuccessfully ? this.Api10.Result : null; if (this.Api10ImmGameIcon is not null) - return tp.GetFromGameIcon(this.Api10ImmGameIcon.Value).GetWrap(); + return tp.GetFromGameIcon(this.Api10ImmGameIcon.Value).GetWrapOrEmpty(); if (this.Api10ImmGamePath is not null) - return tp.GetFromGame(this.Api10ImmGamePath).GetWrap(); + return tp.GetFromGame(this.Api10ImmGamePath).GetWrapOrEmpty(); if (this.Api10ImmFile is not null) - return tp.GetFromFile(this.Api10ImmFile).GetWrap(); + return tp.GetFromFile(this.Api10ImmFile).GetWrapOrEmpty(); if (this.Api10ImmManifestResource is not null) { return tp.GetFromManifestResource( this.Api10ImmManifestResource.Value.Assembly, - this.Api10ImmManifestResource.Value.Name).GetWrap(); + this.Api10ImmManifestResource.Value.Name).GetWrapOrEmpty(); } return null; From 111029e9bc3f04e7dac586017723a4590772bde5 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 1 Mar 2024 10:01:51 +0900 Subject: [PATCH 28/57] Fix inconsistency --- Dalamud/Interface/Internal/TextureManager.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index 0d0d3a835..52ddd126e 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -235,7 +235,7 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid { await using var ms = stream.CanSeek ? new MemoryStream((int)stream.Length) : new(); await stream.CopyToAsync(ms, ct).ConfigureAwait(false); - return await this.CreateFromImageAsync(ms.GetBuffer(), ct); + return this.NoThrottleCreateFromImage(ms.GetBuffer()); }, cancellationToken) .ContinueWith( @@ -249,6 +249,10 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid .Unwrap(); /// + // NOTE: if this function is changed to be placed under the effect of the throttler, then look for the usages of + // this function, and for the usages that are used as a part of the delegate passed to LoadTextureAsync, change them + // to create texture in a non-throttling way; otherwise, recursive throttled texture load call will happen, and it + // may deadlock. public IDalamudTextureWrap CreateFromRaw( RawImageSpecification specs, ReadOnlySpan bytes) @@ -317,7 +321,7 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid { await using var ms = stream.CanSeek ? new MemoryStream((int)stream.Length) : new(); await stream.CopyToAsync(ms, ct).ConfigureAwait(false); - return await this.CreateFromRawAsync(specs, ms.GetBuffer(), ct); + return this.CreateFromRaw(specs, ms.GetBuffer().AsSpan(0, (int)ms.Length)); }, cancellationToken) .ContinueWith( From c86be312555551d4ffacce63952ba1c08d8f54f9 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 1 Mar 2024 10:09:04 +0900 Subject: [PATCH 29/57] cleanup --- Dalamud/Interface/Internal/TextureManager.cs | 102 ++++++++++--------- 1 file changed, 52 insertions(+), 50 deletions(-) diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index 52ddd126e..8d80a08d0 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -249,55 +249,10 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid .Unwrap(); /// - // NOTE: if this function is changed to be placed under the effect of the throttler, then look for the usages of - // this function, and for the usages that are used as a part of the delegate passed to LoadTextureAsync, change them - // to create texture in a non-throttling way; otherwise, recursive throttled texture load call will happen, and it - // may deadlock. + // It probably doesn't make sense to throttle this, as it copies the passed bytes to GPU without any transformation. public IDalamudTextureWrap CreateFromRaw( RawImageSpecification specs, - ReadOnlySpan bytes) - { - if (this.interfaceManager.Scene is not { } scene) - { - _ = Service.Get(); - scene = this.interfaceManager.Scene ?? throw new InvalidOperationException(); - } - - ShaderResourceView resView; - unsafe - { - fixed (void* pData = bytes) - { - var texDesc = new Texture2DDescription - { - Width = specs.Width, - Height = specs.Height, - MipLevels = 1, - ArraySize = 1, - Format = (Format)specs.DxgiFormat, - SampleDescription = new(1, 0), - Usage = ResourceUsage.Immutable, - BindFlags = BindFlags.ShaderResource, - CpuAccessFlags = CpuAccessFlags.None, - OptionFlags = ResourceOptionFlags.None, - }; - - using var texture = new Texture2D(scene.Device, texDesc, new DataRectangle(new(pData), specs.Pitch)); - resView = new( - scene.Device, - texture, - new() - { - Format = texDesc.Format, - Dimension = ShaderResourceViewDimension.Texture2D, - Texture2D = { MipLevels = texDesc.MipLevels }, - }); - } - } - - // no sampler for now because the ImGui implementation we copied doesn't allow for changing it - return new DalamudTextureWrap(new D3DTextureWrap(resView, specs.Width, specs.Height)); - } + ReadOnlySpan bytes) => this.NoThrottleCreateFromRaw(specs, bytes); /// public Task CreateFromRawAsync( @@ -306,7 +261,7 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid CancellationToken cancellationToken = default) => this.textureLoadThrottler.LoadTextureAsync( new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), - _ => Task.FromResult(this.CreateFromRaw(specs, bytes.Span)), + _ => Task.FromResult(this.NoThrottleCreateFromRaw(specs, bytes.Span)), cancellationToken); /// @@ -321,7 +276,7 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid { await using var ms = stream.CanSeek ? new MemoryStream((int)stream.Length) : new(); await stream.CopyToAsync(ms, ct).ConfigureAwait(false); - return this.CreateFromRaw(specs, ms.GetBuffer().AsSpan(0, (int)ms.Length)); + return this.NoThrottleCreateFromRaw(specs, ms.GetBuffer().AsSpan(0, (int)ms.Length)); }, cancellationToken) .ContinueWith( @@ -510,6 +465,53 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid ?? throw texFileAttemptException ?? new("Failed to load image because of an unknown reason.")); } + /// + internal IDalamudTextureWrap NoThrottleCreateFromRaw( + RawImageSpecification specs, + ReadOnlySpan bytes) + { + if (this.interfaceManager.Scene is not { } scene) + { + _ = Service.Get(); + scene = this.interfaceManager.Scene ?? throw new InvalidOperationException(); + } + + ShaderResourceView resView; + unsafe + { + fixed (void* pData = bytes) + { + var texDesc = new Texture2DDescription + { + Width = specs.Width, + Height = specs.Height, + MipLevels = 1, + ArraySize = 1, + Format = (Format)specs.DxgiFormat, + SampleDescription = new(1, 0), + Usage = ResourceUsage.Immutable, + BindFlags = BindFlags.ShaderResource, + CpuAccessFlags = CpuAccessFlags.None, + OptionFlags = ResourceOptionFlags.None, + }; + + using var texture = new Texture2D(scene.Device, texDesc, new DataRectangle(new(pData), specs.Pitch)); + resView = new( + scene.Device, + texture, + new() + { + Format = texDesc.Format, + Dimension = ShaderResourceViewDimension.Texture2D, + Texture2D = { MipLevels = texDesc.MipLevels }, + }); + } + } + + // no sampler for now because the ImGui implementation we copied doesn't allow for changing it + return new DalamudTextureWrap(new D3DTextureWrap(resView, specs.Width, specs.Height)); + } + /// Creates a texture from the given . Skips the load throttler; intended to be used /// from implementation of s. /// The data. @@ -526,7 +528,7 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid buffer = buffer.Filter(0, 0, TexFile.TextureFormat.B8G8R8A8); } - return this.CreateFromRaw( + return this.NoThrottleCreateFromRaw( RawImageSpecification.From(buffer.Width, buffer.Height, dxgiFormat), buffer.RawData); } From 1ae11440aaf4c327bba3e9acb1727d2e89c51d99 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sat, 2 Mar 2024 01:41:11 +0900 Subject: [PATCH 30/57] Add ITextureProvider.CreateFromExistingTextureAsync --- .../Interface/Internal/InterfaceManager.cs | 9 + .../Internal/TextureManager.FormatConvert.cs | 484 ++++++++++++++++++ Dalamud/Interface/Internal/TextureManager.cs | 20 +- .../Windows/Data/Widgets/TexWidget.cs | 75 ++- .../FontAtlasFactory.BuildToolkit.cs | 4 +- .../Internals/FontAtlasFactory.cs | 4 +- Dalamud/Plugin/Services/ITextureProvider.cs | 29 ++ Dalamud/Utility/Util.cs | 9 + 8 files changed, 612 insertions(+), 22 deletions(-) create mode 100644 Dalamud/Interface/Internal/TextureManager.FormatConvert.cs diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 7e9c8eed0..68a65ebd1 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -70,6 +70,8 @@ internal class InterfaceManager : IDisposable, IServiceType [ServiceManager.ServiceDependency] private readonly DalamudIme dalamudIme = Service.Get(); + private readonly ConcurrentQueue runBeforePresent = new(); + private readonly SwapChainVtableResolver address = new(); private readonly Hook setCursorHook; private RawDX11Scene? scene; @@ -283,6 +285,10 @@ internal class InterfaceManager : IDisposable, IServiceType this.deferredDisposeDisposables.Add(locked); } + /// Queues an action to be run before Present call. + /// The action. + public void RunBeforePresent(Action action) => this.runBeforePresent.Enqueue(action); + /// /// Get video memory information. /// @@ -520,6 +526,9 @@ internal class InterfaceManager : IDisposable, IServiceType if (!this.dalamudAtlas!.HasBuiltAtlas) return this.presentHook!.Original(swapChain, syncInterval, presentFlags); + while (this.runBeforePresent.TryDequeue(out var action)) + action.InvokeSafely(); + if (this.address.IsReshade) { var pRes = this.presentHook!.Original(swapChain, syncInterval, presentFlags); diff --git a/Dalamud/Interface/Internal/TextureManager.FormatConvert.cs b/Dalamud/Interface/Internal/TextureManager.FormatConvert.cs new file mode 100644 index 000000000..a11cf6c53 --- /dev/null +++ b/Dalamud/Interface/Internal/TextureManager.FormatConvert.cs @@ -0,0 +1,484 @@ +using System.Buffers; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Plugin.Services; +using Dalamud.Utility; + +using ImGuiNET; + +using SharpDX.Direct3D11; +using SharpDX.DXGI; + +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +namespace Dalamud.Interface.Internal; + +/// Service responsible for loading and disposing ImGui texture wraps. +internal sealed partial class TextureManager +{ + private DrawsOneSquare? drawsOneSquare; + + /// + bool ITextureProvider.IsDxgiFormatSupportedForCreateFromExistingTextureAsync(int dxgiFormat) => + this.IsDxgiFormatSupportedForCreateFromExistingTextureAsync((DXGI_FORMAT)dxgiFormat); + + /// + public bool IsDxgiFormatSupportedForCreateFromExistingTextureAsync(DXGI_FORMAT dxgiFormat) + { + if (this.interfaceManager.Scene is not { } scene) + { + _ = Service.Get(); + scene = this.interfaceManager.Scene ?? throw new InvalidOperationException(); + } + + var format = (Format)dxgiFormat; + var support = scene.Device.CheckFormatSupport(format); + return (support & FormatSupport.RenderTarget) != 0 + && (support & FormatSupport.Texture2D) != 0; + } + + /// + Task ITextureProvider.CreateFromExistingTextureAsync( + IDalamudTextureWrap wrap, + Vector2 uv0, + Vector2 uv1, + int dxgiFormat, + CancellationToken cancellationToken) => + this.CreateFromExistingTextureAsync(wrap, uv0, uv1, (DXGI_FORMAT)dxgiFormat, cancellationToken); + + /// + public Task CreateFromExistingTextureAsync( + IDalamudTextureWrap wrap, + Vector2 uv0, + Vector2 uv1, + DXGI_FORMAT format, + CancellationToken cancellationToken = default) + { + var wrapCopy = wrap.CreateWrapSharingLowLevelResource(); + return this.textureLoadThrottler.LoadTextureAsync( + new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), + ct => + { + var tcs = new TaskCompletionSource(); + this.interfaceManager.RunBeforePresent( + () => + { + try + { + ct.ThrowIfCancellationRequested(); + unsafe + { + using var tex = new ComPtr( + this.NoThrottleCreateFromExistingTextureCore( + wrapCopy, + uv0, + uv1, + format, + false)); + + using var device = default(ComPtr); + tex.Get()->GetDevice(device.GetAddressOf()); + + using var srv = default(ComPtr); + var srvDesc = new D3D11_SHADER_RESOURCE_VIEW_DESC( + tex, + D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE2D); + device.Get()->CreateShaderResourceView( + (ID3D11Resource*)tex.Get(), + &srvDesc, + srv.GetAddressOf()) + .ThrowOnError(); + + var desc = default(D3D11_TEXTURE2D_DESC); + tex.Get()->GetDesc(&desc); + + tcs.SetResult( + new UnknownTextureWrap( + (IUnknown*)srv.Get(), + (int)desc.Width, + (int)desc.Height, + true)); + } + } + catch (Exception e) + { + tcs.SetException(e); + } + }); + + return tcs.Task; + }, + cancellationToken) + .ContinueWith( + r => + { + wrapCopy.Dispose(); + return r; + }, + default(CancellationToken)) + .Unwrap(); + } + + private unsafe ID3D11Texture2D* NoThrottleCreateFromExistingTextureCore( + IDalamudTextureWrap wrap, + Vector2 uv0, + Vector2 uv1, + DXGI_FORMAT format, + bool enableCpuRead) + { + ThreadSafety.AssertMainThread(); + + using var resUnk = new ComPtr((IUnknown*)wrap.ImGuiHandle); + + using var texSrv = default(ComPtr); + resUnk.As(&texSrv).ThrowOnError(); + + using var device = default(ComPtr); + texSrv.Get()->GetDevice(device.GetAddressOf()); + + using var deviceContext = default(ComPtr); + device.Get()->GetImmediateContext(deviceContext.GetAddressOf()); + + using var tex2D = default(ComPtr); + using (var texRes = default(ComPtr)) + { + texSrv.Get()->GetResource(texRes.GetAddressOf()); + texRes.As(&tex2D).ThrowOnError(); + } + + var texDesc = default(D3D11_TEXTURE2D_DESC); + tex2D.Get()->GetDesc(&texDesc); + + using var tex2DCopyTemp = default(ComPtr); + var tex2DCopyTempDesc = new D3D11_TEXTURE2D_DESC + { + Width = checked((uint)MathF.Round((uv1.X - uv0.X) * wrap.Width)), + Height = checked((uint)MathF.Round((uv1.Y - uv0.Y) * wrap.Height)), + MipLevels = 1, + ArraySize = 1, + Format = format, + SampleDesc = new(1, 0), + Usage = D3D11_USAGE.D3D11_USAGE_DEFAULT, + BindFlags = (uint)(D3D11_BIND_FLAG.D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_FLAG.D3D11_BIND_RENDER_TARGET), + CPUAccessFlags = 0u, + MiscFlags = 0u, + }; + device.Get()->CreateTexture2D(&tex2DCopyTempDesc, null, tex2DCopyTemp.GetAddressOf()).ThrowOnError(); + + using (var rtvCopyTemp = default(ComPtr)) + { + var rtvCopyTempDesc = new D3D11_RENDER_TARGET_VIEW_DESC( + tex2DCopyTemp, + D3D11_RTV_DIMENSION.D3D11_RTV_DIMENSION_TEXTURE2D); + device.Get()->CreateRenderTargetView( + (ID3D11Resource*)tex2DCopyTemp.Get(), + &rtvCopyTempDesc, + rtvCopyTemp.GetAddressOf()).ThrowOnError(); + + this.drawsOneSquare ??= new(); + this.drawsOneSquare.Setup(device.Get()); + + deviceContext.Get()->OMSetRenderTargets(1u, rtvCopyTemp.GetAddressOf(), null); + this.drawsOneSquare.Draw( + deviceContext.Get(), + texSrv.Get(), + (int)tex2DCopyTempDesc.Width, + (int)tex2DCopyTempDesc.Height, + uv0, + uv1); + deviceContext.Get()->OMSetRenderTargets(0, null, null); + } + + if (!enableCpuRead) + { + tex2DCopyTemp.Get()->AddRef(); + return tex2DCopyTemp.Get(); + } + + using var tex2DTarget = default(ComPtr); + var tex2DTargetDesc = tex2DCopyTempDesc with + { + Usage = D3D11_USAGE.D3D11_USAGE_DYNAMIC, + BindFlags = 0u, + CPUAccessFlags = (uint)D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_READ, + }; + device.Get()->CreateTexture2D(&tex2DTargetDesc, null, tex2DTarget.GetAddressOf()).ThrowOnError(); + + deviceContext.Get()->CopyResource((ID3D11Resource*)tex2DTarget.Get(), (ID3D11Resource*)tex2DCopyTemp.Get()); + + tex2DTarget.Get()->AddRef(); + return tex2DTarget.Get(); + } + + [SuppressMessage( + "StyleCop.CSharp.LayoutRules", + "SA1519:Braces should not be omitted from multi-line child statement", + Justification = "Multiple fixed blocks")] + private sealed unsafe class DrawsOneSquare : IDisposable + { + private ComPtr sampler; + private ComPtr vertexShader; + private ComPtr pixelShader; + private ComPtr inputLayout; + private ComPtr vertexConstantBuffer; + private ComPtr blendState; + private ComPtr rasterizerState; + private ComPtr vertexBufferFill; + private ComPtr vertexBufferMutable; + private ComPtr indexBuffer; + + ~DrawsOneSquare() => this.Dispose(); + + public void Dispose() + { + this.sampler.Reset(); + this.vertexShader.Reset(); + this.pixelShader.Reset(); + this.inputLayout.Reset(); + this.vertexConstantBuffer.Reset(); + this.blendState.Reset(); + this.rasterizerState.Reset(); + this.vertexBufferFill.Reset(); + this.vertexBufferMutable.Reset(); + this.indexBuffer.Reset(); + } + + public void Setup(T* device) where T : unmanaged, ID3D11Device.Interface + { + var assembly = typeof(ImGuiScene.ImGui_Impl_DX11).Assembly; + + // Create the vertex shader + if (this.vertexShader.IsEmpty() || this.inputLayout.IsEmpty()) + { + this.vertexShader.Reset(); + this.inputLayout.Reset(); + + using var stream = assembly.GetManifestResourceStream("imgui-vertex.hlsl.bytes")!; + var array = ArrayPool.Shared.Rent((int)stream.Length); + stream.ReadExactly(array, 0, (int)stream.Length); + fixed (byte* pArray = array) + fixed (ID3D11VertexShader** ppShader = &this.vertexShader.GetPinnableReference()) + fixed (ID3D11InputLayout** ppInputLayout = &this.inputLayout.GetPinnableReference()) + fixed (void* pszPosition = "POSITION"u8) + fixed (void* pszTexCoord = "TEXCOORD"u8) + fixed (void* pszColor = "COLOR"u8) + { + device->CreateVertexShader(pArray, (nuint)stream.Length, null, ppShader).ThrowOnError(); + + var ied = stackalloc D3D11_INPUT_ELEMENT_DESC[] + { + new() + { + SemanticName = (sbyte*)pszPosition, + Format = DXGI_FORMAT.DXGI_FORMAT_R32G32_FLOAT, + AlignedByteOffset = uint.MaxValue, + }, + new() + { + SemanticName = (sbyte*)pszTexCoord, + Format = DXGI_FORMAT.DXGI_FORMAT_R32G32_FLOAT, + AlignedByteOffset = uint.MaxValue, + }, + new() + { + SemanticName = (sbyte*)pszColor, + Format = DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM, + AlignedByteOffset = uint.MaxValue, + }, + }; + device->CreateInputLayout(ied, 3, pArray, (nuint)stream.Length, ppInputLayout).ThrowOnError(); + } + + ArrayPool.Shared.Return(array); + } + + // Create the constant buffer + if (this.vertexConstantBuffer.IsEmpty()) + { + var bufferDesc = new D3D11_BUFFER_DESC( + (uint)sizeof(Matrix4x4), + (uint)D3D11_BIND_FLAG.D3D11_BIND_CONSTANT_BUFFER, + D3D11_USAGE.D3D11_USAGE_IMMUTABLE); + var data = Matrix4x4.Identity; + var subr = new D3D11_SUBRESOURCE_DATA { pSysMem = &data }; + fixed (ID3D11Buffer** ppBuffer = &this.vertexConstantBuffer.GetPinnableReference()) + device->CreateBuffer(&bufferDesc, &subr, ppBuffer).ThrowOnError(); + } + + // Create the pixel shader + if (this.pixelShader.IsEmpty()) + { + using var stream = assembly.GetManifestResourceStream("imgui-frag.hlsl.bytes")!; + var array = ArrayPool.Shared.Rent((int)stream.Length); + stream.ReadExactly(array, 0, (int)stream.Length); + fixed (byte* pArray = array) + fixed (ID3D11PixelShader** ppShader = &this.pixelShader.GetPinnableReference()) + device->CreatePixelShader(pArray, (nuint)stream.Length, null, ppShader).ThrowOnError(); + + ArrayPool.Shared.Return(array); + } + + // Create the blending setup + if (this.blendState.IsEmpty()) + { + var blendStateDesc = new D3D11_BLEND_DESC + { + RenderTarget = + { + e0 = + { + BlendEnable = true, + SrcBlend = D3D11_BLEND.D3D11_BLEND_SRC_ALPHA, + DestBlend = D3D11_BLEND.D3D11_BLEND_INV_SRC_ALPHA, + BlendOp = D3D11_BLEND_OP.D3D11_BLEND_OP_ADD, + SrcBlendAlpha = D3D11_BLEND.D3D11_BLEND_INV_DEST_ALPHA, + DestBlendAlpha = D3D11_BLEND.D3D11_BLEND_ONE, + BlendOpAlpha = D3D11_BLEND_OP.D3D11_BLEND_OP_ADD, + RenderTargetWriteMask = (byte)D3D11_COLOR_WRITE_ENABLE.D3D11_COLOR_WRITE_ENABLE_ALL, + }, + }, + }; + fixed (ID3D11BlendState** ppBlendState = &this.blendState.GetPinnableReference()) + device->CreateBlendState(&blendStateDesc, ppBlendState).ThrowOnError(); + } + + // Create the rasterizer state + if (this.rasterizerState.IsEmpty()) + { + var rasterizerDesc = new D3D11_RASTERIZER_DESC + { + FillMode = D3D11_FILL_MODE.D3D11_FILL_SOLID, + CullMode = D3D11_CULL_MODE.D3D11_CULL_NONE, + }; + fixed (ID3D11RasterizerState** ppRasterizerState = &this.rasterizerState.GetPinnableReference()) + device->CreateRasterizerState(&rasterizerDesc, ppRasterizerState).ThrowOnError(); + } + + // Create the font sampler + if (this.sampler.IsEmpty()) + { + var samplerDesc = new D3D11_SAMPLER_DESC( + D3D11_FILTER.D3D11_FILTER_MIN_MAG_MIP_LINEAR, + D3D11_TEXTURE_ADDRESS_MODE.D3D11_TEXTURE_ADDRESS_WRAP, + D3D11_TEXTURE_ADDRESS_MODE.D3D11_TEXTURE_ADDRESS_WRAP, + D3D11_TEXTURE_ADDRESS_MODE.D3D11_TEXTURE_ADDRESS_WRAP, + 0f, + 0, + D3D11_COMPARISON_FUNC.D3D11_COMPARISON_ALWAYS, + null, + 0, + 0); + fixed (ID3D11SamplerState** ppSampler = &this.sampler.GetPinnableReference()) + device->CreateSamplerState(&samplerDesc, ppSampler).ThrowOnError(); + } + + if (this.vertexBufferFill.IsEmpty()) + { + var data = stackalloc ImDrawVert[] + { + new() { col = uint.MaxValue, pos = new(-1, 1), uv = new(0, 0) }, + new() { col = uint.MaxValue, pos = new(-1, -1), uv = new(0, 1) }, + new() { col = uint.MaxValue, pos = new(1, 1), uv = new(1, 0) }, + new() { col = uint.MaxValue, pos = new(1, -1), uv = new(1, 1) }, + }; + var desc = new D3D11_BUFFER_DESC( + (uint)(sizeof(ImDrawVert) * 4), + (uint)D3D11_BIND_FLAG.D3D11_BIND_VERTEX_BUFFER, + D3D11_USAGE.D3D11_USAGE_IMMUTABLE); + var subr = new D3D11_SUBRESOURCE_DATA { pSysMem = data }; + var buffer = default(ID3D11Buffer*); + device->CreateBuffer(&desc, &subr, &buffer).ThrowOnError(); + this.vertexBufferFill.Attach(buffer); + } + + if (this.vertexBufferMutable.IsEmpty()) + { + var desc = new D3D11_BUFFER_DESC( + (uint)(sizeof(ImDrawVert) * 4), + (uint)D3D11_BIND_FLAG.D3D11_BIND_VERTEX_BUFFER, + D3D11_USAGE.D3D11_USAGE_DYNAMIC, + (uint)D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_WRITE); + var buffer = default(ID3D11Buffer*); + device->CreateBuffer(&desc, null, &buffer).ThrowOnError(); + this.vertexBufferMutable.Attach(buffer); + } + + if (this.indexBuffer.IsEmpty()) + { + var data = stackalloc ushort[] { 0, 1, 2, 1, 2, 3 }; + var desc = new D3D11_BUFFER_DESC( + sizeof(ushort) * 6, + (uint)D3D11_BIND_FLAG.D3D11_BIND_INDEX_BUFFER, + D3D11_USAGE.D3D11_USAGE_IMMUTABLE); + var subr = new D3D11_SUBRESOURCE_DATA { pSysMem = data }; + var buffer = default(ID3D11Buffer*); + device->CreateBuffer(&desc, &subr, &buffer).ThrowOnError(); + this.indexBuffer.Attach(buffer); + } + } + + public void Draw( + ID3D11DeviceContext* ctx, + ID3D11ShaderResourceView* srv, + int width, + int height, + Vector2 uv0, + Vector2 uv1) + { + ID3D11Buffer* buffer; + if (uv0 == Vector2.Zero && uv1 == Vector2.One) + { + buffer = this.vertexBufferFill.Get(); + } + else + { + buffer = this.vertexBufferMutable.Get(); + var mapped = default(D3D11_MAPPED_SUBRESOURCE); + ctx->Map((ID3D11Resource*)buffer, 0, D3D11_MAP.D3D11_MAP_WRITE_DISCARD, 0u, &mapped).ThrowOnError(); + _ = new Span(mapped.pData, 4) + { + [0] = new() { col = uint.MaxValue, pos = new(-1, 1), uv = uv0 }, + [1] = new() { col = uint.MaxValue, pos = new(-1, -1), uv = new(uv0.X, uv1.Y) }, + [2] = new() { col = uint.MaxValue, pos = new(1, 1), uv = new(uv1.X, uv0.Y) }, + [3] = new() { col = uint.MaxValue, pos = new(1, -1), uv = uv1 }, + }; + ctx->Unmap((ID3D11Resource*)buffer, 0u); + } + + var stride = (uint)sizeof(ImDrawVert); + var offset = 0u; + + ctx->IASetInputLayout(this.inputLayout); + ctx->IASetVertexBuffers(0, 1, &buffer, &stride, &offset); + ctx->IASetIndexBuffer(this.indexBuffer, DXGI_FORMAT.DXGI_FORMAT_R16_UINT, 0); + ctx->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY.D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + + var viewport = new D3D11_VIEWPORT(0, 0, width, height); + ctx->RSSetState(this.rasterizerState); + ctx->RSSetViewports(1, &viewport); + + var blendColor = default(Vector4); + ctx->OMSetBlendState(this.blendState, (float*)&blendColor, 0xffffffff); + ctx->OMSetDepthStencilState(null, 0); + + ctx->VSSetShader(this.vertexShader.Get(), null, 0); + buffer = this.vertexConstantBuffer.Get(); + ctx->VSSetConstantBuffers(0, 1, &buffer); + + ctx->PSSetShader(this.pixelShader, null, 0); + var simp = this.sampler.Get(); + ctx->PSSetSamplers(0, 1, &simp); + ctx->PSSetShaderResources(0, 1, &srv); + + ctx->GSSetShader(null, null, 0); + ctx->HSSetShader(null, null, 0); + ctx->DSSetShader(null, null, 0); + ctx->CSSetShader(null, null, 0); + ctx->DrawIndexed(6, 0, 0); + } + } +} diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index 8d80a08d0..52253ff3c 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -26,6 +26,8 @@ using SharpDX.Direct3D; using SharpDX.Direct3D11; using SharpDX.DXGI; +using TerraFX.Interop.DirectX; + namespace Dalamud.Interface.Internal; /// Service responsible for loading and disposing ImGui texture wraps. @@ -36,7 +38,7 @@ namespace Dalamud.Interface.Internal; [ResolveVia] [ResolveVia] #pragma warning restore SA1015 -internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvider, ITextureSubstitutionProvider +internal sealed partial class TextureManager : IServiceType, IDisposable, ITextureProvider, ITextureSubstitutionProvider { private const int PathLookupLruCount = 8192; @@ -109,6 +111,9 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid ReleaseSelfReferences(this.manifestResourceTextures); this.lookupToPath.Clear(); + this.drawsOneSquare?.Dispose(); + this.drawsOneSquare = null; + return; static void ReleaseSelfReferences(ConcurrentDictionary dict) @@ -302,7 +307,11 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid cancellationToken); /// - public bool IsDxgiFormatSupported(int dxgiFormat) + bool ITextureProvider.IsDxgiFormatSupported(int dxgiFormat) => + this.IsDxgiFormatSupported((DXGI_FORMAT)dxgiFormat); + + /// + public bool IsDxgiFormatSupported(DXGI_FORMAT dxgiFormat) { if (this.interfaceManager.Scene is not { } scene) { @@ -310,7 +319,9 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid scene = this.interfaceManager.Scene ?? throw new InvalidOperationException(); } - return scene.Device.CheckFormatSupport((Format)dxgiFormat).HasFlag(FormatSupport.Texture2D); + var format = (Format)dxgiFormat; + var support = scene.Device.CheckFormatSupport(format); + return (support & FormatSupport.Texture2D) != 0; } /// @@ -522,7 +533,8 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid var buffer = file.TextureBuffer; var (dxgiFormat, conversion) = TexFile.GetDxgiFormatFromTextureFormat(file.Header.Format, false); - if (conversion != TexFile.DxgiFormatConversion.NoConversion || !this.IsDxgiFormatSupported(dxgiFormat)) + if (conversion != TexFile.DxgiFormatConversion.NoConversion || + !this.IsDxgiFormatSupported((DXGI_FORMAT)dxgiFormat)) { dxgiFormat = (int)Format.B8G8R8A8_UNorm; buffer = buffer.Filter(0, 0, TexFile.TextureFormat.B8G8R8A8); diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs index 4254b9082..9a63dbcb9 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -16,6 +16,8 @@ using Dalamud.Utility; using ImGuiNET; +using TerraFX.Interop.DirectX; + namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// @@ -41,6 +43,10 @@ internal class TexWidget : IDataWindowWidget private Vector2 inputTexScale = Vector2.Zero; private TextureManager textureManager = null!; + private string[]? supportedRenderTargetFormatNames; + private DXGI_FORMAT[]? supportedRenderTargetFormats; + private int renderTargetChoiceInt; + /// public string[]? CommandShortcuts { get; init; } = { "tex", "texture" }; @@ -66,6 +72,8 @@ internal class TexWidget : IDataWindowWidget this.inputManifestResourceAssemblyIndex = 0; this.inputManifestResourceNameCandidates = null; this.inputManifestResourceNameIndex = 0; + this.supportedRenderTargetFormats = null; + this.supportedRenderTargetFormatNames = null; this.Ready = true; } @@ -142,19 +150,63 @@ internal class TexWidget : IDataWindowWidget ImGui.PopID(); } - TextureEntry? toRemove = null; - TextureEntry? toCopy = null; + ImGui.Dummy(new(ImGui.GetTextLineHeightWithSpacing())); + + if (this.supportedRenderTargetFormats is null) + { + this.supportedRenderTargetFormatNames = null; + this.supportedRenderTargetFormats = + Enum.GetValues() + .Where(this.textureManager.IsDxgiFormatSupportedForCreateFromExistingTextureAsync) + .ToArray(); + this.renderTargetChoiceInt = 0; + } + + this.supportedRenderTargetFormatNames ??= this.supportedRenderTargetFormats.Select(Enum.GetName).ToArray(); + ImGui.Combo( + "CropCopy Format", + ref this.renderTargetChoiceInt, + this.supportedRenderTargetFormatNames, + this.supportedRenderTargetFormatNames.Length); + + Action? runLater = null; foreach (var t in this.addedTextures) { ImGui.PushID(t.Id); if (ImGui.CollapsingHeader($"Tex #{t.Id} {t}###header", ImGuiTreeNodeFlags.DefaultOpen)) { if (ImGui.Button("X")) - toRemove = t; + { + runLater = () => + { + t.Dispose(); + this.addedTextures.Remove(t); + }; + } ImGui.SameLine(); - if (ImGui.Button("Copy")) - toCopy = t; + if (ImGui.Button("Copy Reference")) + runLater = () => this.addedTextures.Add(t.CreateFromSharedLowLevelResource(this.textureManager)); + + ImGui.SameLine(); + if (ImGui.Button("CropCopy")) + { + runLater = () => + { + if (t.GetTexture(this.textureManager) is not { } source) + return; + if (this.supportedRenderTargetFormats is not { } supportedFormats) + return; + if (this.renderTargetChoiceInt < 0 || this.renderTargetChoiceInt >= supportedFormats.Length) + return; + var texTask = this.textureManager.CreateFromExistingTextureAsync( + source, + new(0.25f), + new(0.75f), + supportedFormats[this.renderTargetChoiceInt]); + this.addedTextures.Add(new() { Api10 = texTask }); + }; + } try { @@ -162,7 +214,7 @@ internal class TexWidget : IDataWindowWidget { var scale = new Vector2(tex.Width, tex.Height); if (this.inputTexScale != Vector2.Zero) - scale = this.inputTexScale; + scale *= this.inputTexScale; ImGui.Image(tex.ImGuiHandle, scale, this.inputTexUv0, this.inputTexUv1, this.inputTintCol); } @@ -180,16 +232,7 @@ internal class TexWidget : IDataWindowWidget ImGui.PopID(); } - if (toRemove != null) - { - toRemove.Dispose(); - this.addedTextures.Remove(toRemove); - } - - if (toCopy != null) - { - this.addedTextures.Add(toCopy.CreateFromSharedLowLevelResource(this.textureManager)); - } + runLater?.Invoke(); } private unsafe void DrawLoadedTextures(ICollection textures) diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs index 89a47c552..73530bf0e 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs @@ -18,6 +18,8 @@ using ImGuiNET; using SharpDX.DXGI; +using TerraFX.Interop.DirectX; + namespace Dalamud.Interface.ManagedFontAtlas.Internals; /// @@ -580,7 +582,7 @@ internal sealed partial class FontAtlasFactory var buf = Array.Empty(); try { - var use4 = this.factory.TextureManager.IsDxgiFormatSupported((int)Format.B4G4R4A4_UNorm); + var use4 = this.factory.TextureManager.IsDxgiFormatSupported(DXGI_FORMAT.DXGI_FORMAT_B4G4R4A4_UNORM); var bpp = use4 ? 2 : 4; var width = this.NewImAtlas.TexWidth; var height = this.NewImAtlas.TexHeight; diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs index 532e223fc..a632f14e4 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs @@ -24,6 +24,8 @@ using SharpDX; using SharpDX.Direct3D11; using SharpDX.DXGI; +using TerraFX.Interop.DirectX; + namespace Dalamud.Interface.ManagedFontAtlas.Internals; /// @@ -353,7 +355,7 @@ internal sealed partial class FontAtlasFactory var numPixels = texFile.Header.Width * texFile.Header.Height; _ = Service.Get(); - var targetIsB4G4R4A4 = this.TextureManager.IsDxgiFormatSupported((int)Format.B4G4R4A4_UNorm); + var targetIsB4G4R4A4 = this.TextureManager.IsDxgiFormatSupported(DXGI_FORMAT.DXGI_FORMAT_B4G4R4A4_UNORM); var bpp = targetIsB4G4R4A4 ? 2 : 4; var buffer = ArrayPool.Shared.Rent(numPixels * bpp); try diff --git a/Dalamud/Plugin/Services/ITextureProvider.cs b/Dalamud/Plugin/Services/ITextureProvider.cs index 46ad0fb46..a18ac5b2a 100644 --- a/Dalamud/Plugin/Services/ITextureProvider.cs +++ b/Dalamud/Plugin/Services/ITextureProvider.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Numerics; using System.Reflection; using System.Threading; using System.Threading.Tasks; @@ -27,6 +28,28 @@ namespace Dalamud.Plugin.Services; /// public partial interface ITextureProvider { + /// Creates a texture from the given existing texture, cropping and converting pixel format as needed. + /// + /// The source texture wrap. The passed value may be disposed once this function returns, + /// without having to wait for the completion of the returned . + /// The left top coordinates relative to the size of the source texture. + /// The right bottom coordinates relative to the size of the source texture. + /// The desired target format. + /// The cancellation token. + /// A containing the copied texture on success. Dispose after use. + /// + /// Coordinates in and should be in range between 0 and 1. + /// + /// Supported values for may not necessarily match + /// . + /// + Task CreateFromExistingTextureAsync( + IDalamudTextureWrap wrap, + Vector2 uv0, + Vector2 uv1, + int dxgiFormat, + CancellationToken cancellationToken = default); + /// Gets a texture from the given bytes, trying to interpret it as a .tex file or other well-known image /// files, such as .png. /// The bytes to load. @@ -143,4 +166,10 @@ public partial interface ITextureProvider /// The DXGI format. /// true if supported. bool IsDxgiFormatSupported(int dxgiFormat); + + /// Determines whether the system supports the given DXGI format for use with + /// . + /// The DXGI format. + /// true if supported. + bool IsDxgiFormatSupportedForCreateFromExistingTextureAsync(int dxgiFormat); } diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index 65196b3ee..f0d58dd3c 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -692,12 +692,21 @@ public static class Util /// Throws a corresponding exception if is true. /// /// The result value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static void ThrowOnError(this HRESULT hr) { if (hr.FAILED) Marshal.ThrowExceptionForHR(hr.Value); } + /// Determines if the specified instance of points to null. + /// The pointer. + /// The COM interface type from TerraFX. + /// true if not empty. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static unsafe bool IsEmpty(in this ComPtr f) where T : unmanaged, IUnknown.Interface => + f.Get() is null; + /// /// Calls if the task is incomplete. /// From 54ebe8c02ac4b4943b6a7028467167c471121334 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sat, 2 Mar 2024 02:03:26 +0900 Subject: [PATCH 31/57] fixes --- .../Internal/TextureManager.FormatConvert.cs | 21 ++++++++++++++++--- Dalamud/Interface/Internal/TextureManager.cs | 3 ++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/Dalamud/Interface/Internal/TextureManager.FormatConvert.cs b/Dalamud/Interface/Internal/TextureManager.FormatConvert.cs index a11cf6c53..f35688998 100644 --- a/Dalamud/Interface/Internal/TextureManager.FormatConvert.cs +++ b/Dalamud/Interface/Internal/TextureManager.FormatConvert.cs @@ -35,10 +35,24 @@ internal sealed partial class TextureManager scene = this.interfaceManager.Scene ?? throw new InvalidOperationException(); } + switch (dxgiFormat) + { + // https://learn.microsoft.com/en-us/windows/win32/api/dxgiformat/ne-dxgiformat-dxgi_format + // Video formats requiring use of another DXGI_FORMAT when using with CreateRenderTarget + case DXGI_FORMAT.DXGI_FORMAT_AYUV: + case DXGI_FORMAT.DXGI_FORMAT_NV12: + case DXGI_FORMAT.DXGI_FORMAT_P010: + case DXGI_FORMAT.DXGI_FORMAT_P016: + case DXGI_FORMAT.DXGI_FORMAT_NV11: + return false; + } + var format = (Format)dxgiFormat; var support = scene.Device.CheckFormatSupport(format); - return (support & FormatSupport.RenderTarget) != 0 - && (support & FormatSupport.Texture2D) != 0; + const FormatSupport required = + FormatSupport.RenderTarget | + FormatSupport.Texture2D; + return (support & required) == required; } /// @@ -72,7 +86,8 @@ internal sealed partial class TextureManager ct.ThrowIfCancellationRequested(); unsafe { - using var tex = new ComPtr( + using var tex = default(ComPtr); + tex.Attach( this.NoThrottleCreateFromExistingTextureCore( wrapCopy, uv0, diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index 52253ff3c..1041fc00c 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -321,7 +321,8 @@ internal sealed partial class TextureManager : IServiceType, IDisposable, ITextu var format = (Format)dxgiFormat; var support = scene.Device.CheckFormatSupport(format); - return (support & FormatSupport.Texture2D) != 0; + const FormatSupport required = FormatSupport.Texture2D; + return (support & required) == required; } /// From 5367d288d60063fc407dabcf8256b7aae7bd5662 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sat, 2 Mar 2024 06:57:12 +0900 Subject: [PATCH 32/57] Use WIC to implement ITP.SaveAsImageFormatToStreamAsync --- .../Interface/Internal/InterfaceManager.cs | 42 +- .../Internal/TextureManager.FormatConvert.cs | 328 ++++++++----- .../Interface/Internal/TextureManager.Wic.cs | 273 +++++++++++ Dalamud/Interface/Internal/TextureManager.cs | 30 +- .../Windows/Data/Widgets/TexWidget.cs | 61 ++- Dalamud/Plugin/Services/ITextureProvider.cs | 67 ++- Dalamud/Utility/ManagedIStream.cs | 433 ++++++++++++++++++ 7 files changed, 1105 insertions(+), 129 deletions(-) create mode 100644 Dalamud/Interface/Internal/TextureManager.Wic.cs create mode 100644 Dalamud/Utility/ManagedIStream.cs diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 68a65ebd1..019b462cd 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -287,7 +287,47 @@ internal class InterfaceManager : IDisposable, IServiceType /// Queues an action to be run before Present call. /// The action. - public void RunBeforePresent(Action action) => this.runBeforePresent.Enqueue(action); + /// A that resolves once is run. + public Task RunBeforePresent(Action action) + { + var tcs = new TaskCompletionSource(); + this.runBeforePresent.Enqueue( + () => + { + try + { + action(); + tcs.SetResult(); + } + catch (Exception e) + { + tcs.SetException(e); + } + }); + return tcs.Task; + } + + /// Queues a function to be run before Present call. + /// The type of the return value. + /// The function. + /// A that resolves once is run. + public Task RunBeforePresent(Func func) + { + var tcs = new TaskCompletionSource(); + this.runBeforePresent.Enqueue( + () => + { + try + { + tcs.SetResult(func()); + } + catch (Exception e) + { + tcs.SetException(e); + } + }); + return tcs.Task; + } /// /// Get video memory information. diff --git a/Dalamud/Interface/Internal/TextureManager.FormatConvert.cs b/Dalamud/Interface/Internal/TextureManager.FormatConvert.cs index f35688998..900eb5627 100644 --- a/Dalamud/Interface/Internal/TextureManager.FormatConvert.cs +++ b/Dalamud/Interface/Internal/TextureManager.FormatConvert.cs @@ -75,57 +75,37 @@ internal sealed partial class TextureManager var wrapCopy = wrap.CreateWrapSharingLowLevelResource(); return this.textureLoadThrottler.LoadTextureAsync( new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), - ct => + async _ => { - var tcs = new TaskCompletionSource(); - this.interfaceManager.RunBeforePresent( - () => - { - try - { - ct.ThrowIfCancellationRequested(); - unsafe - { - using var tex = default(ComPtr); - tex.Attach( - this.NoThrottleCreateFromExistingTextureCore( - wrapCopy, - uv0, - uv1, - format, - false)); + using var tex = await this.NoThrottleCreateFromExistingTextureAsync( + wrapCopy, + uv0, + uv1, + format); + using var device = default(ComPtr); + using var srv = default(ComPtr); + var desc = default(D3D11_TEXTURE2D_DESC); + unsafe + { + tex.Get()->GetDevice(device.GetAddressOf()); - using var device = default(ComPtr); - tex.Get()->GetDevice(device.GetAddressOf()); + var srvDesc = new D3D11_SHADER_RESOURCE_VIEW_DESC( + tex, + D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE2D); + device.Get()->CreateShaderResourceView( + (ID3D11Resource*)tex.Get(), + &srvDesc, + srv.GetAddressOf()) + .ThrowOnError(); - using var srv = default(ComPtr); - var srvDesc = new D3D11_SHADER_RESOURCE_VIEW_DESC( - tex, - D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE2D); - device.Get()->CreateShaderResourceView( - (ID3D11Resource*)tex.Get(), - &srvDesc, - srv.GetAddressOf()) - .ThrowOnError(); + tex.Get()->GetDesc(&desc); - var desc = default(D3D11_TEXTURE2D_DESC); - tex.Get()->GetDesc(&desc); - - tcs.SetResult( - new UnknownTextureWrap( - (IUnknown*)srv.Get(), - (int)desc.Width, - (int)desc.Height, - true)); - } - } - catch (Exception e) - { - tcs.SetException(e); - } - }); - - return tcs.Task; + return new UnknownTextureWrap( + (IUnknown*)srv.Get(), + (int)desc.Width, + (int)desc.Height, + true); + } }, cancellationToken) .ContinueWith( @@ -138,95 +118,209 @@ internal sealed partial class TextureManager .Unwrap(); } - private unsafe ID3D11Texture2D* NoThrottleCreateFromExistingTextureCore( + /// + Task<(RawImageSpecification Specification, byte[] RawData)> ITextureProvider.GetRawDataAsync( IDalamudTextureWrap wrap, Vector2 uv0, Vector2 uv1, - DXGI_FORMAT format, - bool enableCpuRead) + int dxgiFormat, + CancellationToken cancellationToken) => + this.GetRawDataAsync(wrap, uv0, uv1, (DXGI_FORMAT)dxgiFormat, cancellationToken); + + /// + public async Task<(RawImageSpecification Specification, byte[] RawData)> GetRawDataAsync( + IDalamudTextureWrap wrap, + Vector2 uv0, + Vector2 uv1, + DXGI_FORMAT dxgiFormat, + CancellationToken cancellationToken) { - ThreadSafety.AssertMainThread(); - - using var resUnk = new ComPtr((IUnknown*)wrap.ImGuiHandle); - + using var resUnk = default(ComPtr); using var texSrv = default(ComPtr); - resUnk.As(&texSrv).ThrowOnError(); - using var device = default(ComPtr); - texSrv.Get()->GetDevice(device.GetAddressOf()); - - using var deviceContext = default(ComPtr); - device.Get()->GetImmediateContext(deviceContext.GetAddressOf()); - + using var context = default(ComPtr); using var tex2D = default(ComPtr); - using (var texRes = default(ComPtr)) + var texDesc = default(D3D11_TEXTURE2D_DESC); + + unsafe { - texSrv.Get()->GetResource(texRes.GetAddressOf()); - texRes.As(&tex2D).ThrowOnError(); + resUnk.Attach((IUnknown*)wrap.ImGuiHandle); + resUnk.Get()->AddRef(); + + resUnk.As(&texSrv).ThrowOnError(); + + texSrv.Get()->GetDevice(device.GetAddressOf()); + + device.Get()->GetImmediateContext(context.GetAddressOf()); + + using (var texRes = default(ComPtr)) + { + texSrv.Get()->GetResource(texRes.GetAddressOf()); + + using var tex2DTemp = default(ComPtr); + texRes.As(&tex2DTemp).ThrowOnError(); + tex2D.Swap(&tex2DTemp); + } + + tex2D.Get()->GetDesc(&texDesc); } + if (texDesc.Format != dxgiFormat && dxgiFormat != DXGI_FORMAT.DXGI_FORMAT_UNKNOWN) + { + using var tmp = await this.NoThrottleCreateFromExistingTextureAsync(wrap, uv0, uv1, dxgiFormat); + unsafe + { + tex2D.Swap(&tmp); + } + } + + cancellationToken.ThrowIfCancellationRequested(); + return await this.interfaceManager.RunBeforePresent( + () => ExtractMappedResource(device, context, tex2D, cancellationToken)); + + static unsafe (RawImageSpecification Specification, byte[] RawData) ExtractMappedResource( + ComPtr device, + ComPtr context, + ComPtr tex2D, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + ID3D11Resource* mapWhat = null; + try + { + using var tmpTex = default(ComPtr); + D3D11_TEXTURE2D_DESC desc; + tex2D.Get()->GetDesc(&desc); + if ((desc.CPUAccessFlags & (uint)D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_READ) == 0) + { + var tmpTexDesc = desc with + { + MipLevels = 1, + ArraySize = 1, + SampleDesc = new(1, 0), + Usage = D3D11_USAGE.D3D11_USAGE_STAGING, + BindFlags = 0u, + CPUAccessFlags = (uint)D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_READ, + MiscFlags = 0u, + }; + device.Get()->CreateTexture2D(&tmpTexDesc, null, tmpTex.GetAddressOf()).ThrowOnError(); + context.Get()->CopyResource((ID3D11Resource*)tmpTex.Get(), (ID3D11Resource*)tex2D.Get()); + + cancellationToken.ThrowIfCancellationRequested(); + } + + D3D11_MAPPED_SUBRESOURCE mapped; + mapWhat = (ID3D11Resource*)(tmpTex.IsEmpty() ? tex2D.Get() : tmpTex.Get()); + context.Get()->Map( + mapWhat, + 0, + D3D11_MAP.D3D11_MAP_READ, + 0, + &mapped).ThrowOnError(); + + var specs = RawImageSpecification.From((int)desc.Width, (int)desc.Height, (int)desc.Format); + var bytes = new Span(mapped.pData, checked((int)mapped.DepthPitch)).ToArray(); + return (specs, bytes); + } + finally + { + if (mapWhat is not null) + context.Get()->Unmap(mapWhat, 0); + } + } + } + + private async Task> NoThrottleCreateFromExistingTextureAsync( + IDalamudTextureWrap wrap, + Vector2 uv0, + Vector2 uv1, + DXGI_FORMAT format) + { + using var resUnk = default(ComPtr); + using var texSrv = default(ComPtr); + using var device = default(ComPtr); + using var context = default(ComPtr); + using var tex2D = default(ComPtr); var texDesc = default(D3D11_TEXTURE2D_DESC); - tex2D.Get()->GetDesc(&texDesc); + + unsafe + { + resUnk.Attach((IUnknown*)wrap.ImGuiHandle); + resUnk.Get()->AddRef(); + + using (var texSrv2 = default(ComPtr)) + { + resUnk.As(&texSrv2).ThrowOnError(); + texSrv.Attach(texSrv2); + texSrv2.Detach(); + } + + texSrv.Get()->GetDevice(device.GetAddressOf()); + + device.Get()->GetImmediateContext(context.GetAddressOf()); + + using (var texRes = default(ComPtr)) + { + texSrv.Get()->GetResource(texRes.GetAddressOf()); + texRes.As(&tex2D).ThrowOnError(); + } + + tex2D.Get()->GetDesc(&texDesc); + } + + var newWidth = checked((uint)MathF.Round((uv1.X - uv0.X) * texDesc.Width)); + var newHeight = checked((uint)MathF.Round((uv1.Y - uv0.Y) * texDesc.Height)); using var tex2DCopyTemp = default(ComPtr); - var tex2DCopyTempDesc = new D3D11_TEXTURE2D_DESC + unsafe { - Width = checked((uint)MathF.Round((uv1.X - uv0.X) * wrap.Width)), - Height = checked((uint)MathF.Round((uv1.Y - uv0.Y) * wrap.Height)), - MipLevels = 1, - ArraySize = 1, - Format = format, - SampleDesc = new(1, 0), - Usage = D3D11_USAGE.D3D11_USAGE_DEFAULT, - BindFlags = (uint)(D3D11_BIND_FLAG.D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_FLAG.D3D11_BIND_RENDER_TARGET), - CPUAccessFlags = 0u, - MiscFlags = 0u, - }; - device.Get()->CreateTexture2D(&tex2DCopyTempDesc, null, tex2DCopyTemp.GetAddressOf()).ThrowOnError(); - - using (var rtvCopyTemp = default(ComPtr)) - { - var rtvCopyTempDesc = new D3D11_RENDER_TARGET_VIEW_DESC( - tex2DCopyTemp, - D3D11_RTV_DIMENSION.D3D11_RTV_DIMENSION_TEXTURE2D); - device.Get()->CreateRenderTargetView( - (ID3D11Resource*)tex2DCopyTemp.Get(), - &rtvCopyTempDesc, - rtvCopyTemp.GetAddressOf()).ThrowOnError(); - - this.drawsOneSquare ??= new(); - this.drawsOneSquare.Setup(device.Get()); - - deviceContext.Get()->OMSetRenderTargets(1u, rtvCopyTemp.GetAddressOf(), null); - this.drawsOneSquare.Draw( - deviceContext.Get(), - texSrv.Get(), - (int)tex2DCopyTempDesc.Width, - (int)tex2DCopyTempDesc.Height, - uv0, - uv1); - deviceContext.Get()->OMSetRenderTargets(0, null, null); + var tex2DCopyTempDesc = new D3D11_TEXTURE2D_DESC + { + Width = newWidth, + Height = newHeight, + MipLevels = 1, + ArraySize = 1, + Format = format, + SampleDesc = new(1, 0), + Usage = D3D11_USAGE.D3D11_USAGE_DEFAULT, + BindFlags = (uint)(D3D11_BIND_FLAG.D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_FLAG.D3D11_BIND_RENDER_TARGET), + CPUAccessFlags = 0u, + MiscFlags = 0u, + }; + device.Get()->CreateTexture2D(&tex2DCopyTempDesc, null, tex2DCopyTemp.GetAddressOf()).ThrowOnError(); } - if (!enableCpuRead) - { - tex2DCopyTemp.Get()->AddRef(); - return tex2DCopyTemp.Get(); - } + await this.interfaceManager.RunBeforePresent( + () => + { + unsafe + { + using var rtvCopyTemp = default(ComPtr); + var rtvCopyTempDesc = new D3D11_RENDER_TARGET_VIEW_DESC( + tex2DCopyTemp, + D3D11_RTV_DIMENSION.D3D11_RTV_DIMENSION_TEXTURE2D); + device.Get()->CreateRenderTargetView( + (ID3D11Resource*)tex2DCopyTemp.Get(), + &rtvCopyTempDesc, + rtvCopyTemp.GetAddressOf()).ThrowOnError(); - using var tex2DTarget = default(ComPtr); - var tex2DTargetDesc = tex2DCopyTempDesc with - { - Usage = D3D11_USAGE.D3D11_USAGE_DYNAMIC, - BindFlags = 0u, - CPUAccessFlags = (uint)D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_READ, - }; - device.Get()->CreateTexture2D(&tex2DTargetDesc, null, tex2DTarget.GetAddressOf()).ThrowOnError(); + this.drawsOneSquare ??= new(); + this.drawsOneSquare.Setup(device.Get()); - deviceContext.Get()->CopyResource((ID3D11Resource*)tex2DTarget.Get(), (ID3D11Resource*)tex2DCopyTemp.Get()); + context.Get()->OMSetRenderTargets(1u, rtvCopyTemp.GetAddressOf(), null); + this.drawsOneSquare.Draw( + context.Get(), + texSrv.Get(), + (int)newWidth, + (int)newHeight, + uv0, + uv1); + context.Get()->OMSetRenderTargets(0, null, null); + } + }); - tex2DTarget.Get()->AddRef(); - return tex2DTarget.Get(); + return new(tex2DCopyTemp); } [SuppressMessage( diff --git a/Dalamud/Interface/Internal/TextureManager.Wic.cs b/Dalamud/Interface/Internal/TextureManager.Wic.cs new file mode 100644 index 000000000..1adeccede --- /dev/null +++ b/Dalamud/Interface/Internal/TextureManager.Wic.cs @@ -0,0 +1,273 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Numerics; +using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Utility; + +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +using static TerraFX.Interop.Windows.Windows; + +namespace Dalamud.Interface.Internal; + +/// Service responsible for loading and disposing ImGui texture wraps. +internal sealed partial class TextureManager +{ + private ComPtr factory; + + /// + [SuppressMessage( + "StyleCop.CSharp.LayoutRules", + "SA1519:Braces should not be omitted from multi-line child statement", + Justification = "Multiple fixed blocks")] + public Task SaveAsImageFormatToStreamAsync( + IDalamudTextureWrap wrap, + string extension, + Stream stream, + bool leaveOpen = false, + IReadOnlyDictionary? props = null, + CancellationToken cancellationToken = default) + { + var container = GUID.GUID_ContainerFormatPng; + foreach (var (k, v) in this.GetSupportedContainerFormats()) + { + if (v.Contains(extension, StringComparer.InvariantCultureIgnoreCase)) + container = k; + } + + return this.SaveToStreamUsingWicAsync( + wrap, + container, + pbag => + { + if (props is null) + return; + unsafe + { + var nprop = 0u; + pbag.Get()->CountProperties(&nprop).ThrowOnError(); + for (var i = 0u; i < nprop; i++) + { + var pbag2 = default(PROPBAG2); + var npropread = 0u; + pbag.Get()->GetPropertyInfo(i, 1, &pbag2, &npropread).ThrowOnError(); + if (npropread == 0) + continue; + var propName = new string((char*)pbag2.pstrName); + if (props.TryGetValue(propName, out var untypedValue)) + { + VARIANT val; + VariantInit(&val); + + switch (untypedValue) + { + case null: + val.vt = (ushort)VARENUM.VT_EMPTY; + break; + case bool value: + val.vt = (ushort)VARENUM.VT_BOOL; + val.boolVal = (short)(value ? 1 : 0); + break; + case byte value: + val.vt = (ushort)VARENUM.VT_UI1; + val.bVal = value; + break; + case ushort value: + val.vt = (ushort)VARENUM.VT_UI2; + val.uiVal = value; + break; + case uint value: + val.vt = (ushort)VARENUM.VT_UI4; + val.uintVal = value; + break; + case ulong value: + val.vt = (ushort)VARENUM.VT_UI8; + val.ullVal = value; + break; + case sbyte value: + val.vt = (ushort)VARENUM.VT_I1; + val.cVal = value; + break; + case short value: + val.vt = (ushort)VARENUM.VT_I2; + val.iVal = value; + break; + case int value: + val.vt = (ushort)VARENUM.VT_I4; + val.intVal = value; + break; + case long value: + val.vt = (ushort)VARENUM.VT_I8; + val.llVal = value; + break; + case float value: + val.vt = (ushort)VARENUM.VT_R4; + val.fltVal = value; + break; + case double value: + val.vt = (ushort)VARENUM.VT_R8; + val.dblVal = value; + break; + default: + VariantClear(&val); + continue; + } + + VariantChangeType(&val, &val, 0, pbag2.vt).ThrowOnError(); + pbag.Get()->Write(1, &pbag2, &val).ThrowOnError(); + VariantClear(&val); + } + + CoTaskMemFree(pbag2.pstrName); + } + } + }, + stream, + leaveOpen, + cancellationToken); + } + + /// + public IEnumerable GetSupportedImageExtensions() => this.GetSupportedContainerFormats().Values; + + [SuppressMessage( + "StyleCop.CSharp.LayoutRules", + "SA1519:Braces should not be omitted from multi-line child statement", + Justification = "Multiple fixed blocks")] + private unsafe Dictionary GetSupportedContainerFormats() + { + var result = new Dictionary(); + using var enumUnknown = default(ComPtr); + this.factory.Get()->CreateComponentEnumerator( + (uint)WICComponentType.WICEncoder, + (uint)WICComponentEnumerateOptions.WICComponentEnumerateDefault, + enumUnknown.GetAddressOf()).ThrowOnError(); + + while (true) + { + using var entry = default(ComPtr); + var fetched = 0u; + enumUnknown.Get()->Next(1, entry.GetAddressOf(), &fetched).ThrowOnError(); + if (fetched == 0) + break; + + using var codecInfo = default(ComPtr); + if (entry.As(&codecInfo).FAILED) + continue; + + Guid containerFormat; + if (codecInfo.Get()->GetContainerFormat(&containerFormat).FAILED) + continue; + + var cch = 0u; + _ = codecInfo.Get()->GetFileExtensions(0, null, &cch); + var buf = new char[(int)cch + 1]; + fixed (char* pBuf = buf) + { + if (codecInfo.Get()->GetFileExtensions(cch + 1, (ushort*)pBuf, &cch).FAILED) + continue; + } + + result.Add(containerFormat, new string(buf, 0, buf.IndexOf('\0')).Split(",")); + } + + return result; + } + + [SuppressMessage( + "StyleCop.CSharp.LayoutRules", + "SA1519:Braces should not be omitted from multi-line child statement", + Justification = "Multiple fixed blocks")] + private async Task SaveToStreamUsingWicAsync( + IDalamudTextureWrap wrap, + Guid containerFormat, + Action> propertyBackSetterDelegate, + Stream stream, + bool leaveOpen = false, + CancellationToken cancellationToken = default) + { + using var wrapCopy = wrap.CreateWrapSharingLowLevelResource(); + await using var streamCloser = leaveOpen ? null : stream; + + var (specs, bytes) = await this.GetRawDataAsync( + wrapCopy, + Vector2.Zero, + Vector2.One, + DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM, + cancellationToken).ConfigureAwait(false); + + using var encoder = default(ComPtr); + using var encoderFrame = default(ComPtr); + using var wrappedStream = new ManagedIStream(stream); + var guidPixelFormat = GUID.GUID_WICPixelFormat32bppBGRA; + unsafe + { + this.factory.Get()->CreateEncoder(&containerFormat, null, encoder.GetAddressOf()).ThrowOnError(); + cancellationToken.ThrowIfCancellationRequested(); + + encoder.Get()->Initialize(wrappedStream, WICBitmapEncoderCacheOption.WICBitmapEncoderNoCache) + .ThrowOnError(); + cancellationToken.ThrowIfCancellationRequested(); + + using var propertyBag = default(ComPtr); + encoder.Get()->CreateNewFrame(encoderFrame.GetAddressOf(), propertyBag.GetAddressOf()).ThrowOnError(); + cancellationToken.ThrowIfCancellationRequested(); + + propertyBackSetterDelegate.Invoke(propertyBag); + encoderFrame.Get()->Initialize(propertyBag).ThrowOnError(); + cancellationToken.ThrowIfCancellationRequested(); + + encoderFrame.Get()->SetPixelFormat(&guidPixelFormat).ThrowOnError(); + encoderFrame.Get()->SetSize(checked((uint)specs.Width), checked((uint)specs.Height)).ThrowOnError(); + + if (guidPixelFormat == GUID.GUID_WICPixelFormat32bppBGRA) + { + fixed (byte* pByte = bytes) + { + encoderFrame.Get()->WritePixels( + (uint)specs.Height, + (uint)specs.Pitch, + checked((uint)bytes.Length), + pByte).ThrowOnError(); + } + } + else + { + using var tempBitmap = default(ComPtr); + fixed (Guid* pGuid = &GUID.GUID_WICPixelFormat32bppBGRA) + fixed (byte* pBytes = bytes) + { + this.factory.Get()->CreateBitmapFromMemory( + (uint)specs.Width, + (uint)specs.Height, + pGuid, + (uint)specs.Pitch, + checked((uint)bytes.Length), + pBytes, + tempBitmap.GetAddressOf()).ThrowOnError(); + } + + using var tempBitmap2 = default(ComPtr); + WICConvertBitmapSource( + &guidPixelFormat, + (IWICBitmapSource*)tempBitmap.Get(), + tempBitmap2.GetAddressOf()).ThrowOnError(); + + encoderFrame.Get()->SetSize(checked((uint)specs.Width), checked((uint)specs.Height)).ThrowOnError(); + encoderFrame.Get()->WriteSource(tempBitmap2.Get(), null).ThrowOnError(); + } + + cancellationToken.ThrowIfCancellationRequested(); + + encoderFrame.Get()->Commit().ThrowOnError(); + cancellationToken.ThrowIfCancellationRequested(); + + encoder.Get()->Commit().ThrowOnError(); + } + } +} diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index 1041fc00c..8e61e42b0 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -27,6 +27,9 @@ using SharpDX.Direct3D11; using SharpDX.DXGI; using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +using static TerraFX.Interop.Windows.Windows; namespace Dalamud.Interface.Internal; @@ -75,8 +78,31 @@ internal sealed partial class TextureManager : IServiceType, IDisposable, ITextu private bool disposing; + [SuppressMessage( + "StyleCop.CSharp.LayoutRules", + "SA1519:Braces should not be omitted from multi-line child statement", + Justification = "Multiple fixed blocks")] [ServiceManager.ServiceConstructor] - private TextureManager() => this.framework.Update += this.FrameworkOnUpdate; + private TextureManager() + { + this.framework.Update += this.FrameworkOnUpdate; + unsafe + { + fixed (Guid* pclsidWicImagingFactory = &CLSID.CLSID_WICImagingFactory) + fixed (Guid* piidWicImagingFactory = &IID.IID_IWICImagingFactory) + { + CoCreateInstance( + pclsidWicImagingFactory, + null, + (uint)CLSCTX.CLSCTX_INPROC_SERVER, + piidWicImagingFactory, + (void**)this.factory.GetAddressOf()).ThrowOnError(); + } + } + } + + /// Finalizes an instance of the class. + ~TextureManager() => this.Dispose(); /// public event ITextureSubstitutionProvider.TextureDataInterceptorDelegate? InterceptTexDataLoad; @@ -114,6 +140,8 @@ internal sealed partial class TextureManager : IServiceType, IDisposable, ITextu this.drawsOneSquare?.Dispose(); this.drawsOneSquare = null; + this.factory.Reset(); + return; static void ReleaseSelfReferences(ConcurrentDictionary dict) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs index 9a63dbcb9..a39a48f66 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -7,6 +7,7 @@ using System.Runtime.Loader; using System.Threading.Tasks; using Dalamud.Interface.Components; +using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Internal.SharedImmediateTextures; using Dalamud.Interface.Utility; @@ -16,6 +17,8 @@ using Dalamud.Utility; using ImGuiNET; +using Serilog; + using TerraFX.Interop.DirectX; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; @@ -42,6 +45,7 @@ internal class TexWidget : IDataWindowWidget private Vector4 inputTintCol = Vector4.One; private Vector2 inputTexScale = Vector2.Zero; private TextureManager textureManager = null!; + private FileDialogManager fileDialogManager = null!; private string[]? supportedRenderTargetFormatNames; private DXGI_FORMAT[]? supportedRenderTargetFormats; @@ -74,6 +78,7 @@ internal class TexWidget : IDataWindowWidget this.inputManifestResourceNameIndex = 0; this.supportedRenderTargetFormats = null; this.supportedRenderTargetFormatNames = null; + this.fileDialogManager = new(); this.Ready = true; } @@ -233,6 +238,8 @@ internal class TexWidget : IDataWindowWidget } runLater?.Invoke(); + + this.fileDialogManager.Draw(); } private unsafe void DrawLoadedTextures(ICollection textures) @@ -241,11 +248,11 @@ internal class TexWidget : IDataWindowWidget if (!ImGui.BeginTable("##table", 6)) return; - const int numIcons = 3; + const int numIcons = 4; float iconWidths; using (im.IconFontHandle?.Push()) { - iconWidths = ImGui.CalcTextSize(FontAwesomeIcon.Image.ToIconString()).X; + iconWidths = ImGui.CalcTextSize(FontAwesomeIcon.Save.ToIconString()).X; iconWidths += ImGui.CalcTextSize(FontAwesomeIcon.Sync.ToIconString()).X; iconWidths += ImGui.CalcTextSize(FontAwesomeIcon.Trash.ToIconString()).X; } @@ -315,7 +322,24 @@ internal class TexWidget : IDataWindowWidget ImGui.TextUnformatted(texture.HasRevivalPossibility ? "Yes" : "No"); ImGui.TableNextColumn(); - ImGuiComponents.IconButton(FontAwesomeIcon.Image); + if (ImGuiComponents.IconButton(FontAwesomeIcon.Save)) + { + this.fileDialogManager.SaveFileDialog( + "Save texture...", + string.Join( + ',', + this.textureManager + .GetSupportedImageExtensions() + .Select(x => $"{string.Join(" | ", x)}{{{string.Join(',', x)}}}")), + Path.ChangeExtension(Path.GetFileName(texture.SourcePathForDebug), ".png"), + ".png", + (ok, path) => + { + if (ok) + Task.Run(() => this.SaveImmediateTexture(texture, path)); + }); + } + if (ImGui.IsItemHovered() && texture.GetWrapOrDefault(null) is { } immediate) { ImGui.BeginTooltip(); @@ -351,6 +375,37 @@ internal class TexWidget : IDataWindowWidget ImGuiHelpers.ScaledDummy(10); } + private async void SaveImmediateTexture(ISharedImmediateTexture texture, string path) + { + try + { + using var rented = await texture.RentAsync(); + await this.textureManager.SaveAsImageFormatToStreamAsync( + rented, + Path.GetExtension(path), + File.Create(path), + props: new Dictionary + { + ["CompressionQuality"] = 1.0f, + ["ImageQuality"] = 1.0f, + }); + } + catch (Exception e) + { + Log.Error(e, $"{nameof(TexWidget)}.{nameof(this.SaveImmediateTexture)}"); + Service.Get().AddNotification( + $"Failed to save file: {e}", + this.DisplayName, + NotificationType.Error); + return; + } + + Service.Get().AddNotification( + $"File saved to: {path}", + this.DisplayName, + NotificationType.Success); + } + private void DrawGetFromGameIcon() { ImGui.InputText("Icon ID", ref this.iconId, 32); diff --git a/Dalamud/Plugin/Services/ITextureProvider.cs b/Dalamud/Plugin/Services/ITextureProvider.cs index a18ac5b2a..723801c9d 100644 --- a/Dalamud/Plugin/Services/ITextureProvider.cs +++ b/Dalamud/Plugin/Services/ITextureProvider.cs @@ -1,4 +1,5 @@ -using System.Diagnostics.CodeAnalysis; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Numerics; using System.Reflection; @@ -112,9 +113,7 @@ public partial interface ITextureProvider /// A texture wrap that can be used to render the texture. Dispose after use. IDalamudTextureWrap CreateFromTexFile(TexFile file); - /// - /// Get a texture handle for the specified Lumina . - /// + /// Get a texture handle for the specified Lumina . /// The texture to obtain a handle to. /// The cancellation token. /// A texture wrap that can be used to render the texture. Dispose after use. @@ -143,9 +142,7 @@ public partial interface ITextureProvider /// The shared texture that you may use to obtain the loaded texture wrap and load states. ISharedImmediateTexture GetFromManifestResource(Assembly assembly, string name); - /// - /// Get a path for a specific icon's .tex file. - /// + /// Get a path for a specific icon's .tex file. /// The icon lookup. /// The path to the icon. /// If a corresponding file could not be found. @@ -159,6 +156,62 @@ public partial interface ITextureProvider /// true if the corresponding file exists and has been set. bool TryGetIconPath(in GameIconLookup lookup, [NotNullWhen(true)] out string? path); + /// Gets the raw data of a texture wrap. + /// The source texture wrap. The passed value may be disposed once this function returns, + /// without having to wait for the completion of the returned . + /// The left top coordinates relative to the size of the source texture. + /// The right bottom coordinates relative to the size of the source texture. + /// The desired target format. + /// If 0 (unknown) is passed, then the format will not be converted. + /// The cancellation token. + /// The raw data and its specifications. + /// + /// The length of the returned RawData may not match + /// * . + /// If is , + /// is , and is 0, + /// then the source data will be returned. + /// This function can fail. + /// + Task<(RawImageSpecification Specification, byte[] RawData)> GetRawDataAsync( + IDalamudTextureWrap wrap, + Vector2 uv0, + Vector2 uv1, + int dxgiFormat = 0, + CancellationToken cancellationToken = default); + + /// Gets the supported image file extensions. + /// The supported extensions. Each string[] entry indicates that there can be multiple extensions + /// that correspond to one container format. + IEnumerable GetSupportedImageExtensions(); + + /// Saves a texture wrap to a stream in an image file format. + /// The texture wrap to save. + /// The extension of the file to deduce the file format with the leading dot. + /// The stream to save to. + /// Whether to leave open. + /// Properties to pass to the encoder. See + /// Microsoft + /// Learn for available parameters. + /// The cancellation token. + /// A task representing the save process. + /// + /// may be disposed as soon as this function returns. + /// If no image container format corresponding to is found, then the image will + /// be saved in png format. + /// + [SuppressMessage( + "StyleCop.CSharp.LayoutRules", + "SA1519:Braces should not be omitted from multi-line child statement", + Justification = "Multiple fixed blocks")] + Task SaveAsImageFormatToStreamAsync( + IDalamudTextureWrap wrap, + string extension, + Stream stream, + bool leaveOpen = false, + IReadOnlyDictionary? props = null, + CancellationToken cancellationToken = default); + /// /// Determines whether the system supports the given DXGI format. /// For use with . diff --git a/Dalamud/Utility/ManagedIStream.cs b/Dalamud/Utility/ManagedIStream.cs new file mode 100644 index 000000000..33c05111c --- /dev/null +++ b/Dalamud/Utility/ManagedIStream.cs @@ -0,0 +1,433 @@ +using System.Buffers; +using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +using TerraFX.Interop; +using TerraFX.Interop.Windows; + +namespace Dalamud.Utility; + +/// An wrapper for . +[Guid("a620678b-56b9-4202-a1da-b821214dc972")] +internal sealed unsafe class ManagedIStream : IStream.Interface, IRefCountable +{ + private static readonly Guid MyGuid = typeof(ManagedIStream).GUID; + + private readonly Stream inner; + private readonly nint[] comObject; + private readonly IStream.Vtbl vtbl; + private GCHandle gchThis; + private GCHandle gchComObject; + private GCHandle gchVtbl; + private int refCount; + + /// Initializes a new instance of the class. + /// The inner stream. + public ManagedIStream(Stream inner) + { + this.inner = inner; + this.comObject = new nint[2]; + + this.vtbl.QueryInterface = &QueryInterfaceStatic; + this.vtbl.AddRef = &AddRefStatic; + this.vtbl.Release = &ReleaseStatic; + this.vtbl.Read = &ReadStatic; + this.vtbl.Write = &WriteStatic; + this.vtbl.Seek = &SeekStatic; + this.vtbl.SetSize = &SetSizeStatic; + this.vtbl.CopyTo = &CopyToStatic; + this.vtbl.Commit = &CommitStatic; + this.vtbl.Revert = &RevertStatic; + this.vtbl.LockRegion = &LockRegionStatic; + this.vtbl.UnlockRegion = &UnlockRegionStatic; + this.vtbl.Stat = &StatStatic; + this.vtbl.Clone = &CloneStatic; + + this.gchThis = GCHandle.Alloc(this); + this.gchVtbl = GCHandle.Alloc(this.vtbl, GCHandleType.Pinned); + this.gchComObject = GCHandle.Alloc(this.comObject, GCHandleType.Pinned); + this.comObject[0] = this.gchVtbl.AddrOfPinnedObject(); + this.comObject[1] = GCHandle.ToIntPtr(this.gchThis); + this.refCount = 1; + + return; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static IStream.Interface? ToManagedObject(void* pThis) => + GCHandle.FromIntPtr(((nint*)pThis)[1]).Target as IStream.Interface; + + [UnmanagedCallersOnly] + static int QueryInterfaceStatic(IStream* pThis, Guid* riid, void** ppvObject) => + ToManagedObject(pThis)?.QueryInterface(riid, ppvObject) ?? E.E_FAIL; + + [UnmanagedCallersOnly] + static uint AddRefStatic(IStream* pThis) => ToManagedObject(pThis)?.AddRef() ?? 0; + + [UnmanagedCallersOnly] + static uint ReleaseStatic(IStream* pThis) => ToManagedObject(pThis)?.Release() ?? 0; + + [UnmanagedCallersOnly] + static int ReadStatic(IStream* pThis, void* pv, uint cb, uint* pcbRead) => + ToManagedObject(pThis)?.Read(pv, cb, pcbRead) ?? E.E_FAIL; + + [UnmanagedCallersOnly] + static int WriteStatic(IStream* pThis, void* pv, uint cb, uint* pcbWritten) => + ToManagedObject(pThis)?.Write(pv, cb, pcbWritten) ?? E.E_FAIL; + + [UnmanagedCallersOnly] + static int SeekStatic( + IStream* pThis, LARGE_INTEGER dlibMove, uint dwOrigin, ULARGE_INTEGER* plibNewPosition) => + ToManagedObject(pThis)?.Seek(dlibMove, dwOrigin, plibNewPosition) ?? E.E_FAIL; + + [UnmanagedCallersOnly] + static int SetSizeStatic(IStream* pThis, ULARGE_INTEGER libNewSize) => + ToManagedObject(pThis)?.SetSize(libNewSize) ?? E.E_FAIL; + + [UnmanagedCallersOnly] + static int CopyToStatic( + IStream* pThis, IStream* pstm, ULARGE_INTEGER cb, ULARGE_INTEGER* pcbRead, + ULARGE_INTEGER* pcbWritten) => + ToManagedObject(pThis)?.CopyTo(pstm, cb, pcbRead, pcbWritten) ?? E.E_FAIL; + + [UnmanagedCallersOnly] + static int CommitStatic(IStream* pThis, uint grfCommitFlags) => + ToManagedObject(pThis)?.Commit(grfCommitFlags) ?? E.E_FAIL; + + [UnmanagedCallersOnly] + static int RevertStatic(IStream* pThis) => ToManagedObject(pThis)?.Revert() ?? E.E_FAIL; + + [UnmanagedCallersOnly] + static int LockRegionStatic(IStream* pThis, ULARGE_INTEGER libOffset, ULARGE_INTEGER cb, uint dwLockType) => + ToManagedObject(pThis)?.LockRegion(libOffset, cb, dwLockType) ?? E.E_FAIL; + + [UnmanagedCallersOnly] + static int UnlockRegionStatic( + IStream* pThis, ULARGE_INTEGER libOffset, ULARGE_INTEGER cb, uint dwLockType) => + ToManagedObject(pThis)?.UnlockRegion(libOffset, cb, dwLockType) ?? E.E_FAIL; + + [UnmanagedCallersOnly] + static int StatStatic(IStream* pThis, STATSTG* pstatstg, uint grfStatFlag) => + ToManagedObject(pThis)?.Stat(pstatstg, grfStatFlag) ?? E.E_FAIL; + + [UnmanagedCallersOnly] + static int CloneStatic(IStream* pThis, IStream** ppstm) => ToManagedObject(pThis)?.Clone(ppstm) ?? E.E_FAIL; + } + + /// + public static Guid* NativeGuid => (Guid*)Unsafe.AsPointer(ref Unsafe.AsRef(in MyGuid)); + + public static implicit operator IUnknown*(ManagedIStream mis) => + (IUnknown*)mis.gchComObject.AddrOfPinnedObject(); + + public static implicit operator ISequentialStream*(ManagedIStream mis) => + (ISequentialStream*)mis.gchComObject.AddrOfPinnedObject(); + + public static implicit operator IStream*(ManagedIStream mis) => + (IStream*)mis.gchComObject.AddrOfPinnedObject(); + + /// + public HRESULT QueryInterface(Guid* riid, void** ppvObject) + { + if (ppvObject == null) + return E.E_POINTER; + + if (*riid == IID.IID_IUnknown || + *riid == IID.IID_ISequentialStream || + *riid == IID.IID_IStream || + *riid == MyGuid) + { + try + { + this.AddRef(); + } + catch + { + return E.E_FAIL; + } + + *ppvObject = (IUnknown*)this; + return S.S_OK; + } + + *ppvObject = null; + return E.E_NOINTERFACE; + } + + /// + public int AddRef() => IRefCountable.AlterRefCount(1, ref this.refCount, out var newRefCount) switch + { + IRefCountable.RefCountResult.StillAlive => newRefCount, + IRefCountable.RefCountResult.AlreadyDisposed => throw new ObjectDisposedException(nameof(ManagedIStream)), + 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.gchThis.Free(); + this.gchComObject.Free(); + this.gchVtbl.Free(); + return newRefCount; + + case IRefCountable.RefCountResult.AlreadyDisposed: + throw new ObjectDisposedException(nameof(ManagedIStream)); + + default: + throw new InvalidOperationException(); + } + } + + /// + uint IUnknown.Interface.AddRef() + { + try + { + return (uint)this.AddRef(); + } + catch + { + return 0; + } + } + + /// + uint IUnknown.Interface.Release() + { + try + { + return (uint)this.Release(); + } + catch + { + return 0; + } + } + + /// + public HRESULT Read(void* pv, uint cb, uint* pcbRead) + { + if (pcbRead == null) + { + var tmp = stackalloc uint[1]; + pcbRead = tmp; + } + + ref var read = ref *pcbRead; + for (read = 0u; read < cb;) + { + var chunkSize = unchecked((int)Math.Min(0x10000000u, cb)); + var chunkRead = (uint)this.inner.Read(new(pv, chunkSize)); + if (chunkRead == 0) + break; + pv = (byte*)pv + chunkRead; + read += chunkRead; + } + + return read == cb ? S.S_OK : S.S_FALSE; + } + + /// + public HRESULT Write(void* pv, uint cb, uint* pcbWritten) + { + if (pcbWritten == null) + { + var tmp = stackalloc uint[1]; + pcbWritten = tmp; + } + + ref var written = ref *pcbWritten; + try + { + for (written = 0u; written < cb;) + { + var chunkSize = Math.Min(0x10000000u, cb); + this.inner.Write(new(pv, (int)chunkSize)); + pv = (byte*)pv + chunkSize; + written += chunkSize; + } + + return S.S_OK; + } + catch (Exception e) when (e.HResult == unchecked((int)(0x80070000u | ERROR.ERROR_HANDLE_DISK_FULL))) + { + return STG.STG_E_MEDIUMFULL; + } + catch (Exception e) when (e.HResult == unchecked((int)(0x80070000u | ERROR.ERROR_DISK_FULL))) + { + return STG.STG_E_MEDIUMFULL; + } + catch (IOException) + { + return STG.STG_E_CANTSAVE; + } + } + + /// + public HRESULT Seek(LARGE_INTEGER dlibMove, uint dwOrigin, ULARGE_INTEGER* plibNewPosition) + { + SeekOrigin seekOrigin; + + switch ((STREAM_SEEK)dwOrigin) + { + case STREAM_SEEK.STREAM_SEEK_SET: + seekOrigin = SeekOrigin.Begin; + break; + case STREAM_SEEK.STREAM_SEEK_CUR: + seekOrigin = SeekOrigin.Current; + break; + case STREAM_SEEK.STREAM_SEEK_END: + seekOrigin = SeekOrigin.End; + break; + default: + return STG.STG_E_INVALIDFUNCTION; + } + + try + { + var position = this.inner.Seek(dlibMove.QuadPart, seekOrigin); + if (plibNewPosition != null) + { + *plibNewPosition = new() { QuadPart = (ulong)position }; + } + + return S.S_OK; + } + catch + { + return STG.STG_E_INVALIDFUNCTION; + } + } + + /// + public HRESULT SetSize(ULARGE_INTEGER libNewSize) + { + try + { + this.inner.SetLength(checked((long)libNewSize.QuadPart)); + return S.S_OK; + } + catch (Exception e) when (e.HResult == unchecked((int)(0x80070000u | ERROR.ERROR_HANDLE_DISK_FULL))) + { + return STG.STG_E_MEDIUMFULL; + } + catch (Exception e) when (e.HResult == unchecked((int)(0x80070000u | ERROR.ERROR_DISK_FULL))) + { + return STG.STG_E_MEDIUMFULL; + } + catch (IOException) + { + return STG.STG_E_INVALIDFUNCTION; + } + } + + /// + public HRESULT CopyTo(IStream* pstm, ULARGE_INTEGER cb, ULARGE_INTEGER* pcbRead, ULARGE_INTEGER* pcbWritten) + { + if (pcbRead == null) + { + var temp = stackalloc ULARGE_INTEGER[1]; + pcbRead = temp; + } + + if (pcbWritten == null) + { + var temp = stackalloc ULARGE_INTEGER[1]; + pcbWritten = temp; + } + + ref var cbRead = ref pcbRead->QuadPart; + ref var cbWritten = ref pcbWritten->QuadPart; + cbRead = cbWritten = 0; + + var buf = ArrayPool.Shared.Rent(8192); + try + { + fixed (byte* pbuf = buf) + { + while (cbRead < cb) + { + var read = checked((uint)this.inner.Read(buf.AsSpan())); + if (read == 0) + break; + cbRead += read; + + var written = 0u; + var writeResult = pstm->Write(pbuf, read, &written); + if (writeResult.FAILED) + return writeResult; + cbWritten += written; + } + } + + return S.S_OK; + } + catch (Exception e) when (e.HResult == unchecked((int)(0x80070000u | ERROR.ERROR_HANDLE_DISK_FULL))) + { + return STG.STG_E_MEDIUMFULL; + } + catch (Exception e) when (e.HResult == unchecked((int)(0x80070000u | ERROR.ERROR_DISK_FULL))) + { + return STG.STG_E_MEDIUMFULL; + } + catch (Exception e) + { + // Undefined return value according to the documentation, but meh + return e.HResult < 0 ? e.HResult : E.E_FAIL; + } + finally + { + ArrayPool.Shared.Return(buf); + } + } + + /// + // On streams open in direct mode, this method has no effect. + public HRESULT Commit(uint grfCommitFlags) => S.S_OK; + + /// + // On streams open in direct mode, this method has no effect. + public HRESULT Revert() => S.S_OK; + + /// + // Locking is not supported at all or the specific type of lock requested is not supported. + public HRESULT LockRegion(ULARGE_INTEGER libOffset, ULARGE_INTEGER cb, uint dwLockType) => + STG.STG_E_INVALIDFUNCTION; + + /// + // Locking is not supported at all or the specific type of lock requested is not supported. + public HRESULT UnlockRegion(ULARGE_INTEGER libOffset, ULARGE_INTEGER cb, uint dwLockType) => + STG.STG_E_INVALIDFUNCTION; + + /// + public HRESULT Stat(STATSTG* pstatstg, uint grfStatFlag) + { + if (pstatstg is null) + return STG.STG_E_INVALIDPOINTER; + ref var streamStats = ref *pstatstg; + streamStats.type = (uint)STGTY.STGTY_STREAM; + streamStats.cbSize = (ulong)this.inner.Length; + streamStats.grfMode = 0; + if (this.inner.CanRead && this.inner.CanWrite) + streamStats.grfMode |= STGM.STGM_READWRITE; + else if (this.inner.CanRead) + streamStats.grfMode |= STGM.STGM_READ; + else if (this.inner.CanWrite) + streamStats.grfMode |= STGM.STGM_WRITE; + else + return STG.STG_E_REVERTED; + return S.S_OK; + } + + /// + // Undefined return value according to the documentation, but meh + public HRESULT Clone(IStream** ppstm) => E.E_NOTIMPL; +} From 0aa75306d4bada6de5da501a3c7ec0aef3c94a4c Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sat, 2 Mar 2024 07:50:37 +0900 Subject: [PATCH 33/57] fixese --- .../FileSystemSharedImmediateTexture.cs | 3 +- .../ManifestResourceSharedImmediateTexture.cs | 2 +- .../Internal/TextureManager.FormatConvert.cs | 6 +- .../Interface/Internal/TextureManager.Wic.cs | 283 +++++++++++++++--- Dalamud/Interface/Internal/TextureManager.cs | 52 +--- .../Windows/Data/Widgets/TexWidget.cs | 39 ++- .../Internals/FontAtlasFactory.cs | 2 +- Dalamud/Plugin/Services/ITextureProvider.cs | 9 +- 8 files changed, 301 insertions(+), 95 deletions(-) diff --git a/Dalamud/Interface/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs b/Dalamud/Interface/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs index 702a83f52..06c366601 100644 --- a/Dalamud/Interface/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs +++ b/Dalamud/Interface/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs @@ -1,4 +1,3 @@ -using System.IO; using System.Threading; using System.Threading.Tasks; @@ -44,6 +43,6 @@ internal sealed class FileSystemSharedImmediateTexture : SharedImmediateTexture private async Task CreateTextureAsync(CancellationToken cancellationToken) { var tm = await Service.GetAsync(); - return tm.NoThrottleCreateFromImage(await File.ReadAllBytesAsync(this.path, cancellationToken)); + return await tm.NoThrottleCreateFromFileAsync(this.path, cancellationToken); } } diff --git a/Dalamud/Interface/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs b/Dalamud/Interface/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs index a75a7cb68..c017a0764 100644 --- a/Dalamud/Interface/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs +++ b/Dalamud/Interface/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs @@ -59,6 +59,6 @@ internal sealed class ManifestResourceSharedImmediateTexture : SharedImmediateTe var tm = await Service.GetAsync(); var ms = new MemoryStream(stream.CanSeek ? (int)stream.Length : 0); await stream.CopyToAsync(ms, cancellationToken); - return tm.NoThrottleCreateFromImage(ms.GetBuffer().AsMemory(0, (int)ms.Length)); + return tm.NoThrottleCreateFromImage(ms.GetBuffer().AsMemory(0, (int)ms.Length), cancellationToken); } } diff --git a/Dalamud/Interface/Internal/TextureManager.FormatConvert.cs b/Dalamud/Interface/Internal/TextureManager.FormatConvert.cs index 900eb5627..99474dca2 100644 --- a/Dalamud/Interface/Internal/TextureManager.FormatConvert.cs +++ b/Dalamud/Interface/Internal/TextureManager.FormatConvert.cs @@ -219,7 +219,11 @@ internal sealed partial class TextureManager 0, &mapped).ThrowOnError(); - var specs = RawImageSpecification.From((int)desc.Width, (int)desc.Height, (int)desc.Format); + var specs = new RawImageSpecification( + (int)desc.Width, + (int)desc.Height, + (int)mapped.RowPitch, + (int)desc.Format); var bytes = new Span(mapped.pData, checked((int)mapped.DepthPitch)).ToArray(); return (specs, bytes); } diff --git a/Dalamud/Interface/Internal/TextureManager.Wic.cs b/Dalamud/Interface/Internal/TextureManager.Wic.cs index 1adeccede..66be9ca58 100644 --- a/Dalamud/Interface/Internal/TextureManager.Wic.cs +++ b/Dalamud/Interface/Internal/TextureManager.Wic.cs @@ -3,11 +3,17 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Numerics; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; +using Dalamud.Interface.Internal.SharedImmediateTextures; +using Dalamud.Plugin.Services; using Dalamud.Utility; +using Lumina.Data; +using Lumina.Data.Files; + using TerraFX.Interop.DirectX; using TerraFX.Interop.Windows; @@ -18,7 +24,7 @@ namespace Dalamud.Interface.Internal; /// Service responsible for loading and disposing ImGui texture wraps. internal sealed partial class TextureManager { - private ComPtr factory; + private ComPtr wicFactory; /// [SuppressMessage( @@ -34,7 +40,7 @@ internal sealed partial class TextureManager CancellationToken cancellationToken = default) { var container = GUID.GUID_ContainerFormatPng; - foreach (var (k, v) in this.GetSupportedContainerFormats()) + foreach (var (k, v) in this.GetSupportedContainerFormats(WICComponentType.WICEncoder)) { if (v.Contains(extension, StringComparer.InvariantCultureIgnoreCase)) container = k; @@ -133,18 +139,231 @@ internal sealed partial class TextureManager } /// - public IEnumerable GetSupportedImageExtensions() => this.GetSupportedContainerFormats().Values; + public IEnumerable GetLoadSupportedImageExtensions() => + this.GetSupportedContainerFormats(WICComponentType.WICDecoder).Values; + + /// + public IEnumerable GetSaveSupportedImageExtensions() => + this.GetSupportedContainerFormats(WICComponentType.WICEncoder).Values; + + /// Creates a texture from the given bytes of an image file. Skips the load throttler; intended to be used + /// from implementation of s. + /// The data. + /// The cancellation token. + /// The loaded texture. + internal unsafe IDalamudTextureWrap NoThrottleCreateFromImage( + ReadOnlyMemory bytes, + CancellationToken cancellationToken = default) + { + ObjectDisposedException.ThrowIf(this.disposing, this); + + cancellationToken.ThrowIfCancellationRequested(); + + if (TexFileExtensions.IsPossiblyTexFile2D(bytes.Span)) + { + var bytesArray = bytes.ToArray(); + var tf = new TexFile(); + typeof(TexFile).GetProperty(nameof(tf.Data))!.GetSetMethod(true)!.Invoke( + tf, + new object?[] { bytesArray }); + typeof(TexFile).GetProperty(nameof(tf.Reader))!.GetSetMethod(true)!.Invoke( + tf, + new object?[] { new LuminaBinaryReader(bytesArray) }); + // Note: FileInfo and FilePath are not used from TexFile; skip it. + try + { + return this.NoThrottleCreateFromTexFile(tf); + } + catch (Exception) + { + // ignore + } + } + + fixed (byte* p = bytes.Span) + { + using var wicStream = default(ComPtr); + this.wicFactory.Get()->CreateStream(wicStream.GetAddressOf()).ThrowOnError(); + wicStream.Get()->InitializeFromMemory(p, checked((uint)bytes.Length)).ThrowOnError(); + return this.NoThrottleCreateFromWicStream((IStream*)wicStream.Get(), cancellationToken); + } + } + + /// Creates a texture from the given path to an image file. Skips the load throttler; intended to be used + /// from implementation of s. + /// The path of the file.. + /// The cancellation token. + /// The loaded texture. + internal async Task NoThrottleCreateFromFileAsync( + string path, + CancellationToken cancellationToken = default) + { + ObjectDisposedException.ThrowIf(this.disposing, this); + + cancellationToken.ThrowIfCancellationRequested(); + + try + { + unsafe + { + fixed (char* pPath = path) + { + using var wicStream = default(ComPtr); + this.wicFactory.Get()->CreateStream(wicStream.GetAddressOf()).ThrowOnError(); + wicStream.Get()->InitializeFromFilename((ushort*)pPath, GENERIC_READ).ThrowOnError(); + return this.NoThrottleCreateFromWicStream((IStream*)wicStream.Get(), cancellationToken); + } + } + } + catch + { + try + { + await using var fp = File.OpenRead(path); + if (fp.Length >= Unsafe.SizeOf()) + { + var bytesArray = new byte[fp.Length]; + await fp.ReadExactlyAsync(bytesArray, cancellationToken); + if (TexFileExtensions.IsPossiblyTexFile2D(bytesArray)) + { + var tf = new TexFile(); + typeof(TexFile).GetProperty(nameof(tf.Data))!.GetSetMethod(true)!.Invoke( + tf, + new object?[] { bytesArray }); + typeof(TexFile).GetProperty(nameof(tf.Reader))!.GetSetMethod(true)!.Invoke( + tf, + new object?[] { new LuminaBinaryReader(bytesArray) }); + // Note: FileInfo and FilePath are not used from TexFile; skip it. + return this.NoThrottleCreateFromTexFile(tf); + } + } + } + catch (Exception) + { + // ignore + } + + throw; + } + } + + /// + /// Gets the corresponding from a containing a WIC pixel format. + /// + /// The WIC pixel format. + /// The corresponding , or if + /// unavailable. + private static DXGI_FORMAT GetCorrespondingDxgiFormat(Guid fmt) => 0 switch + { + // See https://github.com/microsoft/DirectXTex/wiki/WIC-I-O-Functions#savetowicmemory-savetowicfile + _ when fmt == GUID.GUID_WICPixelFormat128bppRGBAFloat => DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_FLOAT, + _ when fmt == GUID.GUID_WICPixelFormat64bppRGBAHalf => DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_FLOAT, + _ when fmt == GUID.GUID_WICPixelFormat64bppRGBA => DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_UNORM, + _ when fmt == GUID.GUID_WICPixelFormat32bppRGBA1010102XR => DXGI_FORMAT.DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM, + _ when fmt == GUID.GUID_WICPixelFormat32bppRGBA1010102 => DXGI_FORMAT.DXGI_FORMAT_R10G10B10A2_UNORM, + _ when fmt == GUID.GUID_WICPixelFormat16bppBGRA5551 => DXGI_FORMAT.DXGI_FORMAT_B5G5R5A1_UNORM, + _ when fmt == GUID.GUID_WICPixelFormat16bppBGR565 => DXGI_FORMAT.DXGI_FORMAT_B5G6R5_UNORM, + _ when fmt == GUID.GUID_WICPixelFormat32bppGrayFloat => DXGI_FORMAT.DXGI_FORMAT_R32_FLOAT, + _ when fmt == GUID.GUID_WICPixelFormat16bppGrayHalf => DXGI_FORMAT.DXGI_FORMAT_R16_FLOAT, + _ when fmt == GUID.GUID_WICPixelFormat16bppGray => DXGI_FORMAT.DXGI_FORMAT_R16_UNORM, + _ when fmt == GUID.GUID_WICPixelFormat8bppGray => DXGI_FORMAT.DXGI_FORMAT_R8_UNORM, + _ when fmt == GUID.GUID_WICPixelFormat8bppAlpha => DXGI_FORMAT.DXGI_FORMAT_A8_UNORM, + _ when fmt == GUID.GUID_WICPixelFormat32bppRGBA => DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM, + _ when fmt == GUID.GUID_WICPixelFormat32bppBGRA => DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM, + _ when fmt == GUID.GUID_WICPixelFormat32bppBGR => DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM, + _ => DXGI_FORMAT.DXGI_FORMAT_UNKNOWN, + }; + + private unsafe IDalamudTextureWrap NoThrottleCreateFromWicStream( + IStream* wicStream, + CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + using var decoder = default(ComPtr); + this.wicFactory.Get()->CreateDecoderFromStream( + wicStream, + null, + WICDecodeOptions.WICDecodeMetadataCacheOnDemand, + decoder.GetAddressOf()).ThrowOnError(); + + cancellationToken.ThrowIfCancellationRequested(); + + using var frame = default(ComPtr); + decoder.Get()->GetFrame(0, frame.GetAddressOf()).ThrowOnError(); + var pixelFormat = default(Guid); + frame.Get()->GetPixelFormat(&pixelFormat).ThrowOnError(); + var dxgiFormat = GetCorrespondingDxgiFormat(pixelFormat); + + cancellationToken.ThrowIfCancellationRequested(); + + using var bitmapSource = default(ComPtr); + if (dxgiFormat == DXGI_FORMAT.DXGI_FORMAT_UNKNOWN || !this.IsDxgiFormatSupported(dxgiFormat)) + { + dxgiFormat = DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM; + pixelFormat = GUID.GUID_WICPixelFormat32bppBGRA; + WICConvertBitmapSource(&pixelFormat, (IWICBitmapSource*)frame.Get(), bitmapSource.GetAddressOf()) + .ThrowOnError(); + } + else + { + frame.As(&bitmapSource); + } + + cancellationToken.ThrowIfCancellationRequested(); + + using var bitmap = default(ComPtr); + using var bitmapLock = default(ComPtr); + WICRect rcLock; + uint stride; + uint cbBufferSize; + byte* pbData; + if (bitmapSource.As(&bitmap).FAILED) + { + bitmapSource.Get()->GetSize((uint*)&rcLock.Width, (uint*)&rcLock.Height).ThrowOnError(); + this.wicFactory.Get()->CreateBitmap( + (uint)rcLock.Width, + (uint)rcLock.Height, + &pixelFormat, + WICBitmapCreateCacheOption.WICBitmapCacheOnDemand, + bitmap.GetAddressOf()).ThrowOnError(); + + bitmap.Get()->Lock( + &rcLock, + (uint)WICBitmapLockFlags.WICBitmapLockWrite, + bitmapLock.ReleaseAndGetAddressOf()) + .ThrowOnError(); + bitmapLock.Get()->GetStride(&stride).ThrowOnError(); + bitmapLock.Get()->GetDataPointer(&cbBufferSize, &pbData).ThrowOnError(); + bitmapSource.Get()->CopyPixels(null, stride, cbBufferSize, pbData).ThrowOnError(); + } + + cancellationToken.ThrowIfCancellationRequested(); + + bitmap.Get()->Lock( + &rcLock, + (uint)WICBitmapLockFlags.WICBitmapLockRead, + bitmapLock.ReleaseAndGetAddressOf()) + .ThrowOnError(); + bitmapSource.Get()->GetSize((uint*)&rcLock.Width, (uint*)&rcLock.Height).ThrowOnError(); + bitmapLock.Get()->GetStride(&stride).ThrowOnError(); + bitmapLock.Get()->GetDataPointer(&cbBufferSize, &pbData).ThrowOnError(); + bitmapSource.Get()->CopyPixels(null, stride, cbBufferSize, pbData).ThrowOnError(); + return this.NoThrottleCreateFromRaw( + new RawImageSpecification(rcLock.Width, rcLock.Height, (int)stride, (int)dxgiFormat), + new(pbData, (int)cbBufferSize)); + } [SuppressMessage( "StyleCop.CSharp.LayoutRules", "SA1519:Braces should not be omitted from multi-line child statement", Justification = "Multiple fixed blocks")] - private unsafe Dictionary GetSupportedContainerFormats() + private unsafe Dictionary GetSupportedContainerFormats(WICComponentType componentType) { var result = new Dictionary(); using var enumUnknown = default(ComPtr); - this.factory.Get()->CreateComponentEnumerator( - (uint)WICComponentType.WICEncoder, + this.wicFactory.Get()->CreateComponentEnumerator( + (uint)componentType, (uint)WICComponentEnumerateOptions.WICComponentEnumerateDefault, enumUnknown.GetAddressOf()).ThrowOnError(); @@ -207,7 +426,7 @@ internal sealed partial class TextureManager var guidPixelFormat = GUID.GUID_WICPixelFormat32bppBGRA; unsafe { - this.factory.Get()->CreateEncoder(&containerFormat, null, encoder.GetAddressOf()).ThrowOnError(); + this.wicFactory.Get()->CreateEncoder(&containerFormat, null, encoder.GetAddressOf()).ThrowOnError(); cancellationToken.ThrowIfCancellationRequested(); encoder.Get()->Initialize(wrappedStream, WICBitmapEncoderCacheOption.WICBitmapEncoderNoCache) @@ -225,42 +444,28 @@ internal sealed partial class TextureManager encoderFrame.Get()->SetPixelFormat(&guidPixelFormat).ThrowOnError(); encoderFrame.Get()->SetSize(checked((uint)specs.Width), checked((uint)specs.Height)).ThrowOnError(); - if (guidPixelFormat == GUID.GUID_WICPixelFormat32bppBGRA) + using var tempBitmap = default(ComPtr); + fixed (Guid* pGuid = &GUID.GUID_WICPixelFormat32bppBGRA) + fixed (byte* pBytes = bytes) { - fixed (byte* pByte = bytes) - { - encoderFrame.Get()->WritePixels( - (uint)specs.Height, - (uint)specs.Pitch, - checked((uint)bytes.Length), - pByte).ThrowOnError(); - } + this.wicFactory.Get()->CreateBitmapFromMemory( + (uint)specs.Width, + (uint)specs.Height, + pGuid, + (uint)specs.Pitch, + checked((uint)bytes.Length), + pBytes, + tempBitmap.GetAddressOf()).ThrowOnError(); } - else - { - using var tempBitmap = default(ComPtr); - fixed (Guid* pGuid = &GUID.GUID_WICPixelFormat32bppBGRA) - fixed (byte* pBytes = bytes) - { - this.factory.Get()->CreateBitmapFromMemory( - (uint)specs.Width, - (uint)specs.Height, - pGuid, - (uint)specs.Pitch, - checked((uint)bytes.Length), - pBytes, - tempBitmap.GetAddressOf()).ThrowOnError(); - } - using var tempBitmap2 = default(ComPtr); - WICConvertBitmapSource( - &guidPixelFormat, - (IWICBitmapSource*)tempBitmap.Get(), - tempBitmap2.GetAddressOf()).ThrowOnError(); + using var tempBitmap2 = default(ComPtr); + WICConvertBitmapSource( + &guidPixelFormat, + (IWICBitmapSource*)tempBitmap.Get(), + tempBitmap2.GetAddressOf()).ThrowOnError(); - encoderFrame.Get()->SetSize(checked((uint)specs.Width), checked((uint)specs.Height)).ThrowOnError(); - encoderFrame.Get()->WriteSource(tempBitmap2.Get(), null).ThrowOnError(); - } + encoderFrame.Get()->SetSize(checked((uint)specs.Width), checked((uint)specs.Height)).ThrowOnError(); + encoderFrame.Get()->WriteSource(tempBitmap2.Get(), null).ThrowOnError(); cancellationToken.ThrowIfCancellationRequested(); diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index 8e61e42b0..ce9f2a22d 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -18,7 +18,6 @@ using Dalamud.Logging.Internal; using Dalamud.Plugin.Services; using Dalamud.Utility; -using Lumina.Data; using Lumina.Data.Files; using SharpDX; @@ -36,7 +35,7 @@ namespace Dalamud.Interface.Internal; /// Service responsible for loading and disposing ImGui texture wraps. [PluginInterface] [InterfaceVersion("1.0")] -[ServiceManager.BlockingEarlyLoadedService] +[ServiceManager.EarlyLoadedService] #pragma warning disable SA1015 [ResolveVia] [ResolveVia] @@ -96,7 +95,7 @@ internal sealed partial class TextureManager : IServiceType, IDisposable, ITextu null, (uint)CLSCTX.CLSCTX_INPROC_SERVER, piidWicImagingFactory, - (void**)this.factory.GetAddressOf()).ThrowOnError(); + (void**)this.wicFactory.GetAddressOf()).ThrowOnError(); } } } @@ -140,7 +139,7 @@ internal sealed partial class TextureManager : IServiceType, IDisposable, ITextu this.drawsOneSquare?.Dispose(); this.drawsOneSquare = null; - this.factory.Reset(); + this.wicFactory.Reset(); return; @@ -254,7 +253,7 @@ internal sealed partial class TextureManager : IServiceType, IDisposable, ITextu CancellationToken cancellationToken = default) => this.textureLoadThrottler.LoadTextureAsync( new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), - ct => Task.Run(() => this.NoThrottleCreateFromImage(bytes.ToArray()), ct), + ct => Task.Run(() => this.NoThrottleCreateFromImage(bytes.ToArray(), ct), ct), cancellationToken); /// @@ -268,7 +267,7 @@ internal sealed partial class TextureManager : IServiceType, IDisposable, ITextu { await using var ms = stream.CanSeek ? new MemoryStream((int)stream.Length) : new(); await stream.CopyToAsync(ms, ct).ConfigureAwait(false); - return this.NoThrottleCreateFromImage(ms.GetBuffer()); + return this.NoThrottleCreateFromImage(ms.GetBuffer(), ct); }, cancellationToken) .ContinueWith( @@ -464,47 +463,6 @@ internal sealed partial class TextureManager : IServiceType, IDisposable, ITextu } } - /// Creates a texture from the given bytes of an image file. Skips the load throttler; intended to be used - /// from implementation of s. - /// The data. - /// The loaded texture. - internal IDalamudTextureWrap NoThrottleCreateFromImage(ReadOnlyMemory bytes) - { - ObjectDisposedException.ThrowIf(this.disposing, this); - - if (this.interfaceManager.Scene is not { } scene) - { - _ = Service.Get(); - scene = this.interfaceManager.Scene ?? throw new InvalidOperationException(); - } - - var bytesArray = bytes.ToArray(); - var texFileAttemptException = default(Exception); - if (TexFileExtensions.IsPossiblyTexFile2D(bytesArray)) - { - var tf = new TexFile(); - typeof(TexFile).GetProperty(nameof(tf.Data))!.GetSetMethod(true)!.Invoke( - tf, - new object?[] { bytesArray }); - typeof(TexFile).GetProperty(nameof(tf.Reader))!.GetSetMethod(true)!.Invoke( - tf, - new object?[] { new LuminaBinaryReader(bytesArray) }); - // Note: FileInfo and FilePath are not used from TexFile; skip it. - try - { - return this.NoThrottleCreateFromTexFile(tf); - } - catch (Exception e) - { - texFileAttemptException = e; - } - } - - return new DalamudTextureWrap( - scene.LoadImage(bytesArray) - ?? throw texFileAttemptException ?? new("Failed to load image because of an unknown reason.")); - } - /// internal IDalamudTextureWrap NoThrottleCreateFromRaw( RawImageSpecification specs, diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs index a39a48f66..45a3a5331 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -189,6 +189,25 @@ internal class TexWidget : IDataWindowWidget }; } + ImGui.SameLine(); + if (ImGui.Button("Save")) + { + this.fileDialogManager.SaveFileDialog( + "Save texture...", + string.Join( + ',', + this.textureManager + .GetSaveSupportedImageExtensions() + .Select(x => $"{string.Join(" | ", x)}{{{string.Join(',', x)}}}")), + $"Texture {t.Id}.png", + ".png", + (ok, path) => + { + if (ok && t.GetTexture(this.textureManager) is { } source) + Task.Run(() => this.SaveTextureWrap(source, path)); + }); + } + ImGui.SameLine(); if (ImGui.Button("Copy Reference")) runLater = () => this.addedTextures.Add(t.CreateFromSharedLowLevelResource(this.textureManager)); @@ -329,7 +348,7 @@ internal class TexWidget : IDataWindowWidget string.Join( ',', this.textureManager - .GetSupportedImageExtensions() + .GetSaveSupportedImageExtensions() .Select(x => $"{string.Join(" | ", x)}{{{string.Join(',', x)}}}")), Path.ChangeExtension(Path.GetFileName(texture.SourcePathForDebug), ".png"), ".png", @@ -380,8 +399,24 @@ internal class TexWidget : IDataWindowWidget try { using var rented = await texture.RentAsync(); + this.SaveTextureWrap(rented, path); + } + catch (Exception e) + { + Log.Error(e, $"{nameof(TexWidget)}.{nameof(this.SaveImmediateTexture)}"); + Service.Get().AddNotification( + $"Failed to save file: {e}", + this.DisplayName, + NotificationType.Error); + } + } + + private async void SaveTextureWrap(IDalamudTextureWrap texture, string path) + { + try + { await this.textureManager.SaveAsImageFormatToStreamAsync( - rented, + texture, Path.GetExtension(path), File.Create(path), props: new Dictionary diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs index a632f14e4..6f2a56394 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs @@ -31,7 +31,7 @@ namespace Dalamud.Interface.ManagedFontAtlas.Internals; /// /// Factory for the implementation of . /// -[ServiceManager.BlockingEarlyLoadedService] +[ServiceManager.EarlyLoadedService] internal sealed partial class FontAtlasFactory : IServiceType, GamePrebakedFontHandle.IGameFontTextureProvider, IDisposable { diff --git a/Dalamud/Plugin/Services/ITextureProvider.cs b/Dalamud/Plugin/Services/ITextureProvider.cs index 723801c9d..ac6ab8baf 100644 --- a/Dalamud/Plugin/Services/ITextureProvider.cs +++ b/Dalamud/Plugin/Services/ITextureProvider.cs @@ -180,10 +180,15 @@ public partial interface ITextureProvider int dxgiFormat = 0, CancellationToken cancellationToken = default); - /// Gets the supported image file extensions. + /// Gets the supported image file extensions available for loading. /// The supported extensions. Each string[] entry indicates that there can be multiple extensions /// that correspond to one container format. - IEnumerable GetSupportedImageExtensions(); + IEnumerable GetLoadSupportedImageExtensions(); + + /// Gets the supported image file extensions available for saving. + /// The supported extensions. Each string[] entry indicates that there can be multiple extensions + /// that correspond to one container format. + IEnumerable GetSaveSupportedImageExtensions(); /// Saves a texture wrap to a stream in an image file format. /// The texture wrap to save. From 3415df5d403cef5dbf4e4121d81cb540038fa0aa Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sat, 2 Mar 2024 23:43:47 +0900 Subject: [PATCH 34/57] Cleanup --- .../Interface/Internal/TextureManager.Wic.cs | 478 -------------- Dalamud/Interface/Internal/TextureManager.cs | 582 ------------------ .../Windows/Data/Widgets/IconBrowserWidget.cs | 3 +- .../Windows/Data/Widgets/TexWidget.cs | 222 ++++--- .../Internal/Windows/PluginImageCache.cs | 1 + .../PluginInstaller/PluginInstallerWindow.cs | 5 +- .../Internals/FontAtlasFactory.cs | 1 + .../DalamudTextureWrap.cs | 1 + .../Interface/Textures/IBitmapCodecInfo.cs | 19 + .../IDalamudTextureWrap.cs | 3 + .../{ => Textures}/ISharedImmediateTexture.cs | 2 +- .../Textures/Internal/BitmapCodecInfo.cs | 55 ++ .../Internal/DisposeSuppressingTextureWrap.cs | 4 +- .../FileSystemSharedImmediateTexture.cs | 5 +- .../GamePathSharedImmediateTexture.cs | 8 +- .../ManifestResourceSharedImmediateTexture.cs | 5 +- .../SharedImmediateTexture.cs | 3 +- .../Internal/TextureLoadThrottler.cs | 4 +- .../Textures/Internal/TextureManager.Api9.cs | 55 ++ .../TextureManager.FromExistingTexture.cs} | 11 +- .../Internal/TextureManager.GamePath.cs | 120 ++++ .../Internal/TextureManager.SharedTextures.cs | 163 +++++ .../Textures/Internal/TextureManager.Wic.cs | 527 ++++++++++++++++ .../Textures/Internal/TextureManager.cs | 291 +++++++++ .../Internal/UnknownTextureWrap.cs | 3 +- Dalamud/Interface/UiBuilder.cs | 1 + Dalamud/Interface/UldWrapper.cs | 1 + Dalamud/Plugin/Services/ITextureProvider.cs | 71 ++- .../Plugin/Services/RawImageSpecification.cs | 329 +++++----- Dalamud/Storage/Assets/DalamudAssetManager.cs | 1 + .../ManagedIEnumUnknownEnumerator.cs | 59 ++ .../{ => TerraFxCom}/ManagedIStream.cs | 53 +- 32 files changed, 1718 insertions(+), 1368 deletions(-) delete mode 100644 Dalamud/Interface/Internal/TextureManager.Wic.cs delete mode 100644 Dalamud/Interface/Internal/TextureManager.cs rename Dalamud/Interface/{Internal => Textures}/DalamudTextureWrap.cs (97%) create mode 100644 Dalamud/Interface/Textures/IBitmapCodecInfo.cs rename Dalamud/Interface/{Internal => Textures}/IDalamudTextureWrap.cs (96%) rename Dalamud/Interface/{ => Textures}/ISharedImmediateTexture.cs (99%) create mode 100644 Dalamud/Interface/Textures/Internal/BitmapCodecInfo.cs rename Dalamud/Interface/{ => Textures}/Internal/DisposeSuppressingTextureWrap.cs (90%) rename Dalamud/Interface/{ => Textures}/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs (89%) rename Dalamud/Interface/{ => Textures}/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs (85%) rename Dalamud/Interface/{ => Textures}/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs (93%) rename Dalamud/Interface/{ => Textures}/Internal/SharedImmediateTextures/SharedImmediateTexture.cs (99%) rename Dalamud/Interface/{ => Textures}/Internal/TextureLoadThrottler.cs (99%) create mode 100644 Dalamud/Interface/Textures/Internal/TextureManager.Api9.cs rename Dalamud/Interface/{Internal/TextureManager.FormatConvert.cs => Textures/Internal/TextureManager.FromExistingTexture.cs} (98%) create mode 100644 Dalamud/Interface/Textures/Internal/TextureManager.GamePath.cs create mode 100644 Dalamud/Interface/Textures/Internal/TextureManager.SharedTextures.cs create mode 100644 Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs create mode 100644 Dalamud/Interface/Textures/Internal/TextureManager.cs rename Dalamud/Interface/{ => Textures}/Internal/UnknownTextureWrap.cs (96%) create mode 100644 Dalamud/Utility/TerraFxCom/ManagedIEnumUnknownEnumerator.cs rename Dalamud/Utility/{ => TerraFxCom}/ManagedIStream.cs (88%) diff --git a/Dalamud/Interface/Internal/TextureManager.Wic.cs b/Dalamud/Interface/Internal/TextureManager.Wic.cs deleted file mode 100644 index 66be9ca58..000000000 --- a/Dalamud/Interface/Internal/TextureManager.Wic.cs +++ /dev/null @@ -1,478 +0,0 @@ -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; -using System.Numerics; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; - -using Dalamud.Interface.Internal.SharedImmediateTextures; -using Dalamud.Plugin.Services; -using Dalamud.Utility; - -using Lumina.Data; -using Lumina.Data.Files; - -using TerraFX.Interop.DirectX; -using TerraFX.Interop.Windows; - -using static TerraFX.Interop.Windows.Windows; - -namespace Dalamud.Interface.Internal; - -/// Service responsible for loading and disposing ImGui texture wraps. -internal sealed partial class TextureManager -{ - private ComPtr wicFactory; - - /// - [SuppressMessage( - "StyleCop.CSharp.LayoutRules", - "SA1519:Braces should not be omitted from multi-line child statement", - Justification = "Multiple fixed blocks")] - public Task SaveAsImageFormatToStreamAsync( - IDalamudTextureWrap wrap, - string extension, - Stream stream, - bool leaveOpen = false, - IReadOnlyDictionary? props = null, - CancellationToken cancellationToken = default) - { - var container = GUID.GUID_ContainerFormatPng; - foreach (var (k, v) in this.GetSupportedContainerFormats(WICComponentType.WICEncoder)) - { - if (v.Contains(extension, StringComparer.InvariantCultureIgnoreCase)) - container = k; - } - - return this.SaveToStreamUsingWicAsync( - wrap, - container, - pbag => - { - if (props is null) - return; - unsafe - { - var nprop = 0u; - pbag.Get()->CountProperties(&nprop).ThrowOnError(); - for (var i = 0u; i < nprop; i++) - { - var pbag2 = default(PROPBAG2); - var npropread = 0u; - pbag.Get()->GetPropertyInfo(i, 1, &pbag2, &npropread).ThrowOnError(); - if (npropread == 0) - continue; - var propName = new string((char*)pbag2.pstrName); - if (props.TryGetValue(propName, out var untypedValue)) - { - VARIANT val; - VariantInit(&val); - - switch (untypedValue) - { - case null: - val.vt = (ushort)VARENUM.VT_EMPTY; - break; - case bool value: - val.vt = (ushort)VARENUM.VT_BOOL; - val.boolVal = (short)(value ? 1 : 0); - break; - case byte value: - val.vt = (ushort)VARENUM.VT_UI1; - val.bVal = value; - break; - case ushort value: - val.vt = (ushort)VARENUM.VT_UI2; - val.uiVal = value; - break; - case uint value: - val.vt = (ushort)VARENUM.VT_UI4; - val.uintVal = value; - break; - case ulong value: - val.vt = (ushort)VARENUM.VT_UI8; - val.ullVal = value; - break; - case sbyte value: - val.vt = (ushort)VARENUM.VT_I1; - val.cVal = value; - break; - case short value: - val.vt = (ushort)VARENUM.VT_I2; - val.iVal = value; - break; - case int value: - val.vt = (ushort)VARENUM.VT_I4; - val.intVal = value; - break; - case long value: - val.vt = (ushort)VARENUM.VT_I8; - val.llVal = value; - break; - case float value: - val.vt = (ushort)VARENUM.VT_R4; - val.fltVal = value; - break; - case double value: - val.vt = (ushort)VARENUM.VT_R8; - val.dblVal = value; - break; - default: - VariantClear(&val); - continue; - } - - VariantChangeType(&val, &val, 0, pbag2.vt).ThrowOnError(); - pbag.Get()->Write(1, &pbag2, &val).ThrowOnError(); - VariantClear(&val); - } - - CoTaskMemFree(pbag2.pstrName); - } - } - }, - stream, - leaveOpen, - cancellationToken); - } - - /// - public IEnumerable GetLoadSupportedImageExtensions() => - this.GetSupportedContainerFormats(WICComponentType.WICDecoder).Values; - - /// - public IEnumerable GetSaveSupportedImageExtensions() => - this.GetSupportedContainerFormats(WICComponentType.WICEncoder).Values; - - /// Creates a texture from the given bytes of an image file. Skips the load throttler; intended to be used - /// from implementation of s. - /// The data. - /// The cancellation token. - /// The loaded texture. - internal unsafe IDalamudTextureWrap NoThrottleCreateFromImage( - ReadOnlyMemory bytes, - CancellationToken cancellationToken = default) - { - ObjectDisposedException.ThrowIf(this.disposing, this); - - cancellationToken.ThrowIfCancellationRequested(); - - if (TexFileExtensions.IsPossiblyTexFile2D(bytes.Span)) - { - var bytesArray = bytes.ToArray(); - var tf = new TexFile(); - typeof(TexFile).GetProperty(nameof(tf.Data))!.GetSetMethod(true)!.Invoke( - tf, - new object?[] { bytesArray }); - typeof(TexFile).GetProperty(nameof(tf.Reader))!.GetSetMethod(true)!.Invoke( - tf, - new object?[] { new LuminaBinaryReader(bytesArray) }); - // Note: FileInfo and FilePath are not used from TexFile; skip it. - try - { - return this.NoThrottleCreateFromTexFile(tf); - } - catch (Exception) - { - // ignore - } - } - - fixed (byte* p = bytes.Span) - { - using var wicStream = default(ComPtr); - this.wicFactory.Get()->CreateStream(wicStream.GetAddressOf()).ThrowOnError(); - wicStream.Get()->InitializeFromMemory(p, checked((uint)bytes.Length)).ThrowOnError(); - return this.NoThrottleCreateFromWicStream((IStream*)wicStream.Get(), cancellationToken); - } - } - - /// Creates a texture from the given path to an image file. Skips the load throttler; intended to be used - /// from implementation of s. - /// The path of the file.. - /// The cancellation token. - /// The loaded texture. - internal async Task NoThrottleCreateFromFileAsync( - string path, - CancellationToken cancellationToken = default) - { - ObjectDisposedException.ThrowIf(this.disposing, this); - - cancellationToken.ThrowIfCancellationRequested(); - - try - { - unsafe - { - fixed (char* pPath = path) - { - using var wicStream = default(ComPtr); - this.wicFactory.Get()->CreateStream(wicStream.GetAddressOf()).ThrowOnError(); - wicStream.Get()->InitializeFromFilename((ushort*)pPath, GENERIC_READ).ThrowOnError(); - return this.NoThrottleCreateFromWicStream((IStream*)wicStream.Get(), cancellationToken); - } - } - } - catch - { - try - { - await using var fp = File.OpenRead(path); - if (fp.Length >= Unsafe.SizeOf()) - { - var bytesArray = new byte[fp.Length]; - await fp.ReadExactlyAsync(bytesArray, cancellationToken); - if (TexFileExtensions.IsPossiblyTexFile2D(bytesArray)) - { - var tf = new TexFile(); - typeof(TexFile).GetProperty(nameof(tf.Data))!.GetSetMethod(true)!.Invoke( - tf, - new object?[] { bytesArray }); - typeof(TexFile).GetProperty(nameof(tf.Reader))!.GetSetMethod(true)!.Invoke( - tf, - new object?[] { new LuminaBinaryReader(bytesArray) }); - // Note: FileInfo and FilePath are not used from TexFile; skip it. - return this.NoThrottleCreateFromTexFile(tf); - } - } - } - catch (Exception) - { - // ignore - } - - throw; - } - } - - /// - /// Gets the corresponding from a containing a WIC pixel format. - /// - /// The WIC pixel format. - /// The corresponding , or if - /// unavailable. - private static DXGI_FORMAT GetCorrespondingDxgiFormat(Guid fmt) => 0 switch - { - // See https://github.com/microsoft/DirectXTex/wiki/WIC-I-O-Functions#savetowicmemory-savetowicfile - _ when fmt == GUID.GUID_WICPixelFormat128bppRGBAFloat => DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_FLOAT, - _ when fmt == GUID.GUID_WICPixelFormat64bppRGBAHalf => DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_FLOAT, - _ when fmt == GUID.GUID_WICPixelFormat64bppRGBA => DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_UNORM, - _ when fmt == GUID.GUID_WICPixelFormat32bppRGBA1010102XR => DXGI_FORMAT.DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM, - _ when fmt == GUID.GUID_WICPixelFormat32bppRGBA1010102 => DXGI_FORMAT.DXGI_FORMAT_R10G10B10A2_UNORM, - _ when fmt == GUID.GUID_WICPixelFormat16bppBGRA5551 => DXGI_FORMAT.DXGI_FORMAT_B5G5R5A1_UNORM, - _ when fmt == GUID.GUID_WICPixelFormat16bppBGR565 => DXGI_FORMAT.DXGI_FORMAT_B5G6R5_UNORM, - _ when fmt == GUID.GUID_WICPixelFormat32bppGrayFloat => DXGI_FORMAT.DXGI_FORMAT_R32_FLOAT, - _ when fmt == GUID.GUID_WICPixelFormat16bppGrayHalf => DXGI_FORMAT.DXGI_FORMAT_R16_FLOAT, - _ when fmt == GUID.GUID_WICPixelFormat16bppGray => DXGI_FORMAT.DXGI_FORMAT_R16_UNORM, - _ when fmt == GUID.GUID_WICPixelFormat8bppGray => DXGI_FORMAT.DXGI_FORMAT_R8_UNORM, - _ when fmt == GUID.GUID_WICPixelFormat8bppAlpha => DXGI_FORMAT.DXGI_FORMAT_A8_UNORM, - _ when fmt == GUID.GUID_WICPixelFormat32bppRGBA => DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM, - _ when fmt == GUID.GUID_WICPixelFormat32bppBGRA => DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM, - _ when fmt == GUID.GUID_WICPixelFormat32bppBGR => DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM, - _ => DXGI_FORMAT.DXGI_FORMAT_UNKNOWN, - }; - - private unsafe IDalamudTextureWrap NoThrottleCreateFromWicStream( - IStream* wicStream, - CancellationToken cancellationToken = default) - { - cancellationToken.ThrowIfCancellationRequested(); - - using var decoder = default(ComPtr); - this.wicFactory.Get()->CreateDecoderFromStream( - wicStream, - null, - WICDecodeOptions.WICDecodeMetadataCacheOnDemand, - decoder.GetAddressOf()).ThrowOnError(); - - cancellationToken.ThrowIfCancellationRequested(); - - using var frame = default(ComPtr); - decoder.Get()->GetFrame(0, frame.GetAddressOf()).ThrowOnError(); - var pixelFormat = default(Guid); - frame.Get()->GetPixelFormat(&pixelFormat).ThrowOnError(); - var dxgiFormat = GetCorrespondingDxgiFormat(pixelFormat); - - cancellationToken.ThrowIfCancellationRequested(); - - using var bitmapSource = default(ComPtr); - if (dxgiFormat == DXGI_FORMAT.DXGI_FORMAT_UNKNOWN || !this.IsDxgiFormatSupported(dxgiFormat)) - { - dxgiFormat = DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM; - pixelFormat = GUID.GUID_WICPixelFormat32bppBGRA; - WICConvertBitmapSource(&pixelFormat, (IWICBitmapSource*)frame.Get(), bitmapSource.GetAddressOf()) - .ThrowOnError(); - } - else - { - frame.As(&bitmapSource); - } - - cancellationToken.ThrowIfCancellationRequested(); - - using var bitmap = default(ComPtr); - using var bitmapLock = default(ComPtr); - WICRect rcLock; - uint stride; - uint cbBufferSize; - byte* pbData; - if (bitmapSource.As(&bitmap).FAILED) - { - bitmapSource.Get()->GetSize((uint*)&rcLock.Width, (uint*)&rcLock.Height).ThrowOnError(); - this.wicFactory.Get()->CreateBitmap( - (uint)rcLock.Width, - (uint)rcLock.Height, - &pixelFormat, - WICBitmapCreateCacheOption.WICBitmapCacheOnDemand, - bitmap.GetAddressOf()).ThrowOnError(); - - bitmap.Get()->Lock( - &rcLock, - (uint)WICBitmapLockFlags.WICBitmapLockWrite, - bitmapLock.ReleaseAndGetAddressOf()) - .ThrowOnError(); - bitmapLock.Get()->GetStride(&stride).ThrowOnError(); - bitmapLock.Get()->GetDataPointer(&cbBufferSize, &pbData).ThrowOnError(); - bitmapSource.Get()->CopyPixels(null, stride, cbBufferSize, pbData).ThrowOnError(); - } - - cancellationToken.ThrowIfCancellationRequested(); - - bitmap.Get()->Lock( - &rcLock, - (uint)WICBitmapLockFlags.WICBitmapLockRead, - bitmapLock.ReleaseAndGetAddressOf()) - .ThrowOnError(); - bitmapSource.Get()->GetSize((uint*)&rcLock.Width, (uint*)&rcLock.Height).ThrowOnError(); - bitmapLock.Get()->GetStride(&stride).ThrowOnError(); - bitmapLock.Get()->GetDataPointer(&cbBufferSize, &pbData).ThrowOnError(); - bitmapSource.Get()->CopyPixels(null, stride, cbBufferSize, pbData).ThrowOnError(); - return this.NoThrottleCreateFromRaw( - new RawImageSpecification(rcLock.Width, rcLock.Height, (int)stride, (int)dxgiFormat), - new(pbData, (int)cbBufferSize)); - } - - [SuppressMessage( - "StyleCop.CSharp.LayoutRules", - "SA1519:Braces should not be omitted from multi-line child statement", - Justification = "Multiple fixed blocks")] - private unsafe Dictionary GetSupportedContainerFormats(WICComponentType componentType) - { - var result = new Dictionary(); - using var enumUnknown = default(ComPtr); - this.wicFactory.Get()->CreateComponentEnumerator( - (uint)componentType, - (uint)WICComponentEnumerateOptions.WICComponentEnumerateDefault, - enumUnknown.GetAddressOf()).ThrowOnError(); - - while (true) - { - using var entry = default(ComPtr); - var fetched = 0u; - enumUnknown.Get()->Next(1, entry.GetAddressOf(), &fetched).ThrowOnError(); - if (fetched == 0) - break; - - using var codecInfo = default(ComPtr); - if (entry.As(&codecInfo).FAILED) - continue; - - Guid containerFormat; - if (codecInfo.Get()->GetContainerFormat(&containerFormat).FAILED) - continue; - - var cch = 0u; - _ = codecInfo.Get()->GetFileExtensions(0, null, &cch); - var buf = new char[(int)cch + 1]; - fixed (char* pBuf = buf) - { - if (codecInfo.Get()->GetFileExtensions(cch + 1, (ushort*)pBuf, &cch).FAILED) - continue; - } - - result.Add(containerFormat, new string(buf, 0, buf.IndexOf('\0')).Split(",")); - } - - return result; - } - - [SuppressMessage( - "StyleCop.CSharp.LayoutRules", - "SA1519:Braces should not be omitted from multi-line child statement", - Justification = "Multiple fixed blocks")] - private async Task SaveToStreamUsingWicAsync( - IDalamudTextureWrap wrap, - Guid containerFormat, - Action> propertyBackSetterDelegate, - Stream stream, - bool leaveOpen = false, - CancellationToken cancellationToken = default) - { - using var wrapCopy = wrap.CreateWrapSharingLowLevelResource(); - await using var streamCloser = leaveOpen ? null : stream; - - var (specs, bytes) = await this.GetRawDataAsync( - wrapCopy, - Vector2.Zero, - Vector2.One, - DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM, - cancellationToken).ConfigureAwait(false); - - using var encoder = default(ComPtr); - using var encoderFrame = default(ComPtr); - using var wrappedStream = new ManagedIStream(stream); - var guidPixelFormat = GUID.GUID_WICPixelFormat32bppBGRA; - unsafe - { - this.wicFactory.Get()->CreateEncoder(&containerFormat, null, encoder.GetAddressOf()).ThrowOnError(); - cancellationToken.ThrowIfCancellationRequested(); - - encoder.Get()->Initialize(wrappedStream, WICBitmapEncoderCacheOption.WICBitmapEncoderNoCache) - .ThrowOnError(); - cancellationToken.ThrowIfCancellationRequested(); - - using var propertyBag = default(ComPtr); - encoder.Get()->CreateNewFrame(encoderFrame.GetAddressOf(), propertyBag.GetAddressOf()).ThrowOnError(); - cancellationToken.ThrowIfCancellationRequested(); - - propertyBackSetterDelegate.Invoke(propertyBag); - encoderFrame.Get()->Initialize(propertyBag).ThrowOnError(); - cancellationToken.ThrowIfCancellationRequested(); - - encoderFrame.Get()->SetPixelFormat(&guidPixelFormat).ThrowOnError(); - encoderFrame.Get()->SetSize(checked((uint)specs.Width), checked((uint)specs.Height)).ThrowOnError(); - - using var tempBitmap = default(ComPtr); - fixed (Guid* pGuid = &GUID.GUID_WICPixelFormat32bppBGRA) - fixed (byte* pBytes = bytes) - { - this.wicFactory.Get()->CreateBitmapFromMemory( - (uint)specs.Width, - (uint)specs.Height, - pGuid, - (uint)specs.Pitch, - checked((uint)bytes.Length), - pBytes, - tempBitmap.GetAddressOf()).ThrowOnError(); - } - - using var tempBitmap2 = default(ComPtr); - WICConvertBitmapSource( - &guidPixelFormat, - (IWICBitmapSource*)tempBitmap.Get(), - tempBitmap2.GetAddressOf()).ThrowOnError(); - - encoderFrame.Get()->SetSize(checked((uint)specs.Width), checked((uint)specs.Height)).ThrowOnError(); - encoderFrame.Get()->WriteSource(tempBitmap2.Get(), null).ThrowOnError(); - - cancellationToken.ThrowIfCancellationRequested(); - - encoderFrame.Get()->Commit().ThrowOnError(); - cancellationToken.ThrowIfCancellationRequested(); - - encoder.Get()->Commit().ThrowOnError(); - } - } -} diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs deleted file mode 100644 index ce9f2a22d..000000000 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ /dev/null @@ -1,582 +0,0 @@ -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; - -using BitFaster.Caching.Lru; - -using Dalamud.Data; -using Dalamud.Game; -using Dalamud.Interface.Internal.SharedImmediateTextures; -using Dalamud.IoC; -using Dalamud.IoC.Internal; -using Dalamud.Logging.Internal; -using Dalamud.Plugin.Services; -using Dalamud.Utility; - -using Lumina.Data.Files; - -using SharpDX; -using SharpDX.Direct3D; -using SharpDX.Direct3D11; -using SharpDX.DXGI; - -using TerraFX.Interop.DirectX; -using TerraFX.Interop.Windows; - -using static TerraFX.Interop.Windows.Windows; - -namespace Dalamud.Interface.Internal; - -/// Service responsible for loading and disposing ImGui texture wraps. -[PluginInterface] -[InterfaceVersion("1.0")] -[ServiceManager.EarlyLoadedService] -#pragma warning disable SA1015 -[ResolveVia] -[ResolveVia] -#pragma warning restore SA1015 -internal sealed partial 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 static readonly ModuleLog Log = new(nameof(TextureManager)); - - [ServiceManager.ServiceDependency] - private readonly Dalamud dalamud = Service.Get(); - - [ServiceManager.ServiceDependency] - private readonly DataManager dataManager = Service.Get(); - - [ServiceManager.ServiceDependency] - private readonly Framework framework = Service.Get(); - - [ServiceManager.ServiceDependency] - private readonly InterfaceManager interfaceManager = Service.Get(); - - [ServiceManager.ServiceDependency] - private readonly TextureLoadThrottler textureLoadThrottler = Service.Get(); - - private readonly ConcurrentLru lookupToPath = new(PathLookupLruCount); - - private readonly ConcurrentDictionary gamePathTextures = new(); - - private readonly ConcurrentDictionary fileSystemTextures = new(); - - private readonly ConcurrentDictionary<(Assembly Assembly, string Name), SharedImmediateTexture> - manifestResourceTextures = new(); - - private readonly HashSet invalidatedTextures = new(); - - private bool disposing; - - [SuppressMessage( - "StyleCop.CSharp.LayoutRules", - "SA1519:Braces should not be omitted from multi-line child statement", - Justification = "Multiple fixed blocks")] - [ServiceManager.ServiceConstructor] - private TextureManager() - { - this.framework.Update += this.FrameworkOnUpdate; - unsafe - { - fixed (Guid* pclsidWicImagingFactory = &CLSID.CLSID_WICImagingFactory) - fixed (Guid* piidWicImagingFactory = &IID.IID_IWICImagingFactory) - { - CoCreateInstance( - pclsidWicImagingFactory, - null, - (uint)CLSCTX.CLSCTX_INPROC_SERVER, - piidWicImagingFactory, - (void**)this.wicFactory.GetAddressOf()).ThrowOnError(); - } - } - } - - /// Finalizes an instance of the class. - ~TextureManager() => this.Dispose(); - - /// - public event ITextureSubstitutionProvider.TextureDataInterceptorDelegate? InterceptTexDataLoad; - - /// Gets all the loaded textures from game resources. - public ICollection GamePathTexturesForDebug => this.gamePathTextures.Values; - - /// Gets all the loaded textures from filesystem. - public ICollection FileSystemTexturesForDebug => this.fileSystemTextures.Values; - - /// Gets all the loaded textures from assembly manifest resources. - public ICollection ManifestResourceTexturesForDebug => this.manifestResourceTextures.Values; - - /// Gets all the loaded textures that are invalidated from . - /// lock on use of the value returned from this property. - [SuppressMessage( - "ReSharper", - "InconsistentlySynchronizedField", - Justification = "Debug use only; users are expected to lock around this")] - public ICollection InvalidatedTexturesForDebug => this.invalidatedTextures; - - /// - public void Dispose() - { - if (this.disposing) - return; - - this.disposing = true; - - ReleaseSelfReferences(this.gamePathTextures); - ReleaseSelfReferences(this.fileSystemTextures); - ReleaseSelfReferences(this.manifestResourceTextures); - this.lookupToPath.Clear(); - - this.drawsOneSquare?.Dispose(); - this.drawsOneSquare = null; - - this.wicFactory.Reset(); - - return; - - static void ReleaseSelfReferences(ConcurrentDictionary dict) - { - foreach (var v in dict.Values) - v.ReleaseSelfReference(true); - dict.Clear(); - } - } - - #region API9 compat - -#pragma warning disable CS0618 // Type or member is obsolete - /// - [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] - [Obsolete("See interface definition.")] - string? ITextureProvider.GetIconPath(uint iconId, ITextureProvider.IconFlags flags, ClientLanguage? language) - => 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.")] - IDalamudTextureWrap? ITextureProvider.GetIcon( - uint iconId, - ITextureProvider.IconFlags flags, - ClientLanguage? language, - bool keepAlive) => - this.GetFromGameIcon( - new( - iconId, - (flags & ITextureProvider.IconFlags.ItemHighQuality) != 0, - (flags & ITextureProvider.IconFlags.HiRes) != 0, - language)) - .GetAvailableOnAccessWrapForApi9(); - - /// - [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] - [Obsolete("See interface definition.")] - IDalamudTextureWrap? ITextureProvider.GetTextureFromGame(string path, bool keepAlive) => - this.GetFromGame(path).GetAvailableOnAccessWrapForApi9(); - - /// - [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] - [Obsolete("See interface definition.")] - IDalamudTextureWrap? ITextureProvider.GetTextureFromFile(FileInfo file, bool keepAlive) => - this.GetFromFile(file.FullName).GetAvailableOnAccessWrapForApi9(); -#pragma warning restore CS0618 // Type or member is obsolete - - #endregion - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public SharedImmediateTexture GetFromGameIcon(in GameIconLookup lookup) => - this.GetFromGame(this.lookupToPath.GetOrAdd(lookup, this.GetIconPathByValue)); - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public SharedImmediateTexture GetFromGame(string path) - { - ObjectDisposedException.ThrowIf(this.disposing, this); - return this.gamePathTextures.GetOrAdd(path, GamePathSharedImmediateTexture.CreatePlaceholder); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public SharedImmediateTexture GetFromFile(string path) - { - ObjectDisposedException.ThrowIf(this.disposing, this); - return this.fileSystemTextures.GetOrAdd(path, FileSystemSharedImmediateTexture.CreatePlaceholder); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public SharedImmediateTexture GetFromManifestResource(Assembly assembly, string name) - { - ObjectDisposedException.ThrowIf(this.disposing, this); - return this.manifestResourceTextures.GetOrAdd( - (assembly, name), - ManifestResourceSharedImmediateTexture.CreatePlaceholder); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - ISharedImmediateTexture ITextureProvider.GetFromGameIcon(in GameIconLookup lookup) => this.GetFromGameIcon(lookup); - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - ISharedImmediateTexture ITextureProvider.GetFromGame(string path) => this.GetFromGame(path); - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - ISharedImmediateTexture ITextureProvider.GetFromFile(string path) => this.GetFromFile(path); - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - ISharedImmediateTexture ITextureProvider.GetFromManifestResource(Assembly assembly, string name) => - this.GetFromManifestResource(assembly, name); - - /// - public Task CreateFromImageAsync( - ReadOnlyMemory bytes, - CancellationToken cancellationToken = default) => - this.textureLoadThrottler.LoadTextureAsync( - new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), - ct => Task.Run(() => this.NoThrottleCreateFromImage(bytes.ToArray(), ct), ct), - cancellationToken); - - /// - public Task CreateFromImageAsync( - 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(); - - /// - // It probably doesn't make sense to throttle this, as it copies the passed bytes to GPU without any transformation. - public IDalamudTextureWrap CreateFromRaw( - RawImageSpecification specs, - ReadOnlySpan bytes) => this.NoThrottleCreateFromRaw(specs, bytes); - - /// - public Task CreateFromRawAsync( - RawImageSpecification specs, - ReadOnlyMemory bytes, - CancellationToken cancellationToken = default) => - this.textureLoadThrottler.LoadTextureAsync( - new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), - _ => Task.FromResult(this.NoThrottleCreateFromRaw(specs, bytes.Span)), - cancellationToken); - - /// - public Task CreateFromRawAsync( - RawImageSpecification specs, - 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(); - - /// - public IDalamudTextureWrap CreateFromTexFile(TexFile file) => this.CreateFromTexFileAsync(file).Result; - - /// - public Task CreateFromTexFileAsync( - TexFile file, - CancellationToken cancellationToken = default) => - this.textureLoadThrottler.LoadTextureAsync( - new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), - ct => Task.Run(() => this.NoThrottleCreateFromTexFile(file), ct), - cancellationToken); - - /// - bool ITextureProvider.IsDxgiFormatSupported(int dxgiFormat) => - this.IsDxgiFormatSupported((DXGI_FORMAT)dxgiFormat); - - /// - public bool IsDxgiFormatSupported(DXGI_FORMAT dxgiFormat) - { - if (this.interfaceManager.Scene is not { } scene) - { - _ = Service.Get(); - scene = this.interfaceManager.Scene ?? throw new InvalidOperationException(); - } - - var format = (Format)dxgiFormat; - var support = scene.Device.CheckFormatSupport(format); - const FormatSupport required = FormatSupport.Texture2D; - return (support & required) == required; - } - - /// - public bool TryGetIconPath(in GameIconLookup lookup, out string path) - { - // 1. Item - path = FormatIconPath( - lookup.IconId, - lookup.ItemHq ? "hq/" : string.Empty, - lookup.HiRes); - if (this.dataManager.FileExists(path)) - 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/", - _ => null, - }; - - if (languageFolder is not null) - { - // 2. Regular icon, with language, hi-res - path = FormatIconPath( - lookup.IconId, - languageFolder, - lookup.HiRes); - if (this.dataManager.FileExists(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( - lookup.IconId, - null, - lookup.HiRes); - if (this.dataManager.FileExists(path)) - return true; - - // 4. Regular icon, without language, no hi-res - if (lookup.HiRes) - { - path = FormatIconPath( - lookup.IconId, - null, - false); - if (this.dataManager.FileExists(path)) - return true; - } - - return false; - } - - /// - 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); - - if (interceptPath != null) - { - Log.Verbose("Intercept: {OriginalPath} => {ReplacePath}", originalPath, interceptPath); - return interceptPath; - } - - return originalPath; - } - - /// - public void InvalidatePaths(IEnumerable paths) - { - foreach (var path in paths) - { - if (this.gamePathTextures.TryRemove(path, out var r)) - { - if (r.ReleaseSelfReference(true) != 0 || r.HasRevivalPossibility) - { - lock (this.invalidatedTextures) - this.invalidatedTextures.Add(r); - } - } - - if (this.fileSystemTextures.TryRemove(path, out r)) - { - if (r.ReleaseSelfReference(true) != 0 || r.HasRevivalPossibility) - { - lock (this.invalidatedTextures) - this.invalidatedTextures.Add(r); - } - } - } - } - - /// - internal IDalamudTextureWrap NoThrottleCreateFromRaw( - RawImageSpecification specs, - ReadOnlySpan bytes) - { - if (this.interfaceManager.Scene is not { } scene) - { - _ = Service.Get(); - scene = this.interfaceManager.Scene ?? throw new InvalidOperationException(); - } - - ShaderResourceView resView; - unsafe - { - fixed (void* pData = bytes) - { - var texDesc = new Texture2DDescription - { - Width = specs.Width, - Height = specs.Height, - MipLevels = 1, - ArraySize = 1, - Format = (Format)specs.DxgiFormat, - SampleDescription = new(1, 0), - Usage = ResourceUsage.Immutable, - BindFlags = BindFlags.ShaderResource, - CpuAccessFlags = CpuAccessFlags.None, - OptionFlags = ResourceOptionFlags.None, - }; - - using var texture = new Texture2D(scene.Device, texDesc, new DataRectangle(new(pData), specs.Pitch)); - resView = new( - scene.Device, - texture, - new() - { - Format = texDesc.Format, - Dimension = ShaderResourceViewDimension.Texture2D, - Texture2D = { MipLevels = texDesc.MipLevels }, - }); - } - } - - // no sampler for now because the ImGui implementation we copied doesn't allow for changing it - return new DalamudTextureWrap(new D3DTextureWrap(resView, specs.Width, specs.Height)); - } - - /// Creates a texture from the given . Skips the load throttler; intended to be used - /// from implementation of s. - /// The data. - /// The loaded texture. - internal IDalamudTextureWrap NoThrottleCreateFromTexFile(TexFile file) - { - ObjectDisposedException.ThrowIf(this.disposing, this); - - var buffer = file.TextureBuffer; - var (dxgiFormat, conversion) = TexFile.GetDxgiFormatFromTextureFormat(file.Header.Format, false); - if (conversion != TexFile.DxgiFormatConversion.NoConversion || - !this.IsDxgiFormatSupported((DXGI_FORMAT)dxgiFormat)) - { - dxgiFormat = (int)Format.B8G8R8A8_UNorm; - buffer = buffer.Filter(0, 0, TexFile.TextureFormat.B8G8R8A8); - } - - return this.NoThrottleCreateFromRaw( - RawImageSpecification.From(buffer.Width, buffer.Height, dxgiFormat), - buffer.RawData); - } - - 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 void FrameworkOnUpdate(IFramework unused) - { - RemoveFinalReleased(this.gamePathTextures); - RemoveFinalReleased(this.fileSystemTextures); - RemoveFinalReleased(this.manifestResourceTextures); - - // ReSharper disable once InconsistentlySynchronizedField - if (this.invalidatedTextures.Count != 0) - { - lock (this.invalidatedTextures) - this.invalidatedTextures.RemoveWhere(TextureFinalReleasePredicate); - } - - return; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - static void RemoveFinalReleased(ConcurrentDictionary dict) - { - if (!dict.IsEmpty) - { - foreach (var (k, v) in dict) - { - if (TextureFinalReleasePredicate(v)) - _ = dict.TryRemove(k, out _); - } - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - static bool TextureFinalReleasePredicate(SharedImmediateTexture v) => - v.ContentQueried && v.ReleaseSelfReference(false) == 0 && !v.HasRevivalPossibility; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private string GetIconPathByValue(GameIconLookup lookup) => - this.TryGetIconPath(lookup, out var path) ? path : throw new FileNotFoundException(); -} diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs index 1f4e5f29d..b7b897e68 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs @@ -3,6 +3,7 @@ using System.Numerics; using System.Threading.Tasks; using Dalamud.Interface.Colors; +using Dalamud.Interface.Textures.Internal; using Dalamud.Interface.Utility; using ImGuiNET; @@ -141,7 +142,7 @@ public class IconBrowserWidget : IDataWindowWidget var texm = Service.Get(); var cursor = ImGui.GetCursorScreenPos(); - if (texm.GetFromGameIcon(new((uint)iconId)).TryGetWrap(out var texture, out var exc)) + if (texm.Shared.GetFromGameIcon(new((uint)iconId)).TryGetWrap(out var texture, out var exc)) { ImGui.Image(texture.ImGuiHandle, this.iconSize); diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs index 45a3a5331..6779d0d60 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -9,7 +9,8 @@ using System.Threading.Tasks; using Dalamud.Interface.Components; using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.Internal.Notifications; -using Dalamud.Interface.Internal.SharedImmediateTextures; +using Dalamud.Interface.Textures.Internal; +using Dalamud.Interface.Textures.Internal.SharedImmediateTextures; using Dalamud.Interface.Utility; using Dalamud.Plugin.Services; using Dalamud.Storage.Assets; @@ -21,6 +22,8 @@ using Serilog; using TerraFX.Interop.DirectX; +using TextureManager = Dalamud.Interface.Textures.Internal.TextureManager; + namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// @@ -92,29 +95,29 @@ internal class TexWidget : IDataWindowWidget ImGui.PushID("loadedGameTextures"); if (ImGui.CollapsingHeader( - $"Loaded Game Textures: {this.textureManager.GamePathTexturesForDebug.Count:g}###header")) - this.DrawLoadedTextures(this.textureManager.GamePathTexturesForDebug); + $"Loaded Game Textures: {this.textureManager.Shared.ForDebugGamePathTextures.Count:g}###header")) + this.DrawLoadedTextures(this.textureManager.Shared.ForDebugGamePathTextures); ImGui.PopID(); ImGui.PushID("loadedFileTextures"); if (ImGui.CollapsingHeader( - $"Loaded File Textures: {this.textureManager.FileSystemTexturesForDebug.Count:g}###header")) - this.DrawLoadedTextures(this.textureManager.FileSystemTexturesForDebug); + $"Loaded File Textures: {this.textureManager.Shared.ForDebugFileSystemTextures.Count:g}###header")) + this.DrawLoadedTextures(this.textureManager.Shared.ForDebugFileSystemTextures); ImGui.PopID(); ImGui.PushID("loadedManifestResourceTextures"); if (ImGui.CollapsingHeader( - $"Loaded Manifest Resource Textures: {this.textureManager.ManifestResourceTexturesForDebug.Count:g}###header")) - this.DrawLoadedTextures(this.textureManager.ManifestResourceTexturesForDebug); + $"Loaded Manifest Resource Textures: {this.textureManager.Shared.ForDebugManifestResourceTextures.Count:g}###header")) + this.DrawLoadedTextures(this.textureManager.Shared.ForDebugManifestResourceTextures); ImGui.PopID(); - lock (this.textureManager.InvalidatedTexturesForDebug) + lock (this.textureManager.Shared.ForDebugInvalidatedTextures) { ImGui.PushID("invalidatedTextures"); if (ImGui.CollapsingHeader( - $"Invalidated: {this.textureManager.InvalidatedTexturesForDebug.Count:g}###header")) + $"Invalidated: {this.textureManager.Shared.ForDebugInvalidatedTextures.Count:g}###header")) { - this.DrawLoadedTextures(this.textureManager.InvalidatedTexturesForDebug); + this.DrawLoadedTextures(this.textureManager.Shared.ForDebugInvalidatedTextures); } ImGui.PopID(); @@ -192,20 +195,9 @@ internal class TexWidget : IDataWindowWidget ImGui.SameLine(); if (ImGui.Button("Save")) { - this.fileDialogManager.SaveFileDialog( - "Save texture...", - string.Join( - ',', - this.textureManager - .GetSaveSupportedImageExtensions() - .Select(x => $"{string.Join(" | ", x)}{{{string.Join(',', x)}}}")), - $"Texture {t.Id}.png", - ".png", - (ok, path) => - { - if (ok && t.GetTexture(this.textureManager) is { } source) - Task.Run(() => this.SaveTextureWrap(source, path)); - }); + this.SaveTextureAsync( + $"Texture {t.Id}", + () => t.CreateNewTextureWrapReference(this.textureManager)); } ImGui.SameLine(); @@ -244,7 +236,7 @@ internal class TexWidget : IDataWindowWidget } else { - ImGui.TextUnformatted(t.DescribeError()); + ImGui.TextUnformatted(t.DescribeError() ?? "Loading"); } } catch (Exception e) @@ -343,20 +335,8 @@ internal class TexWidget : IDataWindowWidget ImGui.TableNextColumn(); if (ImGuiComponents.IconButton(FontAwesomeIcon.Save)) { - this.fileDialogManager.SaveFileDialog( - "Save texture...", - string.Join( - ',', - this.textureManager - .GetSaveSupportedImageExtensions() - .Select(x => $"{string.Join(" | ", x)}{{{string.Join(',', x)}}}")), - Path.ChangeExtension(Path.GetFileName(texture.SourcePathForDebug), ".png"), - ".png", - (ok, path) => - { - if (ok) - Task.Run(() => this.SaveImmediateTexture(texture, path)); - }); + var name = Path.ChangeExtension(Path.GetFileName(texture.SourcePathForDebug), null); + this.SaveTextureAsync(name, () => texture.RentAsync()); } if (ImGui.IsItemHovered() && texture.GetWrapOrDefault(null) is { } immediate) @@ -394,53 +374,6 @@ internal class TexWidget : IDataWindowWidget ImGuiHelpers.ScaledDummy(10); } - private async void SaveImmediateTexture(ISharedImmediateTexture texture, string path) - { - try - { - using var rented = await texture.RentAsync(); - this.SaveTextureWrap(rented, path); - } - catch (Exception e) - { - Log.Error(e, $"{nameof(TexWidget)}.{nameof(this.SaveImmediateTexture)}"); - Service.Get().AddNotification( - $"Failed to save file: {e}", - this.DisplayName, - NotificationType.Error); - } - } - - private async void SaveTextureWrap(IDalamudTextureWrap texture, string path) - { - try - { - await this.textureManager.SaveAsImageFormatToStreamAsync( - texture, - Path.GetExtension(path), - File.Create(path), - props: new Dictionary - { - ["CompressionQuality"] = 1.0f, - ["ImageQuality"] = 1.0f, - }); - } - catch (Exception e) - { - Log.Error(e, $"{nameof(TexWidget)}.{nameof(this.SaveImmediateTexture)}"); - Service.Get().AddNotification( - $"Failed to save file: {e}", - this.DisplayName, - NotificationType.Error); - return; - } - - Service.Get().AddNotification( - $"File saved to: {path}", - this.DisplayName, - NotificationType.Success); - } - private void DrawGetFromGameIcon() { ImGui.InputText("Icon ID", ref this.iconId, 32); @@ -464,6 +397,7 @@ internal class TexWidget : IDataWindowWidget this.addedTextures.Add( new( Api10: this.textureManager + .Shared .GetFromGameIcon(new(uint.Parse(this.iconId), this.hq, this.hiRes)) .RentAsync())); } @@ -486,7 +420,7 @@ internal class TexWidget : IDataWindowWidget ImGui.SameLine(); if (ImGui.Button("Load Tex (Async)")) - this.addedTextures.Add(new(Api10: this.textureManager.GetFromGame(this.inputTexPath).RentAsync())); + this.addedTextures.Add(new(Api10: this.textureManager.Shared.GetFromGame(this.inputTexPath).RentAsync())); ImGui.SameLine(); if (ImGui.Button("Load Tex (Immediate)")) @@ -506,7 +440,7 @@ internal class TexWidget : IDataWindowWidget ImGui.SameLine(); if (ImGui.Button("Load File (Async)")) - this.addedTextures.Add(new(Api10: this.textureManager.GetFromFile(this.inputFilePath).RentAsync())); + this.addedTextures.Add(new(Api10: this.textureManager.Shared.GetFromFile(this.inputFilePath).RentAsync())); ImGui.SameLine(); if (ImGui.Button("Load File (Immediate)")) @@ -579,7 +513,7 @@ internal class TexWidget : IDataWindowWidget if (ImGui.Button("Load File (Async)")) { this.addedTextures.Add( - new(Api10: this.textureManager.GetFromManifestResource(assembly, name).RentAsync())); + new(Api10: this.textureManager.Shared.GetFromManifestResource(assembly, name).RentAsync())); } ImGui.SameLine(); @@ -600,6 +534,100 @@ internal class TexWidget : IDataWindowWidget ImGuiHelpers.ScaledDummy(10); } + private async void SaveTextureAsync(string name, Func> textureGetter) + { + try + { + BitmapCodecInfo encoder; + { + var off = ImGui.GetCursorScreenPos(); + var first = true; + var encoders = this.textureManager + .Wic + .GetSupportedEncoderInfos() + .ToList(); + var tcs = new TaskCompletionSource(); + Service.Get().Draw += DrawChoices; + + encoder = await tcs.Task; + + void DrawChoices() + { + if (first) + { + ImGui.OpenPopup(nameof(this.SaveTextureAsync)); + first = false; + } + + ImGui.SetNextWindowPos(off, ImGuiCond.Appearing); + if (!ImGui.BeginPopup( + nameof(this.SaveTextureAsync), + ImGuiWindowFlags.AlwaysAutoResize | + ImGuiWindowFlags.NoTitleBar | + ImGuiWindowFlags.NoSavedSettings)) + { + Service.Get().Draw -= DrawChoices; + tcs.TrySetCanceled(); + return; + } + + foreach (var encoder2 in encoders) + { + if (ImGui.Selectable(encoder2.Name)) + tcs.TrySetResult(encoder2); + } + + if (tcs.Task.IsCompleted) + ImGui.CloseCurrentPopup(); + + ImGui.EndPopup(); + } + } + + string path; + { + var tcs = new TaskCompletionSource(); + this.fileDialogManager.SaveFileDialog( + "Save texture...", + $"{encoder.Name.Replace(',', '.')}{{{string.Join(',', encoder.Extensions)}}}", + name + encoder.Extensions.First(), + encoder.Extensions.First(), + (ok, path2) => + { + if (!ok) + tcs.SetCanceled(); + else + tcs.SetResult(path2); + }); + path = await tcs.Task.ConfigureAwait(false); + } + + using var textureWrap = await textureGetter.Invoke(); + await this.textureManager.SaveToFileAsync( + textureWrap, + encoder.ContainerGuid, + path, + props: new Dictionary + { + ["CompressionQuality"] = 1.0f, + ["ImageQuality"] = 1.0f, + }); + + Service.Get().AddNotification( + $"File saved to: {path}", + this.DisplayName, + NotificationType.Success); + } + catch (Exception e) + { + Log.Error(e, $"{nameof(TexWidget)}.{nameof(this.SaveTextureAsync)}"); + Service.Get().AddNotification( + $"Failed to save file: {e}", + this.DisplayName, + NotificationType.Error); + } + } + private void TextRightAlign(string s) { var width = ImGui.CalcTextSize(s).X; @@ -656,7 +684,7 @@ internal class TexWidget : IDataWindowWidget _ = this.Api10?.ToContentDisposedTask(); } - public string DescribeError() + public string? DescribeError() { if (this.SharedResource is not null) return "Unknown error"; @@ -665,7 +693,7 @@ internal class TexWidget : IDataWindowWidget if (this.Api10 is not null) { return !this.Api10.IsCompleted - ? "Loading" + ? null : this.Api10.Exception?.ToString() ?? (this.Api10.IsCanceled ? "Canceled" : "Unknown error"); } @@ -704,6 +732,18 @@ internal class TexWidget : IDataWindowWidget return null; } + public async Task CreateNewTextureWrapReference(ITextureProvider tp) + { + while (true) + { + if (this.GetTexture(tp) is { } textureWrap) + return textureWrap.CreateWrapSharingLowLevelResource(); + if (this.DescribeError() is { } err) + throw new(err); + await Task.Delay(100); + } + } + public TextureEntry CreateFromSharedLowLevelResource(ITextureProvider tp) => new() { SharedResource = this.GetTexture(tp)?.CreateWrapSharingLowLevelResource() }; diff --git a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs index 50450aaae..1042b0741 100644 --- a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs +++ b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs @@ -7,6 +7,7 @@ using System.Threading; using System.Threading.Tasks; using Dalamud.Game; +using Dalamud.Interface.Textures.Internal; using Dalamud.Networking.Http; using Dalamud.Plugin.Internal; using Dalamud.Plugin.Internal.Types; diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 0e30658ef..186263783 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -16,6 +16,7 @@ using Dalamud.Interface.Animation.EasingFunctions; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.Textures.Internal; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; @@ -1745,7 +1746,7 @@ internal class PluginInstallerWindow : Window, IDisposable if (!this.testerIconPath.IsNullOrEmpty()) { - this.testerIcon = tm.GetFromFile(this.testerIconPath).RentAsync(); + this.testerIcon = tm.Shared.GetFromFile(this.testerIconPath).RentAsync(); } this.testerImages = new Task?[this.testerImagePaths.Length]; @@ -1756,7 +1757,7 @@ internal class PluginInstallerWindow : Window, IDisposable continue; _ = this.testerImages[i]?.ToContentDisposedTask(); - this.testerImages[i] = tm.GetFromFile(this.testerImagePaths[i]).RentAsync(); + this.testerImages[i] = tm.Shared.GetFromFile(this.testerImagePaths[i]).RentAsync(); } } catch (Exception ex) diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs index 6f2a56394..87776f53a 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs @@ -11,6 +11,7 @@ using Dalamud.Game; using Dalamud.Interface.FontIdentifier; using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal; +using Dalamud.Interface.Textures.Internal; using Dalamud.Storage.Assets; using Dalamud.Utility; diff --git a/Dalamud/Interface/Internal/DalamudTextureWrap.cs b/Dalamud/Interface/Textures/DalamudTextureWrap.cs similarity index 97% rename from Dalamud/Interface/Internal/DalamudTextureWrap.cs rename to Dalamud/Interface/Textures/DalamudTextureWrap.cs index b49c6f07b..3795abad2 100644 --- a/Dalamud/Interface/Internal/DalamudTextureWrap.cs +++ b/Dalamud/Interface/Textures/DalamudTextureWrap.cs @@ -2,6 +2,7 @@ using ImGuiScene; +// ReSharper disable once CheckNamespace namespace Dalamud.Interface.Internal; /// diff --git a/Dalamud/Interface/Textures/IBitmapCodecInfo.cs b/Dalamud/Interface/Textures/IBitmapCodecInfo.cs new file mode 100644 index 000000000..7a6f300ca --- /dev/null +++ b/Dalamud/Interface/Textures/IBitmapCodecInfo.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; + +namespace Dalamud.Interface.Textures; + +/// Represents an available bitmap codec. +public interface IBitmapCodecInfo +{ + /// Gets the friendly name for the codec. + string Name { get; } + + /// Gets the representing the container. + Guid ContainerGuid { get; } + + /// Gets the suggested file extensions. + IReadOnlyCollection Extensions { get; } + + /// Gets the corresponding mime types. + IReadOnlyCollection MimeTypes { get; } +} diff --git a/Dalamud/Interface/Internal/IDalamudTextureWrap.cs b/Dalamud/Interface/Textures/IDalamudTextureWrap.cs similarity index 96% rename from Dalamud/Interface/Internal/IDalamudTextureWrap.cs rename to Dalamud/Interface/Textures/IDalamudTextureWrap.cs index 8e2e56c26..d2915b5a0 100644 --- a/Dalamud/Interface/Internal/IDalamudTextureWrap.cs +++ b/Dalamud/Interface/Textures/IDalamudTextureWrap.cs @@ -1,7 +1,10 @@ using System.Numerics; +using Dalamud.Interface.Textures.Internal; + using TerraFX.Interop.Windows; +// ReSharper disable once CheckNamespace namespace Dalamud.Interface.Internal; /// diff --git a/Dalamud/Interface/ISharedImmediateTexture.cs b/Dalamud/Interface/Textures/ISharedImmediateTexture.cs similarity index 99% rename from Dalamud/Interface/ISharedImmediateTexture.cs rename to Dalamud/Interface/Textures/ISharedImmediateTexture.cs index f6c63ee10..f8c727557 100644 --- a/Dalamud/Interface/ISharedImmediateTexture.cs +++ b/Dalamud/Interface/Textures/ISharedImmediateTexture.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; using Dalamud.Interface.Internal; using Dalamud.Utility; -namespace Dalamud.Interface; +namespace Dalamud.Interface.Textures; /// A texture with a backing instance of that is shared across multiple /// requesters. diff --git a/Dalamud/Interface/Textures/Internal/BitmapCodecInfo.cs b/Dalamud/Interface/Textures/Internal/BitmapCodecInfo.cs new file mode 100644 index 000000000..3d5456500 --- /dev/null +++ b/Dalamud/Interface/Textures/Internal/BitmapCodecInfo.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Runtime.InteropServices; + +using Dalamud.Utility; + +using TerraFX.Interop.Windows; + +namespace Dalamud.Interface.Textures.Internal; + +/// Represents an available bitmap codec. +internal sealed class BitmapCodecInfo : IBitmapCodecInfo +{ + /// Initializes a new instance of the class. + /// The source codec info. Ownership is not transferred. + public unsafe BitmapCodecInfo(ComPtr codecInfo) + { + this.Name = ReadStringUsing( + codecInfo, + ((IWICBitmapCodecInfo.Vtbl*)codecInfo.Get()->lpVtbl)->GetFriendlyName); + Guid temp; + codecInfo.Get()->GetContainerFormat(&temp).ThrowOnError(); + this.ContainerGuid = temp; + this.Extensions = ReadStringUsing( + codecInfo, + ((IWICBitmapCodecInfo.Vtbl*)codecInfo.Get()->lpVtbl)->GetFileExtensions) + .Split(','); + this.MimeTypes = ReadStringUsing( + codecInfo, + ((IWICBitmapCodecInfo.Vtbl*)codecInfo.Get()->lpVtbl)->GetMimeTypes) + .Split(','); + } + + /// Gets the friendly name for the codec. + public string Name { get; } + + /// Gets the representing the container. + public Guid ContainerGuid { get; } + + /// Gets the suggested file extensions. + public IReadOnlyCollection Extensions { get; } + + /// Gets the corresponding mime types. + public IReadOnlyCollection MimeTypes { get; } + + private static unsafe string ReadStringUsing( + IWICBitmapCodecInfo* codecInfo, + delegate* unmanaged readFuncPtr) + { + var cch = 0u; + _ = readFuncPtr(codecInfo, 0, null, &cch); + var buf = stackalloc char[(int)cch + 1]; + Marshal.ThrowExceptionForHR(readFuncPtr(codecInfo, cch + 1, (ushort*)buf, &cch)); + return new(buf, 0, (int)cch); + } +} diff --git a/Dalamud/Interface/Internal/DisposeSuppressingTextureWrap.cs b/Dalamud/Interface/Textures/Internal/DisposeSuppressingTextureWrap.cs similarity index 90% rename from Dalamud/Interface/Internal/DisposeSuppressingTextureWrap.cs rename to Dalamud/Interface/Textures/Internal/DisposeSuppressingTextureWrap.cs index d099bae69..17a88e270 100644 --- a/Dalamud/Interface/Internal/DisposeSuppressingTextureWrap.cs +++ b/Dalamud/Interface/Textures/Internal/DisposeSuppressingTextureWrap.cs @@ -1,4 +1,6 @@ -namespace Dalamud.Interface.Internal; +using Dalamud.Interface.Internal; + +namespace Dalamud.Interface.Textures.Internal; /// /// A texture wrap that ignores calls. diff --git a/Dalamud/Interface/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs similarity index 89% rename from Dalamud/Interface/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs rename to Dalamud/Interface/Textures/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs index 06c366601..6cdd0aa25 100644 --- a/Dalamud/Interface/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs +++ b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs @@ -1,9 +1,10 @@ using System.Threading; using System.Threading.Tasks; +using Dalamud.Interface.Internal; using Dalamud.Utility; -namespace Dalamud.Interface.Internal.SharedImmediateTextures; +namespace Dalamud.Interface.Textures.Internal.SharedImmediateTextures; /// Represents a sharable texture, based on a file on the system filesystem. internal sealed class FileSystemSharedImmediateTexture : SharedImmediateTexture @@ -42,7 +43,7 @@ internal sealed class FileSystemSharedImmediateTexture : SharedImmediateTexture private async Task CreateTextureAsync(CancellationToken cancellationToken) { - var tm = await Service.GetAsync(); + var tm = await Service.GetAsync(); return await tm.NoThrottleCreateFromFileAsync(this.path, cancellationToken); } } diff --git a/Dalamud/Interface/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs similarity index 85% rename from Dalamud/Interface/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs rename to Dalamud/Interface/Textures/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs index e22998813..a0562f1ef 100644 --- a/Dalamud/Interface/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs +++ b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs @@ -3,11 +3,12 @@ using System.Threading; using System.Threading.Tasks; using Dalamud.Data; +using Dalamud.Interface.Internal; using Dalamud.Utility; using Lumina.Data.Files; -namespace Dalamud.Interface.Internal.SharedImmediateTextures; +namespace Dalamud.Interface.Textures.Internal.SharedImmediateTextures; /// Represents a sharable texture, based on a file in game resources. internal sealed class GamePathSharedImmediateTexture : SharedImmediateTexture @@ -46,8 +47,9 @@ internal sealed class GamePathSharedImmediateTexture : SharedImmediateTexture private async Task CreateTextureAsync(CancellationToken cancellationToken) { var dm = await Service.GetAsync(); - var tm = await Service.GetAsync(); - if (dm.GetFile(this.path) is not { } file) + var tm = await Service.GetAsync(); + var substPath = tm.GetSubstitutedPath(this.path); + if (dm.GetFile(substPath) is not { } file) throw new FileNotFoundException(); cancellationToken.ThrowIfCancellationRequested(); return tm.NoThrottleCreateFromTexFile(file); diff --git a/Dalamud/Interface/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs similarity index 93% rename from Dalamud/Interface/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs rename to Dalamud/Interface/Textures/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs index c017a0764..c9bdea067 100644 --- a/Dalamud/Interface/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs +++ b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs @@ -3,9 +3,10 @@ using System.Reflection; using System.Threading; using System.Threading.Tasks; +using Dalamud.Interface.Internal; using Dalamud.Utility; -namespace Dalamud.Interface.Internal.SharedImmediateTextures; +namespace Dalamud.Interface.Textures.Internal.SharedImmediateTextures; /// Represents a sharable texture, based on a manifest texture obtained from /// . @@ -56,7 +57,7 @@ internal sealed class ManifestResourceSharedImmediateTexture : SharedImmediateTe if (stream is null) throw new FileNotFoundException("The resource file could not be found."); - var tm = await Service.GetAsync(); + var tm = await Service.GetAsync(); var ms = new MemoryStream(stream.CanSeek ? (int)stream.Length : 0); await stream.CopyToAsync(ms, cancellationToken); return tm.NoThrottleCreateFromImage(ms.GetBuffer().AsMemory(0, (int)ms.Length), cancellationToken); diff --git a/Dalamud/Interface/Internal/SharedImmediateTextures/SharedImmediateTexture.cs b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/SharedImmediateTexture.cs similarity index 99% rename from Dalamud/Interface/Internal/SharedImmediateTextures/SharedImmediateTexture.cs rename to Dalamud/Interface/Textures/Internal/SharedImmediateTextures/SharedImmediateTexture.cs index 426c61b2c..f730637e4 100644 --- a/Dalamud/Interface/Internal/SharedImmediateTextures/SharedImmediateTexture.cs +++ b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/SharedImmediateTexture.cs @@ -3,10 +3,11 @@ using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; +using Dalamud.Interface.Internal; using Dalamud.Storage.Assets; using Dalamud.Utility; -namespace Dalamud.Interface.Internal.SharedImmediateTextures; +namespace Dalamud.Interface.Textures.Internal.SharedImmediateTextures; /// Represents a texture that may have multiple reference holders (owners). internal abstract class SharedImmediateTexture diff --git a/Dalamud/Interface/Internal/TextureLoadThrottler.cs b/Dalamud/Interface/Textures/Internal/TextureLoadThrottler.cs similarity index 99% rename from Dalamud/Interface/Internal/TextureLoadThrottler.cs rename to Dalamud/Interface/Textures/Internal/TextureLoadThrottler.cs index 043906782..a996a2890 100644 --- a/Dalamud/Interface/Internal/TextureLoadThrottler.cs +++ b/Dalamud/Interface/Textures/Internal/TextureLoadThrottler.cs @@ -4,7 +4,9 @@ using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; -namespace Dalamud.Interface.Internal; +using Dalamud.Interface.Internal; + +namespace Dalamud.Interface.Textures.Internal; /// /// Service for managing texture loads. diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.Api9.cs b/Dalamud/Interface/Textures/Internal/TextureManager.Api9.cs new file mode 100644 index 000000000..8e08b43f8 --- /dev/null +++ b/Dalamud/Interface/Textures/Internal/TextureManager.Api9.cs @@ -0,0 +1,55 @@ +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 + +/// Service responsible for loading and disposing ImGui texture wraps. +internal sealed partial class TextureManager +{ + /// + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + [Obsolete("See interface definition.")] + string? ITextureProvider.GetIconPath(uint iconId, ITextureProvider.IconFlags flags, ClientLanguage? language) + => 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.")] + IDalamudTextureWrap? ITextureProvider.GetIcon( + uint iconId, + ITextureProvider.IconFlags flags, + ClientLanguage? language, + bool keepAlive) => + this.Shared.GetFromGameIcon( + new( + iconId, + (flags & ITextureProvider.IconFlags.ItemHighQuality) != 0, + (flags & ITextureProvider.IconFlags.HiRes) != 0, + language)) + .GetAvailableOnAccessWrapForApi9(); + + /// + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + [Obsolete("See interface definition.")] + IDalamudTextureWrap? ITextureProvider.GetTextureFromGame(string path, bool keepAlive) => + this.Shared.GetFromGame(path).GetAvailableOnAccessWrapForApi9(); + + /// + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + [Obsolete("See interface definition.")] + IDalamudTextureWrap? ITextureProvider.GetTextureFromFile(FileInfo file, bool keepAlive) => + this.Shared.GetFromFile(file.FullName).GetAvailableOnAccessWrapForApi9(); +} diff --git a/Dalamud/Interface/Internal/TextureManager.FormatConvert.cs b/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs similarity index 98% rename from Dalamud/Interface/Internal/TextureManager.FormatConvert.cs rename to Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs index 99474dca2..e3a130a14 100644 --- a/Dalamud/Interface/Internal/TextureManager.FormatConvert.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs @@ -4,6 +4,7 @@ using System.Numerics; using System.Threading; using System.Threading.Tasks; +using Dalamud.Interface.Internal; using Dalamud.Plugin.Services; using Dalamud.Utility; @@ -15,7 +16,7 @@ using SharpDX.DXGI; using TerraFX.Interop.DirectX; using TerraFX.Interop.Windows; -namespace Dalamud.Interface.Internal; +namespace Dalamud.Interface.Textures.Internal; /// Service responsible for loading and disposing ImGui texture wraps. internal sealed partial class TextureManager @@ -119,16 +120,16 @@ internal sealed partial class TextureManager } /// - Task<(RawImageSpecification Specification, byte[] RawData)> ITextureProvider.GetRawDataAsync( + Task<(RawImageSpecification Specification, byte[] RawData)> ITextureProvider.GetRawDataFromExistingTextureAsync( IDalamudTextureWrap wrap, Vector2 uv0, Vector2 uv1, int dxgiFormat, CancellationToken cancellationToken) => - this.GetRawDataAsync(wrap, uv0, uv1, (DXGI_FORMAT)dxgiFormat, cancellationToken); + this.GetRawDataFromExistingTextureAsync(wrap, uv0, uv1, (DXGI_FORMAT)dxgiFormat, cancellationToken); - /// - public async Task<(RawImageSpecification Specification, byte[] RawData)> GetRawDataAsync( + /// + public async Task<(RawImageSpecification Specification, byte[] RawData)> GetRawDataFromExistingTextureAsync( IDalamudTextureWrap wrap, Vector2 uv0, Vector2 uv1, diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.GamePath.cs b/Dalamud/Interface/Textures/Internal/TextureManager.GamePath.cs new file mode 100644 index 000000000..455b6f504 --- /dev/null +++ b/Dalamud/Interface/Textures/Internal/TextureManager.GamePath.cs @@ -0,0 +1,120 @@ +using System.Collections.Generic; +using System.IO; + +using Dalamud.Plugin.Services; + +namespace Dalamud.Interface.Textures.Internal; + +/// Service responsible for loading and disposing ImGui texture wraps. +internal sealed partial class TextureManager +{ + 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"; + + /// + public event ITextureSubstitutionProvider.TextureDataInterceptorDelegate? InterceptTexDataLoad; + + /// + public bool TryGetIconPath(in GameIconLookup lookup, out string path) + { + // 1. Item + path = FormatIconPath( + lookup.IconId, + lookup.ItemHq ? "hq/" : string.Empty, + lookup.HiRes); + if (this.dataManager.FileExists(path)) + 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/", + _ => null, + }; + + if (languageFolder is not null) + { + // 2. Regular icon, with language, hi-res + path = FormatIconPath( + lookup.IconId, + languageFolder, + lookup.HiRes); + if (this.dataManager.FileExists(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( + lookup.IconId, + null, + lookup.HiRes); + if (this.dataManager.FileExists(path)) + return true; + + // 4. Regular icon, without language, no hi-res + if (lookup.HiRes) + { + path = FormatIconPath( + lookup.IconId, + null, + false); + if (this.dataManager.FileExists(path)) + return true; + } + + return false; + } + + /// + 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); + + if (interceptPath != null) + { + Log.Verbose("Intercept: {OriginalPath} => {ReplacePath}", originalPath, interceptPath); + return interceptPath; + } + + return originalPath; + } + + /// + public void InvalidatePaths(IEnumerable paths) + { + foreach (var path in paths) + this.Shared.FlushFromGameCache(path); + } + + 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); + } +} diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.SharedTextures.cs b/Dalamud/Interface/Textures/Internal/TextureManager.SharedTextures.cs new file mode 100644 index 000000000..3a121c4c5 --- /dev/null +++ b/Dalamud/Interface/Textures/Internal/TextureManager.SharedTextures.cs @@ -0,0 +1,163 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Reflection; +using System.Runtime.CompilerServices; + +using BitFaster.Caching.Lru; + +using Dalamud.Interface.Textures.Internal.SharedImmediateTextures; +using Dalamud.Plugin.Services; + +namespace Dalamud.Interface.Textures.Internal; + +/// Service responsible for loading and disposing ImGui texture wraps. +internal sealed partial class TextureManager +{ + /// + ISharedImmediateTexture ITextureProvider.GetFromGameIcon(in GameIconLookup lookup) => + this.Shared.GetFromGameIcon(lookup); + + /// + ISharedImmediateTexture ITextureProvider.GetFromGame(string path) => + this.Shared.GetFromGame(path); + + /// + ISharedImmediateTexture ITextureProvider.GetFromFile(string path) => + this.Shared.GetFromFile(path); + + /// + ISharedImmediateTexture ITextureProvider.GetFromManifestResource(Assembly assembly, string name) => + this.Shared.GetFromManifestResource(assembly, name); + + /// A part of texture manager that deals with s. + internal sealed class SharedTextureManager : IDisposable + { + private const int PathLookupLruCount = 8192; + + private readonly TextureManager textureManager; + private readonly ConcurrentLru lookupCache = new(PathLookupLruCount); + private readonly ConcurrentDictionary gameDict = new(); + private readonly ConcurrentDictionary fileDict = new(); + private readonly ConcurrentDictionary<(Assembly, string), SharedImmediateTexture> manifestResourceDict = new(); + private readonly HashSet invalidatedTextures = new(); + + /// Initializes a new instance of the class. + /// An instance of . + public SharedTextureManager(TextureManager textureManager) + { + this.textureManager = textureManager; + this.textureManager.framework.Update += this.FrameworkOnUpdate; + } + + /// Gets all the loaded textures from game resources. + public ICollection ForDebugGamePathTextures => this.gameDict.Values; + + /// Gets all the loaded textures from filesystem. + public ICollection ForDebugFileSystemTextures => this.fileDict.Values; + + /// Gets all the loaded textures from assembly manifest resources. + public ICollection ForDebugManifestResourceTextures => this.manifestResourceDict.Values; + + /// Gets all the loaded textures that are invalidated from . + /// lock on use of the value returned from this property. + [SuppressMessage( + "ReSharper", + "InconsistentlySynchronizedField", + Justification = "Debug use only; users are expected to lock around this")] + public ICollection ForDebugInvalidatedTextures => this.invalidatedTextures; + + /// + public void Dispose() + { + this.textureManager.framework.Update -= this.FrameworkOnUpdate; + this.lookupCache.Clear(); + ReleaseSelfReferences(this.gameDict); + ReleaseSelfReferences(this.fileDict); + ReleaseSelfReferences(this.manifestResourceDict); + return; + + static void ReleaseSelfReferences(ConcurrentDictionary dict) + { + foreach (var v in dict.Values) + v.ReleaseSelfReference(true); + dict.Clear(); + } + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public SharedImmediateTexture GetFromGameIcon(in GameIconLookup lookup) => + this.GetFromGame(this.lookupCache.GetOrAdd(lookup, this.GetIconPathByValue)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public SharedImmediateTexture GetFromGame(string path) => + this.gameDict.GetOrAdd(path, GamePathSharedImmediateTexture.CreatePlaceholder); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public SharedImmediateTexture GetFromFile(string path) => + this.fileDict.GetOrAdd(path, FileSystemSharedImmediateTexture.CreatePlaceholder); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public SharedImmediateTexture GetFromManifestResource(Assembly assembly, string name) => + this.manifestResourceDict.GetOrAdd( + (assembly, name), + ManifestResourceSharedImmediateTexture.CreatePlaceholder); + + /// Invalidates a cached item from and . + /// + /// The path to invalidate. + public void FlushFromGameCache(string path) + { + if (this.gameDict.TryRemove(path, out var r)) + { + if (r.ReleaseSelfReference(true) != 0 || r.HasRevivalPossibility) + { + lock (this.invalidatedTextures) + this.invalidatedTextures.Add(r); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private string GetIconPathByValue(GameIconLookup lookup) => + this.textureManager.TryGetIconPath(lookup, out var path) ? path : throw new FileNotFoundException(); + + private void FrameworkOnUpdate(IFramework unused) + { + RemoveFinalReleased(this.gameDict); + RemoveFinalReleased(this.fileDict); + RemoveFinalReleased(this.manifestResourceDict); + + // ReSharper disable once InconsistentlySynchronizedField + if (this.invalidatedTextures.Count != 0) + { + lock (this.invalidatedTextures) + this.invalidatedTextures.RemoveWhere(TextureFinalReleasePredicate); + } + + return; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static void RemoveFinalReleased(ConcurrentDictionary dict) + { + if (!dict.IsEmpty) + { + foreach (var (k, v) in dict) + { + if (TextureFinalReleasePredicate(v)) + _ = dict.TryRemove(k, out _); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static bool TextureFinalReleasePredicate(SharedImmediateTexture v) => + v.ContentQueried && v.ReleaseSelfReference(false) == 0 && !v.HasRevivalPossibility; + } + } +} diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs b/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs new file mode 100644 index 000000000..6da68b7e0 --- /dev/null +++ b/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs @@ -0,0 +1,527 @@ +using System.Buffers; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Numerics; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Interface.Internal; +using Dalamud.Interface.Textures.Internal.SharedImmediateTextures; +using Dalamud.Plugin.Services; +using Dalamud.Utility; +using Dalamud.Utility.TerraFxCom; + +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +using static TerraFX.Interop.Windows.Windows; + +namespace Dalamud.Interface.Textures.Internal; + +/// Service responsible for loading and disposing ImGui texture wraps. +[SuppressMessage( + "StyleCop.CSharp.LayoutRules", + "SA1519:Braces should not be omitted from multi-line child statement", + Justification = "Multiple fixed blocks")] +internal sealed partial class TextureManager +{ + /// + [SuppressMessage( + "StyleCop.CSharp.LayoutRules", + "SA1519:Braces should not be omitted from multi-line child statement", + Justification = "Multiple fixed blocks")] + public async Task SaveToStreamAsync( + IDalamudTextureWrap wrap, + Guid containerGuid, + Stream stream, + bool leaveOpen = false, + IReadOnlyDictionary? props = null, + CancellationToken cancellationToken = default) + { + using var istream = ManagedIStream.Create(stream, leaveOpen); + + RawImageSpecification specs; + byte[] bytes; + using (var wrapCopy = wrap.CreateWrapSharingLowLevelResource()) + { + (specs, bytes) = await this.GetRawDataFromExistingTextureAsync( + wrapCopy, + Vector2.Zero, + Vector2.One, + DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM, + cancellationToken).ConfigureAwait(false); + } + + this.Wic.SaveToStreamUsingWic( + specs, + bytes, + containerGuid, + istream, + props, + cancellationToken); + } + + /// + public async Task SaveToFileAsync( + IDalamudTextureWrap wrap, + Guid containerGuid, + string path, + IReadOnlyDictionary? props = null, + CancellationToken cancellationToken = default) + { + var pathTemp = $"{path}.{GetCurrentThreadId():X08}{Environment.TickCount64:X16}.tmp"; + try + { + await this.SaveToStreamAsync(wrap, containerGuid, File.Create(pathTemp), false, props, cancellationToken); + } + catch (Exception e) + { + try + { + if (File.Exists(pathTemp)) + File.Delete(pathTemp); + } + catch (Exception e2) + { + throw new AggregateException( + "Failed to save the file, and failed to remove the temporary file.", + e, + e2); + } + + throw; + } + + try + { + try + { + File.Replace(pathTemp, path, null, true); + } + catch + { + File.Move(pathTemp, path, true); + } + } + catch (Exception e) + { + try + { + if (File.Exists(pathTemp)) + File.Delete(pathTemp); + } + catch (Exception e2) + { + throw new AggregateException( + "Failed to move the temporary file to the target path, and failed to remove the temporary file.", + e, + e2); + } + + throw; + } + } + + /// + IEnumerable ITextureProvider.GetSupportedImageDecoderInfos() => + this.Wic.GetSupportedDecoderInfos(); + + /// + IEnumerable ITextureProvider.GetSupportedImageEncoderInfos() => + this.Wic.GetSupportedEncoderInfos(); + + /// Creates a texture from the given bytes of an image file. Skips the load throttler; intended to be used + /// from implementation of s. + /// The data. + /// The cancellation token. + /// The loaded texture. + internal IDalamudTextureWrap NoThrottleCreateFromImage( + ReadOnlyMemory bytes, + CancellationToken cancellationToken = default) + { + ObjectDisposedException.ThrowIf(this.disposing, this); + cancellationToken.ThrowIfCancellationRequested(); + + try + { + using var handle = bytes.Pin(); + using var stream = this.Wic.CreateIStreamFromMemory(handle, bytes.Length); + return this.Wic.NoThrottleCreateFromWicStream(stream, cancellationToken); + } + catch (Exception e1) + { + try + { + return this.NoThrottleCreateFromTexFile(bytes.Span); + } + catch (Exception e2) + { + throw new AggregateException(e1, e2); + } + } + } + + /// Creates a texture from the given path to an image file. Skips the load throttler; intended to be used + /// from implementation of s. + /// The path of the file.. + /// The cancellation token. + /// The loaded texture. + internal async Task NoThrottleCreateFromFileAsync( + string path, + CancellationToken cancellationToken = default) + { + ObjectDisposedException.ThrowIf(this.disposing, this); + cancellationToken.ThrowIfCancellationRequested(); + + try + { + using var stream = this.Wic.CreateIStreamFromFile(path); + return this.Wic.NoThrottleCreateFromWicStream(stream, cancellationToken); + } + catch (Exception e1) + { + try + { + return this.NoThrottleCreateFromTexFile(await File.ReadAllBytesAsync(path, cancellationToken)); + } + catch (Exception e2) + { + throw new AggregateException(e1, e2); + } + } + } + + /// A part of texture manager that uses Windows Imaging Component under the hood. + internal sealed class WicManager : IDisposable + { + private readonly TextureManager textureManager; + private ComPtr wicFactory; + + /// Initializes a new instance of the class. + /// An instance of . + public WicManager(TextureManager textureManager) + { + this.textureManager = textureManager; + unsafe + { + fixed (Guid* pclsidWicImagingFactory = &CLSID.CLSID_WICImagingFactory) + fixed (Guid* piidWicImagingFactory = &IID.IID_IWICImagingFactory) + { + CoCreateInstance( + pclsidWicImagingFactory, + null, + (uint)CLSCTX.CLSCTX_INPROC_SERVER, + piidWicImagingFactory, + (void**)this.wicFactory.GetAddressOf()).ThrowOnError(); + } + } + } + + /// + /// Finalizes an instance of the class. + /// + ~WicManager() => this.ReleaseUnmanagedResource(); + + /// + public void Dispose() + { + this.ReleaseUnmanagedResource(); + GC.SuppressFinalize(this); + } + + /// Creates a new instance of from a . + /// An instance of . + /// The number of bytes in the memory. + /// The new instance of . + public unsafe ComPtr CreateIStreamFromMemory(MemoryHandle handle, int length) + { + using var wicStream = default(ComPtr); + this.wicFactory.Get()->CreateStream(wicStream.GetAddressOf()).ThrowOnError(); + wicStream.Get()->InitializeFromMemory((byte*)handle.Pointer, checked((uint)length)).ThrowOnError(); + + var res = default(ComPtr); + wicStream.As(ref res).ThrowOnError(); + return res; + } + + /// Creates a new instance of from a file path. + /// The file path. + /// The new instance of . + public unsafe ComPtr CreateIStreamFromFile(string path) + { + using var wicStream = default(ComPtr); + this.wicFactory.Get()->CreateStream(wicStream.GetAddressOf()).ThrowOnError(); + fixed (char* pPath = path) + wicStream.Get()->InitializeFromFilename((ushort*)pPath, GENERIC_READ).ThrowOnError(); + + var res = default(ComPtr); + wicStream.As(ref res).ThrowOnError(); + return res; + } + + /// Creates a new instance of from a . + /// The stream that will NOT be closed after. + /// The cancellation token. + /// The newly loaded texture. + public unsafe IDalamudTextureWrap NoThrottleCreateFromWicStream( + ComPtr stream, + CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + using var decoder = default(ComPtr); + this.wicFactory.Get()->CreateDecoderFromStream( + stream, + null, + WICDecodeOptions.WICDecodeMetadataCacheOnDemand, + decoder.GetAddressOf()).ThrowOnError(); + + cancellationToken.ThrowIfCancellationRequested(); + + using var frame = default(ComPtr); + decoder.Get()->GetFrame(0, frame.GetAddressOf()).ThrowOnError(); + var pixelFormat = default(Guid); + frame.Get()->GetPixelFormat(&pixelFormat).ThrowOnError(); + var dxgiFormat = GetCorrespondingDxgiFormat(pixelFormat); + + cancellationToken.ThrowIfCancellationRequested(); + + using var bitmapSource = default(ComPtr); + if (dxgiFormat == DXGI_FORMAT.DXGI_FORMAT_UNKNOWN || !this.textureManager.IsDxgiFormatSupported(dxgiFormat)) + { + dxgiFormat = DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM; + pixelFormat = GUID.GUID_WICPixelFormat32bppBGRA; + WICConvertBitmapSource(&pixelFormat, (IWICBitmapSource*)frame.Get(), bitmapSource.GetAddressOf()) + .ThrowOnError(); + } + else + { + frame.As(&bitmapSource); + } + + cancellationToken.ThrowIfCancellationRequested(); + + using var bitmap = default(ComPtr); + using var bitmapLock = default(ComPtr); + WICRect rcLock; + uint stride; + uint cbBufferSize; + byte* pbData; + if (bitmapSource.As(&bitmap).FAILED) + { + bitmapSource.Get()->GetSize((uint*)&rcLock.Width, (uint*)&rcLock.Height).ThrowOnError(); + this.wicFactory.Get()->CreateBitmap( + (uint)rcLock.Width, + (uint)rcLock.Height, + &pixelFormat, + WICBitmapCreateCacheOption.WICBitmapCacheOnDemand, + bitmap.GetAddressOf()).ThrowOnError(); + + bitmap.Get()->Lock( + &rcLock, + (uint)WICBitmapLockFlags.WICBitmapLockWrite, + bitmapLock.ReleaseAndGetAddressOf()) + .ThrowOnError(); + bitmapLock.Get()->GetStride(&stride).ThrowOnError(); + bitmapLock.Get()->GetDataPointer(&cbBufferSize, &pbData).ThrowOnError(); + bitmapSource.Get()->CopyPixels(null, stride, cbBufferSize, pbData).ThrowOnError(); + } + + cancellationToken.ThrowIfCancellationRequested(); + + bitmap.Get()->Lock( + &rcLock, + (uint)WICBitmapLockFlags.WICBitmapLockRead, + bitmapLock.ReleaseAndGetAddressOf()) + .ThrowOnError(); + bitmapSource.Get()->GetSize((uint*)&rcLock.Width, (uint*)&rcLock.Height).ThrowOnError(); + bitmapLock.Get()->GetStride(&stride).ThrowOnError(); + bitmapLock.Get()->GetDataPointer(&cbBufferSize, &pbData).ThrowOnError(); + bitmapSource.Get()->CopyPixels(null, stride, cbBufferSize, pbData).ThrowOnError(); + return this.textureManager.NoThrottleCreateFromRaw( + new(rcLock.Width, rcLock.Height, (int)stride, (int)dxgiFormat), + new(pbData, (int)cbBufferSize)); + } + + /// Gets the supported bitmap codecs. + /// The supported encoders. + public IEnumerable GetSupportedEncoderInfos() + { + foreach (var ptr in new ComponentEnumerable( + this.wicFactory, + WICComponentType.WICEncoder)) + yield return new(ptr); + } + + /// Gets the supported bitmap codecs. + /// The supported decoders. + public IEnumerable GetSupportedDecoderInfos() + { + foreach (var ptr in new ComponentEnumerable( + this.wicFactory, + WICComponentType.WICDecoder)) + yield return new(ptr); + } + + /// Saves the given raw bitmap to a stream. + /// The raw bitmap specifications. + /// The raw bitmap bytes. + /// The container format from . + /// The stream to write to. The ownership is not transferred. + /// The encoder properties. + /// The cancellation token. + public unsafe void SaveToStreamUsingWic( + RawImageSpecification specs, + byte[] bytes, + Guid containerFormat, + ComPtr stream, + IReadOnlyDictionary? props = null, + CancellationToken cancellationToken = default) + { + using var encoder = default(ComPtr); + using var encoderFrame = default(ComPtr); + var guidPixelFormat = GUID.GUID_WICPixelFormat32bppBGRA; + this.wicFactory.Get()->CreateEncoder(&containerFormat, null, encoder.GetAddressOf()).ThrowOnError(); + cancellationToken.ThrowIfCancellationRequested(); + + encoder.Get()->Initialize(stream, WICBitmapEncoderCacheOption.WICBitmapEncoderNoCache) + .ThrowOnError(); + cancellationToken.ThrowIfCancellationRequested(); + + using var propertyBag = default(ComPtr); + encoder.Get()->CreateNewFrame(encoderFrame.GetAddressOf(), propertyBag.GetAddressOf()).ThrowOnError(); + cancellationToken.ThrowIfCancellationRequested(); + + if (props is not null) + { + var nprop = 0u; + propertyBag.Get()->CountProperties(&nprop).ThrowOnError(); + for (var i = 0u; i < nprop; i++) + { + var pbag2 = default(PROPBAG2); + var npropread = 0u; + propertyBag.Get()->GetPropertyInfo(i, 1, &pbag2, &npropread).ThrowOnError(); + if (npropread == 0) + continue; + try + { + var propName = new string((char*)pbag2.pstrName); + if (props.TryGetValue(propName, out var untypedValue)) + { + VARIANT val; + // Marshal calls VariantInit. + Marshal.GetNativeVariantForObject(untypedValue, (nint)(&val)); + VariantChangeType(&val, &val, 0, pbag2.vt).ThrowOnError(); + propertyBag.Get()->Write(1, &pbag2, &val).ThrowOnError(); + VariantClear(&val); + } + } + finally + { + CoTaskMemFree(pbag2.pstrName); + } + } + } + + encoderFrame.Get()->Initialize(propertyBag).ThrowOnError(); + cancellationToken.ThrowIfCancellationRequested(); + + encoderFrame.Get()->SetPixelFormat(&guidPixelFormat).ThrowOnError(); + encoderFrame.Get()->SetSize(checked((uint)specs.Width), checked((uint)specs.Height)).ThrowOnError(); + + using var tempBitmap = default(ComPtr); + fixed (Guid* pGuid = &GUID.GUID_WICPixelFormat32bppBGRA) + fixed (byte* pBytes = bytes) + { + this.wicFactory.Get()->CreateBitmapFromMemory( + (uint)specs.Width, + (uint)specs.Height, + pGuid, + (uint)specs.Pitch, + checked((uint)bytes.Length), + pBytes, + tempBitmap.GetAddressOf()).ThrowOnError(); + } + + using var tempBitmap2 = default(ComPtr); + WICConvertBitmapSource( + &guidPixelFormat, + (IWICBitmapSource*)tempBitmap.Get(), + tempBitmap2.GetAddressOf()).ThrowOnError(); + + encoderFrame.Get()->SetSize(checked((uint)specs.Width), checked((uint)specs.Height)).ThrowOnError(); + encoderFrame.Get()->WriteSource(tempBitmap2.Get(), null).ThrowOnError(); + + cancellationToken.ThrowIfCancellationRequested(); + + encoderFrame.Get()->Commit().ThrowOnError(); + cancellationToken.ThrowIfCancellationRequested(); + + encoder.Get()->Commit().ThrowOnError(); + } + + /// + /// Gets the corresponding from a containing a WIC pixel format. + /// + /// The WIC pixel format. + /// The corresponding , or if + /// unavailable. + private static DXGI_FORMAT GetCorrespondingDxgiFormat(Guid fmt) => 0 switch + { + // See https://github.com/microsoft/DirectXTex/wiki/WIC-I-O-Functions#savetowicmemory-savetowicfile + _ when fmt == GUID.GUID_WICPixelFormat128bppRGBAFloat => DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_FLOAT, + _ when fmt == GUID.GUID_WICPixelFormat64bppRGBAHalf => DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_FLOAT, + _ when fmt == GUID.GUID_WICPixelFormat64bppRGBA => DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_UNORM, + _ when fmt == GUID.GUID_WICPixelFormat32bppRGBA1010102XR => DXGI_FORMAT + .DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM, + _ when fmt == GUID.GUID_WICPixelFormat32bppRGBA1010102 => DXGI_FORMAT.DXGI_FORMAT_R10G10B10A2_UNORM, + _ when fmt == GUID.GUID_WICPixelFormat16bppBGRA5551 => DXGI_FORMAT.DXGI_FORMAT_B5G5R5A1_UNORM, + _ when fmt == GUID.GUID_WICPixelFormat16bppBGR565 => DXGI_FORMAT.DXGI_FORMAT_B5G6R5_UNORM, + _ when fmt == GUID.GUID_WICPixelFormat32bppGrayFloat => DXGI_FORMAT.DXGI_FORMAT_R32_FLOAT, + _ when fmt == GUID.GUID_WICPixelFormat16bppGrayHalf => DXGI_FORMAT.DXGI_FORMAT_R16_FLOAT, + _ when fmt == GUID.GUID_WICPixelFormat16bppGray => DXGI_FORMAT.DXGI_FORMAT_R16_UNORM, + _ when fmt == GUID.GUID_WICPixelFormat8bppGray => DXGI_FORMAT.DXGI_FORMAT_R8_UNORM, + _ when fmt == GUID.GUID_WICPixelFormat8bppAlpha => DXGI_FORMAT.DXGI_FORMAT_A8_UNORM, + _ when fmt == GUID.GUID_WICPixelFormat32bppRGBA => DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM, + _ when fmt == GUID.GUID_WICPixelFormat32bppBGRA => DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM, + _ when fmt == GUID.GUID_WICPixelFormat32bppBGR => DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM, + _ => DXGI_FORMAT.DXGI_FORMAT_UNKNOWN, + }; + + private void ReleaseUnmanagedResource() => this.wicFactory.Reset(); + + private readonly struct ComponentEnumerable : IEnumerable> + where T : unmanaged, IWICComponentInfo.Interface + { + private readonly ComPtr factory; + private readonly WICComponentType componentType; + + /// Initializes a new instance of the struct. + /// The WIC factory. Ownership is not transferred. + /// + /// The component type to enumerate. + public ComponentEnumerable(ComPtr factory, WICComponentType componentType) + { + this.factory = factory; + this.componentType = componentType; + } + + public unsafe ManagedIEnumUnknownEnumerator GetEnumerator() + { + var enumUnknown = default(ComPtr); + this.factory.Get()->CreateComponentEnumerator( + (uint)this.componentType, + (uint)WICComponentEnumerateOptions.WICComponentEnumerateDefault, + enumUnknown.GetAddressOf()).ThrowOnError(); + return new(enumUnknown); + } + + IEnumerator> IEnumerable>.GetEnumerator() => this.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + } + } +} diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.cs b/Dalamud/Interface/Textures/Internal/TextureManager.cs new file mode 100644 index 000000000..2a17f4d73 --- /dev/null +++ b/Dalamud/Interface/Textures/Internal/TextureManager.cs @@ -0,0 +1,291 @@ +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +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.Services; +using Dalamud.Utility; + +using Lumina.Data; +using Lumina.Data.Files; + +using SharpDX; +using SharpDX.Direct3D; +using SharpDX.Direct3D11; +using SharpDX.DXGI; + +using TerraFX.Interop.DirectX; + +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] +#pragma warning restore SA1015 +internal sealed partial class TextureManager : IServiceType, IDisposable, ITextureProvider, ITextureSubstitutionProvider +{ + private static readonly ModuleLog Log = new(nameof(TextureManager)); + + [ServiceManager.ServiceDependency] + private readonly Dalamud dalamud = Service.Get(); + + [ServiceManager.ServiceDependency] + private readonly DataManager dataManager = Service.Get(); + + [ServiceManager.ServiceDependency] + private readonly Framework framework = Service.Get(); + + [ServiceManager.ServiceDependency] + private readonly InterfaceManager interfaceManager = Service.Get(); + + [ServiceManager.ServiceDependency] + private readonly TextureLoadThrottler textureLoadThrottler = Service.Get(); + + private SharedTextureManager? sharedTextureManager; + private WicManager? wicManager; + private bool disposing; + + [SuppressMessage( + "StyleCop.CSharp.LayoutRules", + "SA1519:Braces should not be omitted from multi-line child statement", + Justification = "Multiple fixed blocks")] + [ServiceManager.ServiceConstructor] + private TextureManager() + { + this.sharedTextureManager = new(this); + this.wicManager = new(this); + } + + /// Gets the shared texture manager. + public SharedTextureManager Shared => + this.sharedTextureManager ?? + throw new ObjectDisposedException(nameof(this.sharedTextureManager)); + + /// Gets the WIC manager. + public WicManager Wic => + this.wicManager ?? + throw new ObjectDisposedException(nameof(this.sharedTextureManager)); + + /// + public void Dispose() + { + if (this.disposing) + return; + + this.disposing = true; + + this.drawsOneSquare?.Dispose(); + this.drawsOneSquare = null; + + Interlocked.Exchange(ref this.sharedTextureManager, null)?.Dispose(); + Interlocked.Exchange(ref this.wicManager, null)?.Dispose(); + } + + /// + public Task CreateFromImageAsync( + ReadOnlyMemory bytes, + CancellationToken cancellationToken = default) => + this.textureLoadThrottler.LoadTextureAsync( + new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), + ct => Task.Run(() => this.NoThrottleCreateFromImage(bytes.ToArray(), ct), ct), + cancellationToken); + + /// + public Task CreateFromImageAsync( + 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(); + + /// + // It probably doesn't make sense to throttle this, as it copies the passed bytes to GPU without any transformation. + public IDalamudTextureWrap CreateFromRaw( + RawImageSpecification specs, + ReadOnlySpan bytes) => this.NoThrottleCreateFromRaw(specs, bytes); + + /// + public Task CreateFromRawAsync( + RawImageSpecification specs, + ReadOnlyMemory bytes, + CancellationToken cancellationToken = default) => + this.textureLoadThrottler.LoadTextureAsync( + new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), + _ => Task.FromResult(this.NoThrottleCreateFromRaw(specs, bytes.Span)), + cancellationToken); + + /// + public Task CreateFromRawAsync( + RawImageSpecification specs, + 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(); + + /// + public IDalamudTextureWrap CreateFromTexFile(TexFile file) => this.CreateFromTexFileAsync(file).Result; + + /// + public Task CreateFromTexFileAsync( + TexFile file, + CancellationToken cancellationToken = default) => + this.textureLoadThrottler.LoadTextureAsync( + new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), + ct => Task.Run(() => this.NoThrottleCreateFromTexFile(file), ct), + cancellationToken); + + /// + bool ITextureProvider.IsDxgiFormatSupported(int dxgiFormat) => + this.IsDxgiFormatSupported((DXGI_FORMAT)dxgiFormat); + + /// + public bool IsDxgiFormatSupported(DXGI_FORMAT dxgiFormat) + { + if (this.interfaceManager.Scene is not { } scene) + { + _ = Service.Get(); + scene = this.interfaceManager.Scene ?? throw new InvalidOperationException(); + } + + var format = (Format)dxgiFormat; + var support = scene.Device.CheckFormatSupport(format); + const FormatSupport required = FormatSupport.Texture2D; + return (support & required) == required; + } + + /// + internal IDalamudTextureWrap NoThrottleCreateFromRaw( + RawImageSpecification specs, + ReadOnlySpan bytes) + { + if (this.interfaceManager.Scene is not { } scene) + { + _ = Service.Get(); + scene = this.interfaceManager.Scene ?? throw new InvalidOperationException(); + } + + ShaderResourceView resView; + unsafe + { + fixed (void* pData = bytes) + { + var texDesc = new Texture2DDescription + { + Width = specs.Width, + Height = specs.Height, + MipLevels = 1, + ArraySize = 1, + Format = (Format)specs.DxgiFormat, + SampleDescription = new(1, 0), + Usage = ResourceUsage.Immutable, + BindFlags = BindFlags.ShaderResource, + CpuAccessFlags = CpuAccessFlags.None, + OptionFlags = ResourceOptionFlags.None, + }; + + using var texture = new Texture2D(scene.Device, texDesc, new DataRectangle(new(pData), specs.Pitch)); + resView = new( + scene.Device, + texture, + new() + { + Format = texDesc.Format, + Dimension = ShaderResourceViewDimension.Texture2D, + Texture2D = { MipLevels = texDesc.MipLevels }, + }); + } + } + + // no sampler for now because the ImGui implementation we copied doesn't allow for changing it + return new DalamudTextureWrap(new D3DTextureWrap(resView, specs.Width, specs.Height)); + } + + /// Creates a texture from the given . Skips the load throttler; intended to be used + /// from implementation of s. + /// The data. + /// The loaded texture. + internal IDalamudTextureWrap NoThrottleCreateFromTexFile(TexFile file) + { + ObjectDisposedException.ThrowIf(this.disposing, this); + + var buffer = file.TextureBuffer; + var (dxgiFormat, conversion) = TexFile.GetDxgiFormatFromTextureFormat(file.Header.Format, false); + if (conversion != TexFile.DxgiFormatConversion.NoConversion || + !this.IsDxgiFormatSupported((DXGI_FORMAT)dxgiFormat)) + { + dxgiFormat = (int)Format.B8G8R8A8_UNorm; + buffer = buffer.Filter(0, 0, TexFile.TextureFormat.B8G8R8A8); + } + + return this.NoThrottleCreateFromRaw( + RawImageSpecification.From(buffer.Width, buffer.Height, dxgiFormat), + buffer.RawData); + } + + /// Creates a texture from the given , trying to interpret it as a + /// . + /// The file bytes. + /// The loaded texture. + internal IDalamudTextureWrap NoThrottleCreateFromTexFile(ReadOnlySpan fileBytes) + { + ObjectDisposedException.ThrowIf(this.disposing, this); + + if (!TexFileExtensions.IsPossiblyTexFile2D(fileBytes)) + throw new InvalidDataException("The file is not a TexFile."); + + var bytesArray = fileBytes.ToArray(); + var tf = new TexFile(); + typeof(TexFile).GetProperty(nameof(tf.Data))!.GetSetMethod(true)!.Invoke( + tf, + new object?[] { bytesArray }); + typeof(TexFile).GetProperty(nameof(tf.Reader))!.GetSetMethod(true)!.Invoke( + tf, + new object?[] { new LuminaBinaryReader(bytesArray) }); + // Note: FileInfo and FilePath are not used from TexFile; skip it. + return this.NoThrottleCreateFromTexFile(tf); + } +} diff --git a/Dalamud/Interface/Internal/UnknownTextureWrap.cs b/Dalamud/Interface/Textures/Internal/UnknownTextureWrap.cs similarity index 96% rename from Dalamud/Interface/Internal/UnknownTextureWrap.cs rename to Dalamud/Interface/Textures/Internal/UnknownTextureWrap.cs index 41164f2c3..24e9a8bc1 100644 --- a/Dalamud/Interface/Internal/UnknownTextureWrap.cs +++ b/Dalamud/Interface/Textures/Internal/UnknownTextureWrap.cs @@ -1,10 +1,11 @@ using System.Threading; +using Dalamud.Interface.Internal; using Dalamud.Utility; using TerraFX.Interop.Windows; -namespace Dalamud.Interface.Internal; +namespace Dalamud.Interface.Textures.Internal; /// /// A texture wrap that is created by cloning the underlying . diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 3a718ef4c..f28f400c1 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -14,6 +14,7 @@ using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.ManagedFontAtlas.Internals; +using Dalamud.Interface.Textures.Internal; using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; using Dalamud.Utility; diff --git a/Dalamud/Interface/UldWrapper.cs b/Dalamud/Interface/UldWrapper.cs index 289db6faf..507730662 100644 --- a/Dalamud/Interface/UldWrapper.cs +++ b/Dalamud/Interface/UldWrapper.cs @@ -4,6 +4,7 @@ using System.Linq; using Dalamud.Data; using Dalamud.Interface.Internal; +using Dalamud.Interface.Textures.Internal; using Dalamud.Plugin.Services; using Dalamud.Utility; using Lumina.Data.Files; diff --git a/Dalamud/Plugin/Services/ITextureProvider.cs b/Dalamud/Plugin/Services/ITextureProvider.cs index ac6ab8baf..7715bd5d0 100644 --- a/Dalamud/Plugin/Services/ITextureProvider.cs +++ b/Dalamud/Plugin/Services/ITextureProvider.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Dalamud.Interface; using Dalamud.Interface.Internal; +using Dalamud.Interface.Textures; using Lumina.Data.Files; @@ -121,14 +122,41 @@ public partial interface ITextureProvider TexFile file, CancellationToken cancellationToken = default); + /// Gets the supported bitmap decoders. + /// The supported bitmap decoders. + /// + /// The following functions support the files of the container types pointed by yielded values. + ///
    + ///
  • + ///
  • + ///
  • + ///
  • + ///
+ ///
+ IEnumerable GetSupportedImageDecoderInfos(); + + /// Gets the supported bitmap encoders. + /// The supported bitmap encoders. + /// + /// The following function supports the files of the container types pointed by yielded values. + ///
    + ///
  • + ///
+ ///
+ IEnumerable GetSupportedImageEncoderInfos(); + /// Gets a shared texture corresponding to the given game resource icon specifier. /// A game icon specifier. /// The shared texture that you may use to obtain the loaded texture wrap and load states. + /// This function is under the effect of . + /// ISharedImmediateTexture GetFromGameIcon(in GameIconLookup lookup); /// Gets a shared texture corresponding to the given path to a game resource. /// A path to a game resource. /// The shared texture that you may use to obtain the loaded texture wrap and load states. + /// This function is under the effect of . + /// ISharedImmediateTexture GetFromGame(string path); /// Gets a shared texture corresponding to the given file on the filesystem. @@ -173,26 +201,16 @@ public partial interface ITextureProvider /// then the source data will be returned. /// This function can fail. /// - Task<(RawImageSpecification Specification, byte[] RawData)> GetRawDataAsync( + Task<(RawImageSpecification Specification, byte[] RawData)> GetRawDataFromExistingTextureAsync( IDalamudTextureWrap wrap, Vector2 uv0, Vector2 uv1, int dxgiFormat = 0, CancellationToken cancellationToken = default); - /// Gets the supported image file extensions available for loading. - /// The supported extensions. Each string[] entry indicates that there can be multiple extensions - /// that correspond to one container format. - IEnumerable GetLoadSupportedImageExtensions(); - - /// Gets the supported image file extensions available for saving. - /// The supported extensions. Each string[] entry indicates that there can be multiple extensions - /// that correspond to one container format. - IEnumerable GetSaveSupportedImageExtensions(); - /// Saves a texture wrap to a stream in an image file format. /// The texture wrap to save. - /// The extension of the file to deduce the file format with the leading dot. + /// The container GUID, obtained from . /// The stream to save to. /// Whether to leave open. /// Properties to pass to the encoder. See @@ -202,20 +220,33 @@ public partial interface ITextureProvider /// A task representing the save process. /// /// may be disposed as soon as this function returns. - /// If no image container format corresponding to is found, then the image will - /// be saved in png format. /// - [SuppressMessage( - "StyleCop.CSharp.LayoutRules", - "SA1519:Braces should not be omitted from multi-line child statement", - Justification = "Multiple fixed blocks")] - Task SaveAsImageFormatToStreamAsync( + Task SaveToStreamAsync( IDalamudTextureWrap wrap, - string extension, + Guid containerGuid, Stream stream, bool leaveOpen = false, IReadOnlyDictionary? props = null, CancellationToken cancellationToken = default); + + /// Saves a texture wrap to a file as an image file. + /// The texture wrap to save. + /// The container GUID, obtained from . + /// The target file path. The target file will be overwritten if it exist. + /// Properties to pass to the encoder. See + /// Microsoft + /// Learn for available parameters. + /// The cancellation token. + /// A task representing the save process. + /// + /// may be disposed as soon as this function returns. + /// + Task SaveToFileAsync( + IDalamudTextureWrap wrap, + Guid containerGuid, + string path, + IReadOnlyDictionary? props = null, + CancellationToken cancellationToken = default); /// /// Determines whether the system supports the given DXGI format. diff --git a/Dalamud/Plugin/Services/RawImageSpecification.cs b/Dalamud/Plugin/Services/RawImageSpecification.cs index 206ce578e..4d0aa2e9e 100644 --- a/Dalamud/Plugin/Services/RawImageSpecification.cs +++ b/Dalamud/Plugin/Services/RawImageSpecification.cs @@ -9,14 +9,24 @@ namespace Dalamud.Plugin.Services; /// /// The width of the image. /// The height of the image. -/// The pitch of the image. +/// The pitch of the image in bytes. The value may not always exactly match +/// * bytesPerPixelFromDxgiFormat. /// 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) +public readonly record struct RawImageSpecification(int Width, int Height, int Pitch, int DxgiFormat) { + private const string FormatNotSupportedMessage = $"{nameof(DxgiFormat)} is not supported."; + + /// Gets the number of bits per pixel. + /// Thrown if is not supported. + public int BitsPerPixel => + GetFormatInfo((DXGI_FORMAT)this.DxgiFormat, out var bitsPerPixel, out _) + ? bitsPerPixel + : throw new NotSupportedException(FormatNotSupportedMessage); + /// /// Creates a new instance of record using the given resolution and pixel /// format. Pitch will be automatically calculated. @@ -25,167 +35,11 @@ public record struct RawImageSpecification(int Width, int Height, int Pitch, int /// The height. /// The format. /// The new instance. + /// Thrown if is not supported. 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(); - } + if (!GetFormatInfo((DXGI_FORMAT)format, out var bitsPerPixel, out var isBlockCompression)) + throw new NotSupportedException(FormatNotSupportedMessage); var pitch = isBlockCompression ? Math.Max(1, (width + 3) / 4) * 2 * bitsPerPixel @@ -223,4 +77,157 @@ public record struct RawImageSpecification(int Width, int Height, int Pitch, int /// The new instance. public static RawImageSpecification A8(int width, int height) => new(width, height, width, (int)DXGI_FORMAT.DXGI_FORMAT_A8_UNORM); + + private static bool GetFormatInfo(DXGI_FORMAT format, out int bitsPerPixel, out bool isBlockCompression) + { + switch (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; + isBlockCompression = false; + return true; + 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; + isBlockCompression = false; + return true; + 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; + isBlockCompression = false; + return true; + 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; + isBlockCompression = false; + return true; + 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; + isBlockCompression = false; + return true; + 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; + isBlockCompression = false; + return true; + case DXGI_FORMAT.DXGI_FORMAT_R1_UNORM: + bitsPerPixel = 1; + isBlockCompression = false; + return true; + 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; + return true; + 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; + return true; + 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; + return true; + 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; + return true; + case DXGI_FORMAT.DXGI_FORMAT_B5G6R5_UNORM: + case DXGI_FORMAT.DXGI_FORMAT_B5G5R5A1_UNORM: + bitsPerPixel = 16; + isBlockCompression = false; + return true; + 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; + isBlockCompression = false; + return true; + 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; + return true; + case DXGI_FORMAT.DXGI_FORMAT_B4G4R4A4_UNORM: + bitsPerPixel = 16; + isBlockCompression = true; + return false; + default: + bitsPerPixel = 0; + isBlockCompression = false; + return false; + } + } } diff --git a/Dalamud/Storage/Assets/DalamudAssetManager.cs b/Dalamud/Storage/Assets/DalamudAssetManager.cs index 9db6d55a4..a9293eb6d 100644 --- a/Dalamud/Storage/Assets/DalamudAssetManager.cs +++ b/Dalamud/Storage/Assets/DalamudAssetManager.cs @@ -7,6 +7,7 @@ using System.Threading; using System.Threading.Tasks; using Dalamud.Interface.Internal; +using Dalamud.Interface.Textures.Internal; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Networking.Http; diff --git a/Dalamud/Utility/TerraFxCom/ManagedIEnumUnknownEnumerator.cs b/Dalamud/Utility/TerraFxCom/ManagedIEnumUnknownEnumerator.cs new file mode 100644 index 000000000..1f1ac9ffb --- /dev/null +++ b/Dalamud/Utility/TerraFxCom/ManagedIEnumUnknownEnumerator.cs @@ -0,0 +1,59 @@ +using System.Collections; +using System.Collections.Generic; + +using TerraFX.Interop.Windows; + +namespace Dalamud.Utility.TerraFxCom; + +/// Managed iterator for . +/// The unknown type. +internal sealed class ManagedIEnumUnknownEnumerator : IEnumerator> + where T : unmanaged, IUnknown.Interface +{ + private ComPtr unknownEnumerator; + private ComPtr current; + + /// Initializes a new instance of the class. + /// An instance of . Ownership is transferred. + public ManagedIEnumUnknownEnumerator(ComPtr unknownEnumerator) => + this.unknownEnumerator = unknownEnumerator; + + /// Finalizes an instance of the class. + ~ManagedIEnumUnknownEnumerator() => this.ReleaseUnmanagedResources(); + + /// + public ComPtr Current => this.current; + + /// + object IEnumerator.Current => this.current; + + /// + public void Dispose() + { + this.ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + + /// + public unsafe bool MoveNext() + { + using var punk = default(ComPtr); + var fetched = 0u; + while (this.unknownEnumerator.Get()->Next(1u, punk.ReleaseAndGetAddressOf(), &fetched) == S.S_OK && fetched == 1) + { + if (punk.As(ref this.current).SUCCEEDED) + return true; + } + + return false; + } + + /// + public unsafe void Reset() => this.unknownEnumerator.Get()->Reset().ThrowOnError(); + + private void ReleaseUnmanagedResources() + { + this.unknownEnumerator.Reset(); + this.current.Reset(); + } +} diff --git a/Dalamud/Utility/ManagedIStream.cs b/Dalamud/Utility/TerraFxCom/ManagedIStream.cs similarity index 88% rename from Dalamud/Utility/ManagedIStream.cs rename to Dalamud/Utility/TerraFxCom/ManagedIStream.cs index 33c05111c..942a9baf3 100644 --- a/Dalamud/Utility/ManagedIStream.cs +++ b/Dalamud/Utility/TerraFxCom/ManagedIStream.cs @@ -1,5 +1,4 @@ using System.Buffers; -using System.Diagnostics; using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -7,7 +6,7 @@ using System.Runtime.InteropServices; using TerraFX.Interop; using TerraFX.Interop.Windows; -namespace Dalamud.Utility; +namespace Dalamud.Utility.TerraFxCom; /// An wrapper for . [Guid("a620678b-56b9-4202-a1da-b821214dc972")] @@ -15,7 +14,8 @@ internal sealed unsafe class ManagedIStream : IStream.Interface, IRefCountable { private static readonly Guid MyGuid = typeof(ManagedIStream).GUID; - private readonly Stream inner; + private readonly Stream innerStream; + private readonly bool leaveOpen; private readonly nint[] comObject; private readonly IStream.Vtbl vtbl; private GCHandle gchThis; @@ -23,11 +23,10 @@ internal sealed unsafe class ManagedIStream : IStream.Interface, IRefCountable private GCHandle gchVtbl; private int refCount; - /// Initializes a new instance of the class. - /// The inner stream. - public ManagedIStream(Stream inner) + private ManagedIStream(Stream innerStream, bool leaveOpen = false) { - this.inner = inner; + this.innerStream = innerStream ?? throw new NullReferenceException(); + this.leaveOpen = leaveOpen; this.comObject = new nint[2]; this.vtbl.QueryInterface = &QueryInterfaceStatic; @@ -127,6 +126,26 @@ internal sealed unsafe class ManagedIStream : IStream.Interface, IRefCountable public static implicit operator IStream*(ManagedIStream mis) => (IStream*)mis.gchComObject.AddrOfPinnedObject(); + /// Creates a new instance of based on a managed . + /// The inner stream. + /// Whether to leave open on final release. + /// The new instance of based on . + public static ComPtr Create(Stream innerStream, bool leaveOpen = false) + { + try + { + var res = default(ComPtr); + res.Attach(new ManagedIStream(innerStream, leaveOpen)); + return res; + } + catch + { + if (!leaveOpen) + innerStream.Dispose(); + throw; + } + } + /// public HRESULT QueryInterface(Guid* riid, void** ppvObject) { @@ -176,6 +195,8 @@ internal sealed unsafe class ManagedIStream : IStream.Interface, IRefCountable this.gchThis.Free(); this.gchComObject.Free(); this.gchVtbl.Free(); + if (!this.leaveOpen) + this.innerStream.Dispose(); return newRefCount; case IRefCountable.RefCountResult.AlreadyDisposed: @@ -225,7 +246,7 @@ internal sealed unsafe class ManagedIStream : IStream.Interface, IRefCountable for (read = 0u; read < cb;) { var chunkSize = unchecked((int)Math.Min(0x10000000u, cb)); - var chunkRead = (uint)this.inner.Read(new(pv, chunkSize)); + var chunkRead = (uint)this.innerStream.Read(new(pv, chunkSize)); if (chunkRead == 0) break; pv = (byte*)pv + chunkRead; @@ -250,7 +271,7 @@ internal sealed unsafe class ManagedIStream : IStream.Interface, IRefCountable for (written = 0u; written < cb;) { var chunkSize = Math.Min(0x10000000u, cb); - this.inner.Write(new(pv, (int)chunkSize)); + this.innerStream.Write(new(pv, (int)chunkSize)); pv = (byte*)pv + chunkSize; written += chunkSize; } @@ -293,7 +314,7 @@ internal sealed unsafe class ManagedIStream : IStream.Interface, IRefCountable try { - var position = this.inner.Seek(dlibMove.QuadPart, seekOrigin); + var position = this.innerStream.Seek(dlibMove.QuadPart, seekOrigin); if (plibNewPosition != null) { *plibNewPosition = new() { QuadPart = (ulong)position }; @@ -312,7 +333,7 @@ internal sealed unsafe class ManagedIStream : IStream.Interface, IRefCountable { try { - this.inner.SetLength(checked((long)libNewSize.QuadPart)); + this.innerStream.SetLength(checked((long)libNewSize.QuadPart)); return S.S_OK; } catch (Exception e) when (e.HResult == unchecked((int)(0x80070000u | ERROR.ERROR_HANDLE_DISK_FULL))) @@ -355,7 +376,7 @@ internal sealed unsafe class ManagedIStream : IStream.Interface, IRefCountable { while (cbRead < cb) { - var read = checked((uint)this.inner.Read(buf.AsSpan())); + var read = checked((uint)this.innerStream.Read(buf.AsSpan())); if (read == 0) break; cbRead += read; @@ -414,13 +435,13 @@ internal sealed unsafe class ManagedIStream : IStream.Interface, IRefCountable return STG.STG_E_INVALIDPOINTER; ref var streamStats = ref *pstatstg; streamStats.type = (uint)STGTY.STGTY_STREAM; - streamStats.cbSize = (ulong)this.inner.Length; + streamStats.cbSize = (ulong)this.innerStream.Length; streamStats.grfMode = 0; - if (this.inner.CanRead && this.inner.CanWrite) + if (this.innerStream.CanRead && this.innerStream.CanWrite) streamStats.grfMode |= STGM.STGM_READWRITE; - else if (this.inner.CanRead) + else if (this.innerStream.CanRead) streamStats.grfMode |= STGM.STGM_READ; - else if (this.inner.CanWrite) + else if (this.innerStream.CanWrite) streamStats.grfMode |= STGM.STGM_WRITE; else return STG.STG_E_REVERTED; From 3853191c48bc75b1090a229b493cf3ba7c15f2b5 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 3 Mar 2024 00:51:28 +0900 Subject: [PATCH 35/57] More cleanup --- .../Windows/Data/Widgets/TexWidget.cs | 26 ++- .../Internals/FontAtlasFactory.cs | 29 +-- .../Textures/Internal/TextureLoadThrottler.cs | 37 +++- .../TextureManager.FromExistingTexture.cs | 165 ++++++++---------- .../Textures/Internal/TextureManager.Wic.cs | 27 +-- .../Textures/Internal/TextureManager.cs | 110 ++++++------ Dalamud/Plugin/Services/ITextureProvider.cs | 28 ++- 7 files changed, 226 insertions(+), 196 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs index 6779d0d60..fb4330bea 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -216,13 +216,37 @@ internal class TexWidget : IDataWindowWidget if (this.renderTargetChoiceInt < 0 || this.renderTargetChoiceInt >= supportedFormats.Length) return; var texTask = this.textureManager.CreateFromExistingTextureAsync( - source, + source.CreateWrapSharingLowLevelResource(), new(0.25f), new(0.75f), supportedFormats[this.renderTargetChoiceInt]); this.addedTextures.Add(new() { Api10 = texTask }); }; } + + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + unsafe + { + if (t.GetTexture(this.textureManager) is { } source) + { + var psrv = (ID3D11ShaderResourceView*)source.ImGuiHandle; + var rcsrv = psrv->AddRef() - 1; + psrv->Release(); + + var pres = default(ID3D11Resource*); + psrv->GetResource(&pres); + var rcres = pres->AddRef() - 1; + pres->Release(); + pres->Release(); + + ImGui.TextUnformatted($"RC: Resource({rcres})/View({rcsrv})"); + } + else + { + ImGui.TextUnformatted("RC: -"); + } + } try { diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs index 87776f53a..27888bb0b 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs @@ -21,10 +21,6 @@ using ImGuiScene; using Lumina.Data.Files; -using SharpDX; -using SharpDX.Direct3D11; -using SharpDX.DXGI; - using TerraFX.Interop.DirectX; namespace Dalamud.Interface.ManagedFontAtlas.Internals; @@ -249,31 +245,12 @@ internal sealed partial class FontAtlasFactory var fileIndex = textureIndex / 4; var channelIndex = FdtReader.FontTableEntry.TextureChannelOrder[textureIndex % 4]; wraps[textureIndex] ??= this.GetChannelTexture(texPathFormat, fileIndex, channelIndex); - return CloneTextureWrap(wraps[textureIndex]); + return wraps[textureIndex].CreateWrapSharingLowLevelResource(); } } private static T ExtractResult(Task t) => t.IsCompleted ? t.Result : t.GetAwaiter().GetResult(); - /// - /// Clones a texture wrap, by getting a new reference to the underlying and the - /// texture behind. - /// - /// The to clone from. - /// The cloned . - private static IDalamudTextureWrap CloneTextureWrap(IDalamudTextureWrap wrap) - { - var srv = CppObject.FromPointer(wrap.ImGuiHandle); - using var res = srv.Resource; - using var tex2D = res.QueryInterface(); - var description = tex2D.Description; - return new DalamudTextureWrap( - new D3DTextureWrap( - srv.QueryInterface(), - description.Width, - description.Height)); - } - private static unsafe void ExtractChannelFromB8G8R8A8( Span target, ReadOnlySpan source, @@ -384,7 +361,9 @@ internal sealed partial class FontAtlasFactory texFile.Header.Width, texFile.Header.Height, texFile.Header.Width * bpp, - (int)(targetIsB4G4R4A4 ? Format.B4G4R4A4_UNorm : Format.B8G8R8A8_UNorm)), + (int)(targetIsB4G4R4A4 + ? DXGI_FORMAT.DXGI_FORMAT_B4G4R4A4_UNORM + : DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM)), buffer)); } finally diff --git a/Dalamud/Interface/Textures/Internal/TextureLoadThrottler.cs b/Dalamud/Interface/Textures/Internal/TextureLoadThrottler.cs index a996a2890..1e7db4659 100644 --- a/Dalamud/Interface/Textures/Internal/TextureLoadThrottler.cs +++ b/Dalamud/Interface/Textures/Internal/TextureLoadThrottler.cs @@ -72,18 +72,21 @@ internal class TextureLoadThrottler : IServiceType, IDisposable /// The throttle basis. /// The immediate load function. /// The cancellation token. + /// Disposables to dispose when the task completes. /// The task. public Task LoadTextureAsync( IThrottleBasisProvider basis, Func> immediateLoadFunction, - CancellationToken cancellationToken) + CancellationToken cancellationToken, + params IDisposable?[] disposables) { - var work = new WorkItem(basis, immediateLoadFunction, cancellationToken); + var work = new WorkItem(basis, immediateLoadFunction, cancellationToken, disposables); - return - this.newItemChannel.Writer.TryWrite(work) - ? work.Task - : Task.FromException(new ObjectDisposedException(nameof(TextureLoadThrottler))); + if (this.newItemChannel.Writer.TryWrite(work)) + return work.Task; + + work.Dispose(); + return Task.FromException(new ObjectDisposedException(nameof(TextureLoadThrottler))); } private async Task LoopAddWorkItemAsync() @@ -118,6 +121,7 @@ internal class TextureLoadThrottler : IServiceType, IDisposable continue; await work.Process(this.disposeCancellationTokenSource.Token); + work.Dispose(); } } @@ -136,6 +140,7 @@ internal class TextureLoadThrottler : IServiceType, IDisposable { if (itemRef.CancelAsRequested()) { + itemRef.Dispose(); itemRef = lastRef; this.workItemPending.RemoveAt(this.workItemPending.Count - 1); break; @@ -152,7 +157,13 @@ internal class TextureLoadThrottler : IServiceType, IDisposable var last = this.workItemPending[^1]; this.workItemPending.RemoveAt(this.workItemPending.Count - 1); - return last.CancelAsRequested() ? null : last; + if (last.CancelAsRequested()) + { + last.Dispose(); + return null; + } + + return last; } } @@ -171,26 +182,34 @@ internal class TextureLoadThrottler : IServiceType, IDisposable public long LatestRequestedTick { get; init; } = Environment.TickCount64; } - private class WorkItem : IComparable + private sealed 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( IThrottleBasisProvider basis, Func> immediateLoadFunction, - CancellationToken cancellationToken) + CancellationToken cancellationToken, IDisposable?[] disposables) { this.taskCompletionSource = new(); this.basis = basis; this.cancellationToken = cancellationToken; + this.disposables = disposables; this.immediateLoadFunction = immediateLoadFunction; } public Task Task => this.taskCompletionSource.Task; + public void Dispose() + { + foreach (ref var d in this.disposables.AsSpan()) + Interlocked.Exchange(ref d, null)?.Dispose(); + } + public int CompareTo(WorkItem other) { if (this.basis.IsOpportunistic != other.basis.IsOpportunistic) diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs b/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs index e3a130a14..fc2a9e70f 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs @@ -10,9 +10,6 @@ using Dalamud.Utility; using ImGuiNET; -using SharpDX.Direct3D11; -using SharpDX.DXGI; - using TerraFX.Interop.DirectX; using TerraFX.Interop.Windows; @@ -28,14 +25,8 @@ internal sealed partial class TextureManager this.IsDxgiFormatSupportedForCreateFromExistingTextureAsync((DXGI_FORMAT)dxgiFormat); /// - public bool IsDxgiFormatSupportedForCreateFromExistingTextureAsync(DXGI_FORMAT dxgiFormat) + public unsafe bool IsDxgiFormatSupportedForCreateFromExistingTextureAsync(DXGI_FORMAT dxgiFormat) { - if (this.interfaceManager.Scene is not { } scene) - { - _ = Service.Get(); - scene = this.interfaceManager.Scene ?? throw new InvalidOperationException(); - } - switch (dxgiFormat) { // https://learn.microsoft.com/en-us/windows/win32/api/dxgiformat/ne-dxgiformat-dxgi_format @@ -48,12 +39,14 @@ internal sealed partial class TextureManager return false; } - var format = (Format)dxgiFormat; - var support = scene.Device.CheckFormatSupport(format); - const FormatSupport required = - FormatSupport.RenderTarget | - FormatSupport.Texture2D; - return (support & required) == required; + D3D11_FORMAT_SUPPORT supported; + if (this.Device.Get()->CheckFormatSupport(dxgiFormat, (uint*)&supported).FAILED) + return false; + + const D3D11_FORMAT_SUPPORT required = + D3D11_FORMAT_SUPPORT.D3D11_FORMAT_SUPPORT_TEXTURE2D + | D3D11_FORMAT_SUPPORT.D3D11_FORMAT_SUPPORT_RENDER_TARGET; + return (supported & required) == required; } /// @@ -62,61 +55,56 @@ internal sealed partial class TextureManager Vector2 uv0, Vector2 uv1, int dxgiFormat, + bool leaveWrapOpen, CancellationToken cancellationToken) => - this.CreateFromExistingTextureAsync(wrap, uv0, uv1, (DXGI_FORMAT)dxgiFormat, cancellationToken); + this.CreateFromExistingTextureAsync( + wrap, + uv0, + uv1, + (DXGI_FORMAT)dxgiFormat, + leaveWrapOpen, + cancellationToken); /// public Task CreateFromExistingTextureAsync( IDalamudTextureWrap wrap, Vector2 uv0, Vector2 uv1, - DXGI_FORMAT format, + DXGI_FORMAT format = DXGI_FORMAT.DXGI_FORMAT_UNKNOWN, + bool leaveWrapOpen = false, CancellationToken cancellationToken = default) { - var wrapCopy = wrap.CreateWrapSharingLowLevelResource(); return this.textureLoadThrottler.LoadTextureAsync( - new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), - async _ => - { - using var tex = await this.NoThrottleCreateFromExistingTextureAsync( - wrapCopy, - uv0, - uv1, - format); - using var device = default(ComPtr); - using var srv = default(ComPtr); - var desc = default(D3D11_TEXTURE2D_DESC); - unsafe - { - tex.Get()->GetDevice(device.GetAddressOf()); + new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), + ImmediateLoadFunction, + cancellationToken, + leaveWrapOpen ? null : wrap); - var srvDesc = new D3D11_SHADER_RESOURCE_VIEW_DESC( - tex, - D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE2D); - device.Get()->CreateShaderResourceView( - (ID3D11Resource*)tex.Get(), - &srvDesc, - srv.GetAddressOf()) - .ThrowOnError(); + async Task ImmediateLoadFunction(CancellationToken ct) + { + using var tex = await this.NoThrottleCreateFromExistingTextureAsync(wrap, uv0, uv1, format); - tex.Get()->GetDesc(&desc); + 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(); - return new UnknownTextureWrap( - (IUnknown*)srv.Get(), - (int)desc.Width, - (int)desc.Height, - true); - } - }, - cancellationToken) - .ContinueWith( - r => - { - wrapCopy.Dispose(); - return r; - }, - default(CancellationToken)) - .Unwrap(); + var desc = default(D3D11_TEXTURE2D_DESC); + tex.Get()->GetDesc(&desc); + return new UnknownTextureWrap( + (IUnknown*)srv.Get(), + (int)desc.Width, + (int)desc.Height, + true); + } + } } /// @@ -125,34 +113,37 @@ internal sealed partial class TextureManager Vector2 uv0, Vector2 uv1, int dxgiFormat, + bool leaveWrapOpen, CancellationToken cancellationToken) => - this.GetRawDataFromExistingTextureAsync(wrap, uv0, uv1, (DXGI_FORMAT)dxgiFormat, cancellationToken); + this.GetRawDataFromExistingTextureAsync( + wrap, + uv0, + uv1, + (DXGI_FORMAT)dxgiFormat, + leaveWrapOpen, + cancellationToken); /// public async Task<(RawImageSpecification Specification, byte[] RawData)> GetRawDataFromExistingTextureAsync( IDalamudTextureWrap wrap, Vector2 uv0, Vector2 uv1, - DXGI_FORMAT dxgiFormat, - CancellationToken cancellationToken) + DXGI_FORMAT dxgiFormat = DXGI_FORMAT.DXGI_FORMAT_UNKNOWN, + bool leaveWrapOpen = false, + CancellationToken cancellationToken = default) { - using var resUnk = default(ComPtr); + using var wrapDispose = leaveWrapOpen ? null : wrap; using var texSrv = default(ComPtr); - using var device = default(ComPtr); using var context = default(ComPtr); using var tex2D = default(ComPtr); var texDesc = default(D3D11_TEXTURE2D_DESC); unsafe { - resUnk.Attach((IUnknown*)wrap.ImGuiHandle); - resUnk.Get()->AddRef(); + fixed (Guid* piid = &IID.IID_ID3D11ShaderResourceView) + ((IUnknown*)wrap.ImGuiHandle)->QueryInterface(piid, (void**)texSrv.GetAddressOf()).ThrowOnError(); - resUnk.As(&texSrv).ThrowOnError(); - - texSrv.Get()->GetDevice(device.GetAddressOf()); - - device.Get()->GetImmediateContext(context.GetAddressOf()); + this.Device.Get()->GetImmediateContext(context.GetAddressOf()); using (var texRes = default(ComPtr)) { @@ -177,7 +168,7 @@ internal sealed partial class TextureManager cancellationToken.ThrowIfCancellationRequested(); return await this.interfaceManager.RunBeforePresent( - () => ExtractMappedResource(device, context, tex2D, cancellationToken)); + () => ExtractMappedResource(this.Device, context, tex2D, cancellationToken)); static unsafe (RawImageSpecification Specification, byte[] RawData) ExtractMappedResource( ComPtr device, @@ -242,28 +233,17 @@ internal sealed partial class TextureManager Vector2 uv1, DXGI_FORMAT format) { - using var resUnk = default(ComPtr); using var texSrv = default(ComPtr); - using var device = default(ComPtr); using var context = default(ComPtr); using var tex2D = default(ComPtr); var texDesc = default(D3D11_TEXTURE2D_DESC); unsafe { - resUnk.Attach((IUnknown*)wrap.ImGuiHandle); - resUnk.Get()->AddRef(); + fixed (Guid* piid = &IID.IID_ID3D11ShaderResourceView) + ((IUnknown*)wrap.ImGuiHandle)->QueryInterface(piid, (void**)texSrv.GetAddressOf()).ThrowOnError(); - using (var texSrv2 = default(ComPtr)) - { - resUnk.As(&texSrv2).ThrowOnError(); - texSrv.Attach(texSrv2); - texSrv2.Detach(); - } - - texSrv.Get()->GetDevice(device.GetAddressOf()); - - device.Get()->GetImmediateContext(context.GetAddressOf()); + this.Device.Get()->GetImmediateContext(context.GetAddressOf()); using (var texRes = default(ComPtr)) { @@ -274,6 +254,9 @@ internal sealed partial class TextureManager tex2D.Get()->GetDesc(&texDesc); } + if (format == DXGI_FORMAT.DXGI_FORMAT_UNKNOWN) + format = texDesc.Format; + var newWidth = checked((uint)MathF.Round((uv1.X - uv0.X) * texDesc.Width)); var newHeight = checked((uint)MathF.Round((uv1.Y - uv0.Y) * texDesc.Height)); @@ -289,11 +272,12 @@ internal sealed partial class TextureManager Format = format, SampleDesc = new(1, 0), Usage = D3D11_USAGE.D3D11_USAGE_DEFAULT, - BindFlags = (uint)(D3D11_BIND_FLAG.D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_FLAG.D3D11_BIND_RENDER_TARGET), + BindFlags = + (uint)(D3D11_BIND_FLAG.D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_FLAG.D3D11_BIND_RENDER_TARGET), CPUAccessFlags = 0u, MiscFlags = 0u, }; - device.Get()->CreateTexture2D(&tex2DCopyTempDesc, null, tex2DCopyTemp.GetAddressOf()).ThrowOnError(); + this.Device.Get()->CreateTexture2D(&tex2DCopyTempDesc, null, tex2DCopyTemp.GetAddressOf()).ThrowOnError(); } await this.interfaceManager.RunBeforePresent( @@ -305,13 +289,13 @@ internal sealed partial class TextureManager var rtvCopyTempDesc = new D3D11_RENDER_TARGET_VIEW_DESC( tex2DCopyTemp, D3D11_RTV_DIMENSION.D3D11_RTV_DIMENSION_TEXTURE2D); - device.Get()->CreateRenderTargetView( + this.Device.Get()->CreateRenderTargetView( (ID3D11Resource*)tex2DCopyTemp.Get(), &rtvCopyTempDesc, rtvCopyTemp.GetAddressOf()).ThrowOnError(); this.drawsOneSquare ??= new(); - this.drawsOneSquare.Setup(device.Get()); + this.drawsOneSquare.Setup(this.Device.Get()); context.Get()->OMSetRenderTargets(1u, rtvCopyTemp.GetAddressOf(), null); this.drawsOneSquare.Draw( @@ -593,6 +577,9 @@ internal sealed partial class TextureManager ctx->DSSetShader(null, null, 0); ctx->CSSetShader(null, null, 0); ctx->DrawIndexed(6, 0, 0); + + var ppn = default(ID3D11ShaderResourceView*); + ctx->PSSetShaderResources(0, 1, &ppn); } } } diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs b/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs index 6da68b7e0..ba70a95bf 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs @@ -37,23 +37,21 @@ internal sealed partial class TextureManager IDalamudTextureWrap wrap, Guid containerGuid, Stream stream, - bool leaveOpen = false, IReadOnlyDictionary? props = null, + bool leaveWrapOpen = false, + bool leaveStreamOpen = false, CancellationToken cancellationToken = default) { - using var istream = ManagedIStream.Create(stream, leaveOpen); + using var wrapDispose = leaveWrapOpen ? null : wrap; + using var istream = ManagedIStream.Create(stream, leaveStreamOpen); - RawImageSpecification specs; - byte[] bytes; - using (var wrapCopy = wrap.CreateWrapSharingLowLevelResource()) - { - (specs, bytes) = await this.GetRawDataFromExistingTextureAsync( - wrapCopy, + var (specs, bytes) = await this.GetRawDataFromExistingTextureAsync( + wrap, Vector2.Zero, Vector2.One, DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM, + true, cancellationToken).ConfigureAwait(false); - } this.Wic.SaveToStreamUsingWic( specs, @@ -70,12 +68,21 @@ internal sealed partial class TextureManager Guid containerGuid, string path, IReadOnlyDictionary? props = null, + bool leaveWrapOpen = false, CancellationToken cancellationToken = default) { + using var wrapDispose = leaveWrapOpen ? null : wrap; var pathTemp = $"{path}.{GetCurrentThreadId():X08}{Environment.TickCount64:X16}.tmp"; try { - await this.SaveToStreamAsync(wrap, containerGuid, File.Create(pathTemp), false, props, cancellationToken); + await this.SaveToStreamAsync( + wrap, + containerGuid, + File.Create(pathTemp), + props, + true, + false, + cancellationToken); } catch (Exception e) { diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.cs b/Dalamud/Interface/Textures/Internal/TextureManager.cs index 2a17f4d73..522167c55 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.cs @@ -16,12 +16,8 @@ using Dalamud.Utility; using Lumina.Data; using Lumina.Data.Files; -using SharpDX; -using SharpDX.Direct3D; -using SharpDX.Direct3D11; -using SharpDX.DXGI; - using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; namespace Dalamud.Interface.Textures.Internal; @@ -67,6 +63,23 @@ internal sealed partial class TextureManager : IServiceType, IDisposable, ITextu this.wicManager = new(this); } + /// Gets the D3D11 Device used to create textures. + public unsafe ComPtr Device + { + get + { + if (this.interfaceManager.Scene is not { } scene) + { + _ = Service.Get(); + scene = this.interfaceManager.Scene ?? throw new InvalidOperationException(); + } + + var device = default(ComPtr); + device.Attach((ID3D11Device*)scene.Device.NativePointer); + return device; + } + } + /// Gets the shared texture manager. public SharedTextureManager Shared => this.sharedTextureManager ?? @@ -183,65 +196,54 @@ internal sealed partial class TextureManager : IServiceType, IDisposable, ITextu this.IsDxgiFormatSupported((DXGI_FORMAT)dxgiFormat); /// - public bool IsDxgiFormatSupported(DXGI_FORMAT dxgiFormat) + public unsafe bool IsDxgiFormatSupported(DXGI_FORMAT dxgiFormat) { - if (this.interfaceManager.Scene is not { } scene) - { - _ = Service.Get(); - scene = this.interfaceManager.Scene ?? throw new InvalidOperationException(); - } - - var format = (Format)dxgiFormat; - var support = scene.Device.CheckFormatSupport(format); - const FormatSupport required = FormatSupport.Texture2D; - return (support & required) == required; + D3D11_FORMAT_SUPPORT supported; + if (this.Device.Get()->CheckFormatSupport(dxgiFormat, (uint*)&supported).FAILED) + return false; + + const D3D11_FORMAT_SUPPORT required = D3D11_FORMAT_SUPPORT.D3D11_FORMAT_SUPPORT_TEXTURE2D; + return (supported & required) == required; } /// - internal IDalamudTextureWrap NoThrottleCreateFromRaw( + internal unsafe IDalamudTextureWrap NoThrottleCreateFromRaw( RawImageSpecification specs, ReadOnlySpan bytes) { - if (this.interfaceManager.Scene is not { } scene) + var device = this.Device; + + var texd = new D3D11_TEXTURE2D_DESC { - _ = Service.Get(); - scene = this.interfaceManager.Scene ?? throw new InvalidOperationException(); - } - - ShaderResourceView resView; - unsafe + Width = (uint)specs.Width, + Height = (uint)specs.Height, + MipLevels = 1, + ArraySize = 1, + Format = (DXGI_FORMAT)specs.DxgiFormat, + SampleDesc = new(1, 0), + Usage = D3D11_USAGE.D3D11_USAGE_IMMUTABLE, + BindFlags = (uint)D3D11_BIND_FLAG.D3D11_BIND_SHADER_RESOURCE, + CPUAccessFlags = 0, + MiscFlags = 0, + }; + using var texture = default(ComPtr); + fixed (void* dataPtr = bytes) { - fixed (void* pData = bytes) - { - var texDesc = new Texture2DDescription - { - Width = specs.Width, - Height = specs.Height, - MipLevels = 1, - ArraySize = 1, - Format = (Format)specs.DxgiFormat, - SampleDescription = new(1, 0), - Usage = ResourceUsage.Immutable, - BindFlags = BindFlags.ShaderResource, - CpuAccessFlags = CpuAccessFlags.None, - OptionFlags = ResourceOptionFlags.None, - }; - - using var texture = new Texture2D(scene.Device, texDesc, new DataRectangle(new(pData), specs.Pitch)); - resView = new( - scene.Device, - texture, - new() - { - Format = texDesc.Format, - Dimension = ShaderResourceViewDimension.Texture2D, - Texture2D = { MipLevels = texDesc.MipLevels }, - }); - } + var subrdata = new D3D11_SUBRESOURCE_DATA { pSysMem = dataPtr, SysMemPitch = (uint)specs.Pitch }; + device.Get()->CreateTexture2D(&texd, &subrdata, texture.GetAddressOf()).ThrowOnError(); } + + var viewDesc = new D3D11_SHADER_RESOURCE_VIEW_DESC + { + Format = texd.Format, + ViewDimension = D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE2D, + Texture2D = new() { MipLevels = texd.MipLevels }, + }; + using var view = default(ComPtr); + device.Get()->CreateShaderResourceView((ID3D11Resource*)texture.Get(), &viewDesc, view.GetAddressOf()) + .ThrowOnError(); - // no sampler for now because the ImGui implementation we copied doesn't allow for changing it - return new DalamudTextureWrap(new D3DTextureWrap(resView, specs.Width, specs.Height)); + return new UnknownTextureWrap((IUnknown*)view.Get(), specs.Width, specs.Height, true); } /// Creates a texture from the given . Skips the load throttler; intended to be used @@ -257,7 +259,7 @@ internal sealed partial class TextureManager : IServiceType, IDisposable, ITextu if (conversion != TexFile.DxgiFormatConversion.NoConversion || !this.IsDxgiFormatSupported((DXGI_FORMAT)dxgiFormat)) { - dxgiFormat = (int)Format.B8G8R8A8_UNorm; + dxgiFormat = (int)DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM; buffer = buffer.Filter(0, 0, TexFile.TextureFormat.B8G8R8A8); } diff --git a/Dalamud/Plugin/Services/ITextureProvider.cs b/Dalamud/Plugin/Services/ITextureProvider.cs index 7715bd5d0..854579bcd 100644 --- a/Dalamud/Plugin/Services/ITextureProvider.cs +++ b/Dalamud/Plugin/Services/ITextureProvider.cs @@ -36,7 +36,9 @@ public partial interface ITextureProvider /// without having to wait for the completion of the returned . /// The left top coordinates relative to the size of the source texture. /// The right bottom coordinates relative to the size of the source texture. - /// The desired target format. + /// The desired target format. Use 0 to use the source format. + /// Whether to leave non-disposed when the returned + /// completes. /// The cancellation token. /// A containing the copied texture on success. Dispose after use. /// @@ -49,7 +51,8 @@ public partial interface ITextureProvider IDalamudTextureWrap wrap, Vector2 uv0, Vector2 uv1, - int dxgiFormat, + int dxgiFormat = 0, + bool leaveWrapOpen = false, CancellationToken cancellationToken = default); /// Gets a texture from the given bytes, trying to interpret it as a .tex file or other well-known image @@ -185,12 +188,13 @@ public partial interface ITextureProvider bool TryGetIconPath(in GameIconLookup lookup, [NotNullWhen(true)] out string? path); /// Gets the raw data of a texture wrap. - /// The source texture wrap. The passed value may be disposed once this function returns, - /// without having to wait for the completion of the returned . + /// The source texture wrap. /// The left top coordinates relative to the size of the source texture. /// The right bottom coordinates relative to the size of the source texture. /// The desired target format. /// If 0 (unknown) is passed, then the format will not be converted. + /// Whether to leave non-disposed when the returned + /// completes. /// The cancellation token. /// The raw data and its specifications. /// @@ -206,27 +210,32 @@ public partial interface ITextureProvider Vector2 uv0, Vector2 uv1, int dxgiFormat = 0, + bool leaveWrapOpen = false, CancellationToken cancellationToken = default); /// Saves a texture wrap to a stream in an image file format. /// The texture wrap to save. /// The container GUID, obtained from . /// The stream to save to. - /// Whether to leave open. /// Properties to pass to the encoder. See /// Microsoft /// Learn for available parameters. + /// Whether to leave non-disposed when the returned + /// completes. + /// Whether to leave open when the returned + /// completes. /// The cancellation token. /// A task representing the save process. /// - /// may be disposed as soon as this function returns. + /// must not be disposed until the task finishes. /// Task SaveToStreamAsync( IDalamudTextureWrap wrap, Guid containerGuid, Stream stream, - bool leaveOpen = false, IReadOnlyDictionary? props = null, + bool leaveWrapOpen = false, + bool leaveStreamOpen = false, CancellationToken cancellationToken = default); /// Saves a texture wrap to a file as an image file. @@ -236,16 +245,19 @@ public partial interface ITextureProvider /// Properties to pass to the encoder. See /// Microsoft /// Learn for available parameters. + /// Whether to leave non-disposed when the returned + /// completes. /// The cancellation token. /// A task representing the save process. /// - /// may be disposed as soon as this function returns. + /// must not be disposed until the task finishes. /// Task SaveToFileAsync( IDalamudTextureWrap wrap, Guid containerGuid, string path, IReadOnlyDictionary? props = null, + bool leaveWrapOpen = false, CancellationToken cancellationToken = default); /// From c0938bd3a95e0e576ecde82b55857f7e27b862c4 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 3 Mar 2024 16:21:18 +0900 Subject: [PATCH 36/57] fix --- .../Textures/Internal/TextureManager.Wic.cs | 56 +++++++++++++++---- 1 file changed, 46 insertions(+), 10 deletions(-) diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs b/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs index ba70a95bf..54e30dab0 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs @@ -388,9 +388,13 @@ internal sealed partial class TextureManager IReadOnlyDictionary? props = null, CancellationToken cancellationToken = default) { + var outPixelFormat = GUID.GUID_WICPixelFormat32bppBGRA; + var inPixelFormat = GetCorrespondingWicPixelFormat((DXGI_FORMAT)specs.DxgiFormat); + if (inPixelFormat == Guid.Empty) + throw new NotSupportedException("DXGI_FORMAT from specs is not supported by WIC."); + using var encoder = default(ComPtr); using var encoderFrame = default(ComPtr); - var guidPixelFormat = GUID.GUID_WICPixelFormat32bppBGRA; this.wicFactory.Get()->CreateEncoder(&containerFormat, null, encoder.GetAddressOf()).ThrowOnError(); cancellationToken.ThrowIfCancellationRequested(); @@ -436,31 +440,37 @@ internal sealed partial class TextureManager encoderFrame.Get()->Initialize(propertyBag).ThrowOnError(); cancellationToken.ThrowIfCancellationRequested(); - encoderFrame.Get()->SetPixelFormat(&guidPixelFormat).ThrowOnError(); + encoderFrame.Get()->SetPixelFormat(&outPixelFormat).ThrowOnError(); encoderFrame.Get()->SetSize(checked((uint)specs.Width), checked((uint)specs.Height)).ThrowOnError(); using var tempBitmap = default(ComPtr); - fixed (Guid* pGuid = &GUID.GUID_WICPixelFormat32bppBGRA) fixed (byte* pBytes = bytes) { this.wicFactory.Get()->CreateBitmapFromMemory( (uint)specs.Width, (uint)specs.Height, - pGuid, + &inPixelFormat, (uint)specs.Pitch, checked((uint)bytes.Length), pBytes, tempBitmap.GetAddressOf()).ThrowOnError(); } - using var tempBitmap2 = default(ComPtr); - WICConvertBitmapSource( - &guidPixelFormat, - (IWICBitmapSource*)tempBitmap.Get(), - tempBitmap2.GetAddressOf()).ThrowOnError(); + using var outBitmapSource = default(ComPtr); + if (inPixelFormat != outPixelFormat) + { + WICConvertBitmapSource( + &outPixelFormat, + (IWICBitmapSource*)tempBitmap.Get(), + outBitmapSource.GetAddressOf()).ThrowOnError(); + } + else + { + tempBitmap.As(&outBitmapSource); + } encoderFrame.Get()->SetSize(checked((uint)specs.Width), checked((uint)specs.Height)).ThrowOnError(); - encoderFrame.Get()->WriteSource(tempBitmap2.Get(), null).ThrowOnError(); + encoderFrame.Get()->WriteSource(outBitmapSource.Get(), null).ThrowOnError(); cancellationToken.ThrowIfCancellationRequested(); @@ -498,6 +508,32 @@ internal sealed partial class TextureManager _ => DXGI_FORMAT.DXGI_FORMAT_UNKNOWN, }; + /// + /// Gets the corresponding containing a WIC pixel format from a . + /// + /// The DXGI pixel format. + /// The corresponding , or if unavailable. + private static Guid GetCorrespondingWicPixelFormat(DXGI_FORMAT fmt) => fmt switch + { + // See https://github.com/microsoft/DirectXTex/wiki/WIC-I-O-Functions#savetowicmemory-savetowicfile + DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_FLOAT => GUID.GUID_WICPixelFormat128bppRGBAFloat, + DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_FLOAT => GUID.GUID_WICPixelFormat64bppRGBAHalf, + DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_UNORM => GUID.GUID_WICPixelFormat64bppRGBA, + DXGI_FORMAT.DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM => GUID.GUID_WICPixelFormat32bppRGBA1010102XR, + DXGI_FORMAT.DXGI_FORMAT_R10G10B10A2_UNORM => GUID.GUID_WICPixelFormat32bppRGBA1010102, + DXGI_FORMAT.DXGI_FORMAT_B5G5R5A1_UNORM => GUID.GUID_WICPixelFormat16bppBGRA5551, + DXGI_FORMAT.DXGI_FORMAT_B5G6R5_UNORM => GUID.GUID_WICPixelFormat16bppBGR565, + DXGI_FORMAT.DXGI_FORMAT_R32_FLOAT => GUID.GUID_WICPixelFormat32bppGrayFloat, + DXGI_FORMAT.DXGI_FORMAT_R16_FLOAT => GUID.GUID_WICPixelFormat16bppGrayHalf, + DXGI_FORMAT.DXGI_FORMAT_R16_UNORM => GUID.GUID_WICPixelFormat16bppGray, + DXGI_FORMAT.DXGI_FORMAT_R8_UNORM => GUID.GUID_WICPixelFormat8bppGray, + DXGI_FORMAT.DXGI_FORMAT_A8_UNORM => GUID.GUID_WICPixelFormat8bppAlpha, + DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM => GUID.GUID_WICPixelFormat32bppRGBA, + DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM => GUID.GUID_WICPixelFormat32bppBGRA, + DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM => GUID.GUID_WICPixelFormat32bppBGR, + _ => Guid.Empty, + }; + private void ReleaseUnmanagedResource() => this.wicFactory.Reset(); private readonly struct ComponentEnumerable : IEnumerable> From c04ce36b9cfb9a9b7bd1dde2ce5e51c3343fae67 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 3 Mar 2024 20:08:52 +0900 Subject: [PATCH 37/57] Add CreateFromGameScreen --- Dalamud/DalamudAsset.cs | 11 +- .../Interface/Internal/InterfaceManager.cs | 70 ++- .../Windows/Data/Widgets/TexWidget.cs | 122 +++-- .../FontAtlasFactory.BuildToolkit.cs | 4 +- .../Internals/FontAtlasFactory.cs | 4 +- .../ExistingTextureModificationArgs.cs | 95 ++++ .../GamePathSharedImmediateTexture.cs | 3 +- .../Internal/TextureManager.Drawer.cs | 406 +++++++++++++++++ .../TextureManager.FromExistingTexture.cs | 415 +++--------------- .../Internal/TextureManager.SharedTextures.cs | 2 +- .../Textures/Internal/TextureManager.Wic.cs | 285 +++++++----- .../Textures/Internal/TextureManager.cs | 20 +- .../Textures/Internal/ViewportTextureWrap.cs | 239 ++++++++++ Dalamud/Plugin/Services/ITextureProvider.cs | 114 +++-- .../Plugin/Services/RawImageSpecification.cs | 85 ++-- Dalamud/Storage/Assets/DalamudAssetManager.cs | 3 + .../Assets/DalamudAssetRawTextureAttribute.cs | 18 +- .../Storage/Assets/IDalamudAssetManager.cs | 5 + .../TerraFxCom/IPropertyBag2Extensions.cs | 83 ++++ 19 files changed, 1403 insertions(+), 581 deletions(-) create mode 100644 Dalamud/Interface/Textures/ExistingTextureModificationArgs.cs create mode 100644 Dalamud/Interface/Textures/Internal/TextureManager.Drawer.cs create mode 100644 Dalamud/Interface/Textures/Internal/ViewportTextureWrap.cs create mode 100644 Dalamud/Utility/TerraFxCom/IPropertyBag2Extensions.cs diff --git a/Dalamud/DalamudAsset.cs b/Dalamud/DalamudAsset.cs index a7b35b196..15d342962 100644 --- a/Dalamud/DalamudAsset.cs +++ b/Dalamud/DalamudAsset.cs @@ -1,5 +1,7 @@ using Dalamud.Storage.Assets; +using TerraFX.Interop.DirectX; + namespace Dalamud; /// @@ -19,9 +21,16 @@ public enum DalamudAsset /// : The fallback empty texture. /// [DalamudAsset(DalamudAssetPurpose.TextureFromRaw, data: new byte[] { 0, 0, 0, 0, 0, 0, 0, 0 })] - [DalamudAssetRawTexture(4, 8, 4, SharpDX.DXGI.Format.BC1_UNorm)] + [DalamudAssetRawTexture(4, 4, DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM, 8)] Empty4X4 = 1000, + /// + /// : The fallback empty texture. + /// + [DalamudAsset(DalamudAssetPurpose.TextureFromRaw, data: new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, 0, 0, 0, 0 })] + [DalamudAssetRawTexture(4, 4, DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM, 8)] + White4X4 = 1014, + /// /// : The Dalamud logo. /// diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 019b462cd..65c10d3a0 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -70,7 +70,8 @@ internal class InterfaceManager : IDisposable, IServiceType [ServiceManager.ServiceDependency] private readonly DalamudIme dalamudIme = Service.Get(); - private readonly ConcurrentQueue runBeforePresent = new(); + private readonly ConcurrentQueue runBeforeImGuiRender = new(); + private readonly ConcurrentQueue runAfterImGuiRender = new(); private readonly SwapChainVtableResolver address = new(); private readonly Hook setCursorHook; @@ -285,13 +286,13 @@ internal class InterfaceManager : IDisposable, IServiceType this.deferredDisposeDisposables.Add(locked); } - /// Queues an action to be run before Present call. + /// Queues an action to be run before call. /// The action. /// A that resolves once is run. - public Task RunBeforePresent(Action action) + public Task RunBeforeImGuiRender(Action action) { var tcs = new TaskCompletionSource(); - this.runBeforePresent.Enqueue( + this.runBeforeImGuiRender.Enqueue( () => { try @@ -307,14 +308,58 @@ internal class InterfaceManager : IDisposable, IServiceType return tcs.Task; } - /// Queues a function to be run before Present call. + /// Queues a function to be run before call. /// The type of the return value. /// The function. /// A that resolves once is run. - public Task RunBeforePresent(Func func) + public Task RunBeforeImGuiRender(Func func) { var tcs = new TaskCompletionSource(); - this.runBeforePresent.Enqueue( + this.runBeforeImGuiRender.Enqueue( + () => + { + try + { + tcs.SetResult(func()); + } + catch (Exception e) + { + tcs.SetException(e); + } + }); + return tcs.Task; + } + + /// Queues an action to be run after call. + /// The action. + /// A that resolves once is run. + public Task RunAfterImGuiRender(Action action) + { + var tcs = new TaskCompletionSource(); + this.runAfterImGuiRender.Enqueue( + () => + { + try + { + action(); + tcs.SetResult(); + } + catch (Exception e) + { + tcs.SetException(e); + } + }); + return tcs.Task; + } + + /// Queues a function to be run after call. + /// The type of the return value. + /// The function. + /// A that resolves once is run. + public Task RunAfterImGuiRender(Func func) + { + var tcs = new TaskCompletionSource(); + this.runAfterImGuiRender.Enqueue( () => { try @@ -566,7 +611,7 @@ internal class InterfaceManager : IDisposable, IServiceType if (!this.dalamudAtlas!.HasBuiltAtlas) return this.presentHook!.Original(swapChain, syncInterval, presentFlags); - while (this.runBeforePresent.TryDequeue(out var action)) + while (this.runBeforeImGuiRender.TryDequeue(out var action)) action.InvokeSafely(); if (this.address.IsReshade) @@ -574,19 +619,22 @@ internal class InterfaceManager : IDisposable, IServiceType var pRes = this.presentHook!.Original(swapChain, syncInterval, presentFlags); RenderImGui(this.scene!); - this.CleanupPostImGuiRender(); + this.PostImGuiRender(); return pRes; } RenderImGui(this.scene!); - this.CleanupPostImGuiRender(); + this.PostImGuiRender(); return this.presentHook!.Original(swapChain, syncInterval, presentFlags); } - private void CleanupPostImGuiRender() + private void PostImGuiRender() { + while (this.runAfterImGuiRender.TryDequeue(out var action)) + action.InvokeSafely(); + if (!this.deferredDisposeTextures.IsEmpty) { var count = 0; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs index fb4330bea..769df073b 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Dalamud.Interface.Components; using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.Textures; using Dalamud.Interface.Textures.Internal; using Dalamud.Interface.Textures.Internal.SharedImmediateTextures; using Dalamud.Interface.Utility; @@ -21,6 +22,7 @@ using ImGuiNET; using Serilog; using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; using TextureManager = Dalamud.Interface.Textures.Internal.TextureManager; @@ -49,6 +51,7 @@ internal class TexWidget : IDataWindowWidget private Vector2 inputTexScale = Vector2.Zero; private TextureManager textureManager = null!; private FileDialogManager fileDialogManager = null!; + private ExistingTextureModificationArgs existingTextureModificationArgs; private string[]? supportedRenderTargetFormatNames; private DXGI_FORMAT[]? supportedRenderTargetFormats; @@ -82,6 +85,13 @@ internal class TexWidget : IDataWindowWidget this.supportedRenderTargetFormats = null; this.supportedRenderTargetFormatNames = null; this.fileDialogManager = new(); + this.existingTextureModificationArgs = new() + { + Uv0 = new(0.25f), + Uv1 = new(0.75f), + NewWidth = 320, + NewHeight = 240, + }; this.Ready = true; } @@ -123,6 +133,31 @@ internal class TexWidget : IDataWindowWidget ImGui.PopID(); } + ImGui.Dummy(new(ImGui.GetTextLineHeightWithSpacing())); + + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Capture: "); + if (ImGui.Button("Game")) + this.addedTextures.Add(new() { Api10 = this.textureManager.CreateFromGameScreen() }); + + ImGui.SameLine(); + if (ImGui.Button("Game (Auto)")) + this.addedTextures.Add(new() { Api10 = this.textureManager.CreateFromGameScreen(true) }); + + ImGui.SameLine(); + if (ImGui.Button("Main Viewport")) + { + this.addedTextures.Add( + new() { Api10 = this.textureManager.CreateFromImGuiViewport(ImGui.GetMainViewport().ID) }); + } + + ImGui.SameLine(); + if (ImGui.Button("Main Viewport (Auto)")) + { + this.addedTextures.Add( + new() { Api10 = this.textureManager.CreateFromImGuiViewport(ImGui.GetMainViewport().ID, true) }); + } + if (ImGui.CollapsingHeader(nameof(ITextureProvider.GetFromGameIcon), ImGuiTreeNodeFlags.DefaultOpen)) { ImGui.PushID(nameof(this.DrawGetFromGameIcon)); @@ -158,24 +193,14 @@ internal class TexWidget : IDataWindowWidget ImGui.PopID(); } - ImGui.Dummy(new(ImGui.GetTextLineHeightWithSpacing())); - - if (this.supportedRenderTargetFormats is null) + if (ImGui.CollapsingHeader($"CropCopy##{this.DrawExistingTextureModificationArgs}")) { - this.supportedRenderTargetFormatNames = null; - this.supportedRenderTargetFormats = - Enum.GetValues() - .Where(this.textureManager.IsDxgiFormatSupportedForCreateFromExistingTextureAsync) - .ToArray(); - this.renderTargetChoiceInt = 0; + ImGui.PushID(nameof(this.DrawExistingTextureModificationArgs)); + this.DrawExistingTextureModificationArgs(); + ImGui.PopID(); } - this.supportedRenderTargetFormatNames ??= this.supportedRenderTargetFormats.Select(Enum.GetName).ToArray(); - ImGui.Combo( - "CropCopy Format", - ref this.renderTargetChoiceInt, - this.supportedRenderTargetFormatNames, - this.supportedRenderTargetFormatNames.Length); + ImGui.Dummy(new(ImGui.GetTextLineHeightWithSpacing())); Action? runLater = null; foreach (var t in this.addedTextures) @@ -217,13 +242,14 @@ internal class TexWidget : IDataWindowWidget return; var texTask = this.textureManager.CreateFromExistingTextureAsync( source.CreateWrapSharingLowLevelResource(), - new(0.25f), - new(0.75f), - supportedFormats[this.renderTargetChoiceInt]); + this.existingTextureModificationArgs with + { + Format = supportedFormats[this.renderTargetChoiceInt], + }); this.addedTextures.Add(new() { Api10 = texTask }); }; } - + ImGui.SameLine(); ImGui.AlignTextToFramePadding(); unsafe @@ -233,7 +259,7 @@ internal class TexWidget : IDataWindowWidget var psrv = (ID3D11ShaderResourceView*)source.ImGuiHandle; var rcsrv = psrv->AddRef() - 1; psrv->Release(); - + var pres = default(ID3D11Resource*); psrv->GetResource(&pres); var rcres = pres->AddRef() - 1; @@ -558,6 +584,49 @@ internal class TexWidget : IDataWindowWidget ImGuiHelpers.ScaledDummy(10); } + private void DrawExistingTextureModificationArgs() + { + var vec2 = this.existingTextureModificationArgs.Uv0; + if (ImGui.InputFloat2("UV0", ref vec2)) + this.existingTextureModificationArgs.Uv0 = vec2; + + vec2 = this.existingTextureModificationArgs.Uv1; + if (ImGui.InputFloat2("UV1", ref vec2)) + this.existingTextureModificationArgs.Uv1 = vec2; + + Span wh = stackalloc int[2]; + wh[0] = this.existingTextureModificationArgs.NewWidth; + wh[1] = this.existingTextureModificationArgs.NewHeight; + if (ImGui.InputInt2("New Size", ref wh[0])) + { + this.existingTextureModificationArgs.NewWidth = wh[0]; + this.existingTextureModificationArgs.NewHeight = wh[1]; + } + + var b = this.existingTextureModificationArgs.MakeOpaque; + if (ImGui.Checkbox("Make Opaque", ref b)) + this.existingTextureModificationArgs.MakeOpaque = b; + + if (this.supportedRenderTargetFormats is null) + { + this.supportedRenderTargetFormatNames = null; + this.supportedRenderTargetFormats = + Enum.GetValues() + .Where(this.textureManager.IsDxgiFormatSupportedForCreateFromExistingTextureAsync) + .ToArray(); + this.renderTargetChoiceInt = 0; + } + + this.supportedRenderTargetFormatNames ??= this.supportedRenderTargetFormats.Select(Enum.GetName).ToArray(); + ImGui.Combo( + "Format", + ref this.renderTargetChoiceInt, + this.supportedRenderTargetFormatNames, + this.supportedRenderTargetFormatNames.Length); + + ImGuiHelpers.ScaledDummy(10); + } + private async void SaveTextureAsync(string name, Func> textureGetter) { try @@ -627,15 +696,18 @@ internal class TexWidget : IDataWindowWidget } using var textureWrap = await textureGetter.Invoke(); + var props = new Dictionary(); + if (encoder.ContainerGuid == GUID.GUID_ContainerFormatTiff) + props["CompressionQuality"] = 1.0f; + else if (encoder.ContainerGuid == GUID.GUID_ContainerFormatJpeg || + encoder.ContainerGuid == GUID.GUID_ContainerFormatHeif || + encoder.ContainerGuid == GUID.GUID_ContainerFormatWmp) + props["ImageQuality"] = 1.0f; await this.textureManager.SaveToFileAsync( textureWrap, encoder.ContainerGuid, path, - props: new Dictionary - { - ["CompressionQuality"] = 1.0f, - ["ImageQuality"] = 1.0f, - }); + props: props); Service.Get().AddNotification( $"File saved to: {path}", diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs index 73530bf0e..316174c4e 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs @@ -638,8 +638,8 @@ internal sealed partial class FontAtlasFactory new( width, height, - width * bpp, - (int)(use4 ? Format.B4G4R4A4_UNorm : Format.B8G8R8A8_UNorm)), + (int)(use4 ? Format.B4G4R4A4_UNorm : Format.B8G8R8A8_UNorm), + width * bpp), buf); this.data.AddExistingTexture(wrap); texture.TexID = wrap.ImGuiHandle; diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs index 27888bb0b..39e969fb8 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs @@ -360,10 +360,10 @@ internal sealed partial class FontAtlasFactory new( texFile.Header.Width, texFile.Header.Height, - texFile.Header.Width * bpp, (int)(targetIsB4G4R4A4 ? DXGI_FORMAT.DXGI_FORMAT_B4G4R4A4_UNORM - : DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM)), + : DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM), + texFile.Header.Width * bpp), buffer)); } finally diff --git a/Dalamud/Interface/Textures/ExistingTextureModificationArgs.cs b/Dalamud/Interface/Textures/ExistingTextureModificationArgs.cs new file mode 100644 index 000000000..9bcb9f34d --- /dev/null +++ b/Dalamud/Interface/Textures/ExistingTextureModificationArgs.cs @@ -0,0 +1,95 @@ +using System.Numerics; + +using Dalamud.Plugin.Services; + +using TerraFX.Interop.DirectX; + +namespace Dalamud.Interface.Textures; + +/// Describes how to modify an existing texture. +public record struct ExistingTextureModificationArgs() +{ + /// Gets or sets the left top coordinates relative to the size of the source texture. + /// Coordinates should be in range between 0 and 1. + public Vector2 Uv0 { get; set; } = Vector2.Zero; + + /// Gets or sets the right bottom coordinates relative to the size of the source texture. + /// Coordinates should be in range between 0 and 1. + /// If set to , then it will be interpreted as , + /// to accommodate the use of default value of this record struct. + public Vector2 Uv1 { get; set; } = Vector2.One; + + /// Gets or sets the new width. + /// Set to 0 to automatically calculate according to the original texture size, , and + /// . + public int NewWidth { get; set; } + + /// Gets or sets the new height. + /// Set to 0 to automatically calculate according to the original texture size, , and + /// . + public int NewHeight { get; set; } + + /// Gets or sets a value indicating whether to make the texture opaque. + /// Alpha channel values will be filled with 1.0. + public bool MakeOpaque { get; set; } = false; + + /// Gets or sets the new DXGI format. + /// + /// Set to 0 () to use the source pixel format. + /// Supported values can be queried with + /// . This may not necessarily + /// match . + /// + public int DxgiFormat { get; set; } = (int)DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM; + + /// Gets or sets the format (typed). + internal DXGI_FORMAT Format + { + get => (DXGI_FORMAT)this.DxgiFormat; + set => this.DxgiFormat = (int)value; + } + + /// Gets the effective value of . + internal Vector2 Uv1Effective => this.Uv1 == Vector2.Zero ? Vector2.One : this.Uv1; + + /// Test if this instance of does not instruct to change the + /// underlying data of a texture. + /// The texture description to test against. + /// true if this instance of does not instruct to + /// change the underlying data of a texture. + internal bool IsCompleteSourceCopy(in D3D11_TEXTURE2D_DESC desc) => + this.Uv0 == Vector2.Zero + && this.Uv1 == Vector2.One + && (this.NewWidth == 0 || this.NewWidth == desc.Width) + && (this.NewHeight == 0 || this.NewHeight == desc.Height) + && !this.MakeOpaque + && (this.Format == DXGI_FORMAT.DXGI_FORMAT_UNKNOWN || this.Format == desc.Format); + + /// Checks the properties and throws an exception if values are invalid. + internal void ThrowOnInvalidValues() + { + if (this.Uv0.X is < 0 or > 1 or float.NaN) + throw new ArgumentException($"{nameof(this.Uv0)}.X is out of range."); + + if (this.Uv0.Y is < 0 or > 1 or float.NaN) + throw new ArgumentException($"{nameof(this.Uv0)}.Y is out of range."); + + if (this.Uv1Effective.X is < 0 or > 1 or float.NaN) + throw new ArgumentException($"{nameof(this.Uv1)}.X is out of range."); + + if (this.Uv1Effective.Y is < 0 or > 1 or float.NaN) + throw new ArgumentException($"{nameof(this.Uv1)}.Y is out of range."); + + if (this.Uv0.X >= this.Uv1Effective.X || this.Uv0.Y >= this.Uv1Effective.Y) + { + throw new ArgumentException( + $"{nameof(this.Uv0)} must be strictly less than {nameof(this.Uv1)} in a componentwise way."); + } + + if (this.NewWidth < 0) + throw new ArgumentException($"{nameof(this.NewWidth)} cannot be a negative number."); + + if (this.NewHeight < 0) + throw new ArgumentException($"{nameof(this.NewHeight)} cannot be a negative number."); + } +} diff --git a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs index a0562f1ef..1dfe96fe2 100644 --- a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs +++ b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs @@ -28,7 +28,8 @@ internal sealed class GamePathSharedImmediateTexture : SharedImmediateTexture public static SharedImmediateTexture CreatePlaceholder(string path) => new GamePathSharedImmediateTexture(path); /// - public override string ToString() => $"{nameof(GamePathSharedImmediateTexture)}#{this.InstanceIdForDebug}({this.path})"; + public override string ToString() => + $"{nameof(GamePathSharedImmediateTexture)}#{this.InstanceIdForDebug}({this.path})"; /// protected override void ReleaseResources() diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.Drawer.cs b/Dalamud/Interface/Textures/Internal/TextureManager.Drawer.cs new file mode 100644 index 000000000..c1043e914 --- /dev/null +++ b/Dalamud/Interface/Textures/Internal/TextureManager.Drawer.cs @@ -0,0 +1,406 @@ +using System.Buffers; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; + +using Dalamud.Interface.Internal; +using Dalamud.Storage.Assets; +using Dalamud.Utility; + +using ImGuiNET; + +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +namespace Dalamud.Interface.Textures.Internal; + +/// Service responsible for loading and disposing ImGui texture wraps. +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", + "SA1519:Braces should not be omitted from multi-line child statement", + Justification = "Multiple fixed blocks")] + internal sealed unsafe class SimpleDrawerImpl : IDisposable + { + private ComPtr sampler; + private ComPtr vertexShader; + private ComPtr pixelShader; + private ComPtr inputLayout; + private ComPtr vertexConstantBuffer; + private ComPtr blendState; + private ComPtr blendStateForStrippingAlpha; + private ComPtr rasterizerState; + private ComPtr vertexBufferFill; + private ComPtr vertexBufferMutable; + private ComPtr indexBuffer; + + /// Finalizes an instance of the class. + ~SimpleDrawerImpl() => this.Dispose(); + + /// + public void Dispose() + { + this.sampler.Reset(); + this.vertexShader.Reset(); + this.pixelShader.Reset(); + this.inputLayout.Reset(); + this.vertexConstantBuffer.Reset(); + this.blendState.Reset(); + this.blendStateForStrippingAlpha.Reset(); + this.rasterizerState.Reset(); + this.vertexBufferFill.Reset(); + this.vertexBufferMutable.Reset(); + this.indexBuffer.Reset(); + GC.SuppressFinalize(this); + } + + /// Sets up this instance of . + /// The device. + public void Setup(ID3D11Device* device) + { + var assembly = typeof(ImGuiScene.ImGui_Impl_DX11).Assembly; + + // Create the vertex shader + if (this.vertexShader.IsEmpty() || this.inputLayout.IsEmpty()) + { + this.vertexShader.Reset(); + this.inputLayout.Reset(); + + using var stream = assembly.GetManifestResourceStream("imgui-vertex.hlsl.bytes")!; + var array = ArrayPool.Shared.Rent((int)stream.Length); + stream.ReadExactly(array, 0, (int)stream.Length); + fixed (byte* pArray = array) + fixed (ID3D11VertexShader** ppShader = &this.vertexShader.GetPinnableReference()) + fixed (ID3D11InputLayout** ppInputLayout = &this.inputLayout.GetPinnableReference()) + fixed (void* pszPosition = "POSITION"u8) + fixed (void* pszTexCoord = "TEXCOORD"u8) + fixed (void* pszColor = "COLOR"u8) + { + device->CreateVertexShader(pArray, (nuint)stream.Length, null, ppShader).ThrowOnError(); + + var ied = stackalloc D3D11_INPUT_ELEMENT_DESC[] + { + new() + { + SemanticName = (sbyte*)pszPosition, + Format = DXGI_FORMAT.DXGI_FORMAT_R32G32_FLOAT, + AlignedByteOffset = uint.MaxValue, + }, + new() + { + SemanticName = (sbyte*)pszTexCoord, + Format = DXGI_FORMAT.DXGI_FORMAT_R32G32_FLOAT, + AlignedByteOffset = uint.MaxValue, + }, + new() + { + SemanticName = (sbyte*)pszColor, + Format = DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM, + AlignedByteOffset = uint.MaxValue, + }, + }; + device->CreateInputLayout(ied, 3, pArray, (nuint)stream.Length, ppInputLayout).ThrowOnError(); + } + + ArrayPool.Shared.Return(array); + } + + // Create the constant buffer + if (this.vertexConstantBuffer.IsEmpty()) + { + var bufferDesc = new D3D11_BUFFER_DESC( + (uint)sizeof(Matrix4x4), + (uint)D3D11_BIND_FLAG.D3D11_BIND_CONSTANT_BUFFER, + D3D11_USAGE.D3D11_USAGE_IMMUTABLE); + var data = Matrix4x4.Identity; + var subr = new D3D11_SUBRESOURCE_DATA { pSysMem = &data }; + fixed (ID3D11Buffer** ppBuffer = &this.vertexConstantBuffer.GetPinnableReference()) + device->CreateBuffer(&bufferDesc, &subr, ppBuffer).ThrowOnError(); + } + + // Create the pixel shader + if (this.pixelShader.IsEmpty()) + { + using var stream = assembly.GetManifestResourceStream("imgui-frag.hlsl.bytes")!; + var array = ArrayPool.Shared.Rent((int)stream.Length); + stream.ReadExactly(array, 0, (int)stream.Length); + fixed (byte* pArray = array) + fixed (ID3D11PixelShader** ppShader = &this.pixelShader.GetPinnableReference()) + device->CreatePixelShader(pArray, (nuint)stream.Length, null, ppShader).ThrowOnError(); + + ArrayPool.Shared.Return(array); + } + + // Create the blending setup + if (this.blendState.IsEmpty()) + { + var blendStateDesc = new D3D11_BLEND_DESC + { + RenderTarget = + { + e0 = + { + BlendEnable = true, + SrcBlend = D3D11_BLEND.D3D11_BLEND_SRC_ALPHA, + DestBlend = D3D11_BLEND.D3D11_BLEND_INV_SRC_ALPHA, + BlendOp = D3D11_BLEND_OP.D3D11_BLEND_OP_ADD, + SrcBlendAlpha = D3D11_BLEND.D3D11_BLEND_INV_DEST_ALPHA, + DestBlendAlpha = D3D11_BLEND.D3D11_BLEND_ONE, + BlendOpAlpha = D3D11_BLEND_OP.D3D11_BLEND_OP_ADD, + RenderTargetWriteMask = (byte)D3D11_COLOR_WRITE_ENABLE.D3D11_COLOR_WRITE_ENABLE_ALL, + }, + }, + }; + fixed (ID3D11BlendState** ppBlendState = &this.blendState.GetPinnableReference()) + device->CreateBlendState(&blendStateDesc, ppBlendState).ThrowOnError(); + } + + if (this.blendStateForStrippingAlpha.IsEmpty()) + { + var blendStateDesc = new D3D11_BLEND_DESC + { + RenderTarget = + { + e0 = + { + BlendEnable = true, + SrcBlend = D3D11_BLEND.D3D11_BLEND_ZERO, + DestBlend = D3D11_BLEND.D3D11_BLEND_ONE, + BlendOp = D3D11_BLEND_OP.D3D11_BLEND_OP_ADD, + SrcBlendAlpha = D3D11_BLEND.D3D11_BLEND_ONE, + DestBlendAlpha = D3D11_BLEND.D3D11_BLEND_ZERO, + BlendOpAlpha = D3D11_BLEND_OP.D3D11_BLEND_OP_ADD, + RenderTargetWriteMask = (byte)D3D11_COLOR_WRITE_ENABLE.D3D11_COLOR_WRITE_ENABLE_ALPHA, + }, + }, + }; + fixed (ID3D11BlendState** ppBlendState = &this.blendStateForStrippingAlpha.GetPinnableReference()) + device->CreateBlendState(&blendStateDesc, ppBlendState).ThrowOnError(); + } + + // Create the rasterizer state + if (this.rasterizerState.IsEmpty()) + { + var rasterizerDesc = new D3D11_RASTERIZER_DESC + { + FillMode = D3D11_FILL_MODE.D3D11_FILL_SOLID, + CullMode = D3D11_CULL_MODE.D3D11_CULL_NONE, + }; + fixed (ID3D11RasterizerState** ppRasterizerState = &this.rasterizerState.GetPinnableReference()) + device->CreateRasterizerState(&rasterizerDesc, ppRasterizerState).ThrowOnError(); + } + + // Create the font sampler + if (this.sampler.IsEmpty()) + { + var samplerDesc = new D3D11_SAMPLER_DESC( + D3D11_FILTER.D3D11_FILTER_MIN_MAG_MIP_LINEAR, + D3D11_TEXTURE_ADDRESS_MODE.D3D11_TEXTURE_ADDRESS_WRAP, + D3D11_TEXTURE_ADDRESS_MODE.D3D11_TEXTURE_ADDRESS_WRAP, + D3D11_TEXTURE_ADDRESS_MODE.D3D11_TEXTURE_ADDRESS_WRAP, + 0f, + 0, + D3D11_COMPARISON_FUNC.D3D11_COMPARISON_ALWAYS, + null, + 0, + 0); + fixed (ID3D11SamplerState** ppSampler = &this.sampler.GetPinnableReference()) + device->CreateSamplerState(&samplerDesc, ppSampler).ThrowOnError(); + } + + if (this.vertexBufferFill.IsEmpty()) + { + var data = stackalloc ImDrawVert[] + { + new() { col = uint.MaxValue, pos = new(-1, 1), uv = new(0, 0) }, + new() { col = uint.MaxValue, pos = new(-1, -1), uv = new(0, 1) }, + new() { col = uint.MaxValue, pos = new(1, 1), uv = new(1, 0) }, + new() { col = uint.MaxValue, pos = new(1, -1), uv = new(1, 1) }, + }; + var desc = new D3D11_BUFFER_DESC( + (uint)(sizeof(ImDrawVert) * 4), + (uint)D3D11_BIND_FLAG.D3D11_BIND_VERTEX_BUFFER, + D3D11_USAGE.D3D11_USAGE_IMMUTABLE); + var subr = new D3D11_SUBRESOURCE_DATA { pSysMem = data }; + var buffer = default(ID3D11Buffer*); + device->CreateBuffer(&desc, &subr, &buffer).ThrowOnError(); + this.vertexBufferFill.Attach(buffer); + } + + if (this.vertexBufferMutable.IsEmpty()) + { + var desc = new D3D11_BUFFER_DESC( + (uint)(sizeof(ImDrawVert) * 4), + (uint)D3D11_BIND_FLAG.D3D11_BIND_VERTEX_BUFFER, + D3D11_USAGE.D3D11_USAGE_DYNAMIC, + (uint)D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_WRITE); + var buffer = default(ID3D11Buffer*); + device->CreateBuffer(&desc, null, &buffer).ThrowOnError(); + this.vertexBufferMutable.Attach(buffer); + } + + if (this.indexBuffer.IsEmpty()) + { + var data = stackalloc ushort[] { 0, 1, 2, 1, 2, 3 }; + var desc = new D3D11_BUFFER_DESC( + sizeof(ushort) * 6, + (uint)D3D11_BIND_FLAG.D3D11_BIND_INDEX_BUFFER, + D3D11_USAGE.D3D11_USAGE_IMMUTABLE); + var subr = new D3D11_SUBRESOURCE_DATA { pSysMem = data }; + var buffer = default(ID3D11Buffer*); + device->CreateBuffer(&desc, &subr, &buffer).ThrowOnError(); + this.indexBuffer.Attach(buffer); + } + } + + /// Draws the given shader resource view to the current render target. + /// An instance of . + /// The shader resource view. + /// The left top coordinates relative to the size of the source texture. + /// The right bottom coordinates relative to the size of the source texture. + /// This function does not throw. + public void Draw( + ID3D11DeviceContext* ctx, + ID3D11ShaderResourceView* srv, + Vector2 uv0, + Vector2 uv1) + { + using var rtv = default(ComPtr); + ctx->OMGetRenderTargets(1, rtv.GetAddressOf(), null); + if (rtv.IsEmpty()) + return; + + using var rtvRes = default(ComPtr); + rtv.Get()->GetResource(rtvRes.GetAddressOf()); + + using var rtvTex = default(ComPtr); + if (rtvRes.As(&rtvTex).FAILED) + return; + + D3D11_TEXTURE2D_DESC texDesc; + rtvTex.Get()->GetDesc(&texDesc); + + ID3D11Buffer* buffer; + if (uv0 == Vector2.Zero && uv1 == Vector2.One) + { + buffer = this.vertexBufferFill.Get(); + } + else + { + buffer = this.vertexBufferMutable.Get(); + var mapped = default(D3D11_MAPPED_SUBRESOURCE); + if (ctx->Map((ID3D11Resource*)buffer, 0, D3D11_MAP.D3D11_MAP_WRITE_DISCARD, 0u, &mapped).FAILED) + return; + _ = new Span(mapped.pData, 4) + { + [0] = new() { col = uint.MaxValue, pos = new(-1, 1), uv = uv0 }, + [1] = new() { col = uint.MaxValue, pos = new(-1, -1), uv = new(uv0.X, uv1.Y) }, + [2] = new() { col = uint.MaxValue, pos = new(1, 1), uv = new(uv1.X, uv0.Y) }, + [3] = new() { col = uint.MaxValue, pos = new(1, -1), uv = uv1 }, + }; + ctx->Unmap((ID3D11Resource*)buffer, 0u); + } + + var stride = (uint)sizeof(ImDrawVert); + var offset = 0u; + + ctx->IASetInputLayout(this.inputLayout); + ctx->IASetVertexBuffers(0, 1, &buffer, &stride, &offset); + ctx->IASetIndexBuffer(this.indexBuffer, DXGI_FORMAT.DXGI_FORMAT_R16_UINT, 0); + ctx->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY.D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + + var viewport = new D3D11_VIEWPORT(0, 0, texDesc.Width, texDesc.Height); + ctx->RSSetState(this.rasterizerState); + ctx->RSSetViewports(1, &viewport); + + var blendColor = default(Vector4); + ctx->OMSetBlendState(this.blendState, (float*)&blendColor, 0xffffffff); + ctx->OMSetDepthStencilState(null, 0); + + ctx->VSSetShader(this.vertexShader.Get(), null, 0); + buffer = this.vertexConstantBuffer.Get(); + ctx->VSSetConstantBuffers(0, 1, &buffer); + + ctx->PSSetShader(this.pixelShader, null, 0); + var simp = this.sampler.Get(); + ctx->PSSetSamplers(0, 1, &simp); + ctx->PSSetShaderResources(0, 1, &srv); + + ctx->GSSetShader(null, null, 0); + ctx->HSSetShader(null, null, 0); + ctx->DSSetShader(null, null, 0); + ctx->CSSetShader(null, null, 0); + ctx->DrawIndexed(6, 0, 0); + + var ppn = default(ID3D11ShaderResourceView*); + ctx->PSSetShaderResources(0, 1, &ppn); + } + + /// Fills alpha channel to 1.0 from the current render target. + /// An instance of . + /// This function does not throw. + public void StripAlpha(ID3D11DeviceContext* ctx) + { + using var rtv = default(ComPtr); + ctx->OMGetRenderTargets(1, rtv.GetAddressOf(), null); + if (rtv.IsEmpty()) + return; + + using var rtvRes = default(ComPtr); + rtv.Get()->GetResource(rtvRes.GetAddressOf()); + + using var rtvTex = default(ComPtr); + if (rtvRes.As(&rtvTex).FAILED) + return; + + D3D11_TEXTURE2D_DESC texDesc; + rtvTex.Get()->GetDesc(&texDesc); + + var buffer = this.vertexBufferFill.Get(); + var stride = (uint)sizeof(ImDrawVert); + var offset = 0u; + + ctx->IASetInputLayout(this.inputLayout); + ctx->IASetVertexBuffers(0, 1, &buffer, &stride, &offset); + ctx->IASetIndexBuffer(this.indexBuffer, DXGI_FORMAT.DXGI_FORMAT_R16_UINT, 0); + ctx->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY.D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + + var viewport = new D3D11_VIEWPORT(0, 0, texDesc.Width, texDesc.Height); + ctx->RSSetState(this.rasterizerState); + ctx->RSSetViewports(1, &viewport); + + var blendColor = default(Vector4); + ctx->OMSetBlendState(this.blendStateForStrippingAlpha, (float*)&blendColor, 0xffffffff); + ctx->OMSetDepthStencilState(null, 0); + + ctx->VSSetShader(this.vertexShader.Get(), null, 0); + buffer = this.vertexConstantBuffer.Get(); + ctx->VSSetConstantBuffers(0, 1, &buffer); + + ctx->PSSetShader(this.pixelShader, null, 0); + var simp = this.sampler.Get(); + ctx->PSSetSamplers(0, 1, &simp); + var ppn = (ID3D11ShaderResourceView*)Service.Get().White4X4.ImGuiHandle; + ctx->PSSetShaderResources(0, 1, &ppn); + + ctx->GSSetShader(null, null, 0); + ctx->HSSetShader(null, null, 0); + ctx->DSSetShader(null, null, 0); + ctx->CSSetShader(null, null, 0); + ctx->DrawIndexed(6, 0, 0); + + ppn = default; + ctx->PSSetShaderResources(0, 1, &ppn); + } + } +} diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs b/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs index fc2a9e70f..56b0356e5 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs @@ -1,6 +1,3 @@ -using System.Buffers; -using System.Diagnostics.CodeAnalysis; -using System.Numerics; using System.Threading; using System.Threading.Tasks; @@ -18,8 +15,6 @@ namespace Dalamud.Interface.Textures.Internal; /// Service responsible for loading and disposing ImGui texture wraps. internal sealed partial class TextureManager { - private DrawsOneSquare? drawsOneSquare; - /// bool ITextureProvider.IsDxgiFormatSupportedForCreateFromExistingTextureAsync(int dxgiFormat) => this.IsDxgiFormatSupportedForCreateFromExistingTextureAsync((DXGI_FORMAT)dxgiFormat); @@ -50,27 +45,9 @@ internal sealed partial class TextureManager } /// - Task ITextureProvider.CreateFromExistingTextureAsync( - IDalamudTextureWrap wrap, - Vector2 uv0, - Vector2 uv1, - int dxgiFormat, - bool leaveWrapOpen, - CancellationToken cancellationToken) => - this.CreateFromExistingTextureAsync( - wrap, - uv0, - uv1, - (DXGI_FORMAT)dxgiFormat, - leaveWrapOpen, - cancellationToken); - - /// public Task CreateFromExistingTextureAsync( IDalamudTextureWrap wrap, - Vector2 uv0, - Vector2 uv1, - DXGI_FORMAT format = DXGI_FORMAT.DXGI_FORMAT_UNKNOWN, + ExistingTextureModificationArgs args = default, bool leaveWrapOpen = false, CancellationToken cancellationToken = default) { @@ -82,7 +59,7 @@ internal sealed partial class TextureManager async Task ImmediateLoadFunction(CancellationToken ct) { - using var tex = await this.NoThrottleCreateFromExistingTextureAsync(wrap, uv0, uv1, format); + using var tex = await this.NoThrottleCreateFromExistingTextureAsync(wrap, args); unsafe { @@ -108,27 +85,52 @@ internal sealed partial class TextureManager } /// - Task<(RawImageSpecification Specification, byte[] RawData)> ITextureProvider.GetRawDataFromExistingTextureAsync( - IDalamudTextureWrap wrap, - Vector2 uv0, - Vector2 uv1, - int dxgiFormat, - bool leaveWrapOpen, - CancellationToken cancellationToken) => - this.GetRawDataFromExistingTextureAsync( - wrap, - uv0, - uv1, - (DXGI_FORMAT)dxgiFormat, - leaveWrapOpen, - cancellationToken); + public Task CreateFromGameScreen( + bool autoUpdate = false, + CancellationToken cancellationToken = default) => + this.interfaceManager.RunBeforeImGuiRender( + () => + { + cancellationToken.ThrowIfCancellationRequested(); + var t = new ViewportTextureWrap(ImGui.GetMainViewport().ID, true, autoUpdate, cancellationToken); + t.Update(); + try + { + return t.FirstUpdateTask.Result; + } + catch + { + t.Dispose(); + throw; + } + }); - /// + /// + public Task CreateFromImGuiViewport( + uint viewportId, + bool autoUpdate = false, + CancellationToken cancellationToken = default) => + this.interfaceManager.RunBeforeImGuiRender( + () => + { + cancellationToken.ThrowIfCancellationRequested(); + var t = new ViewportTextureWrap(viewportId, false, autoUpdate, cancellationToken); + t.Update(); + try + { + return t.FirstUpdateTask.Result; + } + catch + { + t.Dispose(); + throw; + } + }); + + /// public async Task<(RawImageSpecification Specification, byte[] RawData)> GetRawDataFromExistingTextureAsync( IDalamudTextureWrap wrap, - Vector2 uv0, - Vector2 uv1, - DXGI_FORMAT dxgiFormat = DXGI_FORMAT.DXGI_FORMAT_UNKNOWN, + ExistingTextureModificationArgs args = default, bool leaveWrapOpen = false, CancellationToken cancellationToken = default) { @@ -157,9 +159,9 @@ internal sealed partial class TextureManager tex2D.Get()->GetDesc(&texDesc); } - if (texDesc.Format != dxgiFormat && dxgiFormat != DXGI_FORMAT.DXGI_FORMAT_UNKNOWN) + if (!args.IsCompleteSourceCopy(texDesc)) { - using var tmp = await this.NoThrottleCreateFromExistingTextureAsync(wrap, uv0, uv1, dxgiFormat); + using var tmp = await this.NoThrottleCreateFromExistingTextureAsync(wrap, args); unsafe { tex2D.Swap(&tmp); @@ -167,7 +169,7 @@ internal sealed partial class TextureManager } cancellationToken.ThrowIfCancellationRequested(); - return await this.interfaceManager.RunBeforePresent( + return await this.interfaceManager.RunBeforeImGuiRender( () => ExtractMappedResource(this.Device, context, tex2D, cancellationToken)); static unsafe (RawImageSpecification Specification, byte[] RawData) ExtractMappedResource( @@ -214,8 +216,8 @@ internal sealed partial class TextureManager var specs = new RawImageSpecification( (int)desc.Width, (int)desc.Height, - (int)mapped.RowPitch, - (int)desc.Format); + (int)desc.Format, + (int)mapped.RowPitch); var bytes = new Span(mapped.pData, checked((int)mapped.DepthPitch)).ToArray(); return (specs, bytes); } @@ -229,10 +231,10 @@ internal sealed partial class TextureManager private async Task> NoThrottleCreateFromExistingTextureAsync( IDalamudTextureWrap wrap, - Vector2 uv0, - Vector2 uv1, - DXGI_FORMAT format) + ExistingTextureModificationArgs args) { + args.ThrowOnInvalidValues(); + using var texSrv = default(ComPtr); using var context = default(ComPtr); using var tex2D = default(ComPtr); @@ -254,33 +256,34 @@ internal sealed partial class TextureManager tex2D.Get()->GetDesc(&texDesc); } - if (format == DXGI_FORMAT.DXGI_FORMAT_UNKNOWN) - format = texDesc.Format; - - var newWidth = checked((uint)MathF.Round((uv1.X - uv0.X) * texDesc.Width)); - var newHeight = checked((uint)MathF.Round((uv1.Y - uv0.Y) * texDesc.Height)); + if (args.Format == DXGI_FORMAT.DXGI_FORMAT_UNKNOWN) + args = args with { Format = texDesc.Format }; + if (args.NewWidth == 0) + args = args with { NewWidth = (int)MathF.Round((args.Uv1Effective.X - args.Uv0.X) * texDesc.Width) }; + if (args.NewHeight == 0) + args = args with { NewHeight = (int)MathF.Round((args.Uv1Effective.Y - args.Uv0.Y) * texDesc.Height) }; using var tex2DCopyTemp = default(ComPtr); unsafe { var tex2DCopyTempDesc = new D3D11_TEXTURE2D_DESC { - Width = newWidth, - Height = newHeight, + Width = (uint)args.NewWidth, + Height = (uint)args.NewHeight, MipLevels = 1, ArraySize = 1, - Format = format, + Format = args.Format, SampleDesc = new(1, 0), Usage = D3D11_USAGE.D3D11_USAGE_DEFAULT, - BindFlags = - (uint)(D3D11_BIND_FLAG.D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_FLAG.D3D11_BIND_RENDER_TARGET), + BindFlags = (uint)(D3D11_BIND_FLAG.D3D11_BIND_SHADER_RESOURCE | + D3D11_BIND_FLAG.D3D11_BIND_RENDER_TARGET), CPUAccessFlags = 0u, MiscFlags = 0u, }; this.Device.Get()->CreateTexture2D(&tex2DCopyTempDesc, null, tex2DCopyTemp.GetAddressOf()).ThrowOnError(); } - await this.interfaceManager.RunBeforePresent( + await this.interfaceManager.RunBeforeImGuiRender( () => { unsafe @@ -294,292 +297,20 @@ internal sealed partial class TextureManager &rtvCopyTempDesc, rtvCopyTemp.GetAddressOf()).ThrowOnError(); - this.drawsOneSquare ??= new(); - this.drawsOneSquare.Setup(this.Device.Get()); - context.Get()->OMSetRenderTargets(1u, rtvCopyTemp.GetAddressOf(), null); - this.drawsOneSquare.Draw( + this.SimpleDrawer.Draw( context.Get(), texSrv.Get(), - (int)newWidth, - (int)newHeight, - uv0, - uv1); - context.Get()->OMSetRenderTargets(0, null, null); + args.Uv0, + args.Uv1Effective); + if (args.MakeOpaque) + this.SimpleDrawer.StripAlpha(context.Get()); + + var dummy = default(ID3D11RenderTargetView*); + context.Get()->OMSetRenderTargets(1u, &dummy, null); } }); return new(tex2DCopyTemp); } - - [SuppressMessage( - "StyleCop.CSharp.LayoutRules", - "SA1519:Braces should not be omitted from multi-line child statement", - Justification = "Multiple fixed blocks")] - private sealed unsafe class DrawsOneSquare : IDisposable - { - private ComPtr sampler; - private ComPtr vertexShader; - private ComPtr pixelShader; - private ComPtr inputLayout; - private ComPtr vertexConstantBuffer; - private ComPtr blendState; - private ComPtr rasterizerState; - private ComPtr vertexBufferFill; - private ComPtr vertexBufferMutable; - private ComPtr indexBuffer; - - ~DrawsOneSquare() => this.Dispose(); - - public void Dispose() - { - this.sampler.Reset(); - this.vertexShader.Reset(); - this.pixelShader.Reset(); - this.inputLayout.Reset(); - this.vertexConstantBuffer.Reset(); - this.blendState.Reset(); - this.rasterizerState.Reset(); - this.vertexBufferFill.Reset(); - this.vertexBufferMutable.Reset(); - this.indexBuffer.Reset(); - } - - public void Setup(T* device) where T : unmanaged, ID3D11Device.Interface - { - var assembly = typeof(ImGuiScene.ImGui_Impl_DX11).Assembly; - - // Create the vertex shader - if (this.vertexShader.IsEmpty() || this.inputLayout.IsEmpty()) - { - this.vertexShader.Reset(); - this.inputLayout.Reset(); - - using var stream = assembly.GetManifestResourceStream("imgui-vertex.hlsl.bytes")!; - var array = ArrayPool.Shared.Rent((int)stream.Length); - stream.ReadExactly(array, 0, (int)stream.Length); - fixed (byte* pArray = array) - fixed (ID3D11VertexShader** ppShader = &this.vertexShader.GetPinnableReference()) - fixed (ID3D11InputLayout** ppInputLayout = &this.inputLayout.GetPinnableReference()) - fixed (void* pszPosition = "POSITION"u8) - fixed (void* pszTexCoord = "TEXCOORD"u8) - fixed (void* pszColor = "COLOR"u8) - { - device->CreateVertexShader(pArray, (nuint)stream.Length, null, ppShader).ThrowOnError(); - - var ied = stackalloc D3D11_INPUT_ELEMENT_DESC[] - { - new() - { - SemanticName = (sbyte*)pszPosition, - Format = DXGI_FORMAT.DXGI_FORMAT_R32G32_FLOAT, - AlignedByteOffset = uint.MaxValue, - }, - new() - { - SemanticName = (sbyte*)pszTexCoord, - Format = DXGI_FORMAT.DXGI_FORMAT_R32G32_FLOAT, - AlignedByteOffset = uint.MaxValue, - }, - new() - { - SemanticName = (sbyte*)pszColor, - Format = DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM, - AlignedByteOffset = uint.MaxValue, - }, - }; - device->CreateInputLayout(ied, 3, pArray, (nuint)stream.Length, ppInputLayout).ThrowOnError(); - } - - ArrayPool.Shared.Return(array); - } - - // Create the constant buffer - if (this.vertexConstantBuffer.IsEmpty()) - { - var bufferDesc = new D3D11_BUFFER_DESC( - (uint)sizeof(Matrix4x4), - (uint)D3D11_BIND_FLAG.D3D11_BIND_CONSTANT_BUFFER, - D3D11_USAGE.D3D11_USAGE_IMMUTABLE); - var data = Matrix4x4.Identity; - var subr = new D3D11_SUBRESOURCE_DATA { pSysMem = &data }; - fixed (ID3D11Buffer** ppBuffer = &this.vertexConstantBuffer.GetPinnableReference()) - device->CreateBuffer(&bufferDesc, &subr, ppBuffer).ThrowOnError(); - } - - // Create the pixel shader - if (this.pixelShader.IsEmpty()) - { - using var stream = assembly.GetManifestResourceStream("imgui-frag.hlsl.bytes")!; - var array = ArrayPool.Shared.Rent((int)stream.Length); - stream.ReadExactly(array, 0, (int)stream.Length); - fixed (byte* pArray = array) - fixed (ID3D11PixelShader** ppShader = &this.pixelShader.GetPinnableReference()) - device->CreatePixelShader(pArray, (nuint)stream.Length, null, ppShader).ThrowOnError(); - - ArrayPool.Shared.Return(array); - } - - // Create the blending setup - if (this.blendState.IsEmpty()) - { - var blendStateDesc = new D3D11_BLEND_DESC - { - RenderTarget = - { - e0 = - { - BlendEnable = true, - SrcBlend = D3D11_BLEND.D3D11_BLEND_SRC_ALPHA, - DestBlend = D3D11_BLEND.D3D11_BLEND_INV_SRC_ALPHA, - BlendOp = D3D11_BLEND_OP.D3D11_BLEND_OP_ADD, - SrcBlendAlpha = D3D11_BLEND.D3D11_BLEND_INV_DEST_ALPHA, - DestBlendAlpha = D3D11_BLEND.D3D11_BLEND_ONE, - BlendOpAlpha = D3D11_BLEND_OP.D3D11_BLEND_OP_ADD, - RenderTargetWriteMask = (byte)D3D11_COLOR_WRITE_ENABLE.D3D11_COLOR_WRITE_ENABLE_ALL, - }, - }, - }; - fixed (ID3D11BlendState** ppBlendState = &this.blendState.GetPinnableReference()) - device->CreateBlendState(&blendStateDesc, ppBlendState).ThrowOnError(); - } - - // Create the rasterizer state - if (this.rasterizerState.IsEmpty()) - { - var rasterizerDesc = new D3D11_RASTERIZER_DESC - { - FillMode = D3D11_FILL_MODE.D3D11_FILL_SOLID, - CullMode = D3D11_CULL_MODE.D3D11_CULL_NONE, - }; - fixed (ID3D11RasterizerState** ppRasterizerState = &this.rasterizerState.GetPinnableReference()) - device->CreateRasterizerState(&rasterizerDesc, ppRasterizerState).ThrowOnError(); - } - - // Create the font sampler - if (this.sampler.IsEmpty()) - { - var samplerDesc = new D3D11_SAMPLER_DESC( - D3D11_FILTER.D3D11_FILTER_MIN_MAG_MIP_LINEAR, - D3D11_TEXTURE_ADDRESS_MODE.D3D11_TEXTURE_ADDRESS_WRAP, - D3D11_TEXTURE_ADDRESS_MODE.D3D11_TEXTURE_ADDRESS_WRAP, - D3D11_TEXTURE_ADDRESS_MODE.D3D11_TEXTURE_ADDRESS_WRAP, - 0f, - 0, - D3D11_COMPARISON_FUNC.D3D11_COMPARISON_ALWAYS, - null, - 0, - 0); - fixed (ID3D11SamplerState** ppSampler = &this.sampler.GetPinnableReference()) - device->CreateSamplerState(&samplerDesc, ppSampler).ThrowOnError(); - } - - if (this.vertexBufferFill.IsEmpty()) - { - var data = stackalloc ImDrawVert[] - { - new() { col = uint.MaxValue, pos = new(-1, 1), uv = new(0, 0) }, - new() { col = uint.MaxValue, pos = new(-1, -1), uv = new(0, 1) }, - new() { col = uint.MaxValue, pos = new(1, 1), uv = new(1, 0) }, - new() { col = uint.MaxValue, pos = new(1, -1), uv = new(1, 1) }, - }; - var desc = new D3D11_BUFFER_DESC( - (uint)(sizeof(ImDrawVert) * 4), - (uint)D3D11_BIND_FLAG.D3D11_BIND_VERTEX_BUFFER, - D3D11_USAGE.D3D11_USAGE_IMMUTABLE); - var subr = new D3D11_SUBRESOURCE_DATA { pSysMem = data }; - var buffer = default(ID3D11Buffer*); - device->CreateBuffer(&desc, &subr, &buffer).ThrowOnError(); - this.vertexBufferFill.Attach(buffer); - } - - if (this.vertexBufferMutable.IsEmpty()) - { - var desc = new D3D11_BUFFER_DESC( - (uint)(sizeof(ImDrawVert) * 4), - (uint)D3D11_BIND_FLAG.D3D11_BIND_VERTEX_BUFFER, - D3D11_USAGE.D3D11_USAGE_DYNAMIC, - (uint)D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_WRITE); - var buffer = default(ID3D11Buffer*); - device->CreateBuffer(&desc, null, &buffer).ThrowOnError(); - this.vertexBufferMutable.Attach(buffer); - } - - if (this.indexBuffer.IsEmpty()) - { - var data = stackalloc ushort[] { 0, 1, 2, 1, 2, 3 }; - var desc = new D3D11_BUFFER_DESC( - sizeof(ushort) * 6, - (uint)D3D11_BIND_FLAG.D3D11_BIND_INDEX_BUFFER, - D3D11_USAGE.D3D11_USAGE_IMMUTABLE); - var subr = new D3D11_SUBRESOURCE_DATA { pSysMem = data }; - var buffer = default(ID3D11Buffer*); - device->CreateBuffer(&desc, &subr, &buffer).ThrowOnError(); - this.indexBuffer.Attach(buffer); - } - } - - public void Draw( - ID3D11DeviceContext* ctx, - ID3D11ShaderResourceView* srv, - int width, - int height, - Vector2 uv0, - Vector2 uv1) - { - ID3D11Buffer* buffer; - if (uv0 == Vector2.Zero && uv1 == Vector2.One) - { - buffer = this.vertexBufferFill.Get(); - } - else - { - buffer = this.vertexBufferMutable.Get(); - var mapped = default(D3D11_MAPPED_SUBRESOURCE); - ctx->Map((ID3D11Resource*)buffer, 0, D3D11_MAP.D3D11_MAP_WRITE_DISCARD, 0u, &mapped).ThrowOnError(); - _ = new Span(mapped.pData, 4) - { - [0] = new() { col = uint.MaxValue, pos = new(-1, 1), uv = uv0 }, - [1] = new() { col = uint.MaxValue, pos = new(-1, -1), uv = new(uv0.X, uv1.Y) }, - [2] = new() { col = uint.MaxValue, pos = new(1, 1), uv = new(uv1.X, uv0.Y) }, - [3] = new() { col = uint.MaxValue, pos = new(1, -1), uv = uv1 }, - }; - ctx->Unmap((ID3D11Resource*)buffer, 0u); - } - - var stride = (uint)sizeof(ImDrawVert); - var offset = 0u; - - ctx->IASetInputLayout(this.inputLayout); - ctx->IASetVertexBuffers(0, 1, &buffer, &stride, &offset); - ctx->IASetIndexBuffer(this.indexBuffer, DXGI_FORMAT.DXGI_FORMAT_R16_UINT, 0); - ctx->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY.D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); - - var viewport = new D3D11_VIEWPORT(0, 0, width, height); - ctx->RSSetState(this.rasterizerState); - ctx->RSSetViewports(1, &viewport); - - var blendColor = default(Vector4); - ctx->OMSetBlendState(this.blendState, (float*)&blendColor, 0xffffffff); - ctx->OMSetDepthStencilState(null, 0); - - ctx->VSSetShader(this.vertexShader.Get(), null, 0); - buffer = this.vertexConstantBuffer.Get(); - ctx->VSSetConstantBuffers(0, 1, &buffer); - - ctx->PSSetShader(this.pixelShader, null, 0); - var simp = this.sampler.Get(); - ctx->PSSetSamplers(0, 1, &simp); - ctx->PSSetShaderResources(0, 1, &srv); - - ctx->GSSetShader(null, null, 0); - ctx->HSSetShader(null, null, 0); - ctx->DSSetShader(null, null, 0); - ctx->CSSetShader(null, null, 0); - ctx->DrawIndexed(6, 0, 0); - - var ppn = default(ID3D11ShaderResourceView*); - ctx->PSSetShaderResources(0, 1, &ppn); - } - } } diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.SharedTextures.cs b/Dalamud/Interface/Textures/Internal/TextureManager.SharedTextures.cs index 3a121c4c5..71f300479 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.SharedTextures.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.SharedTextures.cs @@ -50,7 +50,7 @@ internal sealed partial class TextureManager this.textureManager = textureManager; this.textureManager.framework.Update += this.FrameworkOnUpdate; } - + /// Gets all the loaded textures from game resources. public ICollection ForDebugGamePathTextures => this.gameDict.Values; diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs b/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs index 54e30dab0..b35864a17 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs @@ -3,8 +3,6 @@ using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; -using System.Numerics; -using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; @@ -43,13 +41,42 @@ internal sealed partial class TextureManager CancellationToken cancellationToken = default) { using var wrapDispose = leaveWrapOpen ? null : wrap; + var texDesc = default(D3D11_TEXTURE2D_DESC); + + unsafe + { + using var texSrv = default(ComPtr); + using var context = default(ComPtr); + using var tex2D = default(ComPtr); + fixed (Guid* piid = &IID.IID_ID3D11ShaderResourceView) + ((IUnknown*)wrap.ImGuiHandle)->QueryInterface(piid, (void**)texSrv.GetAddressOf()).ThrowOnError(); + + this.Device.Get()->GetImmediateContext(context.GetAddressOf()); + + using (var texRes = default(ComPtr)) + { + texSrv.Get()->GetResource(texRes.GetAddressOf()); + + using var tex2DTemp = default(ComPtr); + texRes.As(&tex2DTemp).ThrowOnError(); + tex2D.Swap(&tex2DTemp); + } + + tex2D.Get()->GetDesc(&texDesc); + } + + var dxgiFormat = texDesc.Format; + if (!WicManager.GetCorrespondingWicPixelFormat(dxgiFormat, out _, out _)) + dxgiFormat = DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM; + using var istream = ManagedIStream.Create(stream, leaveStreamOpen); var (specs, bytes) = await this.GetRawDataFromExistingTextureAsync( wrap, - Vector2.Zero, - Vector2.One, - DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM, + new() + { + Format = dxgiFormat, + }, true, cancellationToken).ConfigureAwait(false); @@ -80,9 +107,9 @@ internal sealed partial class TextureManager containerGuid, File.Create(pathTemp), props, - true, - false, - cancellationToken); + leaveWrapOpen: true, + leaveStreamOpen: false, + cancellationToken: cancellationToken); } catch (Exception e) { @@ -206,6 +233,7 @@ internal sealed partial class TextureManager { private readonly TextureManager textureManager; private ComPtr wicFactory; + private ComPtr wicFactory2; /// Initializes a new instance of the class. /// An instance of . @@ -216,13 +244,27 @@ internal sealed partial class TextureManager { fixed (Guid* pclsidWicImagingFactory = &CLSID.CLSID_WICImagingFactory) fixed (Guid* piidWicImagingFactory = &IID.IID_IWICImagingFactory) + fixed (Guid* pclsidWicImagingFactory2 = &CLSID.CLSID_WICImagingFactory2) + fixed (Guid* piidWicImagingFactory2 = &IID.IID_IWICImagingFactory2) { - CoCreateInstance( - pclsidWicImagingFactory, - null, - (uint)CLSCTX.CLSCTX_INPROC_SERVER, - piidWicImagingFactory, - (void**)this.wicFactory.GetAddressOf()).ThrowOnError(); + if (CoCreateInstance( + pclsidWicImagingFactory2, + null, + (uint)CLSCTX.CLSCTX_INPROC_SERVER, + piidWicImagingFactory2, + (void**)this.wicFactory2.GetAddressOf()).SUCCEEDED) + { + this.wicFactory2.As(ref this.wicFactory).ThrowOnError(); + } + else + { + CoCreateInstance( + pclsidWicImagingFactory, + null, + (uint)CLSCTX.CLSCTX_INPROC_SERVER, + piidWicImagingFactory, + (void**)this.wicFactory.GetAddressOf()).ThrowOnError(); + } } } } @@ -232,6 +274,76 @@ internal sealed partial class TextureManager /// ~WicManager() => this.ReleaseUnmanagedResource(); + /// + /// Gets the corresponding from a containing a WIC pixel format. + /// + /// The WIC pixel format. + /// The corresponding , or if + /// unavailable. + public static DXGI_FORMAT GetCorrespondingDxgiFormat(Guid fmt) => 0 switch + { + // See https://github.com/microsoft/DirectXTex/wiki/WIC-I-O-Functions#savetowicmemory-savetowicfile + _ when fmt == GUID.GUID_WICPixelFormat128bppRGBAFloat => DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_FLOAT, + _ when fmt == GUID.GUID_WICPixelFormat64bppRGBAHalf => DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_FLOAT, + _ when fmt == GUID.GUID_WICPixelFormat64bppRGBA => DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_UNORM, + _ when fmt == GUID.GUID_WICPixelFormat32bppRGBA1010102XR => DXGI_FORMAT + .DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM, + _ when fmt == GUID.GUID_WICPixelFormat32bppRGBA1010102 => DXGI_FORMAT.DXGI_FORMAT_R10G10B10A2_UNORM, + _ when fmt == GUID.GUID_WICPixelFormat16bppBGRA5551 => DXGI_FORMAT.DXGI_FORMAT_B5G5R5A1_UNORM, + _ when fmt == GUID.GUID_WICPixelFormat16bppBGR565 => DXGI_FORMAT.DXGI_FORMAT_B5G6R5_UNORM, + _ when fmt == GUID.GUID_WICPixelFormat32bppGrayFloat => DXGI_FORMAT.DXGI_FORMAT_R32_FLOAT, + _ when fmt == GUID.GUID_WICPixelFormat16bppGrayHalf => DXGI_FORMAT.DXGI_FORMAT_R16_FLOAT, + _ when fmt == GUID.GUID_WICPixelFormat16bppGray => DXGI_FORMAT.DXGI_FORMAT_R16_UNORM, + _ when fmt == GUID.GUID_WICPixelFormat8bppGray => DXGI_FORMAT.DXGI_FORMAT_R8_UNORM, + _ when fmt == GUID.GUID_WICPixelFormat8bppAlpha => DXGI_FORMAT.DXGI_FORMAT_A8_UNORM, + _ when fmt == GUID.GUID_WICPixelFormat32bppRGBA => DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM, + _ when fmt == GUID.GUID_WICPixelFormat32bppBGRA => DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM, + _ when fmt == GUID.GUID_WICPixelFormat32bppBGR => DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM, + _ => DXGI_FORMAT.DXGI_FORMAT_UNKNOWN, + }; + + /// + /// Gets the corresponding containing a WIC pixel format from a . + /// + /// The DXGI pixel format. + /// The corresponding . + /// Whether the image is in SRGB. + /// true if a corresponding pixel format exists. + public static bool GetCorrespondingWicPixelFormat( + DXGI_FORMAT dxgiPixelFormat, + out Guid wicPixelFormat, + out bool srgb) + { + wicPixelFormat = dxgiPixelFormat switch + { + // See https://github.com/microsoft/DirectXTex/wiki/WIC-I-O-Functions#savetowicmemory-savetowicfile + DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_FLOAT => GUID.GUID_WICPixelFormat128bppRGBAFloat, + DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_FLOAT => GUID.GUID_WICPixelFormat64bppRGBAHalf, + DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_UNORM => GUID.GUID_WICPixelFormat64bppRGBA, + DXGI_FORMAT.DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM => GUID.GUID_WICPixelFormat32bppRGBA1010102XR, + DXGI_FORMAT.DXGI_FORMAT_R10G10B10A2_UNORM => GUID.GUID_WICPixelFormat32bppRGBA1010102, + DXGI_FORMAT.DXGI_FORMAT_B5G5R5A1_UNORM => GUID.GUID_WICPixelFormat16bppBGRA5551, + DXGI_FORMAT.DXGI_FORMAT_B5G6R5_UNORM => GUID.GUID_WICPixelFormat16bppBGR565, + DXGI_FORMAT.DXGI_FORMAT_R32_FLOAT => GUID.GUID_WICPixelFormat32bppGrayFloat, + DXGI_FORMAT.DXGI_FORMAT_R16_FLOAT => GUID.GUID_WICPixelFormat16bppGrayHalf, + DXGI_FORMAT.DXGI_FORMAT_R16_UNORM => GUID.GUID_WICPixelFormat16bppGray, + DXGI_FORMAT.DXGI_FORMAT_R8_UNORM => GUID.GUID_WICPixelFormat8bppGray, + DXGI_FORMAT.DXGI_FORMAT_A8_UNORM => GUID.GUID_WICPixelFormat8bppAlpha, + DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM => GUID.GUID_WICPixelFormat32bppRGBA, + DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM_SRGB => GUID.GUID_WICPixelFormat32bppRGBA, + DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM => GUID.GUID_WICPixelFormat32bppBGRA, + DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM_SRGB => GUID.GUID_WICPixelFormat32bppBGRA, + DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM => GUID.GUID_WICPixelFormat32bppBGR, + DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM_SRGB => GUID.GUID_WICPixelFormat32bppBGR, + _ => Guid.Empty, + }; + srgb = dxgiPixelFormat + is DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM_SRGB + or DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM_SRGB + or DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM_SRGB; + return wicPixelFormat != Guid.Empty; + } + /// public void Dispose() { @@ -349,7 +461,7 @@ internal sealed partial class TextureManager bitmapLock.Get()->GetDataPointer(&cbBufferSize, &pbData).ThrowOnError(); bitmapSource.Get()->CopyPixels(null, stride, cbBufferSize, pbData).ThrowOnError(); return this.textureManager.NoThrottleCreateFromRaw( - new(rcLock.Width, rcLock.Height, (int)stride, (int)dxgiFormat), + new(rcLock.Width, rcLock.Height, (int)dxgiFormat, (int)stride), new(pbData, (int)cbBufferSize)); } @@ -382,15 +494,13 @@ internal sealed partial class TextureManager /// The cancellation token. public unsafe void SaveToStreamUsingWic( RawImageSpecification specs, - byte[] bytes, + ReadOnlySpan bytes, Guid containerFormat, ComPtr stream, IReadOnlyDictionary? props = null, CancellationToken cancellationToken = default) { - var outPixelFormat = GUID.GUID_WICPixelFormat32bppBGRA; - var inPixelFormat = GetCorrespondingWicPixelFormat((DXGI_FORMAT)specs.DxgiFormat); - if (inPixelFormat == Guid.Empty) + if (!GetCorrespondingWicPixelFormat((DXGI_FORMAT)specs.DxgiFormat, out var inPixelFormat, out var srgb)) throw new NotSupportedException("DXGI_FORMAT from specs is not supported by WIC."); using var encoder = default(ComPtr); @@ -398,6 +508,24 @@ internal sealed partial class TextureManager this.wicFactory.Get()->CreateEncoder(&containerFormat, null, encoder.GetAddressOf()).ThrowOnError(); cancellationToken.ThrowIfCancellationRequested(); + // See: DirectXTK/Src/ScreenGrab.cpp + var outPixelFormat = (DXGI_FORMAT)specs.DxgiFormat switch + { + DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_FLOAT => GUID.GUID_WICPixelFormat128bppRGBAFloat, + DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_FLOAT when !this.wicFactory2.IsEmpty() => + GUID.GUID_WICPixelFormat128bppRGBAFloat, + DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_FLOAT => GUID.GUID_WICPixelFormat32bppBGRA, + DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_UNORM => GUID.GUID_WICPixelFormat64bppBGRA, + DXGI_FORMAT.DXGI_FORMAT_B5G5R5A1_UNORM => GUID.GUID_WICPixelFormat16bppBGRA5551, + DXGI_FORMAT.DXGI_FORMAT_B5G6R5_UNORM => GUID.GUID_WICPixelFormat16bppBGR565, + DXGI_FORMAT.DXGI_FORMAT_R32_FLOAT => GUID.GUID_WICPixelFormat8bppGray, + DXGI_FORMAT.DXGI_FORMAT_R16_FLOAT => GUID.GUID_WICPixelFormat8bppGray, + DXGI_FORMAT.DXGI_FORMAT_R16_UNORM => GUID.GUID_WICPixelFormat8bppGray, + DXGI_FORMAT.DXGI_FORMAT_R8_UNORM => GUID.GUID_WICPixelFormat8bppGray, + DXGI_FORMAT.DXGI_FORMAT_A8_UNORM => GUID.GUID_WICPixelFormat8bppGray, + _ => GUID.GUID_WICPixelFormat24bppBGR, + }; + encoder.Get()->Initialize(stream, WICBitmapEncoderCacheOption.WICBitmapEncoderNoCache) .ThrowOnError(); cancellationToken.ThrowIfCancellationRequested(); @@ -406,35 +534,14 @@ internal sealed partial class TextureManager encoder.Get()->CreateNewFrame(encoderFrame.GetAddressOf(), propertyBag.GetAddressOf()).ThrowOnError(); cancellationToken.ThrowIfCancellationRequested(); + // Opt-in to the WIC2 support for writing 32-bit Windows BMP files with an alpha channel + if (containerFormat == GUID.GUID_ContainerFormatBmp && !this.wicFactory2.IsEmpty()) + propertyBag.Get()->Write("EnableV5Header32bppBGRA", true).ThrowOnError(); + if (props is not null) { - var nprop = 0u; - propertyBag.Get()->CountProperties(&nprop).ThrowOnError(); - for (var i = 0u; i < nprop; i++) - { - var pbag2 = default(PROPBAG2); - var npropread = 0u; - propertyBag.Get()->GetPropertyInfo(i, 1, &pbag2, &npropread).ThrowOnError(); - if (npropread == 0) - continue; - try - { - var propName = new string((char*)pbag2.pstrName); - if (props.TryGetValue(propName, out var untypedValue)) - { - VARIANT val; - // Marshal calls VariantInit. - Marshal.GetNativeVariantForObject(untypedValue, (nint)(&val)); - VariantChangeType(&val, &val, 0, pbag2.vt).ThrowOnError(); - propertyBag.Get()->Write(1, &pbag2, &val).ThrowOnError(); - VariantClear(&val); - } - } - finally - { - CoTaskMemFree(pbag2.pstrName); - } - } + foreach (var (name, untypedValue) in props) + propertyBag.Get()->Write(name, untypedValue).ThrowOnError(); } encoderFrame.Get()->Initialize(propertyBag).ThrowOnError(); @@ -442,6 +549,34 @@ internal sealed partial class TextureManager encoderFrame.Get()->SetPixelFormat(&outPixelFormat).ThrowOnError(); encoderFrame.Get()->SetSize(checked((uint)specs.Width), checked((uint)specs.Height)).ThrowOnError(); + using (var metaWriter = default(ComPtr)) + { + if (encoderFrame.Get()->GetMetadataQueryWriter(metaWriter.GetAddressOf()).SUCCEEDED) + { + if (containerFormat == GUID.GUID_ContainerFormatPng) + { + // Set sRGB chunk + if (srgb) + { + _ = metaWriter.Get()->SetMetadataByName("/sRGB/RenderingIntent", (byte)0); + } + else + { + // add gAMA chunk with gamma 1.0 + // gama value * 100,000 -- i.e. gamma 1.0 + _ = metaWriter.Get()->SetMetadataByName("/sRGB/RenderingIntent", 100000U); + + // remove sRGB chunk which is added by default. + _ = metaWriter.Get()->RemoveMetadataByName("/sRGB/RenderingIntent"); + } + } + else + { + // Set EXIF Colorspace of sRGB + _ = metaWriter.Get()->SetMetadataByName("System.Image.ColorSpace", (ushort)0); + } + } + } using var tempBitmap = default(ComPtr); fixed (byte* pBytes = bytes) @@ -480,61 +615,11 @@ internal sealed partial class TextureManager encoder.Get()->Commit().ThrowOnError(); } - /// - /// Gets the corresponding from a containing a WIC pixel format. - /// - /// The WIC pixel format. - /// The corresponding , or if - /// unavailable. - private static DXGI_FORMAT GetCorrespondingDxgiFormat(Guid fmt) => 0 switch + private void ReleaseUnmanagedResource() { - // See https://github.com/microsoft/DirectXTex/wiki/WIC-I-O-Functions#savetowicmemory-savetowicfile - _ when fmt == GUID.GUID_WICPixelFormat128bppRGBAFloat => DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_FLOAT, - _ when fmt == GUID.GUID_WICPixelFormat64bppRGBAHalf => DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_FLOAT, - _ when fmt == GUID.GUID_WICPixelFormat64bppRGBA => DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_UNORM, - _ when fmt == GUID.GUID_WICPixelFormat32bppRGBA1010102XR => DXGI_FORMAT - .DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM, - _ when fmt == GUID.GUID_WICPixelFormat32bppRGBA1010102 => DXGI_FORMAT.DXGI_FORMAT_R10G10B10A2_UNORM, - _ when fmt == GUID.GUID_WICPixelFormat16bppBGRA5551 => DXGI_FORMAT.DXGI_FORMAT_B5G5R5A1_UNORM, - _ when fmt == GUID.GUID_WICPixelFormat16bppBGR565 => DXGI_FORMAT.DXGI_FORMAT_B5G6R5_UNORM, - _ when fmt == GUID.GUID_WICPixelFormat32bppGrayFloat => DXGI_FORMAT.DXGI_FORMAT_R32_FLOAT, - _ when fmt == GUID.GUID_WICPixelFormat16bppGrayHalf => DXGI_FORMAT.DXGI_FORMAT_R16_FLOAT, - _ when fmt == GUID.GUID_WICPixelFormat16bppGray => DXGI_FORMAT.DXGI_FORMAT_R16_UNORM, - _ when fmt == GUID.GUID_WICPixelFormat8bppGray => DXGI_FORMAT.DXGI_FORMAT_R8_UNORM, - _ when fmt == GUID.GUID_WICPixelFormat8bppAlpha => DXGI_FORMAT.DXGI_FORMAT_A8_UNORM, - _ when fmt == GUID.GUID_WICPixelFormat32bppRGBA => DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM, - _ when fmt == GUID.GUID_WICPixelFormat32bppBGRA => DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM, - _ when fmt == GUID.GUID_WICPixelFormat32bppBGR => DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM, - _ => DXGI_FORMAT.DXGI_FORMAT_UNKNOWN, - }; - - /// - /// Gets the corresponding containing a WIC pixel format from a . - /// - /// The DXGI pixel format. - /// The corresponding , or if unavailable. - private static Guid GetCorrespondingWicPixelFormat(DXGI_FORMAT fmt) => fmt switch - { - // See https://github.com/microsoft/DirectXTex/wiki/WIC-I-O-Functions#savetowicmemory-savetowicfile - DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_FLOAT => GUID.GUID_WICPixelFormat128bppRGBAFloat, - DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_FLOAT => GUID.GUID_WICPixelFormat64bppRGBAHalf, - DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_UNORM => GUID.GUID_WICPixelFormat64bppRGBA, - DXGI_FORMAT.DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM => GUID.GUID_WICPixelFormat32bppRGBA1010102XR, - DXGI_FORMAT.DXGI_FORMAT_R10G10B10A2_UNORM => GUID.GUID_WICPixelFormat32bppRGBA1010102, - DXGI_FORMAT.DXGI_FORMAT_B5G5R5A1_UNORM => GUID.GUID_WICPixelFormat16bppBGRA5551, - DXGI_FORMAT.DXGI_FORMAT_B5G6R5_UNORM => GUID.GUID_WICPixelFormat16bppBGR565, - DXGI_FORMAT.DXGI_FORMAT_R32_FLOAT => GUID.GUID_WICPixelFormat32bppGrayFloat, - DXGI_FORMAT.DXGI_FORMAT_R16_FLOAT => GUID.GUID_WICPixelFormat16bppGrayHalf, - DXGI_FORMAT.DXGI_FORMAT_R16_UNORM => GUID.GUID_WICPixelFormat16bppGray, - DXGI_FORMAT.DXGI_FORMAT_R8_UNORM => GUID.GUID_WICPixelFormat8bppGray, - DXGI_FORMAT.DXGI_FORMAT_A8_UNORM => GUID.GUID_WICPixelFormat8bppAlpha, - DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM => GUID.GUID_WICPixelFormat32bppRGBA, - DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM => GUID.GUID_WICPixelFormat32bppBGRA, - DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM => GUID.GUID_WICPixelFormat32bppBGR, - _ => Guid.Empty, - }; - - private void ReleaseUnmanagedResource() => this.wicFactory.Reset(); + this.wicFactory.Reset(); + this.wicFactory2.Reset(); + } private readonly struct ComponentEnumerable : IEnumerable> where T : unmanaged, IWICComponentInfo.Interface diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.cs b/Dalamud/Interface/Textures/Internal/TextureManager.cs index 522167c55..921febe94 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.cs @@ -63,7 +63,7 @@ internal sealed partial class TextureManager : IServiceType, IDisposable, ITextu this.wicManager = new(this); } - /// Gets the D3D11 Device used to create textures. + /// Gets the D3D11 Device used to create textures. Ownership is not transferred. public unsafe ComPtr Device { get @@ -80,15 +80,19 @@ internal sealed partial class TextureManager : IServiceType, IDisposable, ITextu } } + /// Gets a simpler drawer. + public SimpleDrawerImpl SimpleDrawer => + this.simpleDrawer ?? throw new ObjectDisposedException(nameof(TextureManager)); + /// Gets the shared texture manager. public SharedTextureManager Shared => this.sharedTextureManager ?? - throw new ObjectDisposedException(nameof(this.sharedTextureManager)); + throw new ObjectDisposedException(nameof(TextureManager)); /// Gets the WIC manager. public WicManager Wic => this.wicManager ?? - throw new ObjectDisposedException(nameof(this.sharedTextureManager)); + throw new ObjectDisposedException(nameof(TextureManager)); /// public void Dispose() @@ -98,9 +102,7 @@ internal sealed partial class TextureManager : IServiceType, IDisposable, ITextu this.disposing = true; - this.drawsOneSquare?.Dispose(); - this.drawsOneSquare = null; - + Interlocked.Exchange(ref this.simpleDrawer, null)?.Dispose(); Interlocked.Exchange(ref this.sharedTextureManager, null)?.Dispose(); Interlocked.Exchange(ref this.wicManager, null)?.Dispose(); } @@ -201,7 +203,7 @@ internal sealed partial class TextureManager : IServiceType, IDisposable, ITextu D3D11_FORMAT_SUPPORT supported; if (this.Device.Get()->CheckFormatSupport(dxgiFormat, (uint*)&supported).FAILED) return false; - + const D3D11_FORMAT_SUPPORT required = D3D11_FORMAT_SUPPORT.D3D11_FORMAT_SUPPORT_TEXTURE2D; return (supported & required) == required; } @@ -232,7 +234,7 @@ internal sealed partial class TextureManager : IServiceType, IDisposable, ITextu var subrdata = new D3D11_SUBRESOURCE_DATA { pSysMem = dataPtr, SysMemPitch = (uint)specs.Pitch }; device.Get()->CreateTexture2D(&texd, &subrdata, texture.GetAddressOf()).ThrowOnError(); } - + var viewDesc = new D3D11_SHADER_RESOURCE_VIEW_DESC { Format = texd.Format, @@ -264,7 +266,7 @@ internal sealed partial class TextureManager : IServiceType, IDisposable, ITextu } return this.NoThrottleCreateFromRaw( - RawImageSpecification.From(buffer.Width, buffer.Height, dxgiFormat), + new(buffer.Width, buffer.Height, dxgiFormat), buffer.RawData); } diff --git a/Dalamud/Interface/Textures/Internal/ViewportTextureWrap.cs b/Dalamud/Interface/Textures/Internal/ViewportTextureWrap.cs new file mode 100644 index 000000000..8a0a17183 --- /dev/null +++ b/Dalamud/Interface/Textures/Internal/ViewportTextureWrap.cs @@ -0,0 +1,239 @@ +using System.Diagnostics; +using System.Numerics; +using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Game; +using Dalamud.Interface.Internal; +using Dalamud.Utility; + +using ImGuiNET; + +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +using NotSupportedException = System.NotSupportedException; + +namespace Dalamud.Interface.Textures.Internal; + +/// A texture wrap that takes its buffer from the frame buffer (of swap chain). +internal sealed class ViewportTextureWrap : IDalamudTextureWrap, IDeferredDisposable +{ + private readonly uint viewportId; + private readonly bool beforeImGuiRender; + private readonly CancellationToken cancellationToken; + private readonly TaskCompletionSource firstUpdateTaskCompletionSource = new(); + + private D3D11_TEXTURE2D_DESC desc; + private ComPtr tex; + private ComPtr srv; + private ComPtr rtv; + + private bool autoUpdate; + private bool disposed; + + /// Initializes a new instance of the class. + /// The source viewport ID. + /// Capture before calling . + /// If true, automatically update the underlying texture. + /// The cancellation token. + public ViewportTextureWrap( + uint viewportId, + bool beforeImGuiRender, + bool autoUpdate, + CancellationToken cancellationToken) + { + this.viewportId = viewportId; + this.beforeImGuiRender = beforeImGuiRender; + this.autoUpdate = autoUpdate; + this.cancellationToken = cancellationToken; + } + + /// Finalizes an instance of the class. + ~ViewportTextureWrap() => this.Dispose(false); + + /// + public unsafe nint ImGuiHandle => (nint)this.srv.Get(); + + /// + public int Width => (int)this.desc.Width; + + /// + public int Height => (int)this.desc.Height; + + /// Gets the task representing the first call. + public Task FirstUpdateTask => this.firstUpdateTaskCompletionSource.Task; + + /// Updates the texture from the source viewport. + public unsafe void Update() + { + if (this.cancellationToken.IsCancellationRequested || this.disposed) + { + this.firstUpdateTaskCompletionSource.TrySetCanceled(); + return; + } + + try + { + ThreadSafety.AssertMainThread(); + + using var backBuffer = GetImGuiViewportBackBuffer(this.viewportId); + D3D11_TEXTURE2D_DESC newDesc; + backBuffer.Get()->GetDesc(&newDesc); + + if (newDesc.SampleDesc.Count > 1) + throw new NotSupportedException("Multisampling is not expected"); + + using var device = default(ComPtr); + backBuffer.Get()->GetDevice(device.GetAddressOf()); + + using var context = default(ComPtr); + device.Get()->GetImmediateContext(context.GetAddressOf()); + + if (this.desc.Width != newDesc.Width + || this.desc.Height != newDesc.Height + || this.desc.Format != newDesc.Format) + { + var texDesc = new D3D11_TEXTURE2D_DESC + { + Width = newDesc.Width, + Height = newDesc.Height, + MipLevels = 1, + ArraySize = 1, + Format = newDesc.Format, + SampleDesc = new(1, 0), + Usage = D3D11_USAGE.D3D11_USAGE_DEFAULT, + BindFlags = (uint)(D3D11_BIND_FLAG.D3D11_BIND_SHADER_RESOURCE | + D3D11_BIND_FLAG.D3D11_BIND_RENDER_TARGET), + CPUAccessFlags = 0u, + MiscFlags = 0u, + }; + + using var texTemp = default(ComPtr); + device.Get()->CreateTexture2D(&texDesc, null, texTemp.GetAddressOf()).ThrowOnError(); + + var rtvDesc = new D3D11_RENDER_TARGET_VIEW_DESC( + texTemp, + D3D11_RTV_DIMENSION.D3D11_RTV_DIMENSION_TEXTURE2D); + using var rtvTemp = default(ComPtr); + device.Get()->CreateRenderTargetView( + (ID3D11Resource*)texTemp.Get(), + &rtvDesc, + rtvTemp.GetAddressOf()).ThrowOnError(); + + var srvDesc = new D3D11_SHADER_RESOURCE_VIEW_DESC( + texTemp, + D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE2D); + using var srvTemp = default(ComPtr); + device.Get()->CreateShaderResourceView( + (ID3D11Resource*)texTemp.Get(), + &srvDesc, + srvTemp.GetAddressOf()) + .ThrowOnError(); + + this.desc = newDesc; + srvTemp.Swap(ref this.srv); + rtvTemp.Swap(ref this.rtv); + texTemp.Swap(ref this.tex); + } + + context.Get()->CopyResource((ID3D11Resource*)this.tex.Get(), (ID3D11Resource*)backBuffer.Get()); + var rtvLocal = this.rtv.Get(); + context.Get()->OMSetRenderTargets(1u, &rtvLocal, null); + Service.Get().SimpleDrawer.StripAlpha(context.Get()); + + var dummy = default(ID3D11RenderTargetView*); + context.Get()->OMSetRenderTargets(1u, &dummy, null); + + this.firstUpdateTaskCompletionSource.TrySetResult(this); + } + catch (Exception e) + { + this.firstUpdateTaskCompletionSource.TrySetException(e); + } + + if (this.autoUpdate) + { + Service.Get().RunOnTick( + () => + { + if (this.beforeImGuiRender) + Service.Get().RunBeforeImGuiRender(this.Update); + else + Service.Get().RunAfterImGuiRender(this.Update); + }, + cancellationToken: this.cancellationToken); + } + } + + /// Queue the texture to be disposed once the frame ends. + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// Actually dispose the wrapped texture. + void IDeferredDisposable.RealDispose() + { + _ = this.FirstUpdateTask.Exception; + this.tex.Reset(); + this.srv.Reset(); + } + + private static unsafe ComPtr GetImGuiViewportBackBuffer(uint viewportId) + { + ThreadSafety.AssertMainThread(); + var viewports = ImGui.GetPlatformIO().Viewports; + var viewportIndex = 0; + for (; viewportIndex < viewports.Size; viewportIndex++) + { + if (viewports[viewportIndex].ID == viewportId) + break; + } + + if (viewportIndex >= viewports.Size) + { + throw new ArgumentOutOfRangeException( + nameof(viewportId), + viewportId, + "Could not find a viewport with the given ID."); + } + + var texture = default(ComPtr); + + Debug.Assert(viewports[0].ID == ImGui.GetMainViewport().ID, "ImGui has changed"); + if (viewportId == viewports[0].ID) + { + var device = FFXIVClientStructs.FFXIV.Client.Graphics.Kernel.Device.Instance(); + fixed (Guid* piid = &IID.IID_ID3D11Texture2D) + { + ((IDXGISwapChain*)device->SwapChain->DXGISwapChain)->GetBuffer(0, piid, (void**)texture.GetAddressOf()) + .ThrowOnError(); + } + } + else + { + // See: ImGui_Impl_DX11.ImGuiViewportDataDx11 + var rud = (nint*)viewports[viewportIndex].RendererUserData; + if (rud == null || rud[0] == nint.Zero || rud[1] == nint.Zero) + throw new InvalidOperationException(); + + using var resource = default(ComPtr); + ((ID3D11RenderTargetView*)rud[viewportIndex])->GetResource(resource.GetAddressOf()); + resource.As(&texture).ThrowOnError(); + } + + return texture; + } + + private void Dispose(bool disposing) + { + this.disposed = true; + this.autoUpdate = false; + if (disposing) + Service.GetNullable()?.EnqueueDeferredDispose(this); + else + ((IDeferredDisposable)this).RealDispose(); + } +} diff --git a/Dalamud/Plugin/Services/ITextureProvider.cs b/Dalamud/Plugin/Services/ITextureProvider.cs index 854579bcd..79b2a7ad3 100644 --- a/Dalamud/Plugin/Services/ITextureProvider.cs +++ b/Dalamud/Plugin/Services/ITextureProvider.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; -using System.Numerics; using System.Reflection; using System.Threading; using System.Threading.Tasks; @@ -34,32 +33,47 @@ public partial interface ITextureProvider /// /// The source texture wrap. The passed value may be disposed once this function returns, /// without having to wait for the completion of the returned . - /// The left top coordinates relative to the size of the source texture. - /// The right bottom coordinates relative to the size of the source texture. - /// The desired target format. Use 0 to use the source format. + /// The texture modification arguments. /// Whether to leave non-disposed when the returned /// completes. /// The cancellation token. /// A containing the copied texture on success. Dispose after use. - /// - /// Coordinates in and should be in range between 0 and 1. - /// - /// Supported values for may not necessarily match - /// . - /// + /// This function may throw an exception. Task CreateFromExistingTextureAsync( IDalamudTextureWrap wrap, - Vector2 uv0, - Vector2 uv1, - int dxgiFormat = 0, + ExistingTextureModificationArgs args = default, bool leaveWrapOpen = false, CancellationToken cancellationToken = default); + /// Creates a texture from the game screen, before rendering Dalamud. + /// If true, automatically update the underlying texture. + /// The cancellation token. + /// A containing the copied texture on success. Dispose after use. + /// This function may throw an exception. + Task CreateFromGameScreen( + bool autoUpdate = false, + CancellationToken cancellationToken = default); + + /// Creates a texture from the game screen, before rendering Dalamud. + /// The viewport ID. + /// If true, automatically update the underlying texture. + /// The cancellation token. + /// A containing the copied texture on success. Dispose after use. + /// + /// Use ImGui.GetMainViewport().ID to capture the game screen with Dalamud rendered. + /// This function may throw an exception. + /// + Task CreateFromImGuiViewport( + uint viewportId, + bool autoUpdate = false, + CancellationToken cancellationToken = default); + /// Gets a texture from the given bytes, trying to interpret it as a .tex file or other well-known image /// files, such as .png. /// The bytes to load. /// The cancellation token. /// A containing the loaded texture on success. Dispose after use. + /// This function may throw an exception. Task CreateFromImageAsync( ReadOnlyMemory bytes, CancellationToken cancellationToken = default); @@ -70,8 +84,10 @@ public partial interface ITextureProvider /// Whether to leave the stream open once the task completes, sucessfully or not. /// The cancellation token. /// A containing the loaded texture on success. Dispose after use. - /// will be closed or not only according to ; - /// is irrelevant in closing the stream. + /// + /// will be closed or not only according to ; + /// is irrelevant in closing the stream. + /// This function may throw an exception. Task CreateFromImageAsync( Stream stream, bool leaveOpen = false, @@ -81,6 +97,7 @@ public partial interface ITextureProvider /// The specifications for the raw bitmap. /// The bytes to load. /// The texture loaded from the supplied raw bitmap. Dispose after use. + /// This function may throw an exception. IDalamudTextureWrap CreateFromRaw( RawImageSpecification specs, ReadOnlySpan bytes); @@ -90,6 +107,7 @@ public partial interface ITextureProvider /// The bytes to load. /// The cancellation token. /// A containing the loaded texture on success. Dispose after use. + /// This function may throw an exception. Task CreateFromRawAsync( RawImageSpecification specs, ReadOnlyMemory bytes, @@ -101,8 +119,11 @@ public partial interface ITextureProvider /// Whether to leave the stream open once the task completes, sucessfully or not. /// The cancellation token. /// A containing the loaded texture on success. Dispose after use. - /// will be closed or not only according to ; - /// is irrelevant in closing the stream. + /// + /// will be closed or not only according to ; + /// is irrelevant in closing the stream. + /// This function may throw an exception. + /// Task CreateFromRawAsync( RawImageSpecification specs, Stream stream, @@ -115,12 +136,14 @@ public partial interface ITextureProvider /// /// The texture to obtain a handle to. /// A texture wrap that can be used to render the texture. Dispose after use. + /// This function may throw an exception. IDalamudTextureWrap CreateFromTexFile(TexFile file); /// Get a texture handle for the specified Lumina . /// The texture to obtain a handle to. /// The cancellation token. /// A texture wrap that can be used to render the texture. Dispose after use. + /// This function may throw an exception. Task CreateFromTexFileAsync( TexFile file, CancellationToken cancellationToken = default); @@ -128,13 +151,14 @@ public partial interface ITextureProvider /// Gets the supported bitmap decoders. /// The supported bitmap decoders. /// - /// The following functions support the files of the container types pointed by yielded values. + /// The following functions support the files of the container types pointed by yielded values. ///
    ///
  • ///
  • ///
  • ///
  • ///
+ /// This function may throw an exception. ///
IEnumerable GetSupportedImageDecoderInfos(); @@ -145,32 +169,39 @@ public partial interface ITextureProvider ///
    ///
  • ///
+ /// This function may throw an exception. /// IEnumerable GetSupportedImageEncoderInfos(); /// Gets a shared texture corresponding to the given game resource icon specifier. /// A game icon specifier. /// The shared texture that you may use to obtain the loaded texture wrap and load states. - /// This function is under the effect of . + /// + /// This function is under the effect of . + /// This function does not throw exceptions. /// ISharedImmediateTexture GetFromGameIcon(in GameIconLookup lookup); /// Gets a shared texture corresponding to the given path to a game resource. /// A path to a game resource. /// The shared texture that you may use to obtain the loaded texture wrap and load states. - /// This function is under the effect of . + /// + /// This function is under the effect of . + /// This function does not throw exceptions. /// ISharedImmediateTexture GetFromGame(string path); /// Gets a shared texture corresponding to the given file on the filesystem. /// A path to a file on the filesystem. /// The shared texture that you may use to obtain the loaded texture wrap and load states. + /// This function does not throw exceptions. ISharedImmediateTexture GetFromFile(string path); /// Gets a shared texture corresponding to the given file of the assembly manifest resources. /// The assembly containing manifest resources. /// The case-sensitive name of the manifest resource being requested. /// The shared texture that you may use to obtain the loaded texture wrap and load states. + /// This function does not throw exceptions. ISharedImmediateTexture GetFromManifestResource(Assembly assembly, string name); /// Get a path for a specific icon's .tex file. @@ -185,14 +216,12 @@ public partial interface ITextureProvider /// The icon lookup. /// The resolved path. /// true if the corresponding file exists and has been set. + /// This function does not throw exceptions. bool TryGetIconPath(in GameIconLookup lookup, [NotNullWhen(true)] out string? path); /// Gets the raw data of a texture wrap. /// The source texture wrap. - /// The left top coordinates relative to the size of the source texture. - /// The right bottom coordinates relative to the size of the source texture. - /// The desired target format. - /// If 0 (unknown) is passed, then the format will not be converted. + /// The texture modification arguments. /// Whether to leave non-disposed when the returned /// completes. /// The cancellation token. @@ -200,16 +229,11 @@ public partial interface ITextureProvider /// /// The length of the returned RawData may not match /// * . - /// If is , - /// is , and is 0, - /// then the source data will be returned. - /// This function can fail. + /// This function may throw an exception. /// Task<(RawImageSpecification Specification, byte[] RawData)> GetRawDataFromExistingTextureAsync( IDalamudTextureWrap wrap, - Vector2 uv0, - Vector2 uv1, - int dxgiFormat = 0, + ExistingTextureModificationArgs args = default, bool leaveWrapOpen = false, CancellationToken cancellationToken = default); @@ -217,9 +241,7 @@ public partial interface ITextureProvider /// The texture wrap to save. /// The container GUID, obtained from . /// The stream to save to. - /// Properties to pass to the encoder. See - /// Microsoft - /// Learn for available parameters. + /// Properties to pass to the encoder. See remarks for valid values. /// Whether to leave non-disposed when the returned /// completes. /// Whether to leave open when the returned @@ -228,6 +250,15 @@ public partial interface ITextureProvider /// A task representing the save process. /// /// must not be disposed until the task finishes. + /// See the following webpages for the valid values for per + /// . + /// + /// This function may throw an exception. /// Task SaveToStreamAsync( IDalamudTextureWrap wrap, @@ -242,15 +273,22 @@ public partial interface ITextureProvider /// The texture wrap to save. /// The container GUID, obtained from . /// The target file path. The target file will be overwritten if it exist. - /// Properties to pass to the encoder. See - /// Microsoft - /// Learn for available parameters. + /// Properties to pass to the encoder. See remarks for valid values. /// Whether to leave non-disposed when the returned /// completes. /// The cancellation token. /// A task representing the save process. /// /// must not be disposed until the task finishes. + /// See the following webpages for the valid values for per + /// . + /// + /// This function may throw an exception. /// Task SaveToFileAsync( IDalamudTextureWrap wrap, @@ -266,11 +304,13 @@ public partial interface ITextureProvider ///
/// The DXGI format. /// true if supported. + /// This function does not throw exceptions. bool IsDxgiFormatSupported(int dxgiFormat); /// Determines whether the system supports the given DXGI format for use with /// . /// The DXGI format. /// true if supported. + /// This function does not throw exceptions. bool IsDxgiFormatSupportedForCreateFromExistingTextureAsync(int dxgiFormat); } diff --git a/Dalamud/Plugin/Services/RawImageSpecification.cs b/Dalamud/Plugin/Services/RawImageSpecification.cs index 4d0aa2e9e..7c17ee9fb 100644 --- a/Dalamud/Plugin/Services/RawImageSpecification.cs +++ b/Dalamud/Plugin/Services/RawImageSpecification.cs @@ -1,25 +1,53 @@ -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 in bytes. The value may not always exactly match -/// * bytesPerPixelFromDxgiFormat. -/// The format of the image. See DXGI_FORMAT. -[SuppressMessage( - "StyleCop.CSharp.NamingRules", - "SA1313:Parameter names should begin with lower-case letter", - Justification = "no")] -public readonly record struct RawImageSpecification(int Width, int Height, int Pitch, int DxgiFormat) +/// Describes a raw image. +public record struct RawImageSpecification { private const string FormatNotSupportedMessage = $"{nameof(DxgiFormat)} is not supported."; + /// Initializes a new instance of the class. + /// The width of the raw image. + /// The height of the raw image. + /// The DXGI format of the raw image. + /// The pitch of the raw image in bytes. + /// Specify -1 to calculate it from other parameters. + public RawImageSpecification(int width, int height, int dxgiFormat, int pitch = -1) + { + if (pitch < 0) + { + if (!GetFormatInfo((DXGI_FORMAT)dxgiFormat, out var bitsPerPixel, out var isBlockCompression)) + throw new NotSupportedException(FormatNotSupportedMessage); + + pitch = isBlockCompression + ? Math.Max(1, (width + 3) / 4) * 2 * bitsPerPixel + : ((width * bitsPerPixel) + 7) / 8; + } + + this.Width = width; + this.Height = height; + this.Pitch = pitch; + this.DxgiFormat = dxgiFormat; + } + + /// Gets or sets the width of the raw image. + public int Width { get; set; } + + /// Gets or sets the height of the raw image. + public int Height { get; set; } + + /// Gets or sets the pitch of the raw image in bytes. + /// The value may not always exactly match + /// * bytesPerPixelFromDxgiFormat. + /// + public int Pitch { get; set; } + + /// Gets or sets the format of the raw image. + /// See + /// DXGI_FORMAT. + public int DxgiFormat { get; set; } + /// Gets the number of bits per pixel. /// Thrown if is not supported. public int BitsPerPixel => @@ -27,27 +55,6 @@ public readonly record struct RawImageSpecification(int Width, int Height, int P ? bitsPerPixel : throw new NotSupportedException(FormatNotSupportedMessage); - /// - /// 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. - /// Thrown if is not supported. - public static RawImageSpecification From(int width, int height, int format) - { - if (!GetFormatInfo((DXGI_FORMAT)format, out var bitsPerPixel, out var isBlockCompression)) - throw new NotSupportedException(FormatNotSupportedMessage); - - 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. @@ -56,7 +63,7 @@ public readonly record struct RawImageSpecification(int Width, int Height, int P /// 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); + new(width, height, (int)DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM, width * 4); /// /// Creates a new instance of record using the given resolution, @@ -66,7 +73,7 @@ public readonly record struct RawImageSpecification(int Width, int Height, int P /// 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); + new(width, height, (int)DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM, width * 4); /// /// Creates a new instance of record using the given resolution, @@ -76,7 +83,7 @@ public readonly record struct RawImageSpecification(int Width, int Height, int P /// The height. /// The new instance. public static RawImageSpecification A8(int width, int height) => - new(width, height, width, (int)DXGI_FORMAT.DXGI_FORMAT_A8_UNORM); + new(width, height, (int)DXGI_FORMAT.DXGI_FORMAT_A8_UNORM, width); private static bool GetFormatInfo(DXGI_FORMAT format, out int bitsPerPixel, out bool isBlockCompression) { diff --git a/Dalamud/Storage/Assets/DalamudAssetManager.cs b/Dalamud/Storage/Assets/DalamudAssetManager.cs index a9293eb6d..e51788c0b 100644 --- a/Dalamud/Storage/Assets/DalamudAssetManager.cs +++ b/Dalamud/Storage/Assets/DalamudAssetManager.cs @@ -83,6 +83,9 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA /// public IDalamudTextureWrap Empty4X4 => this.GetDalamudTextureWrap(DalamudAsset.Empty4X4); + /// + public IDalamudTextureWrap White4X4 => this.GetDalamudTextureWrap(DalamudAsset.White4X4); + /// public void Dispose() { diff --git a/Dalamud/Storage/Assets/DalamudAssetRawTextureAttribute.cs b/Dalamud/Storage/Assets/DalamudAssetRawTextureAttribute.cs index 99253411b..4fba51a2f 100644 --- a/Dalamud/Storage/Assets/DalamudAssetRawTextureAttribute.cs +++ b/Dalamud/Storage/Assets/DalamudAssetRawTextureAttribute.cs @@ -2,25 +2,21 @@ using SharpDX.DXGI; +using TerraFX.Interop.DirectX; + namespace Dalamud.Storage.Assets; -/// -/// Provide raw texture data directly. -/// +/// Provide raw texture data directly. [AttributeUsage(AttributeTargets.Field)] internal class DalamudAssetRawTextureAttribute : Attribute { - /// - /// Initializes a new instance of the class. - /// + /// Initializes a new instance of the class. /// The width. - /// The pitch. /// The height. /// The format. - public DalamudAssetRawTextureAttribute(int width, int pitch, int height, Format format) - { - this.Specification = new(width, height, pitch, (int)format); - } + /// The pitch. + public DalamudAssetRawTextureAttribute(int width, int height, DXGI_FORMAT format, int pitch) => + this.Specification = new(width, height, (int)format, pitch); /// /// Gets the specification. diff --git a/Dalamud/Storage/Assets/IDalamudAssetManager.cs b/Dalamud/Storage/Assets/IDalamudAssetManager.cs index 643eef18c..f402c9f93 100644 --- a/Dalamud/Storage/Assets/IDalamudAssetManager.cs +++ b/Dalamud/Storage/Assets/IDalamudAssetManager.cs @@ -23,6 +23,11 @@ public interface IDalamudAssetManager /// IDalamudTextureWrap Empty4X4 { get; } + /// + /// Gets the shared texture wrap for . + /// + IDalamudTextureWrap White4X4 { get; } + /// /// Gets whether the stream for the asset is instantly available. /// diff --git a/Dalamud/Utility/TerraFxCom/IPropertyBag2Extensions.cs b/Dalamud/Utility/TerraFxCom/IPropertyBag2Extensions.cs new file mode 100644 index 000000000..cfb11ae41 --- /dev/null +++ b/Dalamud/Utility/TerraFxCom/IPropertyBag2Extensions.cs @@ -0,0 +1,83 @@ +using System.Runtime.InteropServices; + +using TerraFX.Interop.Windows; + +using static TerraFX.Interop.Windows.Windows; + +namespace Dalamud.Utility.TerraFxCom; + +/// Utilities for and its derivatives. +internal static unsafe partial class TerraFxComInterfaceExtensions +{ + /// Calls . + /// The property bag. + /// The name of the item to be interpreted as a VARIANT. + /// The new value, to be interpreted as a . + /// Return value from . + public static HRESULT Write(ref this IPropertyBag2 obj, string name, object? value) + { + VARIANT varValue; + // Marshal calls VariantInit. + Marshal.GetNativeVariantForObject(value, (nint)(&varValue)); + try + { + fixed (char* pName = name) + { + var option = new PROPBAG2 { pstrName = (ushort*)pName }; + return obj.Write(1, &option, &varValue); + } + } + finally + { + VariantClear(&varValue); + } + } + + /// Calls . + /// The object. + /// The name of the metadata. + /// The new value, to be interpreted as a . + /// Return value from . + public static HRESULT SetMetadataByName(ref this IWICMetadataQueryWriter obj, string name, object? value) + { + VARIANT varValue; + // Marshal calls VariantInit. + Marshal.GetNativeVariantForObject(value, (nint)(&varValue)); + try + { + PROPVARIANT propVarValue; + var propVarRes = VariantToPropVariant(&varValue, &propVarValue); + if (propVarRes < 0) + return propVarRes; + + try + { + fixed (char* pName = name) + return obj.SetMetadataByName((ushort*)pName, &propVarValue); + } + finally + { + _ = PropVariantClear(&propVarValue); + } + } + finally + { + _ = VariantClear(&varValue); + } + } + + /// Calls . + /// The object. + /// The name of the metadata. + /// Return value from . + public static HRESULT RemoveMetadataByName(ref this IWICMetadataQueryWriter obj, string name) + { + fixed (char* pName = name) + return obj.RemoveMetadataByName((ushort*)pName); + } + + [LibraryImport("propsys.dll")] + private static partial int VariantToPropVariant( + void* pVarIn, + void* pPropVarOut); +} From e1bdba06deefb1461ff79eb12e844d6fc29ce4e2 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 3 Mar 2024 20:41:41 +0900 Subject: [PATCH 38/57] cleanup --- .../Windows/Data/Widgets/TexWidget.cs | 136 ++++++++++++------ .../ExistingTextureModificationArgs.cs | 38 ++--- .../Textures/ImGuiViewportTextureArgs.cs | 68 +++++++++ .../TextureManager.FromExistingTexture.cs | 52 ++----- .../Textures/Internal/ViewportTextureWrap.cs | 96 +++++++------ Dalamud/Plugin/Services/ITextureProvider.cs | 19 +-- 6 files changed, 245 insertions(+), 164 deletions(-) create mode 100644 Dalamud/Interface/Textures/ImGuiViewportTextureArgs.cs diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs index 769df073b..6de891189 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -53,6 +53,8 @@ internal class TexWidget : IDataWindowWidget private FileDialogManager fileDialogManager = null!; private ExistingTextureModificationArgs existingTextureModificationArgs; + private ImGuiViewportTextureArgs viewportTextureArgs; + private int viewportIndexInt; private string[]? supportedRenderTargetFormatNames; private DXGI_FORMAT[]? supportedRenderTargetFormats; private int renderTargetChoiceInt; @@ -84,6 +86,7 @@ internal class TexWidget : IDataWindowWidget this.inputManifestResourceNameIndex = 0; this.supportedRenderTargetFormats = null; this.supportedRenderTargetFormatNames = null; + this.renderTargetChoiceInt = 0; this.fileDialogManager = new(); this.existingTextureModificationArgs = new() { @@ -92,6 +95,8 @@ internal class TexWidget : IDataWindowWidget NewWidth = 320, NewHeight = 240, }; + this.viewportTextureArgs = default; + this.viewportIndexInt = 0; this.Ready = true; } @@ -135,57 +140,41 @@ internal class TexWidget : IDataWindowWidget ImGui.Dummy(new(ImGui.GetTextLineHeightWithSpacing())); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted("Capture: "); - if (ImGui.Button("Game")) - this.addedTextures.Add(new() { Api10 = this.textureManager.CreateFromGameScreen() }); - - ImGui.SameLine(); - if (ImGui.Button("Game (Auto)")) - this.addedTextures.Add(new() { Api10 = this.textureManager.CreateFromGameScreen(true) }); - - ImGui.SameLine(); - if (ImGui.Button("Main Viewport")) - { - this.addedTextures.Add( - new() { Api10 = this.textureManager.CreateFromImGuiViewport(ImGui.GetMainViewport().ID) }); - } - - ImGui.SameLine(); - if (ImGui.Button("Main Viewport (Auto)")) - { - this.addedTextures.Add( - new() { Api10 = this.textureManager.CreateFromImGuiViewport(ImGui.GetMainViewport().ID, true) }); - } - - if (ImGui.CollapsingHeader(nameof(ITextureProvider.GetFromGameIcon), ImGuiTreeNodeFlags.DefaultOpen)) + if (ImGui.CollapsingHeader(nameof(ITextureProvider.GetFromGameIcon))) { ImGui.PushID(nameof(this.DrawGetFromGameIcon)); this.DrawGetFromGameIcon(); ImGui.PopID(); } - if (ImGui.CollapsingHeader(nameof(ITextureProvider.GetFromGame), ImGuiTreeNodeFlags.DefaultOpen)) + if (ImGui.CollapsingHeader(nameof(ITextureProvider.GetFromGame))) { ImGui.PushID(nameof(this.DrawGetFromGame)); this.DrawGetFromGame(); ImGui.PopID(); } - if (ImGui.CollapsingHeader(nameof(ITextureProvider.GetFromFile), ImGuiTreeNodeFlags.DefaultOpen)) + if (ImGui.CollapsingHeader(nameof(ITextureProvider.GetFromFile))) { ImGui.PushID(nameof(this.DrawGetFromFile)); this.DrawGetFromFile(); ImGui.PopID(); } - if (ImGui.CollapsingHeader(nameof(ITextureProvider.GetFromManifestResource), ImGuiTreeNodeFlags.DefaultOpen)) + if (ImGui.CollapsingHeader(nameof(ITextureProvider.GetFromManifestResource))) { ImGui.PushID(nameof(this.DrawGetFromManifestResource)); this.DrawGetFromManifestResource(); ImGui.PopID(); } + if (ImGui.CollapsingHeader(nameof(ITextureProvider.CreateFromImGuiViewportAsync))) + { + ImGui.PushID(nameof(this.DrawCreateFromImGuiViewportAsync)); + this.DrawCreateFromImGuiViewportAsync(); + ImGui.PopID(); + } + if (ImGui.CollapsingHeader("UV")) { ImGui.PushID(nameof(this.DrawUvInput)); @@ -574,6 +563,57 @@ internal class TexWidget : IDataWindowWidget ImGuiHelpers.ScaledDummy(10); } + private void DrawCreateFromImGuiViewportAsync() + { + var viewports = ImGui.GetPlatformIO().Viewports; + if (ImGui.BeginCombo( + nameof(this.viewportTextureArgs.ViewportId), + $"{this.viewportIndexInt}. {viewports[this.viewportIndexInt].ID:X08}")) + { + for (var i = 0; i < viewports.Size; i++) + { + var sel = this.viewportIndexInt == i; + if (ImGui.Selectable($"#{i}: {viewports[i].ID:X08}", ref sel)) + { + this.viewportIndexInt = i; + ImGui.SetItemDefaultFocus(); + } + } + + ImGui.EndCombo(); + } + + var b = this.viewportTextureArgs.KeepTransparency; + if (ImGui.Checkbox(nameof(this.viewportTextureArgs.KeepTransparency), ref b)) + this.viewportTextureArgs.KeepTransparency = b; + + b = this.viewportTextureArgs.AutoUpdate; + if (ImGui.Checkbox(nameof(this.viewportTextureArgs.AutoUpdate), ref b)) + this.viewportTextureArgs.AutoUpdate = b; + + b = this.viewportTextureArgs.TakeBeforeImGuiRender; + if (ImGui.Checkbox(nameof(this.viewportTextureArgs.TakeBeforeImGuiRender), ref b)) + this.viewportTextureArgs.TakeBeforeImGuiRender = b; + + var vec2 = this.viewportTextureArgs.Uv0; + if (ImGui.InputFloat2(nameof(this.viewportTextureArgs.Uv0), ref vec2)) + this.viewportTextureArgs.Uv0 = vec2; + + vec2 = this.viewportTextureArgs.Uv1; + if (ImGui.InputFloat2(nameof(this.viewportTextureArgs.Uv1), ref vec2)) + this.viewportTextureArgs.Uv1 = vec2; + + if (ImGui.Button("Create") && this.viewportIndexInt >= 0 && this.viewportIndexInt < viewports.Size) + { + this.addedTextures.Add( + new() + { + Api10 = this.textureManager.CreateFromImGuiViewportAsync( + this.viewportTextureArgs with { ViewportId = viewports[this.viewportIndexInt].ID }), + }); + } + } + private void DrawUvInput() { ImGui.InputFloat2("UV0", ref this.inputTexUv0); @@ -586,25 +626,8 @@ internal class TexWidget : IDataWindowWidget private void DrawExistingTextureModificationArgs() { - var vec2 = this.existingTextureModificationArgs.Uv0; - if (ImGui.InputFloat2("UV0", ref vec2)) - this.existingTextureModificationArgs.Uv0 = vec2; - - vec2 = this.existingTextureModificationArgs.Uv1; - if (ImGui.InputFloat2("UV1", ref vec2)) - this.existingTextureModificationArgs.Uv1 = vec2; - - Span wh = stackalloc int[2]; - wh[0] = this.existingTextureModificationArgs.NewWidth; - wh[1] = this.existingTextureModificationArgs.NewHeight; - if (ImGui.InputInt2("New Size", ref wh[0])) - { - this.existingTextureModificationArgs.NewWidth = wh[0]; - this.existingTextureModificationArgs.NewHeight = wh[1]; - } - var b = this.existingTextureModificationArgs.MakeOpaque; - if (ImGui.Checkbox("Make Opaque", ref b)) + if (ImGui.Checkbox(nameof(this.existingTextureModificationArgs.MakeOpaque), ref b)) this.existingTextureModificationArgs.MakeOpaque = b; if (this.supportedRenderTargetFormats is null) @@ -619,11 +642,30 @@ internal class TexWidget : IDataWindowWidget this.supportedRenderTargetFormatNames ??= this.supportedRenderTargetFormats.Select(Enum.GetName).ToArray(); ImGui.Combo( - "Format", + nameof(this.existingTextureModificationArgs.DxgiFormat), ref this.renderTargetChoiceInt, this.supportedRenderTargetFormatNames, this.supportedRenderTargetFormatNames.Length); - + + Span wh = stackalloc int[2]; + wh[0] = this.existingTextureModificationArgs.NewWidth; + wh[1] = this.existingTextureModificationArgs.NewHeight; + if (ImGui.InputInt2( + $"{nameof(this.existingTextureModificationArgs.NewWidth)}/{nameof(this.existingTextureModificationArgs.NewHeight)}", + ref wh[0])) + { + this.existingTextureModificationArgs.NewWidth = wh[0]; + this.existingTextureModificationArgs.NewHeight = wh[1]; + } + + var vec2 = this.existingTextureModificationArgs.Uv0; + if (ImGui.InputFloat2(nameof(this.existingTextureModificationArgs.Uv0), ref vec2)) + this.existingTextureModificationArgs.Uv0 = vec2; + + vec2 = this.existingTextureModificationArgs.Uv1; + if (ImGui.InputFloat2(nameof(this.existingTextureModificationArgs.Uv1), ref vec2)) + this.existingTextureModificationArgs.Uv1 = vec2; + ImGuiHelpers.ScaledDummy(10); } diff --git a/Dalamud/Interface/Textures/ExistingTextureModificationArgs.cs b/Dalamud/Interface/Textures/ExistingTextureModificationArgs.cs index 9bcb9f34d..9c70b52cc 100644 --- a/Dalamud/Interface/Textures/ExistingTextureModificationArgs.cs +++ b/Dalamud/Interface/Textures/ExistingTextureModificationArgs.cs @@ -9,15 +9,18 @@ namespace Dalamud.Interface.Textures; /// Describes how to modify an existing texture. public record struct ExistingTextureModificationArgs() { - /// Gets or sets the left top coordinates relative to the size of the source texture. - /// Coordinates should be in range between 0 and 1. - public Vector2 Uv0 { get; set; } = Vector2.Zero; + /// Gets or sets a value indicating whether to make the texture opaque. + /// If true, then the alpha channel values will be filled with 1.0. + public bool MakeOpaque { get; set; } = false; - /// Gets or sets the right bottom coordinates relative to the size of the source texture. - /// Coordinates should be in range between 0 and 1. - /// If set to , then it will be interpreted as , - /// to accommodate the use of default value of this record struct. - public Vector2 Uv1 { get; set; } = Vector2.One; + /// Gets or sets the new DXGI format. + /// + /// Set to 0 () to use the source pixel format. + /// Supported values can be queried with + /// . This may not necessarily + /// match . + /// + public int DxgiFormat { get; set; } = (int)DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM; /// Gets or sets the new width. /// Set to 0 to automatically calculate according to the original texture size, , and @@ -29,18 +32,15 @@ public record struct ExistingTextureModificationArgs() /// . public int NewHeight { get; set; } - /// Gets or sets a value indicating whether to make the texture opaque. - /// Alpha channel values will be filled with 1.0. - public bool MakeOpaque { get; set; } = false; + /// Gets or sets the left top coordinates relative to the size of the source texture. + /// Coordinates should be in range between 0 and 1. + public Vector2 Uv0 { get; set; } = Vector2.Zero; - /// Gets or sets the new DXGI format. - /// - /// Set to 0 () to use the source pixel format. - /// Supported values can be queried with - /// . This may not necessarily - /// match . - /// - public int DxgiFormat { get; set; } = (int)DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM; + /// Gets or sets the right bottom coordinates relative to the size of the source texture. + /// Coordinates should be in range between 0 and 1. + /// If set to , then it will be interpreted as , + /// to accommodate the use of default value of this record struct. + public Vector2 Uv1 { get; set; } = Vector2.One; /// Gets or sets the format (typed). internal DXGI_FORMAT Format diff --git a/Dalamud/Interface/Textures/ImGuiViewportTextureArgs.cs b/Dalamud/Interface/Textures/ImGuiViewportTextureArgs.cs new file mode 100644 index 000000000..b53bd0f48 --- /dev/null +++ b/Dalamud/Interface/Textures/ImGuiViewportTextureArgs.cs @@ -0,0 +1,68 @@ +using System.Numerics; + +using Dalamud.Interface.Internal; + +using ImGuiNET; + +namespace Dalamud.Interface.Textures; + +/// Describes how to take a texture of an existing ImGui viewport. +public record struct ImGuiViewportTextureArgs() +{ + /// Gets or sets the ImGui Viewport ID to capture. + /// Use from to take the main viewport, + /// where the game renders to. + public uint ViewportId { get; set; } + + /// Gets or sets a value indicating whether to automatically update the texture. + /// Enabling this will also update as needed. + public bool AutoUpdate { get; set; } + + /// Gets or sets a value indicating whether to get the texture before rendering ImGui. + /// It probably makes no sense to enable this unless points to the main viewport. + /// + public bool TakeBeforeImGuiRender { get; set; } + + /// Gets or sets a value indicating whether to keep the transparency. + /// + /// If true, then the alpha channel values will be filled with 1.0. + /// Keep in mind that screen captures generally do not need alpha values. + /// + // Intentionally not "MakeOpaque", to accommodate the use of default value of this record struct. + public bool KeepTransparency { get; set; } = false; + + /// Gets or sets the left top coordinates relative to the size of the source texture. + /// Coordinates should be in range between 0 and 1. + public Vector2 Uv0 { get; set; } = Vector2.Zero; + + /// Gets or sets the right bottom coordinates relative to the size of the source texture. + /// Coordinates should be in range between 0 and 1. + /// If set to , then it will be interpreted as , + /// to accommodate the use of default value of this record struct. + public Vector2 Uv1 { get; set; } = Vector2.One; + + /// Gets the effective value of . + internal Vector2 Uv1Effective => this.Uv1 == Vector2.Zero ? Vector2.One : this.Uv1; + + /// Checks the properties and throws an exception if values are invalid. + internal void ThrowOnInvalidValues() + { + if (this.Uv0.X is < 0 or > 1 or float.NaN) + throw new ArgumentException($"{nameof(this.Uv0)}.X is out of range."); + + if (this.Uv0.Y is < 0 or > 1 or float.NaN) + throw new ArgumentException($"{nameof(this.Uv0)}.Y is out of range."); + + if (this.Uv1Effective.X is < 0 or > 1 or float.NaN) + throw new ArgumentException($"{nameof(this.Uv1)}.X is out of range."); + + if (this.Uv1Effective.Y is < 0 or > 1 or float.NaN) + throw new ArgumentException($"{nameof(this.Uv1)}.Y is out of range."); + + if (this.Uv0.X >= this.Uv1Effective.X || this.Uv0.Y >= this.Uv1Effective.Y) + { + throw new ArgumentException( + $"{nameof(this.Uv0)} must be strictly less than {nameof(this.Uv1)} in a componentwise way."); + } + } +} diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs b/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs index 56b0356e5..067368ce5 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs @@ -5,8 +5,6 @@ using Dalamud.Interface.Internal; using Dalamud.Plugin.Services; using Dalamud.Utility; -using ImGuiNET; - using TerraFX.Interop.DirectX; using TerraFX.Interop.Windows; @@ -85,47 +83,15 @@ internal sealed partial class TextureManager } /// - public Task CreateFromGameScreen( - bool autoUpdate = false, - CancellationToken cancellationToken = default) => - this.interfaceManager.RunBeforeImGuiRender( - () => - { - cancellationToken.ThrowIfCancellationRequested(); - var t = new ViewportTextureWrap(ImGui.GetMainViewport().ID, true, autoUpdate, cancellationToken); - t.Update(); - try - { - return t.FirstUpdateTask.Result; - } - catch - { - t.Dispose(); - throw; - } - }); - - /// - public Task CreateFromImGuiViewport( - uint viewportId, - bool autoUpdate = false, - CancellationToken cancellationToken = default) => - this.interfaceManager.RunBeforeImGuiRender( - () => - { - cancellationToken.ThrowIfCancellationRequested(); - var t = new ViewportTextureWrap(viewportId, false, autoUpdate, cancellationToken); - t.Update(); - try - { - return t.FirstUpdateTask.Result; - } - catch - { - t.Dispose(); - throw; - } - }); + public async Task CreateFromImGuiViewportAsync( + ImGuiViewportTextureArgs args, + CancellationToken cancellationToken = default) + { + // This constructor may throw; keep the function "async", to wrap the exception as a Task. + var t = new ViewportTextureWrap(args, cancellationToken); + t.QueueUpdate(); + return await t.FirstUpdateTask; + } /// public async Task<(RawImageSpecification Specification, byte[] RawData)> GetRawDataFromExistingTextureAsync( diff --git a/Dalamud/Interface/Textures/Internal/ViewportTextureWrap.cs b/Dalamud/Interface/Textures/Internal/ViewportTextureWrap.cs index 8a0a17183..28e8f6e3d 100644 --- a/Dalamud/Interface/Textures/Internal/ViewportTextureWrap.cs +++ b/Dalamud/Interface/Textures/Internal/ViewportTextureWrap.cs @@ -1,5 +1,4 @@ using System.Diagnostics; -using System.Numerics; using System.Threading; using System.Threading.Tasks; @@ -19,33 +18,25 @@ namespace Dalamud.Interface.Textures.Internal; /// A texture wrap that takes its buffer from the frame buffer (of swap chain). internal sealed class ViewportTextureWrap : IDalamudTextureWrap, IDeferredDisposable { - private readonly uint viewportId; - private readonly bool beforeImGuiRender; private readonly CancellationToken cancellationToken; private readonly TaskCompletionSource firstUpdateTaskCompletionSource = new(); + private ImGuiViewportTextureArgs args; private D3D11_TEXTURE2D_DESC desc; private ComPtr tex; private ComPtr srv; private ComPtr rtv; - private bool autoUpdate; private bool disposed; /// Initializes a new instance of the class. - /// The source viewport ID. - /// Capture before calling . - /// If true, automatically update the underlying texture. + /// The arguments for creating a texture. /// The cancellation token. - public ViewportTextureWrap( - uint viewportId, - bool beforeImGuiRender, - bool autoUpdate, - CancellationToken cancellationToken) + public ViewportTextureWrap(ImGuiViewportTextureArgs args, CancellationToken cancellationToken) { - this.viewportId = viewportId; - this.beforeImGuiRender = beforeImGuiRender; - this.autoUpdate = autoUpdate; + args.ThrowOnInvalidValues(); + + this.args = args; this.cancellationToken = cancellationToken; } @@ -77,7 +68,7 @@ internal sealed class ViewportTextureWrap : IDalamudTextureWrap, IDeferredDispos { ThreadSafety.AssertMainThread(); - using var backBuffer = GetImGuiViewportBackBuffer(this.viewportId); + using var backBuffer = GetImGuiViewportBackBuffer(this.args.ViewportId); D3D11_TEXTURE2D_DESC newDesc; backBuffer.Get()->GetDesc(&newDesc); @@ -90,14 +81,24 @@ internal sealed class ViewportTextureWrap : IDalamudTextureWrap, IDeferredDispos using var context = default(ComPtr); device.Get()->GetImmediateContext(context.GetAddressOf()); - if (this.desc.Width != newDesc.Width - || this.desc.Height != newDesc.Height + var copyBox = new D3D11_BOX + { + left = (uint)MathF.Round(newDesc.Width * this.args.Uv0.X), + top = (uint)MathF.Round(newDesc.Height * this.args.Uv0.Y), + right = (uint)MathF.Round(newDesc.Width * this.args.Uv1Effective.X), + bottom = (uint)MathF.Round(newDesc.Height * this.args.Uv1Effective.Y), + front = 0, + back = 1, + }; + + if (this.desc.Width != copyBox.right - copyBox.left + || this.desc.Height != copyBox.bottom - copyBox.top || this.desc.Format != newDesc.Format) { var texDesc = new D3D11_TEXTURE2D_DESC { - Width = newDesc.Width, - Height = newDesc.Height, + Width = copyBox.right - copyBox.left, + Height = copyBox.bottom - copyBox.top, MipLevels = 1, ArraySize = 1, Format = newDesc.Format, @@ -131,19 +132,32 @@ internal sealed class ViewportTextureWrap : IDalamudTextureWrap, IDeferredDispos srvTemp.GetAddressOf()) .ThrowOnError(); - this.desc = newDesc; + this.desc = texDesc; srvTemp.Swap(ref this.srv); rtvTemp.Swap(ref this.rtv); texTemp.Swap(ref this.tex); } - context.Get()->CopyResource((ID3D11Resource*)this.tex.Get(), (ID3D11Resource*)backBuffer.Get()); - var rtvLocal = this.rtv.Get(); - context.Get()->OMSetRenderTargets(1u, &rtvLocal, null); - Service.Get().SimpleDrawer.StripAlpha(context.Get()); + // context.Get()->CopyResource((ID3D11Resource*)this.tex.Get(), (ID3D11Resource*)backBuffer.Get()); + context.Get()->CopySubresourceRegion( + (ID3D11Resource*)this.tex.Get(), + 0, + 0, + 0, + 0, + (ID3D11Resource*)backBuffer.Get(), + 0, + ©Box); - var dummy = default(ID3D11RenderTargetView*); - context.Get()->OMSetRenderTargets(1u, &dummy, null); + if (!this.args.KeepTransparency) + { + var rtvLocal = this.rtv.Get(); + context.Get()->OMSetRenderTargets(1u, &rtvLocal, null); + Service.Get().SimpleDrawer.StripAlpha(context.Get()); + + var dummy = default(ID3D11RenderTargetView*); + context.Get()->OMSetRenderTargets(1u, &dummy, null); + } this.firstUpdateTaskCompletionSource.TrySetResult(this); } @@ -152,20 +166,22 @@ internal sealed class ViewportTextureWrap : IDalamudTextureWrap, IDeferredDispos this.firstUpdateTaskCompletionSource.TrySetException(e); } - if (this.autoUpdate) - { - Service.Get().RunOnTick( - () => - { - if (this.beforeImGuiRender) - Service.Get().RunBeforeImGuiRender(this.Update); - else - Service.Get().RunAfterImGuiRender(this.Update); - }, - cancellationToken: this.cancellationToken); - } + if (this.args.AutoUpdate) + this.QueueUpdate(); } + /// Queues a call to . + public void QueueUpdate() => + Service.Get().RunOnTick( + () => + { + if (this.args.TakeBeforeImGuiRender) + Service.Get().RunBeforeImGuiRender(this.Update); + else + Service.Get().RunAfterImGuiRender(this.Update); + }, + cancellationToken: this.cancellationToken); + /// Queue the texture to be disposed once the frame ends. public void Dispose() { @@ -230,7 +246,7 @@ internal sealed class ViewportTextureWrap : IDalamudTextureWrap, IDeferredDispos private void Dispose(bool disposing) { this.disposed = true; - this.autoUpdate = false; + this.args.AutoUpdate = false; if (disposing) Service.GetNullable()?.EnqueueDeferredDispose(this); else diff --git a/Dalamud/Plugin/Services/ITextureProvider.cs b/Dalamud/Plugin/Services/ITextureProvider.cs index 79b2a7ad3..f007b4649 100644 --- a/Dalamud/Plugin/Services/ITextureProvider.cs +++ b/Dalamud/Plugin/Services/ITextureProvider.cs @@ -45,27 +45,16 @@ public partial interface ITextureProvider bool leaveWrapOpen = false, CancellationToken cancellationToken = default); - /// Creates a texture from the game screen, before rendering Dalamud. - /// If true, automatically update the underlying texture. - /// The cancellation token. - /// A containing the copied texture on success. Dispose after use. - /// This function may throw an exception. - Task CreateFromGameScreen( - bool autoUpdate = false, - CancellationToken cancellationToken = default); - - /// Creates a texture from the game screen, before rendering Dalamud. - /// The viewport ID. - /// If true, automatically update the underlying texture. + /// Creates a texture from an ImGui viewport. + /// The arguments for creating a texture. /// The cancellation token. /// A containing the copied texture on success. Dispose after use. /// /// Use ImGui.GetMainViewport().ID to capture the game screen with Dalamud rendered. /// This function may throw an exception. /// - Task CreateFromImGuiViewport( - uint viewportId, - bool autoUpdate = false, + Task CreateFromImGuiViewportAsync( + ImGuiViewportTextureArgs args, CancellationToken cancellationToken = default); /// Gets a texture from the given bytes, trying to interpret it as a .tex file or other well-known image From 858d27ab8edf0b526fa6951ca7b23e0d308adef0 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 3 Mar 2024 20:44:49 +0900 Subject: [PATCH 39/57] Fix namespace --- .../Internals/FontAtlasFactory.BuildToolkit.cs | 2 +- .../Services => Interface/Textures}/RawImageSpecification.cs | 2 +- Dalamud/Interface/UiBuilder.cs | 1 + Dalamud/Interface/UldWrapper.cs | 2 +- Dalamud/Storage/Assets/DalamudAssetRawTextureAttribute.cs | 4 +--- 5 files changed, 5 insertions(+), 6 deletions(-) rename Dalamud/{Plugin/Services => Interface/Textures}/RawImageSpecification.cs (99%) diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs index 316174c4e..811937949 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs @@ -9,8 +9,8 @@ using Dalamud.Configuration.Internal; using Dalamud.Interface.FontIdentifier; using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal; +using Dalamud.Interface.Textures; using Dalamud.Interface.Utility; -using Dalamud.Plugin.Services; using Dalamud.Storage.Assets; using Dalamud.Utility; diff --git a/Dalamud/Plugin/Services/RawImageSpecification.cs b/Dalamud/Interface/Textures/RawImageSpecification.cs similarity index 99% rename from Dalamud/Plugin/Services/RawImageSpecification.cs rename to Dalamud/Interface/Textures/RawImageSpecification.cs index 7c17ee9fb..083e12bad 100644 --- a/Dalamud/Plugin/Services/RawImageSpecification.cs +++ b/Dalamud/Interface/Textures/RawImageSpecification.cs @@ -1,6 +1,6 @@ using TerraFX.Interop.DirectX; -namespace Dalamud.Plugin.Services; +namespace Dalamud.Interface.Textures; /// Describes a raw image. public record struct RawImageSpecification diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index f28f400c1..8874c85f0 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -14,6 +14,7 @@ using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.ManagedFontAtlas.Internals; +using Dalamud.Interface.Textures; using Dalamud.Interface.Textures.Internal; using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; diff --git a/Dalamud/Interface/UldWrapper.cs b/Dalamud/Interface/UldWrapper.cs index 507730662..f70dd88e4 100644 --- a/Dalamud/Interface/UldWrapper.cs +++ b/Dalamud/Interface/UldWrapper.cs @@ -4,8 +4,8 @@ using System.Linq; using Dalamud.Data; using Dalamud.Interface.Internal; +using Dalamud.Interface.Textures; using Dalamud.Interface.Textures.Internal; -using Dalamud.Plugin.Services; using Dalamud.Utility; using Lumina.Data.Files; using Lumina.Data.Parsing.Uld; diff --git a/Dalamud/Storage/Assets/DalamudAssetRawTextureAttribute.cs b/Dalamud/Storage/Assets/DalamudAssetRawTextureAttribute.cs index 4fba51a2f..b9219afcc 100644 --- a/Dalamud/Storage/Assets/DalamudAssetRawTextureAttribute.cs +++ b/Dalamud/Storage/Assets/DalamudAssetRawTextureAttribute.cs @@ -1,6 +1,4 @@ -using Dalamud.Plugin.Services; - -using SharpDX.DXGI; +using Dalamud.Interface.Textures; using TerraFX.Interop.DirectX; From fa5e708a438fd874dca86e7d4031c405fbb90dcb Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 3 Mar 2024 20:50:58 +0900 Subject: [PATCH 40/57] Cleanup and reformat --- .../Interface/Textures/DalamudTextureWrap.cs | 52 +++++-------------- .../ExistingTextureModificationArgs.cs | 2 +- .../Interface/Textures/IDalamudTextureWrap.cs | 17 ++---- .../Textures/ISharedImmediateTexture.cs | 2 +- .../Textures/ImGuiViewportTextureArgs.cs | 2 +- .../FileSystemSharedImmediateTexture.cs | 2 +- .../GamePathSharedImmediateTexture.cs | 2 +- .../ManifestResourceSharedImmediateTexture.cs | 2 +- .../Textures/Internal/UnknownTextureWrap.cs | 20 ++----- .../Textures/Internal/ViewportTextureWrap.cs | 2 +- .../Textures/RawImageSpecification.cs | 4 +- 11 files changed, 32 insertions(+), 75 deletions(-) diff --git a/Dalamud/Interface/Textures/DalamudTextureWrap.cs b/Dalamud/Interface/Textures/DalamudTextureWrap.cs index 3795abad2..668e5a177 100644 --- a/Dalamud/Interface/Textures/DalamudTextureWrap.cs +++ b/Dalamud/Interface/Textures/DalamudTextureWrap.cs @@ -5,68 +5,42 @@ using ImGuiScene; // ReSharper disable once CheckNamespace namespace Dalamud.Interface.Internal; -/// -/// Safety harness for ImGuiScene textures that will defer destruction until -/// the end of the frame. -/// +/// Safety harness for ImGuiScene textures that will defer destruction until the end of the frame. +[Obsolete($"Use {nameof(IDalamudTextureWrap)}.")] +[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] public class DalamudTextureWrap : IDalamudTextureWrap, IDeferredDisposable { private readonly TextureWrap wrappedWrap; - /// - /// Initializes a new instance of the class. - /// + /// Initializes a new instance of the class. /// The texture wrap to wrap. - internal DalamudTextureWrap(TextureWrap wrappingWrap) - { - this.wrappedWrap = wrappingWrap; - } + internal DalamudTextureWrap(TextureWrap wrappingWrap) => this.wrappedWrap = wrappingWrap; - /// - /// Finalizes an instance of the class. - /// - ~DalamudTextureWrap() - { - this.Dispose(false); - } + /// Finalizes an instance of the class. + ~DalamudTextureWrap() => this.Dispose(false); - /// - /// Gets the ImGui handle of the texture. - /// + /// public IntPtr ImGuiHandle => this.wrappedWrap.ImGuiHandle; - /// - /// Gets the width of the texture. - /// + /// public int Width => this.wrappedWrap.Width; - /// - /// Gets the height of the texture. - /// + /// public int Height => this.wrappedWrap.Height; - /// - /// Queue the texture to be disposed once the frame ends. - /// + /// Queue the texture to be disposed once the frame ends. public void Dispose() { this.Dispose(true); GC.SuppressFinalize(this); } - /// - /// Actually dispose the wrapped texture. - /// - void IDeferredDisposable.RealDispose() - { - this.wrappedWrap.Dispose(); - } + /// Actually dispose the wrapped texture. + void IDeferredDisposable.RealDispose() => this.wrappedWrap.Dispose(); private void Dispose(bool disposing) { if (disposing) - { Service.GetNullable()?.EnqueueDeferredDispose(this); - } } } diff --git a/Dalamud/Interface/Textures/ExistingTextureModificationArgs.cs b/Dalamud/Interface/Textures/ExistingTextureModificationArgs.cs index 9c70b52cc..d4558c7eb 100644 --- a/Dalamud/Interface/Textures/ExistingTextureModificationArgs.cs +++ b/Dalamud/Interface/Textures/ExistingTextureModificationArgs.cs @@ -20,7 +20,7 @@ public record struct ExistingTextureModificationArgs() /// . This may not necessarily /// match . /// - public int DxgiFormat { get; set; } = (int)DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM; + public int DxgiFormat { get; set; } = (int)DXGI_FORMAT.DXGI_FORMAT_UNKNOWN; /// Gets or sets the new width. /// Set to 0 to automatically calculate according to the original texture size, , and diff --git a/Dalamud/Interface/Textures/IDalamudTextureWrap.cs b/Dalamud/Interface/Textures/IDalamudTextureWrap.cs index d2915b5a0..dad453cb9 100644 --- a/Dalamud/Interface/Textures/IDalamudTextureWrap.cs +++ b/Dalamud/Interface/Textures/IDalamudTextureWrap.cs @@ -6,6 +6,7 @@ using TerraFX.Interop.Windows; // ReSharper disable once CheckNamespace namespace Dalamud.Interface.Internal; +// TODO(api10): fix namespace maybe? /// /// Base TextureWrap interface for all Dalamud-owned texture wraps. @@ -13,24 +14,16 @@ namespace Dalamud.Interface.Internal; /// public interface IDalamudTextureWrap : IDisposable { - /// - /// Gets a texture handle suitable for direct use with ImGui functions. - /// + /// Gets a texture handle suitable for direct use with ImGui functions. IntPtr ImGuiHandle { get; } - /// - /// Gets the width of the texture. - /// + /// Gets the width of the texture. int Width { get; } - /// - /// Gets the height of the texture. - /// + /// Gets the height of the texture. int Height { get; } - /// - /// Gets the size vector of the texture using Width, Height. - /// + /// Gets the size vector of the texture using Width, Height. Vector2 Size => new(this.Width, this.Height); /// diff --git a/Dalamud/Interface/Textures/ISharedImmediateTexture.cs b/Dalamud/Interface/Textures/ISharedImmediateTexture.cs index f8c727557..f9683e6c5 100644 --- a/Dalamud/Interface/Textures/ISharedImmediateTexture.cs +++ b/Dalamud/Interface/Textures/ISharedImmediateTexture.cs @@ -42,7 +42,7 @@ public interface ISharedImmediateTexture /// [return: NotNullIfNotNull(nameof(defaultWrap))] IDalamudTextureWrap? GetWrapOrDefault(IDalamudTextureWrap? defaultWrap = null); - + /// Attempts to get the texture for use with the current frame. /// An instance of that is guaranteed to be available for /// the current frame being drawn, or null if texture is not loaded (yet). diff --git a/Dalamud/Interface/Textures/ImGuiViewportTextureArgs.cs b/Dalamud/Interface/Textures/ImGuiViewportTextureArgs.cs index b53bd0f48..8193d65e6 100644 --- a/Dalamud/Interface/Textures/ImGuiViewportTextureArgs.cs +++ b/Dalamud/Interface/Textures/ImGuiViewportTextureArgs.cs @@ -17,7 +17,7 @@ public record struct ImGuiViewportTextureArgs() /// Gets or sets a value indicating whether to automatically update the texture. /// Enabling this will also update as needed. public bool AutoUpdate { get; set; } - + /// Gets or sets a value indicating whether to get the texture before rendering ImGui. /// It probably makes no sense to enable this unless points to the main viewport. /// diff --git a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs index 6cdd0aa25..a98f5c940 100644 --- a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs +++ b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs @@ -43,7 +43,7 @@ internal sealed class FileSystemSharedImmediateTexture : SharedImmediateTexture private async Task CreateTextureAsync(CancellationToken cancellationToken) { - var tm = await Service.GetAsync(); + 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 1dfe96fe2..0af66c457 100644 --- a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs +++ b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs @@ -48,7 +48,7 @@ internal sealed class GamePathSharedImmediateTexture : SharedImmediateTexture private async Task CreateTextureAsync(CancellationToken cancellationToken) { var dm = await Service.GetAsync(); - var tm = await Service.GetAsync(); + var tm = await Service.GetAsync(); var substPath = tm.GetSubstitutedPath(this.path); if (dm.GetFile(substPath) is not { } file) throw new FileNotFoundException(); diff --git a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs index c9bdea067..62f315809 100644 --- a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs +++ b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs @@ -57,7 +57,7 @@ internal sealed class ManifestResourceSharedImmediateTexture : SharedImmediateTe if (stream is null) throw new FileNotFoundException("The resource file could not be found."); - var tm = await Service.GetAsync(); + var tm = await Service.GetAsync(); var ms = new MemoryStream(stream.CanSeek ? (int)stream.Length : 0); await stream.CopyToAsync(ms, cancellationToken); return tm.NoThrottleCreateFromImage(ms.GetBuffer().AsMemory(0, (int)ms.Length), cancellationToken); diff --git a/Dalamud/Interface/Textures/Internal/UnknownTextureWrap.cs b/Dalamud/Interface/Textures/Internal/UnknownTextureWrap.cs index 24e9a8bc1..c159f9b36 100644 --- a/Dalamud/Interface/Textures/Internal/UnknownTextureWrap.cs +++ b/Dalamud/Interface/Textures/Internal/UnknownTextureWrap.cs @@ -7,16 +7,12 @@ using TerraFX.Interop.Windows; namespace Dalamud.Interface.Textures.Internal; -/// -/// A texture wrap that is created by cloning the underlying . -/// +/// A texture wrap that is created from an . internal sealed unsafe class UnknownTextureWrap : IDalamudTextureWrap, IDeferredDisposable { private IntPtr imGuiHandle; - /// - /// Initializes a new instance of the class. - /// + /// Initializes a new instance of the class. /// The pointer to that is suitable for use with /// . /// The width of the texture. @@ -32,9 +28,7 @@ internal sealed unsafe class UnknownTextureWrap : IDalamudTextureWrap, IDeferred unknown->AddRef(); } - /// - /// Finalizes an instance of the class. - /// + /// Finalizes an instance of the class. ~UnknownTextureWrap() => this.Dispose(false); /// @@ -49,18 +43,14 @@ internal sealed unsafe class UnknownTextureWrap : IDalamudTextureWrap, IDeferred /// public int Height { get; } - /// - /// Queue the texture to be disposed once the frame ends. - /// + /// Queue the texture to be disposed once the frame ends. public void Dispose() { this.Dispose(true); GC.SuppressFinalize(this); } - /// - /// Actually dispose the wrapped texture. - /// + /// Actually dispose the wrapped texture. void IDeferredDisposable.RealDispose() { var handle = Interlocked.Exchange(ref this.imGuiHandle, nint.Zero); diff --git a/Dalamud/Interface/Textures/Internal/ViewportTextureWrap.cs b/Dalamud/Interface/Textures/Internal/ViewportTextureWrap.cs index 28e8f6e3d..099361907 100644 --- a/Dalamud/Interface/Textures/Internal/ViewportTextureWrap.cs +++ b/Dalamud/Interface/Textures/Internal/ViewportTextureWrap.cs @@ -89,7 +89,7 @@ internal sealed class ViewportTextureWrap : IDalamudTextureWrap, IDeferredDispos bottom = (uint)MathF.Round(newDesc.Height * this.args.Uv1Effective.Y), front = 0, back = 1, - }; + }; if (this.desc.Width != copyBox.right - copyBox.left || this.desc.Height != copyBox.bottom - copyBox.top diff --git a/Dalamud/Interface/Textures/RawImageSpecification.cs b/Dalamud/Interface/Textures/RawImageSpecification.cs index 083e12bad..2abf3729d 100644 --- a/Dalamud/Interface/Textures/RawImageSpecification.cs +++ b/Dalamud/Interface/Textures/RawImageSpecification.cs @@ -21,8 +21,8 @@ public record struct RawImageSpecification throw new NotSupportedException(FormatNotSupportedMessage); pitch = isBlockCompression - ? Math.Max(1, (width + 3) / 4) * 2 * bitsPerPixel - : ((width * bitsPerPixel) + 7) / 8; + ? Math.Max(1, (width + 3) / 4) * 2 * bitsPerPixel + : ((width * bitsPerPixel) + 7) / 8; } this.Width = width; From 5ad8edbc0454e93e41d48085a227807293cb845c Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 3 Mar 2024 21:00:37 +0900 Subject: [PATCH 41/57] Move save get/save functions to ITextureReadbackProvider --- .../Windows/Data/Widgets/TexWidget.cs | 36 +++--- .../TextureManager.FromExistingTexture.cs | 8 +- .../Textures/Internal/TextureManager.Wic.cs | 4 +- .../Textures/Internal/TextureManager.cs | 3 +- ...tionArgs.cs => TextureModificationArgs.cs} | 8 +- Dalamud/Plugin/Services/ITextureProvider.cs | 92 +--------------- .../Services/ITextureReadbackProvider.cs | 104 ++++++++++++++++++ 7 files changed, 135 insertions(+), 120 deletions(-) rename Dalamud/Interface/Textures/{ExistingTextureModificationArgs.cs => TextureModificationArgs.cs} (92%) create mode 100644 Dalamud/Plugin/Services/ITextureReadbackProvider.cs diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs index 6de891189..092b3f74f 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -51,7 +51,7 @@ internal class TexWidget : IDataWindowWidget private Vector2 inputTexScale = Vector2.Zero; private TextureManager textureManager = null!; private FileDialogManager fileDialogManager = null!; - private ExistingTextureModificationArgs existingTextureModificationArgs; + private TextureModificationArgs textureModificationArgs; private ImGuiViewportTextureArgs viewportTextureArgs; private int viewportIndexInt; @@ -88,7 +88,7 @@ internal class TexWidget : IDataWindowWidget this.supportedRenderTargetFormatNames = null; this.renderTargetChoiceInt = 0; this.fileDialogManager = new(); - this.existingTextureModificationArgs = new() + this.textureModificationArgs = new() { Uv0 = new(0.25f), Uv1 = new(0.75f), @@ -231,7 +231,7 @@ internal class TexWidget : IDataWindowWidget return; var texTask = this.textureManager.CreateFromExistingTextureAsync( source.CreateWrapSharingLowLevelResource(), - this.existingTextureModificationArgs with + this.textureModificationArgs with { Format = supportedFormats[this.renderTargetChoiceInt], }); @@ -626,9 +626,9 @@ internal class TexWidget : IDataWindowWidget private void DrawExistingTextureModificationArgs() { - var b = this.existingTextureModificationArgs.MakeOpaque; - if (ImGui.Checkbox(nameof(this.existingTextureModificationArgs.MakeOpaque), ref b)) - this.existingTextureModificationArgs.MakeOpaque = b; + var b = this.textureModificationArgs.MakeOpaque; + if (ImGui.Checkbox(nameof(this.textureModificationArgs.MakeOpaque), ref b)) + this.textureModificationArgs.MakeOpaque = b; if (this.supportedRenderTargetFormats is null) { @@ -642,29 +642,29 @@ internal class TexWidget : IDataWindowWidget this.supportedRenderTargetFormatNames ??= this.supportedRenderTargetFormats.Select(Enum.GetName).ToArray(); ImGui.Combo( - nameof(this.existingTextureModificationArgs.DxgiFormat), + nameof(this.textureModificationArgs.DxgiFormat), ref this.renderTargetChoiceInt, this.supportedRenderTargetFormatNames, this.supportedRenderTargetFormatNames.Length); Span wh = stackalloc int[2]; - wh[0] = this.existingTextureModificationArgs.NewWidth; - wh[1] = this.existingTextureModificationArgs.NewHeight; + wh[0] = this.textureModificationArgs.NewWidth; + wh[1] = this.textureModificationArgs.NewHeight; if (ImGui.InputInt2( - $"{nameof(this.existingTextureModificationArgs.NewWidth)}/{nameof(this.existingTextureModificationArgs.NewHeight)}", + $"{nameof(this.textureModificationArgs.NewWidth)}/{nameof(this.textureModificationArgs.NewHeight)}", ref wh[0])) { - this.existingTextureModificationArgs.NewWidth = wh[0]; - this.existingTextureModificationArgs.NewHeight = wh[1]; + this.textureModificationArgs.NewWidth = wh[0]; + this.textureModificationArgs.NewHeight = wh[1]; } - var vec2 = this.existingTextureModificationArgs.Uv0; - if (ImGui.InputFloat2(nameof(this.existingTextureModificationArgs.Uv0), ref vec2)) - this.existingTextureModificationArgs.Uv0 = vec2; + var vec2 = this.textureModificationArgs.Uv0; + if (ImGui.InputFloat2(nameof(this.textureModificationArgs.Uv0), ref vec2)) + this.textureModificationArgs.Uv0 = vec2; - vec2 = this.existingTextureModificationArgs.Uv1; - if (ImGui.InputFloat2(nameof(this.existingTextureModificationArgs.Uv1), ref vec2)) - this.existingTextureModificationArgs.Uv1 = vec2; + vec2 = this.textureModificationArgs.Uv1; + if (ImGui.InputFloat2(nameof(this.textureModificationArgs.Uv1), ref vec2)) + this.textureModificationArgs.Uv1 = vec2; ImGuiHelpers.ScaledDummy(10); } diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs b/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs index 067368ce5..2f9874217 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs @@ -45,7 +45,7 @@ internal sealed partial class TextureManager /// public Task CreateFromExistingTextureAsync( IDalamudTextureWrap wrap, - ExistingTextureModificationArgs args = default, + TextureModificationArgs args = default, bool leaveWrapOpen = false, CancellationToken cancellationToken = default) { @@ -94,9 +94,9 @@ internal sealed partial class TextureManager } /// - public async Task<(RawImageSpecification Specification, byte[] RawData)> GetRawDataFromExistingTextureAsync( + public async Task<(RawImageSpecification Specification, byte[] RawData)> GetRawImageAsync( IDalamudTextureWrap wrap, - ExistingTextureModificationArgs args = default, + TextureModificationArgs args = default, bool leaveWrapOpen = false, CancellationToken cancellationToken = default) { @@ -197,7 +197,7 @@ internal sealed partial class TextureManager private async Task> NoThrottleCreateFromExistingTextureAsync( IDalamudTextureWrap wrap, - ExistingTextureModificationArgs args) + TextureModificationArgs args) { args.ThrowOnInvalidValues(); diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs b/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs index b35864a17..3f8d9897c 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs @@ -71,7 +71,7 @@ internal sealed partial class TextureManager using var istream = ManagedIStream.Create(stream, leaveStreamOpen); - var (specs, bytes) = await this.GetRawDataFromExistingTextureAsync( + var (specs, bytes) = await this.GetRawImageAsync( wrap, new() { @@ -164,7 +164,7 @@ internal sealed partial class TextureManager this.Wic.GetSupportedDecoderInfos(); /// - IEnumerable ITextureProvider.GetSupportedImageEncoderInfos() => + IEnumerable ITextureReadbackProvider.GetSupportedImageEncoderInfos() => this.Wic.GetSupportedEncoderInfos(); /// Creates a texture from the given bytes of an image file. Skips the load throttler; intended to be used diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.cs b/Dalamud/Interface/Textures/Internal/TextureManager.cs index 921febe94..07f721587 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.cs @@ -29,7 +29,8 @@ namespace Dalamud.Interface.Textures.Internal; [ResolveVia] [ResolveVia] #pragma warning restore SA1015 -internal sealed partial class TextureManager : IServiceType, IDisposable, ITextureProvider, ITextureSubstitutionProvider +internal sealed partial class TextureManager + : IServiceType, IDisposable, ITextureProvider, ITextureSubstitutionProvider, ITextureReadbackProvider { private static readonly ModuleLog Log = new(nameof(TextureManager)); diff --git a/Dalamud/Interface/Textures/ExistingTextureModificationArgs.cs b/Dalamud/Interface/Textures/TextureModificationArgs.cs similarity index 92% rename from Dalamud/Interface/Textures/ExistingTextureModificationArgs.cs rename to Dalamud/Interface/Textures/TextureModificationArgs.cs index d4558c7eb..fac04189c 100644 --- a/Dalamud/Interface/Textures/ExistingTextureModificationArgs.cs +++ b/Dalamud/Interface/Textures/TextureModificationArgs.cs @@ -6,8 +6,8 @@ using TerraFX.Interop.DirectX; namespace Dalamud.Interface.Textures; -/// Describes how to modify an existing texture. -public record struct ExistingTextureModificationArgs() +/// Describes how to modify a texture. +public record struct TextureModificationArgs() { /// Gets or sets a value indicating whether to make the texture opaque. /// If true, then the alpha channel values will be filled with 1.0. @@ -52,10 +52,10 @@ public record struct ExistingTextureModificationArgs() /// Gets the effective value of . internal Vector2 Uv1Effective => this.Uv1 == Vector2.Zero ? Vector2.One : this.Uv1; - /// Test if this instance of does not instruct to change the + /// Test if this instance of does not instruct to change the /// underlying data of a texture. /// The texture description to test against. - /// true if this instance of does not instruct to + /// true if this instance of does not instruct to /// change the underlying data of a texture. internal bool IsCompleteSourceCopy(in D3D11_TEXTURE2D_DESC desc) => this.Uv0 == Vector2.Zero diff --git a/Dalamud/Plugin/Services/ITextureProvider.cs b/Dalamud/Plugin/Services/ITextureProvider.cs index f007b4649..59c9af2fc 100644 --- a/Dalamud/Plugin/Services/ITextureProvider.cs +++ b/Dalamud/Plugin/Services/ITextureProvider.cs @@ -41,7 +41,7 @@ public partial interface ITextureProvider /// This function may throw an exception. Task CreateFromExistingTextureAsync( IDalamudTextureWrap wrap, - ExistingTextureModificationArgs args = default, + TextureModificationArgs args = default, bool leaveWrapOpen = false, CancellationToken cancellationToken = default); @@ -151,17 +151,6 @@ public partial interface ITextureProvider /// IEnumerable GetSupportedImageDecoderInfos(); - /// Gets the supported bitmap encoders. - /// The supported bitmap encoders. - /// - /// The following function supports the files of the container types pointed by yielded values. - ///
    - ///
  • - ///
- /// This function may throw an exception. - ///
- IEnumerable GetSupportedImageEncoderInfos(); - /// Gets a shared texture corresponding to the given game resource icon specifier. /// A game icon specifier. /// The shared texture that you may use to obtain the loaded texture wrap and load states. @@ -208,85 +197,6 @@ public partial interface ITextureProvider /// This function does not throw exceptions. bool TryGetIconPath(in GameIconLookup lookup, [NotNullWhen(true)] out string? path); - /// Gets the raw data of a texture wrap. - /// The source texture wrap. - /// The texture modification arguments. - /// Whether to leave non-disposed when the returned - /// completes. - /// The cancellation token. - /// The raw data and its specifications. - /// - /// The length of the returned RawData may not match - /// * . - /// This function may throw an exception. - /// - Task<(RawImageSpecification Specification, byte[] RawData)> GetRawDataFromExistingTextureAsync( - IDalamudTextureWrap wrap, - ExistingTextureModificationArgs args = default, - bool leaveWrapOpen = false, - CancellationToken cancellationToken = default); - - /// Saves a texture wrap to a stream in an image file format. - /// The texture wrap to save. - /// The container GUID, obtained from . - /// The stream to save to. - /// Properties to pass to the encoder. See remarks for valid values. - /// Whether to leave non-disposed when the returned - /// completes. - /// Whether to leave open when the returned - /// completes. - /// The cancellation token. - /// A task representing the save process. - /// - /// must not be disposed until the task finishes. - /// See the following webpages for the valid values for per - /// . - /// - /// This function may throw an exception. - /// - Task SaveToStreamAsync( - IDalamudTextureWrap wrap, - Guid containerGuid, - Stream stream, - IReadOnlyDictionary? props = null, - bool leaveWrapOpen = false, - bool leaveStreamOpen = false, - CancellationToken cancellationToken = default); - - /// Saves a texture wrap to a file as an image file. - /// The texture wrap to save. - /// The container GUID, obtained from . - /// The target file path. The target file will be overwritten if it exist. - /// Properties to pass to the encoder. See remarks for valid values. - /// Whether to leave non-disposed when the returned - /// completes. - /// The cancellation token. - /// A task representing the save process. - /// - /// must not be disposed until the task finishes. - /// See the following webpages for the valid values for per - /// . - /// - /// This function may throw an exception. - /// - Task SaveToFileAsync( - IDalamudTextureWrap wrap, - Guid containerGuid, - string path, - IReadOnlyDictionary? props = null, - bool leaveWrapOpen = false, - CancellationToken cancellationToken = default); - /// /// Determines whether the system supports the given DXGI format. /// For use with . diff --git a/Dalamud/Plugin/Services/ITextureReadbackProvider.cs b/Dalamud/Plugin/Services/ITextureReadbackProvider.cs new file mode 100644 index 000000000..fa666c3c2 --- /dev/null +++ b/Dalamud/Plugin/Services/ITextureReadbackProvider.cs @@ -0,0 +1,104 @@ +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Interface.Internal; +using Dalamud.Interface.Textures; + +namespace Dalamud.Plugin.Services; + +/// Service that grants you to read instances of . +public interface ITextureReadbackProvider +{ + /// Gets the raw data of a texture wrap. + /// The source texture wrap. + /// The texture modification arguments. + /// Whether to leave non-disposed when the returned + /// completes. + /// The cancellation token. + /// The raw data and its specifications. + /// + /// The length of the returned RawData may not match + /// * . + /// This function may throw an exception. + /// + Task<(RawImageSpecification Specification, byte[] RawData)> GetRawImageAsync( + IDalamudTextureWrap wrap, + TextureModificationArgs args = default, + bool leaveWrapOpen = false, + CancellationToken cancellationToken = default); + + /// Gets the supported bitmap encoders. + /// The supported bitmap encoders. + /// + /// The following functions support the files of the container types pointed by yielded values. + ///
    + ///
  • + ///
  • + ///
+ /// This function may throw an exception. + ///
+ IEnumerable GetSupportedImageEncoderInfos(); + + /// Saves a texture wrap to a stream in an image file format. + /// The texture wrap to save. + /// The container GUID, obtained from . + /// The stream to save to. + /// Properties to pass to the encoder. See remarks for valid values. + /// Whether to leave non-disposed when the returned + /// completes. + /// Whether to leave open when the returned + /// completes. + /// The cancellation token. + /// A task representing the save process. + /// + /// must not be disposed until the task finishes. + /// See the following webpages for the valid values for per + /// . + /// + /// This function may throw an exception. + /// + Task SaveToStreamAsync( + IDalamudTextureWrap wrap, + Guid containerGuid, + Stream stream, + IReadOnlyDictionary? props = null, + bool leaveWrapOpen = false, + bool leaveStreamOpen = false, + CancellationToken cancellationToken = default); + + /// Saves a texture wrap to a file as an image file. + /// The texture wrap to save. + /// The container GUID, obtained from . + /// The target file path. The target file will be overwritten if it exist. + /// Properties to pass to the encoder. See remarks for valid values. + /// Whether to leave non-disposed when the returned + /// completes. + /// The cancellation token. + /// A task representing the save process. + /// + /// must not be disposed until the task finishes. + /// See the following webpages for the valid values for per + /// . + /// + /// This function may throw an exception. + /// + Task SaveToFileAsync( + IDalamudTextureWrap wrap, + Guid containerGuid, + string path, + IReadOnlyDictionary? props = null, + bool leaveWrapOpen = false, + CancellationToken cancellationToken = default); +} From bb76f0eea4140f1fe34d06dc6469db0c4f509d6c Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 3 Mar 2024 21:03:49 +0900 Subject: [PATCH 42/57] fix --- Dalamud/Interface/Textures/Internal/TextureManager.cs | 1 + Dalamud/Interface/Textures/Internal/ViewportTextureWrap.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.cs b/Dalamud/Interface/Textures/Internal/TextureManager.cs index 07f721587..f4f8e62d9 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.cs @@ -28,6 +28,7 @@ namespace Dalamud.Interface.Textures.Internal; #pragma warning disable SA1015 [ResolveVia] [ResolveVia] +[ResolveVia] #pragma warning restore SA1015 internal sealed partial class TextureManager : IServiceType, IDisposable, ITextureProvider, ITextureSubstitutionProvider, ITextureReadbackProvider diff --git a/Dalamud/Interface/Textures/Internal/ViewportTextureWrap.cs b/Dalamud/Interface/Textures/Internal/ViewportTextureWrap.cs index 099361907..ca914a234 100644 --- a/Dalamud/Interface/Textures/Internal/ViewportTextureWrap.cs +++ b/Dalamud/Interface/Textures/Internal/ViewportTextureWrap.cs @@ -236,7 +236,7 @@ internal sealed class ViewportTextureWrap : IDalamudTextureWrap, IDeferredDispos throw new InvalidOperationException(); using var resource = default(ComPtr); - ((ID3D11RenderTargetView*)rud[viewportIndex])->GetResource(resource.GetAddressOf()); + ((ID3D11RenderTargetView*)rud[1])->GetResource(resource.GetAddressOf()); resource.As(&texture).ThrowOnError(); } From 6c8c42ca059a26a2e62afe8299e449789ce39f56 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 3 Mar 2024 21:46:49 +0900 Subject: [PATCH 43/57] Cleanup --- .../Textures/Internal/TextureManager.Wic.cs | 102 +++++----- .../TerraFxCom/IPropertyBag2Extensions.cs | 83 --------- .../TerraFxComInterfaceExtensions.cs | 175 ++++++++++++++++++ 3 files changed, 225 insertions(+), 135 deletions(-) delete mode 100644 Dalamud/Utility/TerraFxCom/IPropertyBag2Extensions.cs create mode 100644 Dalamud/Utility/TerraFxCom/TerraFxComInterfaceExtensions.cs diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs b/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs index 3f8d9897c..2311923f8 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs @@ -41,31 +41,8 @@ internal sealed partial class TextureManager CancellationToken cancellationToken = default) { using var wrapDispose = leaveWrapOpen ? null : wrap; - var texDesc = default(D3D11_TEXTURE2D_DESC); - unsafe - { - using var texSrv = default(ComPtr); - using var context = default(ComPtr); - using var tex2D = default(ComPtr); - fixed (Guid* piid = &IID.IID_ID3D11ShaderResourceView) - ((IUnknown*)wrap.ImGuiHandle)->QueryInterface(piid, (void**)texSrv.GetAddressOf()).ThrowOnError(); - - this.Device.Get()->GetImmediateContext(context.GetAddressOf()); - - using (var texRes = default(ComPtr)) - { - texSrv.Get()->GetResource(texRes.GetAddressOf()); - - using var tex2DTemp = default(ComPtr); - texRes.As(&tex2DTemp).ThrowOnError(); - tex2D.Swap(&tex2DTemp); - } - - tex2D.Get()->GetDesc(&texDesc); - } - - var dxgiFormat = texDesc.Format; + var dxgiFormat = this.GetFormatOf(wrap); if (!WicManager.GetCorrespondingWicPixelFormat(dxgiFormat, out _, out _)) dxgiFormat = DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM; @@ -73,10 +50,7 @@ internal sealed partial class TextureManager var (specs, bytes) = await this.GetRawImageAsync( wrap, - new() - { - Format = dxgiFormat, - }, + new() { Format = dxgiFormat }, true, cancellationToken).ConfigureAwait(false); @@ -102,14 +76,29 @@ internal sealed partial class TextureManager var pathTemp = $"{path}.{GetCurrentThreadId():X08}{Environment.TickCount64:X16}.tmp"; try { - await this.SaveToStreamAsync( - wrap, + var dxgiFormat = this.GetFormatOf(wrap); + if (!WicManager.GetCorrespondingWicPixelFormat(dxgiFormat, out _, out _)) + dxgiFormat = DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM; + + using var istream = TerraFxComInterfaceExtensions.CreateIStreamFromFile( + pathTemp, + FileMode.Create, + FileAccess.Write, + FileShare.None); + + var (specs, bytes) = await this.GetRawImageAsync( + wrap, + new() { Format = dxgiFormat }, + true, + cancellationToken).ConfigureAwait(false); + + this.Wic.SaveToStreamUsingWic( + specs, + bytes, containerGuid, - File.Create(pathTemp), + istream, props, - leaveWrapOpen: true, - leaveStreamOpen: false, - cancellationToken: cancellationToken); + cancellationToken); } catch (Exception e) { @@ -182,7 +171,7 @@ internal sealed partial class TextureManager try { using var handle = bytes.Pin(); - using var stream = this.Wic.CreateIStreamFromMemory(handle, bytes.Length); + using var stream = this.Wic.CreateIStreamViewOfMemory(handle, bytes.Length); return this.Wic.NoThrottleCreateFromWicStream(stream, cancellationToken); } catch (Exception e1) @@ -212,7 +201,11 @@ internal sealed partial class TextureManager try { - using var stream = this.Wic.CreateIStreamFromFile(path); + using var stream = TerraFxComInterfaceExtensions.CreateIStreamFromFile( + path, + FileMode.Open, + FileAccess.Read, + FileShare.Read); return this.Wic.NoThrottleCreateFromWicStream(stream, cancellationToken); } catch (Exception e1) @@ -228,6 +221,26 @@ internal sealed partial class TextureManager } } + private unsafe DXGI_FORMAT GetFormatOf(IDalamudTextureWrap wrap) + { + using var texSrv = default(ComPtr); + using var context = default(ComPtr); + fixed (Guid* piid = &IID.IID_ID3D11ShaderResourceView) + ((IUnknown*)wrap.ImGuiHandle)->QueryInterface(piid, (void**)texSrv.GetAddressOf()).ThrowOnError(); + + this.Device.Get()->GetImmediateContext(context.GetAddressOf()); + + using var texRes = default(ComPtr); + texSrv.Get()->GetResource(texRes.GetAddressOf()); + + using var tex2D = default(ComPtr); + texRes.As(&tex2D).ThrowOnError(); + + var texDesc = default(D3D11_TEXTURE2D_DESC); + tex2D.Get()->GetDesc(&texDesc); + return texDesc.Format; + } + /// A part of texture manager that uses Windows Imaging Component under the hood. internal sealed class WicManager : IDisposable { @@ -355,7 +368,7 @@ internal sealed partial class TextureManager /// An instance of . /// The number of bytes in the memory. /// The new instance of . - public unsafe ComPtr CreateIStreamFromMemory(MemoryHandle handle, int length) + public unsafe ComPtr CreateIStreamViewOfMemory(MemoryHandle handle, int length) { using var wicStream = default(ComPtr); this.wicFactory.Get()->CreateStream(wicStream.GetAddressOf()).ThrowOnError(); @@ -366,21 +379,6 @@ internal sealed partial class TextureManager return res; } - /// Creates a new instance of from a file path. - /// The file path. - /// The new instance of . - public unsafe ComPtr CreateIStreamFromFile(string path) - { - using var wicStream = default(ComPtr); - this.wicFactory.Get()->CreateStream(wicStream.GetAddressOf()).ThrowOnError(); - fixed (char* pPath = path) - wicStream.Get()->InitializeFromFilename((ushort*)pPath, GENERIC_READ).ThrowOnError(); - - var res = default(ComPtr); - wicStream.As(ref res).ThrowOnError(); - return res; - } - /// Creates a new instance of from a . /// The stream that will NOT be closed after. /// The cancellation token. diff --git a/Dalamud/Utility/TerraFxCom/IPropertyBag2Extensions.cs b/Dalamud/Utility/TerraFxCom/IPropertyBag2Extensions.cs deleted file mode 100644 index cfb11ae41..000000000 --- a/Dalamud/Utility/TerraFxCom/IPropertyBag2Extensions.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System.Runtime.InteropServices; - -using TerraFX.Interop.Windows; - -using static TerraFX.Interop.Windows.Windows; - -namespace Dalamud.Utility.TerraFxCom; - -/// Utilities for and its derivatives. -internal static unsafe partial class TerraFxComInterfaceExtensions -{ - /// Calls . - /// The property bag. - /// The name of the item to be interpreted as a VARIANT. - /// The new value, to be interpreted as a . - /// Return value from . - public static HRESULT Write(ref this IPropertyBag2 obj, string name, object? value) - { - VARIANT varValue; - // Marshal calls VariantInit. - Marshal.GetNativeVariantForObject(value, (nint)(&varValue)); - try - { - fixed (char* pName = name) - { - var option = new PROPBAG2 { pstrName = (ushort*)pName }; - return obj.Write(1, &option, &varValue); - } - } - finally - { - VariantClear(&varValue); - } - } - - /// Calls . - /// The object. - /// The name of the metadata. - /// The new value, to be interpreted as a . - /// Return value from . - public static HRESULT SetMetadataByName(ref this IWICMetadataQueryWriter obj, string name, object? value) - { - VARIANT varValue; - // Marshal calls VariantInit. - Marshal.GetNativeVariantForObject(value, (nint)(&varValue)); - try - { - PROPVARIANT propVarValue; - var propVarRes = VariantToPropVariant(&varValue, &propVarValue); - if (propVarRes < 0) - return propVarRes; - - try - { - fixed (char* pName = name) - return obj.SetMetadataByName((ushort*)pName, &propVarValue); - } - finally - { - _ = PropVariantClear(&propVarValue); - } - } - finally - { - _ = VariantClear(&varValue); - } - } - - /// Calls . - /// The object. - /// The name of the metadata. - /// Return value from . - public static HRESULT RemoveMetadataByName(ref this IWICMetadataQueryWriter obj, string name) - { - fixed (char* pName = name) - return obj.RemoveMetadataByName((ushort*)pName); - } - - [LibraryImport("propsys.dll")] - private static partial int VariantToPropVariant( - void* pVarIn, - void* pPropVarOut); -} diff --git a/Dalamud/Utility/TerraFxCom/TerraFxComInterfaceExtensions.cs b/Dalamud/Utility/TerraFxCom/TerraFxComInterfaceExtensions.cs new file mode 100644 index 000000000..f9252839f --- /dev/null +++ b/Dalamud/Utility/TerraFxCom/TerraFxComInterfaceExtensions.cs @@ -0,0 +1,175 @@ +using System.IO; +using System.Runtime.InteropServices; + +using TerraFX.Interop.Windows; + +using static TerraFX.Interop.Windows.Windows; + +namespace Dalamud.Utility.TerraFxCom; + +/// Utilities for and its derivatives. +internal static unsafe partial class TerraFxComInterfaceExtensions +{ + /// Creates a new instance of from a file path. + /// The file path. + /// The file open mode. + /// The file access mode. + /// The file share mode. + /// The file attributes. + /// The new instance of . + public static ComPtr CreateIStreamFromFile( + string path, + FileMode mode, + FileAccess access, + FileShare share, + FileAttributes attributes = FileAttributes.Normal) + { + var grfMode = 0u; + bool fCreate; + switch (mode) + { + case FileMode.CreateNew: + fCreate = true; + grfMode |= STGM.STGM_FAILIFTHERE; + break; + case FileMode.Create: + fCreate = true; + grfMode |= STGM.STGM_CREATE; + break; + case FileMode.Open: + fCreate = false; + grfMode |= STGM.STGM_FAILIFTHERE; // yes + break; + case FileMode.OpenOrCreate: + throw new NotSupportedException( + $"${FileMode.OpenOrCreate} is not supported. It might be, but it needs testing."); + case FileMode.Append: + throw new NotSupportedException($"${FileMode.Append} is not supported."); + case FileMode.Truncate: + throw new NotSupportedException($"${FileMode.Truncate} is not supported."); + default: + throw new ArgumentOutOfRangeException(nameof(mode), mode, null); + } + + switch (access) + { + case FileAccess.Read: + grfMode |= STGM.STGM_READ; + break; + case FileAccess.Write: + grfMode |= STGM.STGM_WRITE; + break; + case FileAccess.ReadWrite: + grfMode |= STGM.STGM_READWRITE; + break; + default: + throw new ArgumentOutOfRangeException(nameof(access), access, null); + } + + switch (share) + { + case FileShare.None: + grfMode |= STGM.STGM_SHARE_EXCLUSIVE; + break; + case FileShare.Read: + grfMode |= STGM.STGM_SHARE_DENY_WRITE; + break; + case FileShare.Write: + grfMode |= STGM.STGM_SHARE_DENY_READ; + break; + case FileShare.ReadWrite: + grfMode |= STGM.STGM_SHARE_DENY_NONE; + break; + default: + throw new NotSupportedException($"Only ${FileShare.Read} and ${FileShare.Write} are supported."); + } + + using var stream = default(ComPtr); + fixed (char* pPath = path) + { + SHCreateStreamOnFileEx( + (ushort*)pPath, + grfMode, + (uint)attributes, + fCreate, + null, + stream.GetAddressOf()).ThrowOnError(); + } + + var res = default(ComPtr); + stream.As(ref res).ThrowOnError(); + return res; + } + + /// Calls . + /// The property bag. + /// The name of the item to be interpreted as a VARIANT. + /// The new value, to be interpreted as a . + /// Return value from . + public static HRESULT Write(ref this IPropertyBag2 obj, string name, object? value) + { + VARIANT varValue; + // Marshal calls VariantInit. + Marshal.GetNativeVariantForObject(value, (nint)(&varValue)); + try + { + fixed (char* pName = name) + { + var option = new PROPBAG2 { pstrName = (ushort*)pName }; + return obj.Write(1, &option, &varValue); + } + } + finally + { + VariantClear(&varValue); + } + } + + /// Calls . + /// The object. + /// The name of the metadata. + /// The new value, to be interpreted as a . + /// Return value from . + public static HRESULT SetMetadataByName(ref this IWICMetadataQueryWriter obj, string name, object? value) + { + VARIANT varValue; + // Marshal calls VariantInit. + Marshal.GetNativeVariantForObject(value, (nint)(&varValue)); + try + { + PROPVARIANT propVarValue; + var propVarRes = VariantToPropVariant(&varValue, &propVarValue); + if (propVarRes < 0) + return propVarRes; + + try + { + fixed (char* pName = name) + return obj.SetMetadataByName((ushort*)pName, &propVarValue); + } + finally + { + _ = PropVariantClear(&propVarValue); + } + } + finally + { + _ = VariantClear(&varValue); + } + } + + /// Calls . + /// The object. + /// The name of the metadata. + /// Return value from . + public static HRESULT RemoveMetadataByName(ref this IWICMetadataQueryWriter obj, string name) + { + fixed (char* pName = name) + return obj.RemoveMetadataByName((ushort*)pName); + } + + [LibraryImport("propsys.dll")] + private static partial int VariantToPropVariant( + void* pVarIn, + void* pPropVarOut); +} From 5fd7457df4a9c551e8689d2a9f01c938748c7df0 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 3 Mar 2024 22:24:15 +0900 Subject: [PATCH 44/57] Add ForwardingTextureWrap --- .../Textures/ForwardingTextureWrap.cs | 81 +++++++++++++++++ .../Interface/Textures/IDalamudTextureWrap.cs | 3 + .../Internal/DisposeSuppressingTextureWrap.cs | 24 ++--- .../SharedImmediateTexture.cs | 88 ++++++------------- .../Textures/Internal/ViewportTextureWrap.cs | 1 + 5 files changed, 120 insertions(+), 77 deletions(-) create mode 100644 Dalamud/Interface/Textures/ForwardingTextureWrap.cs diff --git a/Dalamud/Interface/Textures/ForwardingTextureWrap.cs b/Dalamud/Interface/Textures/ForwardingTextureWrap.cs new file mode 100644 index 000000000..c398fc55c --- /dev/null +++ b/Dalamud/Interface/Textures/ForwardingTextureWrap.cs @@ -0,0 +1,81 @@ +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using System.Runtime.CompilerServices; + +using Dalamud.Interface.Internal; +using Dalamud.Interface.Textures.Internal; + +using TerraFX.Interop.Windows; + +namespace Dalamud.Interface.Textures; + +/// Base class for implementations of that forwards to another. +public abstract class ForwardingTextureWrap : IDalamudTextureWrap +{ + /// + public IntPtr ImGuiHandle + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.GetWrap().ImGuiHandle; + } + + /// + public int Width + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.GetWrap().Width; + } + + /// + public int Height + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.GetWrap().Height; + } + + /// + public Vector2 Size + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => new(this.Width, this.Height); + } + + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// + public virtual unsafe IDalamudTextureWrap CreateWrapSharingLowLevelResource() + { + // Dalamud specific: IDalamudTextureWrap always points to an ID3D11ShaderResourceView. + var handle = (IUnknown*)this.ImGuiHandle; + return new UnknownTextureWrap(handle, this.Width, this.Height, true); + } + + /// + public override string ToString() => $"{this.GetType()}({(this.TryGetWrap(out var wrap) ? wrap : null)})"; + + /// Called on . + /// true if called from . + /// + /// Base implementation will not dispose the result of . + /// If you need to implement a finalizer, then make it call this function with false. + /// + protected virtual void Dispose(bool disposing) + { + } + + /// Gets the inner wrap. + /// The inner wrap. + /// true if not disposed and is available. + protected abstract bool TryGetWrap([NotNullWhen(true)] out IDalamudTextureWrap? wrap); + + /// Gets the inner wrap. + /// The inner wrap. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected IDalamudTextureWrap GetWrap() => + this.TryGetWrap(out var wrap) ? wrap : throw new ObjectDisposedException(this.GetType().Name); +} diff --git a/Dalamud/Interface/Textures/IDalamudTextureWrap.cs b/Dalamud/Interface/Textures/IDalamudTextureWrap.cs index dad453cb9..c51a69f06 100644 --- a/Dalamud/Interface/Textures/IDalamudTextureWrap.cs +++ b/Dalamud/Interface/Textures/IDalamudTextureWrap.cs @@ -1,5 +1,6 @@ using System.Numerics; +using Dalamud.Interface.Textures; using Dalamud.Interface.Textures.Internal; using TerraFX.Interop.Windows; @@ -12,6 +13,8 @@ namespace Dalamud.Interface.Internal; /// Base TextureWrap interface for all Dalamud-owned texture wraps. /// Used to avoid referencing ImGuiScene. ///
+/// If you want to implement this, see if you're actually wrapping an existing instance of +/// ; if you are, then use . public interface IDalamudTextureWrap : IDisposable { /// Gets a texture handle suitable for direct use with ImGui functions. diff --git a/Dalamud/Interface/Textures/Internal/DisposeSuppressingTextureWrap.cs b/Dalamud/Interface/Textures/Internal/DisposeSuppressingTextureWrap.cs index 17a88e270..88d8d9ca0 100644 --- a/Dalamud/Interface/Textures/Internal/DisposeSuppressingTextureWrap.cs +++ b/Dalamud/Interface/Textures/Internal/DisposeSuppressingTextureWrap.cs @@ -2,31 +2,19 @@ using Dalamud.Interface.Internal; namespace Dalamud.Interface.Textures.Internal; -/// -/// A texture wrap that ignores calls. -/// -internal sealed class DisposeSuppressingTextureWrap : IDalamudTextureWrap +/// A texture wrap that ignores calls. +internal class DisposeSuppressingTextureWrap : ForwardingTextureWrap { private readonly IDalamudTextureWrap innerWrap; - /// - /// Initializes a new instance of the class. - /// + /// 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() + protected override bool TryGetWrap(out IDalamudTextureWrap? wrap) { - // suppressed + wrap = this.innerWrap; + return true; } } diff --git a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/SharedImmediateTexture.cs b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/SharedImmediateTexture.cs index f730637e4..4a6289ceb 100644 --- a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/SharedImmediateTexture.cs +++ b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/SharedImmediateTexture.cs @@ -363,46 +363,35 @@ internal abstract class SharedImmediateTexture } } - private sealed class NotOwnedTextureWrap : IDalamudTextureWrap + /// Same with , but with a custom implementation of + /// . + private sealed class NotOwnedTextureWrap : DisposeSuppressingTextureWrap { - private readonly IDalamudTextureWrap innerWrap; private readonly IRefCountable owner; /// Initializes a new instance of the class. /// The inner wrap. /// The reference counting owner. public NotOwnedTextureWrap(IDalamudTextureWrap wrap, IRefCountable owner) + : base(wrap) { - this.innerWrap = wrap; this.owner = owner; } /// - public IntPtr ImGuiHandle => this.innerWrap.ImGuiHandle; - - /// - public int Width => this.innerWrap.Width; - - /// - public int Height => this.innerWrap.Height; - - /// - public IDalamudTextureWrap CreateWrapSharingLowLevelResource() + public override IDalamudTextureWrap CreateWrapSharingLowLevelResource() { + var wrap = this.GetWrap(); this.owner.AddRef(); - return new RefCountableWrappingTextureWrap(this.innerWrap, this.owner); - } - - /// - public void Dispose() - { + return new RefCountableWrappingTextureWrap(wrap, this.owner); } /// public override string ToString() => $"{nameof(NotOwnedTextureWrap)}({this.owner})"; } - private sealed class RefCountableWrappingTextureWrap : IDalamudTextureWrap + /// Reference counting texture wrap, to be used with . + private sealed class RefCountableWrappingTextureWrap : ForwardingTextureWrap { private IDalamudTextureWrap? innerWrap; private IRefCountable? owner; @@ -416,22 +405,11 @@ internal abstract class SharedImmediateTexture this.owner = owner; } - ~RefCountableWrappingTextureWrap() => this.Dispose(); + /// Finalizes an instance of the class. + ~RefCountableWrappingTextureWrap() => this.Dispose(false); /// - 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 IDalamudTextureWrap CreateWrapSharingLowLevelResource() + public override IDalamudTextureWrap CreateWrapSharingLowLevelResource() { var ownerCopy = this.owner; var wrapCopy = this.innerWrap; @@ -443,7 +421,13 @@ internal abstract class SharedImmediateTexture } /// - public void Dispose() + public override string ToString() => $"{nameof(RefCountableWrappingTextureWrap)}({this.owner})"; + + /// + protected override bool TryGetWrap(out IDalamudTextureWrap? wrap) => (wrap = this.innerWrap) is not null; + + /// + protected override void Dispose(bool disposing) { while (true) { @@ -455,32 +439,22 @@ internal abstract class SharedImmediateTexture // Note: do not dispose this; life of the wrap is managed by the owner. this.innerWrap = null; ownerCopy.Release(); - GC.SuppressFinalize(this); } } - - /// - public override string ToString() => $"{nameof(RefCountableWrappingTextureWrap)}({this.owner})"; } + /// A texture wrap that revives and waits for the underlying texture as needed on every access. [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] - private sealed class AvailableOnAccessTextureWrap : IDalamudTextureWrap + private sealed class AvailableOnAccessTextureWrap : ForwardingTextureWrap { private readonly SharedImmediateTexture inner; + /// Initializes a new instance of the class. + /// The shared texture. public AvailableOnAccessTextureWrap(SharedImmediateTexture inner) => this.inner = inner; /// - public IntPtr ImGuiHandle => this.WaitGet().ImGuiHandle; - - /// - public int Width => this.WaitGet().Width; - - /// - public int Height => this.WaitGet().Height; - - /// - public IDalamudTextureWrap CreateWrapSharingLowLevelResource() + public override IDalamudTextureWrap CreateWrapSharingLowLevelResource() { this.inner.AddRef(); try @@ -506,22 +480,18 @@ internal abstract class SharedImmediateTexture } } - /// - public void Dispose() - { - // ignore - } - /// public override string ToString() => $"{nameof(AvailableOnAccessTextureWrap)}({this.inner})"; - private IDalamudTextureWrap WaitGet() + /// + protected override bool TryGetWrap(out IDalamudTextureWrap? wrap) { if (this.inner.TryGetWrapCore(out var t, out _)) - return t; + wrap = t; this.inner.UnderlyingWrap?.Wait(); - return this.inner.nonOwningWrap ?? Service.Get().Empty4X4; + wrap = this.inner.nonOwningWrap ?? Service.Get().Empty4X4; + return true; } } } diff --git a/Dalamud/Interface/Textures/Internal/ViewportTextureWrap.cs b/Dalamud/Interface/Textures/Internal/ViewportTextureWrap.cs index ca914a234..272a2d8ee 100644 --- a/Dalamud/Interface/Textures/Internal/ViewportTextureWrap.cs +++ b/Dalamud/Interface/Textures/Internal/ViewportTextureWrap.cs @@ -195,6 +195,7 @@ internal sealed class ViewportTextureWrap : IDalamudTextureWrap, IDeferredDispos _ = this.FirstUpdateTask.Exception; this.tex.Reset(); this.srv.Reset(); + this.rtv.Reset(); } private static unsafe ComPtr GetImGuiViewportBackBuffer(uint viewportId) From 0a658477c648b18972dd9e137269a44ce2d36d87 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Mon, 4 Mar 2024 21:13:00 +0900 Subject: [PATCH 45/57] cleanup --- .../TextureManager.FromExistingTexture.cs | 206 ++++++++++++------ .../Textures/Internal/TextureManager.Wic.cs | 201 ++++++++--------- .../Services/ITextureReadbackProvider.cs | 1 + 3 files changed, 230 insertions(+), 178 deletions(-) diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs b/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs index 2f9874217..c9ec65502 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -57,7 +58,9 @@ internal sealed partial class TextureManager async Task ImmediateLoadFunction(CancellationToken ct) { - using var tex = await this.NoThrottleCreateFromExistingTextureAsync(wrap, args); + // leaveWrapOpen is taken care from calling LoadTextureAsync + using var wrapAux = new WrapAux(wrap, true); + using var tex = await this.NoThrottleCreateFromExistingTextureAsync(wrapAux, args); unsafe { @@ -100,34 +103,19 @@ internal sealed partial class TextureManager bool leaveWrapOpen = false, CancellationToken cancellationToken = default) { - using var wrapDispose = leaveWrapOpen ? null : wrap; - using var texSrv = default(ComPtr); - using var context = default(ComPtr); - using var tex2D = default(ComPtr); - var texDesc = default(D3D11_TEXTURE2D_DESC); + using var wrapAux = new WrapAux(wrap, leaveWrapOpen); + return await this.GetRawImageAsync(wrapAux, args, cancellationToken); + } - unsafe + private async Task<(RawImageSpecification Specification, byte[] RawData)> GetRawImageAsync( + WrapAux wrapAux, + TextureModificationArgs args = default, + CancellationToken cancellationToken = default) + { + using var tex2D = wrapAux.NewTexRef(); + if (!args.IsCompleteSourceCopy(wrapAux.Desc)) { - fixed (Guid* piid = &IID.IID_ID3D11ShaderResourceView) - ((IUnknown*)wrap.ImGuiHandle)->QueryInterface(piid, (void**)texSrv.GetAddressOf()).ThrowOnError(); - - this.Device.Get()->GetImmediateContext(context.GetAddressOf()); - - using (var texRes = default(ComPtr)) - { - texSrv.Get()->GetResource(texRes.GetAddressOf()); - - using var tex2DTemp = default(ComPtr); - texRes.As(&tex2DTemp).ThrowOnError(); - tex2D.Swap(&tex2DTemp); - } - - tex2D.Get()->GetDesc(&texDesc); - } - - if (!args.IsCompleteSourceCopy(texDesc)) - { - using var tmp = await this.NoThrottleCreateFromExistingTextureAsync(wrap, args); + using var tmp = await this.NoThrottleCreateFromExistingTextureAsync(wrapAux, args); unsafe { tex2D.Swap(&tmp); @@ -136,11 +124,10 @@ internal sealed partial class TextureManager cancellationToken.ThrowIfCancellationRequested(); return await this.interfaceManager.RunBeforeImGuiRender( - () => ExtractMappedResource(this.Device, context, tex2D, cancellationToken)); + () => ExtractMappedResource(wrapAux, tex2D, cancellationToken)); static unsafe (RawImageSpecification Specification, byte[] RawData) ExtractMappedResource( - ComPtr device, - ComPtr context, + in WrapAux wrapAux, ComPtr tex2D, CancellationToken cancellationToken) { @@ -150,11 +137,9 @@ internal sealed partial class TextureManager try { using var tmpTex = default(ComPtr); - D3D11_TEXTURE2D_DESC desc; - tex2D.Get()->GetDesc(&desc); - if ((desc.CPUAccessFlags & (uint)D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_READ) == 0) + if ((wrapAux.Desc.CPUAccessFlags & (uint)D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_READ) == 0) { - var tmpTexDesc = desc with + var tmpTexDesc = wrapAux.Desc with { MipLevels = 1, ArraySize = 1, @@ -164,15 +149,15 @@ internal sealed partial class TextureManager CPUAccessFlags = (uint)D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_READ, MiscFlags = 0u, }; - device.Get()->CreateTexture2D(&tmpTexDesc, null, tmpTex.GetAddressOf()).ThrowOnError(); - context.Get()->CopyResource((ID3D11Resource*)tmpTex.Get(), (ID3D11Resource*)tex2D.Get()); + wrapAux.DevPtr->CreateTexture2D(&tmpTexDesc, null, tmpTex.GetAddressOf()).ThrowOnError(); + wrapAux.CtxPtr->CopyResource((ID3D11Resource*)tmpTex.Get(), (ID3D11Resource*)tex2D.Get()); cancellationToken.ThrowIfCancellationRequested(); } D3D11_MAPPED_SUBRESOURCE mapped; mapWhat = (ID3D11Resource*)(tmpTex.IsEmpty() ? tex2D.Get() : tmpTex.Get()); - context.Get()->Map( + wrapAux.CtxPtr->Map( mapWhat, 0, D3D11_MAP.D3D11_MAP_READ, @@ -180,9 +165,9 @@ internal sealed partial class TextureManager &mapped).ThrowOnError(); var specs = new RawImageSpecification( - (int)desc.Width, - (int)desc.Height, - (int)desc.Format, + (int)wrapAux.Desc.Width, + (int)wrapAux.Desc.Height, + (int)wrapAux.Desc.Format, (int)mapped.RowPitch); var bytes = new Span(mapped.pData, checked((int)mapped.DepthPitch)).ToArray(); return (specs, bytes); @@ -190,44 +175,23 @@ internal sealed partial class TextureManager finally { if (mapWhat is not null) - context.Get()->Unmap(mapWhat, 0); + wrapAux.CtxPtr->Unmap(mapWhat, 0); } } } private async Task> NoThrottleCreateFromExistingTextureAsync( - IDalamudTextureWrap wrap, + WrapAux wrapAux, TextureModificationArgs args) { args.ThrowOnInvalidValues(); - using var texSrv = default(ComPtr); - using var context = default(ComPtr); - using var tex2D = default(ComPtr); - var texDesc = default(D3D11_TEXTURE2D_DESC); - - unsafe - { - fixed (Guid* piid = &IID.IID_ID3D11ShaderResourceView) - ((IUnknown*)wrap.ImGuiHandle)->QueryInterface(piid, (void**)texSrv.GetAddressOf()).ThrowOnError(); - - this.Device.Get()->GetImmediateContext(context.GetAddressOf()); - - using (var texRes = default(ComPtr)) - { - texSrv.Get()->GetResource(texRes.GetAddressOf()); - texRes.As(&tex2D).ThrowOnError(); - } - - tex2D.Get()->GetDesc(&texDesc); - } - if (args.Format == DXGI_FORMAT.DXGI_FORMAT_UNKNOWN) - args = args with { Format = texDesc.Format }; + args = args with { Format = wrapAux.Desc.Format }; if (args.NewWidth == 0) - args = args with { NewWidth = (int)MathF.Round((args.Uv1Effective.X - args.Uv0.X) * texDesc.Width) }; + args = args with { NewWidth = (int)MathF.Round((args.Uv1Effective.X - args.Uv0.X) * wrapAux.Desc.Width) }; if (args.NewHeight == 0) - args = args with { NewHeight = (int)MathF.Round((args.Uv1Effective.Y - args.Uv0.Y) * texDesc.Height) }; + args = args with { NewHeight = (int)MathF.Round((args.Uv1Effective.Y - args.Uv0.Y) * wrapAux.Desc.Height) }; using var tex2DCopyTemp = default(ComPtr); unsafe @@ -263,20 +227,120 @@ internal sealed partial class TextureManager &rtvCopyTempDesc, rtvCopyTemp.GetAddressOf()).ThrowOnError(); - context.Get()->OMSetRenderTargets(1u, rtvCopyTemp.GetAddressOf(), null); + wrapAux.CtxPtr->OMSetRenderTargets(1u, rtvCopyTemp.GetAddressOf(), null); this.SimpleDrawer.Draw( - context.Get(), - texSrv.Get(), + wrapAux.CtxPtr, + wrapAux.SrvPtr, args.Uv0, args.Uv1Effective); if (args.MakeOpaque) - this.SimpleDrawer.StripAlpha(context.Get()); + this.SimpleDrawer.StripAlpha(wrapAux.CtxPtr); var dummy = default(ID3D11RenderTargetView*); - context.Get()->OMSetRenderTargets(1u, &dummy, null); + wrapAux.CtxPtr->OMSetRenderTargets(1u, &dummy, null); } }); return new(tex2DCopyTemp); } + + /// Auxiliary data from . + private unsafe struct WrapAux : IDisposable + { + public readonly D3D11_TEXTURE2D_DESC Desc; + + private IDalamudTextureWrap? wrapToClose; + + private ComPtr srv; + private ComPtr res; + private ComPtr tex; + private ComPtr device; + private ComPtr context; + + public WrapAux(IDalamudTextureWrap wrap, bool leaveWrapOpen) + { + this.wrapToClose = leaveWrapOpen ? null : wrap; + + using var unk = new ComPtr((IUnknown*)wrap.ImGuiHandle); + + using var srvTemp = default(ComPtr); + unk.As(&srvTemp).ThrowOnError(); + + using var resTemp = default(ComPtr); + srvTemp.Get()->GetResource(resTemp.GetAddressOf()); + + using var texTemp = default(ComPtr); + resTemp.As(&texTemp).ThrowOnError(); + + using var deviceTemp = default(ComPtr); + texTemp.Get()->GetDevice(deviceTemp.GetAddressOf()); + + using var contextTemp = default(ComPtr); + deviceTemp.Get()->GetImmediateContext(contextTemp.GetAddressOf()); + + fixed (D3D11_TEXTURE2D_DESC* pDesc = &this.Desc) + texTemp.Get()->GetDesc(pDesc); + + srvTemp.Swap(ref this.srv); + resTemp.Swap(ref this.res); + texTemp.Swap(ref this.tex); + deviceTemp.Swap(ref this.device); + contextTemp.Swap(ref this.context); + } + + public ID3D11ShaderResourceView* SrvPtr + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.srv.Get(); + } + + public ID3D11Resource* ResPtr + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.res.Get(); + } + + public ID3D11Texture2D* TexPtr + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.tex.Get(); + } + + public ID3D11Device* DevPtr + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.device.Get(); + } + + public ID3D11DeviceContext* CtxPtr + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.context.Get(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ComPtr NewSrvRef() => new(this.srv); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ComPtr NewResRef() => new(this.res); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ComPtr NewTexRef() => new(this.tex); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ComPtr NewDevRef() => new(this.device); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ComPtr NewCtxRef() => new(this.context); + + public void Dispose() + { + this.srv.Reset(); + this.res.Reset(); + this.tex.Reset(); + this.device.Reset(); + this.context.Reset(); + Interlocked.Exchange(ref this.wrapToClose, null)?.Dispose(); + } + } } diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs b/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs index 2311923f8..e59a3a1f2 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs @@ -27,124 +27,131 @@ namespace Dalamud.Interface.Textures.Internal; internal sealed partial class TextureManager { /// - [SuppressMessage( - "StyleCop.CSharp.LayoutRules", - "SA1519:Braces should not be omitted from multi-line child statement", - Justification = "Multiple fixed blocks")] public async Task SaveToStreamAsync( - IDalamudTextureWrap wrap, + IDalamudTextureWrap? wrap, Guid containerGuid, - Stream stream, + Stream? stream, IReadOnlyDictionary? props = null, bool leaveWrapOpen = false, bool leaveStreamOpen = false, CancellationToken cancellationToken = default) { - using var wrapDispose = leaveWrapOpen ? null : wrap; + try + { + if (wrap is null) + throw new NullReferenceException($"{nameof(wrap)} cannot be null."); + if (stream is null) + throw new NullReferenceException($"{nameof(stream)} cannot be null."); - var dxgiFormat = this.GetFormatOf(wrap); - if (!WicManager.GetCorrespondingWicPixelFormat(dxgiFormat, out _, out _)) - dxgiFormat = DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM; + using var istream = ManagedIStream.Create(stream, true); + using var wrapAux = new WrapAux(wrap, true); - using var istream = ManagedIStream.Create(stream, leaveStreamOpen); + var dxgiFormat = + WicManager.GetCorrespondingWicPixelFormat(wrapAux.Desc.Format, out _, out _) + ? wrapAux.Desc.Format + : DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM; - var (specs, bytes) = await this.GetRawImageAsync( - wrap, - new() { Format = dxgiFormat }, - true, - cancellationToken).ConfigureAwait(false); + var (specs, bytes) = await this.GetRawImageAsync(wrapAux, new() { Format = dxgiFormat }, cancellationToken) + .ConfigureAwait(false); - this.Wic.SaveToStreamUsingWic( - specs, - bytes, - containerGuid, - istream, - props, - cancellationToken); + await Task.Run( + () => this.Wic.SaveToStreamUsingWic( + specs, + bytes, + containerGuid, + istream, + props, + cancellationToken), + cancellationToken); + } + finally + { + if (!leaveWrapOpen) + wrap?.Dispose(); + if (!leaveStreamOpen) + stream?.Dispose(); + } } /// public async Task SaveToFileAsync( - IDalamudTextureWrap wrap, + IDalamudTextureWrap? wrap, Guid containerGuid, - string path, + string? path, IReadOnlyDictionary? props = null, bool leaveWrapOpen = false, CancellationToken cancellationToken = default) { - using var wrapDispose = leaveWrapOpen ? null : wrap; - var pathTemp = $"{path}.{GetCurrentThreadId():X08}{Environment.TickCount64:X16}.tmp"; try { - var dxgiFormat = this.GetFormatOf(wrap); - if (!WicManager.GetCorrespondingWicPixelFormat(dxgiFormat, out _, out _)) - dxgiFormat = DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM; + if (wrap is null) + throw new NullReferenceException($"{nameof(wrap)} cannot be null."); + if (path is null) + throw new NullReferenceException($"{nameof(path)} cannot be null."); - using var istream = TerraFxComInterfaceExtensions.CreateIStreamFromFile( - pathTemp, - FileMode.Create, - FileAccess.Write, - FileShare.None); - - var (specs, bytes) = await this.GetRawImageAsync( - wrap, - new() { Format = dxgiFormat }, - true, - cancellationToken).ConfigureAwait(false); - - this.Wic.SaveToStreamUsingWic( - specs, - bytes, - containerGuid, - istream, - props, - cancellationToken); - } - catch (Exception e) - { + using var wrapAux = new WrapAux(wrap, true); + var pathTemp = $"{path}.{GetCurrentThreadId():X08}{Environment.TickCount64:X16}.tmp"; + var trashfire = new List(); try { - if (File.Exists(pathTemp)) - File.Delete(pathTemp); + using (var istream = TerraFxComInterfaceExtensions.CreateIStreamFromFile( + pathTemp, + FileMode.Create, + FileAccess.Write, + FileShare.None)) + { + var dxgiFormat = + WicManager.GetCorrespondingWicPixelFormat(wrapAux.Desc.Format, out _, out _) + ? wrapAux.Desc.Format + : DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM; + + var (specs, bytes) = await this.GetRawImageAsync( + wrapAux, + new() { Format = dxgiFormat }, + cancellationToken).ConfigureAwait(false); + + await Task.Run( + () => this.Wic.SaveToStreamUsingWic( + specs, + bytes, + containerGuid, + istream, + props, + cancellationToken), + cancellationToken); + } + + try + { + File.Replace(pathTemp, path, null, true); + } + catch (Exception e) + { + trashfire.Add(e); + File.Move(pathTemp, path, true); + } + + return; } - catch (Exception e2) + catch (Exception e) { - throw new AggregateException( - "Failed to save the file, and failed to remove the temporary file.", - e, - e2); + trashfire.Add(e); + try + { + if (File.Exists(pathTemp)) + File.Delete(pathTemp); + } + catch (Exception e2) + { + trashfire.Add(e2); + } } - throw; + throw new AggregateException($"{nameof(this.SaveToFileAsync)} error.", trashfire); } - - try + finally { - try - { - File.Replace(pathTemp, path, null, true); - } - catch - { - File.Move(pathTemp, path, true); - } - } - catch (Exception e) - { - try - { - if (File.Exists(pathTemp)) - File.Delete(pathTemp); - } - catch (Exception e2) - { - throw new AggregateException( - "Failed to move the temporary file to the target path, and failed to remove the temporary file.", - e, - e2); - } - - throw; + wrap?.Dispose(); } } @@ -221,26 +228,6 @@ internal sealed partial class TextureManager } } - private unsafe DXGI_FORMAT GetFormatOf(IDalamudTextureWrap wrap) - { - using var texSrv = default(ComPtr); - using var context = default(ComPtr); - fixed (Guid* piid = &IID.IID_ID3D11ShaderResourceView) - ((IUnknown*)wrap.ImGuiHandle)->QueryInterface(piid, (void**)texSrv.GetAddressOf()).ThrowOnError(); - - this.Device.Get()->GetImmediateContext(context.GetAddressOf()); - - using var texRes = default(ComPtr); - texSrv.Get()->GetResource(texRes.GetAddressOf()); - - using var tex2D = default(ComPtr); - texRes.As(&tex2D).ThrowOnError(); - - var texDesc = default(D3D11_TEXTURE2D_DESC); - tex2D.Get()->GetDesc(&texDesc); - return texDesc.Format; - } - /// A part of texture manager that uses Windows Imaging Component under the hood. internal sealed class WicManager : IDisposable { diff --git a/Dalamud/Plugin/Services/ITextureReadbackProvider.cs b/Dalamud/Plugin/Services/ITextureReadbackProvider.cs index fa666c3c2..96c817091 100644 --- a/Dalamud/Plugin/Services/ITextureReadbackProvider.cs +++ b/Dalamud/Plugin/Services/ITextureReadbackProvider.cs @@ -84,6 +84,7 @@ public interface ITextureReadbackProvider /// A task representing the save process. /// /// must not be disposed until the task finishes. + /// If the target file exists, it will be overwritten only if the save operation is successful. /// See the following webpages for the valid values for per /// . ///
    From 2572f24e088ea3824b69f4afe8fea9adb445c3df Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Mon, 4 Mar 2024 22:46:38 +0900 Subject: [PATCH 46/57] 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) { From 6a0f7746252cea35f3313cac01ad2564c9d94198 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Tue, 5 Mar 2024 01:06:02 +0900 Subject: [PATCH 47/57] Add texture leak tracker --- .../Internal/DalamudConfiguration.cs | 3 + .../Windows/Data/Widgets/TexWidget.cs | 105 ++++- .../Internals/FontAtlasFactory.cs | 6 +- .../FileSystemSharedImmediateTexture.cs | 4 +- .../GamePathSharedImmediateTexture.cs | 4 +- .../ManifestResourceSharedImmediateTexture.cs | 4 +- .../Internal/TextureManager.BlameTracker.cs | 373 ++++++++++++++++++ .../TextureManager.FromExistingTexture.cs | 16 +- .../Textures/Internal/TextureManager.cs | 63 +-- .../Internal/TextureManagerPluginScoped.cs | 6 +- .../Textures/Internal/ViewportTextureWrap.cs | 22 +- Dalamud/Interface/UiBuilder.cs | 20 +- Dalamud/Plugin/DalamudPluginInterface.cs | 2 +- Dalamud/Utility/TerraFxCom/ManagedIStream.cs | 32 +- 14 files changed, 597 insertions(+), 63 deletions(-) create mode 100644 Dalamud/Interface/Textures/Internal/TextureManager.BlameTracker.cs diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 85a9507c9..6114c0f62 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -443,6 +443,9 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable ///
public double UiBuilderHitch { get; set; } = 100; + /// Gets or sets a value indicating whether to track texture allocation by plugins. + public bool UseTexturePluginTracking { get; set; } + /// /// Gets or sets the page of the plugin installer that is shown by default when opened. /// diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs index 092b3f74f..d14aeb5ab 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -6,6 +6,7 @@ using System.Reflection; using System.Runtime.Loader; using System.Threading.Tasks; +using Dalamud.Configuration.Internal; using Dalamud.Interface.Components; using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.Internal.Notifications; @@ -108,6 +109,11 @@ internal class TexWidget : IDataWindowWidget if (ImGui.Button("GC")) GC.Collect(); + ImGui.PushID("blames"); + if (ImGui.CollapsingHeader($"All Loaded Textures: {this.textureManager.AllBlamesForDebug.Count:g}###header")) + this.DrawBlame(this.textureManager.AllBlamesForDebug); + ImGui.PopID(); + ImGui.PushID("loadedGameTextures"); if (ImGui.CollapsingHeader( $"Loaded Game Textures: {this.textureManager.Shared.ForDebugGamePathTextures.Count:g}###header")) @@ -292,6 +298,96 @@ internal class TexWidget : IDataWindowWidget this.fileDialogManager.Draw(); } + private unsafe void DrawBlame(IReadOnlyList allBlames) + { + var conf = Service.Get(); + var im = Service.Get(); + var blame = conf.UseTexturePluginTracking; + if (ImGui.Checkbox("Enable", ref blame)) + { + conf.UseTexturePluginTracking = blame; + conf.QueueSave(); + } + + if (!ImGui.BeginTable("##table", 5)) + return; + + const int numIcons = 1; + float iconWidths; + using (im.IconFontHandle?.Push()) + iconWidths = ImGui.CalcTextSize(FontAwesomeIcon.Save.ToIconString()).X; + + ImGui.TableSetupScrollFreeze(0, 1); + ImGui.TableSetupColumn("Size", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("00000x00000").X); + ImGui.TableSetupColumn( + "Format", + ImGuiTableColumnFlags.WidthFixed, + ImGui.CalcTextSize("R32G32B32A32_TYPELESS").X); + ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn( + "Actions", + ImGuiTableColumnFlags.WidthFixed, + iconWidths + + (ImGui.GetStyle().FramePadding.X * 2 * numIcons) + + (ImGui.GetStyle().ItemSpacing.X * 1 * numIcons)); + ImGui.TableSetupColumn( + "Plugins", + ImGuiTableColumnFlags.WidthFixed, + ImGui.CalcTextSize("Aaaaaaaaaa Aaaaaaaaaa Aaaaaaaaaa").X); + ImGui.TableHeadersRow(); + + var clipper = new ImGuiListClipperPtr(ImGuiNative.ImGuiListClipper_ImGuiListClipper()); + clipper.Begin(allBlames.Count); + + while (clipper.Step()) + { + for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) + { + var wrap = allBlames[i]; + ImGui.TableNextRow(); + ImGui.PushID(i); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{wrap.Width}x{wrap.Height}"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted(Enum.GetName(wrap.Format)?[12..] ?? wrap.Format.ToString()); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted(wrap.Name); + + ImGui.TableNextColumn(); + if (ImGuiComponents.IconButton(FontAwesomeIcon.Save)) + { + this.SaveTextureAsync( + $"{wrap.ImGuiHandle:X16}", + () => Task.FromResult(wrap.CreateWrapSharingLowLevelResource())); + } + + if (ImGui.IsItemHovered()) + { + ImGui.BeginTooltip(); + ImGui.Image(wrap.ImGuiHandle, wrap.Size); + ImGui.EndTooltip(); + } + + ImGui.TableNextColumn(); + lock (wrap.OwnerPlugins) + { + foreach (var plugin in wrap.OwnerPlugins) + ImGui.TextUnformatted(plugin.Name); + } + + ImGui.PopID(); + } + } + + clipper.Destroy(); + ImGui.EndTable(); + + ImGuiHelpers.ScaledDummy(10); + } + private unsafe void DrawLoadedTextures(ICollection textures) { var im = Service.Get(); @@ -354,6 +450,7 @@ internal class TexWidget : IDataWindowWidget } var remain = texture.SelfReferenceExpiresInForDebug; + ImGui.PushID(row); ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); @@ -400,6 +497,8 @@ internal class TexWidget : IDataWindowWidget ImGui.SetTooltip("Release self-reference immediately."); if (remain <= 0) ImGui.EndDisabled(); + + ImGui.PopID(); } if (!valid) @@ -609,7 +708,8 @@ internal class TexWidget : IDataWindowWidget new() { Api10 = this.textureManager.CreateFromImGuiViewportAsync( - this.viewportTextureArgs with { ViewportId = viewports[this.viewportIndexInt].ID }), + this.viewportTextureArgs with { ViewportId = viewports[this.viewportIndexInt].ID }, + null), }); } } @@ -758,6 +858,9 @@ internal class TexWidget : IDataWindowWidget } catch (Exception e) { + if (e is OperationCanceledException) + return; + Log.Error(e, $"{nameof(TexWidget)}.{nameof(this.SaveTextureAsync)}"); Service.Get().AddNotification( $"Failed to save file: {e}", diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs index 39e969fb8..a9b393d3a 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs @@ -45,13 +45,11 @@ internal sealed partial class FontAtlasFactory DataManager dataManager, Framework framework, InterfaceManager interfaceManager, - DalamudAssetManager dalamudAssetManager, - TextureManager textureManager) + DalamudAssetManager dalamudAssetManager) { this.Framework = framework; this.InterfaceManager = interfaceManager; this.dalamudAssetManager = dalamudAssetManager; - this.TextureManager = textureManager; this.SceneTask = Service .GetAsync() .ContinueWith(r => r.Result.Manager.Scene); @@ -148,7 +146,7 @@ internal sealed partial class FontAtlasFactory /// /// Gets the service instance of . /// - public TextureManager TextureManager { get; } + public TextureManager TextureManager => Service.Get(); /// /// Gets the async task for inside . diff --git a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs index 4735c1af7..9e6af982d 100644 --- a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs +++ b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs @@ -28,6 +28,8 @@ internal sealed class FileSystemSharedImmediateTexture : SharedImmediateTexture protected override async Task CreateTextureAsync(CancellationToken cancellationToken) { var tm = await Service.GetAsync(); - return await tm.NoThrottleCreateFromFileAsync(this.path, cancellationToken); + var wrap = await tm.NoThrottleCreateFromFileAsync(this.path, cancellationToken); + tm.BlameSetName(wrap, this.ToString()); + return wrap; } } diff --git a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs index da3cc1a8d..e33091127 100644 --- a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs +++ b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs @@ -37,6 +37,8 @@ internal sealed class GamePathSharedImmediateTexture : SharedImmediateTexture if (dm.GetFile(substPath) is not { } file) throw new FileNotFoundException(); cancellationToken.ThrowIfCancellationRequested(); - return tm.NoThrottleCreateFromTexFile(file); + var wrap = tm.NoThrottleCreateFromTexFile(file); + tm.BlameSetName(wrap, this.ToString()); + return wrap; } } diff --git a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs index 00e2f34d5..525e25159 100644 --- a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs +++ b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs @@ -40,6 +40,8 @@ internal sealed class ManifestResourceSharedImmediateTexture : SharedImmediateTe var tm = await Service.GetAsync(); var ms = new MemoryStream(stream.CanSeek ? checked((int)stream.Length) : 0); await stream.CopyToAsync(ms, cancellationToken); - return tm.NoThrottleCreateFromImage(ms.GetBuffer().AsMemory(0, checked((int)ms.Length)), cancellationToken); + var wrap = tm.NoThrottleCreateFromImage(ms.GetBuffer().AsMemory(0, checked((int)ms.Length)), cancellationToken); + tm.BlameSetName(wrap, this.ToString()); + return wrap; } } diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.BlameTracker.cs b/Dalamud/Interface/Textures/Internal/TextureManager.BlameTracker.cs new file mode 100644 index 000000000..a401a9a73 --- /dev/null +++ b/Dalamud/Interface/Textures/Internal/TextureManager.BlameTracker.cs @@ -0,0 +1,373 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +using Dalamud.Interface.Internal; +using Dalamud.Plugin.Internal.Types; +using Dalamud.Plugin.Services; +using Dalamud.Storage.Assets; +using Dalamud.Utility; + +using TerraFX.Interop; +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +namespace Dalamud.Interface.Textures.Internal; + +/// Service responsible for loading and disposing ImGui texture wraps. +internal sealed partial class TextureManager +{ + private readonly List blameTracker = new(); + + /// A wrapper for underlying texture2D resources. + public interface IBlameableDalamudTextureWrap : IDalamudTextureWrap + { + /// Gets the name of the underlying resource of this texture wrap. + public string Name { get; } + + /// Gets the format of the texture. + public DXGI_FORMAT Format { get; } + + /// Gets the list of owner plugins. + public List OwnerPlugins { get; } + } + + /// Gets all the loaded textures from plugins. + /// The enumerable that goes through all textures and relevant plugins. + /// Returned value must be used inside a lock. + [SuppressMessage("ReSharper", "InconsistentlySynchronizedField", Justification = "Caller locks the return value.")] + public IReadOnlyList AllBlamesForDebug => this.blameTracker; + + /// Puts a plugin on blame for a texture. + /// The texture. + /// The plugin. + /// Same . + public unsafe IDalamudTextureWrap Blame(IDalamudTextureWrap textureWrap, LocalPlugin? ownerPlugin) + { + if (!this.dalamudConfiguration.UseTexturePluginTracking) + return textureWrap; + + using var wrapAux = new WrapAux(textureWrap, true); + var blame = BlameTag.From(wrapAux.ResPtr, out var isNew); + + if (ownerPlugin is not null) + { + lock (blame.OwnerPlugins) + blame.OwnerPlugins.Add(ownerPlugin); + } + + if (isNew) + { + lock (this.blameTracker) + this.blameTracker.Add(blame); + } + + return textureWrap; + } + + /// Sets the blame name for a texture. + /// The texture. + /// The name. + /// Same . + public unsafe IDalamudTextureWrap BlameSetName(IDalamudTextureWrap textureWrap, string name) + { + if (!this.dalamudConfiguration.UseTexturePluginTracking) + return textureWrap; + + using var wrapAux = new WrapAux(textureWrap, true); + var blame = BlameTag.From(wrapAux.ResPtr, out var isNew); + blame.Name = name; + + if (isNew) + { + lock (this.blameTracker) + this.blameTracker.Add(blame); + } + + return textureWrap; + } + + private void BlameTrackerUpdate(IFramework unused) + { + lock (this.blameTracker) + { + for (var i = 0; i < this.blameTracker.Count;) + { + var entry = this.blameTracker[i]; + if (entry.TestIsReleasedOrShouldRelease()) + { + this.blameTracker[i] = this.blameTracker[^1]; + this.blameTracker.RemoveAt(this.blameTracker.Count - 1); + } + else + { + ++i; + } + } + } + } + + /// A COM object that works by tagging itself to a DirectX resource. When the resource destructs, it will + /// also release our instance of the tag, letting us know that it is no longer being used, and can be evicted from + /// our tracker. + [Guid("2c3809e4-4f22-4c50-abde-4f22e5120875")] + private sealed unsafe class BlameTag : IUnknown.Interface, IRefCountable, IBlameableDalamudTextureWrap + { + private static readonly Guid MyGuid = typeof(BlameTag).GUID; + + private readonly nint[] comObject; + private readonly IUnknown.Vtbl vtbl; + private readonly D3D11_TEXTURE2D_DESC desc; + + private ID3D11Texture2D* tex2D; + private GCHandle gchThis; + private GCHandle gchComObject; + private GCHandle gchVtbl; + private int refCount; + + private ComPtr srvDebugPreview; + private long srvDebugPreviewExpiryTick; + + private BlameTag(IUnknown* trackWhat) + { + try + { + fixed (Guid* piid = &IID.IID_ID3D11Texture2D) + fixed (ID3D11Texture2D** ppTex2D = &this.tex2D) + trackWhat->QueryInterface(piid, (void**)ppTex2D).ThrowOnError(); + + fixed (D3D11_TEXTURE2D_DESC* pDesc = &this.desc) + this.tex2D->GetDesc(pDesc); + + this.comObject = new nint[2]; + + this.vtbl.QueryInterface = &QueryInterfaceStatic; + this.vtbl.AddRef = &AddRefStatic; + this.vtbl.Release = &ReleaseStatic; + + this.gchThis = GCHandle.Alloc(this); + this.gchVtbl = GCHandle.Alloc(this.vtbl, GCHandleType.Pinned); + this.gchComObject = GCHandle.Alloc(this.comObject, GCHandleType.Pinned); + this.comObject[0] = this.gchVtbl.AddrOfPinnedObject(); + this.comObject[1] = GCHandle.ToIntPtr(this.gchThis); + this.refCount = 1; + } + catch + { + this.refCount = 0; + if (this.gchComObject.IsAllocated) + this.gchComObject.Free(); + if (this.gchVtbl.IsAllocated) + this.gchVtbl.Free(); + if (this.gchThis.IsAllocated) + this.gchThis.Free(); + this.tex2D->Release(); + throw; + } + + try + { + fixed (Guid* pMyGuid = &MyGuid) + this.tex2D->SetPrivateDataInterface(pMyGuid, this).ThrowOnError(); + } + finally + { + // We don't own this. + this.tex2D->Release(); + + // If the try block above failed, then we will dispose ourselves right away. + // Otherwise, we are transferring our ownership to the device child tagging system. + this.Release(); + } + + return; + + [UnmanagedCallersOnly] + static int QueryInterfaceStatic(IUnknown* pThis, Guid* riid, void** ppvObject) => + ToManagedObject(pThis)?.QueryInterface(riid, ppvObject) ?? E.E_UNEXPECTED; + + [UnmanagedCallersOnly] + static uint AddRefStatic(IUnknown* pThis) => (uint)(ToManagedObject(pThis)?.AddRef() ?? 0); + + [UnmanagedCallersOnly] + static uint ReleaseStatic(IUnknown* pThis) => (uint)(ToManagedObject(pThis)?.Release() ?? 0); + } + + /// + public static Guid* NativeGuid => (Guid*)Unsafe.AsPointer(ref Unsafe.AsRef(in MyGuid)); + + /// + public List OwnerPlugins { get; } = new(); + + /// + public string Name { get; set; } = ""; + + /// + public DXGI_FORMAT Format => this.desc.Format; + + /// + public IntPtr ImGuiHandle + { + get + { + if (this.refCount == 0) + return Service.Get().Empty4X4.ImGuiHandle; + + this.srvDebugPreviewExpiryTick = Environment.TickCount64 + 1000; + if (!this.srvDebugPreview.IsEmpty()) + return (nint)this.srvDebugPreview.Get(); + var srvDesc = new D3D11_SHADER_RESOURCE_VIEW_DESC( + this.tex2D, + D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE2D); + + using var device = default(ComPtr); + this.tex2D->GetDevice(device.GetAddressOf()); + + using var srv = default(ComPtr); + if (device.Get()->CreateShaderResourceView((ID3D11Resource*)this.tex2D, &srvDesc, srv.GetAddressOf()) + .FAILED) + return Service.Get().Empty4X4.ImGuiHandle; + + srv.Swap(ref this.srvDebugPreview); + return (nint)this.srvDebugPreview.Get(); + } + } + + /// + public int Width => (int)this.desc.Width; + + /// + public int Height => (int)this.desc.Height; + + public static implicit operator IUnknown*(BlameTag bt) => (IUnknown*)bt.gchComObject.AddrOfPinnedObject(); + + /// Gets or creates an instance of for the given resource. + /// The COM object to track. + /// true if the tracker is new. + /// A COM object type. + /// A new instance of . + public static BlameTag From(T* trackWhat, out bool isNew) where T : unmanaged, IUnknown.Interface + { + using var deviceChild = default(ComPtr); + fixed (Guid* piid = &IID.IID_ID3D11DeviceChild) + trackWhat->QueryInterface(piid, (void**)deviceChild.GetAddressOf()).ThrowOnError(); + + fixed (Guid* pMyGuid = &MyGuid) + { + var dataSize = (uint)sizeof(nint); + IUnknown* existingTag; + if (deviceChild.Get()->GetPrivateData(pMyGuid, &dataSize, &existingTag).SUCCEEDED) + { + if (ToManagedObject(existingTag) is { } existingTagInstance) + { + existingTagInstance.Release(); + isNew = false; + return existingTagInstance; + } + } + } + + isNew = true; + return new((IUnknown*)trackWhat); + } + + /// Tests whether the tag and the underlying resource are released or should be released. + /// true if there are no more remaining references to this instance. + public bool TestIsReleasedOrShouldRelease() + { + if (this.srvDebugPreviewExpiryTick <= Environment.TickCount64) + this.srvDebugPreview.Reset(); + + return this.refCount == 0; + } + + /// + public HRESULT QueryInterface(Guid* riid, void** ppvObject) + { + if (ppvObject == null) + return E.E_POINTER; + + if (*riid == IID.IID_IUnknown || + *riid == MyGuid) + { + try + { + this.AddRef(); + } + catch + { + return E.E_FAIL; + } + + *ppvObject = (IUnknown*)this; + return S.S_OK; + } + + *ppvObject = null; + return E.E_NOINTERFACE; + } + + /// + public int AddRef() => IRefCountable.AlterRefCount(1, ref this.refCount, out var newRefCount) switch + { + IRefCountable.RefCountResult.StillAlive => newRefCount, + IRefCountable.RefCountResult.AlreadyDisposed => throw new ObjectDisposedException(nameof(BlameTag)), + 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.gchThis.Free(); + this.gchComObject.Free(); + this.gchVtbl.Free(); + return newRefCount; + + case IRefCountable.RefCountResult.AlreadyDisposed: + throw new ObjectDisposedException(nameof(BlameTag)); + + default: + throw new InvalidOperationException(); + } + } + + /// + uint IUnknown.Interface.AddRef() + { + try + { + return (uint)this.AddRef(); + } + catch + { + return 0; + } + } + + /// + uint IUnknown.Interface.Release() + { + this.srvDebugPreviewExpiryTick = 0; + try + { + return (uint)this.Release(); + } + catch + { + return 0; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static BlameTag? ToManagedObject(void* pThis) => + GCHandle.FromIntPtr(((nint*)pThis)[1]).Target as BlameTag; + } +} diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs b/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs index 3f0b69b96..41829f88c 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs @@ -3,6 +3,7 @@ using System.Threading; using System.Threading.Tasks; using Dalamud.Interface.Internal; +using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; using Dalamud.Utility; @@ -71,23 +72,34 @@ internal sealed partial class TextureManager var desc = default(D3D11_TEXTURE2D_DESC); tex.Get()->GetDesc(&desc); - return new UnknownTextureWrap( + + var outWrap = new UnknownTextureWrap( (IUnknown*)srv.Get(), (int)desc.Width, (int)desc.Height, true); + this.BlameSetName( + outWrap, + $"{nameof(this.CreateFromExistingTextureAsync)}({nameof(wrap)}, {nameof(args)}, {nameof(leaveWrapOpen)}, {nameof(cancellationToken)})"); + return outWrap; } }, cancellationToken, leaveWrapOpen ? null : wrap); /// + Task ITextureProvider.CreateFromImGuiViewportAsync( + ImGuiViewportTextureArgs args, + CancellationToken cancellationToken) => this.CreateFromImGuiViewportAsync(args, null, cancellationToken); + + /// public Task CreateFromImGuiViewportAsync( ImGuiViewportTextureArgs args, + LocalPlugin? ownerPlugin, CancellationToken cancellationToken = default) { args.ThrowOnInvalidValues(); - var t = new ViewportTextureWrap(args, cancellationToken); + var t = new ViewportTextureWrap(args, ownerPlugin, cancellationToken); t.QueueUpdate(); return t.FirstUpdateTask; } diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.cs b/Dalamud/Interface/Textures/Internal/TextureManager.cs index 25f3c634e..6d631a8ec 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Runtime.CompilerServices; using System.Threading; @@ -11,7 +9,6 @@ using Dalamud.Game; using Dalamud.Interface.Internal; using Dalamud.Interface.Textures.Internal.SharedImmediateTextures; using Dalamud.Logging.Internal; -using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; using Dalamud.Utility; @@ -64,6 +61,8 @@ internal sealed partial class TextureManager failsafe.Add(this.sharedTextureManager = new(this)); failsafe.Add(this.wicManager = new(this)); failsafe.Add(this.simpleDrawer = new()); + this.framework.Update += this.BlameTrackerUpdate; + failsafe.Add(() => this.framework.Update -= this.BlameTrackerUpdate); this.simpleDrawer.Setup(this.device.Get()); failsafe.Cancel(); @@ -116,21 +115,18 @@ internal sealed partial class TextureManager 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.DynamicPriorityTextureLoader.LoadAsync( null, - ct => Task.Run(() => this.NoThrottleCreateFromImage(bytes.ToArray(), ct), ct), + ct => Task.Run( + () => + this.BlameSetName( + this.NoThrottleCreateFromImage(bytes.ToArray(), ct), + $"{nameof(this.CreateFromImageAsync)}({nameof(bytes)}, {nameof(cancellationToken)})"), + ct), cancellationToken); /// @@ -144,7 +140,9 @@ internal sealed partial class TextureManager { 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); + return this.BlameSetName( + this.NoThrottleCreateFromImage(ms.GetBuffer(), ct), + $"{nameof(this.CreateFromImageAsync)}({nameof(stream)}, {nameof(leaveOpen)}, {nameof(cancellationToken)})"); }, cancellationToken, leaveOpen ? null : stream); @@ -153,7 +151,10 @@ internal sealed partial class TextureManager // It probably doesn't make sense to throttle this, as it copies the passed bytes to GPU without any transformation. public IDalamudTextureWrap CreateFromRaw( RawImageSpecification specs, - ReadOnlySpan bytes) => this.NoThrottleCreateFromRaw(specs, bytes); + ReadOnlySpan bytes) => + this.BlameSetName( + this.NoThrottleCreateFromRaw(specs, bytes), + $"{nameof(this.CreateFromRaw)}({nameof(specs)}, {nameof(bytes)})"); /// public Task CreateFromRawAsync( @@ -162,7 +163,10 @@ internal sealed partial class TextureManager CancellationToken cancellationToken = default) => this.DynamicPriorityTextureLoader.LoadAsync( null, - _ => Task.FromResult(this.NoThrottleCreateFromRaw(specs, bytes.Span)), + _ => Task.FromResult( + this.BlameSetName( + this.NoThrottleCreateFromRaw(specs, bytes.Span), + $"{nameof(this.CreateFromRawAsync)}({nameof(specs)}, {nameof(bytes)}, {nameof(cancellationToken)})")), cancellationToken); /// @@ -177,13 +181,18 @@ internal sealed partial class TextureManager { 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)); + return this.BlameSetName( + this.NoThrottleCreateFromRaw(specs, ms.GetBuffer().AsSpan(0, (int)ms.Length)), + $"{nameof(this.CreateFromRawAsync)}({nameof(specs)}, {nameof(stream)}, {nameof(leaveOpen)}, {nameof(cancellationToken)})"); }, cancellationToken, leaveOpen ? null : stream); /// - public IDalamudTextureWrap CreateFromTexFile(TexFile file) => this.CreateFromTexFileAsync(file).Result; + public IDalamudTextureWrap CreateFromTexFile(TexFile file) => + this.BlameSetName( + this.CreateFromTexFileAsync(file).Result, + $"{nameof(this.CreateFromTexFile)}({nameof(file)})"); /// public Task CreateFromTexFileAsync( @@ -191,7 +200,10 @@ internal sealed partial class TextureManager CancellationToken cancellationToken = default) => this.DynamicPriorityTextureLoader.LoadAsync( null, - _ => Task.FromResult(this.NoThrottleCreateFromTexFile(file)), + _ => Task.FromResult( + this.BlameSetName( + this.NoThrottleCreateFromTexFile(file), + $"{nameof(this.CreateFromTexFile)}({nameof(file)})")), cancellationToken); /// @@ -244,7 +256,9 @@ internal sealed partial class TextureManager this.device.Get()->CreateShaderResourceView((ID3D11Resource*)texture.Get(), &viewDesc, view.GetAddressOf()) .ThrowOnError(); - return new UnknownTextureWrap((IUnknown*)view.Get(), specs.Width, specs.Height, true); + var wrap = new UnknownTextureWrap((IUnknown*)view.Get(), specs.Width, specs.Height, true); + this.BlameSetName(wrap, $"{nameof(this.NoThrottleCreateFromRaw)}({nameof(specs)}, {nameof(bytes)})"); + return wrap; } /// Creates a texture from the given . Skips the load throttler; intended to be used @@ -264,9 +278,9 @@ internal sealed partial class TextureManager buffer = buffer.Filter(0, 0, TexFile.TextureFormat.B8G8R8A8); } - return this.NoThrottleCreateFromRaw( - new(buffer.Width, buffer.Height, dxgiFormat), - buffer.RawData); + var wrap = this.NoThrottleCreateFromRaw(new(buffer.Width, buffer.Height, dxgiFormat), buffer.RawData); + this.BlameSetName(wrap, $"{nameof(this.NoThrottleCreateFromTexFile)}({nameof(file)})"); + return wrap; } /// Creates a texture from the given , trying to interpret it as a @@ -289,7 +303,10 @@ internal sealed partial class TextureManager tf, new object?[] { new LuminaBinaryReader(bytesArray) }); // Note: FileInfo and FilePath are not used from TexFile; skip it. - return this.NoThrottleCreateFromTexFile(tf); + + var wrap = this.NoThrottleCreateFromTexFile(tf); + this.BlameSetName(wrap, $"{nameof(this.NoThrottleCreateFromTexFile)}({nameof(fileBytes)})"); + return wrap; } private void ReleaseUnmanagedResources() => this.device.Reset(); diff --git a/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs b/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs index 6ec346c30..8971409a7 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs @@ -40,8 +40,10 @@ internal sealed partial class TextureManagerPluginScoped private Task? managerTaskNullable; + /// Initializes a new instance of the class. + /// The plugin. [ServiceManager.ServiceConstructor] - private TextureManagerPluginScoped(LocalPlugin plugin) + public TextureManagerPluginScoped(LocalPlugin plugin) { this.plugin = plugin; if (plugin.Manifest is LocalPluginManifest lpm) @@ -155,7 +157,7 @@ internal sealed partial class TextureManagerPluginScoped CancellationToken cancellationToken = default) { var manager = await this.ManagerTask; - var textureWrap = await manager.CreateFromImGuiViewportAsync(args, cancellationToken); + var textureWrap = await manager.CreateFromImGuiViewportAsync(args, this.plugin, cancellationToken); manager.Blame(textureWrap, this.plugin); return textureWrap; } diff --git a/Dalamud/Interface/Textures/Internal/ViewportTextureWrap.cs b/Dalamud/Interface/Textures/Internal/ViewportTextureWrap.cs index a8692b323..daa247170 100644 --- a/Dalamud/Interface/Textures/Internal/ViewportTextureWrap.cs +++ b/Dalamud/Interface/Textures/Internal/ViewportTextureWrap.cs @@ -4,6 +4,8 @@ using System.Threading.Tasks; using Dalamud.Game; using Dalamud.Interface.Internal; +using Dalamud.Plugin.Internal.Types; +using Dalamud.Storage.Assets; using Dalamud.Utility; using ImGuiNET; @@ -18,6 +20,7 @@ namespace Dalamud.Interface.Textures.Internal; /// A texture wrap that takes its buffer from the frame buffer (of swap chain). internal sealed class ViewportTextureWrap : IDalamudTextureWrap, IDeferredDisposable { + private readonly LocalPlugin? ownerPlugin; private readonly CancellationToken cancellationToken; private readonly TaskCompletionSource firstUpdateTaskCompletionSource = new(); @@ -31,10 +34,13 @@ internal sealed class ViewportTextureWrap : IDalamudTextureWrap, IDeferredDispos /// Initializes a new instance of the class. /// The arguments for creating a texture. + /// The owner plugin. /// The cancellation token. - public ViewportTextureWrap(ImGuiViewportTextureArgs args, CancellationToken cancellationToken) + public ViewportTextureWrap( + ImGuiViewportTextureArgs args, LocalPlugin? ownerPlugin, CancellationToken cancellationToken) { this.args = args; + this.ownerPlugin = ownerPlugin; this.cancellationToken = cancellationToken; } @@ -42,7 +48,14 @@ internal sealed class ViewportTextureWrap : IDalamudTextureWrap, IDeferredDispos ~ViewportTextureWrap() => this.Dispose(false); /// - public unsafe nint ImGuiHandle => (nint)this.srv.Get(); + public unsafe nint ImGuiHandle + { + get + { + var t = (nint)this.srv.Get(); + return t == nint.Zero ? Service.Get().Empty4X4.ImGuiHandle : t; + } + } /// public int Width => (int)this.desc.Width; @@ -134,6 +147,11 @@ internal sealed class ViewportTextureWrap : IDalamudTextureWrap, IDeferredDispos srvTemp.Swap(ref this.srv); rtvTemp.Swap(ref this.rtv); texTemp.Swap(ref this.tex); + + Service.Get().Blame(this, this.ownerPlugin); + Service.Get().BlameSetName( + this, + $"{nameof(ViewportTextureWrap)}({this.args})"); } // context.Get()->CopyResource((ID3D11Resource*)this.tex.Get(), (ID3D11Resource*)backBuffer.Get()); diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 8874c85f0..3b6a754a9 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -42,6 +42,7 @@ public sealed class UiBuilder : IDisposable private readonly DalamudConfiguration configuration = Service.Get(); private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new(); + private readonly TextureManagerPluginScoped scopedTextureProvider; private bool hasErrorWindow = false; private bool lastFrameUiHideState = false; @@ -54,8 +55,9 @@ public sealed class UiBuilder : IDisposable /// Initializes a new instance of the class and registers it. /// You do not have to call this manually. /// + /// The plugin. /// The plugin namespace. - internal UiBuilder(string namespaceName) + internal UiBuilder(LocalPlugin plugin, string namespaceName) { try { @@ -69,6 +71,8 @@ public sealed class UiBuilder : IDisposable this.interfaceManager.ResizeBuffers += this.OnResizeBuffers; this.scopedFinalizer.Add(() => this.interfaceManager.ResizeBuffers -= this.OnResizeBuffers); + this.scopedFinalizer.Add(this.scopedTextureProvider = new(plugin)); + this.FontAtlas = this.scopedFinalizer .Add( @@ -381,8 +385,6 @@ public sealed class UiBuilder : IDisposable private Task InterfaceManagerWithSceneAsync => Service.GetAsync().ContinueWith(task => task.Result.Manager); - private ITextureProvider TextureProvider => Service.Get(); - /// /// Loads an image from the specified file. /// @@ -391,7 +393,7 @@ public sealed class UiBuilder : IDisposable [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] [Obsolete($"Use {nameof(ITextureProvider.GetFromFile)}.")] public IDalamudTextureWrap LoadImage(string filePath) => - this.TextureProvider.GetFromFile(filePath).RentAsync().Result; + this.scopedTextureProvider.GetFromFile(filePath).RentAsync().Result; /// /// Loads an image from a byte stream, such as a png downloaded into memory. @@ -401,7 +403,7 @@ public sealed class UiBuilder : IDisposable [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] [Obsolete($"Use {nameof(ITextureProvider.CreateFromImageAsync)}.")] public IDalamudTextureWrap LoadImage(byte[] imageData) => - this.TextureProvider.CreateFromImageAsync(imageData).Result; + this.scopedTextureProvider.CreateFromImageAsync(imageData).Result; /// /// Loads an image from raw unformatted pixel data, with no type or header information. To load formatted data, use . @@ -416,7 +418,7 @@ public sealed class UiBuilder : IDisposable public IDalamudTextureWrap LoadImageRaw(byte[] imageData, int width, int height, int numChannels) => numChannels switch { - 4 => this.TextureProvider.CreateFromRaw(RawImageSpecification.Rgba32(width, height), imageData), + 4 => this.scopedTextureProvider.CreateFromRaw(RawImageSpecification.Rgba32(width, height), imageData), _ => throw new NotSupportedException(), }; @@ -436,7 +438,7 @@ public sealed class UiBuilder : IDisposable [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] [Obsolete($"Use {nameof(ITextureProvider.GetFromFile)}.")] public Task LoadImageAsync(string filePath) => - this.TextureProvider.GetFromFile(filePath).RentAsync(); + this.scopedTextureProvider.GetFromFile(filePath).RentAsync(); /// /// Asynchronously loads an image from a byte stream, such as a png downloaded into memory, when it's possible to do so. @@ -446,7 +448,7 @@ public sealed class UiBuilder : IDisposable [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] [Obsolete($"Use {nameof(ITextureProvider.CreateFromImageAsync)}.")] public Task LoadImageAsync(byte[] imageData) => - this.TextureProvider.CreateFromImageAsync(imageData); + this.scopedTextureProvider.CreateFromImageAsync(imageData); /// /// Asynchronously loads an image from raw unformatted pixel data, with no type or header information, when it's possible to do so. To load formatted data, use . @@ -461,7 +463,7 @@ public sealed class UiBuilder : IDisposable public Task LoadImageRawAsync(byte[] imageData, int width, int height, int numChannels) => numChannels switch { - 4 => this.TextureProvider.CreateFromRawAsync(RawImageSpecification.Rgba32(width, height), imageData), + 4 => this.scopedTextureProvider.CreateFromRawAsync(RawImageSpecification.Rgba32(width, height), imageData), _ => Task.FromException(new NotSupportedException()), }; diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index 82f19aa49..2b184288d 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -52,7 +52,7 @@ public sealed class DalamudPluginInterface : IDisposable var dataManager = Service.Get(); var localization = Service.Get(); - this.UiBuilder = new UiBuilder(plugin.Name); + this.UiBuilder = new(plugin, plugin.Name); this.configs = Service.Get().PluginConfigs; this.Reason = reason; diff --git a/Dalamud/Utility/TerraFxCom/ManagedIStream.cs b/Dalamud/Utility/TerraFxCom/ManagedIStream.cs index 942a9baf3..caec65da2 100644 --- a/Dalamud/Utility/TerraFxCom/ManagedIStream.cs +++ b/Dalamud/Utility/TerraFxCom/ManagedIStream.cs @@ -54,64 +54,64 @@ internal sealed unsafe class ManagedIStream : IStream.Interface, IRefCountable return; [MethodImpl(MethodImplOptions.AggressiveInlining)] - static IStream.Interface? ToManagedObject(void* pThis) => - GCHandle.FromIntPtr(((nint*)pThis)[1]).Target as IStream.Interface; + static ManagedIStream? ToManagedObject(void* pThis) => + GCHandle.FromIntPtr(((nint*)pThis)[1]).Target as ManagedIStream; [UnmanagedCallersOnly] static int QueryInterfaceStatic(IStream* pThis, Guid* riid, void** ppvObject) => - ToManagedObject(pThis)?.QueryInterface(riid, ppvObject) ?? E.E_FAIL; + ToManagedObject(pThis)?.QueryInterface(riid, ppvObject) ?? E.E_UNEXPECTED; [UnmanagedCallersOnly] - static uint AddRefStatic(IStream* pThis) => ToManagedObject(pThis)?.AddRef() ?? 0; + static uint AddRefStatic(IStream* pThis) => (uint)(ToManagedObject(pThis)?.AddRef() ?? 0); [UnmanagedCallersOnly] - static uint ReleaseStatic(IStream* pThis) => ToManagedObject(pThis)?.Release() ?? 0; + static uint ReleaseStatic(IStream* pThis) => (uint)(ToManagedObject(pThis)?.Release() ?? 0); [UnmanagedCallersOnly] static int ReadStatic(IStream* pThis, void* pv, uint cb, uint* pcbRead) => - ToManagedObject(pThis)?.Read(pv, cb, pcbRead) ?? E.E_FAIL; + ToManagedObject(pThis)?.Read(pv, cb, pcbRead) ?? E.E_UNEXPECTED; [UnmanagedCallersOnly] static int WriteStatic(IStream* pThis, void* pv, uint cb, uint* pcbWritten) => - ToManagedObject(pThis)?.Write(pv, cb, pcbWritten) ?? E.E_FAIL; + ToManagedObject(pThis)?.Write(pv, cb, pcbWritten) ?? E.E_UNEXPECTED; [UnmanagedCallersOnly] static int SeekStatic( IStream* pThis, LARGE_INTEGER dlibMove, uint dwOrigin, ULARGE_INTEGER* plibNewPosition) => - ToManagedObject(pThis)?.Seek(dlibMove, dwOrigin, plibNewPosition) ?? E.E_FAIL; + ToManagedObject(pThis)?.Seek(dlibMove, dwOrigin, plibNewPosition) ?? E.E_UNEXPECTED; [UnmanagedCallersOnly] static int SetSizeStatic(IStream* pThis, ULARGE_INTEGER libNewSize) => - ToManagedObject(pThis)?.SetSize(libNewSize) ?? E.E_FAIL; + ToManagedObject(pThis)?.SetSize(libNewSize) ?? E.E_UNEXPECTED; [UnmanagedCallersOnly] static int CopyToStatic( IStream* pThis, IStream* pstm, ULARGE_INTEGER cb, ULARGE_INTEGER* pcbRead, ULARGE_INTEGER* pcbWritten) => - ToManagedObject(pThis)?.CopyTo(pstm, cb, pcbRead, pcbWritten) ?? E.E_FAIL; + ToManagedObject(pThis)?.CopyTo(pstm, cb, pcbRead, pcbWritten) ?? E.E_UNEXPECTED; [UnmanagedCallersOnly] static int CommitStatic(IStream* pThis, uint grfCommitFlags) => - ToManagedObject(pThis)?.Commit(grfCommitFlags) ?? E.E_FAIL; + ToManagedObject(pThis)?.Commit(grfCommitFlags) ?? E.E_UNEXPECTED; [UnmanagedCallersOnly] - static int RevertStatic(IStream* pThis) => ToManagedObject(pThis)?.Revert() ?? E.E_FAIL; + static int RevertStatic(IStream* pThis) => ToManagedObject(pThis)?.Revert() ?? E.E_UNEXPECTED; [UnmanagedCallersOnly] static int LockRegionStatic(IStream* pThis, ULARGE_INTEGER libOffset, ULARGE_INTEGER cb, uint dwLockType) => - ToManagedObject(pThis)?.LockRegion(libOffset, cb, dwLockType) ?? E.E_FAIL; + ToManagedObject(pThis)?.LockRegion(libOffset, cb, dwLockType) ?? E.E_UNEXPECTED; [UnmanagedCallersOnly] static int UnlockRegionStatic( IStream* pThis, ULARGE_INTEGER libOffset, ULARGE_INTEGER cb, uint dwLockType) => - ToManagedObject(pThis)?.UnlockRegion(libOffset, cb, dwLockType) ?? E.E_FAIL; + ToManagedObject(pThis)?.UnlockRegion(libOffset, cb, dwLockType) ?? E.E_UNEXPECTED; [UnmanagedCallersOnly] static int StatStatic(IStream* pThis, STATSTG* pstatstg, uint grfStatFlag) => - ToManagedObject(pThis)?.Stat(pstatstg, grfStatFlag) ?? E.E_FAIL; + ToManagedObject(pThis)?.Stat(pstatstg, grfStatFlag) ?? E.E_UNEXPECTED; [UnmanagedCallersOnly] - static int CloneStatic(IStream* pThis, IStream** ppstm) => ToManagedObject(pThis)?.Clone(ppstm) ?? E.E_FAIL; + static int CloneStatic(IStream* pThis, IStream** ppstm) => ToManagedObject(pThis)?.Clone(ppstm) ?? E.E_UNEXPECTED; } /// From 7f12e3f3dab3f8558504589b05b097c62c614ff7 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Tue, 5 Mar 2024 01:22:16 +0900 Subject: [PATCH 48/57] Assign debug names for textures --- .../Internal/Windows/PluginImageCache.cs | 4 ++- .../FontAtlasFactory.BuildToolkit.cs | 12 +++++-- .../Internals/FontAtlasFactory.cs | 3 +- .../TextureManager.FromExistingTexture.cs | 9 ++++-- .../Textures/Internal/TextureManager.cs | 16 ++++++++-- .../Internal/TextureManagerPluginScoped.cs | 25 ++++++++++----- .../Textures/Internal/ViewportTextureWrap.cs | 7 +++-- Dalamud/Interface/UldWrapper.cs | 5 ++- Dalamud/Plugin/Services/ITextureProvider.cs | 31 ++++++++++++++++--- Dalamud/Storage/Assets/DalamudAssetManager.cs | 23 ++++++++------ 10 files changed, 100 insertions(+), 35 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs index 1042b0741..078b4cc8d 100644 --- a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs +++ b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs @@ -276,7 +276,9 @@ internal class PluginImageCache : IDisposable, IServiceType // FIXME(goat): This is a hack around this call failing randomly in certain situations. Might be related to not being called on the main thread. try { - image = await textureManager.CreateFromImageAsync(bytes); + image = await textureManager.CreateFromImageAsync( + bytes, + $"{nameof(PluginImageCache)}({name} for {manifest.InternalName} at {loc})"); } catch (Exception ex) { diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs index 811937949..f091e3164 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs @@ -586,8 +586,12 @@ internal sealed partial class FontAtlasFactory var bpp = use4 ? 2 : 4; var width = this.NewImAtlas.TexWidth; var height = this.NewImAtlas.TexHeight; - foreach (ref var texture in this.data.ImTextures.DataSpan) + var textureSpan = this.data.ImTextures.DataSpan; + for (var i = 0; i < textureSpan.Length; i++) { + ref var texture = ref textureSpan[i]; + var name = + $"FontAtlas.{ this.data.Owner?.Name ?? "(no owner or name)"}[0x{(long)this.data.Atlas.NativePtr:X}][{i}]"; if (texture.TexID != 0) { // Nothing to do @@ -596,7 +600,8 @@ internal sealed partial class FontAtlasFactory { var wrap = this.factory.TextureManager.CreateFromRaw( RawImageSpecification.Rgba32(width, height), - new(texture.TexPixelsRGBA32, width * height * 4)); + new(texture.TexPixelsRGBA32, width * height * 4), + name); this.data.AddExistingTexture(wrap); texture.TexID = wrap.ImGuiHandle; } @@ -640,7 +645,8 @@ internal sealed partial class FontAtlasFactory height, (int)(use4 ? Format.B4G4R4A4_UNorm : Format.B8G8R8A8_UNorm), width * bpp), - buf); + buf, + name); this.data.AddExistingTexture(wrap); texture.TexID = wrap.ImGuiHandle; continue; diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs index a9b393d3a..59df710d6 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs @@ -362,7 +362,8 @@ internal sealed partial class FontAtlasFactory ? DXGI_FORMAT.DXGI_FORMAT_B4G4R4A4_UNORM : DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM), texFile.Header.Width * bpp), - buffer)); + buffer, + $"{nameof(FontAtlasFactory)}:{texPathFormat.Format(fileIndex)}:{channelIndex}")); } finally { diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs b/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs index 41829f88c..9d10457dc 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs @@ -49,6 +49,7 @@ internal sealed partial class TextureManager IDalamudTextureWrap wrap, TextureModificationArgs args = default, bool leaveWrapOpen = false, + string? debugName = null, CancellationToken cancellationToken = default) => this.DynamicPriorityTextureLoader.LoadAsync( null, @@ -80,6 +81,7 @@ internal sealed partial class TextureManager true); this.BlameSetName( outWrap, + debugName ?? $"{nameof(this.CreateFromExistingTextureAsync)}({nameof(wrap)}, {nameof(args)}, {nameof(leaveWrapOpen)}, {nameof(cancellationToken)})"); return outWrap; } @@ -90,16 +92,19 @@ internal sealed partial class TextureManager /// Task ITextureProvider.CreateFromImGuiViewportAsync( ImGuiViewportTextureArgs args, - CancellationToken cancellationToken) => this.CreateFromImGuiViewportAsync(args, null, cancellationToken); + string? debugName, + CancellationToken cancellationToken) => + this.CreateFromImGuiViewportAsync(args, null, debugName, cancellationToken); /// public Task CreateFromImGuiViewportAsync( ImGuiViewportTextureArgs args, LocalPlugin? ownerPlugin, + string? debugName = null, CancellationToken cancellationToken = default) { args.ThrowOnInvalidValues(); - var t = new ViewportTextureWrap(args, ownerPlugin, cancellationToken); + var t = new ViewportTextureWrap(args, debugName, ownerPlugin, cancellationToken); t.QueueUpdate(); return t.FirstUpdateTask; } diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.cs b/Dalamud/Interface/Textures/Internal/TextureManager.cs index 6d631a8ec..8fa9efa25 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.cs @@ -118,6 +118,7 @@ internal sealed partial class TextureManager /// public Task CreateFromImageAsync( ReadOnlyMemory bytes, + string? debugName = null, CancellationToken cancellationToken = default) => this.DynamicPriorityTextureLoader.LoadAsync( null, @@ -125,6 +126,7 @@ internal sealed partial class TextureManager () => this.BlameSetName( this.NoThrottleCreateFromImage(bytes.ToArray(), ct), + debugName ?? $"{nameof(this.CreateFromImageAsync)}({nameof(bytes)}, {nameof(cancellationToken)})"), ct), cancellationToken); @@ -133,6 +135,7 @@ internal sealed partial class TextureManager public Task CreateFromImageAsync( Stream stream, bool leaveOpen = false, + string? debugName = null, CancellationToken cancellationToken = default) => this.DynamicPriorityTextureLoader.LoadAsync( null, @@ -142,6 +145,7 @@ internal sealed partial class TextureManager await stream.CopyToAsync(ms, ct).ConfigureAwait(false); return this.BlameSetName( this.NoThrottleCreateFromImage(ms.GetBuffer(), ct), + debugName ?? $"{nameof(this.CreateFromImageAsync)}({nameof(stream)}, {nameof(leaveOpen)}, {nameof(cancellationToken)})"); }, cancellationToken, @@ -151,21 +155,24 @@ internal sealed partial class TextureManager // It probably doesn't make sense to throttle this, as it copies the passed bytes to GPU without any transformation. public IDalamudTextureWrap CreateFromRaw( RawImageSpecification specs, - ReadOnlySpan bytes) => + ReadOnlySpan bytes, + string? debugName = null) => this.BlameSetName( this.NoThrottleCreateFromRaw(specs, bytes), - $"{nameof(this.CreateFromRaw)}({nameof(specs)}, {nameof(bytes)})"); + debugName ?? $"{nameof(this.CreateFromRaw)}({nameof(specs)}, {nameof(bytes)})"); /// public Task CreateFromRawAsync( RawImageSpecification specs, ReadOnlyMemory bytes, + string? debugName = null, CancellationToken cancellationToken = default) => this.DynamicPriorityTextureLoader.LoadAsync( null, _ => Task.FromResult( this.BlameSetName( this.NoThrottleCreateFromRaw(specs, bytes.Span), + debugName ?? $"{nameof(this.CreateFromRawAsync)}({nameof(specs)}, {nameof(bytes)}, {nameof(cancellationToken)})")), cancellationToken); @@ -174,6 +181,7 @@ internal sealed partial class TextureManager RawImageSpecification specs, Stream stream, bool leaveOpen = false, + string? debugName = null, CancellationToken cancellationToken = default) => this.DynamicPriorityTextureLoader.LoadAsync( null, @@ -183,6 +191,7 @@ internal sealed partial class TextureManager await stream.CopyToAsync(ms, ct).ConfigureAwait(false); return this.BlameSetName( this.NoThrottleCreateFromRaw(specs, ms.GetBuffer().AsSpan(0, (int)ms.Length)), + debugName ?? $"{nameof(this.CreateFromRawAsync)}({nameof(specs)}, {nameof(stream)}, {nameof(leaveOpen)}, {nameof(cancellationToken)})"); }, cancellationToken, @@ -197,13 +206,14 @@ internal sealed partial class TextureManager /// public Task CreateFromTexFileAsync( TexFile file, + string? debugName = null, CancellationToken cancellationToken = default) => this.DynamicPriorityTextureLoader.LoadAsync( null, _ => Task.FromResult( this.BlameSetName( this.NoThrottleCreateFromTexFile(file), - $"{nameof(this.CreateFromTexFile)}({nameof(file)})")), + debugName ?? $"{nameof(this.CreateFromTexFile)}({nameof(file)})")), cancellationToken); /// diff --git a/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs b/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs index 8971409a7..39b91edda 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs @@ -139,6 +139,7 @@ internal sealed partial class TextureManagerPluginScoped IDalamudTextureWrap wrap, TextureModificationArgs args = default, bool leaveWrapOpen = false, + string? debugName = null, CancellationToken cancellationToken = default) { var manager = await this.ManagerTask; @@ -146,6 +147,7 @@ internal sealed partial class TextureManagerPluginScoped wrap, args, leaveWrapOpen, + debugName, cancellationToken); manager.Blame(textureWrap, this.plugin); return textureWrap; @@ -154,10 +156,11 @@ internal sealed partial class TextureManagerPluginScoped /// public async Task CreateFromImGuiViewportAsync( ImGuiViewportTextureArgs args, + string? debugName = null, CancellationToken cancellationToken = default) { var manager = await this.ManagerTask; - var textureWrap = await manager.CreateFromImGuiViewportAsync(args, this.plugin, cancellationToken); + var textureWrap = await manager.CreateFromImGuiViewportAsync(args, this.plugin, debugName, cancellationToken); manager.Blame(textureWrap, this.plugin); return textureWrap; } @@ -165,10 +168,11 @@ internal sealed partial class TextureManagerPluginScoped /// public async Task CreateFromImageAsync( ReadOnlyMemory bytes, + string? debugName = null, CancellationToken cancellationToken = default) { var manager = await this.ManagerTask; - var textureWrap = await manager.CreateFromImageAsync(bytes, cancellationToken); + var textureWrap = await manager.CreateFromImageAsync(bytes, debugName, cancellationToken); manager.Blame(textureWrap, this.plugin); return textureWrap; } @@ -177,10 +181,11 @@ internal sealed partial class TextureManagerPluginScoped public async Task CreateFromImageAsync( Stream stream, bool leaveOpen = false, + string? debugName = null, CancellationToken cancellationToken = default) { var manager = await this.ManagerTask; - var textureWrap = await manager.CreateFromImageAsync(stream, leaveOpen, cancellationToken); + var textureWrap = await manager.CreateFromImageAsync(stream, leaveOpen, debugName, cancellationToken); manager.Blame(textureWrap, this.plugin); return textureWrap; } @@ -188,10 +193,11 @@ internal sealed partial class TextureManagerPluginScoped /// public IDalamudTextureWrap CreateFromRaw( RawImageSpecification specs, - ReadOnlySpan bytes) + ReadOnlySpan bytes, + string? debugName = null) { var manager = this.ManagerOrThrow; - var textureWrap = manager.CreateFromRaw(specs, bytes); + var textureWrap = manager.CreateFromRaw(specs, bytes, debugName); manager.Blame(textureWrap, this.plugin); return textureWrap; } @@ -200,10 +206,11 @@ internal sealed partial class TextureManagerPluginScoped public async Task CreateFromRawAsync( RawImageSpecification specs, ReadOnlyMemory bytes, + string? debugName = null, CancellationToken cancellationToken = default) { var manager = await this.ManagerTask; - var textureWrap = await manager.CreateFromRawAsync(specs, bytes, cancellationToken); + var textureWrap = await manager.CreateFromRawAsync(specs, bytes, debugName, cancellationToken); manager.Blame(textureWrap, this.plugin); return textureWrap; } @@ -213,10 +220,11 @@ internal sealed partial class TextureManagerPluginScoped RawImageSpecification specs, Stream stream, bool leaveOpen = false, + string? debugName = null, CancellationToken cancellationToken = default) { var manager = await this.ManagerTask; - var textureWrap = await manager.CreateFromRawAsync(specs, stream, leaveOpen, cancellationToken); + var textureWrap = await manager.CreateFromRawAsync(specs, stream, leaveOpen, debugName, cancellationToken); manager.Blame(textureWrap, this.plugin); return textureWrap; } @@ -233,10 +241,11 @@ internal sealed partial class TextureManagerPluginScoped /// public async Task CreateFromTexFileAsync( TexFile file, + string? debugName = null, CancellationToken cancellationToken = default) { var manager = await this.ManagerTask; - var textureWrap = await manager.CreateFromTexFileAsync(file, cancellationToken); + var textureWrap = await manager.CreateFromTexFileAsync(file, debugName, cancellationToken); manager.Blame(textureWrap, this.plugin); return textureWrap; } diff --git a/Dalamud/Interface/Textures/Internal/ViewportTextureWrap.cs b/Dalamud/Interface/Textures/Internal/ViewportTextureWrap.cs index daa247170..77ddc2e34 100644 --- a/Dalamud/Interface/Textures/Internal/ViewportTextureWrap.cs +++ b/Dalamud/Interface/Textures/Internal/ViewportTextureWrap.cs @@ -20,6 +20,7 @@ namespace Dalamud.Interface.Textures.Internal; /// A texture wrap that takes its buffer from the frame buffer (of swap chain). internal sealed class ViewportTextureWrap : IDalamudTextureWrap, IDeferredDisposable { + private readonly string? debugName; private readonly LocalPlugin? ownerPlugin; private readonly CancellationToken cancellationToken; private readonly TaskCompletionSource firstUpdateTaskCompletionSource = new(); @@ -34,12 +35,14 @@ internal sealed class ViewportTextureWrap : IDalamudTextureWrap, IDeferredDispos /// Initializes a new instance of the class. /// The arguments for creating a texture. + /// Name for debug display purposes. /// The owner plugin. /// The cancellation token. public ViewportTextureWrap( - ImGuiViewportTextureArgs args, LocalPlugin? ownerPlugin, CancellationToken cancellationToken) + ImGuiViewportTextureArgs args, string? debugName, LocalPlugin? ownerPlugin, CancellationToken cancellationToken) { this.args = args; + this.debugName = debugName; this.ownerPlugin = ownerPlugin; this.cancellationToken = cancellationToken; } @@ -151,7 +154,7 @@ internal sealed class ViewportTextureWrap : IDalamudTextureWrap, IDeferredDispos Service.Get().Blame(this, this.ownerPlugin); Service.Get().BlameSetName( this, - $"{nameof(ViewportTextureWrap)}({this.args})"); + this.debugName ?? $"{nameof(ViewportTextureWrap)}({this.args})"); } // context.Get()->CopyResource((ID3D11Resource*)this.tex.Get(), (ID3D11Resource*)backBuffer.Get()); diff --git a/Dalamud/Interface/UldWrapper.cs b/Dalamud/Interface/UldWrapper.cs index f70dd88e4..35330c5d0 100644 --- a/Dalamud/Interface/UldWrapper.cs +++ b/Dalamud/Interface/UldWrapper.cs @@ -126,7 +126,10 @@ public class UldWrapper : IDisposable inputSlice.CopyTo(outputSlice); } - return this.textureManager.CreateFromRaw(RawImageSpecification.Rgba32(part.W, part.H), imageData); + return this.textureManager.CreateFromRaw( + RawImageSpecification.Rgba32(part.W, part.H), + imageData, + $"{nameof(UldWrapper)}({this.Uld?.FilePath.Path}: {part.TextureId})"); } private (uint Id, int Width, int Height, bool HD, byte[] RgbaData)? GetTexture(string texturePath) diff --git a/Dalamud/Plugin/Services/ITextureProvider.cs b/Dalamud/Plugin/Services/ITextureProvider.cs index 59c9af2fc..61f400a0e 100644 --- a/Dalamud/Plugin/Services/ITextureProvider.cs +++ b/Dalamud/Plugin/Services/ITextureProvider.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Dalamud.Interface; using Dalamud.Interface.Internal; +using Dalamud.Interface.Internal.Windows.Data.Widgets; using Dalamud.Interface.Textures; using Lumina.Data.Files; @@ -16,6 +17,10 @@ namespace Dalamud.Plugin.Services; /// Service that grants you access to textures you may render via ImGui. /// /// +/// Create functions will return a new texture, and the returned instance of +/// must be disposed after use. +/// +/// /// Get functions will return a shared texture, and the returnd instance of /// do not require calling , unless a new reference has been created by calling /// .
@@ -23,8 +28,8 @@ namespace Dalamud.Plugin.Services; /// that will stay valid for the rest of the frame. ///
/// -/// Create functions will return a new texture, and the returned instance of -/// must be disposed after use. +/// debugName parameter can be used to name your textures, to aid debugging resource leaks using +/// . /// ///
public partial interface ITextureProvider @@ -36,6 +41,7 @@ public partial interface ITextureProvider /// The texture modification arguments. /// Whether to leave non-disposed when the returned /// completes. + /// Name for debug display purposes. /// The cancellation token. /// A containing the copied texture on success. Dispose after use. /// This function may throw an exception. @@ -43,10 +49,12 @@ public partial interface ITextureProvider IDalamudTextureWrap wrap, TextureModificationArgs args = default, bool leaveWrapOpen = false, + string? debugName = null, CancellationToken cancellationToken = default); /// Creates a texture from an ImGui viewport. /// The arguments for creating a texture. + /// Name for debug display purposes. /// The cancellation token. /// A containing the copied texture on success. Dispose after use. /// @@ -55,22 +63,26 @@ public partial interface ITextureProvider /// Task CreateFromImGuiViewportAsync( ImGuiViewportTextureArgs args, + string? debugName = null, CancellationToken cancellationToken = default); /// Gets a texture from the given bytes, trying to interpret it as a .tex file or other well-known image /// files, such as .png. /// The bytes to load. + /// Name for debug display purposes. /// The cancellation token. /// A containing the loaded texture on success. Dispose after use. /// This function may throw an exception. Task CreateFromImageAsync( ReadOnlyMemory bytes, + string? debugName = null, CancellationToken cancellationToken = default); /// Gets a texture from the given stream, trying to interpret it as a .tex file or other well-known image /// files, such as .png. /// The stream to load data from. /// Whether to leave the stream open once the task completes, sucessfully or not. + /// Name for debug display purposes. /// The cancellation token. /// A containing the loaded texture on success. Dispose after use. /// @@ -80,32 +92,38 @@ public partial interface ITextureProvider Task CreateFromImageAsync( Stream stream, bool leaveOpen = false, + string? debugName = null, CancellationToken cancellationToken = default); /// Gets a texture from the given bytes, interpreting it as a raw bitmap. /// The specifications for the raw bitmap. /// The bytes to load. + /// Name for debug display purposes. /// The texture loaded from the supplied raw bitmap. Dispose after use. /// This function may throw an exception. IDalamudTextureWrap CreateFromRaw( RawImageSpecification specs, - ReadOnlySpan bytes); + ReadOnlySpan bytes, + string? debugName = null); /// Gets a texture from the given bytes, interpreting it as a raw bitmap. /// The specifications for the raw bitmap. /// The bytes to load. + /// Name for debug display purposes. /// The cancellation token. /// A containing the loaded texture on success. Dispose after use. /// This function may throw an exception. Task CreateFromRawAsync( RawImageSpecification specs, ReadOnlyMemory bytes, + string? debugName = null, CancellationToken cancellationToken = default); /// Gets a texture from the given stream, interpreting the read data as a raw bitmap. /// The specifications for the raw bitmap. /// The stream to load data from. /// Whether to leave the stream open once the task completes, sucessfully or not. + /// Name for debug display purposes. /// The cancellation token. /// A containing the loaded texture on success. Dispose after use. /// @@ -117,6 +135,7 @@ public partial interface ITextureProvider RawImageSpecification specs, Stream stream, bool leaveOpen = false, + string? debugName = null, CancellationToken cancellationToken = default); /// @@ -130,11 +149,13 @@ public partial interface ITextureProvider /// Get a texture handle for the specified Lumina . /// The texture to obtain a handle to. + /// Name for debug display purposes. /// The cancellation token. /// A texture wrap that can be used to render the texture. Dispose after use. /// This function may throw an exception. Task CreateFromTexFileAsync( TexFile file, + string? debugName = null, CancellationToken cancellationToken = default); /// Gets the supported bitmap decoders. @@ -144,8 +165,8 @@ public partial interface ITextureProvider ///
    ///
  • ///
  • - ///
  • - ///
  • + ///
  • + ///
  • ///
/// This function may throw an exception. /// diff --git a/Dalamud/Storage/Assets/DalamudAssetManager.cs b/Dalamud/Storage/Assets/DalamudAssetManager.cs index e51788c0b..a8566d5eb 100644 --- a/Dalamud/Storage/Assets/DalamudAssetManager.cs +++ b/Dalamud/Storage/Assets/DalamudAssetManager.cs @@ -58,7 +58,7 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA this.fileStreams = Enum.GetValues().ToDictionary(x => x, _ => (Task?)null); this.textureWraps = Enum.GetValues().ToDictionary(x => x, _ => (Task?)null); - + // Block until all the required assets to be ready. var loadTimings = Timings.Start("DAM LoadAll"); registerStartupBlocker( @@ -72,11 +72,11 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA "Prevent Dalamud from loading more stuff, until we've ensured that all required assets are available."); Task.WhenAll( - Enum.GetValues() - .Where(x => x is not DalamudAsset.Empty4X4) - .Where(x => x.GetAttribute()?.Required is false) - .Select(this.CreateStreamAsync) - .Select(x => x.ToContentDisposedTask())) + Enum.GetValues() + .Where(x => x is not DalamudAsset.Empty4X4) + .Where(x => x.GetAttribute()?.Required is false) + .Select(this.CreateStreamAsync) + .Select(x => x.ToContentDisposedTask())) .ContinueWith(r => Log.Verbose($"Optional assets load state: {r}")); } @@ -206,7 +206,7 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA this.cancellationTokenSource.Token); } - for (var j = RenameAttemptCount; ; j--) + for (var j = RenameAttemptCount;; j--) { try { @@ -313,10 +313,15 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA stream.ReadExactly(buf, 0, length); var image = purpose switch { - DalamudAssetPurpose.TextureFromPng => await tm.CreateFromImageAsync(buf), + DalamudAssetPurpose.TextureFromPng => await tm.CreateFromImageAsync( + buf, + $"{nameof(DalamudAsset)}.{Enum.GetName(asset)}"), DalamudAssetPurpose.TextureFromRaw => asset.GetAttribute() is { } raw - ? await tm.CreateFromRawAsync(raw.Specification, buf) + ? await tm.CreateFromRawAsync( + raw.Specification, + buf, + $"{nameof(DalamudAsset)}.{Enum.GetName(asset)}") : throw new InvalidOperationException( "TextureFromRaw must accompany a DalamudAssetRawTextureAttribute."), _ => null, From e9b903b2a7ddad9633234deed07f23e5c968bc5c Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Tue, 5 Mar 2024 01:37:46 +0900 Subject: [PATCH 49/57] Track shared textures --- .../SharedImmediateTexture.cs | 56 ++++++++++++++++-- .../Internal/TextureManager.BlameTracker.cs | 20 +++++++ .../TextureManagerPluginScoped.Api9.cs | 59 +++++++++---------- .../Internal/TextureManagerPluginScoped.cs | 16 +++-- 4 files changed, 112 insertions(+), 39 deletions(-) diff --git a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/SharedImmediateTexture.cs b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/SharedImmediateTexture.cs index 07eb52500..ed76223a3 100644 --- a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/SharedImmediateTexture.cs +++ b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/SharedImmediateTexture.cs @@ -1,9 +1,12 @@ +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Dalamud.Interface.Internal; +using Dalamud.Plugin.Internal.Types; using Dalamud.Storage.Assets; using Dalamud.Utility; @@ -42,6 +45,9 @@ internal abstract class SharedImmediateTexture this.FirstRequestedTick = this.LatestRequestedTick = Environment.TickCount64; } + /// Gets the list of owner plugins. + public List OwnerPlugins { get; } = new(); + /// Gets the instance ID. Debug use only. public long InstanceIdForDebug { get; } @@ -274,6 +280,26 @@ internal abstract class SharedImmediateTexture return this.availableOnAccessWrapForApi9; } + /// Adds a plugin to , in a thread-safe way. + /// The plugin to add. + public void AddOwnerPlugin(LocalPlugin plugin) + { + lock (this.OwnerPlugins) + { + if (!this.OwnerPlugins.Contains(plugin)) + { + this.OwnerPlugins.Add(plugin); + this.UnderlyingWrap?.ContinueWith( + r => + { + if (r.IsCompletedSuccessfully) + Service.Get().Blame(r.Result, plugin); + }, + default(CancellationToken)); + } + } + } + /// public override string ToString() => $"{this.GetType().Name}#{this.InstanceIdForDebug}({this.SourcePathForDebug})"; @@ -285,11 +311,31 @@ internal abstract class SharedImmediateTexture } /// Attempts to restore the reference to this texture. - protected void LoadUnderlyingWrap() => - this.UnderlyingWrap = Service.Get().DynamicPriorityTextureLoader.LoadAsync( - this, - this.CreateTextureAsync, - this.LoadCancellationToken); + protected void LoadUnderlyingWrap() + { + int addLen; + lock (this.OwnerPlugins) + { + this.UnderlyingWrap = Service.Get().DynamicPriorityTextureLoader.LoadAsync( + this, + this.CreateTextureAsync, + this.LoadCancellationToken); + + addLen = this.OwnerPlugins.Count; + } + + if (addLen == 0) + return; + this.UnderlyingWrap.ContinueWith( + r => + { + if (!r.IsCompletedSuccessfully) + return; + foreach (var op in this.OwnerPlugins.Take(addLen)) + Service.Get().Blame(r.Result, op); + }, + default(CancellationToken)); + } /// Creates the texture. /// The cancellation token. diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.BlameTracker.cs b/Dalamud/Interface/Textures/Internal/TextureManager.BlameTracker.cs index a401a9a73..5e47d2c8e 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.BlameTracker.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.BlameTracker.cs @@ -48,6 +48,16 @@ internal sealed partial class TextureManager if (!this.dalamudConfiguration.UseTexturePluginTracking) return textureWrap; + try + { + if (textureWrap.ImGuiHandle == nint.Zero) + return textureWrap; + } + catch (ObjectDisposedException) + { + return textureWrap; + } + using var wrapAux = new WrapAux(textureWrap, true); var blame = BlameTag.From(wrapAux.ResPtr, out var isNew); @@ -75,6 +85,16 @@ internal sealed partial class TextureManager if (!this.dalamudConfiguration.UseTexturePluginTracking) return textureWrap; + try + { + if (textureWrap.ImGuiHandle == nint.Zero) + return textureWrap; + } + catch (ObjectDisposedException) + { + return textureWrap; + } + using var wrapAux = new WrapAux(textureWrap, true); var blame = BlameTag.From(wrapAux.ResPtr, out var isNew); blame.Name = name; diff --git a/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.Api9.cs b/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.Api9.cs index 21ee1291c..ca5399d0c 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.Api9.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.Api9.cs @@ -14,43 +14,42 @@ internal sealed partial class TextureManagerPluginScoped /// [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] [Obsolete("See interface definition.")] - public IDalamudTextureWrap? GetIcon( + string? ITextureProvider.GetIconPath(uint iconId, ITextureProvider.IconFlags flags, ClientLanguage? language) + => 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.")] + IDalamudTextureWrap? ITextureProvider.GetIcon( uint iconId, - ITextureProvider.IconFlags flags = ITextureProvider.IconFlags.HiRes, - ClientLanguage? language = null, - bool keepAlive = false) - { - throw new NotImplementedException(); - } + ITextureProvider.IconFlags flags, + ClientLanguage? language, + bool keepAlive) => + this.ManagerOrThrow.Shared.GetFromGameIcon( + new( + iconId, + (flags & ITextureProvider.IconFlags.ItemHighQuality) != 0, + (flags & ITextureProvider.IconFlags.HiRes) != 0, + language)) + .GetAvailableOnAccessWrapForApi9(); /// [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] [Obsolete("See interface definition.")] - public string? GetIconPath( - uint iconId, - ITextureProvider.IconFlags flags = ITextureProvider.IconFlags.HiRes, - ClientLanguage? language = null) - { - throw new NotImplementedException(); - } + IDalamudTextureWrap? ITextureProvider.GetTextureFromGame(string path, bool keepAlive) => + this.ManagerOrThrow.Shared.GetFromGame(path).GetAvailableOnAccessWrapForApi9(); /// [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(); - } + IDalamudTextureWrap? ITextureProvider.GetTextureFromFile(FileInfo file, bool keepAlive) => + this.ManagerOrThrow.Shared.GetFromFile(file.FullName).GetAvailableOnAccessWrapForApi9(); } diff --git a/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs b/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs index 39b91edda..afdca0f31 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs @@ -257,25 +257,33 @@ internal sealed partial class TextureManagerPluginScoped /// public ISharedImmediateTexture GetFromGameIcon(in GameIconLookup lookup) { - return this.ManagerOrThrow.Shared.GetFromGameIcon(lookup); + var shared = this.ManagerOrThrow.Shared.GetFromGameIcon(lookup); + shared.AddOwnerPlugin(this.plugin); + return shared; } /// public ISharedImmediateTexture GetFromGame(string path) { - return this.ManagerOrThrow.Shared.GetFromGame(path); + var shared = this.ManagerOrThrow.Shared.GetFromGame(path); + shared.AddOwnerPlugin(this.plugin); + return shared; } /// public ISharedImmediateTexture GetFromFile(string path) { - return this.ManagerOrThrow.Shared.GetFromFile(path); + var shared = this.ManagerOrThrow.Shared.GetFromFile(path); + shared.AddOwnerPlugin(this.plugin); + return shared; } /// public ISharedImmediateTexture GetFromManifestResource(Assembly assembly, string name) { - return this.ManagerOrThrow.Shared.GetFromManifestResource(assembly, name); + var shared = this.ManagerOrThrow.Shared.GetFromManifestResource(assembly, name); + shared.AddOwnerPlugin(this.plugin); + return shared; } /// From 9ba0a297c9f5c6905dd5fe07e8ba72d8bf659c8f Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Tue, 5 Mar 2024 01:53:31 +0900 Subject: [PATCH 50/57] Add texture vram usage estimation --- .../Windows/Data/Widgets/TexWidget.cs | 20 +++++++++++++++--- .../Textures/Internal/TextureManager.Wic.cs | 4 ++-- .../Textures/Internal/TextureManager.cs | 2 +- .../Textures/RawImageSpecification.cs | 21 +++++++++++++++++-- 4 files changed, 39 insertions(+), 8 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs index d14aeb5ab..58a2b6b6e 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -309,7 +309,7 @@ internal class TexWidget : IDataWindowWidget conf.QueueSave(); } - if (!ImGui.BeginTable("##table", 5)) + if (!ImGui.BeginTable("##table", 6)) return; const int numIcons = 1; @@ -318,12 +318,21 @@ internal class TexWidget : IDataWindowWidget iconWidths = ImGui.CalcTextSize(FontAwesomeIcon.Save.ToIconString()).X; ImGui.TableSetupScrollFreeze(0, 1); - ImGui.TableSetupColumn("Size", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("00000x00000").X); + ImGui.TableSetupColumn( + "Dimensions", + ImGuiTableColumnFlags.WidthFixed, + ImGui.CalcTextSize("00000x00000").X); ImGui.TableSetupColumn( "Format", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("R32G32B32A32_TYPELESS").X); - ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn( + "Size", + ImGuiTableColumnFlags.WidthFixed, + ImGui.CalcTextSize("123.45 MB").X); + ImGui.TableSetupColumn( + "Name", + ImGuiTableColumnFlags.WidthStretch); ImGui.TableSetupColumn( "Actions", ImGuiTableColumnFlags.WidthFixed, @@ -353,6 +362,11 @@ internal class TexWidget : IDataWindowWidget ImGui.TableNextColumn(); ImGui.TextUnformatted(Enum.GetName(wrap.Format)?[12..] ?? wrap.Format.ToString()); + ImGui.TableNextColumn(); + var rawSpec = new RawImageSpecification(wrap.Width, wrap.Height, (int)wrap.Format, 0); + var bytes = rawSpec.EstimatedBytes; + ImGui.TextUnformatted(bytes < 0 ? "-" : Util.FormatBytes(bytes)); + ImGui.TableNextColumn(); ImGui.TextUnformatted(wrap.Name); diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs b/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs index e59a3a1f2..bc10e5b77 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs @@ -485,7 +485,7 @@ internal sealed partial class TextureManager IReadOnlyDictionary? props = null, CancellationToken cancellationToken = default) { - if (!GetCorrespondingWicPixelFormat((DXGI_FORMAT)specs.DxgiFormat, out var inPixelFormat, out var srgb)) + if (!GetCorrespondingWicPixelFormat(specs.Format, out var inPixelFormat, out var srgb)) throw new NotSupportedException("DXGI_FORMAT from specs is not supported by WIC."); using var encoder = default(ComPtr); @@ -494,7 +494,7 @@ internal sealed partial class TextureManager cancellationToken.ThrowIfCancellationRequested(); // See: DirectXTK/Src/ScreenGrab.cpp - var outPixelFormat = (DXGI_FORMAT)specs.DxgiFormat switch + var outPixelFormat = specs.Format switch { DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_FLOAT => GUID.GUID_WICPixelFormat128bppRGBAFloat, DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_FLOAT when !this.wicFactory2.IsEmpty() => diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.cs b/Dalamud/Interface/Textures/Internal/TextureManager.cs index 8fa9efa25..f69f01877 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.cs @@ -242,7 +242,7 @@ internal sealed partial class TextureManager Height = (uint)specs.Height, MipLevels = 1, ArraySize = 1, - Format = (DXGI_FORMAT)specs.DxgiFormat, + Format = specs.Format, SampleDesc = new(1, 0), Usage = D3D11_USAGE.D3D11_USAGE_IMMUTABLE, BindFlags = (uint)D3D11_BIND_FLAG.D3D11_BIND_SHADER_RESOURCE, diff --git a/Dalamud/Interface/Textures/RawImageSpecification.cs b/Dalamud/Interface/Textures/RawImageSpecification.cs index 2abf3729d..9ac649812 100644 --- a/Dalamud/Interface/Textures/RawImageSpecification.cs +++ b/Dalamud/Interface/Textures/RawImageSpecification.cs @@ -51,10 +51,27 @@ public record struct RawImageSpecification /// Gets the number of bits per pixel. /// Thrown if is not supported. public int BitsPerPixel => - GetFormatInfo((DXGI_FORMAT)this.DxgiFormat, out var bitsPerPixel, out _) + GetFormatInfo(this.Format, out var bitsPerPixel, out _) ? bitsPerPixel : throw new NotSupportedException(FormatNotSupportedMessage); + /// Gets or sets the format (typed). + internal DXGI_FORMAT Format + { + get => (DXGI_FORMAT)this.DxgiFormat; + set => this.DxgiFormat = (int)value; + } + + /// Gets the estimated number of bytes. + /// -1 if failed. + internal int EstimatedBytes => + GetFormatInfo(this.Format, out var bitsPerPixel, out var isBlockCompression) + ? isBlockCompression + ? (((Math.Max(1, (this.Width + 3) / 4) * 2 * bitsPerPixel) + 63) / 64) * 64 * + Math.Max(1, (this.Height + 3) / 4) + : (((((bitsPerPixel * this.Width) + 7) / 8) + 63) / 64) * 64 * this.Height + : -1; + /// /// Creates a new instance of record using the given resolution, /// in B8G8R8A8(BGRA32) UNorm pixel format. @@ -230,7 +247,7 @@ public record struct RawImageSpecification case DXGI_FORMAT.DXGI_FORMAT_B4G4R4A4_UNORM: bitsPerPixel = 16; isBlockCompression = true; - return false; + return true; default: bitsPerPixel = 0; isBlockCompression = false; From 70eecdaaefd6b33b08ca5f737413c90c1b66d480 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Tue, 5 Mar 2024 17:41:27 +0900 Subject: [PATCH 51/57] more --- Dalamud/Interface/GameIconLookup.cs | 20 - .../Interface/Internal/DalamudInterface.cs | 128 +++++++ .../Interface/Internal/InterfaceManager.cs | 11 +- .../Windows/Data/Widgets/IconBrowserWidget.cs | 10 +- .../Windows/Data/Widgets/TexWidget.cs | 356 +++++++++--------- .../FontAtlasFactory.BuildToolkit.cs | 4 +- .../FontAtlasFactory.Implementation.cs | 9 +- .../Internals/FontAtlasFactory.cs | 9 +- Dalamud/Interface/Textures/GameIconLookup.cs | 55 +++ .../Textures/ImGuiViewportTextureArgs.cs | 27 ++ .../FileSystemSharedImmediateTexture.cs | 1 + .../GamePathSharedImmediateTexture.cs | 1 + .../ManifestResourceSharedImmediateTexture.cs | 2 + .../SharedImmediateTexture.cs | 73 +++- .../Internal/TextureManager.BlameTracker.cs | 87 +++-- .../TextureManager.FromExistingTexture.cs | 132 ++++--- .../Internal/TextureManager.SharedTextures.cs | 17 +- .../Textures/Internal/TextureManager.Wic.cs | 28 +- .../Textures/Internal/TextureManager.cs | 51 ++- .../Textures/RawImageSpecification.cs | 16 +- .../Textures/TextureModificationArgs.cs | 31 ++ .../{ => TextureWraps}/DalamudTextureWrap.cs | 0 .../ForwardingTextureWrap.cs | 4 +- .../{ => TextureWraps}/IDalamudTextureWrap.cs | 2 + .../Internal/DisposeSuppressingTextureWrap.cs | 2 +- .../Internal/UnknownTextureWrap.cs | 7 +- .../Internal/ViewportTextureWrap.cs | 3 +- Dalamud/Interface/UiBuilder.cs | 12 +- .../Plugin/Services/ITextureProvider.Api9.cs | 1 + Dalamud/Storage/Assets/DalamudAssetManager.cs | 13 +- 30 files changed, 767 insertions(+), 345 deletions(-) delete mode 100644 Dalamud/Interface/GameIconLookup.cs create mode 100644 Dalamud/Interface/Textures/GameIconLookup.cs rename Dalamud/Interface/Textures/{ => TextureWraps}/DalamudTextureWrap.cs (100%) rename Dalamud/Interface/Textures/{ => TextureWraps}/ForwardingTextureWrap.cs (96%) rename Dalamud/Interface/Textures/{ => TextureWraps}/IDalamudTextureWrap.cs (96%) rename Dalamud/Interface/Textures/{ => TextureWraps}/Internal/DisposeSuppressingTextureWrap.cs (91%) rename Dalamud/Interface/Textures/{ => TextureWraps}/Internal/UnknownTextureWrap.cs (88%) rename Dalamud/Interface/Textures/{ => TextureWraps}/Internal/ViewportTextureWrap.cs (99%) diff --git a/Dalamud/Interface/GameIconLookup.cs b/Dalamud/Interface/GameIconLookup.cs deleted file mode 100644 index 001f519aa..000000000 --- a/Dalamud/Interface/GameIconLookup.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -namespace Dalamud.Interface; - -/// -/// 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/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 00bef19af..aab4996bb 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -1,9 +1,12 @@ +using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Numerics; using System.Reflection; using System.Runtime.InteropServices; +using System.Threading.Tasks; using CheapLoc; using Dalamud.Configuration.Internal; @@ -15,7 +18,9 @@ using Dalamud.Game.Internal; using Dalamud.Hooking; using Dalamud.Interface.Animation.EasingFunctions; using Dalamud.Interface.Colors; +using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.Internal.ManagedAsserts; +using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Internal.Windows; using Dalamud.Interface.Internal.Windows.Data; using Dalamud.Interface.Internal.Windows.PluginInstaller; @@ -24,6 +29,7 @@ using Dalamud.Interface.Internal.Windows.Settings; using Dalamud.Interface.Internal.Windows.StyleEditor; using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Style; +using Dalamud.Interface.Textures.Internal; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; @@ -40,6 +46,10 @@ using ImPlotNET; using PInvoke; using Serilog.Events; +using TerraFX.Interop.Windows; + +using Task = System.Threading.Tasks.Task; + namespace Dalamud.Interface.Internal; /// @@ -56,6 +66,8 @@ internal class DalamudInterface : IDisposable, IServiceType private readonly DalamudConfiguration configuration; private readonly InterfaceManager interfaceManager; + private readonly FileDialogManager fileDialogManager; + private readonly ChangelogWindow changelogWindow; private readonly ColorDemoWindow colorDemoWindow; private readonly ComponentDemoWindow componentDemoWindow; @@ -109,6 +121,7 @@ internal class DalamudInterface : IDisposable, IServiceType this.interfaceManager = interfaceManager; this.WindowSystem = new WindowSystem("DalamudCore"); + this.fileDialogManager = new(); this.colorDemoWindow = new ColorDemoWindow() { IsOpen = false }; this.componentDemoWindow = new ComponentDemoWindow() { IsOpen = false }; @@ -486,6 +499,119 @@ internal class DalamudInterface : IDisposable, IServiceType this.creditsDarkeningAnimation.Restart(); } + /// Shows a context menu confirming texture save. + /// Name of the initiator. + /// Suggested name of the file being saved. + /// A task returning the texture to save. + /// A representing the asynchronous operation. + public async Task ShowTextureSaveMenuAsync( + string initiatorName, + string name, + Task texture) + { + try + { + var initiatorScreenOffset = ImGui.GetMousePos(); + using var textureWrap = await texture; + var textureManager = await Service.GetAsync(); + var popupName = $"{nameof(this.ShowTextureSaveMenuAsync)}_{textureWrap.ImGuiHandle:X}"; + + BitmapCodecInfo encoder; + { + var first = true; + var encoders = textureManager.Wic.GetSupportedEncoderInfos().ToList(); + var tcs = new TaskCompletionSource(); + Service.Get().Draw += DrawChoices; + + encoder = await tcs.Task; + + [SuppressMessage("ReSharper", "AccessToDisposedClosure", Justification = "This shall not escape")] + void DrawChoices() + { + if (first) + { + ImGui.OpenPopup(popupName); + first = false; + } + + ImGui.SetNextWindowPos(initiatorScreenOffset, ImGuiCond.Appearing); + if (!ImGui.BeginPopup( + popupName, + ImGuiWindowFlags.AlwaysAutoResize | + ImGuiWindowFlags.NoTitleBar | + ImGuiWindowFlags.NoSavedSettings)) + { + Service.Get().Draw -= DrawChoices; + tcs.TrySetCanceled(); + return; + } + + foreach (var encoder2 in encoders) + { + if (ImGui.Selectable(encoder2.Name)) + tcs.TrySetResult(encoder2); + } + + const float previewImageWidth = 320; + var size = textureWrap.Size; + if (size.X > previewImageWidth) + size *= previewImageWidth / size.X; + if (size.Y > previewImageWidth) + size *= previewImageWidth / size.Y; + ImGui.Image(textureWrap.ImGuiHandle, size); + + if (tcs.Task.IsCompleted) + ImGui.CloseCurrentPopup(); + + ImGui.EndPopup(); + } + } + + string path; + { + var tcs = new TaskCompletionSource(); + this.fileDialogManager.SaveFileDialog( + "Save texture...", + $"{encoder.Name.Replace(',', '.')}{{{string.Join(',', encoder.Extensions)}}}", + name + encoder.Extensions.First(), + encoder.Extensions.First(), + (ok, path2) => + { + if (!ok) + tcs.SetCanceled(); + else + tcs.SetResult(path2); + }); + path = await tcs.Task.ConfigureAwait(false); + } + + var props = new Dictionary(); + if (encoder.ContainerGuid == GUID.GUID_ContainerFormatTiff) + props["CompressionQuality"] = 1.0f; + else if (encoder.ContainerGuid == GUID.GUID_ContainerFormatJpeg || + encoder.ContainerGuid == GUID.GUID_ContainerFormatHeif || + encoder.ContainerGuid == GUID.GUID_ContainerFormatWmp) + props["ImageQuality"] = 1.0f; + await textureManager.SaveToFileAsync(textureWrap, encoder.ContainerGuid, path, props: props); + + Service.Get().AddNotification( + $"File saved to: {path}", + initiatorName, + NotificationType.Success); + } + catch (Exception e) + { + if (e is OperationCanceledException) + return; + + Log.Error(e, $"{nameof(DalamudInterface)}.{nameof(this.ShowTextureSaveMenuAsync)}({initiatorName}, {name})"); + Service.Get().AddNotification( + $"Failed to save file: {e}", + initiatorName, + NotificationType.Error); + } + } + private void OnDraw() { this.FrameCount++; @@ -537,6 +663,8 @@ internal class DalamudInterface : IDisposable, IServiceType { ImGui.SetWindowFocus(null); } + + this.fileDialogManager.Draw(); } catch (Exception ex) { diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 65c10d3a0..87bff1325 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -199,6 +199,10 @@ internal class InterfaceManager : IDisposable, IServiceType /// public bool IsDispatchingEvents { get; set; } = true; + /// Gets a value indicating whether the main thread is executing . + /// This still will be true even when queried off the main thread. + public bool IsInPresent { get; private set; } + /// /// Gets a value indicating the native handle of the game main window. /// @@ -595,8 +599,6 @@ internal class InterfaceManager : IDisposable, IServiceType */ private IntPtr PresentDetour(IntPtr swapChain, uint syncInterval, uint presentFlags) { - this.CumulativePresentCalls++; - Debug.Assert(this.presentHook is not null, "How did PresentDetour get called when presentHook is null?"); Debug.Assert(this.dalamudAtlas is not null, "dalamudAtlas should have been set already"); @@ -611,6 +613,9 @@ internal class InterfaceManager : IDisposable, IServiceType if (!this.dalamudAtlas!.HasBuiltAtlas) return this.presentHook!.Original(swapChain, syncInterval, presentFlags); + this.CumulativePresentCalls++; + this.IsInPresent = true; + while (this.runBeforeImGuiRender.TryDequeue(out var action)) action.InvokeSafely(); @@ -620,12 +625,14 @@ internal class InterfaceManager : IDisposable, IServiceType RenderImGui(this.scene!); this.PostImGuiRender(); + this.IsInPresent = false; return pRes; } RenderImGui(this.scene!); this.PostImGuiRender(); + this.IsInPresent = false; return this.presentHook!.Original(swapChain, syncInterval, presentFlags); } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs index b7b897e68..e10fca0e7 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs @@ -142,7 +142,7 @@ public class IconBrowserWidget : IDataWindowWidget var texm = Service.Get(); var cursor = ImGui.GetCursorScreenPos(); - if (texm.Shared.GetFromGameIcon(new((uint)iconId)).TryGetWrap(out var texture, out var exc)) + if (texm.Shared.GetFromGameIcon(iconId).TryGetWrap(out var texture, out var exc)) { ImGui.Image(texture.ImGuiHandle, this.iconSize); @@ -168,6 +168,14 @@ public class IconBrowserWidget : IDataWindowWidget ImGui.SetTooltip(iconId.ToString()); } + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + _ = Service.Get().ShowTextureSaveMenuAsync( + this.DisplayName, + iconId.ToString(), + Task.FromResult(texture.CreateWrapSharingLowLevelResource())); + } + ImGui.GetWindowDrawList().AddRect( cursor, cursor + this.iconSize, diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs index 58a2b6b6e..4713022e6 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -8,10 +8,8 @@ using System.Threading.Tasks; using Dalamud.Configuration.Internal; using Dalamud.Interface.Components; -using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Textures; -using Dalamud.Interface.Textures.Internal; using Dalamud.Interface.Textures.Internal.SharedImmediateTextures; using Dalamud.Interface.Utility; using Dalamud.Plugin.Services; @@ -20,10 +18,7 @@ using Dalamud.Utility; using ImGuiNET; -using Serilog; - using TerraFX.Interop.DirectX; -using TerraFX.Interop.Windows; using TextureManager = Dalamud.Interface.Textures.Internal.TextureManager; @@ -34,8 +29,22 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// internal class TexWidget : IDataWindowWidget { + private static readonly Dictionary< + DrawBlameTableColumnUserId, + Func> DrawBlameTableColumnColumnComparers = new() + { + [DrawBlameTableColumnUserId.Plugins] = static x => string.Join(", ", x.OwnerPlugins.Select(y => y.Name)), + [DrawBlameTableColumnUserId.Name] = static x => x.Name, + [DrawBlameTableColumnUserId.Size] = static x => x.RawSpecs.EstimatedBytes, + [DrawBlameTableColumnUserId.Format] = static x => x.Format, + [DrawBlameTableColumnUserId.Width] = static x => x.Width, + [DrawBlameTableColumnUserId.Height] = static x => x.Height, + [DrawBlameTableColumnUserId.NativeAddress] = static x => x.ResourceAddress, + }; + private readonly List addedTextures = new(); + private string allLoadedTexturesTableName = "##table"; private string iconId = "18"; private bool hiRes = true; private bool hq = false; @@ -51,7 +60,6 @@ internal class TexWidget : IDataWindowWidget private Vector4 inputTintCol = Vector4.One; private Vector2 inputTexScale = Vector2.Zero; private TextureManager textureManager = null!; - private FileDialogManager fileDialogManager = null!; private TextureModificationArgs textureModificationArgs; private ImGuiViewportTextureArgs viewportTextureArgs; @@ -60,6 +68,19 @@ internal class TexWidget : IDataWindowWidget private DXGI_FORMAT[]? supportedRenderTargetFormats; private int renderTargetChoiceInt; + private enum DrawBlameTableColumnUserId + { + NativeAddress, + Actions, + Name, + Width, + Height, + Format, + Size, + Plugins, + ColumnCount, + } + /// public string[]? CommandShortcuts { get; init; } = { "tex", "texture" }; @@ -74,6 +95,7 @@ internal class TexWidget : IDataWindowWidget /// public void Load() { + this.allLoadedTexturesTableName = "##table" + Environment.TickCount64; this.addedTextures.AggregateToDisposable().Dispose(); this.addedTextures.Clear(); this.inputTexPath = "ui/loadingimage/-nowloading_base25_hr1.tex"; @@ -88,7 +110,6 @@ internal class TexWidget : IDataWindowWidget this.supportedRenderTargetFormats = null; this.supportedRenderTargetFormatNames = null; this.renderTargetChoiceInt = 0; - this.fileDialogManager = new(); this.textureModificationArgs = new() { Uv0 = new(0.25f), @@ -105,30 +126,44 @@ internal class TexWidget : IDataWindowWidget public void Draw() { this.textureManager = Service.Get(); + var conf = Service.Get(); if (ImGui.Button("GC")) GC.Collect(); - ImGui.PushID("blames"); - if (ImGui.CollapsingHeader($"All Loaded Textures: {this.textureManager.AllBlamesForDebug.Count:g}###header")) - this.DrawBlame(this.textureManager.AllBlamesForDebug); - ImGui.PopID(); + var useTexturePluginTracking = conf.UseTexturePluginTracking; + if (ImGui.Checkbox("Enable Texture Tracking", ref useTexturePluginTracking)) + { + conf.UseTexturePluginTracking = useTexturePluginTracking; + conf.QueueSave(); + } + + var allBlames = this.textureManager.BlameTracker; + lock (allBlames) + { + ImGui.PushID("blames"); + var sizeSum = allBlames.Sum(static x => Math.Max(0, x.RawSpecs.EstimatedBytes)); + if (ImGui.CollapsingHeader( + $"All Loaded Textures: {allBlames.Count:n0} ({Util.FormatBytes(sizeSum)})###header")) + this.DrawBlame(allBlames); + ImGui.PopID(); + } ImGui.PushID("loadedGameTextures"); if (ImGui.CollapsingHeader( - $"Loaded Game Textures: {this.textureManager.Shared.ForDebugGamePathTextures.Count:g}###header")) + $"Loaded Game Textures: {this.textureManager.Shared.ForDebugGamePathTextures.Count:n0}###header")) this.DrawLoadedTextures(this.textureManager.Shared.ForDebugGamePathTextures); ImGui.PopID(); ImGui.PushID("loadedFileTextures"); if (ImGui.CollapsingHeader( - $"Loaded File Textures: {this.textureManager.Shared.ForDebugFileSystemTextures.Count:g}###header")) + $"Loaded File Textures: {this.textureManager.Shared.ForDebugFileSystemTextures.Count:n0}###header")) this.DrawLoadedTextures(this.textureManager.Shared.ForDebugFileSystemTextures); ImGui.PopID(); ImGui.PushID("loadedManifestResourceTextures"); if (ImGui.CollapsingHeader( - $"Loaded Manifest Resource Textures: {this.textureManager.Shared.ForDebugManifestResourceTextures.Count:g}###header")) + $"Loaded Manifest Resource Textures: {this.textureManager.Shared.ForDebugManifestResourceTextures.Count:n0}###header")) this.DrawLoadedTextures(this.textureManager.Shared.ForDebugManifestResourceTextures); ImGui.PopID(); @@ -136,7 +171,7 @@ internal class TexWidget : IDataWindowWidget { ImGui.PushID("invalidatedTextures"); if (ImGui.CollapsingHeader( - $"Invalidated: {this.textureManager.Shared.ForDebugInvalidatedTextures.Count:g}###header")) + $"Invalidated: {this.textureManager.Shared.ForDebugInvalidatedTextures.Count:n0}###header")) { this.DrawLoadedTextures(this.textureManager.Shared.ForDebugInvalidatedTextures); } @@ -215,9 +250,10 @@ internal class TexWidget : IDataWindowWidget ImGui.SameLine(); if (ImGui.Button("Save")) { - this.SaveTextureAsync( + _ = Service.Get().ShowTextureSaveMenuAsync( + this.DisplayName, $"Texture {t.Id}", - () => t.CreateNewTextureWrapReference(this.textureManager)); + t.CreateNewTextureWrapReference(this.textureManager)); } ImGui.SameLine(); @@ -262,10 +298,12 @@ internal class TexWidget : IDataWindowWidget pres->Release(); ImGui.TextUnformatted($"RC: Resource({rcres})/View({rcsrv})"); + ImGui.TextUnformatted(source.ToString()); } else { ImGui.TextUnformatted("RC: -"); + ImGui.TextUnformatted(" "); } } @@ -294,22 +332,24 @@ internal class TexWidget : IDataWindowWidget } runLater?.Invoke(); - - this.fileDialogManager.Draw(); } - private unsafe void DrawBlame(IReadOnlyList allBlames) + private unsafe void DrawBlame(List allBlames) { - var conf = Service.Get(); var im = Service.Get(); - var blame = conf.UseTexturePluginTracking; - if (ImGui.Checkbox("Enable", ref blame)) - { - conf.UseTexturePluginTracking = blame; - conf.QueueSave(); - } - if (!ImGui.BeginTable("##table", 6)) + var shouldSortAgain = ImGui.Button("Sort again"); + + ImGui.SameLine(); + if (ImGui.Button("Reset Columns")) + this.allLoadedTexturesTableName = "##table" + Environment.TickCount64; + + if (!ImGui.BeginTable( + this.allLoadedTexturesTableName, + (int)DrawBlameTableColumnUserId.ColumnCount, + ImGuiTableFlags.Sortable | ImGuiTableFlags.SortTristate | ImGuiTableFlags.SortMulti | + ImGuiTableFlags.Reorderable | ImGuiTableFlags.Resizable | ImGuiTableFlags.NoBordersInBodyUntilResize | + ImGuiTableFlags.NoSavedSettings)) return; const int numIcons = 1; @@ -319,32 +359,88 @@ internal class TexWidget : IDataWindowWidget ImGui.TableSetupScrollFreeze(0, 1); ImGui.TableSetupColumn( - "Dimensions", + "Address", ImGuiTableColumnFlags.WidthFixed, - ImGui.CalcTextSize("00000x00000").X); + ImGui.CalcTextSize("0x7F0000000000").X, + (uint)DrawBlameTableColumnUserId.NativeAddress); + ImGui.TableSetupColumn( + "Actions", + ImGuiTableColumnFlags.WidthFixed | ImGuiTableColumnFlags.NoSort, + iconWidths + + (ImGui.GetStyle().FramePadding.X * 2 * numIcons) + + (ImGui.GetStyle().ItemSpacing.X * 1 * numIcons), + (uint)DrawBlameTableColumnUserId.Actions); + ImGui.TableSetupColumn( + "Name", + ImGuiTableColumnFlags.WidthStretch, + 0f, + (uint)DrawBlameTableColumnUserId.Name); + ImGui.TableSetupColumn( + "Width", + ImGuiTableColumnFlags.WidthFixed, + ImGui.CalcTextSize("000000").X, + (uint)DrawBlameTableColumnUserId.Width); + ImGui.TableSetupColumn( + "Height", + ImGuiTableColumnFlags.WidthFixed, + ImGui.CalcTextSize("000000").X, + (uint)DrawBlameTableColumnUserId.Height); ImGui.TableSetupColumn( "Format", ImGuiTableColumnFlags.WidthFixed, - ImGui.CalcTextSize("R32G32B32A32_TYPELESS").X); + ImGui.CalcTextSize("R32G32B32A32_TYPELESS").X, + (uint)DrawBlameTableColumnUserId.Format); ImGui.TableSetupColumn( "Size", ImGuiTableColumnFlags.WidthFixed, - ImGui.CalcTextSize("123.45 MB").X); - ImGui.TableSetupColumn( - "Name", - ImGuiTableColumnFlags.WidthStretch); - ImGui.TableSetupColumn( - "Actions", - ImGuiTableColumnFlags.WidthFixed, - iconWidths + - (ImGui.GetStyle().FramePadding.X * 2 * numIcons) + - (ImGui.GetStyle().ItemSpacing.X * 1 * numIcons)); + ImGui.CalcTextSize("123.45 MB").X, + (uint)DrawBlameTableColumnUserId.Size); ImGui.TableSetupColumn( "Plugins", ImGuiTableColumnFlags.WidthFixed, - ImGui.CalcTextSize("Aaaaaaaaaa Aaaaaaaaaa Aaaaaaaaaa").X); + ImGui.CalcTextSize("Aaaaaaaaaa Aaaaaaaaaa Aaaaaaaaaa").X, + (uint)DrawBlameTableColumnUserId.Plugins); ImGui.TableHeadersRow(); + var sortSpecs = ImGui.TableGetSortSpecs(); + if (sortSpecs.NativePtr is not null && (sortSpecs.SpecsDirty || shouldSortAgain)) + { + allBlames.Sort( + static (a, b) => + { + var sortSpecs = ImGui.TableGetSortSpecs(); + var specs = new Span(sortSpecs.NativePtr->Specs, sortSpecs.SpecsCount); + Span sorted = stackalloc bool[(int)DrawBlameTableColumnUserId.ColumnCount]; + foreach (ref var spec in specs) + { + if (!DrawBlameTableColumnColumnComparers.TryGetValue( + (DrawBlameTableColumnUserId)spec.ColumnUserID, + out var comparableGetter)) + continue; + sorted[(int)spec.ColumnUserID] = true; + var ac = comparableGetter(a); + var bc = comparableGetter(b); + var c = ac.CompareTo(bc); + if (c != 0) + return spec.SortDirection == ImGuiSortDirection.Ascending ? c : -c; + } + + foreach (var (col, comparableGetter) in DrawBlameTableColumnColumnComparers) + { + if (sorted[(int)col]) + continue; + var ac = comparableGetter(a); + var bc = comparableGetter(b); + var c = ac.CompareTo(bc); + if (c != 0) + return c; + } + + return 0; + }); + sortSpecs.SpecsDirty = false; + } + var clipper = new ImGuiListClipperPtr(ImGuiNative.ImGuiListClipper_ImGuiListClipper()); clipper.Begin(allBlames.Count); @@ -357,25 +453,16 @@ internal class TexWidget : IDataWindowWidget ImGui.PushID(i); ImGui.TableNextColumn(); - ImGui.TextUnformatted($"{wrap.Width}x{wrap.Height}"); - - ImGui.TableNextColumn(); - ImGui.TextUnformatted(Enum.GetName(wrap.Format)?[12..] ?? wrap.Format.ToString()); - - ImGui.TableNextColumn(); - var rawSpec = new RawImageSpecification(wrap.Width, wrap.Height, (int)wrap.Format, 0); - var bytes = rawSpec.EstimatedBytes; - ImGui.TextUnformatted(bytes < 0 ? "-" : Util.FormatBytes(bytes)); - - ImGui.TableNextColumn(); - ImGui.TextUnformatted(wrap.Name); + ImGui.AlignTextToFramePadding(); + this.TextCopiable($"0x{wrap.ResourceAddress:X}", true, true); ImGui.TableNextColumn(); if (ImGuiComponents.IconButton(FontAwesomeIcon.Save)) { - this.SaveTextureAsync( + _ = Service.Get().ShowTextureSaveMenuAsync( + this.DisplayName, $"{wrap.ImGuiHandle:X16}", - () => Task.FromResult(wrap.CreateWrapSharingLowLevelResource())); + Task.FromResult(wrap.CreateWrapSharingLowLevelResource())); } if (ImGui.IsItemHovered()) @@ -385,12 +472,25 @@ internal class TexWidget : IDataWindowWidget ImGui.EndTooltip(); } + ImGui.TableNextColumn(); + this.TextCopiable(wrap.Name, false, true); + + ImGui.TableNextColumn(); + this.TextCopiable($"{wrap.Width:n0}", true, true); + + ImGui.TableNextColumn(); + this.TextCopiable($"{wrap.Height:n0}", true, true); + + ImGui.TableNextColumn(); + this.TextCopiable(Enum.GetName(wrap.Format)?[12..] ?? wrap.Format.ToString(), false, true); + + ImGui.TableNextColumn(); + var bytes = wrap.RawSpecs.EstimatedBytes; + this.TextCopiable(bytes < 0 ? "?" : $"{bytes:n0}", true, true); + ImGui.TableNextColumn(); lock (wrap.OwnerPlugins) - { - foreach (var plugin in wrap.OwnerPlugins) - ImGui.TextUnformatted(plugin.Name); - } + this.TextCopiable(string.Join(", ", wrap.OwnerPlugins.Select(static x => x.Name)), false, true); ImGui.PopID(); } @@ -468,16 +568,16 @@ internal class TexWidget : IDataWindowWidget ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - this.TextRightAlign($"{texture.InstanceIdForDebug:n0}"); + this.TextCopiable($"{texture.InstanceIdForDebug:n0}", true, true); ImGui.TableNextColumn(); - this.TextCopiable(texture.SourcePathForDebug, true); + this.TextCopiable(texture.SourcePathForDebug, false, true); ImGui.TableNextColumn(); - this.TextRightAlign($"{texture.RefCountForDebug:n0}"); + this.TextCopiable($"{texture.RefCountForDebug:n0}", true, true); ImGui.TableNextColumn(); - this.TextRightAlign(remain <= 0 ? "-" : $"{remain:00.000}"); + this.TextCopiable(remain <= 0 ? "-" : $"{remain:00.000}", true, true); ImGui.TableNextColumn(); ImGui.TextUnformatted(texture.HasRevivalPossibility ? "Yes" : "No"); @@ -486,7 +586,10 @@ internal class TexWidget : IDataWindowWidget if (ImGuiComponents.IconButton(FontAwesomeIcon.Save)) { var name = Path.ChangeExtension(Path.GetFileName(texture.SourcePathForDebug), null); - this.SaveTextureAsync(name, () => texture.RentAsync()); + _ = Service.Get().ShowTextureSaveMenuAsync( + this.DisplayName, + name, + texture.RentAsync()); } if (ImGui.IsItemHovered() && texture.GetWrapOrDefault(null) is { } immediate) @@ -783,119 +886,24 @@ internal class TexWidget : IDataWindowWidget ImGuiHelpers.ScaledDummy(10); } - private async void SaveTextureAsync(string name, Func> textureGetter) - { - try - { - BitmapCodecInfo encoder; - { - var off = ImGui.GetCursorScreenPos(); - var first = true; - var encoders = this.textureManager - .Wic - .GetSupportedEncoderInfos() - .ToList(); - var tcs = new TaskCompletionSource(); - Service.Get().Draw += DrawChoices; - - encoder = await tcs.Task; - - void DrawChoices() - { - if (first) - { - ImGui.OpenPopup(nameof(this.SaveTextureAsync)); - first = false; - } - - ImGui.SetNextWindowPos(off, ImGuiCond.Appearing); - if (!ImGui.BeginPopup( - nameof(this.SaveTextureAsync), - ImGuiWindowFlags.AlwaysAutoResize | - ImGuiWindowFlags.NoTitleBar | - ImGuiWindowFlags.NoSavedSettings)) - { - Service.Get().Draw -= DrawChoices; - tcs.TrySetCanceled(); - return; - } - - foreach (var encoder2 in encoders) - { - if (ImGui.Selectable(encoder2.Name)) - tcs.TrySetResult(encoder2); - } - - if (tcs.Task.IsCompleted) - ImGui.CloseCurrentPopup(); - - ImGui.EndPopup(); - } - } - - string path; - { - var tcs = new TaskCompletionSource(); - this.fileDialogManager.SaveFileDialog( - "Save texture...", - $"{encoder.Name.Replace(',', '.')}{{{string.Join(',', encoder.Extensions)}}}", - name + encoder.Extensions.First(), - encoder.Extensions.First(), - (ok, path2) => - { - if (!ok) - tcs.SetCanceled(); - else - tcs.SetResult(path2); - }); - path = await tcs.Task.ConfigureAwait(false); - } - - using var textureWrap = await textureGetter.Invoke(); - var props = new Dictionary(); - if (encoder.ContainerGuid == GUID.GUID_ContainerFormatTiff) - props["CompressionQuality"] = 1.0f; - else if (encoder.ContainerGuid == GUID.GUID_ContainerFormatJpeg || - encoder.ContainerGuid == GUID.GUID_ContainerFormatHeif || - encoder.ContainerGuid == GUID.GUID_ContainerFormatWmp) - props["ImageQuality"] = 1.0f; - await this.textureManager.SaveToFileAsync( - textureWrap, - encoder.ContainerGuid, - path, - props: props); - - Service.Get().AddNotification( - $"File saved to: {path}", - this.DisplayName, - NotificationType.Success); - } - catch (Exception e) - { - if (e is OperationCanceledException) - return; - - Log.Error(e, $"{nameof(TexWidget)}.{nameof(this.SaveTextureAsync)}"); - Service.Get().AddNotification( - $"Failed to save file: {e}", - this.DisplayName, - NotificationType.Error); - } - } - - private void TextRightAlign(string s) - { - var width = ImGui.CalcTextSize(s).X; - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetColumnWidth() - width); - ImGui.TextUnformatted(s); - } - - private void TextCopiable(string s, bool framepad = false) + private void TextCopiable(string s, bool alignRight, bool framepad) { var offset = ImGui.GetCursorScreenPos() + new Vector2(0, framepad ? ImGui.GetStyle().FramePadding.Y : 0); if (framepad) ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted(s); + if (alignRight) + { + var width = ImGui.CalcTextSize(s).X; + var xoff = ImGui.GetColumnWidth() - width; + offset.X += xoff; + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + xoff); + ImGui.TextUnformatted(s); + } + else + { + ImGui.TextUnformatted(s); + } + if (ImGui.IsItemHovered()) { ImGui.SetNextWindowPos(offset - ImGui.GetStyle().WindowPadding); diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs index f091e3164..d2e513c1b 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs @@ -591,7 +591,7 @@ internal sealed partial class FontAtlasFactory { ref var texture = ref textureSpan[i]; var name = - $"FontAtlas.{ this.data.Owner?.Name ?? "(no owner or name)"}[0x{(long)this.data.Atlas.NativePtr:X}][{i}]"; + $"{nameof(FontAtlasBuiltData)}[{this.data.Owner?.Name ?? "-"}][0x{(long)this.data.Atlas.NativePtr:X}][{i}]"; if (texture.TexID != 0) { // Nothing to do @@ -602,6 +602,7 @@ internal sealed partial class FontAtlasFactory RawImageSpecification.Rgba32(width, height), new(texture.TexPixelsRGBA32, width * height * 4), name); + this.factory.TextureManager.Blame(wrap, this.data.Owner?.OwnerPlugin); this.data.AddExistingTexture(wrap); texture.TexID = wrap.ImGuiHandle; } @@ -647,6 +648,7 @@ internal sealed partial class FontAtlasFactory width * bpp), buf, name); + this.factory.TextureManager.Blame(wrap, this.data.Owner?.OwnerPlugin); this.data.AddExistingTexture(wrap); texture.TexID = wrap.ImGuiHandle; continue; diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs index 883fcbbfc..9a6520e25 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs @@ -12,6 +12,7 @@ using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal; using Dalamud.Interface.Utility; using Dalamud.Logging.Internal; +using Dalamud.Plugin.Internal.Types; using Dalamud.Utility; using ImGuiNET; @@ -256,12 +257,15 @@ internal sealed partial class FontAtlasFactory /// Name of atlas, for debugging and logging purposes. /// Specify how to auto rebuild. /// Whether the fonts in the atlas are under the effect of global scale. + /// The owner plugin, if any. public DalamudFontAtlas( FontAtlasFactory factory, string atlasName, FontAtlasAutoRebuildMode autoRebuildMode, - bool isGlobalScaled) + bool isGlobalScaled, + LocalPlugin? ownerPlugin) { + this.OwnerPlugin = ownerPlugin; this.IsGlobalScaled = isGlobalScaled; try { @@ -355,6 +359,9 @@ internal sealed partial class FontAtlasFactory /// public bool IsGlobalScaled { get; } + /// Gets the owner plugin, if any. + public LocalPlugin? OwnerPlugin { get; } + /// public void Dispose() { diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs index 59df710d6..aaf031cba 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs @@ -12,6 +12,7 @@ using Dalamud.Interface.FontIdentifier; using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal; using Dalamud.Interface.Textures.Internal; +using Dalamud.Plugin.Internal.Types; using Dalamud.Storage.Assets; using Dalamud.Utility; @@ -178,12 +179,14 @@ internal sealed partial class FontAtlasFactory /// Name of atlas, for debugging and logging purposes. /// Specify how to auto rebuild. /// Whether the fonts in the atlas is global scaled. + /// The owner plugin, if any. /// The new font atlas. public IFontAtlas CreateFontAtlas( string atlasName, FontAtlasAutoRebuildMode autoRebuildMode, - bool isGlobalScaled = true) => - new DalamudFontAtlas(this, atlasName, autoRebuildMode, isGlobalScaled); + bool isGlobalScaled = true, + LocalPlugin? ownerPlugin = null) => + new DalamudFontAtlas(this, atlasName, autoRebuildMode, isGlobalScaled, ownerPlugin); /// /// Adds the font from Dalamud Assets. @@ -363,7 +366,7 @@ internal sealed partial class FontAtlasFactory : DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM), texFile.Header.Width * bpp), buffer, - $"{nameof(FontAtlasFactory)}:{texPathFormat.Format(fileIndex)}:{channelIndex}")); + $"{nameof(FontAtlasFactory)}[{texPathFormat.Format(fileIndex)}][{channelIndex}]")); } finally { diff --git a/Dalamud/Interface/Textures/GameIconLookup.cs b/Dalamud/Interface/Textures/GameIconLookup.cs new file mode 100644 index 000000000..ccc999d56 --- /dev/null +++ b/Dalamud/Interface/Textures/GameIconLookup.cs @@ -0,0 +1,55 @@ +using System.Text; + +namespace Dalamud.Interface.Textures; + +/// Represents a lookup for a game icon. +public readonly record struct GameIconLookup +{ + /// Initializes a new instance of the class. + /// 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. + public GameIconLookup(uint iconId, bool itemHq = false, bool hiRes = true, ClientLanguage? language = null) + { + this.IconId = iconId; + this.ItemHq = itemHq; + this.HiRes = hiRes; + this.Language = language; + } + + public static implicit operator GameIconLookup(int iconId) => new(checked((uint)iconId)); + + public static implicit operator GameIconLookup(uint iconId) => new(iconId); + + /// Gets the icon ID. + public uint IconId { get; init; } + + /// Gets a value indicating whether the HQ icon is requested, where HQ is in the context of items. + public bool ItemHq { get; init; } + + /// Gets a value indicating whether the high-resolution icon is requested. + public bool HiRes { get; init; } + + /// Gets the language of the icon to load. + /// + /// null will use the active game language. + /// If the specified resource does not have variants per language, the language-neutral texture will be used. + /// + /// + public ClientLanguage? Language { get; init; } + + /// + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append(nameof(GameIconLookup)).Append('(').Append(this.IconId); + if (this.ItemHq) + sb.Append(", HQ"); + if (this.HiRes) + sb.Append(", HR1"); + if (this.Language is not null) + sb.Append(", ").Append(Enum.GetName(this.Language.Value)); + return sb.Append(')').ToString(); + } +} diff --git a/Dalamud/Interface/Textures/ImGuiViewportTextureArgs.cs b/Dalamud/Interface/Textures/ImGuiViewportTextureArgs.cs index 8193d65e6..1159f5dbf 100644 --- a/Dalamud/Interface/Textures/ImGuiViewportTextureArgs.cs +++ b/Dalamud/Interface/Textures/ImGuiViewportTextureArgs.cs @@ -1,9 +1,12 @@ using System.Numerics; +using System.Text; using Dalamud.Interface.Internal; using ImGuiNET; +using TerraFX.Interop.DirectX; + namespace Dalamud.Interface.Textures; /// Describes how to take a texture of an existing ImGui viewport. @@ -44,6 +47,30 @@ public record struct ImGuiViewportTextureArgs() /// Gets the effective value of . internal Vector2 Uv1Effective => this.Uv1 == Vector2.Zero ? Vector2.One : this.Uv1; + /// + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append(nameof(ImGuiViewportTextureArgs)).Append('('); + sb.Append($"0x{this.ViewportId:X}"); + if (this.AutoUpdate) + sb.Append($", {nameof(this.AutoUpdate)}"); + if (this.TakeBeforeImGuiRender) + sb.Append($", {nameof(this.TakeBeforeImGuiRender)}"); + if (this.KeepTransparency) + sb.Append($", {nameof(this.KeepTransparency)}"); + + if (this.Uv0 != Vector2.Zero || this.Uv1Effective != Vector2.One) + { + sb.Append(", ") + .Append(this.Uv0.ToString()) + .Append('-') + .Append(this.Uv1.ToString()); + } + + return sb.Append(')').ToString(); + } + /// Checks the properties and throws an exception if values are invalid. internal void ThrowOnInvalidValues() { diff --git a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs index 9e6af982d..69aca5c69 100644 --- a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs +++ b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs @@ -18,6 +18,7 @@ internal sealed class FileSystemSharedImmediateTexture : SharedImmediateTexture /// Creates a new placeholder instance of . /// The path. /// The new instance. + /// Only to be used from . public static SharedImmediateTexture CreatePlaceholder(string path) => new FileSystemSharedImmediateTexture(path); /// diff --git a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs index e33091127..8a1caacd6 100644 --- a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs +++ b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs @@ -22,6 +22,7 @@ internal sealed class GamePathSharedImmediateTexture : SharedImmediateTexture /// Creates a new placeholder instance of . /// The path. /// The new instance. + /// Only to be used from . public static SharedImmediateTexture CreatePlaceholder(string path) => new GamePathSharedImmediateTexture(path); /// diff --git a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs index 525e25159..34ffbaf0e 100644 --- a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs +++ b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs @@ -27,6 +27,8 @@ internal sealed class ManifestResourceSharedImmediateTexture : SharedImmediateTe /// Creates a new placeholder instance of . /// The arguments to pass to the constructor. /// The new instance. + /// Only to be used from . + /// public static SharedImmediateTexture CreatePlaceholder((Assembly Assembly, string Name) args) => new ManifestResourceSharedImmediateTexture(args.Assembly, args.Name); diff --git a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/SharedImmediateTexture.cs b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/SharedImmediateTexture.cs index ed76223a3..a20736e0a 100644 --- a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/SharedImmediateTexture.cs +++ b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/SharedImmediateTexture.cs @@ -6,6 +6,8 @@ using System.Threading; using System.Threading.Tasks; using Dalamud.Interface.Internal; +using Dalamud.Interface.Textures.TextureWraps; +using Dalamud.Interface.Textures.TextureWraps.Internal; using Dalamud.Plugin.Internal.Types; using Dalamud.Storage.Assets; using Dalamud.Utility; @@ -22,6 +24,7 @@ internal abstract class SharedImmediateTexture private static long instanceCounter; private readonly object reviveLock = new(); + private readonly List ownerPlugins = new(); private bool resourceReleased; private int refCount; @@ -43,10 +46,11 @@ internal abstract class SharedImmediateTexture this.IsOpportunistic = true; this.resourceReleased = true; this.FirstRequestedTick = this.LatestRequestedTick = Environment.TickCount64; + this.PublicUseInstance = new(this); } - /// Gets the list of owner plugins. - public List OwnerPlugins { get; } = new(); + /// Gets a wrapper for this instance which disables resource reference management. + public PureImpl PublicUseInstance { get; } /// Gets the instance ID. Debug use only. public long InstanceIdForDebug { get; } @@ -280,15 +284,15 @@ internal abstract class SharedImmediateTexture return this.availableOnAccessWrapForApi9; } - /// Adds a plugin to , in a thread-safe way. + /// Adds a plugin to , in a thread-safe way. /// The plugin to add. public void AddOwnerPlugin(LocalPlugin plugin) { - lock (this.OwnerPlugins) + lock (this.ownerPlugins) { - if (!this.OwnerPlugins.Contains(plugin)) + if (!this.ownerPlugins.Contains(plugin)) { - this.OwnerPlugins.Add(plugin); + this.ownerPlugins.Add(plugin); this.UnderlyingWrap?.ContinueWith( r => { @@ -314,14 +318,14 @@ internal abstract class SharedImmediateTexture protected void LoadUnderlyingWrap() { int addLen; - lock (this.OwnerPlugins) + lock (this.ownerPlugins) { this.UnderlyingWrap = Service.Get().DynamicPriorityTextureLoader.LoadAsync( this, this.CreateTextureAsync, this.LoadCancellationToken); - addLen = this.OwnerPlugins.Count; + addLen = this.ownerPlugins.Count; } if (addLen == 0) @@ -331,8 +335,11 @@ internal abstract class SharedImmediateTexture { if (!r.IsCompletedSuccessfully) return; - foreach (var op in this.OwnerPlugins.Take(addLen)) - Service.Get().Blame(r.Result, op); + lock (this.ownerPlugins) + { + foreach (var op in this.ownerPlugins.Take(addLen)) + Service.Get().Blame(r.Result, op); + } }, default(CancellationToken)); } @@ -427,6 +434,52 @@ internal abstract class SharedImmediateTexture } } + /// A wrapper around , to prevent external consumers from mistakenly + /// calling or . + internal sealed class PureImpl : ISharedImmediateTexture + { + private readonly SharedImmediateTexture inner; + + /// Initializes a new instance of the class. + /// The actual instance. + public PureImpl(SharedImmediateTexture inner) => this.inner = inner; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public IDalamudTextureWrap GetWrapOrEmpty() => + this.inner.GetWrapOrEmpty(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [return: NotNullIfNotNull(nameof(defaultWrap))] + public IDalamudTextureWrap? GetWrapOrDefault(IDalamudTextureWrap? defaultWrap = null) => + this.inner.GetWrapOrDefault(defaultWrap); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetWrap([NotNullWhen(true)] out IDalamudTextureWrap? texture, out Exception? exception) => + this.inner.TryGetWrap(out texture, out exception); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Task RentAsync(CancellationToken cancellationToken = default) => + this.inner.RentAsync(cancellationToken); + + /// + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public IDalamudTextureWrap? GetAvailableOnAccessWrapForApi9() => + this.inner.GetAvailableOnAccessWrapForApi9(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AddOwnerPlugin(LocalPlugin plugin) => + this.inner.AddOwnerPlugin(plugin); + + /// + public override string ToString() => $"{this.inner}({nameof(PureImpl)})"; + } + /// Same with , but with a custom implementation of /// . private sealed class NotOwnedTextureWrap : DisposeSuppressingTextureWrap diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.BlameTracker.cs b/Dalamud/Interface/Textures/Internal/TextureManager.BlameTracker.cs index 5e47d2c8e..a306b7c64 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.BlameTracker.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.BlameTracker.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -18,11 +17,12 @@ namespace Dalamud.Interface.Textures.Internal; /// Service responsible for loading and disposing ImGui texture wraps. internal sealed partial class TextureManager { - private readonly List blameTracker = new(); - /// A wrapper for underlying texture2D resources. public interface IBlameableDalamudTextureWrap : IDalamudTextureWrap { + /// Gets the address of the native resource. + public nint ResourceAddress { get; } + /// Gets the name of the underlying resource of this texture wrap. public string Name { get; } @@ -31,13 +31,27 @@ internal sealed partial class TextureManager /// Gets the list of owner plugins. public List OwnerPlugins { get; } + + /// Gets the raw image specification. + public RawImageSpecification RawSpecs { get; } + + /// Tests whether the tag and the underlying resource are released or should be released. + /// true if there are no more remaining references to this instance. + bool TestIsReleasedOrShouldRelease(); } - /// Gets all the loaded textures from plugins. - /// The enumerable that goes through all textures and relevant plugins. + /// Gets the list containing all the loaded textures from plugins. /// Returned value must be used inside a lock. - [SuppressMessage("ReSharper", "InconsistentlySynchronizedField", Justification = "Caller locks the return value.")] - public IReadOnlyList AllBlamesForDebug => this.blameTracker; + public List BlameTracker { get; } = new(); + + /// Gets the blame for a texture wrap. + /// The texture wrap. + /// The blame, if it exists. + public unsafe IBlameableDalamudTextureWrap? GetBlame(IDalamudTextureWrap textureWrap) + { + using var wrapAux = new WrapAux(textureWrap, true); + return BlameTag.Get(wrapAux.ResPtr); + } /// Puts a plugin on blame for a texture. /// The texture. @@ -59,7 +73,7 @@ internal sealed partial class TextureManager } using var wrapAux = new WrapAux(textureWrap, true); - var blame = BlameTag.From(wrapAux.ResPtr, out var isNew); + var blame = BlameTag.GetOrCreate(wrapAux.ResPtr, out var isNew); if (ownerPlugin is not null) { @@ -69,8 +83,8 @@ internal sealed partial class TextureManager if (isNew) { - lock (this.blameTracker) - this.blameTracker.Add(blame); + lock (this.BlameTracker) + this.BlameTracker.Add(blame); } return textureWrap; @@ -96,13 +110,13 @@ internal sealed partial class TextureManager } using var wrapAux = new WrapAux(textureWrap, true); - var blame = BlameTag.From(wrapAux.ResPtr, out var isNew); - blame.Name = name; + var blame = BlameTag.GetOrCreate(wrapAux.ResPtr, out var isNew); + blame.Name = name.Length <= 1024 ? name : $"{name[..1024]}..."; if (isNew) { - lock (this.blameTracker) - this.blameTracker.Add(blame); + lock (this.BlameTracker) + this.BlameTracker.Add(blame); } return textureWrap; @@ -110,15 +124,15 @@ internal sealed partial class TextureManager private void BlameTrackerUpdate(IFramework unused) { - lock (this.blameTracker) + lock (this.BlameTracker) { - for (var i = 0; i < this.blameTracker.Count;) + for (var i = 0; i < this.BlameTracker.Count;) { - var entry = this.blameTracker[i]; + var entry = this.BlameTracker[i]; if (entry.TestIsReleasedOrShouldRelease()) { - this.blameTracker[i] = this.blameTracker[^1]; - this.blameTracker.RemoveAt(this.blameTracker.Count - 1); + this.BlameTracker[i] = this.BlameTracker[^1]; + this.BlameTracker.RemoveAt(this.BlameTracker.Count - 1); } else { @@ -220,12 +234,22 @@ internal sealed partial class TextureManager /// public List OwnerPlugins { get; } = new(); + /// + public nint ResourceAddress => (nint)this.tex2D; + /// public string Name { get; set; } = ""; /// public DXGI_FORMAT Format => this.desc.Format; + /// + public RawImageSpecification RawSpecs => new( + (int)this.desc.Width, + (int)this.desc.Height, + (int)this.desc.Format, + 0); + /// public IntPtr ImGuiHandle { @@ -267,7 +291,23 @@ internal sealed partial class TextureManager /// true if the tracker is new. /// A COM object type. /// A new instance of . - public static BlameTag From(T* trackWhat, out bool isNew) where T : unmanaged, IUnknown.Interface + public static BlameTag GetOrCreate(T* trackWhat, out bool isNew) where T : unmanaged, IUnknown.Interface + { + if (Get(trackWhat) is { } v) + { + isNew = false; + return v; + } + + isNew = true; + return new((IUnknown*)trackWhat); + } + + /// Gets an existing instance of for the given resource. + /// The COM object to track. + /// A COM object type. + /// An existing instance of . + public static BlameTag? Get(T* trackWhat) where T : unmanaged, IUnknown.Interface { using var deviceChild = default(ComPtr); fixed (Guid* piid = &IID.IID_ID3D11DeviceChild) @@ -282,18 +322,15 @@ internal sealed partial class TextureManager if (ToManagedObject(existingTag) is { } existingTagInstance) { existingTagInstance.Release(); - isNew = false; return existingTagInstance; } } } - isNew = true; - return new((IUnknown*)trackWhat); + return null; } - /// Tests whether the tag and the underlying resource are released or should be released. - /// true if there are no more remaining references to this instance. + /// public bool TestIsReleasedOrShouldRelease() { if (this.srvDebugPreviewExpiryTick <= Environment.TickCount64) diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs b/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs index 9d10457dc..7e9b209cf 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs @@ -3,6 +3,7 @@ using System.Threading; using System.Threading.Tasks; using Dalamud.Interface.Internal; +using Dalamud.Interface.Textures.TextureWraps.Internal; using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; using Dalamud.Utility; @@ -82,7 +83,7 @@ internal sealed partial class TextureManager this.BlameSetName( outWrap, debugName ?? - $"{nameof(this.CreateFromExistingTextureAsync)}({nameof(wrap)}, {nameof(args)}, {nameof(leaveWrapOpen)}, {nameof(cancellationToken)})"); + $"{nameof(this.CreateFromExistingTextureAsync)}({wrap}, {args})"); return outWrap; } }, @@ -136,59 +137,57 @@ internal sealed partial class TextureManager } cancellationToken.ThrowIfCancellationRequested(); - return await this.interfaceManager.RunBeforeImGuiRender( - () => ExtractMappedResource(wrapAux, tex2D, cancellationToken)); + + // ID3D11DeviceContext is not a threadsafe resource, and it must be used from the UI thread. + return await this.RunDuringPresent(() => ExtractMappedResource(tex2D, cancellationToken)); static unsafe (RawImageSpecification Specification, byte[] RawData) ExtractMappedResource( - in WrapAux wrapAux, ComPtr tex2D, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - ID3D11Resource* mapWhat = null; + D3D11_TEXTURE2D_DESC desc; + tex2D.Get()->GetDesc(&desc); + + using var device = default(ComPtr); + tex2D.Get()->GetDevice(device.GetAddressOf()); + using var context = default(ComPtr); + device.Get()->GetImmediateContext(context.GetAddressOf()); + + using var tmpTex = default(ComPtr); + if ((desc.CPUAccessFlags & (uint)D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_READ) == 0) + { + var tmpTexDesc = desc with + { + MipLevels = 1, + ArraySize = 1, + SampleDesc = new(1, 0), + Usage = D3D11_USAGE.D3D11_USAGE_STAGING, + BindFlags = 0u, + CPUAccessFlags = (uint)D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_READ, + MiscFlags = 0u, + }; + device.Get()->CreateTexture2D(&tmpTexDesc, null, tmpTex.GetAddressOf()).ThrowOnError(); + context.Get()->CopyResource((ID3D11Resource*)tmpTex.Get(), (ID3D11Resource*)tex2D.Get()); + + cancellationToken.ThrowIfCancellationRequested(); + } + + var mapWhat = (ID3D11Resource*)(tmpTex.IsEmpty() ? tex2D.Get() : tmpTex.Get()); + + D3D11_MAPPED_SUBRESOURCE mapped; + context.Get()->Map(mapWhat, 0, D3D11_MAP.D3D11_MAP_READ, 0, &mapped).ThrowOnError(); + try { - using var tmpTex = default(ComPtr); - if ((wrapAux.Desc.CPUAccessFlags & (uint)D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_READ) == 0) - { - var tmpTexDesc = wrapAux.Desc with - { - MipLevels = 1, - ArraySize = 1, - SampleDesc = new(1, 0), - Usage = D3D11_USAGE.D3D11_USAGE_STAGING, - BindFlags = 0u, - CPUAccessFlags = (uint)D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_READ, - MiscFlags = 0u, - }; - wrapAux.DevPtr->CreateTexture2D(&tmpTexDesc, null, tmpTex.GetAddressOf()).ThrowOnError(); - wrapAux.CtxPtr->CopyResource((ID3D11Resource*)tmpTex.Get(), (ID3D11Resource*)tex2D.Get()); - - cancellationToken.ThrowIfCancellationRequested(); - } - - D3D11_MAPPED_SUBRESOURCE mapped; - mapWhat = (ID3D11Resource*)(tmpTex.IsEmpty() ? tex2D.Get() : tmpTex.Get()); - wrapAux.CtxPtr->Map( - mapWhat, - 0, - D3D11_MAP.D3D11_MAP_READ, - 0, - &mapped).ThrowOnError(); - - var specs = new RawImageSpecification( - (int)wrapAux.Desc.Width, - (int)wrapAux.Desc.Height, - (int)wrapAux.Desc.Format, - (int)mapped.RowPitch); + var specs = new RawImageSpecification(desc, mapped.RowPitch); var bytes = new Span(mapped.pData, checked((int)mapped.DepthPitch)).ToArray(); return (specs, bytes); } finally { - if (mapWhat is not null) - wrapAux.CtxPtr->Unmap(mapWhat, 0); + context.Get()->Unmap(mapWhat, 0); } } } @@ -226,35 +225,34 @@ internal sealed partial class TextureManager this.device.Get()->CreateTexture2D(&tex2DCopyTempDesc, null, tex2DCopyTemp.GetAddressOf()).ThrowOnError(); } - await this.interfaceManager.RunBeforeImGuiRender( - () => - { - unsafe - { - using var rtvCopyTemp = default(ComPtr); - var rtvCopyTempDesc = new D3D11_RENDER_TARGET_VIEW_DESC( - tex2DCopyTemp, - D3D11_RTV_DIMENSION.D3D11_RTV_DIMENSION_TEXTURE2D); - this.device.Get()->CreateRenderTargetView( - (ID3D11Resource*)tex2DCopyTemp.Get(), - &rtvCopyTempDesc, - rtvCopyTemp.GetAddressOf()).ThrowOnError(); - - wrapAux.CtxPtr->OMSetRenderTargets(1u, rtvCopyTemp.GetAddressOf(), null); - this.SimpleDrawer.Draw( - wrapAux.CtxPtr, - wrapAux.SrvPtr, - args.Uv0, - args.Uv1Effective); - if (args.MakeOpaque) - this.SimpleDrawer.StripAlpha(wrapAux.CtxPtr); - - var dummy = default(ID3D11RenderTargetView*); - wrapAux.CtxPtr->OMSetRenderTargets(1u, &dummy, null); - } - }); + await this.RunDuringPresent(() => DrawSourceTextureToTarget(wrapAux, args, this.SimpleDrawer, tex2DCopyTemp)); return new(tex2DCopyTemp); + + static unsafe void DrawSourceTextureToTarget( + WrapAux wrapAux, + TextureModificationArgs args, + SimpleDrawerImpl simpleDrawer, + ComPtr tex2DCopyTemp) + { + using var rtvCopyTemp = default(ComPtr); + var rtvCopyTempDesc = new D3D11_RENDER_TARGET_VIEW_DESC( + tex2DCopyTemp, + D3D11_RTV_DIMENSION.D3D11_RTV_DIMENSION_TEXTURE2D); + wrapAux.DevPtr->CreateRenderTargetView( + (ID3D11Resource*)tex2DCopyTemp.Get(), + &rtvCopyTempDesc, + rtvCopyTemp.GetAddressOf()) + .ThrowOnError(); + + wrapAux.CtxPtr->OMSetRenderTargets(1u, rtvCopyTemp.GetAddressOf(), null); + simpleDrawer.Draw(wrapAux.CtxPtr, wrapAux.SrvPtr, args.Uv0, args.Uv1Effective); + if (args.MakeOpaque) + simpleDrawer.StripAlpha(wrapAux.CtxPtr); + + var dummy = default(ID3D11RenderTargetView*); + wrapAux.CtxPtr->OMSetRenderTargets(1u, &dummy, null); + } } /// Auxiliary data from . diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.SharedTextures.cs b/Dalamud/Interface/Textures/Internal/TextureManager.SharedTextures.cs index 71f300479..9a7d84deb 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.SharedTextures.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.SharedTextures.cs @@ -88,25 +88,28 @@ internal sealed partial class TextureManager /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public SharedImmediateTexture GetFromGameIcon(in GameIconLookup lookup) => + public SharedImmediateTexture.PureImpl GetFromGameIcon(in GameIconLookup lookup) => this.GetFromGame(this.lookupCache.GetOrAdd(lookup, this.GetIconPathByValue)); /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public SharedImmediateTexture GetFromGame(string path) => - this.gameDict.GetOrAdd(path, GamePathSharedImmediateTexture.CreatePlaceholder); + public SharedImmediateTexture.PureImpl GetFromGame(string path) => + this.gameDict.GetOrAdd(path, GamePathSharedImmediateTexture.CreatePlaceholder) + .PublicUseInstance; /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public SharedImmediateTexture GetFromFile(string path) => - this.fileDict.GetOrAdd(path, FileSystemSharedImmediateTexture.CreatePlaceholder); + public SharedImmediateTexture.PureImpl GetFromFile(string path) => + this.fileDict.GetOrAdd(path, FileSystemSharedImmediateTexture.CreatePlaceholder) + .PublicUseInstance; /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public SharedImmediateTexture GetFromManifestResource(Assembly assembly, string name) => + public SharedImmediateTexture.PureImpl GetFromManifestResource(Assembly assembly, string name) => this.manifestResourceDict.GetOrAdd( (assembly, name), - ManifestResourceSharedImmediateTexture.CreatePlaceholder); + ManifestResourceSharedImmediateTexture.CreatePlaceholder) + .PublicUseInstance; /// Invalidates a cached item from and . /// diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs b/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs index bc10e5b77..40b9d3ee0 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs @@ -318,6 +318,7 @@ internal sealed partial class TextureManager { // See https://github.com/microsoft/DirectXTex/wiki/WIC-I-O-Functions#savetowicmemory-savetowicfile DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_FLOAT => GUID.GUID_WICPixelFormat128bppRGBAFloat, + DXGI_FORMAT.DXGI_FORMAT_R32G32B32_FLOAT => GUID.GUID_WICPixelFormat96bppRGBFloat, DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_FLOAT => GUID.GUID_WICPixelFormat64bppRGBAHalf, DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_UNORM => GUID.GUID_WICPixelFormat64bppRGBA, DXGI_FORMAT.DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM => GUID.GUID_WICPixelFormat32bppRGBA1010102XR, @@ -497,10 +498,10 @@ internal sealed partial class TextureManager var outPixelFormat = specs.Format switch { DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_FLOAT => GUID.GUID_WICPixelFormat128bppRGBAFloat, - DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_FLOAT when !this.wicFactory2.IsEmpty() => - GUID.GUID_WICPixelFormat128bppRGBAFloat, - DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_FLOAT => GUID.GUID_WICPixelFormat32bppBGRA, + DXGI_FORMAT.DXGI_FORMAT_R32G32B32_FLOAT => GUID.GUID_WICPixelFormat96bppRGBFloat, + DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_FLOAT => GUID.GUID_WICPixelFormat128bppRGBAFloat, DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_UNORM => GUID.GUID_WICPixelFormat64bppBGRA, + DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM => GUID.GUID_WICPixelFormat24bppBGR, DXGI_FORMAT.DXGI_FORMAT_B5G5R5A1_UNORM => GUID.GUID_WICPixelFormat16bppBGRA5551, DXGI_FORMAT.DXGI_FORMAT_B5G6R5_UNORM => GUID.GUID_WICPixelFormat16bppBGR565, DXGI_FORMAT.DXGI_FORMAT_R32_FLOAT => GUID.GUID_WICPixelFormat8bppGray, @@ -508,9 +509,25 @@ internal sealed partial class TextureManager DXGI_FORMAT.DXGI_FORMAT_R16_UNORM => GUID.GUID_WICPixelFormat8bppGray, DXGI_FORMAT.DXGI_FORMAT_R8_UNORM => GUID.GUID_WICPixelFormat8bppGray, DXGI_FORMAT.DXGI_FORMAT_A8_UNORM => GUID.GUID_WICPixelFormat8bppGray, - _ => GUID.GUID_WICPixelFormat24bppBGR, + _ => GUID.GUID_WICPixelFormat32bppBGRA, }; + var accepted = false; + foreach (var pfi in new ComponentEnumerable( + this.wicFactory, + WICComponentType.WICPixelFormat)) + { + Guid tmp; + if (pfi.Get()->GetFormatGUID(&tmp).FAILED) + continue; + accepted = tmp == outPixelFormat; + if (accepted) + break; + } + + if (!accepted) + outPixelFormat = GUID.GUID_WICPixelFormat32bppBGRA; + encoder.Get()->Initialize(stream, WICBitmapEncoderCacheOption.WICBitmapEncoderNoCache) .ThrowOnError(); cancellationToken.ThrowIfCancellationRequested(); @@ -613,8 +630,7 @@ internal sealed partial class TextureManager private readonly WICComponentType componentType; /// Initializes a new instance of the struct. - /// The WIC factory. Ownership is not transferred. - /// + /// The WIC factory. Ownership is not transferred. /// The component type to enumerate. public ComponentEnumerable(ComPtr factory, WICComponentType componentType) { diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.cs b/Dalamud/Interface/Textures/Internal/TextureManager.cs index f69f01877..7510ec7dc 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.cs @@ -8,6 +8,7 @@ using Dalamud.Data; using Dalamud.Game; using Dalamud.Interface.Internal; using Dalamud.Interface.Textures.Internal.SharedImmediateTextures; +using Dalamud.Interface.Textures.TextureWraps.Internal; using Dalamud.Logging.Internal; using Dalamud.Plugin.Services; using Dalamud.Utility; @@ -127,7 +128,7 @@ internal sealed partial class TextureManager this.BlameSetName( this.NoThrottleCreateFromImage(bytes.ToArray(), ct), debugName ?? - $"{nameof(this.CreateFromImageAsync)}({nameof(bytes)}, {nameof(cancellationToken)})"), + $"{nameof(this.CreateFromImageAsync)}({bytes.Length:n0}b)"), ct), cancellationToken); @@ -146,7 +147,7 @@ internal sealed partial class TextureManager return this.BlameSetName( this.NoThrottleCreateFromImage(ms.GetBuffer(), ct), debugName ?? - $"{nameof(this.CreateFromImageAsync)}({nameof(stream)}, {nameof(leaveOpen)}, {nameof(cancellationToken)})"); + $"{nameof(this.CreateFromImageAsync)}(stream)"); }, cancellationToken, leaveOpen ? null : stream); @@ -159,7 +160,7 @@ internal sealed partial class TextureManager string? debugName = null) => this.BlameSetName( this.NoThrottleCreateFromRaw(specs, bytes), - debugName ?? $"{nameof(this.CreateFromRaw)}({nameof(specs)}, {nameof(bytes)})"); + debugName ?? $"{nameof(this.CreateFromRaw)}({specs}, {bytes.Length:n0})"); /// public Task CreateFromRawAsync( @@ -173,7 +174,7 @@ internal sealed partial class TextureManager this.BlameSetName( this.NoThrottleCreateFromRaw(specs, bytes.Span), debugName ?? - $"{nameof(this.CreateFromRawAsync)}({nameof(specs)}, {nameof(bytes)}, {nameof(cancellationToken)})")), + $"{nameof(this.CreateFromRawAsync)}({specs}, {bytes.Length:n0})")), cancellationToken); /// @@ -192,7 +193,7 @@ internal sealed partial class TextureManager return this.BlameSetName( this.NoThrottleCreateFromRaw(specs, ms.GetBuffer().AsSpan(0, (int)ms.Length)), debugName ?? - $"{nameof(this.CreateFromRawAsync)}({nameof(specs)}, {nameof(stream)}, {nameof(leaveOpen)}, {nameof(cancellationToken)})"); + $"{nameof(this.CreateFromRawAsync)}({specs}, stream)"); }, cancellationToken, leaveOpen ? null : stream); @@ -207,15 +208,19 @@ internal sealed partial class TextureManager public Task CreateFromTexFileAsync( TexFile file, string? debugName = null, - CancellationToken cancellationToken = default) => - this.DynamicPriorityTextureLoader.LoadAsync( + CancellationToken cancellationToken = default) + { + return this.DynamicPriorityTextureLoader.LoadAsync( null, _ => Task.FromResult( this.BlameSetName( this.NoThrottleCreateFromTexFile(file), - debugName ?? $"{nameof(this.CreateFromTexFile)}({nameof(file)})")), + debugName ?? $"{nameof(this.CreateFromTexFile)}({ForceNullable(file.FilePath)?.Path})")), cancellationToken); + static T? ForceNullable(T s) => s; + } + /// bool ITextureProvider.IsDxgiFormatSupported(int dxgiFormat) => this.IsDxgiFormatSupported((DXGI_FORMAT)dxgiFormat); @@ -267,7 +272,7 @@ internal sealed partial class TextureManager .ThrowOnError(); var wrap = new UnknownTextureWrap((IUnknown*)view.Get(), specs.Width, specs.Height, true); - this.BlameSetName(wrap, $"{nameof(this.NoThrottleCreateFromRaw)}({nameof(specs)}, {nameof(bytes)})"); + this.BlameSetName(wrap, $"{nameof(this.NoThrottleCreateFromRaw)}({specs}, {bytes.Length:n0})"); return wrap; } @@ -289,8 +294,10 @@ internal sealed partial class TextureManager } var wrap = this.NoThrottleCreateFromRaw(new(buffer.Width, buffer.Height, dxgiFormat), buffer.RawData); - this.BlameSetName(wrap, $"{nameof(this.NoThrottleCreateFromTexFile)}({nameof(file)})"); + this.BlameSetName(wrap, $"{nameof(this.NoThrottleCreateFromTexFile)}({ForceNullable(file.FilePath).Path})"); return wrap; + + static T? ForceNullable(T s) => s; } /// Creates a texture from the given , trying to interpret it as a @@ -315,9 +322,31 @@ internal sealed partial class TextureManager // Note: FileInfo and FilePath are not used from TexFile; skip it. var wrap = this.NoThrottleCreateFromTexFile(tf); - this.BlameSetName(wrap, $"{nameof(this.NoThrottleCreateFromTexFile)}({nameof(fileBytes)})"); + this.BlameSetName(wrap, $"{nameof(this.NoThrottleCreateFromTexFile)}({fileBytes.Length:n0})"); return wrap; } private void ReleaseUnmanagedResources() => this.device.Reset(); + + /// Runs the given action in IDXGISwapChain.Present immediately or waiting as needed. + /// The action to run. + // Not sure why this and the below can't be unconditional RunOnFrameworkThread + private async Task RunDuringPresent(Action action) + { + if (this.interfaceManager.IsInPresent && ThreadSafety.IsMainThread) + action(); + else + await this.interfaceManager.RunBeforeImGuiRender(action); + } + + /// Runs the given function in IDXGISwapChain.Present immediately or waiting as needed. + /// The type of the return value. + /// The function to run. + /// The return value from the function. + private async Task RunDuringPresent(Func func) + { + if (this.interfaceManager.IsInPresent && ThreadSafety.IsMainThread) + return func(); + return await this.interfaceManager.RunBeforeImGuiRender(func); + } } diff --git a/Dalamud/Interface/Textures/RawImageSpecification.cs b/Dalamud/Interface/Textures/RawImageSpecification.cs index 9ac649812..e2bad7e5f 100644 --- a/Dalamud/Interface/Textures/RawImageSpecification.cs +++ b/Dalamud/Interface/Textures/RawImageSpecification.cs @@ -12,7 +12,7 @@ public record struct RawImageSpecification /// The height of the raw image. /// The DXGI format of the raw image. /// The pitch of the raw image in bytes. - /// Specify -1 to calculate it from other parameters. + /// Specify -1 to calculate from other parameters. public RawImageSpecification(int width, int height, int dxgiFormat, int pitch = -1) { if (pitch < 0) @@ -31,6 +31,14 @@ public record struct RawImageSpecification this.DxgiFormat = dxgiFormat; } + /// Initializes a new instance of the class. + /// The source texture description. + /// The pitch of the raw image in bytes. + internal RawImageSpecification(in D3D11_TEXTURE2D_DESC desc, uint pitch) + : this((int)desc.Width, (int)desc.Height, (int)desc.Format, checked((int)pitch)) + { + } + /// Gets or sets the width of the raw image. public int Width { get; set; } @@ -102,6 +110,10 @@ public record struct RawImageSpecification public static RawImageSpecification A8(int width, int height) => new(width, height, (int)DXGI_FORMAT.DXGI_FORMAT_A8_UNORM, width); + /// + public override string ToString() => + $"{nameof(RawImageSpecification)}({this.Width}x{this.Height}, {this.Format}, {this.Pitch}b)"; + private static bool GetFormatInfo(DXGI_FORMAT format, out int bitsPerPixel, out bool isBlockCompression) { switch (format) @@ -246,7 +258,7 @@ public record struct RawImageSpecification return true; case DXGI_FORMAT.DXGI_FORMAT_B4G4R4A4_UNORM: bitsPerPixel = 16; - isBlockCompression = true; + isBlockCompression = false; return true; default: bitsPerPixel = 0; diff --git a/Dalamud/Interface/Textures/TextureModificationArgs.cs b/Dalamud/Interface/Textures/TextureModificationArgs.cs index fac04189c..abccca6b5 100644 --- a/Dalamud/Interface/Textures/TextureModificationArgs.cs +++ b/Dalamud/Interface/Textures/TextureModificationArgs.cs @@ -1,4 +1,5 @@ using System.Numerics; +using System.Text; using Dalamud.Plugin.Services; @@ -52,6 +53,36 @@ public record struct TextureModificationArgs() /// Gets the effective value of . internal Vector2 Uv1Effective => this.Uv1 == Vector2.Zero ? Vector2.One : this.Uv1; + /// + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append(nameof(TextureModificationArgs)).Append('('); + if (this.MakeOpaque) + sb.Append($"{nameof(this.MakeOpaque)}, "); + if (this.Format != DXGI_FORMAT.DXGI_FORMAT_UNKNOWN) + sb.Append(Enum.GetName(this.Format) is { } name ? name[12..] : this.Format.ToString()).Append(", "); + if (this.NewWidth != 0 || this.NewHeight != 0) + { + sb.Append(this.NewWidth == 0 ? "?" : this.NewWidth.ToString()) + .Append('x') + .Append(this.NewHeight == 0 ? "?" : this.NewHeight.ToString()) + .Append(", "); + } + + if (this.Uv0 != Vector2.Zero || this.Uv1Effective != Vector2.One) + { + sb.Append(this.Uv0.ToString()) + .Append('-') + .Append(this.Uv1.ToString()) + .Append(", "); + } + + if (sb[^1] != '(') + sb.Remove(sb.Length - 2, 2); + return sb.Append(')').ToString(); + } + /// Test if this instance of does not instruct to change the /// underlying data of a texture. /// The texture description to test against. diff --git a/Dalamud/Interface/Textures/DalamudTextureWrap.cs b/Dalamud/Interface/Textures/TextureWraps/DalamudTextureWrap.cs similarity index 100% rename from Dalamud/Interface/Textures/DalamudTextureWrap.cs rename to Dalamud/Interface/Textures/TextureWraps/DalamudTextureWrap.cs diff --git a/Dalamud/Interface/Textures/ForwardingTextureWrap.cs b/Dalamud/Interface/Textures/TextureWraps/ForwardingTextureWrap.cs similarity index 96% rename from Dalamud/Interface/Textures/ForwardingTextureWrap.cs rename to Dalamud/Interface/Textures/TextureWraps/ForwardingTextureWrap.cs index c398fc55c..8b0516e03 100644 --- a/Dalamud/Interface/Textures/ForwardingTextureWrap.cs +++ b/Dalamud/Interface/Textures/TextureWraps/ForwardingTextureWrap.cs @@ -3,11 +3,11 @@ using System.Numerics; using System.Runtime.CompilerServices; using Dalamud.Interface.Internal; -using Dalamud.Interface.Textures.Internal; +using Dalamud.Interface.Textures.TextureWraps.Internal; using TerraFX.Interop.Windows; -namespace Dalamud.Interface.Textures; +namespace Dalamud.Interface.Textures.TextureWraps; /// Base class for implementations of that forwards to another. public abstract class ForwardingTextureWrap : IDalamudTextureWrap diff --git a/Dalamud/Interface/Textures/IDalamudTextureWrap.cs b/Dalamud/Interface/Textures/TextureWraps/IDalamudTextureWrap.cs similarity index 96% rename from Dalamud/Interface/Textures/IDalamudTextureWrap.cs rename to Dalamud/Interface/Textures/TextureWraps/IDalamudTextureWrap.cs index c51a69f06..09d64ad21 100644 --- a/Dalamud/Interface/Textures/IDalamudTextureWrap.cs +++ b/Dalamud/Interface/Textures/TextureWraps/IDalamudTextureWrap.cs @@ -2,6 +2,8 @@ using System.Numerics; using Dalamud.Interface.Textures; using Dalamud.Interface.Textures.Internal; +using Dalamud.Interface.Textures.TextureWraps; +using Dalamud.Interface.Textures.TextureWraps.Internal; using TerraFX.Interop.Windows; diff --git a/Dalamud/Interface/Textures/Internal/DisposeSuppressingTextureWrap.cs b/Dalamud/Interface/Textures/TextureWraps/Internal/DisposeSuppressingTextureWrap.cs similarity index 91% rename from Dalamud/Interface/Textures/Internal/DisposeSuppressingTextureWrap.cs rename to Dalamud/Interface/Textures/TextureWraps/Internal/DisposeSuppressingTextureWrap.cs index 88d8d9ca0..0dd5c9f25 100644 --- a/Dalamud/Interface/Textures/Internal/DisposeSuppressingTextureWrap.cs +++ b/Dalamud/Interface/Textures/TextureWraps/Internal/DisposeSuppressingTextureWrap.cs @@ -1,6 +1,6 @@ using Dalamud.Interface.Internal; -namespace Dalamud.Interface.Textures.Internal; +namespace Dalamud.Interface.Textures.TextureWraps.Internal; /// A texture wrap that ignores calls. internal class DisposeSuppressingTextureWrap : ForwardingTextureWrap diff --git a/Dalamud/Interface/Textures/Internal/UnknownTextureWrap.cs b/Dalamud/Interface/Textures/TextureWraps/Internal/UnknownTextureWrap.cs similarity index 88% rename from Dalamud/Interface/Textures/Internal/UnknownTextureWrap.cs rename to Dalamud/Interface/Textures/TextureWraps/Internal/UnknownTextureWrap.cs index c159f9b36..ec23d7d03 100644 --- a/Dalamud/Interface/Textures/Internal/UnknownTextureWrap.cs +++ b/Dalamud/Interface/Textures/TextureWraps/Internal/UnknownTextureWrap.cs @@ -1,11 +1,12 @@ using System.Threading; using Dalamud.Interface.Internal; +using Dalamud.Interface.Textures.Internal; using Dalamud.Utility; using TerraFX.Interop.Windows; -namespace Dalamud.Interface.Textures.Internal; +namespace Dalamud.Interface.Textures.TextureWraps.Internal; /// A texture wrap that is created from an . internal sealed unsafe class UnknownTextureWrap : IDalamudTextureWrap, IDeferredDisposable @@ -50,6 +51,10 @@ internal sealed unsafe class UnknownTextureWrap : IDalamudTextureWrap, IDeferred GC.SuppressFinalize(this); } + /// + public override string ToString() => + $"{nameof(UnknownTextureWrap)}({Service.GetNullable()?.GetBlame(this)?.Name ?? $"{this.imGuiHandle:X}"})"; + /// Actually dispose the wrapped texture. void IDeferredDisposable.RealDispose() { diff --git a/Dalamud/Interface/Textures/Internal/ViewportTextureWrap.cs b/Dalamud/Interface/Textures/TextureWraps/Internal/ViewportTextureWrap.cs similarity index 99% rename from Dalamud/Interface/Textures/Internal/ViewportTextureWrap.cs rename to Dalamud/Interface/Textures/TextureWraps/Internal/ViewportTextureWrap.cs index 77ddc2e34..ad3188925 100644 --- a/Dalamud/Interface/Textures/Internal/ViewportTextureWrap.cs +++ b/Dalamud/Interface/Textures/TextureWraps/Internal/ViewportTextureWrap.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Dalamud.Game; using Dalamud.Interface.Internal; +using Dalamud.Interface.Textures.Internal; using Dalamud.Plugin.Internal.Types; using Dalamud.Storage.Assets; using Dalamud.Utility; @@ -15,7 +16,7 @@ using TerraFX.Interop.Windows; using NotSupportedException = System.NotSupportedException; -namespace Dalamud.Interface.Textures.Internal; +namespace Dalamud.Interface.Textures.TextureWraps.Internal; /// A texture wrap that takes its buffer from the frame buffer (of swap chain). internal sealed class ViewportTextureWrap : IDalamudTextureWrap, IDeferredDisposable diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 3b6a754a9..4263fb1bb 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -32,6 +32,7 @@ namespace Dalamud.Interface; /// public sealed class UiBuilder : IDisposable { + private readonly LocalPlugin plugin; private readonly Stopwatch stopwatch; private readonly HitchDetector hitchDetector; private readonly string namespaceName; @@ -42,6 +43,8 @@ public sealed class UiBuilder : IDisposable private readonly DalamudConfiguration configuration = Service.Get(); private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new(); + + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] private readonly TextureManagerPluginScoped scopedTextureProvider; private bool hasErrorWindow = false; @@ -64,6 +67,7 @@ public sealed class UiBuilder : IDisposable this.stopwatch = new Stopwatch(); this.hitchDetector = new HitchDetector($"UiBuilder({namespaceName})", this.configuration.UiBuilderHitch); this.namespaceName = namespaceName; + this.plugin = plugin; this.interfaceManager.Draw += this.OnDraw; this.scopedFinalizer.Add(() => this.interfaceManager.Draw -= this.OnDraw); @@ -78,7 +82,10 @@ public sealed class UiBuilder : IDisposable .Add( Service .Get() - .CreateFontAtlas(namespaceName, FontAtlasAutoRebuildMode.Disable)); + .CreateFontAtlas( + namespaceName, + FontAtlasAutoRebuildMode.Disable, + ownerPlugin: plugin)); this.FontAtlas.BuildStepChange += this.PrivateAtlasOnBuildStepChange; this.FontAtlas.RebuildRecommend += this.RebuildFonts; } @@ -565,7 +572,8 @@ public sealed class UiBuilder : IDisposable .CreateFontAtlas( this.namespaceName + ":" + (debugName ?? "custom"), autoRebuildMode, - isGlobalScaled)); + isGlobalScaled, + this.plugin)); /// /// Add a notification to the notification queue. diff --git a/Dalamud/Plugin/Services/ITextureProvider.Api9.cs b/Dalamud/Plugin/Services/ITextureProvider.Api9.cs index 71d1bf928..1e33cd371 100644 --- a/Dalamud/Plugin/Services/ITextureProvider.Api9.cs +++ b/Dalamud/Plugin/Services/ITextureProvider.Api9.cs @@ -2,6 +2,7 @@ using Dalamud.Interface; using Dalamud.Interface.Internal; +using Dalamud.Interface.Textures; using Dalamud.Utility; using Lumina.Data.Files; diff --git a/Dalamud/Storage/Assets/DalamudAssetManager.cs b/Dalamud/Storage/Assets/DalamudAssetManager.cs index a8566d5eb..dfb785c92 100644 --- a/Dalamud/Storage/Assets/DalamudAssetManager.cs +++ b/Dalamud/Storage/Assets/DalamudAssetManager.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Dalamud.Interface.Internal; using Dalamud.Interface.Textures.Internal; +using Dalamud.Interface.Textures.TextureWraps.Internal; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Networking.Http; @@ -206,7 +207,7 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA this.cancellationTokenSource.Token); } - for (var j = RenameAttemptCount;; j--) + for (var j = RenameAttemptCount; ; j--) { try { @@ -311,17 +312,13 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA var length = checked((int)stream.Length); buf = ArrayPool.Shared.Rent(length); stream.ReadExactly(buf, 0, length); + var name = $"{nameof(DalamudAsset)}[{Enum.GetName(asset)}]"; var image = purpose switch { - DalamudAssetPurpose.TextureFromPng => await tm.CreateFromImageAsync( - buf, - $"{nameof(DalamudAsset)}.{Enum.GetName(asset)}"), + DalamudAssetPurpose.TextureFromPng => await tm.CreateFromImageAsync(buf, name), DalamudAssetPurpose.TextureFromRaw => asset.GetAttribute() is { } raw - ? await tm.CreateFromRawAsync( - raw.Specification, - buf, - $"{nameof(DalamudAsset)}.{Enum.GetName(asset)}") + ? await tm.CreateFromRawAsync(raw.Specification, buf, name) : throw new InvalidOperationException( "TextureFromRaw must accompany a DalamudAssetRawTextureAttribute."), _ => null, From 20717cce3da1514ee8515a435a75653b76c5b5a6 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Tue, 21 May 2024 09:27:51 +0900 Subject: [PATCH 52/57] more --- .../FilePathNotificationIcon.cs | 15 ++- .../GamePathNotificationIcon.cs | 6 +- .../Interface/Internal/DalamudInterface.cs | 3 +- .../Windows/Data/Widgets/ImGuiWidget.cs | 10 +- .../Windows/Data/Widgets/TexWidget.cs | 34 +------ .../PluginInstaller/PluginInstallerWindow.cs | 1 + .../TextureManager.FromExistingTexture.cs | 99 ++++++++----------- .../Textures/Internal/TextureManager.cs | 53 +++++++++- .../Internal/TextureManagerPluginScoped.cs | 20 +++- .../Textures/RawImageSpecification.cs | 7 ++ Dalamud/Interface/UiBuilder.cs | 84 ---------------- Dalamud/Plugin/Services/ITextureProvider.cs | 15 ++- .../TerraFxCom/TerraFxD3D11Extensions.cs | 65 ++++++++++++ 13 files changed, 213 insertions(+), 199 deletions(-) create mode 100644 Dalamud/Utility/TerraFxCom/TerraFxD3D11Extensions.cs diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/FilePathNotificationIcon.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/FilePathNotificationIcon.cs index 3aa712160..fe68fa7ba 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/FilePathNotificationIcon.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/FilePathNotificationIcon.cs @@ -1,7 +1,6 @@ -using System.IO; using System.Numerics; -using Dalamud.Interface.Internal; +using Dalamud.Interface.Textures.Internal; namespace Dalamud.Interface.ImGuiNotification.Internal.NotificationIcon; @@ -9,26 +8,26 @@ namespace Dalamud.Interface.ImGuiNotification.Internal.NotificationIcon; /// If there was no texture loaded for any reason, the plugin icon will be displayed instead. internal class FilePathNotificationIcon : INotificationIcon { - private readonly FileInfo fileInfo; + private readonly string filePath; /// Initializes a new instance of the class. /// The path to a .tex file inside the game resources. - public FilePathNotificationIcon(string filePath) => this.fileInfo = new(filePath); + public FilePathNotificationIcon(string filePath) => this.filePath = new(filePath); /// public bool DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color) => NotificationUtilities.DrawIconFrom( minCoord, maxCoord, - Service.Get().GetTextureFromFile(this.fileInfo)); + Service.Get().Shared.GetFromFile(this.filePath).GetWrapOrDefault()); /// public override bool Equals(object? obj) => - obj is FilePathNotificationIcon r && r.fileInfo.FullName == this.fileInfo.FullName; + obj is FilePathNotificationIcon r && r.filePath == this.filePath; /// - public override int GetHashCode() => HashCode.Combine(this.GetType().GetHashCode(), this.fileInfo.FullName); + public override int GetHashCode() => HashCode.Combine(this.GetType().GetHashCode(), this.filePath); /// - public override string ToString() => $"{nameof(FilePathNotificationIcon)}({this.fileInfo.FullName})"; + public override string ToString() => $"{nameof(FilePathNotificationIcon)}({this.filePath})"; } diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/GamePathNotificationIcon.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/GamePathNotificationIcon.cs index e0699e1b6..93d515ecc 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/GamePathNotificationIcon.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/GamePathNotificationIcon.cs @@ -1,7 +1,6 @@ using System.Numerics; -using Dalamud.Interface.Internal; -using Dalamud.Plugin.Services; +using Dalamud.Interface.Textures.Internal; namespace Dalamud.Interface.ImGuiNotification.Internal.NotificationIcon; @@ -13,7 +12,6 @@ internal class GamePathNotificationIcon : INotificationIcon /// Initializes a new instance of the class. /// The path to a .tex file inside the game resources. - /// Use to get the game path from icon IDs. public GamePathNotificationIcon(string gamePath) => this.gamePath = gamePath; /// @@ -21,7 +19,7 @@ internal class GamePathNotificationIcon : INotificationIcon NotificationUtilities.DrawIconFrom( minCoord, maxCoord, - Service.Get().GetTextureFromGame(this.gamePath)); + Service.Get().Shared.GetFromGame(this.gamePath).GetWrapOrDefault()); /// public override bool Equals(object? obj) => obj is GamePathNotificationIcon r && r.gamePath == this.gamePath; diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index a9e1a0b0c..da5570848 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -19,8 +19,9 @@ using Dalamud.Hooking; using Dalamud.Interface.Animation.EasingFunctions; using Dalamud.Interface.Colors; using Dalamud.Interface.ImGuiFileDialog; +using Dalamud.Interface.ImGuiNotification; +using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal.ManagedAsserts; -using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Internal.Windows; using Dalamud.Interface.Internal.Windows.Data; using Dalamud.Interface.Internal.Windows.PluginInstaller; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs index 47f0dde64..c1a44b583 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using Dalamud.Game.Text; using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.ImGuiNotification.Internal; +using Dalamud.Interface.Textures.Internal; using Dalamud.Interface.Windowing; using Dalamud.Storage.Assets; using Dalamud.Utility; @@ -230,12 +231,14 @@ internal class ImGuiWidget : IDataWindowWidget break; case 7: n.SetIconTexture( - DisposeLoggingTextureWrap.Wrap(tm.GetTextureFromGame(this.notificationTemplate.IconText)), + DisposeLoggingTextureWrap.Wrap( + tm.Shared.GetFromGame(this.notificationTemplate.IconText).GetWrapOrDefault()), this.notificationTemplate.LeaveTexturesOpen); break; case 8: n.SetIconTexture( - DisposeLoggingTextureWrap.Wrap(tm.GetTextureFromFile(new(this.notificationTemplate.IconText))), + DisposeLoggingTextureWrap.Wrap( + tm.Shared.GetFromFile(this.notificationTemplate.IconText).GetWrapOrDefault()), this.notificationTemplate.LeaveTexturesOpen); break; } @@ -306,7 +309,8 @@ internal class ImGuiWidget : IDataWindowWidget foreach (var n in this.notifications) { var i = (uint)Random.Shared.NextInt64(0, 200000); - n.IconTexture = DisposeLoggingTextureWrap.Wrap(Service.Get().GetIcon(i)); + n.IconTexture = DisposeLoggingTextureWrap.Wrap( + Service.Get().Shared.GetFromGameIcon(new(i)).GetWrapOrDefault()); } } } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs index 4713022e6..85b0dc00d 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -8,7 +8,8 @@ using System.Threading.Tasks; using Dalamud.Configuration.Internal; using Dalamud.Interface.Components; -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; +using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Textures; using Dalamud.Interface.Textures.Internal.SharedImmediateTextures; using Dalamud.Interface.Utility; @@ -90,8 +91,6 @@ internal class TexWidget : IDataWindowWidget /// public bool Ready { get; set; } - private ITextureProvider TextureManagerForApi9 => this.textureManager!; - /// public void Load() { @@ -634,17 +633,6 @@ internal class TexWidget : IDataWindowWidget ImGui.InputText("Icon ID", ref this.iconId, 32); ImGui.Checkbox("HQ Item", ref this.hq); ImGui.Checkbox("Hi-Res", ref this.hiRes); -#pragma warning disable CS0618 // Type or member is obsolete - if (ImGui.Button("Load Icon (API9)")) - { - var flags = ITextureProvider.IconFlags.None; - if (this.hq) - flags |= ITextureProvider.IconFlags.ItemHighQuality; - if (this.hiRes) - flags |= ITextureProvider.IconFlags.HiRes; - this.addedTextures.Add(new(Api9: this.TextureManagerForApi9.GetIcon(uint.Parse(this.iconId), flags))); - } -#pragma warning restore CS0618 // Type or member is obsolete ImGui.SameLine(); if (ImGui.Button("Load Icon (Async)")) @@ -668,11 +656,6 @@ internal class TexWidget : IDataWindowWidget { ImGui.InputText("Tex Path", ref this.inputTexPath, 255); -#pragma warning disable CS0618 // Type or member is obsolete - if (ImGui.Button("Load Tex (API9)")) - this.addedTextures.Add(new(Api9: this.TextureManagerForApi9.GetTextureFromGame(this.inputTexPath))); -#pragma warning restore CS0618 // Type or member is obsolete - ImGui.SameLine(); if (ImGui.Button("Load Tex (Async)")) this.addedTextures.Add(new(Api10: this.textureManager.Shared.GetFromGame(this.inputTexPath).RentAsync())); @@ -688,11 +671,6 @@ internal class TexWidget : IDataWindowWidget { ImGui.InputText("File Path", ref this.inputFilePath, 255); -#pragma warning disable CS0618 // Type or member is obsolete - if (ImGui.Button("Load File (API9)")) - this.addedTextures.Add(new(Api9: this.TextureManagerForApi9.GetTextureFromFile(new(this.inputFilePath)))); -#pragma warning restore CS0618 // Type or member is obsolete - ImGui.SameLine(); if (ImGui.Button("Load File (Async)")) this.addedTextures.Add(new(Api10: this.textureManager.Shared.GetFromFile(this.inputFilePath).RentAsync())); @@ -929,7 +907,6 @@ internal class TexWidget : IDataWindowWidget private record TextureEntry( IDalamudTextureWrap? SharedResource = null, - IDalamudTextureWrap? Api9 = null, Task? Api10 = null, GameIconLookup? Api10ImmGameIcon = null, string? Api10ImmGamePath = null, @@ -943,7 +920,6 @@ internal class TexWidget : IDataWindowWidget public void Dispose() { this.SharedResource?.Dispose(); - this.Api9?.Dispose(); _ = this.Api10?.ToContentDisposedTask(); } @@ -951,8 +927,6 @@ internal class TexWidget : IDataWindowWidget { if (this.SharedResource is not null) return "Unknown error"; - if (this.Api9 is not null) - return "Unknown error"; if (this.Api10 is not null) { return !this.Api10.IsCompleted @@ -975,8 +949,6 @@ internal class TexWidget : IDataWindowWidget { if (this.SharedResource is not null) return this.SharedResource; - if (this.Api9 is not null) - return this.Api9; if (this.Api10 is not null) return this.Api10.IsCompletedSuccessfully ? this.Api10.Result : null; if (this.Api10ImmGameIcon is not null) @@ -1014,8 +986,6 @@ internal class TexWidget : IDataWindowWidget { if (this.SharedResource is not null) return $"{nameof(this.SharedResource)}: {this.SharedResource}"; - if (this.Api9 is not null) - return $"{nameof(this.Api9)}: {this.Api9}"; if (this.Api10 is { IsCompletedSuccessfully: true }) return $"{nameof(this.Api10)}: {this.Api10.Result}"; if (this.Api10 is not null) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 02b5f22a5..c7456db2d 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -17,6 +17,7 @@ using Dalamud.Interface.Colors; using Dalamud.Interface.Components; using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.ImGuiNotification.Internal; +using Dalamud.Interface.Textures.Internal; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs b/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs index 7e9b209cf..eee8c6e52 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs @@ -7,6 +7,7 @@ using Dalamud.Interface.Textures.TextureWraps.Internal; using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; using Dalamud.Utility; +using Dalamud.Utility.TerraFxCom; using TerraFX.Interop.DirectX; using TerraFX.Interop.Windows; @@ -62,18 +63,11 @@ internal sealed partial class TextureManager unsafe { - var srvDesc = new D3D11_SHADER_RESOURCE_VIEW_DESC( + using var srv = this.device.CreateShaderResourceView( tex, - D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE2D); - using var srv = default(ComPtr); - this.device.Get()->CreateShaderResourceView( - (ID3D11Resource*)tex.Get(), - &srvDesc, - srv.GetAddressOf()) - .ThrowOnError(); + new(tex.Get(), D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE2D)); - var desc = default(D3D11_TEXTURE2D_DESC); - tex.Get()->GetDesc(&desc); + var desc = tex.GetDesc(); var outWrap = new UnknownTextureWrap( (IUnknown*)srv.Get(), @@ -126,15 +120,10 @@ internal sealed partial class TextureManager TextureModificationArgs args = default, CancellationToken cancellationToken = default) { - using var tex2D = wrapAux.NewTexRef(); - if (!args.IsCompleteSourceCopy(wrapAux.Desc)) - { - using var tmp = await this.NoThrottleCreateFromExistingTextureAsync(wrapAux, args); - unsafe - { - tex2D.Swap(&tmp); - } - } + using var tex2D = + args.IsCompleteSourceCopy(wrapAux.Desc) + ? wrapAux.NewTexRef() + : await this.NoThrottleCreateFromExistingTextureAsync(wrapAux, args); cancellationToken.ThrowIfCancellationRequested(); @@ -147,32 +136,29 @@ internal sealed partial class TextureManager { cancellationToken.ThrowIfCancellationRequested(); - D3D11_TEXTURE2D_DESC desc; - tex2D.Get()->GetDesc(&desc); + var desc = tex2D.GetDesc(); using var device = default(ComPtr); tex2D.Get()->GetDevice(device.GetAddressOf()); using var context = default(ComPtr); device.Get()->GetImmediateContext(context.GetAddressOf()); - using var tmpTex = default(ComPtr); - if ((desc.CPUAccessFlags & (uint)D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_READ) == 0) - { - var tmpTexDesc = desc with - { - MipLevels = 1, - ArraySize = 1, - SampleDesc = new(1, 0), - Usage = D3D11_USAGE.D3D11_USAGE_STAGING, - BindFlags = 0u, - CPUAccessFlags = (uint)D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_READ, - MiscFlags = 0u, - }; - device.Get()->CreateTexture2D(&tmpTexDesc, null, tmpTex.GetAddressOf()).ThrowOnError(); - context.Get()->CopyResource((ID3D11Resource*)tmpTex.Get(), (ID3D11Resource*)tex2D.Get()); - - cancellationToken.ThrowIfCancellationRequested(); - } + using var tmpTex = + (desc.CPUAccessFlags & (uint)D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_READ) == 0 + ? device.CreateTexture2D( + desc with + { + MipLevels = 1, + ArraySize = 1, + SampleDesc = new(1, 0), + Usage = D3D11_USAGE.D3D11_USAGE_STAGING, + BindFlags = 0u, + CPUAccessFlags = (uint)D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_READ, + MiscFlags = 0u, + }, + tex2D) + : default; + cancellationToken.ThrowIfCancellationRequested(); var mapWhat = (ID3D11Resource*)(tmpTex.IsEmpty() ? tex2D.Get() : tmpTex.Get()); @@ -205,25 +191,22 @@ internal sealed partial class TextureManager if (args.NewHeight == 0) args = args with { NewHeight = (int)MathF.Round((args.Uv1Effective.Y - args.Uv0.Y) * wrapAux.Desc.Height) }; - using var tex2DCopyTemp = default(ComPtr); - unsafe - { - var tex2DCopyTempDesc = new D3D11_TEXTURE2D_DESC - { - Width = (uint)args.NewWidth, - Height = (uint)args.NewHeight, - MipLevels = 1, - ArraySize = 1, - Format = args.Format, - SampleDesc = new(1, 0), - Usage = D3D11_USAGE.D3D11_USAGE_DEFAULT, - BindFlags = (uint)(D3D11_BIND_FLAG.D3D11_BIND_SHADER_RESOURCE | - D3D11_BIND_FLAG.D3D11_BIND_RENDER_TARGET), - CPUAccessFlags = 0u, - MiscFlags = 0u, - }; - this.device.Get()->CreateTexture2D(&tex2DCopyTempDesc, null, tex2DCopyTemp.GetAddressOf()).ThrowOnError(); - } + using var tex2DCopyTemp = + this.device.CreateTexture2D( + new() + { + Width = (uint)args.NewWidth, + Height = (uint)args.NewHeight, + MipLevels = 1, + ArraySize = 1, + Format = args.Format, + SampleDesc = new(1, 0), + Usage = D3D11_USAGE.D3D11_USAGE_DEFAULT, + BindFlags = (uint)(D3D11_BIND_FLAG.D3D11_BIND_SHADER_RESOURCE | + D3D11_BIND_FLAG.D3D11_BIND_RENDER_TARGET), + CPUAccessFlags = 0u, + MiscFlags = 0u, + }); await this.RunDuringPresent(() => DrawSourceTextureToTarget(wrapAux, args, this.SimpleDrawer, tex2DCopyTemp)); diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.cs b/Dalamud/Interface/Textures/Internal/TextureManager.cs index 7510ec7dc..9dffc6853 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.cs @@ -12,6 +12,7 @@ using Dalamud.Interface.Textures.TextureWraps.Internal; using Dalamud.Logging.Internal; using Dalamud.Plugin.Services; using Dalamud.Utility; +using Dalamud.Utility.TerraFxCom; using Lumina.Data; using Lumina.Data.Files; @@ -24,8 +25,7 @@ namespace Dalamud.Interface.Textures.Internal; /// Service responsible for loading and disposing ImGui texture wraps. [ServiceManager.EarlyLoadedService] internal sealed partial class TextureManager - : IServiceType, - IDisposable, + : IInternalDisposableService, ITextureProvider, ITextureSubstitutionProvider, ITextureReadbackProvider @@ -101,7 +101,7 @@ internal sealed partial class TextureManager } /// - public void Dispose() + void IInternalDisposableService.DisposeService() { if (this.disposing) return; @@ -221,6 +221,53 @@ internal sealed partial class TextureManager static T? ForceNullable(T s) => s; } + /// + public unsafe IDalamudTextureWrap CreateEmpty( + RawImageSpecification specs, + bool cpuRead, + bool cpuWrite, + string? debugName = null) + { + if (cpuRead && cpuWrite) + throw new ArgumentException("cpuRead and cpuWrite cannot be set at the same time."); + + var cpuaf = default(D3D11_CPU_ACCESS_FLAG); + if (cpuRead) + cpuaf |= D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_READ; + if (cpuWrite) + cpuaf |= D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_WRITE; + + D3D11_USAGE usage; + if (cpuRead) + usage = D3D11_USAGE.D3D11_USAGE_STAGING; + else if (cpuWrite) + usage = D3D11_USAGE.D3D11_USAGE_DYNAMIC; + else + usage = D3D11_USAGE.D3D11_USAGE_DEFAULT; + + using var texture = this.device.CreateTexture2D( + new() + { + Width = (uint)specs.Width, + Height = (uint)specs.Height, + MipLevels = 1, + ArraySize = 1, + Format = specs.Format, + SampleDesc = new(1, 0), + Usage = usage, + BindFlags = (uint)D3D11_BIND_FLAG.D3D11_BIND_SHADER_RESOURCE, + CPUAccessFlags = (uint)cpuaf, + MiscFlags = 0, + }); + using var view = this.device.CreateShaderResourceView( + texture, + new(texture, D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE2D)); + + var wrap = new UnknownTextureWrap((IUnknown*)view.Get(), specs.Width, specs.Height, true); + this.BlameSetName(wrap, debugName ?? $"{nameof(this.CreateEmpty)}({specs})"); + return wrap; + } + /// bool ITextureProvider.IsDxgiFormatSupported(int dxgiFormat) => this.IsDxgiFormatSupported((DXGI_FORMAT)dxgiFormat); diff --git a/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs b/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs index afdca0f31..9e7544fa2 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs @@ -28,9 +28,8 @@ namespace Dalamud.Interface.Textures.Internal; [ResolveVia] [ResolveVia] #pragma warning restore SA1015 -internal sealed partial class TextureManagerPluginScoped - : IServiceType, - IDisposable, +internal sealed class TextureManagerPluginScoped + : IInternalDisposableService, ITextureProvider, ITextureSubstitutionProvider, ITextureReadbackProvider @@ -114,7 +113,7 @@ internal sealed partial class TextureManagerPluginScoped } /// - public void Dispose() + void IInternalDisposableService.DisposeService() { if (Interlocked.Exchange(ref this.managerTaskNullable, null) is not { } task) return; @@ -134,6 +133,19 @@ internal sealed partial class TextureManagerPluginScoped : $"{nameof(TextureManagerPluginScoped)}({this.plugin.Name})"; } + /// + public IDalamudTextureWrap CreateEmpty( + RawImageSpecification specs, + bool cpuRead, + bool cpuWrite, + string? debugName = null) + { + var manager = this.ManagerOrThrow; + var textureWrap = manager.CreateEmpty(specs, cpuRead, cpuWrite, debugName); + manager.Blame(textureWrap, this.plugin); + return textureWrap; + } + /// public async Task CreateFromExistingTextureAsync( IDalamudTextureWrap wrap, diff --git a/Dalamud/Interface/Textures/RawImageSpecification.cs b/Dalamud/Interface/Textures/RawImageSpecification.cs index e2bad7e5f..6f31cbbf7 100644 --- a/Dalamud/Interface/Textures/RawImageSpecification.cs +++ b/Dalamud/Interface/Textures/RawImageSpecification.cs @@ -31,6 +31,13 @@ public record struct RawImageSpecification this.DxgiFormat = dxgiFormat; } + /// Initializes a new instance of the class. + /// The source texture description. + internal RawImageSpecification(in D3D11_TEXTURE2D_DESC desc) + : this((int)desc.Width, (int)desc.Height, (int)desc.Format) + { + } + /// Initializes a new instance of the class. /// The source texture description. /// The pitch of the raw image in bytes. diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 58f4f3cb7..7311b0b91 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -12,16 +12,11 @@ using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.ManagedFontAtlas.Internals; -using Dalamud.Interface.Textures; -using Dalamud.Interface.Textures.Internal; using Dalamud.Plugin.Internal.Types; -using Dalamud.Plugin.Services; using Dalamud.Utility; using ImGuiNET; -using ImGuiScene; - using Serilog; using SharpDX.Direct3D11; @@ -46,9 +41,6 @@ public sealed class UiBuilder : IDisposable private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new(); - [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] - private readonly TextureManagerPluginScoped scopedTextureProvider; - private bool hasErrorWindow = false; private bool lastFrameUiHideState = false; @@ -78,8 +70,6 @@ public sealed class UiBuilder : IDisposable this.interfaceManager.ResizeBuffers += this.OnResizeBuffers; this.scopedFinalizer.Add(() => this.interfaceManager.ResizeBuffers -= this.OnResizeBuffers); - this.scopedFinalizer.Add(this.scopedTextureProvider = new(plugin)); - this.FontAtlas = this.scopedFinalizer .Add( @@ -350,43 +340,6 @@ public sealed class UiBuilder : IDisposable private Task InterfaceManagerWithSceneAsync => Service.GetAsync().ContinueWith(task => task.Result.Manager); - /// - /// Loads an image from the specified file. - /// - /// The full filepath to the image. - /// A object wrapping the created image. Use inside ImGui.Image(). - [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] - [Obsolete($"Use {nameof(ITextureProvider.GetFromFile)}.")] - public IDalamudTextureWrap LoadImage(string filePath) => - this.scopedTextureProvider.GetFromFile(filePath).RentAsync().Result; - - /// - /// Loads an image from a byte stream, such as a png downloaded into memory. - /// - /// 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.CreateFromImageAsync)}.")] - public IDalamudTextureWrap LoadImage(byte[] imageData) => - this.scopedTextureProvider.CreateFromImageAsync(imageData).Result; - - /// - /// Loads an image from raw unformatted pixel data, with no type or header information. To load formatted data, use . - /// - /// A byte array containing the raw pixel data. - /// The width of the image contained in . - /// 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.CreateFromRaw)} or {nameof(ITextureProvider.CreateFromRawAsync)}.")] - public IDalamudTextureWrap LoadImageRaw(byte[] imageData, int width, int height, int numChannels) => - numChannels switch - { - 4 => this.scopedTextureProvider.CreateFromRaw(RawImageSpecification.Rgba32(width, height), imageData), - _ => throw new NotSupportedException(), - }; - /// /// Loads an ULD file that can load textures containing multiple icons in a single texture. /// @@ -395,43 +348,6 @@ public sealed class UiBuilder : IDisposable public UldWrapper LoadUld(string uldPath) => new(this, uldPath); - /// - /// Asynchronously loads an image from the specified file, when it's possible to do so. - /// - /// The full filepath to the image. - /// A object wrapping the created image. Use inside ImGui.Image(). - [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] - [Obsolete($"Use {nameof(ITextureProvider.GetFromFile)}.")] - public Task LoadImageAsync(string filePath) => - this.scopedTextureProvider.GetFromFile(filePath).RentAsync(); - - /// - /// Asynchronously loads an image from a byte stream, such as a png downloaded into memory, when it's possible to do so. - /// - /// 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.CreateFromImageAsync)}.")] - public Task LoadImageAsync(byte[] imageData) => - this.scopedTextureProvider.CreateFromImageAsync(imageData); - - /// - /// Asynchronously loads an image from raw unformatted pixel data, with no type or header information, when it's possible to do so. To load formatted data, use . - /// - /// A byte array containing the raw pixel data. - /// The width of the image contained in . - /// 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.CreateFromRawAsync)}.")] - public Task LoadImageRawAsync(byte[] imageData, int width, int height, int numChannels) => - numChannels switch - { - 4 => this.scopedTextureProvider.CreateFromRawAsync(RawImageSpecification.Rgba32(width, height), imageData), - _ => Task.FromException(new NotSupportedException()), - }; - /// /// Waits for UI to become available for use. /// diff --git a/Dalamud/Plugin/Services/ITextureProvider.cs b/Dalamud/Plugin/Services/ITextureProvider.cs index 61f400a0e..a4fa0d382 100644 --- a/Dalamud/Plugin/Services/ITextureProvider.cs +++ b/Dalamud/Plugin/Services/ITextureProvider.cs @@ -5,7 +5,6 @@ using System.Reflection; using System.Threading; using System.Threading.Tasks; -using Dalamud.Interface; using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.Windows.Data.Widgets; using Dalamud.Interface.Textures; @@ -32,8 +31,20 @@ namespace Dalamud.Plugin.Services; /// . /// /// -public partial interface ITextureProvider +public interface ITextureProvider { + /// Creates an empty texture. + /// Texture specifications. + /// Whether to support reading from CPU, while disabling reading from GPU. + /// Whether to support writing from CPU, while disabling writing from GPU. + /// Name for debug display purposes. + /// A new empty texture. + IDalamudTextureWrap CreateEmpty( + RawImageSpecification specs, + bool cpuRead, + bool cpuWrite, + string? debugName = null); + /// Creates a texture from the given existing texture, cropping and converting pixel format as needed. /// /// The source texture wrap. The passed value may be disposed once this function returns, diff --git a/Dalamud/Utility/TerraFxCom/TerraFxD3D11Extensions.cs b/Dalamud/Utility/TerraFxCom/TerraFxD3D11Extensions.cs new file mode 100644 index 000000000..967c1eb1b --- /dev/null +++ b/Dalamud/Utility/TerraFxCom/TerraFxD3D11Extensions.cs @@ -0,0 +1,65 @@ +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +namespace Dalamud.Utility.TerraFxCom; + +/// Extension methods for D3D11 TerraFX objects. +internal static class TerraFxD3D11Extensions +{ + /// Creates a 2D texture with the given descriptor. + /// Device to copy from and to. + /// Resource descriptor. + /// Optional initial data for the texture. + /// New copied texture. + public static unsafe ComPtr CreateTexture2D( + this ComPtr device, + D3D11_TEXTURE2D_DESC desc, + ComPtr copyFrom = default) + { + using var tmpTex = default(ComPtr); + device.Get()->CreateTexture2D(&desc, null, tmpTex.GetAddressOf()).ThrowOnError(); + + if (!copyFrom.IsEmpty()) + { + using var context = default(ComPtr); + device.Get()->GetImmediateContext(context.GetAddressOf()); + context.Get()->CopyResource((ID3D11Resource*)tmpTex.Get(), (ID3D11Resource*)copyFrom.Get()); + } + + return new(tmpTex); + } + + /// Creates a shader resource view for a resource. + /// Device to create the resource view into. + /// Resource to create a view on. + /// Resource view descriptor. + /// Type of the resource. + /// New shader resource view. + public static unsafe ComPtr CreateShaderResourceView( + this ComPtr device, + ComPtr resource, + in D3D11_SHADER_RESOURCE_VIEW_DESC desc) + where T : unmanaged, ID3D11Resource.Interface + { + fixed (D3D11_SHADER_RESOURCE_VIEW_DESC* pDesc = &desc) + { + var srv = default(ComPtr); + device.Get()->CreateShaderResourceView( + (ID3D11Resource*)resource.Get(), + pDesc, + srv.GetAddressOf()) + .ThrowOnError(); + return srv; + } + } + + /// Gets the descriptor for a . + /// Texture. + /// Texture descriptor. + public static unsafe D3D11_TEXTURE2D_DESC GetDesc(this ComPtr texture) + { + var desc = default(D3D11_TEXTURE2D_DESC); + texture.Get()->GetDesc(&desc); + return desc; + } +} From 6d8102dc79c703e151cb8f63dcf5b6e92d32f704 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 5 Jun 2024 00:08:05 +0900 Subject: [PATCH 53/57] Move ShowTextureSaveMenuAsync to its own class --- .../Interface/Internal/DalamudInterface.cs | 129 -------------- .../Windows/Data/Widgets/IconBrowserWidget.cs | 3 +- .../Windows/Data/Widgets/TexWidget.cs | 7 +- .../Utility/Internal/DevTextureSaveMenu.cs | 164 ++++++++++++++++++ 4 files changed, 170 insertions(+), 133 deletions(-) create mode 100644 Dalamud/Interface/Utility/Internal/DevTextureSaveMenu.cs diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 5f5a0b71b..06795aa53 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -1,12 +1,9 @@ -using System.Collections.Generic; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Numerics; using System.Reflection; using System.Runtime.InteropServices; -using System.Threading.Tasks; using CheapLoc; using Dalamud.Configuration.Internal; @@ -18,9 +15,6 @@ using Dalamud.Game.Internal; using Dalamud.Hooking; using Dalamud.Interface.Animation.EasingFunctions; using Dalamud.Interface.Colors; -using Dalamud.Interface.ImGuiFileDialog; -using Dalamud.Interface.ImGuiNotification; -using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.Internal.Windows; using Dalamud.Interface.Internal.Windows.Data; @@ -30,7 +24,6 @@ using Dalamud.Interface.Internal.Windows.Settings; using Dalamud.Interface.Internal.Windows.StyleEditor; using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Style; -using Dalamud.Interface.Textures.Internal; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; @@ -47,10 +40,6 @@ using ImPlotNET; using PInvoke; using Serilog.Events; -using TerraFX.Interop.Windows; - -using Task = System.Threading.Tasks.Task; - namespace Dalamud.Interface.Internal; /// @@ -67,8 +56,6 @@ internal class DalamudInterface : IInternalDisposableService private readonly DalamudConfiguration configuration; private readonly InterfaceManager interfaceManager; - private readonly FileDialogManager fileDialogManager; - private readonly ChangelogWindow changelogWindow; private readonly ColorDemoWindow colorDemoWindow; private readonly ComponentDemoWindow componentDemoWindow; @@ -121,7 +108,6 @@ internal class DalamudInterface : IInternalDisposableService this.interfaceManager = interfaceManager; this.WindowSystem = new WindowSystem("DalamudCore"); - this.fileDialogManager = new(); this.colorDemoWindow = new ColorDemoWindow() { IsOpen = false }; this.componentDemoWindow = new ComponentDemoWindow() { IsOpen = false }; @@ -501,119 +487,6 @@ internal class DalamudInterface : IInternalDisposableService this.creditsDarkeningAnimation.Restart(); } - /// Shows a context menu confirming texture save. - /// Name of the initiator. - /// Suggested name of the file being saved. - /// A task returning the texture to save. - /// A representing the asynchronous operation. - public async Task ShowTextureSaveMenuAsync( - string initiatorName, - string name, - Task texture) - { - try - { - var initiatorScreenOffset = ImGui.GetMousePos(); - using var textureWrap = await texture; - var textureManager = await Service.GetAsync(); - var popupName = $"{nameof(this.ShowTextureSaveMenuAsync)}_{textureWrap.ImGuiHandle:X}"; - - BitmapCodecInfo encoder; - { - var first = true; - var encoders = textureManager.Wic.GetSupportedEncoderInfos().ToList(); - var tcs = new TaskCompletionSource(); - Service.Get().Draw += DrawChoices; - - encoder = await tcs.Task; - - [SuppressMessage("ReSharper", "AccessToDisposedClosure", Justification = "This shall not escape")] - void DrawChoices() - { - if (first) - { - ImGui.OpenPopup(popupName); - first = false; - } - - ImGui.SetNextWindowPos(initiatorScreenOffset, ImGuiCond.Appearing); - if (!ImGui.BeginPopup( - popupName, - ImGuiWindowFlags.AlwaysAutoResize | - ImGuiWindowFlags.NoTitleBar | - ImGuiWindowFlags.NoSavedSettings)) - { - Service.Get().Draw -= DrawChoices; - tcs.TrySetCanceled(); - return; - } - - foreach (var encoder2 in encoders) - { - if (ImGui.Selectable(encoder2.Name)) - tcs.TrySetResult(encoder2); - } - - const float previewImageWidth = 320; - var size = textureWrap.Size; - if (size.X > previewImageWidth) - size *= previewImageWidth / size.X; - if (size.Y > previewImageWidth) - size *= previewImageWidth / size.Y; - ImGui.Image(textureWrap.ImGuiHandle, size); - - if (tcs.Task.IsCompleted) - ImGui.CloseCurrentPopup(); - - ImGui.EndPopup(); - } - } - - string path; - { - var tcs = new TaskCompletionSource(); - this.fileDialogManager.SaveFileDialog( - "Save texture...", - $"{encoder.Name.Replace(',', '.')}{{{string.Join(',', encoder.Extensions)}}}", - name + encoder.Extensions.First(), - encoder.Extensions.First(), - (ok, path2) => - { - if (!ok) - tcs.SetCanceled(); - else - tcs.SetResult(path2); - }); - path = await tcs.Task.ConfigureAwait(false); - } - - var props = new Dictionary(); - if (encoder.ContainerGuid == GUID.GUID_ContainerFormatTiff) - props["CompressionQuality"] = 1.0f; - else if (encoder.ContainerGuid == GUID.GUID_ContainerFormatJpeg || - encoder.ContainerGuid == GUID.GUID_ContainerFormatHeif || - encoder.ContainerGuid == GUID.GUID_ContainerFormatWmp) - props["ImageQuality"] = 1.0f; - await textureManager.SaveToFileAsync(textureWrap, encoder.ContainerGuid, path, props: props); - - Service.Get().AddNotification( - $"File saved to: {path}", - initiatorName, - NotificationType.Success); - } - catch (Exception e) - { - if (e is OperationCanceledException) - return; - - Log.Error(e, $"{nameof(DalamudInterface)}.{nameof(this.ShowTextureSaveMenuAsync)}({initiatorName}, {name})"); - Service.Get().AddNotification( - $"Failed to save file: {e}", - initiatorName, - NotificationType.Error); - } - } - private void OnDraw() { this.FrameCount++; @@ -665,8 +538,6 @@ internal class DalamudInterface : IInternalDisposableService { ImGui.SetWindowFocus(null); } - - this.fileDialogManager.Draw(); } catch (Exception ex) { diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs index e10fca0e7..20e549f27 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using Dalamud.Interface.Colors; using Dalamud.Interface.Textures.Internal; using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Internal; using ImGuiNET; @@ -170,7 +171,7 @@ public class IconBrowserWidget : IDataWindowWidget if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) { - _ = Service.Get().ShowTextureSaveMenuAsync( + _ = Service.Get().ShowTextureSaveMenuAsync( this.DisplayName, iconId.ToString(), Task.FromResult(texture.CreateWrapSharingLowLevelResource())); diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs index 85b0dc00d..f579b4d59 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -13,6 +13,7 @@ using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Textures; using Dalamud.Interface.Textures.Internal.SharedImmediateTextures; using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Internal; using Dalamud.Plugin.Services; using Dalamud.Storage.Assets; using Dalamud.Utility; @@ -249,7 +250,7 @@ internal class TexWidget : IDataWindowWidget ImGui.SameLine(); if (ImGui.Button("Save")) { - _ = Service.Get().ShowTextureSaveMenuAsync( + _ = Service.Get().ShowTextureSaveMenuAsync( this.DisplayName, $"Texture {t.Id}", t.CreateNewTextureWrapReference(this.textureManager)); @@ -458,7 +459,7 @@ internal class TexWidget : IDataWindowWidget ImGui.TableNextColumn(); if (ImGuiComponents.IconButton(FontAwesomeIcon.Save)) { - _ = Service.Get().ShowTextureSaveMenuAsync( + _ = Service.Get().ShowTextureSaveMenuAsync( this.DisplayName, $"{wrap.ImGuiHandle:X16}", Task.FromResult(wrap.CreateWrapSharingLowLevelResource())); @@ -585,7 +586,7 @@ internal class TexWidget : IDataWindowWidget if (ImGuiComponents.IconButton(FontAwesomeIcon.Save)) { var name = Path.ChangeExtension(Path.GetFileName(texture.SourcePathForDebug), null); - _ = Service.Get().ShowTextureSaveMenuAsync( + _ = Service.Get().ShowTextureSaveMenuAsync( this.DisplayName, name, texture.RentAsync()); diff --git a/Dalamud/Interface/Utility/Internal/DevTextureSaveMenu.cs b/Dalamud/Interface/Utility/Internal/DevTextureSaveMenu.cs new file mode 100644 index 000000000..c9e20ff1c --- /dev/null +++ b/Dalamud/Interface/Utility/Internal/DevTextureSaveMenu.cs @@ -0,0 +1,164 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading.Tasks; + +using Dalamud.Interface.ImGuiFileDialog; +using Dalamud.Interface.ImGuiNotification; +using Dalamud.Interface.ImGuiNotification.Internal; +using Dalamud.Interface.Internal; +using Dalamud.Interface.Textures.Internal; + +using ImGuiNET; + +using Serilog; + +using TerraFX.Interop.Windows; + +namespace Dalamud.Interface.Utility.Internal; + +/// Utility function for saving textures. +[ServiceManager.EarlyLoadedService] +internal sealed class DevTextureSaveMenu : IInternalDisposableService +{ + [ServiceManager.ServiceDependency] + private readonly InterfaceManager interfaceManager = Service.Get(); + + private readonly FileDialogManager fileDialogManager; + + [ServiceManager.ServiceConstructor] + private DevTextureSaveMenu() + { + this.fileDialogManager = new(); + this.interfaceManager.Draw += this.InterfaceManagerOnDraw; + } + + /// + void IInternalDisposableService.DisposeService() => this.interfaceManager.Draw -= this.InterfaceManagerOnDraw; + + /// Shows a context menu confirming texture save. + /// Name of the initiator. + /// Suggested name of the file being saved. + /// A task returning the texture to save. + /// A representing the asynchronous operation. + public async Task ShowTextureSaveMenuAsync( + string initiatorName, + string name, + Task texture) + { + try + { + var initiatorScreenOffset = ImGui.GetMousePos(); + using var textureWrap = await texture; + var textureManager = await Service.GetAsync(); + var popupName = $"{nameof(this.ShowTextureSaveMenuAsync)}_{textureWrap.ImGuiHandle:X}"; + + BitmapCodecInfo encoder; + { + var first = true; + var encoders = textureManager.Wic.GetSupportedEncoderInfos().ToList(); + var tcs = new TaskCompletionSource(); + Service.Get().Draw += DrawChoices; + + encoder = await tcs.Task; + + [SuppressMessage("ReSharper", "AccessToDisposedClosure", Justification = "This shall not escape")] + void DrawChoices() + { + if (first) + { + ImGui.OpenPopup(popupName); + first = false; + } + + ImGui.SetNextWindowPos(initiatorScreenOffset, ImGuiCond.Appearing); + if (!ImGui.BeginPopup( + popupName, + ImGuiWindowFlags.AlwaysAutoResize | + ImGuiWindowFlags.NoTitleBar | + ImGuiWindowFlags.NoSavedSettings)) + { + Service.Get().Draw -= DrawChoices; + tcs.TrySetCanceled(); + return; + } + + foreach (var encoder2 in encoders) + { + if (ImGui.Selectable(encoder2.Name)) + tcs.TrySetResult(encoder2); + } + + const float previewImageWidth = 320; + var size = textureWrap.Size; + if (size.X > previewImageWidth) + size *= previewImageWidth / size.X; + if (size.Y > previewImageWidth) + size *= previewImageWidth / size.Y; + ImGui.Image(textureWrap.ImGuiHandle, size); + + if (tcs.Task.IsCompleted) + ImGui.CloseCurrentPopup(); + + ImGui.EndPopup(); + } + } + + string path; + { + var tcs = new TaskCompletionSource(); + this.fileDialogManager.SaveFileDialog( + "Save texture...", + $"{encoder.Name.Replace(',', '.')}{{{string.Join(',', encoder.Extensions)}}}", + name + encoder.Extensions.First(), + encoder.Extensions.First(), + (ok, path2) => + { + if (!ok) + tcs.SetCanceled(); + else + tcs.SetResult(path2); + }); + path = await tcs.Task.ConfigureAwait(false); + } + + var props = new Dictionary(); + if (encoder.ContainerGuid == GUID.GUID_ContainerFormatTiff) + props["CompressionQuality"] = 1.0f; + else if (encoder.ContainerGuid == GUID.GUID_ContainerFormatJpeg || + encoder.ContainerGuid == GUID.GUID_ContainerFormatHeif || + encoder.ContainerGuid == GUID.GUID_ContainerFormatWmp) + props["ImageQuality"] = 1.0f; + await textureManager.SaveToFileAsync(textureWrap, encoder.ContainerGuid, path, props: props); + + var notif = Service.Get().AddNotification( + new() + { + Content = $"File saved to: {path}", + Title = initiatorName, + Type = NotificationType.Success, + }); + notif.Click += n => + { + Process.Start(new ProcessStartInfo(path) { UseShellExecute = true }); + n.Notification.DismissNow(); + }; + } + catch (Exception e) + { + if (e is OperationCanceledException) + return; + + Log.Error( + e, + $"{nameof(DalamudInterface)}.{nameof(this.ShowTextureSaveMenuAsync)}({initiatorName}, {name})"); + Service.Get().AddNotification( + $"Failed to save file: {e}", + initiatorName, + NotificationType.Error); + } + } + + private void InterfaceManagerOnDraw() => this.fileDialogManager.Draw(); +} From 3f138c2600a219cd4892449be6e9c187319ee88a Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 5 Jun 2024 00:11:33 +0900 Subject: [PATCH 54/57] Make IsMainThreadInPresent and CumulativePresentCalls clearer --- Dalamud/Interface/Internal/InterfaceManager.cs | 16 +++++++++------- .../Textures/Internal/TextureManager.cs | 4 ++-- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index d62d122e2..26b5c8ce2 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -217,7 +217,7 @@ internal class InterfaceManager : IInternalDisposableService /// Gets a value indicating whether the main thread is executing . /// This still will be true even when queried off the main thread. - public bool IsInPresent { get; private set; } + public bool IsMainThreadInPresent { get; private set; } /// /// Gets a value indicating the native handle of the game main window. @@ -249,9 +249,11 @@ internal class InterfaceManager : IInternalDisposableService /// public Task FontBuildTask => WhenFontsReady().dalamudAtlas!.BuildTask; - /// - /// Gets the number of calls to so far. - /// + /// Gets the number of calls to so far. + /// + /// The value increases even when Dalamud is hidden via "/xlui hide". + /// does not. + /// public long CumulativePresentCalls { get; private set; } /// @@ -657,7 +659,7 @@ internal class InterfaceManager : IInternalDisposableService } this.CumulativePresentCalls++; - this.IsInPresent = true; + this.IsMainThreadInPresent = true; while (this.runBeforeImGuiRender.TryDequeue(out var action)) action.InvokeSafely(); @@ -668,14 +670,14 @@ internal class InterfaceManager : IInternalDisposableService RenderImGui(this.scene!); this.PostImGuiRender(); - this.IsInPresent = false; + this.IsMainThreadInPresent = false; return pRes; } RenderImGui(this.scene!); this.PostImGuiRender(); - this.IsInPresent = false; + this.IsMainThreadInPresent = false; return this.presentHook!.Original(swapChain, syncInterval, presentFlags); } diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.cs b/Dalamud/Interface/Textures/Internal/TextureManager.cs index 9dffc6853..3266190df 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.cs @@ -380,7 +380,7 @@ internal sealed partial class TextureManager // Not sure why this and the below can't be unconditional RunOnFrameworkThread private async Task RunDuringPresent(Action action) { - if (this.interfaceManager.IsInPresent && ThreadSafety.IsMainThread) + if (this.interfaceManager.IsMainThreadInPresent && ThreadSafety.IsMainThread) action(); else await this.interfaceManager.RunBeforeImGuiRender(action); @@ -392,7 +392,7 @@ internal sealed partial class TextureManager /// The return value from the function. private async Task RunDuringPresent(Func func) { - if (this.interfaceManager.IsInPresent && ThreadSafety.IsMainThread) + if (this.interfaceManager.IsMainThreadInPresent && ThreadSafety.IsMainThread) return func(); return await this.interfaceManager.RunBeforeImGuiRender(func); } From e2ea30c2fb7c84d6d722ed67f3dffc60e85df26f Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 5 Jun 2024 00:13:16 +0900 Subject: [PATCH 55/57] Add TODO for PluginStats --- Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs index f579b4d59..9dcb9b84a 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -31,6 +31,8 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// internal class TexWidget : IDataWindowWidget { + // TODO: move tracking implementation to PluginStats where applicable, + // and show stats over there instead of TexWidget. private static readonly Dictionary< DrawBlameTableColumnUserId, Func> DrawBlameTableColumnColumnComparers = new() From e144956a483e8b0d2dee16a2d58ccd6b1b2c3bfe Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 5 Jun 2024 00:14:36 +0900 Subject: [PATCH 56/57] Add notes on CreateTextureAsync --- .../SharedImmediateTextures/SharedImmediateTexture.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/SharedImmediateTexture.cs b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/SharedImmediateTexture.cs index a20736e0a..1c218f6af 100644 --- a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/SharedImmediateTexture.cs +++ b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/SharedImmediateTexture.cs @@ -344,9 +344,12 @@ internal abstract class SharedImmediateTexture default(CancellationToken)); } - /// Creates the texture. + /// Creates the texture immediately. /// The cancellation token. /// The task resulting in a loaded texture. + /// This function is intended to be called from texture load scheduler. + /// See and note that this function is being used as the callback from + /// . protected abstract Task CreateTextureAsync(CancellationToken cancellationToken); private IRefCountable.RefCountResult TryAddRef(out int newRefCount) From 729795ff9e0f79fe582ae2a40c682c771f4d2b7e Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 5 Jun 2024 00:29:15 +0900 Subject: [PATCH 57/57] Move temp file gen for File.Replace to separate file --- .../Textures/Internal/TextureManager.Wic.cs | 2 +- Dalamud/Utility/Util.cs | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs b/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs index 40b9d3ee0..3c93ba875 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs @@ -90,7 +90,7 @@ internal sealed partial class TextureManager throw new NullReferenceException($"{nameof(path)} cannot be null."); using var wrapAux = new WrapAux(wrap, true); - var pathTemp = $"{path}.{GetCurrentThreadId():X08}{Environment.TickCount64:X16}.tmp"; + var pathTemp = Util.GetTempFileNameForFileReplacement(path); var trashfire = new List(); try { diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index 2f6b4fb4a..112427cf0 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -640,6 +640,30 @@ public static class Util throw new Win32Exception(); } + /// Gets a temporary file name, for use as the sourceFileName in + /// . + /// The target file. + /// A temporary file name that should be usable with . + /// + /// No write operation is done on the filesystem. + public static string GetTempFileNameForFileReplacement(string targetFile) + { + Span buf = stackalloc byte[9]; + Random.Shared.NextBytes(buf); + for (var i = 0; ; i++) + { + var tempName = + Path.GetFileName(targetFile) + + Convert.ToBase64String(buf) + .TrimEnd('=') + .Replace('+', '-') + .Replace('/', '_'); + var tempPath = Path.Join(Path.GetDirectoryName(targetFile), tempName); + if (i >= 64 || !Path.Exists(tempPath)) + return tempPath; + } + } + /// /// Gets a random, inoffensive, human-friendly string. ///