diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 67c220800..619d233e1 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -456,6 +456,9 @@ internal sealed class DalamudConfiguration : IInternalDisposableService /// 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/Dalamud.csproj b/Dalamud/Dalamud.csproj index e8ec785b8..1286f089e 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -64,6 +64,7 @@ + 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/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/DalamudTextureWrap.cs b/Dalamud/Interface/Internal/DalamudTextureWrap.cs deleted file mode 100644 index b49c6f07b..000000000 --- a/Dalamud/Interface/Internal/DalamudTextureWrap.cs +++ /dev/null @@ -1,71 +0,0 @@ -using Dalamud.Utility; - -using ImGuiScene; - -namespace Dalamud.Interface.Internal; - -/// -/// Safety harness for ImGuiScene textures that will defer destruction until -/// the end of the frame. -/// -public class DalamudTextureWrap : IDalamudTextureWrap, IDeferredDisposable -{ - private readonly TextureWrap wrappedWrap; - - /// - /// Initializes a new instance of the class. - /// - /// The texture wrap to wrap. - internal DalamudTextureWrap(TextureWrap wrappingWrap) - { - this.wrappedWrap = wrappingWrap; - } - - /// - /// 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. - /// - public void Dispose() - { - this.Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// 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/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 65d250cfd..26b5c8ce2 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -33,8 +33,6 @@ using ImGuiScene; using PInvoke; using SharpDX; -using SharpDX.Direct3D; -using SharpDX.Direct3D11; using SharpDX.DXGI; // general dev notes, here because it's easiest @@ -70,7 +68,7 @@ internal class InterfaceManager : IInternalDisposableService private static readonly ModuleLog Log = new("INTERFACE"); private readonly ConcurrentBag deferredDisposeTextures = new(); - private readonly ConcurrentBag deferredDisposeImFontLockeds = new(); + private readonly ConcurrentBag deferredDisposeDisposables = new(); [ServiceManager.ServiceDependency] private readonly WndProcHookManager wndProcHookManager = Service.Get(); @@ -78,6 +76,9 @@ internal class InterfaceManager : IInternalDisposableService [ServiceManager.ServiceDependency] private readonly Framework framework = Service.Get(); + private readonly ConcurrentQueue runBeforeImGuiRender = new(); + private readonly ConcurrentQueue runAfterImGuiRender = new(); + private readonly SwapChainVtableResolver address = new(); private RawDX11Scene? scene; @@ -214,6 +215,10 @@ internal class InterfaceManager : IInternalDisposableService /// 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 IsMainThreadInPresent { get; private set; } + /// /// Gets a value indicating the native handle of the game main window. /// @@ -244,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; } /// @@ -294,138 +301,6 @@ internal class InterfaceManager : IInternalDisposableService } } -#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(Span 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 - /// /// Sets up a deferred invocation of font rebuilding, before the next render frame. /// @@ -448,9 +323,97 @@ internal class InterfaceManager : IInternalDisposableService /// 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); + } + + /// Queues an action to be run before call. + /// The action. + /// A that resolves once is run. + public Task RunBeforeImGuiRender(Action action) + { + var tcs = new TaskCompletionSource(); + this.runBeforeImGuiRender.Enqueue( + () => + { + try + { + action(); + tcs.SetResult(); + } + catch (Exception e) + { + tcs.SetException(e); + } + }); + return tcs.Task; + } + + /// Queues a function to be run before call. + /// The type of the return value. + /// The function. + /// A that resolves once is run. + public Task RunBeforeImGuiRender(Func func) + { + var tcs = new TaskCompletionSource(); + 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 + { + tcs.SetResult(func()); + } + catch (Exception e) + { + tcs.SetException(e); + } + }); + return tcs.Task; } /// @@ -672,8 +635,6 @@ internal class InterfaceManager : IInternalDisposableService */ 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"); @@ -697,24 +658,35 @@ internal class InterfaceManager : IInternalDisposableService return this.presentHook!.Original(swapChain, syncInterval, presentFlags); } + this.CumulativePresentCalls++; + this.IsMainThreadInPresent = true; + + while (this.runBeforeImGuiRender.TryDequeue(out var action)) + action.InvokeSafely(); + if (this.address.IsReshade) { var pRes = this.presentHook!.Original(swapChain, syncInterval, presentFlags); RenderImGui(this.scene!); - this.CleanupPostImGuiRender(); + this.PostImGuiRender(); + this.IsMainThreadInPresent = false; return pRes; } RenderImGui(this.scene!); - this.CleanupPostImGuiRender(); + this.PostImGuiRender(); + this.IsMainThreadInPresent = false; 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; @@ -727,12 +699,12 @@ internal class InterfaceManager : IInternalDisposableService 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/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs deleted file mode 100644 index 2444c2c85..000000000 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ /dev/null @@ -1,515 +0,0 @@ -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Numerics; - -using Dalamud.Data; -using Dalamud.Game; -using Dalamud.IoC; -using Dalamud.IoC.Internal; -using Dalamud.Logging.Internal; -using Dalamud.Plugin.Services; - -using Lumina.Data.Files; -using Lumina.Data.Parsing.Tex.Buffers; -using SharpDX.DXGI; - -namespace Dalamud.Interface.Internal; - -// TODO API10: Remove keepAlive from public APIs - -/// -/// 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 class TextureManager : IInternalDisposableService, ITextureProvider, ITextureSubstitutionProvider -{ - 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; - - private readonly ClientLanguage language; - - private readonly Dictionary activeTextures = new(); - - private IDalamudTextureWrap? fallbackTextureWrap; - - /// - /// 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) - { - 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. - /// - /// 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) - { - var hiRes = flags.HasFlag(ITextureProvider.IconFlags.HiRes); - - // 1. Item - var path = FormatIconPath( - iconId, - flags.HasFlag(ITextureProvider.IconFlags.ItemHighQuality) ? "hq/" : string.Empty, - hiRes); - if (this.dataManager.FileExists(path)) - return path; - - language ??= this.language; - var languageFolder = language switch - { - ClientLanguage.Japanese => "ja/", - ClientLanguage.English => "en/", - ClientLanguage.German => "de/", - ClientLanguage.French => "fr/", - _ => throw new ArgumentOutOfRangeException(nameof(language), $"Unknown Language: {language}"), - }; - - // 2. Regular icon, with language, hi-res - path = FormatIconPath( - iconId, - languageFolder, - hiRes); - if (this.dataManager.FileExists(path)) - return path; - - if (hiRes) - { - // 3. Regular icon, with language, no hi-res - path = FormatIconPath( - iconId, - languageFolder, - false); - if (this.dataManager.FileExists(path)) - return path; - } - - // 4. Regular icon, without language, hi-res - path = FormatIconPath( - iconId, - null, - hiRes); - if (this.dataManager.FileExists(path)) - return path; - - // 4. Regular icon, without language, no hi-res - if (hiRes) - { - path = FormatIconPath( - iconId, - null, - false); - if (this.dataManager.FileExists(path)) - return path; - } - - return 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. - 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 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) - { - lock (this.activeTextures) - { - foreach (var path in paths) - { - if (!this.activeTextures.TryGetValue(path, out var info) || info == null) - continue; - - info.Wrap?.Dispose(); - info.Wrap = null; - } - } - } - - /// - void IInternalDisposableService.DisposeService() - { - 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(); - } - - /// - /// 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 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) - { - lock (this.activeTextures) - { - // 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); - } - } - - 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. - } -} diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs index 4ac37b21f..20e549f27 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs @@ -1,11 +1,12 @@ using System.Collections.Generic; -using System.Linq; using System.Numerics; +using System.Threading.Tasks; -using Dalamud.Data; using Dalamud.Interface.Colors; +using Dalamud.Interface.Textures.Internal; using Dalamud.Interface.Utility; -using Dalamud.Utility; +using Dalamud.Interface.Utility.Internal; + using ImGuiNET; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; @@ -15,23 +16,20 @@ 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 int lastNullValueCount; + + private List? valueRange; + private Task>? iconIdsTask; + 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" }; @@ -45,31 +43,58 @@ public class IconBrowserWidget : IDataWindowWidget public void Load() { } - + /// 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(); } } - + // Limit the popup image to half our screen size. private static float GetImageScaleFactor(IDalamudTextureWrap texture) { @@ -83,83 +108,120 @@ public class IconBrowserWidget : IDataWindowWidget scale = MathF.Min(widthRatio, heightRatio); } - + return scale; } - + private void DrawOptions() { 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); - + ImGui.NextColumn(); ImGui.InputFloat2("Icon Size", ref this.editIconSize); if (ImGui.IsItemDeactivatedAfterEdit()) { this.iconSize = this.editIconSize; } - + ImGui.Columns(1); } - + private void DrawIcon(int iconId) { - try + var texm = Service.Get(); + var cursor = ImGui.GetCursorScreenPos(); + + if (texm.Shared.GetFromGameIcon(iconId).TryGetWrap(out var texture, out var exc)) { - var cursor = ImGui.GetCursorScreenPos(); - - if (!this.IsIconValid(iconId)) + 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) { - this.nullValues.Add(iconId); - return; - } - - if (Service.Get().GetIcon((uint)iconId) is { } texture) - { - 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.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()); - } - } - else - { - // This texture was null, draw nothing, and prevent from trying to show it again. - this.nullValues.Add(iconId); + 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.Text(iconId.ToString()); + + ImGui.Image(texture.ImGuiHandle, texture.Size * scale); + ImGui.EndTooltip(); } - ImGui.GetWindowDrawList().AddRect(cursor, cursor + this.iconSize, ImGui.GetColorU32(ImGuiColors.DalamudWhite)); + // else, just draw the iconId. + else if (ImGui.IsItemHovered()) + { + 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, + 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); } } @@ -181,7 +243,7 @@ public class IconBrowserWidget : IDataWindowWidget this.dragStarted = false; } } - + if (ImGui.IsMouseDragging(ImGuiMouseButton.Left) && this.dragStarted) { var delta = this.mouseDragStart - ImGui.GetMousePos(); @@ -193,24 +255,17 @@ 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() { - 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/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 36651d43e..9dcb9b84a 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -1,13 +1,28 @@ 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.Configuration.Internal; +using Dalamud.Interface.Components; +using Dalamud.Interface.ImGuiNotification; +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; using ImGuiNET; -using Serilog; +using TerraFX.Interop.DirectX; + +using TextureManager = Dalamud.Interface.Textures.Internal.TextureManager; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; @@ -16,23 +31,65 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// internal class TexWidget : IDataWindowWidget { - private readonly List addedTextures = new(); - + // TODO: move tracking implementation to PluginStats where applicable, + // and show stats over there instead of TexWidget. + 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; - private bool keepAlive = 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; private Vector2 inputTexScale = Vector2.Zero; + private TextureManager textureManager = null!; + private TextureModificationArgs textureModificationArgs; + + private ImGuiViewportTextureArgs viewportTextureArgs; + private int viewportIndexInt; + private string[]? supportedRenderTargetFormatNames; + 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" }; - + /// - public string DisplayName { get; init; } = "Tex"; + public string DisplayName { get; init; } = "Tex"; /// public bool Ready { get; set; } @@ -40,97 +97,911 @@ 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"; + 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.supportedRenderTargetFormats = null; + this.supportedRenderTargetFormatNames = null; + this.renderTargetChoiceInt = 0; + this.textureModificationArgs = new() + { + Uv0 = new(0.25f), + Uv1 = new(0.75f), + NewWidth = 320, + NewHeight = 240, + }; + this.viewportTextureArgs = default; + this.viewportIndexInt = 0; this.Ready = true; } /// public void Draw() { - var texManager = Service.Get(); + this.textureManager = Service.Get(); + var conf = Service.Get(); + if (ImGui.Button("GC")) + GC.Collect(); + + 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:n0}###header")) + this.DrawLoadedTextures(this.textureManager.Shared.ForDebugGamePathTextures); + ImGui.PopID(); + + ImGui.PushID("loadedFileTextures"); + if (ImGui.CollapsingHeader( + $"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:n0}###header")) + this.DrawLoadedTextures(this.textureManager.Shared.ForDebugManifestResourceTextures); + ImGui.PopID(); + + lock (this.textureManager.Shared.ForDebugInvalidatedTextures) + { + ImGui.PushID("invalidatedTextures"); + if (ImGui.CollapsingHeader( + $"Invalidated: {this.textureManager.Shared.ForDebugInvalidatedTextures.Count:n0}###header")) + { + this.DrawLoadedTextures(this.textureManager.Shared.ForDebugInvalidatedTextures); + } + + ImGui.PopID(); + } + + ImGui.Dummy(new(ImGui.GetTextLineHeightWithSpacing())); + + if (ImGui.CollapsingHeader(nameof(ITextureProvider.GetFromGameIcon))) + { + ImGui.PushID(nameof(this.DrawGetFromGameIcon)); + this.DrawGetFromGameIcon(); + ImGui.PopID(); + } + + if (ImGui.CollapsingHeader(nameof(ITextureProvider.GetFromGame))) + { + ImGui.PushID(nameof(this.DrawGetFromGame)); + this.DrawGetFromGame(); + ImGui.PopID(); + } + + if (ImGui.CollapsingHeader(nameof(ITextureProvider.GetFromFile))) + { + ImGui.PushID(nameof(this.DrawGetFromFile)); + this.DrawGetFromFile(); + ImGui.PopID(); + } + + 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)); + this.DrawUvInput(); + ImGui.PopID(); + } + + if (ImGui.CollapsingHeader($"CropCopy##{this.DrawExistingTextureModificationArgs}")) + { + ImGui.PushID(nameof(this.DrawExistingTextureModificationArgs)); + this.DrawExistingTextureModificationArgs(); + ImGui.PopID(); + } + + ImGui.Dummy(new(ImGui.GetTextLineHeightWithSpacing())); + + 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")) + { + runLater = () => + { + t.Dispose(); + this.addedTextures.Remove(t); + }; + } + + ImGui.SameLine(); + if (ImGui.Button("Save")) + { + _ = Service.Get().ShowTextureSaveMenuAsync( + this.DisplayName, + $"Texture {t.Id}", + t.CreateNewTextureWrapReference(this.textureManager)); + } + + ImGui.SameLine(); + 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.CreateWrapSharingLowLevelResource(), + this.textureModificationArgs with + { + Format = 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})"); + ImGui.TextUnformatted(source.ToString()); + } + else + { + ImGui.TextUnformatted("RC: -"); + ImGui.TextUnformatted(" "); + } + } + + 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() ?? "Loading"); + } + } + catch (Exception e) + { + ImGui.TextUnformatted(e.ToString()); + } + } + + ImGui.PopID(); + } + + runLater?.Invoke(); + } + + private unsafe void DrawBlame(List allBlames) + { + var im = Service.Get(); + + 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; + float iconWidths; + using (im.IconFontHandle?.Push()) + iconWidths = ImGui.CalcTextSize(FontAwesomeIcon.Save.ToIconString()).X; + + ImGui.TableSetupScrollFreeze(0, 1); + ImGui.TableSetupColumn( + "Address", + ImGuiTableColumnFlags.WidthFixed, + 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, + (uint)DrawBlameTableColumnUserId.Format); + ImGui.TableSetupColumn( + "Size", + ImGuiTableColumnFlags.WidthFixed, + ImGui.CalcTextSize("123.45 MB").X, + (uint)DrawBlameTableColumnUserId.Size); + ImGui.TableSetupColumn( + "Plugins", + ImGuiTableColumnFlags.WidthFixed, + 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); + + while (clipper.Step()) + { + for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) + { + var wrap = allBlames[i]; + ImGui.TableNextRow(); + ImGui.PushID(i); + + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + this.TextCopiable($"0x{wrap.ResourceAddress:X}", true, true); + + ImGui.TableNextColumn(); + if (ImGuiComponents.IconButton(FontAwesomeIcon.Save)) + { + _ = Service.Get().ShowTextureSaveMenuAsync( + this.DisplayName, + $"{wrap.ImGuiHandle:X16}", + Task.FromResult(wrap.CreateWrapSharingLowLevelResource())); + } + + if (ImGui.IsItemHovered()) + { + ImGui.BeginTooltip(); + ImGui.Image(wrap.ImGuiHandle, wrap.Size); + 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) + this.TextCopiable(string.Join(", ", wrap.OwnerPlugins.Select(static x => x.Name)), false, true); + + ImGui.PopID(); + } + } + + clipper.Destroy(); + ImGui.EndTable(); + + ImGuiHelpers.ScaledDummy(10); + } + + private unsafe void DrawLoadedTextures(ICollection textures) + { + var im = Service.Get(); + if (!ImGui.BeginTable("##table", 6)) + return; + + const int numIcons = 4; + float iconWidths; + using (im.IconFontHandle?.Push()) + { + iconWidths = ImGui.CalcTextSize(FontAwesomeIcon.Save.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("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()); + 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; + + ImGui.TableNextRow(); + + if (enu.Current is not { } texture) + { + // Should not happen + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("?"); + continue; + } + + var remain = texture.SelfReferenceExpiresInForDebug; + ImGui.PushID(row); + + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + this.TextCopiable($"{texture.InstanceIdForDebug:n0}", true, true); + + ImGui.TableNextColumn(); + this.TextCopiable(texture.SourcePathForDebug, false, true); + + ImGui.TableNextColumn(); + this.TextCopiable($"{texture.RefCountForDebug:n0}", true, true); + + ImGui.TableNextColumn(); + this.TextCopiable(remain <= 0 ? "-" : $"{remain:00.000}", true, true); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted(texture.HasRevivalPossibility ? "Yes" : "No"); + + ImGui.TableNextColumn(); + if (ImGuiComponents.IconButton(FontAwesomeIcon.Save)) + { + var name = Path.ChangeExtension(Path.GetFileName(texture.SourcePathForDebug), null); + _ = Service.Get().ShowTextureSaveMenuAsync( + this.DisplayName, + name, + texture.RentAsync()); + } + + if (ImGui.IsItemHovered() && texture.GetWrapOrDefault(null) is { } immediate) + { + ImGui.BeginTooltip(); + ImGui.Image(immediate.ImGuiHandle, immediate.Size); + ImGui.EndTooltip(); + } + + ImGui.SameLine(); + if (ImGuiComponents.IconButton(FontAwesomeIcon.Sync)) + this.textureManager.InvalidatePaths(new[] { texture.SourcePathForDebug }); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip($"Call {nameof(ITextureSubstitutionProvider.InvalidatePaths)}."); + + 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(); + + ImGui.PopID(); + } + + if (!valid) + break; + } + } + + clipper.Destroy(); + ImGui.EndTable(); + + ImGuiHelpers.ScaledDummy(10); + } + + private void DrawGetFromGameIcon() + { 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")) - { - try - { - 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.SameLine(); + if (ImGui.Button("Load Icon (Async)")) + { + this.addedTextures.Add( + new( + Api10: this.textureManager + .Shared + .GetFromGameIcon(new(uint.Parse(this.iconId), this.hq, this.hiRes)) + .RentAsync())); } - - ImGui.Separator(); + + 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 DrawGetFromGame() + { ImGui.InputText("Tex Path", ref this.inputTexPath, 255); - if (ImGui.Button("Load Tex")) + + ImGui.SameLine(); + if (ImGui.Button("Load Tex (Async)")) + this.addedTextures.Add(new(Api10: this.textureManager.Shared.GetFromGame(this.inputTexPath).RentAsync())); + + ImGui.SameLine(); + if (ImGui.Button("Load Tex (Immediate)")) + this.addedTextures.Add(new(Api10ImmGamePath: this.inputTexPath)); + + ImGuiHelpers.ScaledDummy(10); + } + + private void DrawGetFromFile() + { + ImGui.InputText("File Path", ref this.inputFilePath, 255); + + ImGui.SameLine(); + if (ImGui.Button("Load File (Async)")) + this.addedTextures.Add(new(Api10: this.textureManager.Shared.GetFromFile(this.inputFilePath).RentAsync())); + + ImGui.SameLine(); + if (ImGui.Button("Load File (Immediate)")) + this.addedTextures.Add(new(Api10ImmFile: this.inputFilePath)); + + ImGuiHelpers.ScaledDummy(10); + } + + private void DrawGetFromManifestResource() + { + if (this.inputManifestResourceAssemblyCandidateNames is null || + this.inputManifestResourceAssemblyCandidates is null) { - try - { - this.addedTextures.Add(texManager.GetTextureFromGame(this.inputTexPath, this.keepAlive)); - } - catch (Exception ex) - { - Log.Error(ex, "Could not load tex"); - } + 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.Button("Load File")) + + if (ImGui.Combo( + "Assembly", + ref this.inputManifestResourceAssemblyIndex, + this.inputManifestResourceAssemblyCandidateNames, + this.inputManifestResourceAssemblyCandidateNames.Length)) { - try - { - this.addedTextures.Add(texManager.GetTextureFromFile(new FileInfo(this.inputTexPath), this.keepAlive)); - } - catch (Exception ex) - { - Log.Error(ex, "Could not load tex"); - } + this.inputManifestResourceNameIndex = 0; + this.inputManifestResourceNameCandidates = null; } - - ImGui.Separator(); + + 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.Shared.GetFromManifestResource(assembly, name).RentAsync())); + } + + ImGui.SameLine(); + if (ImGui.Button("Load File (Immediate)")) + this.addedTextures.Add(new(Api10ImmManifestResource: (assembly, name))); + } + + 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 }, + null), + }); + } + } + + 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); + } - IDalamudTextureWrap? toRemove = null; - for (var i = 0; i < this.addedTextures.Count; i++) + private void DrawExistingTextureModificationArgs() + { + var b = this.textureModificationArgs.MakeOpaque; + if (ImGui.Checkbox(nameof(this.textureModificationArgs.MakeOpaque), ref b)) + this.textureModificationArgs.MakeOpaque = b; + + if (this.supportedRenderTargetFormats is null) { - if (ImGui.CollapsingHeader($"Tex #{i}")) + 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( + nameof(this.textureModificationArgs.DxgiFormat), + ref this.renderTargetChoiceInt, + this.supportedRenderTargetFormatNames, + this.supportedRenderTargetFormatNames.Length); + + Span wh = stackalloc int[2]; + wh[0] = this.textureModificationArgs.NewWidth; + wh[1] = this.textureModificationArgs.NewHeight; + if (ImGui.InputInt2( + $"{nameof(this.textureModificationArgs.NewWidth)}/{nameof(this.textureModificationArgs.NewHeight)}", + ref wh[0])) + { + this.textureModificationArgs.NewWidth = wh[0]; + this.textureModificationArgs.NewHeight = wh[1]; + } + + var vec2 = this.textureModificationArgs.Uv0; + if (ImGui.InputFloat2(nameof(this.textureModificationArgs.Uv0), ref vec2)) + this.textureModificationArgs.Uv0 = vec2; + + vec2 = this.textureModificationArgs.Uv1; + if (ImGui.InputFloat2(nameof(this.textureModificationArgs.Uv1), ref vec2)) + this.textureModificationArgs.Uv1 = vec2; + + ImGuiHelpers.ScaledDummy(10); + } + + 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(); + 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); + 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, + Task? Api10 = null, + GameIconLookup? Api10ImmGameIcon = null, + string? Api10ImmGamePath = null, + string? Api10ImmFile = null, + (Assembly Assembly, string Name)? Api10ImmManifestResource = null) : IDisposable + { + private static int idCounter; + + public int Id { get; } = idCounter++; + + public void Dispose() + { + this.SharedResource?.Dispose(); + _ = this.Api10?.ToContentDisposedTask(); + } + + public string? DescribeError() + { + if (this.SharedResource is not null) + return "Unknown error"; + if (this.Api10 is not null) { - var tex = this.addedTextures[i]; + return !this.Api10.IsCompleted + ? null + : this.Api10.Exception?.ToString() ?? (this.Api10.IsCanceled ? "Canceled" : "Unknown error"); + } - 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 (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"; + if (this.Api10ImmManifestResource is not null) + return "Must not happen"; + return "Not implemented"; + } - if (ImGui.Button($"X##{i}")) - toRemove = tex; + public IDalamudTextureWrap? GetTexture(ITextureProvider tp) + { + if (this.SharedResource is not null) + return this.SharedResource; + 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).GetWrapOrEmpty(); + if (this.Api10ImmGamePath is not null) + return tp.GetFromGame(this.Api10ImmGamePath).GetWrapOrEmpty(); + if (this.Api10ImmFile is not null) + return tp.GetFromFile(this.Api10ImmFile).GetWrapOrEmpty(); + if (this.Api10ImmManifestResource is not null) + { + return tp.GetFromManifestResource( + this.Api10ImmManifestResource.Value.Assembly, + this.Api10ImmManifestResource.Value.Name).GetWrapOrEmpty(); + } - ImGui.SameLine(); - if (ImGui.Button($"Clone##{i}")) - this.addedTextures.Add(tex.CreateWrapSharingLowLevelResource()); + 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); } } - if (toRemove != null) + public TextureEntry CreateFromSharedLowLevelResource(ITextureProvider tp) => + new() { SharedResource = this.GetTexture(tp)?.CreateWrapSharingLowLevelResource() }; + + public override string ToString() { - toRemove.Dispose(); - this.addedTextures.Remove(toRemove); + if (this.SharedResource is not null) + return $"{nameof(this.SharedResource)}: {this.SharedResource}"; + 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}"; + if (this.Api10ImmManifestResource is not null) + return $"{nameof(this.Api10ImmManifestResource)}: {this.Api10ImmManifestResource}"; + return "Not implemented"; } } } diff --git a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs index 634999143..c1e467330 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; @@ -278,33 +279,19 @@ internal class PluginImageCache : IInternalDisposableService 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.CreateFromImageAsync( + bytes, + $"{nameof(PluginImageCache)}({name} for {manifest.InternalName} at {loc})"); } 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 e0579270c..e404f805c 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -18,6 +18,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; @@ -72,8 +73,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; @@ -1514,10 +1515,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); @@ -1611,10 +1612,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); @@ -1670,14 +1685,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); @@ -1699,7 +1737,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 @@ -1712,23 +1750,18 @@ internal class PluginInstallerWindow : Window, IDisposable if (!this.testerIconPath.IsNullOrEmpty()) { - this.testerIcon = im.LoadImage(this.testerIconPath); + this.testerIcon = tm.Shared.GetFromFile(this.testerIconPath).RentAsync(); } - 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.Shared.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 fcb0c560d..2cd6950f8 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs @@ -9,6 +9,7 @@ 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.Storage.Assets; using Dalamud.Utility; @@ -688,24 +689,27 @@ internal sealed partial class FontAtlasFactory var buf = Array.Empty(); try { - var use4 = this.factory.InterfaceManager.SupportsDxgiFormat(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; - 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 = + $"{nameof(FontAtlasBuiltData)}[{this.data.Owner?.Name ?? "-"}][0x{(long)this.data.Atlas.NativePtr:X}][{i}]"; if (texture.TexID != 0) { // Nothing to do } else if (texture.TexPixelsRGBA32 is not null) { - var wrap = this.factory.InterfaceManager.LoadImageFromDxgiFormat( + var wrap = this.factory.TextureManager.CreateFromRaw( + RawImageSpecification.Rgba32(width, height), new(texture.TexPixelsRGBA32, width * height * 4), - width * 4, - width, - height, - use4 ? Format.B4G4R4A4_UNorm : Format.R8G8B8A8_UNorm); + name); + this.factory.TextureManager.Blame(wrap, this.data.Owner?.OwnerPlugin); this.data.AddExistingTexture(wrap); texture.TexID = wrap.ImGuiHandle; } @@ -743,12 +747,15 @@ internal sealed partial class FontAtlasFactory } } - var wrap = this.factory.InterfaceManager.LoadImageFromDxgiFormat( + var wrap = this.factory.TextureManager.CreateFromRaw( + new( + width, + height, + (int)(use4 ? Format.B4G4R4A4_UNorm : Format.B8G8R8A8_UNorm), + width * bpp), buf, - width * bpp, - width, - height, - use4 ? Format.B4G4R4A4_UNorm : Format.B8G8R8A8_UNorm); + 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 83ebac89e..a80248470 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; @@ -273,12 +274,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 { @@ -372,6 +376,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 2e39dcc5e..0c39513b0 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs @@ -11,6 +11,8 @@ using Dalamud.Game; 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; @@ -20,9 +22,7 @@ using ImGuiScene; using Lumina.Data.Files; -using SharpDX; -using SharpDX.Direct3D11; -using SharpDX.DXGI; +using TerraFX.Interop.DirectX; namespace Dalamud.Interface.ManagedFontAtlas.Internals; @@ -144,6 +144,11 @@ internal sealed partial class FontAtlasFactory /// public InterfaceManager InterfaceManager { get; } + /// + /// Gets the service instance of . + /// + public TextureManager TextureManager => Service.Get(); + /// /// Gets the async task for inside . /// @@ -174,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. @@ -239,31 +246,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, @@ -346,7 +334,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.IsDxgiFormatSupported(DXGI_FORMAT.DXGI_FORMAT_B4G4R4A4_UNORM); var bpp = targetIsB4G4R4A4 ? 2 : 4; var buffer = ArrayPool.Shared.Rent(numPixels * bpp); try @@ -369,12 +357,16 @@ internal sealed partial class FontAtlasFactory } return this.scopedFinalizer.Add( - this.InterfaceManager.LoadImageFromDxgiFormat( + this.TextureManager.CreateFromRaw( + new( + texFile.Header.Width, + texFile.Header.Height, + (int)(targetIsB4G4R4A4 + ? DXGI_FORMAT.DXGI_FORMAT_B4G4R4A4_UNORM + : DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM), + texFile.Header.Width * bpp), buffer, - texFile.Header.Width * bpp, - texFile.Header.Width, - texFile.Header.Height, - targetIsB4G4R4A4 ? Format.B4G4R4A4_UNorm : Format.B8G8R8A8_UNorm)); + $"{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/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/Textures/ISharedImmediateTexture.cs b/Dalamud/Interface/Textures/ISharedImmediateTexture.cs new file mode 100644 index 000000000..f9683e6c5 --- /dev/null +++ b/Dalamud/Interface/Textures/ISharedImmediateTexture.cs @@ -0,0 +1,70 @@ +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Interface.Internal; +using Dalamud.Utility; + +namespace Dalamud.Interface.Textures; + +/// 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, or an empty texture if unavailable. + /// 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 GetWrapOrEmpty(); + + /// 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 + /// 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? 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). + /// 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/Textures/ImGuiViewportTextureArgs.cs b/Dalamud/Interface/Textures/ImGuiViewportTextureArgs.cs new file mode 100644 index 000000000..1159f5dbf --- /dev/null +++ b/Dalamud/Interface/Textures/ImGuiViewportTextureArgs.cs @@ -0,0 +1,95 @@ +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. +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; + + /// + 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() + { + 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/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/Textures/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs new file mode 100644 index 000000000..69aca5c69 --- /dev/null +++ b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs @@ -0,0 +1,36 @@ +using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Interface.Internal; + +namespace Dalamud.Interface.Textures.Internal.SharedImmediateTextures; + +/// 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. + /// The path. + private FileSystemSharedImmediateTexture(string path) + : base(path) => this.path = path; + + /// 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); + + /// + public override string ToString() => + $"{nameof(FileSystemSharedImmediateTexture)}#{this.InstanceIdForDebug}({this.path})"; + + /// + protected override async Task CreateTextureAsync(CancellationToken cancellationToken) + { + var tm = await Service.GetAsync(); + 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 new file mode 100644 index 000000000..8a1caacd6 --- /dev/null +++ b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs @@ -0,0 +1,45 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Data; +using Dalamud.Interface.Internal; + +using Lumina.Data.Files; + +namespace Dalamud.Interface.Textures.Internal.SharedImmediateTextures; + +/// 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. + /// The path. + private GamePathSharedImmediateTexture(string path) + : base(path) => this.path = path; + + /// 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); + + /// + public override string ToString() => + $"{nameof(GamePathSharedImmediateTexture)}#{this.InstanceIdForDebug}({this.path})"; + + /// + protected override async Task CreateTextureAsync(CancellationToken cancellationToken) + { + var dm = await Service.GetAsync(); + var tm = await Service.GetAsync(); + var substPath = tm.GetSubstitutedPath(this.path); + if (dm.GetFile(substPath) is not { } file) + throw new FileNotFoundException(); + cancellationToken.ThrowIfCancellationRequested(); + 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 new file mode 100644 index 000000000..34ffbaf0e --- /dev/null +++ b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs @@ -0,0 +1,49 @@ +using System.IO; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Interface.Internal; + +namespace Dalamud.Interface.Textures.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) + : base($"{assembly.GetName().FullName}:{name}") + { + this.assembly = assembly; + this.name = name; + } + + /// 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); + + /// + 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 ? checked((int)stream.Length) : 0); + await stream.CopyToAsync(ms, 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/SharedImmediateTextures/SharedImmediateTexture.cs b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/SharedImmediateTexture.cs new file mode 100644 index 000000000..1c218f6af --- /dev/null +++ b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/SharedImmediateTexture.cs @@ -0,0 +1,617 @@ +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.Interface.Textures.TextureWraps; +using Dalamud.Interface.Textures.TextureWraps.Internal; +using Dalamud.Plugin.Internal.Types; +using Dalamud.Storage.Assets; +using Dalamud.Utility; + +namespace Dalamud.Interface.Textures.Internal.SharedImmediateTextures; + +/// Represents a texture that may have multiple reference holders (owners). +internal abstract class SharedImmediateTexture + : ISharedImmediateTexture, IRefCountable, DynamicPriorityQueueLoader.IThrottleBasisProvider +{ + private const int SelfReferenceDurationTicks = 2000; + private const long SelfReferenceExpiryExpired = long.MaxValue; + + private static long instanceCounter; + + private readonly object reviveLock = new(); + private readonly List ownerPlugins = new(); + + private bool resourceReleased; + private int refCount; + private long selfReferenceExpiry; + private IDalamudTextureWrap? availableOnAccessWrapForApi9; + private CancellationTokenSource? cancellationTokenSource; + private NotOwnedTextureWrap? nonOwningWrap; + + /// Initializes a new instance of the class. + /// Name of the underlying resource. + /// The new instance is a placeholder instance. + protected SharedImmediateTexture(string sourcePathForDebug) + { + this.SourcePathForDebug = sourcePathForDebug; + this.InstanceIdForDebug = Interlocked.Increment(ref instanceCounter); + this.refCount = 0; + this.selfReferenceExpiry = SelfReferenceExpiryExpired; + this.ContentQueried = false; + this.IsOpportunistic = true; + this.resourceReleased = true; + this.FirstRequestedTick = this.LatestRequestedTick = Environment.TickCount64; + this.PublicUseInstance = new(this); + } + + /// 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; } + + /// 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 the source path. Debug use only. + public 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. + public Task? UnderlyingWrap { get; set; } + + /// + public bool IsOpportunistic { get; private set; } + + /// + public long FirstRequestedTick { get; private set; } + + /// + 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 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. + /// + /// 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 + { + IRefCountable.RefCountResult.StillAlive => newRefCount, + IRefCountable.RefCountResult.AlreadyDisposed => throw new ObjectDisposedException( + nameof(SharedImmediateTexture)), + 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 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.cancellationTokenSource?.Cancel(); + this.cancellationTokenSource = null; + this.nonOwningWrap = null; + this.ClearUnderlyingWrap(); + this.resourceReleased = true; + + return newRefCount; + } + } + + case IRefCountable.RefCountResult.AlreadyDisposed: + throw new ObjectDisposedException(nameof(SharedImmediateTexture)); + + default: + throw new InvalidOperationException(); + } + } + + /// 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) + { + while (true) + { + var exp = this.selfReferenceExpiry; + 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; + + this.availableOnAccessWrapForApi9 = null; + return this.Release(); + } + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public IDalamudTextureWrap GetWrapOrEmpty() => this.GetWrapOrDefault(Service.Get().Empty4X4); + + /// + [return: NotNullIfNotNull(nameof(defaultWrap))] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public IDalamudTextureWrap? GetWrapOrDefault(IDalamudTextureWrap? defaultWrap) + { + if (!this.TryGetWrap(out var texture, out _)) + texture = null; + return texture ?? defaultWrap; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + 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(); + + try + { + this.AddRef(); + } + finally + { + this.ContentQueried = true; + } + + if (this.UnderlyingWrap is null) + throw new InvalidOperationException("AddRef returned but UnderlyingWrap is null?"); + + this.IsOpportunistic = false; + this.LatestRequestedTick = Environment.TickCount64; + var uw = this.UnderlyingWrap; + if (cancellationToken != default) + { + while (!uw.IsCompleted) + { + if (cancellationToken.IsCancellationRequested) + { + this.Release(); + throw new OperationCanceledException(cancellationToken); + } + + await Task.WhenAny(uw, Task.Delay(1000000, cancellationToken)); + } + } + + IDalamudTextureWrap dtw; + try + { + dtw = await uw; + } + catch + { + this.Release(); + throw; + } + + return new RefCountableWrappingTextureWrap(dtw, 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; + + var newRefTask = this.RentAsync(this.LoadCancellationToken); + newRefTask.Wait(this.LoadCancellationToken); + if (!newRefTask.IsCompletedSuccessfully) + return null; + newRefTask.Result.Dispose(); + + this.availableOnAccessWrapForApi9 = new AvailableOnAccessTextureWrap(this); + this.RevivalPossibility = new(this.availableOnAccessWrapForApi9); + } + + 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})"; + + /// Cleans up this instance of . + protected void ClearUnderlyingWrap() + { + _ = this.UnderlyingWrap?.ToContentDisposedTask(true); + this.UnderlyingWrap = null; + } + + /// Attempts to restore the reference to this texture. + 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; + lock (this.ownerPlugins) + { + foreach (var op in this.ownerPlugins.Take(addLen)) + Service.Get().Blame(r.Result, op); + } + }, + default(CancellationToken)); + } + + /// 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) + { + var alterResult = IRefCountable.AlterRefCount(1, ref this.refCount, out newRefCount); + if (alterResult != IRefCountable.RefCountResult.AlreadyDisposed) + return alterResult; + + while (true) + { + lock (this.reviveLock) + { + if (!this.resourceReleased) + { + Thread.Yield(); + continue; + } + + alterResult = IRefCountable.AlterRefCount(1, ref this.refCount, out newRefCount); + if (alterResult != IRefCountable.RefCountResult.AlreadyDisposed) + return alterResult; + + this.cancellationTokenSource = new(); + try + { + this.LoadUnderlyingWrap(); + } + catch + { + this.cancellationTokenSource = null; + throw; + } + + if (this.RevivalPossibility?.TryGetTarget(out var target) is true) + this.availableOnAccessWrapForApi9 = target; + + Interlocked.Increment(ref this.refCount); + this.resourceReleased = false; + return IRefCountable.RefCountResult.StillAlive; + } + } + } + + /// , 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; + } + } + + /// 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 + { + 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.owner = owner; + } + + /// + public override IDalamudTextureWrap CreateWrapSharingLowLevelResource() + { + var wrap = this.GetWrap(); + this.owner.AddRef(); + return new RefCountableWrappingTextureWrap(wrap, this.owner); + } + + /// + public override string ToString() => $"{nameof(NotOwnedTextureWrap)}({this.owner})"; + } + + /// Reference counting texture wrap, to be used with . + private sealed class RefCountableWrappingTextureWrap : ForwardingTextureWrap + { + 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; + } + + /// Finalizes an instance of the class. + ~RefCountableWrappingTextureWrap() => this.Dispose(false); + + /// + public override 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 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) + { + if (this.owner is not { } ownerCopy) + 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(); + } + } + } + + /// A texture wrap that revives and waits for the underlying texture as needed on every access. + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + 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 override 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 override string ToString() => $"{nameof(AvailableOnAccessTextureWrap)}({this.inner})"; + + /// + protected override bool TryGetWrap(out IDalamudTextureWrap? wrap) + { + if (this.inner.TryGetWrapCore(out var t, out _)) + wrap = t; + + this.inner.UnderlyingWrap?.Wait(); + wrap = this.inner.nonOwningWrap ?? Service.Get().Empty4X4; + return true; + } + } +} diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.BlameTracker.cs b/Dalamud/Interface/Textures/Internal/TextureManager.BlameTracker.cs new file mode 100644 index 000000000..a306b7c64 --- /dev/null +++ b/Dalamud/Interface/Textures/Internal/TextureManager.BlameTracker.cs @@ -0,0 +1,430 @@ +using System.Collections.Generic; +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 +{ + /// 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; } + + /// Gets the format of the texture. + public DXGI_FORMAT Format { get; } + + /// 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 the list containing all the loaded textures from plugins. + /// Returned value must be used inside a lock. + 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. + /// The plugin. + /// Same . + public unsafe IDalamudTextureWrap Blame(IDalamudTextureWrap textureWrap, LocalPlugin? ownerPlugin) + { + 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.GetOrCreate(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; + + try + { + if (textureWrap.ImGuiHandle == nint.Zero) + return textureWrap; + } + catch (ObjectDisposedException) + { + return textureWrap; + } + + using var wrapAux = new WrapAux(textureWrap, true); + 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); + } + + 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 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 + { + 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 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) + 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(); + return existingTagInstance; + } + } + } + + return null; + } + + /// + 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.Drawer.cs b/Dalamud/Interface/Textures/Internal/TextureManager.Drawer.cs new file mode 100644 index 000000000..7fb79311a --- /dev/null +++ b/Dalamud/Interface/Textures/Internal/TextureManager.Drawer.cs @@ -0,0 +1,398 @@ +using System.Buffers; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; + +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; + + /// 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 new file mode 100644 index 000000000..eee8c6e52 --- /dev/null +++ b/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs @@ -0,0 +1,340 @@ +using System.Runtime.CompilerServices; +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; +using Dalamud.Utility.TerraFxCom; + +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 +{ + /// + bool ITextureProvider.IsDxgiFormatSupportedForCreateFromExistingTextureAsync(int dxgiFormat) => + this.IsDxgiFormatSupportedForCreateFromExistingTextureAsync((DXGI_FORMAT)dxgiFormat); + + /// + public unsafe bool IsDxgiFormatSupportedForCreateFromExistingTextureAsync(DXGI_FORMAT dxgiFormat) + { + 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; + } + + 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; + } + + /// + public Task CreateFromExistingTextureAsync( + IDalamudTextureWrap wrap, + TextureModificationArgs args = default, + bool leaveWrapOpen = false, + string? debugName = null, + 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 + { + using var srv = this.device.CreateShaderResourceView( + tex, + new(tex.Get(), D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE2D)); + + var desc = tex.GetDesc(); + + var outWrap = new UnknownTextureWrap( + (IUnknown*)srv.Get(), + (int)desc.Width, + (int)desc.Height, + true); + this.BlameSetName( + outWrap, + debugName ?? + $"{nameof(this.CreateFromExistingTextureAsync)}({wrap}, {args})"); + return outWrap; + } + }, + cancellationToken, + leaveWrapOpen ? null : wrap); + + /// + Task ITextureProvider.CreateFromImGuiViewportAsync( + ImGuiViewportTextureArgs args, + 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, debugName, ownerPlugin, cancellationToken); + t.QueueUpdate(); + return t.FirstUpdateTask; + } + + /// + public async Task<(RawImageSpecification Specification, byte[] RawData)> GetRawImageAsync( + IDalamudTextureWrap wrap, + TextureModificationArgs args = default, + bool leaveWrapOpen = false, + CancellationToken cancellationToken = default) + { + using var wrapAux = new WrapAux(wrap, leaveWrapOpen); + return await this.GetRawImageAsync(wrapAux, args, cancellationToken); + } + + private async Task<(RawImageSpecification Specification, byte[] RawData)> GetRawImageAsync( + WrapAux wrapAux, + TextureModificationArgs args = default, + CancellationToken cancellationToken = default) + { + using var tex2D = + args.IsCompleteSourceCopy(wrapAux.Desc) + ? wrapAux.NewTexRef() + : await this.NoThrottleCreateFromExistingTextureAsync(wrapAux, args); + + cancellationToken.ThrowIfCancellationRequested(); + + // 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( + ComPtr tex2D, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + 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 = + (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()); + + D3D11_MAPPED_SUBRESOURCE mapped; + context.Get()->Map(mapWhat, 0, D3D11_MAP.D3D11_MAP_READ, 0, &mapped).ThrowOnError(); + + try + { + var specs = new RawImageSpecification(desc, mapped.RowPitch); + var bytes = new Span(mapped.pData, checked((int)mapped.DepthPitch)).ToArray(); + return (specs, bytes); + } + finally + { + context.Get()->Unmap(mapWhat, 0); + } + } + } + + private async Task> NoThrottleCreateFromExistingTextureAsync( + WrapAux wrapAux, + TextureModificationArgs args) + { + args.ThrowOnInvalidValues(); + + if (args.Format == DXGI_FORMAT.DXGI_FORMAT_UNKNOWN) + args = args with { Format = wrapAux.Desc.Format }; + if (args.NewWidth == 0) + 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) * wrapAux.Desc.Height) }; + + 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)); + + 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 . + 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.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..9a7d84deb --- /dev/null +++ b/Dalamud/Interface/Textures/Internal/TextureManager.SharedTextures.cs @@ -0,0 +1,166 @@ +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.PureImpl GetFromGameIcon(in GameIconLookup lookup) => + this.GetFromGame(this.lookupCache.GetOrAdd(lookup, this.GetIconPathByValue)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public SharedImmediateTexture.PureImpl GetFromGame(string path) => + this.gameDict.GetOrAdd(path, GamePathSharedImmediateTexture.CreatePlaceholder) + .PublicUseInstance; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public SharedImmediateTexture.PureImpl GetFromFile(string path) => + this.fileDict.GetOrAdd(path, FileSystemSharedImmediateTexture.CreatePlaceholder) + .PublicUseInstance; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public SharedImmediateTexture.PureImpl GetFromManifestResource(Assembly assembly, string name) => + this.manifestResourceDict.GetOrAdd( + (assembly, name), + ManifestResourceSharedImmediateTexture.CreatePlaceholder) + .PublicUseInstance; + + /// 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..3c93ba875 --- /dev/null +++ b/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs @@ -0,0 +1,656 @@ +using System.Buffers; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +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 +{ + /// + public async Task SaveToStreamAsync( + IDalamudTextureWrap? wrap, + Guid containerGuid, + Stream? stream, + IReadOnlyDictionary? props = null, + bool leaveWrapOpen = false, + bool leaveStreamOpen = false, + CancellationToken cancellationToken = default) + { + 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."); + + using var istream = ManagedIStream.Create(stream, true); + using var wrapAux = new WrapAux(wrap, true); + + 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); + } + finally + { + if (!leaveWrapOpen) + wrap?.Dispose(); + if (!leaveStreamOpen) + stream?.Dispose(); + } + } + + /// + public async Task SaveToFileAsync( + IDalamudTextureWrap? wrap, + Guid containerGuid, + string? path, + IReadOnlyDictionary? props = null, + bool leaveWrapOpen = false, + CancellationToken cancellationToken = default) + { + try + { + 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 wrapAux = new WrapAux(wrap, true); + var pathTemp = Util.GetTempFileNameForFileReplacement(path); + var trashfire = new List(); + try + { + 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 e) + { + trashfire.Add(e); + try + { + if (File.Exists(pathTemp)) + File.Delete(pathTemp); + } + catch (Exception e2) + { + trashfire.Add(e2); + } + } + + throw new AggregateException($"{nameof(this.SaveToFileAsync)} error.", trashfire); + } + finally + { + wrap?.Dispose(); + } + } + + /// + IEnumerable ITextureProvider.GetSupportedImageDecoderInfos() => + this.Wic.GetSupportedDecoderInfos(); + + /// + 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 + /// 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.CreateIStreamViewOfMemory(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 = TerraFxComInterfaceExtensions.CreateIStreamFromFile( + path, + FileMode.Open, + FileAccess.Read, + FileShare.Read); + 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; + private ComPtr wicFactory2; + + /// 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) + fixed (Guid* pclsidWicImagingFactory2 = &CLSID.CLSID_WICImagingFactory2) + fixed (Guid* piidWicImagingFactory2 = &IID.IID_IWICImagingFactory2) + { + 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(); + } + } + } + } + + /// + /// Finalizes an instance of the class. + /// + ~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_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, + 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() + { + 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 CreateIStreamViewOfMemory(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 . + /// 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)dxgiFormat, (int)stride), + 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, + ReadOnlySpan bytes, + Guid containerFormat, + ComPtr stream, + IReadOnlyDictionary? props = null, + CancellationToken cancellationToken = default) + { + 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); + using var encoderFrame = default(ComPtr); + this.wicFactory.Get()->CreateEncoder(&containerFormat, null, encoder.GetAddressOf()).ThrowOnError(); + cancellationToken.ThrowIfCancellationRequested(); + + // See: DirectXTK/Src/ScreenGrab.cpp + var outPixelFormat = specs.Format switch + { + 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_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, + 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_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(); + + using var propertyBag = default(ComPtr); + 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) + { + foreach (var (name, untypedValue) in props) + propertyBag.Get()->Write(name, untypedValue).ThrowOnError(); + } + + encoderFrame.Get()->Initialize(propertyBag).ThrowOnError(); + cancellationToken.ThrowIfCancellationRequested(); + + 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) + { + this.wicFactory.Get()->CreateBitmapFromMemory( + (uint)specs.Width, + (uint)specs.Height, + &inPixelFormat, + (uint)specs.Pitch, + checked((uint)bytes.Length), + pBytes, + tempBitmap.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(outBitmapSource.Get(), null).ThrowOnError(); + + cancellationToken.ThrowIfCancellationRequested(); + + encoderFrame.Get()->Commit().ThrowOnError(); + cancellationToken.ThrowIfCancellationRequested(); + + encoder.Get()->Commit().ThrowOnError(); + } + + private void ReleaseUnmanagedResource() + { + this.wicFactory.Reset(); + this.wicFactory2.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..3266190df --- /dev/null +++ b/Dalamud/Interface/Textures/Internal/TextureManager.cs @@ -0,0 +1,399 @@ +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.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; + +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +namespace Dalamud.Interface.Textures.Internal; + +/// Service responsible for loading and disposing ImGui texture wraps. +[ServiceManager.EarlyLoadedService] +internal sealed partial class TextureManager + : IInternalDisposableService, + 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(); + + [ServiceManager.ServiceDependency] + private readonly Framework framework = Service.Get(); + + [ServiceManager.ServiceDependency] + private readonly InterfaceManager interfaceManager = Service.Get(); + + private DynamicPriorityQueueLoader? dynamicPriorityTextureLoader; + private SharedTextureManager? sharedTextureManager; + private WicManager? wicManager; + private bool disposing; + private ComPtr device; + + [ServiceManager.ServiceConstructor] + private unsafe TextureManager(InterfaceManager.InterfaceManagerWithScene withScene) + { + 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.framework.Update += this.BlameTrackerUpdate; + failsafe.Add(() => this.framework.Update -= this.BlameTrackerUpdate); + this.simpleDrawer.Setup(this.device.Get()); + + failsafe.Cancel(); + } + + /// Finalizes an instance of the class. + ~TextureManager() => this.ReleaseUnmanagedResources(); + + /// 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 + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.simpleDrawer ?? throw new ObjectDisposedException(nameof(TextureManager)); + } + + /// Gets the shared texture manager. + public SharedTextureManager Shared + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.sharedTextureManager ?? throw new ObjectDisposedException(nameof(TextureManager)); + } + + /// Gets the WIC manager. + public WicManager Wic + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.wicManager ?? throw new ObjectDisposedException(nameof(TextureManager)); + } + + /// + void IInternalDisposableService.DisposeService() + { + if (this.disposing) + return; + + 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); + } + + /// + public Task CreateFromImageAsync( + ReadOnlyMemory bytes, + string? debugName = null, + CancellationToken cancellationToken = default) => + this.DynamicPriorityTextureLoader.LoadAsync( + null, + ct => Task.Run( + () => + this.BlameSetName( + this.NoThrottleCreateFromImage(bytes.ToArray(), ct), + debugName ?? + $"{nameof(this.CreateFromImageAsync)}({bytes.Length:n0}b)"), + ct), + cancellationToken); + + /// + public Task CreateFromImageAsync( + Stream stream, + bool leaveOpen = false, + string? debugName = null, + CancellationToken cancellationToken = default) => + 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.BlameSetName( + this.NoThrottleCreateFromImage(ms.GetBuffer(), ct), + debugName ?? + $"{nameof(this.CreateFromImageAsync)}(stream)"); + }, + cancellationToken, + leaveOpen ? null : stream); + + /// + // 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, + string? debugName = null) => + this.BlameSetName( + this.NoThrottleCreateFromRaw(specs, bytes), + debugName ?? $"{nameof(this.CreateFromRaw)}({specs}, {bytes.Length:n0})"); + + /// + 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)}({specs}, {bytes.Length:n0})")), + cancellationToken); + + /// + public Task CreateFromRawAsync( + RawImageSpecification specs, + Stream stream, + bool leaveOpen = false, + string? debugName = null, + CancellationToken cancellationToken = default) => + 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.BlameSetName( + this.NoThrottleCreateFromRaw(specs, ms.GetBuffer().AsSpan(0, (int)ms.Length)), + debugName ?? + $"{nameof(this.CreateFromRawAsync)}({specs}, stream)"); + }, + cancellationToken, + leaveOpen ? null : stream); + + /// + public IDalamudTextureWrap CreateFromTexFile(TexFile file) => + this.BlameSetName( + this.CreateFromTexFileAsync(file).Result, + $"{nameof(this.CreateFromTexFile)}({nameof(file)})"); + + /// + public Task CreateFromTexFileAsync( + TexFile file, + string? debugName = null, + CancellationToken cancellationToken = default) + { + return this.DynamicPriorityTextureLoader.LoadAsync( + null, + _ => Task.FromResult( + this.BlameSetName( + this.NoThrottleCreateFromTexFile(file), + debugName ?? $"{nameof(this.CreateFromTexFile)}({ForceNullable(file.FilePath)?.Path})")), + cancellationToken); + + 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); + + /// + public unsafe bool IsDxgiFormatSupported(DXGI_FORMAT dxgiFormat) + { + 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 unsafe IDalamudTextureWrap NoThrottleCreateFromRaw( + RawImageSpecification specs, + ReadOnlySpan bytes) + { + var texd = new D3D11_TEXTURE2D_DESC + { + Width = (uint)specs.Width, + Height = (uint)specs.Height, + MipLevels = 1, + ArraySize = 1, + Format = specs.Format, + 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) + { + var subrdata = new D3D11_SUBRESOURCE_DATA { pSysMem = dataPtr, SysMemPitch = (uint)specs.Pitch }; + this.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); + this.device.Get()->CreateShaderResourceView((ID3D11Resource*)texture.Get(), &viewDesc, view.GetAddressOf()) + .ThrowOnError(); + + var wrap = new UnknownTextureWrap((IUnknown*)view.Get(), specs.Width, specs.Height, true); + this.BlameSetName(wrap, $"{nameof(this.NoThrottleCreateFromRaw)}({specs}, {bytes.Length:n0})"); + return wrap; + } + + /// 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)DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM; + buffer = buffer.Filter(0, 0, TexFile.TextureFormat.B8G8R8A8); + } + + var wrap = this.NoThrottleCreateFromRaw(new(buffer.Width, buffer.Height, dxgiFormat), buffer.RawData); + 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 + /// . + /// 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. + + var wrap = this.NoThrottleCreateFromTexFile(tf); + 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.IsMainThreadInPresent && 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.IsMainThreadInPresent && ThreadSafety.IsMainThread) + return func(); + return await this.interfaceManager.RunBeforeImGuiRender(func); + } +} diff --git a/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs b/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs new file mode 100644 index 000000000..9e7544fa2 --- /dev/null +++ b/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs @@ -0,0 +1,381 @@ +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 class TextureManagerPluginScoped + : IInternalDisposableService, + ITextureProvider, + ITextureSubstitutionProvider, + ITextureReadbackProvider +{ + private readonly LocalPlugin plugin; + private readonly bool nonAsyncFunctionAccessDuringLoadIsError; + + private Task? managerTaskNullable; + + /// Initializes a new instance of the class. + /// The plugin. + [ServiceManager.ServiceConstructor] + public 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; + } + } + + /// + void IInternalDisposableService.DisposeService() + { + 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 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, + TextureModificationArgs args = default, + bool leaveWrapOpen = false, + string? debugName = null, + CancellationToken cancellationToken = default) + { + var manager = await this.ManagerTask; + var textureWrap = await manager.CreateFromExistingTextureAsync( + wrap, + args, + leaveWrapOpen, + debugName, + cancellationToken); + manager.Blame(textureWrap, this.plugin); + return textureWrap; + } + + /// + 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, debugName, cancellationToken); + manager.Blame(textureWrap, this.plugin); + return textureWrap; + } + + /// + public async Task CreateFromImageAsync( + ReadOnlyMemory bytes, + string? debugName = null, + CancellationToken cancellationToken = default) + { + var manager = await this.ManagerTask; + var textureWrap = await manager.CreateFromImageAsync(bytes, debugName, cancellationToken); + manager.Blame(textureWrap, this.plugin); + return textureWrap; + } + + /// + 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, debugName, cancellationToken); + manager.Blame(textureWrap, this.plugin); + return textureWrap; + } + + /// + public IDalamudTextureWrap CreateFromRaw( + RawImageSpecification specs, + ReadOnlySpan bytes, + string? debugName = null) + { + var manager = this.ManagerOrThrow; + var textureWrap = manager.CreateFromRaw(specs, bytes, debugName); + manager.Blame(textureWrap, this.plugin); + return textureWrap; + } + + /// + 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, debugName, cancellationToken); + manager.Blame(textureWrap, this.plugin); + return textureWrap; + } + + /// + public async Task CreateFromRawAsync( + 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, debugName, 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, + string? debugName = null, + CancellationToken cancellationToken = default) + { + var manager = await this.ManagerTask; + var textureWrap = await manager.CreateFromTexFileAsync(file, debugName, cancellationToken); + manager.Blame(textureWrap, this.plugin); + return textureWrap; + } + + /// + public IEnumerable GetSupportedImageDecoderInfos() => + this.ManagerOrThrow.Wic.GetSupportedDecoderInfos(); + + /// + public ISharedImmediateTexture GetFromGameIcon(in GameIconLookup lookup) + { + var shared = this.ManagerOrThrow.Shared.GetFromGameIcon(lookup); + shared.AddOwnerPlugin(this.plugin); + return shared; + } + + /// + public ISharedImmediateTexture GetFromGame(string path) + { + var shared = this.ManagerOrThrow.Shared.GetFromGame(path); + shared.AddOwnerPlugin(this.plugin); + return shared; + } + + /// + public ISharedImmediateTexture GetFromFile(string path) + { + var shared = this.ManagerOrThrow.Shared.GetFromFile(path); + shared.AddOwnerPlugin(this.plugin); + return shared; + } + + /// + public ISharedImmediateTexture GetFromManifestResource(Assembly assembly, string name) + { + var shared = this.ManagerOrThrow.Shared.GetFromManifestResource(assembly, name); + shared.AddOwnerPlugin(this.plugin); + return shared; + } + + /// + 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/RawImageSpecification.cs b/Dalamud/Interface/Textures/RawImageSpecification.cs new file mode 100644 index 000000000..6f31cbbf7 --- /dev/null +++ b/Dalamud/Interface/Textures/RawImageSpecification.cs @@ -0,0 +1,276 @@ +using TerraFX.Interop.DirectX; + +namespace Dalamud.Interface.Textures; + +/// 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 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; + } + + /// 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. + 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; } + + /// 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 => + 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. + /// + /// The width. + /// The height. + /// The new instance. + public static RawImageSpecification Bgra32(int width, int height) => + new(width, height, (int)DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM, width * 4); + + /// + /// 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, (int)DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM, width * 4); + + /// + /// 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, (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) + { + 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 = false; + return true; + default: + bitsPerPixel = 0; + isBlockCompression = false; + return false; + } + } +} diff --git a/Dalamud/Interface/Textures/TextureModificationArgs.cs b/Dalamud/Interface/Textures/TextureModificationArgs.cs new file mode 100644 index 000000000..abccca6b5 --- /dev/null +++ b/Dalamud/Interface/Textures/TextureModificationArgs.cs @@ -0,0 +1,126 @@ +using System.Numerics; +using System.Text; + +using Dalamud.Plugin.Services; + +using TerraFX.Interop.DirectX; + +namespace Dalamud.Interface.Textures; + +/// 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. + 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_UNKNOWN; + + /// 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 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 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; + + /// + 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. + /// 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/TextureWraps/DalamudTextureWrap.cs b/Dalamud/Interface/Textures/TextureWraps/DalamudTextureWrap.cs new file mode 100644 index 000000000..668e5a177 --- /dev/null +++ b/Dalamud/Interface/Textures/TextureWraps/DalamudTextureWrap.cs @@ -0,0 +1,46 @@ +using Dalamud.Utility; + +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. +[Obsolete($"Use {nameof(IDalamudTextureWrap)}.")] +[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] +public class DalamudTextureWrap : IDalamudTextureWrap, IDeferredDisposable +{ + private readonly TextureWrap wrappedWrap; + + /// Initializes a new instance of the class. + /// The texture wrap to wrap. + internal DalamudTextureWrap(TextureWrap wrappingWrap) => this.wrappedWrap = wrappingWrap; + + /// Finalizes an instance of the class. + ~DalamudTextureWrap() => this.Dispose(false); + + /// + public IntPtr ImGuiHandle => this.wrappedWrap.ImGuiHandle; + + /// + public int Width => this.wrappedWrap.Width; + + /// + public int Height => this.wrappedWrap.Height; + + /// 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(); + + private void Dispose(bool disposing) + { + if (disposing) + Service.GetNullable()?.EnqueueDeferredDispose(this); + } +} diff --git a/Dalamud/Interface/Textures/TextureWraps/ForwardingTextureWrap.cs b/Dalamud/Interface/Textures/TextureWraps/ForwardingTextureWrap.cs new file mode 100644 index 000000000..8b0516e03 --- /dev/null +++ b/Dalamud/Interface/Textures/TextureWraps/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.TextureWraps.Internal; + +using TerraFX.Interop.Windows; + +namespace Dalamud.Interface.Textures.TextureWraps; + +/// 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/Internal/IDalamudTextureWrap.cs b/Dalamud/Interface/Textures/TextureWraps/IDalamudTextureWrap.cs similarity index 72% rename from Dalamud/Interface/Internal/IDalamudTextureWrap.cs rename to Dalamud/Interface/Textures/TextureWraps/IDalamudTextureWrap.cs index 8e2e56c26..09d64ad21 100644 --- a/Dalamud/Interface/Internal/IDalamudTextureWrap.cs +++ b/Dalamud/Interface/Textures/TextureWraps/IDalamudTextureWrap.cs @@ -1,33 +1,34 @@ 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; +// ReSharper disable once CheckNamespace namespace Dalamud.Interface.Internal; +// TODO(api10): fix namespace maybe? /// /// 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. - /// + /// 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/TextureWraps/Internal/DisposeSuppressingTextureWrap.cs b/Dalamud/Interface/Textures/TextureWraps/Internal/DisposeSuppressingTextureWrap.cs new file mode 100644 index 000000000..0dd5c9f25 --- /dev/null +++ b/Dalamud/Interface/Textures/TextureWraps/Internal/DisposeSuppressingTextureWrap.cs @@ -0,0 +1,20 @@ +using Dalamud.Interface.Internal; + +namespace Dalamud.Interface.Textures.TextureWraps.Internal; + +/// A texture wrap that ignores calls. +internal class DisposeSuppressingTextureWrap : ForwardingTextureWrap +{ + private readonly IDalamudTextureWrap innerWrap; + + /// Initializes a new instance of the class. + /// The inner wrap. + public DisposeSuppressingTextureWrap(IDalamudTextureWrap wrap) => this.innerWrap = wrap; + + /// + protected override bool TryGetWrap(out IDalamudTextureWrap? wrap) + { + wrap = this.innerWrap; + return true; + } +} diff --git a/Dalamud/Interface/Internal/UnknownTextureWrap.cs b/Dalamud/Interface/Textures/TextureWraps/Internal/UnknownTextureWrap.cs similarity index 70% rename from Dalamud/Interface/Internal/UnknownTextureWrap.cs rename to Dalamud/Interface/Textures/TextureWraps/Internal/UnknownTextureWrap.cs index 41164f2c3..ec23d7d03 100644 --- a/Dalamud/Interface/Internal/UnknownTextureWrap.cs +++ b/Dalamud/Interface/Textures/TextureWraps/Internal/UnknownTextureWrap.cs @@ -1,21 +1,19 @@ using System.Threading; +using Dalamud.Interface.Internal; +using Dalamud.Interface.Textures.Internal; using Dalamud.Utility; using TerraFX.Interop.Windows; -namespace Dalamud.Interface.Internal; +namespace Dalamud.Interface.Textures.TextureWraps.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. @@ -31,9 +29,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); /// @@ -48,18 +44,18 @@ 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. - /// + /// + public override string ToString() => + $"{nameof(UnknownTextureWrap)}({Service.GetNullable()?.GetBlame(this)?.Name ?? $"{this.imGuiHandle:X}"})"; + + /// Actually dispose the wrapped texture. void IDeferredDisposable.RealDispose() { var handle = Interlocked.Exchange(ref this.imGuiHandle, nint.Zero); diff --git a/Dalamud/Interface/Textures/TextureWraps/Internal/ViewportTextureWrap.cs b/Dalamud/Interface/Textures/TextureWraps/Internal/ViewportTextureWrap.cs new file mode 100644 index 000000000..ad3188925 --- /dev/null +++ b/Dalamud/Interface/Textures/TextureWraps/Internal/ViewportTextureWrap.cs @@ -0,0 +1,276 @@ +using System.Diagnostics; +using System.Threading; +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; + +using ImGuiNET; + +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +using NotSupportedException = System.NotSupportedException; + +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 +{ + private readonly string? debugName; + private readonly LocalPlugin? ownerPlugin; + 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 disposed; + + /// 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, string? debugName, LocalPlugin? ownerPlugin, CancellationToken cancellationToken) + { + this.args = args; + this.debugName = debugName; + this.ownerPlugin = ownerPlugin; + this.cancellationToken = cancellationToken; + } + + /// Finalizes an instance of the class. + ~ViewportTextureWrap() => this.Dispose(false); + + /// + 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; + + /// + 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.args.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()); + + 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 = copyBox.right - copyBox.left, + Height = copyBox.bottom - copyBox.top, + 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 = texDesc; + 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, + this.debugName ?? $"{nameof(ViewportTextureWrap)}({this.args})"); + } + + // 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); + + 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); + } + catch (Exception e) + { + this.firstUpdateTaskCompletionSource.TrySetException(e); + } + + 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() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// Actually dispose the wrapped texture. + void IDeferredDisposable.RealDispose() + { + _ = this.FirstUpdateTask.Exception; + this.tex.Reset(); + this.srv.Reset(); + this.rtv.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[1])->GetResource(resource.GetAddressOf()); + resource.As(&texture).ThrowOnError(); + } + + return texture; + } + + private void Dispose(bool disposing) + { + this.disposed = true; + this.args.AutoUpdate = false; + if (disposing) + Service.GetNullable()?.EnqueueDeferredDispose(this); + else + ((IDeferredDisposable)this).RealDispose(); + } +} diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 12a87a451..7311b0b91 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -12,12 +12,11 @@ using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.ManagedFontAtlas.Internals; +using Dalamud.Plugin.Internal.Types; using Dalamud.Utility; using ImGuiNET; -using ImGuiScene; - using Serilog; using SharpDX.Direct3D11; @@ -30,6 +29,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; @@ -53,14 +53,16 @@ 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 { 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); @@ -338,36 +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(). - public IDalamudTextureWrap LoadImage(string filePath) - => this.InterfaceManagerWithScene?.LoadImage(filePath) - ?? throw new InvalidOperationException("Load failed."); - - /// - /// 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(). - public IDalamudTextureWrap LoadImage(byte[] imageData) - => this.InterfaceManagerWithScene?.LoadImage(imageData) - ?? throw new InvalidOperationException("Load failed."); - - /// - /// 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(). - public IDalamudTextureWrap LoadImageRaw(byte[] imageData, int width, int height, int numChannels) - => this.InterfaceManagerWithScene?.LoadImageRaw(imageData, width, height, numChannels) - ?? throw new InvalidOperationException("Load failed."); - /// /// Loads an ULD file that can load textures containing multiple icons in a single texture. /// @@ -376,39 +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(). - public Task LoadImageAsync(string filePath) => Task.Run( - async () => - (await this.InterfaceManagerWithSceneAsync).LoadImage(filePath) - ?? throw new InvalidOperationException("Load failed.")); - - /// - /// 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(). - public Task LoadImageAsync(byte[] imageData) => Task.Run( - async () => - (await this.InterfaceManagerWithSceneAsync).LoadImage(imageData) - ?? throw new InvalidOperationException("Load failed.")); - - /// - /// 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(). - 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.")); - /// /// Waits for UI to become available for use. /// @@ -482,7 +421,8 @@ public sealed class UiBuilder : IDisposable .CreateFontAtlas( this.namespaceName + ":" + (debugName ?? "custom"), autoRebuildMode, - isGlobalScaled)); + isGlobalScaled, + this.plugin)); /// /// Unregister the UiBuilder. Do not call this in plugin code. diff --git a/Dalamud/Interface/UldWrapper.cs b/Dalamud/Interface/UldWrapper.cs index dd8986bed..35330c5d0 100644 --- a/Dalamud/Interface/UldWrapper.cs +++ b/Dalamud/Interface/UldWrapper.cs @@ -4,6 +4,8 @@ using System.Linq; using Dalamud.Data; using Dalamud.Interface.Internal; +using Dalamud.Interface.Textures; +using Dalamud.Interface.Textures.Internal; using Dalamud.Utility; using Lumina.Data.Files; using Lumina.Data.Parsing.Uld; @@ -14,16 +16,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 +126,10 @@ public class UldWrapper : IDisposable inputSlice.CopyTo(outputSlice); } - return this.uiBuilder.LoadImageRaw(imageData, part.W, part.H, 4); + 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/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(); +} diff --git a/Dalamud/Logging/Internal/TaskTracker.cs b/Dalamud/Logging/Internal/TaskTracker.cs index ae3dae5e9..b45ed82d6 100644 --- a/Dalamud/Logging/Internal/TaskTracker.cs +++ b/Dalamud/Logging/Internal/TaskTracker.cs @@ -37,8 +37,9 @@ internal class TaskTracker : IInternalDisposableService /// /// 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. diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index 3ddfa3101..915d6a392 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -54,7 +54,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/Plugin/Services/ITextureProvider.cs b/Dalamud/Plugin/Services/ITextureProvider.cs index dc8ae1f86..5ebcb6145 100644 --- a/Dalamud/Plugin/Services/ITextureProvider.cs +++ b/Dalamud/Plugin/Services/ITextureProvider.cs @@ -1,95 +1,247 @@ -using System.IO; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; using Dalamud.Interface.Internal; +using Dalamud.Interface.Internal.Windows.Data.Widgets; +using Dalamud.Interface.Textures; + 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. +/// +/// +/// 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 +/// .
+/// Use and alike to obtain a reference of +/// that will stay valid for the rest of the frame. +///
+/// +/// debugName parameter can be used to name your textures, to aid debugging resource leaks using +/// . +/// +///
public interface ITextureProvider { - /// - /// Flags describing the icon you wish to receive. + /// 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. /// - [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 source texture wrap. The passed value may be disposed once this function returns, + /// without having to wait for the completion of the returned . + /// 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. + Task CreateFromExistingTextureAsync( + 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. + /// + /// Use ImGui.GetMainViewport().ID to capture the game screen with Dalamud rendered. + /// This function may throw an exception. + /// + 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. + /// + /// 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, + 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, + 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. + /// + /// 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, + bool leaveOpen = false, + string? debugName = null, + CancellationToken cancellationToken = default); /// - /// 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); - - /// - /// 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); - - /// - /// 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 . + /// Alias for fetching from . /// /// The texture to obtain a handle to. - /// A texture wrap that can be used to render the texture. - public IDalamudTextureWrap GetTexture(TexFile file); + /// 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. + /// 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. + /// The supported bitmap decoders. + /// + /// The following functions support the files of the container types pointed by yielded values. + ///
    + ///
  • + ///
  • + ///
  • + ///
  • + ///
+ /// This function may throw an exception. + ///
+ IEnumerable GetSupportedImageDecoderInfos(); + + /// 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 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 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. + /// 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. + /// This function does not throw exceptions. + 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. + /// 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/ITextureReadbackProvider.cs b/Dalamud/Plugin/Services/ITextureReadbackProvider.cs new file mode 100644 index 000000000..96c817091 --- /dev/null +++ b/Dalamud/Plugin/Services/ITextureReadbackProvider.cs @@ -0,0 +1,105 @@ +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. + /// 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 + /// . + /// + /// This function may throw an exception. + /// + Task SaveToFileAsync( + IDalamudTextureWrap wrap, + Guid containerGuid, + string path, + IReadOnlyDictionary? props = null, + bool leaveWrapOpen = false, + CancellationToken cancellationToken = default); +} 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); } diff --git a/Dalamud/Storage/Assets/DalamudAssetManager.cs b/Dalamud/Storage/Assets/DalamudAssetManager.cs index 3536bfb52..b967b17bc 100644 --- a/Dalamud/Storage/Assets/DalamudAssetManager.cs +++ b/Dalamud/Storage/Assets/DalamudAssetManager.cs @@ -7,6 +7,8 @@ using System.Threading; 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; @@ -58,7 +60,7 @@ internal sealed class DalamudAssetManager : IInternalDisposableService, IDalamud 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( @@ -78,17 +80,20 @@ internal sealed class DalamudAssetManager : IInternalDisposableService, IDalamud "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(true))) + 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}")); } /// public IDalamudTextureWrap Empty4X4 => this.GetDalamudTextureWrap(DalamudAsset.Empty4X4); + /// + public IDalamudTextureWrap White4X4 => this.GetDalamudTextureWrap(DalamudAsset.White4X4); + /// void IInternalDisposableService.DisposeService() { @@ -310,17 +315,18 @@ internal sealed class DalamudAssetManager : IInternalDisposableService, IDalamud 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 name = $"{nameof(DalamudAsset)}[{Enum.GetName(asset)}]"; var image = purpose switch { - DalamudAssetPurpose.TextureFromPng => im.LoadImage(buf), + DalamudAssetPurpose.TextureFromPng => await tm.CreateFromImageAsync(buf, name), DalamudAssetPurpose.TextureFromRaw => asset.GetAttribute() is { } raw - ? im.LoadImageFromDxgiFormat(buf, raw.Pitch, raw.Width, raw.Height, raw.Format) + ? await tm.CreateFromRawAsync(raw.Specification, buf, name) : throw new InvalidOperationException( "TextureFromRaw must accompany a DalamudAssetRawTextureAttribute."), _ => null, @@ -328,7 +334,7 @@ internal sealed class DalamudAssetManager : IInternalDisposableService, IDalamud 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) { @@ -350,26 +356,4 @@ internal sealed class DalamudAssetManager : IInternalDisposableService, IDalamud 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 - } - } } diff --git a/Dalamud/Storage/Assets/DalamudAssetRawTextureAttribute.cs b/Dalamud/Storage/Assets/DalamudAssetRawTextureAttribute.cs index b79abb7d7..b9219afcc 100644 --- a/Dalamud/Storage/Assets/DalamudAssetRawTextureAttribute.cs +++ b/Dalamud/Storage/Assets/DalamudAssetRawTextureAttribute.cs @@ -1,45 +1,23 @@ -using SharpDX.DXGI; +using Dalamud.Interface.Textures; + +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.Width = width; - this.Pitch = pitch; - this.Height = height; - this.Format = format; - } + /// The pitch. + public DalamudAssetRawTextureAttribute(int width, int height, DXGI_FORMAT format, int pitch) => + this.Specification = new(width, height, (int)format, pitch); /// - /// 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; } } 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/DynamicPriorityQueueLoader.cs b/Dalamud/Utility/DynamicPriorityQueueLoader.cs new file mode 100644 index 000000000..8109d2e94 --- /dev/null +++ b/Dalamud/Utility/DynamicPriorityQueueLoader.cs @@ -0,0 +1,288 @@ +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; + +namespace Dalamud.Utility; + +/// Base class for loading resources in dynamic order. +internal class DynamicPriorityQueueLoader : IDisposable +{ + private readonly CancellationTokenSource disposeCancellationTokenSource = new(); + private readonly Task adderTask; + private readonly Task[] workerTasks; + + private readonly Channel newItemChannel; + private readonly Channel workTokenChannel; + private readonly List workItemPending = new(); + + private bool disposing; + + /// 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[concurrency]; + foreach (ref var task in this.workerTasks.AsSpan()) + task = Task.Run(this.LoopProcessWorkItemAsync); + } + + /// Provider for priority metrics. + 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; } + } + + /// + public void Dispose() + { + if (this.disposing) + return; + + this.disposing = true; + 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; + } + + /// 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. + /// + /// 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) + { + 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(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; + var newWorks = new List(batchAddSize); + var reader = this.newItemChannel.Reader; + while (await reader.WaitToReadAsync()) + { + while (newWorks.Count < batchAddSize && reader.TryRead(out var newWork)) + newWorks.Add(newWork); + + lock (this.workItemPending) + this.workItemPending.AddRange(newWorks); + + for (var i = newWorks.Count; i > 0; i--) + this.workTokenChannel.Writer.TryWrite(null); + + newWorks.Clear(); + } + } + + /// Continuously processes work items in , until all items are processed and + /// is called. + private async Task LoopProcessWorkItemAsync() + { + var reader = this.workTokenChannel.Reader; + while (await reader.WaitToReadAsync()) + { + if (!reader.TryRead(out _)) + continue; + + if (this.ExtractHighestPriorityWorkItem() is not { } work) + continue; + + await work.Process(this.disposeCancellationTokenSource.Token); + work.Dispose(); + } + } + + /// 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.workItemPending) + { + for (var startIndex = 0; startIndex < this.workItemPending.Count - 1;) + { + var span = CollectionsMarshal.AsSpan(this.workItemPending)[startIndex..]; + ref var lastRef = ref span[^1]; + foreach (ref var itemRef in span[..^1]) + { + if (itemRef.CancelAsRequested()) + { + itemRef.Dispose(); + itemRef = lastRef; + this.workItemPending.RemoveAt(this.workItemPending.Count - 1); + break; + } + + if (itemRef.CompareTo(lastRef) < 0) + (itemRef, lastRef) = (lastRef, itemRef); + startIndex++; + } + } + + if (this.workItemPending.Count == 0) + return null; + + var last = this.workItemPending[^1]; + this.workItemPending.RemoveAt(this.workItemPending.Count - 1); + if (last.CancelAsRequested()) + { + last.Dispose(); + return null; + } + + return last; + } + } + + /// A read-only implementation of . + private class ReadOnlyThrottleBasisProvider : IThrottleBasisProvider + { + /// + public bool IsOpportunistic { get; init; } = false; + + /// + public long FirstRequestedTick { get; init; } = Environment.TickCount64; + + /// + public long LatestRequestedTick { get; init; } = Environment.TickCount64; + } + + /// Represents a work item added from . + private abstract class WorkItem : IComparable, IDisposable + { + private readonly IThrottleBasisProvider basis; + private readonly IDisposable?[] disposables; + + protected WorkItem( + IThrottleBasisProvider basis, + CancellationToken cancellationToken, + params IDisposable?[] disposables) + { + this.basis = basis; + this.CancellationToken = cancellationToken; + this.disposables = disposables; + } + + protected CancellationToken CancellationToken { get; } + + 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) + 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 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) + { + 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); + + // Suppress the OperationCanceledException caused from the above. + _ = this.taskCompletionSource.Task.Exception; + + return true; + } + + public override async ValueTask Process(CancellationToken serviceDisposeToken) + { + try + { + T 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 as IDisposable)?.Dispose(); + } + catch (Exception e) + { + this.taskCompletionSource.TrySetException(e); + _ = this.taskCompletionSource.Task.Exception; + } + } + } +} 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/TerraFxCom/ManagedIStream.cs b/Dalamud/Utility/TerraFxCom/ManagedIStream.cs new file mode 100644 index 000000000..caec65da2 --- /dev/null +++ b/Dalamud/Utility/TerraFxCom/ManagedIStream.cs @@ -0,0 +1,454 @@ +using System.Buffers; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +using TerraFX.Interop; +using TerraFX.Interop.Windows; + +namespace Dalamud.Utility.TerraFxCom; + +/// 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 innerStream; + private readonly bool leaveOpen; + private readonly nint[] comObject; + private readonly IStream.Vtbl vtbl; + private GCHandle gchThis; + private GCHandle gchComObject; + private GCHandle gchVtbl; + private int refCount; + + private ManagedIStream(Stream innerStream, bool leaveOpen = false) + { + this.innerStream = innerStream ?? throw new NullReferenceException(); + this.leaveOpen = leaveOpen; + 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 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_UNEXPECTED; + + [UnmanagedCallersOnly] + static uint AddRefStatic(IStream* pThis) => (uint)(ToManagedObject(pThis)?.AddRef() ?? 0); + + [UnmanagedCallersOnly] + 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_UNEXPECTED; + + [UnmanagedCallersOnly] + static int WriteStatic(IStream* pThis, void* pv, uint cb, uint* pcbWritten) => + 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_UNEXPECTED; + + [UnmanagedCallersOnly] + static int SetSizeStatic(IStream* pThis, ULARGE_INTEGER libNewSize) => + 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_UNEXPECTED; + + [UnmanagedCallersOnly] + static int CommitStatic(IStream* pThis, uint grfCommitFlags) => + ToManagedObject(pThis)?.Commit(grfCommitFlags) ?? E.E_UNEXPECTED; + + [UnmanagedCallersOnly] + 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_UNEXPECTED; + + [UnmanagedCallersOnly] + static int UnlockRegionStatic( + IStream* pThis, ULARGE_INTEGER libOffset, ULARGE_INTEGER cb, uint dwLockType) => + 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_UNEXPECTED; + + [UnmanagedCallersOnly] + static int CloneStatic(IStream* pThis, IStream** ppstm) => ToManagedObject(pThis)?.Clone(ppstm) ?? E.E_UNEXPECTED; + } + + /// + 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(); + + /// 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) + { + 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(); + if (!this.leaveOpen) + this.innerStream.Dispose(); + 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.innerStream.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.innerStream.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.innerStream.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.innerStream.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.innerStream.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.innerStream.Length; + streamStats.grfMode = 0; + if (this.innerStream.CanRead && this.innerStream.CanWrite) + streamStats.grfMode |= STGM.STGM_READWRITE; + else if (this.innerStream.CanRead) + streamStats.grfMode |= STGM.STGM_READ; + else if (this.innerStream.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; +} 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); +} 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; + } +} 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; + } } diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index f77edf55a..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. /// @@ -657,12 +681,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. ///