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