Merge pull request #1676 from Soreepeong/feature/itextureprovider-updates

ITextureProvider updates
This commit is contained in:
goat 2024-06-06 19:22:42 +02:00 committed by GitHub
commit a1b8ff9141
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
59 changed files with 7775 additions and 1233 deletions

View file

@ -456,6 +456,9 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// </summary>
public double UiBuilderHitch { get; set; } = 100;
/// <summary>Gets or sets a value indicating whether to track texture allocation by plugins.</summary>
public bool UseTexturePluginTracking { get; set; }
/// <summary>
/// Gets or sets the page of the plugin installer that is shown by default when opened.
/// </summary>

View file

@ -64,6 +64,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BitFaster.Caching" Version="2.4.1" />
<PackageReference Include="CheapLoc" Version="1.1.8" />
<PackageReference Include="goaaats.Reloaded.Hooks" Version="4.2.0-goat.4" />
<PackageReference Include="goaaats.Reloaded.Assembler" Version="1.0.14-goat.2" />

View file

@ -1,5 +1,7 @@
using Dalamud.Storage.Assets;
using TerraFX.Interop.DirectX;
namespace Dalamud;
/// <summary>
@ -19,9 +21,16 @@ public enum DalamudAsset
/// <see cref="DalamudAssetPurpose.TextureFromRaw"/>: The fallback empty texture.
/// </summary>
[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,
/// <summary>
/// <see cref="DalamudAssetPurpose.TextureFromRaw"/>: The fallback empty texture.
/// </summary>
[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,
/// <summary>
/// <see cref="DalamudAssetPurpose.TextureFromPng"/>: The Dalamud logo.
/// </summary>

View file

@ -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;
/// <remarks>If there was no texture loaded for any reason, the plugin icon will be displayed instead.</remarks>
internal class FilePathNotificationIcon : INotificationIcon
{
private readonly FileInfo fileInfo;
private readonly string filePath;
/// <summary>Initializes a new instance of the <see cref="FilePathNotificationIcon"/> class.</summary>
/// <param name="filePath">The path to a .tex file inside the game resources.</param>
public FilePathNotificationIcon(string filePath) => this.fileInfo = new(filePath);
public FilePathNotificationIcon(string filePath) => this.filePath = new(filePath);
/// <inheritdoc/>
public bool DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color) =>
NotificationUtilities.DrawIconFrom(
minCoord,
maxCoord,
Service<TextureManager>.Get().GetTextureFromFile(this.fileInfo));
Service<TextureManager>.Get().Shared.GetFromFile(this.filePath).GetWrapOrDefault());
/// <inheritdoc/>
public override bool Equals(object? obj) =>
obj is FilePathNotificationIcon r && r.fileInfo.FullName == this.fileInfo.FullName;
obj is FilePathNotificationIcon r && r.filePath == this.filePath;
/// <inheritdoc/>
public override int GetHashCode() => HashCode.Combine(this.GetType().GetHashCode(), this.fileInfo.FullName);
public override int GetHashCode() => HashCode.Combine(this.GetType().GetHashCode(), this.filePath);
/// <inheritdoc/>
public override string ToString() => $"{nameof(FilePathNotificationIcon)}({this.fileInfo.FullName})";
public override string ToString() => $"{nameof(FilePathNotificationIcon)}({this.filePath})";
}

View file

@ -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
/// <summary>Initializes a new instance of the <see cref="GamePathNotificationIcon"/> class.</summary>
/// <param name="gamePath">The path to a .tex file inside the game resources.</param>
/// <remarks>Use <see cref="ITextureProvider.GetIconPath"/> to get the game path from icon IDs.</remarks>
public GamePathNotificationIcon(string gamePath) => this.gamePath = gamePath;
/// <inheritdoc/>
@ -21,7 +19,7 @@ internal class GamePathNotificationIcon : INotificationIcon
NotificationUtilities.DrawIconFrom(
minCoord,
maxCoord,
Service<TextureManager>.Get().GetTextureFromGame(this.gamePath));
Service<TextureManager>.Get().Shared.GetFromGame(this.gamePath).GetWrapOrDefault());
/// <inheritdoc/>
public override bool Equals(object? obj) => obj is GamePathNotificationIcon r && r.gamePath == this.gamePath;

View file

@ -1,71 +0,0 @@
using Dalamud.Utility;
using ImGuiScene;
namespace Dalamud.Interface.Internal;
/// <summary>
/// Safety harness for ImGuiScene textures that will defer destruction until
/// the end of the frame.
/// </summary>
public class DalamudTextureWrap : IDalamudTextureWrap, IDeferredDisposable
{
private readonly TextureWrap wrappedWrap;
/// <summary>
/// Initializes a new instance of the <see cref="DalamudTextureWrap"/> class.
/// </summary>
/// <param name="wrappingWrap">The texture wrap to wrap.</param>
internal DalamudTextureWrap(TextureWrap wrappingWrap)
{
this.wrappedWrap = wrappingWrap;
}
/// <summary>
/// Finalizes an instance of the <see cref="DalamudTextureWrap"/> class.
/// </summary>
~DalamudTextureWrap()
{
this.Dispose(false);
}
/// <summary>
/// Gets the ImGui handle of the texture.
/// </summary>
public IntPtr ImGuiHandle => this.wrappedWrap.ImGuiHandle;
/// <summary>
/// Gets the width of the texture.
/// </summary>
public int Width => this.wrappedWrap.Width;
/// <summary>
/// Gets the height of the texture.
/// </summary>
public int Height => this.wrappedWrap.Height;
/// <summary>
/// Queue the texture to be disposed once the frame ends.
/// </summary>
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Actually dispose the wrapped texture.
/// </summary>
void IDeferredDisposable.RealDispose()
{
this.wrappedWrap.Dispose();
}
private void Dispose(bool disposing)
{
if (disposing)
{
Service<InterfaceManager>.GetNullable()?.EnqueueDeferredDispose(this);
}
}
}

View file

@ -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<IDeferredDisposable> deferredDisposeTextures = new();
private readonly ConcurrentBag<ILockedImFont> deferredDisposeImFontLockeds = new();
private readonly ConcurrentBag<IDisposable> deferredDisposeDisposables = new();
[ServiceManager.ServiceDependency]
private readonly WndProcHookManager wndProcHookManager = Service<WndProcHookManager>.Get();
@ -78,6 +76,9 @@ internal class InterfaceManager : IInternalDisposableService
[ServiceManager.ServiceDependency]
private readonly Framework framework = Service<Framework>.Get();
private readonly ConcurrentQueue<Action> runBeforeImGuiRender = new();
private readonly ConcurrentQueue<Action> runAfterImGuiRender = new();
private readonly SwapChainVtableResolver address = new();
private RawDX11Scene? scene;
@ -214,6 +215,10 @@ internal class InterfaceManager : IInternalDisposableService
/// </summary>
public bool IsDispatchingEvents { get; set; } = true;
/// <summary>Gets a value indicating whether the main thread is executing <see cref="PresentDetour"/>.</summary>
/// <remarks>This still will be <c>true</c> even when queried off the main thread.</remarks>
public bool IsMainThreadInPresent { get; private set; }
/// <summary>
/// Gets a value indicating the native handle of the game main window.
/// </summary>
@ -244,9 +249,11 @@ internal class InterfaceManager : IInternalDisposableService
/// </summary>
public Task FontBuildTask => WhenFontsReady().dalamudAtlas!.BuildTask;
/// <summary>
/// Gets the number of calls to <see cref="PresentDetour"/> so far.
/// </summary>
/// <summary>Gets the number of calls to <see cref="PresentDetour"/> so far.</summary>
/// <remarks>
/// The value increases even when Dalamud is hidden via &quot;/xlui hide&quot;.
/// <see cref="DalamudInterface.FrameCount"/> does not.
/// </remarks>
public long CumulativePresentCalls { get; private set; }
/// <summary>
@ -294,138 +301,6 @@ internal class InterfaceManager : IInternalDisposableService
}
}
#nullable enable
/// <summary>
/// Load an image from disk.
/// </summary>
/// <param name="filePath">The filepath to load.</param>
/// <returns>A texture, ready to use in ImGui.</returns>
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;
}
/// <summary>
/// Load an image from an array of bytes.
/// </summary>
/// <param name="imageData">The data to load.</param>
/// <returns>A texture, ready to use in ImGui.</returns>
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;
}
/// <summary>
/// Load an image from an array of bytes.
/// </summary>
/// <param name="imageData">The data to load.</param>
/// <param name="width">The width in pixels.</param>
/// <param name="height">The height in pixels.</param>
/// <param name="numChannels">The number of channels.</param>
/// <returns>A texture, ready to use in ImGui.</returns>
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;
}
/// <summary>
/// Check whether the current D3D11 Device supports the given DXGI format.
/// </summary>
/// <param name="dxgiFormat">DXGI format to check.</param>
/// <returns>Whether it is supported.</returns>
public bool SupportsDxgiFormat(Format dxgiFormat) => this.scene is null
? throw new InvalidOperationException("Scene isn't ready.")
: this.scene.Device.CheckFormatSupport(dxgiFormat).HasFlag(FormatSupport.Texture2D);
/// <summary>
/// Load an image from a span of bytes of specified format.
/// </summary>
/// <param name="data">The data to load.</param>
/// <param name="pitch">The pitch(stride) in bytes.</param>
/// <param name="width">The width in pixels.</param>
/// <param name="height">The height in pixels.</param>
/// <param name="dxgiFormat">Format of the texture.</param>
/// <returns>A texture, ready to use in ImGui.</returns>
public DalamudTextureWrap LoadImageFromDxgiFormat(Span<byte> 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
/// <summary>
/// Sets up a deferred invocation of font rebuilding, before the next render frame.
/// </summary>
@ -448,9 +323,97 @@ internal class InterfaceManager : IInternalDisposableService
/// Enqueue an <see cref="ILockedImFont"/> to be disposed at the end of the frame.
/// </summary>
/// <param name="locked">The disposable.</param>
public void EnqueueDeferredDispose(in ILockedImFont locked)
public void EnqueueDeferredDispose(IDisposable locked)
{
this.deferredDisposeImFontLockeds.Add(locked);
this.deferredDisposeDisposables.Add(locked);
}
/// <summary>Queues an action to be run before <see cref="ImGui.Render"/> call.</summary>
/// <param name="action">The action.</param>
/// <returns>A <see cref="Task"/> that resolves once <paramref name="action"/> is run.</returns>
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;
}
/// <summary>Queues a function to be run before <see cref="ImGui.Render"/> call.</summary>
/// <typeparam name="T">The type of the return value.</typeparam>
/// <param name="func">The function.</param>
/// <returns>A <see cref="Task"/> that resolves once <paramref name="func"/> is run.</returns>
public Task<T> RunBeforeImGuiRender<T>(Func<T> func)
{
var tcs = new TaskCompletionSource<T>();
this.runBeforeImGuiRender.Enqueue(
() =>
{
try
{
tcs.SetResult(func());
}
catch (Exception e)
{
tcs.SetException(e);
}
});
return tcs.Task;
}
/// <summary>Queues an action to be run after <see cref="ImGui.Render"/> call.</summary>
/// <param name="action">The action.</param>
/// <returns>A <see cref="Task"/> that resolves once <paramref name="action"/> is run.</returns>
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;
}
/// <summary>Queues a function to be run after <see cref="ImGui.Render"/> call.</summary>
/// <typeparam name="T">The type of the return value.</typeparam>
/// <param name="func">The function.</param>
/// <returns>A <see cref="Task"/> that resolves once <paramref name="func"/> is run.</returns>
public Task<T> RunAfterImGuiRender<T>(Func<T> func)
{
var tcs = new TaskCompletionSource<T>();
this.runAfterImGuiRender.Enqueue(
() =>
{
try
{
tcs.SetResult(func());
}
catch (Exception e)
{
tcs.SetException(e);
}
});
return tcs.Task;
}
/// <summary>
@ -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();
}
}

View file

@ -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
/// <summary>
/// Service responsible for loading and disposing ImGui texture wraps.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.EarlyLoadedService]
#pragma warning disable SA1015
[ResolveVia<ITextureProvider>]
[ResolveVia<ITextureSubstitutionProvider>]
#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<string, TextureInfo> activeTextures = new();
private IDalamudTextureWrap? fallbackTextureWrap;
/// <summary>
/// Initializes a new instance of the <see cref="TextureManager"/> class.
/// </summary>
/// <param name="dalamud">Dalamud instance.</param>
/// <param name="framework">Framework instance.</param>
/// <param name="dataManager">DataManager instance.</param>
/// <param name="im">InterfaceManager instance.</param>
[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<InterfaceManager.InterfaceManagerWithScene>.GetAsync().ContinueWith(_ => this.CreateFallbackTexture());
}
/// <inheritdoc/>
public event ITextureSubstitutionProvider.TextureDataInterceptorDelegate? InterceptTexDataLoad;
/// <summary>
/// Get a texture handle for a specific icon.
/// </summary>
/// <param name="iconId">The ID of the icon to load.</param>
/// <param name="flags">Options to be considered when loading the icon.</param>
/// <param name="language">
/// 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.
/// </param>
/// <param name="keepAlive">
/// Not used. This parameter is ignored.
/// </param>
/// <returns>
/// Null, if the icon does not exist in the specified configuration, or a texture wrap that can be used
/// to render the icon.
/// </returns>
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);
}
/// <summary>
/// Get a path for a specific icon's .tex file.
/// </summary>
/// <param name="iconId">The ID of the icon to look up.</param>
/// <param name="flags">Options to be considered when loading the icon.</param>
/// <param name="language">
/// 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.
/// </param>
/// <returns>
/// 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.
/// </returns>
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;
}
/// <summary>
/// Get a texture handle for the texture at the specified path.
/// You may only specify paths in the game's VFS.
/// </summary>
/// <param name="path">The path to the texture in the game's VFS.</param>
/// <param name="keepAlive">Not used. This parameter is ignored.</param>
/// <returns>Null, if the icon does not exist, or a texture wrap that can be used to render the texture.</returns>
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);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="file">The FileInfo describing the image or texture file.</param>
/// <param name="keepAlive">Not used. This parameter is ignored.</param>
/// <returns>Null, if the file does not exist, or a texture wrap that can be used to render the texture.</returns>
public IDalamudTextureWrap? GetTextureFromFile(FileInfo file, bool keepAlive = false)
{
ArgumentNullException.ThrowIfNull(file);
return !file.Exists ? null : this.CreateWrap(file.FullName);
}
/// <summary>
/// Get a texture handle for the specified Lumina TexFile.
/// </summary>
/// <param name="file">The texture to obtain a handle to.</param>
/// <returns>A texture wrap that can be used to render the texture.</returns>
/// <exception cref="InvalidOperationException">Thrown when the graphics system is not available yet. Relevant for plugins when LoadRequiredState is set to 0 or 1.</exception>
/// <exception cref="NotSupportedException">Thrown when the given <see cref="TexFile"/> is not supported. Most likely is that the file is corrupt.</exception>
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);
}
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
public void InvalidatePaths(IEnumerable<string> 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;
}
}
}
/// <inheritdoc/>
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();
}
/// <summary>
/// Get texture info.
/// </summary>
/// <param name="path">Path to the texture.</param>
/// <param name="rethrow">
/// 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.
/// </param>
/// <returns>Info object storing texture metadata.</returns>
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<TexFile>(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<TexFile>(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<string>();
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");
}
/// <summary>
/// Internal representation of a managed texture.
/// </summary>
internal class TextureInfo
{
/// <summary>
/// Gets or sets the actual texture wrap. May be unpopulated.
/// </summary>
public IDalamudTextureWrap? Wrap { get; set; }
/// <summary>
/// Gets or sets the time the texture was last accessed.
/// </summary>
public DateTime LastAccess { get; set; }
/// <summary>
/// Gets or sets the extents of the texture.
/// </summary>
public Vector2 Extents { get; set; }
}
}
/// <summary>
/// Wrap.
/// </summary>
internal class TextureManagerTextureWrap : IDalamudTextureWrap
{
private readonly TextureManager manager;
private readonly string path;
/// <summary>
/// Initializes a new instance of the <see cref="TextureManagerTextureWrap"/> class.
/// </summary>
/// <param name="path">The path to the texture.</param>
/// <param name="extents">The extents of the texture.</param>
/// <param name="manager">Manager that we obtained this from.</param>
internal TextureManagerTextureWrap(string path, Vector2 extents, TextureManager manager)
{
this.path = path;
this.manager = manager;
this.Width = (int)extents.X;
this.Height = (int)extents.Y;
}
/// <inheritdoc/>
public IntPtr ImGuiHandle => !this.IsDisposed ?
this.manager.GetInfo(this.path).Wrap!.ImGuiHandle :
throw new InvalidOperationException("Texture already disposed. You may not render it.");
/// <inheritdoc/>
public int Width { get; private set; }
/// <inheritdoc/>
public int Height { get; private set; }
/// <summary>
/// Gets a value indicating whether or not this wrap has already been disposed.
/// If true, the handle may be invalid.
/// </summary>
internal bool IsDisposed { get; private set; }
/// <inheritdoc/>
public void Dispose()
{
this.IsDisposed = true;
// This is a no-op. The manager cleans up textures that are not being drawn.
}
}

View file

@ -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;
/// </summary>
public class IconBrowserWidget : IDataWindowWidget
{
// Remove range 170,000 -> 180,000 by default, this specific range causes exceptions.
private readonly HashSet<int> nullValues = Enumerable.Range(170000, 9999).ToHashSet();
private Vector2 iconSize = new(64.0f, 64.0f);
private Vector2 editIconSize = new(64.0f, 64.0f);
private List<int> valueRange = Enumerable.Range(0, 200000).ToList();
private int lastNullValueCount;
private List<int>? valueRange;
private Task<List<(int ItemId, string Path)>>? iconIdsTask;
private int startRange;
private int stopRange = 200000;
private bool showTooltipImage;
private Vector2 mouseDragStart;
private bool dragStarted;
private Vector2 lastWindowSize = Vector2.Zero;
/// <inheritdoc/>
public string[]? CommandShortcuts { get; init; } = { "icon", "icons" };
@ -45,31 +43,58 @@ public class IconBrowserWidget : IDataWindowWidget
public void Load()
{
}
/// <inheritdoc/>
public void Draw()
{
this.iconIdsTask ??= Task.Run(
() =>
{
var texm = Service<TextureManager>.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<TextureManager>.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<TextureManager>.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<DevTextureSaveMenu>.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<InterfaceManager>.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<TextureManager>.Get().GetIconPath((uint)iconId);
return !filePath.IsNullOrEmpty() && Service<DataManager>.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<int>();
}
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);
}
}
}

View file

@ -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<TextureManager>.Get().GetIcon(i));
n.IconTexture = DisposeLoggingTextureWrap.Wrap(
Service<TextureManager>.Get().Shared.GetFromGameIcon(new(i)).GetWrapOrDefault());
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -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<InterfaceManager.InterfaceManagerWithScene>.GetAsync()).Manager;
var framework = await Service<Framework>.GetAsync();
var textureManager = await Service<TextureManager>.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;
}

View file

@ -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<IDalamudTextureWrap>?[]? testerImages;
private Task<IDalamudTextureWrap>? 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<IDalamudTextureWrap>? 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<InterfaceManager>.Get();
var tm = Service<TextureManager>.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<IDalamudTextureWrap>?[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)

View file

@ -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<byte>();
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;

View file

@ -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
/// <param name="atlasName">Name of atlas, for debugging and logging purposes.</param>
/// <param name="autoRebuildMode">Specify how to auto rebuild.</param>
/// <param name="isGlobalScaled">Whether the fonts in the atlas are under the effect of global scale.</param>
/// <param name="ownerPlugin">The owner plugin, if any.</param>
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
/// <inheritdoc/>
public bool IsGlobalScaled { get; }
/// <summary>Gets the owner plugin, if any.</summary>
public LocalPlugin? OwnerPlugin { get; }
/// <inheritdoc/>
public void Dispose()
{

View file

@ -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
/// </summary>
public InterfaceManager InterfaceManager { get; }
/// <summary>
/// Gets the service instance of <see cref="TextureManager"/>.
/// </summary>
public TextureManager TextureManager => Service<TextureManager>.Get();
/// <summary>
/// Gets the async task for <see cref="RawDX11Scene"/> inside <see cref="InterfaceManager"/>.
/// </summary>
@ -174,12 +179,14 @@ internal sealed partial class FontAtlasFactory
/// <param name="atlasName">Name of atlas, for debugging and logging purposes.</param>
/// <param name="autoRebuildMode">Specify how to auto rebuild.</param>
/// <param name="isGlobalScaled">Whether the fonts in the atlas is global scaled.</param>
/// <param name="ownerPlugin">The owner plugin, if any.</param>
/// <returns>The new font atlas.</returns>
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);
/// <summary>
/// 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<T>(Task<T> t) => t.IsCompleted ? t.Result : t.GetAwaiter().GetResult();
/// <summary>
/// Clones a texture wrap, by getting a new reference to the underlying <see cref="ShaderResourceView"/> and the
/// texture behind.
/// </summary>
/// <param name="wrap">The <see cref="IDalamudTextureWrap"/> to clone from.</param>
/// <returns>The cloned <see cref="IDalamudTextureWrap"/>.</returns>
private static IDalamudTextureWrap CloneTextureWrap(IDalamudTextureWrap wrap)
{
var srv = CppObject.FromPointer<ShaderResourceView>(wrap.ImGuiHandle);
using var res = srv.Resource;
using var tex2D = res.QueryInterface<Texture2D>();
var description = tex2D.Description;
return new DalamudTextureWrap(
new D3DTextureWrap(
srv.QueryInterface<ShaderResourceView>(),
description.Width,
description.Height));
}
private static unsafe void ExtractChannelFromB8G8R8A8(
Span<byte> target,
ReadOnlySpan<byte> source,
@ -346,7 +334,7 @@ internal sealed partial class FontAtlasFactory
var numPixels = texFile.Header.Width * texFile.Header.Height;
_ = Service<InterfaceManager.InterfaceManagerWithScene>.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<byte>.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
{

View file

@ -0,0 +1,55 @@
using System.Text;
namespace Dalamud.Interface.Textures;
/// <summary>Represents a lookup for a game icon.</summary>
public readonly record struct GameIconLookup
{
/// <summary>Initializes a new instance of the <see cref="GameIconLookup"/> class.</summary>
/// <param name="iconId">The icon ID.</param>
/// <param name="itemHq">Whether the HQ icon is requested, where HQ is in the context of items.</param>
/// <param name="hiRes">Whether the high-resolution icon is requested.</param>
/// <param name="language">The language of the icon to load.</param>
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);
/// <summary>Gets the icon ID.</summary>
public uint IconId { get; init; }
/// <summary>Gets a value indicating whether the HQ icon is requested, where HQ is in the context of items.</summary>
public bool ItemHq { get; init; }
/// <summary>Gets a value indicating whether the high-resolution icon is requested.</summary>
public bool HiRes { get; init; }
/// <summary>Gets the language of the icon to load.</summary>
/// <remarks>
/// <para><c>null</c> will use the active game language.</para>
/// <para>If the specified resource does not have variants per language, the language-neutral texture will be used.
/// </para>
/// </remarks>
public ClientLanguage? Language { get; init; }
/// <inheritdoc/>
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();
}
}

View file

@ -0,0 +1,19 @@
using System.Collections.Generic;
namespace Dalamud.Interface.Textures;
/// <summary>Represents an available bitmap codec.</summary>
public interface IBitmapCodecInfo
{
/// <summary>Gets the friendly name for the codec.</summary>
string Name { get; }
/// <summary>Gets the <see cref="Guid"/> representing the container.</summary>
Guid ContainerGuid { get; }
/// <summary>Gets the suggested file extensions.</summary>
IReadOnlyCollection<string> Extensions { get; }
/// <summary>Gets the corresponding mime types.</summary>
IReadOnlyCollection<string> MimeTypes { get; }
}

View file

@ -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;
/// <summary>A texture with a backing instance of <see cref="IDalamudTextureWrap"/> that is shared across multiple
/// requesters.</summary>
/// <remarks>
/// <para>Calling <see cref="IDisposable.Dispose"/> on this interface is a no-op.</para>
/// <para><see cref="GetWrapOrEmpty"/> and <see cref="TryGetWrap"/> may stop returning the intended texture at any point.
/// Use <see cref="RentAsync"/> to lock the texture for use in any thread for any duration.</para>
/// </remarks>
public interface ISharedImmediateTexture
{
/// <summary>Gets the texture for use with the current frame, or an empty texture if unavailable.</summary>
/// <returns>An instance of <see cref="IDalamudTextureWrap"/> that is guaranteed to be available for the current
/// frame being drawn.</returns>
/// <remarks>
/// <para>Calling outside the main thread will fail.</para>
/// <para>This function does not throw.</para>
/// <para><see cref="IDisposable.Dispose"/> will be ignored.</para>
/// <para>If the texture is unavailable for any reason, then the returned instance of
/// <see cref="IDalamudTextureWrap"/> will point to an empty texture instead.</para>
/// </remarks>
IDalamudTextureWrap GetWrapOrEmpty();
/// <summary>Gets the texture for use with the current frame, or a default value specified via
/// <paramref name="defaultWrap"/> if unavailable.</summary>
/// <param name="defaultWrap">The default wrap to return if the requested texture was not immediately available.
/// </param>
/// <returns>An instance of <see cref="IDalamudTextureWrap"/> that is guaranteed to be available for the current
/// frame being drawn.</returns>
/// <remarks>
/// <para>Calling outside the main thread will fail.</para>
/// <para>This function does not throw.</para>
/// <para><see cref="IDisposable.Dispose"/> will be ignored.</para>
/// <para>If the texture is unavailable for any reason, then <paramref name="defaultWrap"/> will be returned.</para>
/// </remarks>
[return: NotNullIfNotNull(nameof(defaultWrap))]
IDalamudTextureWrap? GetWrapOrDefault(IDalamudTextureWrap? defaultWrap = null);
/// <summary>Attempts to get the texture for use with the current frame.</summary>
/// <param name="texture">An instance of <see cref="IDalamudTextureWrap"/> that is guaranteed to be available for
/// the current frame being drawn, or <c>null</c> if texture is not loaded (yet).</param>
/// <param name="exception">The load exception, if any.</param>
/// <returns><c>true</c> if <paramref name="texture"/> points to the loaded texture; <c>false</c> if the texture is
/// still being loaded, or the load has failed.</returns>
/// <remarks>
/// <para>Calling outside the main thread will fail.</para>
/// <para>This function does not throw.</para>
/// <para><see cref="IDisposable.Dispose"/> on the returned <paramref name="texture"/> will be ignored.</para>
/// </remarks>
/// <exception cref="InvalidOperationException">Thrown when called outside the UI thread.</exception>
bool TryGetWrap([NotNullWhen(true)] out IDalamudTextureWrap? texture, out Exception? exception);
/// <summary>Creates a new instance of <see cref="IDalamudTextureWrap"/> holding a new reference to this texture.
/// The returned texture is guaranteed to be available until <see cref="IDisposable.Dispose"/> is called.</summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A <see cref="Task{TResult}"/> containing the loaded texture on success.</returns>
/// <remarks>
/// <see cref="IDisposable.Dispose"/> must be called on the resulting instance of <see cref="IDalamudTextureWrap"/>
/// from the returned <see cref="Task{TResult}"/> after use. Consider using
/// <see cref="DisposeSafety.ToContentDisposedTask{T}"/> to dispose the result automatically according to the state
/// of the task.</remarks>
Task<IDalamudTextureWrap> RentAsync(CancellationToken cancellationToken = default);
}

View file

@ -0,0 +1,95 @@
using System.Numerics;
using System.Text;
using Dalamud.Interface.Internal;
using ImGuiNET;
using TerraFX.Interop.DirectX;
namespace Dalamud.Interface.Textures;
/// <summary>Describes how to take a texture of an existing ImGui viewport.</summary>
public record struct ImGuiViewportTextureArgs()
{
/// <summary>Gets or sets the ImGui Viewport ID to capture.</summary>
/// <remarks>Use <see cref="ImGuiViewport.ID"/> from <see cref="ImGui.GetMainViewport"/> to take the main viewport,
/// where the game renders to.</remarks>
public uint ViewportId { get; set; }
/// <summary>Gets or sets a value indicating whether to automatically update the texture.</summary>
/// <remarks>Enabling this will also update <see cref="IDalamudTextureWrap.Size"/> as needed.</remarks>
public bool AutoUpdate { get; set; }
/// <summary>Gets or sets a value indicating whether to get the texture before rendering ImGui.</summary>
/// <remarks>It probably makes no sense to enable this unless <see cref="ViewportId"/> points to the main viewport.
/// </remarks>
public bool TakeBeforeImGuiRender { get; set; }
/// <summary>Gets or sets a value indicating whether to keep the transparency.</summary>
/// <remarks>
/// <para>If <c>true</c>, then the alpha channel values will be filled with 1.0.</para>
/// <para>Keep in mind that screen captures generally do not need alpha values.</para>
/// </remarks>
// Intentionally not "MakeOpaque", to accommodate the use of default value of this record struct.
public bool KeepTransparency { get; set; } = false;
/// <summary>Gets or sets the left top coordinates relative to the size of the source texture.</summary>
/// <para>Coordinates should be in range between 0 and 1.</para>
public Vector2 Uv0 { get; set; } = Vector2.Zero;
/// <summary>Gets or sets the right bottom coordinates relative to the size of the source texture.</summary>
/// <para>Coordinates should be in range between 0 and 1.</para>
/// <remarks>If set to <see cref="Vector2.Zero"/>, then it will be interpreted as <see cref="Vector2.One"/>,
/// to accommodate the use of default value of this record struct.</remarks>
public Vector2 Uv1 { get; set; } = Vector2.One;
/// <summary>Gets the effective value of <see cref="Uv1"/>.</summary>
internal Vector2 Uv1Effective => this.Uv1 == Vector2.Zero ? Vector2.One : this.Uv1;
/// <inheritdoc/>
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();
}
/// <summary>Checks the properties and throws an exception if values are invalid.</summary>
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.");
}
}
}

View file

@ -0,0 +1,55 @@
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Dalamud.Utility;
using TerraFX.Interop.Windows;
namespace Dalamud.Interface.Textures.Internal;
/// <summary>Represents an available bitmap codec.</summary>
internal sealed class BitmapCodecInfo : IBitmapCodecInfo
{
/// <summary>Initializes a new instance of the <see cref="BitmapCodecInfo"/> class.</summary>
/// <param name="codecInfo">The source codec info. Ownership is not transferred.</param>
public unsafe BitmapCodecInfo(ComPtr<IWICBitmapCodecInfo> codecInfo)
{
this.Name = ReadStringUsing(
codecInfo,
((IWICBitmapCodecInfo.Vtbl<IWICBitmapCodecInfo>*)codecInfo.Get()->lpVtbl)->GetFriendlyName);
Guid temp;
codecInfo.Get()->GetContainerFormat(&temp).ThrowOnError();
this.ContainerGuid = temp;
this.Extensions = ReadStringUsing(
codecInfo,
((IWICBitmapCodecInfo.Vtbl<IWICBitmapCodecInfo>*)codecInfo.Get()->lpVtbl)->GetFileExtensions)
.Split(',');
this.MimeTypes = ReadStringUsing(
codecInfo,
((IWICBitmapCodecInfo.Vtbl<IWICBitmapCodecInfo>*)codecInfo.Get()->lpVtbl)->GetMimeTypes)
.Split(',');
}
/// <summary>Gets the friendly name for the codec.</summary>
public string Name { get; }
/// <summary>Gets the <see cref="Guid"/> representing the container.</summary>
public Guid ContainerGuid { get; }
/// <summary>Gets the suggested file extensions.</summary>
public IReadOnlyCollection<string> Extensions { get; }
/// <summary>Gets the corresponding mime types.</summary>
public IReadOnlyCollection<string> MimeTypes { get; }
private static unsafe string ReadStringUsing(
IWICBitmapCodecInfo* codecInfo,
delegate* unmanaged<IWICBitmapCodecInfo*, uint, ushort*, uint*, int> 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);
}
}

View file

@ -0,0 +1,36 @@
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Interface.Internal;
namespace Dalamud.Interface.Textures.Internal.SharedImmediateTextures;
/// <summary>Represents a sharable texture, based on a file on the system filesystem.</summary>
internal sealed class FileSystemSharedImmediateTexture : SharedImmediateTexture
{
private readonly string path;
/// <summary>Initializes a new instance of the <see cref="FileSystemSharedImmediateTexture"/> class.</summary>
/// <param name="path">The path.</param>
private FileSystemSharedImmediateTexture(string path)
: base(path) => this.path = path;
/// <summary>Creates a new placeholder instance of <see cref="GamePathSharedImmediateTexture"/>.</summary>
/// <param name="path">The path.</param>
/// <returns>The new instance.</returns>
/// <remarks>Only to be used from <see cref="TextureManager.SharedTextureManager.GetFromFile"/>.</remarks>
public static SharedImmediateTexture CreatePlaceholder(string path) => new FileSystemSharedImmediateTexture(path);
/// <inheritdoc/>
public override string ToString() =>
$"{nameof(FileSystemSharedImmediateTexture)}#{this.InstanceIdForDebug}({this.path})";
/// <inheritdoc/>
protected override async Task<IDalamudTextureWrap> CreateTextureAsync(CancellationToken cancellationToken)
{
var tm = await Service<TextureManager>.GetAsync();
var wrap = await tm.NoThrottleCreateFromFileAsync(this.path, cancellationToken);
tm.BlameSetName(wrap, this.ToString());
return wrap;
}
}

View file

@ -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;
/// <summary>Represents a sharable texture, based on a file in game resources.</summary>
internal sealed class GamePathSharedImmediateTexture : SharedImmediateTexture
{
private readonly string path;
/// <summary>Initializes a new instance of the <see cref="GamePathSharedImmediateTexture"/> class.</summary>
/// <param name="path">The path.</param>
private GamePathSharedImmediateTexture(string path)
: base(path) => this.path = path;
/// <summary>Creates a new placeholder instance of <see cref="GamePathSharedImmediateTexture"/>.</summary>
/// <param name="path">The path.</param>
/// <returns>The new instance.</returns>
/// <remarks>Only to be used from <see cref="TextureManager.SharedTextureManager.GetFromGame"/>.</remarks>
public static SharedImmediateTexture CreatePlaceholder(string path) => new GamePathSharedImmediateTexture(path);
/// <inheritdoc/>
public override string ToString() =>
$"{nameof(GamePathSharedImmediateTexture)}#{this.InstanceIdForDebug}({this.path})";
/// <inheritdoc/>
protected override async Task<IDalamudTextureWrap> CreateTextureAsync(CancellationToken cancellationToken)
{
var dm = await Service<DataManager>.GetAsync();
var tm = await Service<TextureManager>.GetAsync();
var substPath = tm.GetSubstitutedPath(this.path);
if (dm.GetFile<TexFile>(substPath) is not { } file)
throw new FileNotFoundException();
cancellationToken.ThrowIfCancellationRequested();
var wrap = tm.NoThrottleCreateFromTexFile(file);
tm.BlameSetName(wrap, this.ToString());
return wrap;
}
}

View file

@ -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;
/// <summary>Represents a sharable texture, based on a manifest texture obtained from
/// <see cref="Assembly.GetManifestResourceStream(string)"/>.</summary>
internal sealed class ManifestResourceSharedImmediateTexture : SharedImmediateTexture
{
private readonly Assembly assembly;
private readonly string name;
/// <summary>Initializes a new instance of the <see cref="ManifestResourceSharedImmediateTexture"/> class.</summary>
/// <param name="assembly">The assembly containing manifest resources.</param>
/// <param name="name">The case-sensitive name of the manifest resource being requested.</param>
private ManifestResourceSharedImmediateTexture(Assembly assembly, string name)
: base($"{assembly.GetName().FullName}:{name}")
{
this.assembly = assembly;
this.name = name;
}
/// <summary>Creates a new placeholder instance of <see cref="ManifestResourceSharedImmediateTexture"/>.</summary>
/// <param name="args">The arguments to pass to the constructor.</param>
/// <returns>The new instance.</returns>
/// <remarks>Only to be used from <see cref="TextureManager.SharedTextureManager.GetFromManifestResource"/>.
/// </remarks>
public static SharedImmediateTexture CreatePlaceholder((Assembly Assembly, string Name) args) =>
new ManifestResourceSharedImmediateTexture(args.Assembly, args.Name);
/// <inheritdoc/>
protected override async Task<IDalamudTextureWrap> 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<TextureManager>.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;
}
}

View file

@ -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;
/// <summary>Represents a texture that may have multiple reference holders (owners).</summary>
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<LocalPlugin> ownerPlugins = new();
private bool resourceReleased;
private int refCount;
private long selfReferenceExpiry;
private IDalamudTextureWrap? availableOnAccessWrapForApi9;
private CancellationTokenSource? cancellationTokenSource;
private NotOwnedTextureWrap? nonOwningWrap;
/// <summary>Initializes a new instance of the <see cref="SharedImmediateTexture"/> class.</summary>
/// <param name="sourcePathForDebug">Name of the underlying resource.</param>
/// <remarks>The new instance is a placeholder instance.</remarks>
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);
}
/// <summary>Gets a wrapper for this instance which disables resource reference management.</summary>
public PureImpl PublicUseInstance { get; }
/// <summary>Gets the instance ID. Debug use only.</summary>
public long InstanceIdForDebug { get; }
/// <summary>Gets the remaining time for self reference in milliseconds. Debug use only.</summary>
public long SelfReferenceExpiresInForDebug =>
this.selfReferenceExpiry == SelfReferenceExpiryExpired
? 0
: Math.Max(0, this.selfReferenceExpiry - Environment.TickCount64);
/// <summary>Gets the reference count. Debug use only.</summary>
public int RefCountForDebug => this.refCount;
/// <summary>Gets the source path. Debug use only.</summary>
public string SourcePathForDebug { get; }
/// <summary>Gets a value indicating whether this instance of <see cref="SharedImmediateTexture"/> supports revival.
/// </summary>
public bool HasRevivalPossibility => this.RevivalPossibility?.TryGetTarget(out _) is true;
/// <summary>Gets or sets the underlying texture wrap.</summary>
public Task<IDalamudTextureWrap>? UnderlyingWrap { get; set; }
/// <inheritdoc/>
public bool IsOpportunistic { get; private set; }
/// <inheritdoc/>
public long FirstRequestedTick { get; private set; }
/// <inheritdoc/>
public long LatestRequestedTick { get; private set; }
/// <summary>Gets a value indicating whether the content has been queried,
/// i.e. <see cref="TryGetWrap"/> or <see cref="RentAsync"/> is called.</summary>
public bool ContentQueried { get; private set; }
/// <summary>Gets a cancellation token for cancelling load.
/// Intended to be called from implementors' constructors and <see cref="LoadUnderlyingWrap"/>.</summary>
protected CancellationToken LoadCancellationToken => this.cancellationTokenSource?.Token ?? default;
/// <summary>Gets or sets a weak reference to an object that demands this objects to be alive.</summary>
/// <remarks>
/// 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 <see cref="GetAvailableOnAccessWrapForApi9"/>. If this no longer points to an alive
/// object, and <see cref="availableOnAccessWrapForApi9"/> is null, then this object is not used from API9 use case.
/// </remarks>
private WeakReference<IDalamudTextureWrap>? RevivalPossibility { get; set; }
/// <inheritdoc/>
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(),
};
/// <inheritdoc/>
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();
}
}
/// <summary>Releases self-reference, if conditions are met.</summary>
/// <param name="immediate">If set to <c>true</c>, the self-reference will be released immediately.</param>
/// <returns>Number of the new reference count that may or may not have changed.</returns>
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();
}
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public IDalamudTextureWrap GetWrapOrEmpty() => this.GetWrapOrDefault(Service<DalamudAssetManager>.Get().Empty4X4);
/// <inheritdoc/>
[return: NotNullIfNotNull(nameof(defaultWrap))]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public IDalamudTextureWrap? GetWrapOrDefault(IDalamudTextureWrap? defaultWrap)
{
if (!this.TryGetWrap(out var texture, out _))
texture = null;
return texture ?? defaultWrap;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetWrap([NotNullWhen(true)] out IDalamudTextureWrap? texture, out Exception? exception)
{
ThreadSafety.AssertMainThread();
return this.TryGetWrapCore(out texture, out exception);
}
/// <inheritdoc/>
public async Task<IDalamudTextureWrap> 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);
}
/// <summary>Gets a texture wrap which ensures that the values will be populated on access.</summary>
/// <returns>The texture wrap, or null if failed.</returns>
[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;
}
/// <summary>Adds a plugin to <see cref="ownerPlugins"/>, in a thread-safe way.</summary>
/// <param name="plugin">The plugin to add.</param>
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<TextureManager>.Get().Blame(r.Result, plugin);
},
default(CancellationToken));
}
}
}
/// <inheritdoc/>
public override string ToString() => $"{this.GetType().Name}#{this.InstanceIdForDebug}({this.SourcePathForDebug})";
/// <summary>Cleans up this instance of <see cref="SharedImmediateTexture"/>.</summary>
protected void ClearUnderlyingWrap()
{
_ = this.UnderlyingWrap?.ToContentDisposedTask(true);
this.UnderlyingWrap = null;
}
/// <summary>Attempts to restore the reference to this texture.</summary>
protected void LoadUnderlyingWrap()
{
int addLen;
lock (this.ownerPlugins)
{
this.UnderlyingWrap = Service<TextureManager>.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<TextureManager>.Get().Blame(r.Result, op);
}
},
default(CancellationToken));
}
/// <summary>Creates the texture immediately.</summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The task resulting in a loaded texture.</returns>
/// <remarks>This function is intended to be called from texture load scheduler.
/// See <see cref="LoadUnderlyingWrap"/> and note that this function is being used as the callback from
/// <see cref="DynamicPriorityQueueLoader.LoadAsync{T}"/>.</remarks>
protected abstract Task<IDalamudTextureWrap> 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;
}
}
}
/// <summary><see cref="ISharedImmediateTexture.TryGetWrap"/>, but without checking for thread.</summary>
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<InterfaceManager>.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;
}
}
/// <summary>A wrapper around <see cref="SharedImmediateTexture"/>, to prevent external consumers from mistakenly
/// calling <see cref="IDisposable.Dispose"/> or <see cref="IRefCountable.Release"/>.</summary>
internal sealed class PureImpl : ISharedImmediateTexture
{
private readonly SharedImmediateTexture inner;
/// <summary>Initializes a new instance of the <see cref="PureImpl"/> class.</summary>
/// <param name="inner">The actual instance.</param>
public PureImpl(SharedImmediateTexture inner) => this.inner = inner;
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public IDalamudTextureWrap GetWrapOrEmpty() =>
this.inner.GetWrapOrEmpty();
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[return: NotNullIfNotNull(nameof(defaultWrap))]
public IDalamudTextureWrap? GetWrapOrDefault(IDalamudTextureWrap? defaultWrap = null) =>
this.inner.GetWrapOrDefault(defaultWrap);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetWrap([NotNullWhen(true)] out IDalamudTextureWrap? texture, out Exception? exception) =>
this.inner.TryGetWrap(out texture, out exception);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Task<IDalamudTextureWrap> RentAsync(CancellationToken cancellationToken = default) =>
this.inner.RentAsync(cancellationToken);
/// <inheritdoc cref="SharedImmediateTexture.GetAvailableOnAccessWrapForApi9"/>
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public IDalamudTextureWrap? GetAvailableOnAccessWrapForApi9() =>
this.inner.GetAvailableOnAccessWrapForApi9();
/// <inheritdoc cref="SharedImmediateTexture.AddOwnerPlugin"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddOwnerPlugin(LocalPlugin plugin) =>
this.inner.AddOwnerPlugin(plugin);
/// <inheritdoc/>
public override string ToString() => $"{this.inner}({nameof(PureImpl)})";
}
/// <summary>Same with <see cref="DisposeSuppressingTextureWrap"/>, but with a custom implementation of
/// <see cref="CreateWrapSharingLowLevelResource"/>.</summary>
private sealed class NotOwnedTextureWrap : DisposeSuppressingTextureWrap
{
private readonly IRefCountable owner;
/// <summary>Initializes a new instance of the <see cref="NotOwnedTextureWrap"/> class.</summary>
/// <param name="wrap">The inner wrap.</param>
/// <param name="owner">The reference counting owner.</param>
public NotOwnedTextureWrap(IDalamudTextureWrap wrap, IRefCountable owner)
: base(wrap)
{
this.owner = owner;
}
/// <inheritdoc/>
public override IDalamudTextureWrap CreateWrapSharingLowLevelResource()
{
var wrap = this.GetWrap();
this.owner.AddRef();
return new RefCountableWrappingTextureWrap(wrap, this.owner);
}
/// <inheritdoc/>
public override string ToString() => $"{nameof(NotOwnedTextureWrap)}({this.owner})";
}
/// <summary>Reference counting texture wrap, to be used with <see cref="RentAsync"/>.</summary>
private sealed class RefCountableWrappingTextureWrap : ForwardingTextureWrap
{
private IDalamudTextureWrap? innerWrap;
private IRefCountable? owner;
/// <summary>Initializes a new instance of the <see cref="RefCountableWrappingTextureWrap"/> class.</summary>
/// <param name="wrap">The inner wrap.</param>
/// <param name="owner">The reference counting owner.</param>
public RefCountableWrappingTextureWrap(IDalamudTextureWrap wrap, IRefCountable owner)
{
this.innerWrap = wrap;
this.owner = owner;
}
/// <summary>Finalizes an instance of the <see cref="RefCountableWrappingTextureWrap"/> class.</summary>
~RefCountableWrappingTextureWrap() => this.Dispose(false);
/// <inheritdoc/>
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);
}
/// <inheritdoc/>
public override string ToString() => $"{nameof(RefCountableWrappingTextureWrap)}({this.owner})";
/// <inheritdoc/>
protected override bool TryGetWrap(out IDalamudTextureWrap? wrap) => (wrap = this.innerWrap) is not null;
/// <inheritdoc/>
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();
}
}
}
/// <summary>A texture wrap that revives and waits for the underlying texture as needed on every access.</summary>
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
private sealed class AvailableOnAccessTextureWrap : ForwardingTextureWrap
{
private readonly SharedImmediateTexture inner;
/// <summary>Initializes a new instance of the <see cref="AvailableOnAccessTextureWrap"/> class.</summary>
/// <param name="inner">The shared texture.</param>
public AvailableOnAccessTextureWrap(SharedImmediateTexture inner) => this.inner = inner;
/// <inheritdoc/>
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<DalamudAssetManager>.Get().Empty4X4;
}
}
return new RefCountableWrappingTextureWrap(wrap, this.inner);
}
catch
{
this.inner.Release();
throw;
}
}
/// <inheritdoc/>
public override string ToString() => $"{nameof(AvailableOnAccessTextureWrap)}({this.inner})";
/// <inheritdoc/>
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<DalamudAssetManager>.Get().Empty4X4;
return true;
}
}
}

View file

@ -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;
/// <summary>Service responsible for loading and disposing ImGui texture wraps.</summary>
internal sealed partial class TextureManager
{
/// <summary>A wrapper for underlying texture2D resources.</summary>
public interface IBlameableDalamudTextureWrap : IDalamudTextureWrap
{
/// <summary>Gets the address of the native resource.</summary>
public nint ResourceAddress { get; }
/// <summary>Gets the name of the underlying resource of this texture wrap.</summary>
public string Name { get; }
/// <summary>Gets the format of the texture.</summary>
public DXGI_FORMAT Format { get; }
/// <summary>Gets the list of owner plugins.</summary>
public List<LocalPlugin> OwnerPlugins { get; }
/// <summary>Gets the raw image specification.</summary>
public RawImageSpecification RawSpecs { get; }
/// <summary>Tests whether the tag and the underlying resource are released or should be released.</summary>
/// <returns><c>true</c> if there are no more remaining references to this instance.</returns>
bool TestIsReleasedOrShouldRelease();
}
/// <summary>Gets the list containing all the loaded textures from plugins.</summary>
/// <remarks>Returned value must be used inside a lock.</remarks>
public List<IBlameableDalamudTextureWrap> BlameTracker { get; } = new();
/// <summary>Gets the blame for a texture wrap.</summary>
/// <param name="textureWrap">The texture wrap.</param>
/// <returns>The blame, if it exists.</returns>
public unsafe IBlameableDalamudTextureWrap? GetBlame(IDalamudTextureWrap textureWrap)
{
using var wrapAux = new WrapAux(textureWrap, true);
return BlameTag.Get(wrapAux.ResPtr);
}
/// <summary>Puts a plugin on blame for a texture.</summary>
/// <param name="textureWrap">The texture.</param>
/// <param name="ownerPlugin">The plugin.</param>
/// <returns>Same <paramref name="textureWrap"/>.</returns>
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;
}
/// <summary>Sets the blame name for a texture.</summary>
/// <param name="textureWrap">The texture.</param>
/// <param name="name">The name.</param>
/// <returns>Same <paramref name="textureWrap"/>.</returns>
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;
}
}
}
}
/// <summary>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.</summary>
[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<IUnknown> vtbl;
private readonly D3D11_TEXTURE2D_DESC desc;
private ID3D11Texture2D* tex2D;
private GCHandle gchThis;
private GCHandle gchComObject;
private GCHandle gchVtbl;
private int refCount;
private ComPtr<ID3D11ShaderResourceView> 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);
}
/// <inheritdoc cref="INativeGuid.NativeGuid"/>
public static Guid* NativeGuid => (Guid*)Unsafe.AsPointer(ref Unsafe.AsRef(in MyGuid));
/// <inheritdoc/>
public List<LocalPlugin> OwnerPlugins { get; } = new();
/// <inheritdoc/>
public nint ResourceAddress => (nint)this.tex2D;
/// <inheritdoc/>
public string Name { get; set; } = "<unnamed>";
/// <inheritdoc/>
public DXGI_FORMAT Format => this.desc.Format;
/// <inheritdoc/>
public RawImageSpecification RawSpecs => new(
(int)this.desc.Width,
(int)this.desc.Height,
(int)this.desc.Format,
0);
/// <inheritdoc/>
public IntPtr ImGuiHandle
{
get
{
if (this.refCount == 0)
return Service<DalamudAssetManager>.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<ID3D11Device>);
this.tex2D->GetDevice(device.GetAddressOf());
using var srv = default(ComPtr<ID3D11ShaderResourceView>);
if (device.Get()->CreateShaderResourceView((ID3D11Resource*)this.tex2D, &srvDesc, srv.GetAddressOf())
.FAILED)
return Service<DalamudAssetManager>.Get().Empty4X4.ImGuiHandle;
srv.Swap(ref this.srvDebugPreview);
return (nint)this.srvDebugPreview.Get();
}
}
/// <inheritdoc/>
public int Width => (int)this.desc.Width;
/// <inheritdoc/>
public int Height => (int)this.desc.Height;
public static implicit operator IUnknown*(BlameTag bt) => (IUnknown*)bt.gchComObject.AddrOfPinnedObject();
/// <summary>Gets or creates an instance of <see cref="BlameTag"/> for the given resource.</summary>
/// <param name="trackWhat">The COM object to track.</param>
/// <param name="isNew"><c>true</c> if the tracker is new.</param>
/// <typeparam name="T">A COM object type.</typeparam>
/// <returns>A new instance of <see cref="BlameTag"/>.</returns>
public static BlameTag GetOrCreate<T>(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);
}
/// <summary>Gets an existing instance of <see cref="BlameTag"/> for the given resource.</summary>
/// <param name="trackWhat">The COM object to track.</param>
/// <typeparam name="T">A COM object type.</typeparam>
/// <returns>An existing instance of <see cref="BlameTag"/>.</returns>
public static BlameTag? Get<T>(T* trackWhat) where T : unmanaged, IUnknown.Interface
{
using var deviceChild = default(ComPtr<ID3D11DeviceChild>);
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;
}
/// <inheritdoc/>
public bool TestIsReleasedOrShouldRelease()
{
if (this.srvDebugPreviewExpiryTick <= Environment.TickCount64)
this.srvDebugPreview.Reset();
return this.refCount == 0;
}
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
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(),
};
/// <inheritdoc/>
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();
}
}
/// <inheritdoc/>
uint IUnknown.Interface.AddRef()
{
try
{
return (uint)this.AddRef();
}
catch
{
return 0;
}
}
/// <inheritdoc/>
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;
}
}

View file

@ -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;
/// <summary>Service responsible for loading and disposing ImGui texture wraps.</summary>
internal sealed partial class TextureManager
{
private SimpleDrawerImpl? simpleDrawer;
/// <summary>A class for drawing simple stuff.</summary>
[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<ID3D11SamplerState> sampler;
private ComPtr<ID3D11VertexShader> vertexShader;
private ComPtr<ID3D11PixelShader> pixelShader;
private ComPtr<ID3D11InputLayout> inputLayout;
private ComPtr<ID3D11Buffer> vertexConstantBuffer;
private ComPtr<ID3D11BlendState> blendState;
private ComPtr<ID3D11BlendState> blendStateForStrippingAlpha;
private ComPtr<ID3D11RasterizerState> rasterizerState;
private ComPtr<ID3D11Buffer> vertexBufferFill;
private ComPtr<ID3D11Buffer> vertexBufferMutable;
private ComPtr<ID3D11Buffer> indexBuffer;
/// <summary>Finalizes an instance of the <see cref="SimpleDrawerImpl"/> class.</summary>
~SimpleDrawerImpl() => this.Dispose();
/// <inheritdoc/>
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);
}
/// <summary>Sets up this instance of <see cref="SimpleDrawerImpl"/>.</summary>
/// <param name="device">The device.</param>
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<byte>.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<byte>.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<byte>.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<byte>.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);
}
}
/// <summary>Draws the given shader resource view to the current render target.</summary>
/// <param name="ctx">An instance of <see cref="ID3D11DeviceContext"/>.</param>
/// <param name="srv">The shader resource view.</param>
/// <param name="uv0">The left top coordinates relative to the size of the source texture.</param>
/// <param name="uv1">The right bottom coordinates relative to the size of the source texture.</param>
/// <remarks>This function does not throw.</remarks>
public void Draw(
ID3D11DeviceContext* ctx,
ID3D11ShaderResourceView* srv,
Vector2 uv0,
Vector2 uv1)
{
using var rtv = default(ComPtr<ID3D11RenderTargetView>);
ctx->OMGetRenderTargets(1, rtv.GetAddressOf(), null);
if (rtv.IsEmpty())
return;
using var rtvRes = default(ComPtr<ID3D11Resource>);
rtv.Get()->GetResource(rtvRes.GetAddressOf());
using var rtvTex = default(ComPtr<ID3D11Texture2D>);
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<ImDrawVert>(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);
}
/// <summary>Fills alpha channel to 1.0 from the current render target.</summary>
/// <param name="ctx">An instance of <see cref="ID3D11DeviceContext"/>.</param>
/// <remarks>This function does not throw.</remarks>
public void StripAlpha(ID3D11DeviceContext* ctx)
{
using var rtv = default(ComPtr<ID3D11RenderTargetView>);
ctx->OMGetRenderTargets(1, rtv.GetAddressOf(), null);
if (rtv.IsEmpty())
return;
using var rtvRes = default(ComPtr<ID3D11Resource>);
rtv.Get()->GetResource(rtvRes.GetAddressOf());
using var rtvTex = default(ComPtr<ID3D11Texture2D>);
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<DalamudAssetManager>.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);
}
}
}

View file

@ -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;
/// <summary>Service responsible for loading and disposing ImGui texture wraps.</summary>
internal sealed partial class TextureManager
{
/// <inheritdoc/>
bool ITextureProvider.IsDxgiFormatSupportedForCreateFromExistingTextureAsync(int dxgiFormat) =>
this.IsDxgiFormatSupportedForCreateFromExistingTextureAsync((DXGI_FORMAT)dxgiFormat);
/// <inheritdoc cref="ITextureProvider.IsDxgiFormatSupportedForCreateFromExistingTextureAsync"/>
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;
}
/// <inheritdoc/>
public Task<IDalamudTextureWrap> CreateFromExistingTextureAsync(
IDalamudTextureWrap wrap,
TextureModificationArgs args = default,
bool leaveWrapOpen = false,
string? debugName = null,
CancellationToken cancellationToken = default) =>
this.DynamicPriorityTextureLoader.LoadAsync<IDalamudTextureWrap>(
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);
/// <inheritdoc/>
Task<IDalamudTextureWrap> ITextureProvider.CreateFromImGuiViewportAsync(
ImGuiViewportTextureArgs args,
string? debugName,
CancellationToken cancellationToken) =>
this.CreateFromImGuiViewportAsync(args, null, debugName, cancellationToken);
/// <inheritdoc cref="ITextureProvider.CreateFromImGuiViewportAsync"/>
public Task<IDalamudTextureWrap> 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;
}
/// <inheritdoc/>
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<ID3D11Texture2D> tex2D,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var desc = tex2D.GetDesc();
using var device = default(ComPtr<ID3D11Device>);
tex2D.Get()->GetDevice(device.GetAddressOf());
using var context = default(ComPtr<ID3D11DeviceContext>);
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<byte>(mapped.pData, checked((int)mapped.DepthPitch)).ToArray();
return (specs, bytes);
}
finally
{
context.Get()->Unmap(mapWhat, 0);
}
}
}
private async Task<ComPtr<ID3D11Texture2D>> 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<ID3D11Texture2D> tex2DCopyTemp)
{
using var rtvCopyTemp = default(ComPtr<ID3D11RenderTargetView>);
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);
}
}
/// <summary>Auxiliary data from <see cref="IDalamudTextureWrap"/>.</summary>
private unsafe struct WrapAux : IDisposable
{
public readonly D3D11_TEXTURE2D_DESC Desc;
private IDalamudTextureWrap? wrapToClose;
private ComPtr<ID3D11ShaderResourceView> srv;
private ComPtr<ID3D11Resource> res;
private ComPtr<ID3D11Texture2D> tex;
private ComPtr<ID3D11Device> device;
private ComPtr<ID3D11DeviceContext> context;
public WrapAux(IDalamudTextureWrap wrap, bool leaveWrapOpen)
{
this.wrapToClose = leaveWrapOpen ? null : wrap;
using var unk = new ComPtr<IUnknown>((IUnknown*)wrap.ImGuiHandle);
using var srvTemp = default(ComPtr<ID3D11ShaderResourceView>);
unk.As(&srvTemp).ThrowOnError();
using var resTemp = default(ComPtr<ID3D11Resource>);
srvTemp.Get()->GetResource(resTemp.GetAddressOf());
using var texTemp = default(ComPtr<ID3D11Texture2D>);
resTemp.As(&texTemp).ThrowOnError();
using var deviceTemp = default(ComPtr<ID3D11Device>);
texTemp.Get()->GetDevice(deviceTemp.GetAddressOf());
using var contextTemp = default(ComPtr<ID3D11DeviceContext>);
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<ID3D11ShaderResourceView> NewSrvRef() => new(this.srv);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ComPtr<ID3D11Resource> NewResRef() => new(this.res);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ComPtr<ID3D11Texture2D> NewTexRef() => new(this.tex);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ComPtr<ID3D11Device> NewDevRef() => new(this.device);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ComPtr<ID3D11DeviceContext> 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();
}
}
}

View file

@ -0,0 +1,120 @@
using System.Collections.Generic;
using System.IO;
using Dalamud.Plugin.Services;
namespace Dalamud.Interface.Textures.Internal;
/// <summary>Service responsible for loading and disposing ImGui texture wraps.</summary>
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";
/// <inheritdoc/>
public event ITextureSubstitutionProvider.TextureDataInterceptorDelegate? InterceptTexDataLoad;
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
public string GetIconPath(in GameIconLookup lookup) =>
this.TryGetIconPath(lookup, out var path) ? path : throw new FileNotFoundException();
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
public void InvalidatePaths(IEnumerable<string> 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);
}
}

View file

@ -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;
/// <summary>Service responsible for loading and disposing ImGui texture wraps.</summary>
internal sealed partial class TextureManager
{
/// <inheritdoc/>
ISharedImmediateTexture ITextureProvider.GetFromGameIcon(in GameIconLookup lookup) =>
this.Shared.GetFromGameIcon(lookup);
/// <inheritdoc/>
ISharedImmediateTexture ITextureProvider.GetFromGame(string path) =>
this.Shared.GetFromGame(path);
/// <inheritdoc/>
ISharedImmediateTexture ITextureProvider.GetFromFile(string path) =>
this.Shared.GetFromFile(path);
/// <inheritdoc/>
ISharedImmediateTexture ITextureProvider.GetFromManifestResource(Assembly assembly, string name) =>
this.Shared.GetFromManifestResource(assembly, name);
/// <summary>A part of texture manager that deals with <see cref="ISharedImmediateTexture"/>s.</summary>
internal sealed class SharedTextureManager : IDisposable
{
private const int PathLookupLruCount = 8192;
private readonly TextureManager textureManager;
private readonly ConcurrentLru<GameIconLookup, string> lookupCache = new(PathLookupLruCount);
private readonly ConcurrentDictionary<string, SharedImmediateTexture> gameDict = new();
private readonly ConcurrentDictionary<string, SharedImmediateTexture> fileDict = new();
private readonly ConcurrentDictionary<(Assembly, string), SharedImmediateTexture> manifestResourceDict = new();
private readonly HashSet<SharedImmediateTexture> invalidatedTextures = new();
/// <summary>Initializes a new instance of the <see cref="SharedTextureManager"/> class.</summary>
/// <param name="textureManager">An instance of <see cref="Interface.Textures.Internal.TextureManager"/>.</param>
public SharedTextureManager(TextureManager textureManager)
{
this.textureManager = textureManager;
this.textureManager.framework.Update += this.FrameworkOnUpdate;
}
/// <summary>Gets all the loaded textures from game resources.</summary>
public ICollection<SharedImmediateTexture> ForDebugGamePathTextures => this.gameDict.Values;
/// <summary>Gets all the loaded textures from filesystem.</summary>
public ICollection<SharedImmediateTexture> ForDebugFileSystemTextures => this.fileDict.Values;
/// <summary>Gets all the loaded textures from assembly manifest resources.</summary>
public ICollection<SharedImmediateTexture> ForDebugManifestResourceTextures => this.manifestResourceDict.Values;
/// <summary>Gets all the loaded textures that are invalidated from <see cref="InvalidatePaths"/>.</summary>
/// <remarks><c>lock</c> on use of the value returned from this property.</remarks>
[SuppressMessage(
"ReSharper",
"InconsistentlySynchronizedField",
Justification = "Debug use only; users are expected to lock around this")]
public ICollection<SharedImmediateTexture> ForDebugInvalidatedTextures => this.invalidatedTextures;
/// <inheritdoc/>
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<T>(ConcurrentDictionary<T, SharedImmediateTexture> dict)
{
foreach (var v in dict.Values)
v.ReleaseSelfReference(true);
dict.Clear();
}
}
/// <inheritdoc cref="ITextureProvider.GetFromGameIcon"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public SharedImmediateTexture.PureImpl GetFromGameIcon(in GameIconLookup lookup) =>
this.GetFromGame(this.lookupCache.GetOrAdd(lookup, this.GetIconPathByValue));
/// <inheritdoc cref="ITextureProvider.GetFromGame"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public SharedImmediateTexture.PureImpl GetFromGame(string path) =>
this.gameDict.GetOrAdd(path, GamePathSharedImmediateTexture.CreatePlaceholder)
.PublicUseInstance;
/// <inheritdoc cref="ITextureProvider.GetFromFile"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public SharedImmediateTexture.PureImpl GetFromFile(string path) =>
this.fileDict.GetOrAdd(path, FileSystemSharedImmediateTexture.CreatePlaceholder)
.PublicUseInstance;
/// <inheritdoc cref="ITextureProvider.GetFromFile"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public SharedImmediateTexture.PureImpl GetFromManifestResource(Assembly assembly, string name) =>
this.manifestResourceDict.GetOrAdd(
(assembly, name),
ManifestResourceSharedImmediateTexture.CreatePlaceholder)
.PublicUseInstance;
/// <summary>Invalidates a cached item from <see cref="GetFromGame"/> and <see cref="GetFromGameIcon"/>.
/// </summary>
/// <param name="path">The path to invalidate.</param>
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<T>(ConcurrentDictionary<T, SharedImmediateTexture> 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;
}
}
}

View file

@ -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;
/// <summary>Service responsible for loading and disposing ImGui texture wraps.</summary>
[SuppressMessage(
"StyleCop.CSharp.LayoutRules",
"SA1519:Braces should not be omitted from multi-line child statement",
Justification = "Multiple fixed blocks")]
internal sealed partial class TextureManager
{
/// <inheritdoc/>
public async Task SaveToStreamAsync(
IDalamudTextureWrap? wrap,
Guid containerGuid,
Stream? stream,
IReadOnlyDictionary<string, object>? 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();
}
}
/// <inheritdoc/>
public async Task SaveToFileAsync(
IDalamudTextureWrap? wrap,
Guid containerGuid,
string? path,
IReadOnlyDictionary<string, object>? 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<Exception>();
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();
}
}
/// <inheritdoc/>
IEnumerable<IBitmapCodecInfo> ITextureProvider.GetSupportedImageDecoderInfos() =>
this.Wic.GetSupportedDecoderInfos();
/// <inheritdoc/>
IEnumerable<IBitmapCodecInfo> ITextureReadbackProvider.GetSupportedImageEncoderInfos() =>
this.Wic.GetSupportedEncoderInfos();
/// <summary>Creates a texture from the given bytes of an image file. Skips the load throttler; intended to be used
/// from implementation of <see cref="SharedImmediateTexture"/>s.</summary>
/// <param name="bytes">The data.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The loaded texture.</returns>
internal IDalamudTextureWrap NoThrottleCreateFromImage(
ReadOnlyMemory<byte> 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);
}
}
}
/// <summary>Creates a texture from the given path to an image file. Skips the load throttler; intended to be used
/// from implementation of <see cref="SharedImmediateTexture"/>s.</summary>
/// <param name="path">The path of the file..</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The loaded texture.</returns>
internal async Task<IDalamudTextureWrap> 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);
}
}
}
/// <summary>A part of texture manager that uses Windows Imaging Component under the hood.</summary>
internal sealed class WicManager : IDisposable
{
private readonly TextureManager textureManager;
private ComPtr<IWICImagingFactory> wicFactory;
private ComPtr<IWICImagingFactory2> wicFactory2;
/// <summary>Initializes a new instance of the <see cref="WicManager"/> class.</summary>
/// <param name="textureManager">An instance of <see cref="Interface.Textures.Internal.TextureManager"/>.</param>
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();
}
}
}
}
/// <summary>
/// Finalizes an instance of the <see cref="WicManager"/> class.
/// </summary>
~WicManager() => this.ReleaseUnmanagedResource();
/// <summary>
/// Gets the corresponding <see cref="DXGI_FORMAT"/> from a <see cref="Guid"/> containing a WIC pixel format.
/// </summary>
/// <param name="fmt">The WIC pixel format.</param>
/// <returns>The corresponding <see cref="DXGI_FORMAT"/>, or <see cref="DXGI_FORMAT.DXGI_FORMAT_UNKNOWN"/> if
/// unavailable.</returns>
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,
};
/// <summary>
/// Gets the corresponding <see cref="Guid"/> containing a WIC pixel format from a <see cref="DXGI_FORMAT"/>.
/// </summary>
/// <param name="dxgiPixelFormat">The DXGI pixel format.</param>
/// <param name="wicPixelFormat">The corresponding <see cref="Guid"/>.</param>
/// <param name="srgb">Whether the image is in SRGB.</param>
/// <returns><c>true</c> if a corresponding pixel format exists.</returns>
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;
}
/// <inheritdoc/>
public void Dispose()
{
this.ReleaseUnmanagedResource();
GC.SuppressFinalize(this);
}
/// <summary>Creates a new instance of <see cref="IStream"/> from a <see cref="MemoryHandle"/>.</summary>
/// <param name="handle">An instance of <see cref="MemoryHandle"/>.</param>
/// <param name="length">The number of bytes in the memory.</param>
/// <returns>The new instance of <see cref="IStream"/>.</returns>
public unsafe ComPtr<IStream> CreateIStreamViewOfMemory(MemoryHandle handle, int length)
{
using var wicStream = default(ComPtr<IWICStream>);
this.wicFactory.Get()->CreateStream(wicStream.GetAddressOf()).ThrowOnError();
wicStream.Get()->InitializeFromMemory((byte*)handle.Pointer, checked((uint)length)).ThrowOnError();
var res = default(ComPtr<IStream>);
wicStream.As(ref res).ThrowOnError();
return res;
}
/// <summary>Creates a new instance of <see cref="IDalamudTextureWrap"/> from a <see cref="IStream"/>.</summary>
/// <param name="stream">The stream that will NOT be closed after.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The newly loaded texture.</returns>
public unsafe IDalamudTextureWrap NoThrottleCreateFromWicStream(
ComPtr<IStream> stream,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
using var decoder = default(ComPtr<IWICBitmapDecoder>);
this.wicFactory.Get()->CreateDecoderFromStream(
stream,
null,
WICDecodeOptions.WICDecodeMetadataCacheOnDemand,
decoder.GetAddressOf()).ThrowOnError();
cancellationToken.ThrowIfCancellationRequested();
using var frame = default(ComPtr<IWICBitmapFrameDecode>);
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<IWICBitmapSource>);
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<IWICBitmap>);
using var bitmapLock = default(ComPtr<IWICBitmapLock>);
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));
}
/// <summary>Gets the supported bitmap codecs.</summary>
/// <returns>The supported encoders.</returns>
public IEnumerable<BitmapCodecInfo> GetSupportedEncoderInfos()
{
foreach (var ptr in new ComponentEnumerable<IWICBitmapCodecInfo>(
this.wicFactory,
WICComponentType.WICEncoder))
yield return new(ptr);
}
/// <summary>Gets the supported bitmap codecs.</summary>
/// <returns>The supported decoders.</returns>
public IEnumerable<BitmapCodecInfo> GetSupportedDecoderInfos()
{
foreach (var ptr in new ComponentEnumerable<IWICBitmapCodecInfo>(
this.wicFactory,
WICComponentType.WICDecoder))
yield return new(ptr);
}
/// <summary>Saves the given raw bitmap to a stream.</summary>
/// <param name="specs">The raw bitmap specifications.</param>
/// <param name="bytes">The raw bitmap bytes.</param>
/// <param name="containerFormat">The container format from <see cref="GetSupportedEncoderInfos"/>.</param>
/// <param name="stream">The stream to write to. The ownership is not transferred.</param>
/// <param name="props">The encoder properties.</param>
/// <param name="cancellationToken">The cancellation token.</param>
public unsafe void SaveToStreamUsingWic(
RawImageSpecification specs,
ReadOnlySpan<byte> bytes,
Guid containerFormat,
ComPtr<IStream> stream,
IReadOnlyDictionary<string, object>? 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<IWICBitmapEncoder>);
using var encoderFrame = default(ComPtr<IWICBitmapFrameEncode>);
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<IWICPixelFormatInfo>(
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<IPropertyBag2>);
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<IWICMetadataQueryWriter>))
{
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<IWICBitmap>);
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<IWICBitmapSource>);
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<T> : IEnumerable<ComPtr<T>>
where T : unmanaged, IWICComponentInfo.Interface
{
private readonly ComPtr<IWICImagingFactory> factory;
private readonly WICComponentType componentType;
/// <summary>Initializes a new instance of the <see cref="ComponentEnumerable{T}"/> struct.</summary>
/// <param name="factory">The WIC factory. Ownership is not transferred.</param>
/// <param name="componentType">The component type to enumerate.</param>
public ComponentEnumerable(ComPtr<IWICImagingFactory> factory, WICComponentType componentType)
{
this.factory = factory;
this.componentType = componentType;
}
public unsafe ManagedIEnumUnknownEnumerator<T> GetEnumerator()
{
var enumUnknown = default(ComPtr<IEnumUnknown>);
this.factory.Get()->CreateComponentEnumerator(
(uint)this.componentType,
(uint)WICComponentEnumerateOptions.WICComponentEnumerateDefault,
enumUnknown.GetAddressOf()).ThrowOnError();
return new(enumUnknown);
}
IEnumerator<ComPtr<T>> IEnumerable<ComPtr<T>>.GetEnumerator() => this.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
}
}
}

View file

@ -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;
/// <summary>Service responsible for loading and disposing ImGui texture wraps.</summary>
[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<Dalamud>.Get();
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration dalamudConfiguration = Service<DalamudConfiguration>.Get();
[ServiceManager.ServiceDependency]
private readonly DataManager dataManager = Service<DataManager>.Get();
[ServiceManager.ServiceDependency]
private readonly Framework framework = Service<Framework>.Get();
[ServiceManager.ServiceDependency]
private readonly InterfaceManager interfaceManager = Service<InterfaceManager>.Get();
private DynamicPriorityQueueLoader? dynamicPriorityTextureLoader;
private SharedTextureManager? sharedTextureManager;
private WicManager? wicManager;
private bool disposing;
private ComPtr<ID3D11Device> 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();
}
/// <summary>Finalizes an instance of the <see cref="TextureManager"/> class.</summary>
~TextureManager() => this.ReleaseUnmanagedResources();
/// <summary>Gets the dynamic-priority queue texture loader.</summary>
public DynamicPriorityQueueLoader DynamicPriorityTextureLoader
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => this.dynamicPriorityTextureLoader ?? throw new ObjectDisposedException(nameof(TextureManager));
}
/// <summary>Gets a simpler drawer.</summary>
public SimpleDrawerImpl SimpleDrawer
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => this.simpleDrawer ?? throw new ObjectDisposedException(nameof(TextureManager));
}
/// <summary>Gets the shared texture manager.</summary>
public SharedTextureManager Shared
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => this.sharedTextureManager ?? throw new ObjectDisposedException(nameof(TextureManager));
}
/// <summary>Gets the WIC manager.</summary>
public WicManager Wic
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => this.wicManager ?? throw new ObjectDisposedException(nameof(TextureManager));
}
/// <inheritdoc/>
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);
}
/// <inheritdoc/>
public Task<IDalamudTextureWrap> CreateFromImageAsync(
ReadOnlyMemory<byte> 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);
/// <inheritdoc/>
public Task<IDalamudTextureWrap> 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);
/// <inheritdoc/>
// 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<byte> bytes,
string? debugName = null) =>
this.BlameSetName(
this.NoThrottleCreateFromRaw(specs, bytes),
debugName ?? $"{nameof(this.CreateFromRaw)}({specs}, {bytes.Length:n0})");
/// <inheritdoc/>
public Task<IDalamudTextureWrap> CreateFromRawAsync(
RawImageSpecification specs,
ReadOnlyMemory<byte> 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);
/// <inheritdoc/>
public Task<IDalamudTextureWrap> 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);
/// <inheritdoc/>
public IDalamudTextureWrap CreateFromTexFile(TexFile file) =>
this.BlameSetName(
this.CreateFromTexFileAsync(file).Result,
$"{nameof(this.CreateFromTexFile)}({nameof(file)})");
/// <inheritdoc/>
public Task<IDalamudTextureWrap> 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>(T s) => s;
}
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
bool ITextureProvider.IsDxgiFormatSupported(int dxgiFormat) =>
this.IsDxgiFormatSupported((DXGI_FORMAT)dxgiFormat);
/// <inheritdoc cref="ITextureProvider.IsDxgiFormatSupported"/>
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;
}
/// <inheritdoc cref="ITextureProvider.CreateFromRaw"/>
internal unsafe IDalamudTextureWrap NoThrottleCreateFromRaw(
RawImageSpecification specs,
ReadOnlySpan<byte> 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<ID3D11Texture2D>);
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<ID3D11ShaderResourceView>);
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;
}
/// <summary>Creates a texture from the given <see cref="TexFile"/>. Skips the load throttler; intended to be used
/// from implementation of <see cref="SharedImmediateTexture"/>s.</summary>
/// <param name="file">The data.</param>
/// <returns>The loaded texture.</returns>
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>(T s) => s;
}
/// <summary>Creates a texture from the given <paramref name="fileBytes"/>, trying to interpret it as a
/// <see cref="TexFile"/>.</summary>
/// <param name="fileBytes">The file bytes.</param>
/// <returns>The loaded texture.</returns>
internal IDalamudTextureWrap NoThrottleCreateFromTexFile(ReadOnlySpan<byte> 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();
/// <summary>Runs the given action in IDXGISwapChain.Present immediately or waiting as needed.</summary>
/// <param name="action">The action to run.</param>
// 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);
}
/// <summary>Runs the given function in IDXGISwapChain.Present immediately or waiting as needed.</summary>
/// <typeparam name="T">The type of the return value.</typeparam>
/// <param name="func">The function to run.</param>
/// <returns>The return value from the function.</returns>
private async Task<T> RunDuringPresent<T>(Func<T> func)
{
if (this.interfaceManager.IsMainThreadInPresent && ThreadSafety.IsMainThread)
return func();
return await this.interfaceManager.RunBeforeImGuiRender(func);
}
}

View file

@ -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;
/// <summary>Plugin-scoped version of <see cref="TextureManager"/>.</summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.ScopedService]
#pragma warning disable SA1015
[ResolveVia<ITextureProvider>]
[ResolveVia<ITextureSubstitutionProvider>]
[ResolveVia<ITextureReadbackProvider>]
#pragma warning restore SA1015
internal sealed class TextureManagerPluginScoped
: IInternalDisposableService,
ITextureProvider,
ITextureSubstitutionProvider,
ITextureReadbackProvider
{
private readonly LocalPlugin plugin;
private readonly bool nonAsyncFunctionAccessDuringLoadIsError;
private Task<TextureManager>? managerTaskNullable;
/// <summary>Initializes a new instance of the <see cref="TextureManagerPluginScoped"/> class.</summary>
/// <param name="plugin">The plugin.</param>
[ServiceManager.ServiceConstructor]
public TextureManagerPluginScoped(LocalPlugin plugin)
{
this.plugin = plugin;
if (plugin.Manifest is LocalPluginManifest lpm)
this.nonAsyncFunctionAccessDuringLoadIsError = lpm.LoadSync && lpm.LoadRequiredState != 0;
this.managerTaskNullable =
Service<TextureManager>
.GetAsync()
.ContinueWith(
r =>
{
if (r.IsCompletedSuccessfully)
r.Result.InterceptTexDataLoad += this.ResultOnInterceptTexDataLoad;
return r;
})
.Unwrap();
}
/// <inheritdoc/>
public event ITextureSubstitutionProvider.TextureDataInterceptorDelegate? InterceptTexDataLoad;
/// <summary>Gets the task resulting in an instance of <see cref="TextureManager"/>.</summary>
/// <exception cref="ObjectDisposedException">Thrown if disposed.</exception>
private Task<TextureManager> ManagerTask
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => this.managerTaskNullable ?? throw new ObjectDisposedException(this.ToString());
}
/// <summary>Gets an instance of <see cref="TextureManager"/>.</summary>
/// <exception cref="ObjectDisposedException">Thrown if disposed.</exception>
/// <exception cref="InvalidOperationException">Thrown if called at an unfortune time.</exception>
private TextureManager ManagerOrThrow
{
get
{
var task = this.ManagerTask;
// Check for IMWS too, as TextureManager is constructed after IMWS, and UiBuilder.RunWhenUiPrepared gets
// resolved when IMWS is constructed.
if (!task.IsCompleted && Service<InterfaceManager.InterfaceManagerWithScene>.GetNullable() is null)
{
if (this.nonAsyncFunctionAccessDuringLoadIsError && this.plugin.State != PluginState.Loaded)
{
throw new InvalidOperationException(
"The function you've called will wait for the drawing facilities to be available, and as " +
"Dalamud is already waiting for your plugin to be fully constructed before even attempting " +
"to initialize the drawing facilities, calling this function will stall the game until and " +
"is forbidden until your plugin has been fully loaded.\n" +
$"Consider using {nameof(UiBuilder.RunWhenUiPrepared)} to wait for the right moment.\n" +
"\n" +
$"Note that your plugin has {nameof(LocalPluginManifest.LoadSync)} set and " +
$"{nameof(LocalPluginManifest.LoadRequiredState)} that is nonzero.");
}
if (ThreadSafety.IsMainThread)
{
throw new InvalidOperationException(
"The function you've called will wait for the drawing facilities to be available, and as " +
"the drawing facilities are initialized from the main thread, calling this function will " +
"stall the game until and is forbidden until your plugin has been fully loaded.\n" +
$"Consider using {nameof(UiBuilder.RunWhenUiPrepared)} to wait for the right moment.");
}
}
return task.Result;
}
}
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
if (Interlocked.Exchange(ref this.managerTaskNullable, null) is not { } task)
return;
task.ContinueWith(
r =>
{
if (r.IsCompletedSuccessfully)
r.Result.InterceptTexDataLoad -= this.ResultOnInterceptTexDataLoad;
});
}
/// <inheritdoc/>
public override string ToString()
{
return this.managerTaskNullable is null
? $"{nameof(TextureManagerPluginScoped)}({this.plugin.Name}, disposed)"
: $"{nameof(TextureManagerPluginScoped)}({this.plugin.Name})";
}
/// <inheritdoc/>
public IDalamudTextureWrap CreateEmpty(
RawImageSpecification specs,
bool cpuRead,
bool cpuWrite,
string? debugName = null)
{
var manager = this.ManagerOrThrow;
var textureWrap = manager.CreateEmpty(specs, cpuRead, cpuWrite, debugName);
manager.Blame(textureWrap, this.plugin);
return textureWrap;
}
/// <inheritdoc/>
public async Task<IDalamudTextureWrap> CreateFromExistingTextureAsync(
IDalamudTextureWrap wrap,
TextureModificationArgs args = default,
bool leaveWrapOpen = false,
string? debugName = null,
CancellationToken cancellationToken = default)
{
var manager = await this.ManagerTask;
var textureWrap = await manager.CreateFromExistingTextureAsync(
wrap,
args,
leaveWrapOpen,
debugName,
cancellationToken);
manager.Blame(textureWrap, this.plugin);
return textureWrap;
}
/// <inheritdoc/>
public async Task<IDalamudTextureWrap> CreateFromImGuiViewportAsync(
ImGuiViewportTextureArgs args,
string? debugName = null,
CancellationToken cancellationToken = default)
{
var manager = await this.ManagerTask;
var textureWrap = await manager.CreateFromImGuiViewportAsync(args, this.plugin, debugName, cancellationToken);
manager.Blame(textureWrap, this.plugin);
return textureWrap;
}
/// <inheritdoc/>
public async Task<IDalamudTextureWrap> CreateFromImageAsync(
ReadOnlyMemory<byte> bytes,
string? debugName = null,
CancellationToken cancellationToken = default)
{
var manager = await this.ManagerTask;
var textureWrap = await manager.CreateFromImageAsync(bytes, debugName, cancellationToken);
manager.Blame(textureWrap, this.plugin);
return textureWrap;
}
/// <inheritdoc/>
public async Task<IDalamudTextureWrap> CreateFromImageAsync(
Stream stream,
bool leaveOpen = false,
string? debugName = null,
CancellationToken cancellationToken = default)
{
var manager = await this.ManagerTask;
var textureWrap = await manager.CreateFromImageAsync(stream, leaveOpen, debugName, cancellationToken);
manager.Blame(textureWrap, this.plugin);
return textureWrap;
}
/// <inheritdoc/>
public IDalamudTextureWrap CreateFromRaw(
RawImageSpecification specs,
ReadOnlySpan<byte> bytes,
string? debugName = null)
{
var manager = this.ManagerOrThrow;
var textureWrap = manager.CreateFromRaw(specs, bytes, debugName);
manager.Blame(textureWrap, this.plugin);
return textureWrap;
}
/// <inheritdoc/>
public async Task<IDalamudTextureWrap> CreateFromRawAsync(
RawImageSpecification specs,
ReadOnlyMemory<byte> bytes,
string? debugName = null,
CancellationToken cancellationToken = default)
{
var manager = await this.ManagerTask;
var textureWrap = await manager.CreateFromRawAsync(specs, bytes, debugName, cancellationToken);
manager.Blame(textureWrap, this.plugin);
return textureWrap;
}
/// <inheritdoc/>
public async Task<IDalamudTextureWrap> CreateFromRawAsync(
RawImageSpecification specs,
Stream stream,
bool leaveOpen = false,
string? debugName = null,
CancellationToken cancellationToken = default)
{
var manager = await this.ManagerTask;
var textureWrap = await manager.CreateFromRawAsync(specs, stream, leaveOpen, debugName, cancellationToken);
manager.Blame(textureWrap, this.plugin);
return textureWrap;
}
/// <inheritdoc/>
public IDalamudTextureWrap CreateFromTexFile(TexFile file)
{
var manager = this.ManagerOrThrow;
var textureWrap = manager.CreateFromTexFile(file);
manager.Blame(textureWrap, this.plugin);
return textureWrap;
}
/// <inheritdoc/>
public async Task<IDalamudTextureWrap> CreateFromTexFileAsync(
TexFile file,
string? debugName = null,
CancellationToken cancellationToken = default)
{
var manager = await this.ManagerTask;
var textureWrap = await manager.CreateFromTexFileAsync(file, debugName, cancellationToken);
manager.Blame(textureWrap, this.plugin);
return textureWrap;
}
/// <inheritdoc/>
public IEnumerable<IBitmapCodecInfo> GetSupportedImageDecoderInfos() =>
this.ManagerOrThrow.Wic.GetSupportedDecoderInfos();
/// <inheritdoc/>
public ISharedImmediateTexture GetFromGameIcon(in GameIconLookup lookup)
{
var shared = this.ManagerOrThrow.Shared.GetFromGameIcon(lookup);
shared.AddOwnerPlugin(this.plugin);
return shared;
}
/// <inheritdoc/>
public ISharedImmediateTexture GetFromGame(string path)
{
var shared = this.ManagerOrThrow.Shared.GetFromGame(path);
shared.AddOwnerPlugin(this.plugin);
return shared;
}
/// <inheritdoc/>
public ISharedImmediateTexture GetFromFile(string path)
{
var shared = this.ManagerOrThrow.Shared.GetFromFile(path);
shared.AddOwnerPlugin(this.plugin);
return shared;
}
/// <inheritdoc/>
public ISharedImmediateTexture GetFromManifestResource(Assembly assembly, string name)
{
var shared = this.ManagerOrThrow.Shared.GetFromManifestResource(assembly, name);
shared.AddOwnerPlugin(this.plugin);
return shared;
}
/// <inheritdoc/>
public string GetIconPath(in GameIconLookup lookup) => this.ManagerOrThrow.GetIconPath(lookup);
/// <inheritdoc/>
public bool TryGetIconPath(in GameIconLookup lookup, out string? path) =>
this.ManagerOrThrow.TryGetIconPath(lookup, out path);
/// <inheritdoc/>
public bool IsDxgiFormatSupported(int dxgiFormat) =>
this.ManagerOrThrow.IsDxgiFormatSupported((DXGI_FORMAT)dxgiFormat);
/// <inheritdoc/>
public bool IsDxgiFormatSupportedForCreateFromExistingTextureAsync(int dxgiFormat) =>
this.ManagerOrThrow.IsDxgiFormatSupportedForCreateFromExistingTextureAsync((DXGI_FORMAT)dxgiFormat);
/// <inheritdoc/>
public string GetSubstitutedPath(string originalPath) =>
this.ManagerOrThrow.GetSubstitutedPath(originalPath);
/// <inheritdoc/>
public void InvalidatePaths(IEnumerable<string> paths) =>
this.ManagerOrThrow.InvalidatePaths(paths);
/// <inheritdoc/>
public async Task<(RawImageSpecification Specification, byte[] RawData)> GetRawImageAsync(
IDalamudTextureWrap wrap,
TextureModificationArgs args = default,
bool leaveWrapOpen = false,
CancellationToken cancellationToken = default)
{
var manager = await this.ManagerTask;
return await manager.GetRawImageAsync(wrap, args, leaveWrapOpen, cancellationToken);
}
/// <inheritdoc/>
public IEnumerable<IBitmapCodecInfo> GetSupportedImageEncoderInfos() =>
this.ManagerOrThrow.Wic.GetSupportedEncoderInfos();
/// <inheritdoc/>
public async Task SaveToStreamAsync(
IDalamudTextureWrap wrap,
Guid containerGuid,
Stream stream,
IReadOnlyDictionary<string, object>? props = null,
bool leaveWrapOpen = false,
bool leaveStreamOpen = false,
CancellationToken cancellationToken = default)
{
var manager = await this.ManagerTask;
await manager.SaveToStreamAsync(
wrap,
containerGuid,
stream,
props,
leaveWrapOpen,
leaveStreamOpen,
cancellationToken);
}
/// <inheritdoc/>
public async Task SaveToFileAsync(
IDalamudTextureWrap wrap,
Guid containerGuid,
string path,
IReadOnlyDictionary<string, object>? props = null,
bool leaveWrapOpen = false,
CancellationToken cancellationToken = default)
{
var manager = await this.ManagerTask;
await manager.SaveToFileAsync(
wrap,
containerGuid,
path,
props,
leaveWrapOpen,
cancellationToken);
}
private void ResultOnInterceptTexDataLoad(string path, ref string? replacementPath) =>
this.InterceptTexDataLoad?.Invoke(path, ref replacementPath);
}

View file

@ -0,0 +1,276 @@
using TerraFX.Interop.DirectX;
namespace Dalamud.Interface.Textures;
/// <summary>Describes a raw image.</summary>
public record struct RawImageSpecification
{
private const string FormatNotSupportedMessage = $"{nameof(DxgiFormat)} is not supported.";
/// <summary>Initializes a new instance of the <see cref="RawImageSpecification"/> class.</summary>
/// <param name="width">The width of the raw image.</param>
/// <param name="height">The height of the raw image.</param>
/// <param name="dxgiFormat">The DXGI format of the raw image.</param>
/// <param name="pitch">The pitch of the raw image in bytes.
/// Specify <c>-1</c> to calculate from other parameters.</param>
public RawImageSpecification(int width, int height, int dxgiFormat, int pitch = -1)
{
if (pitch < 0)
{
if (!GetFormatInfo((DXGI_FORMAT)dxgiFormat, out var bitsPerPixel, out var isBlockCompression))
throw new NotSupportedException(FormatNotSupportedMessage);
pitch = isBlockCompression
? Math.Max(1, (width + 3) / 4) * 2 * bitsPerPixel
: ((width * bitsPerPixel) + 7) / 8;
}
this.Width = width;
this.Height = height;
this.Pitch = pitch;
this.DxgiFormat = dxgiFormat;
}
/// <summary>Initializes a new instance of the <see cref="RawImageSpecification"/> class.</summary>
/// <param name="desc">The source texture description.</param>
internal RawImageSpecification(in D3D11_TEXTURE2D_DESC desc)
: this((int)desc.Width, (int)desc.Height, (int)desc.Format)
{
}
/// <summary>Initializes a new instance of the <see cref="RawImageSpecification"/> class.</summary>
/// <param name="desc">The source texture description.</param>
/// <param name="pitch">The pitch of the raw image in bytes.</param>
internal RawImageSpecification(in D3D11_TEXTURE2D_DESC desc, uint pitch)
: this((int)desc.Width, (int)desc.Height, (int)desc.Format, checked((int)pitch))
{
}
/// <summary>Gets or sets the width of the raw image.</summary>
public int Width { get; set; }
/// <summary>Gets or sets the height of the raw image.</summary>
public int Height { get; set; }
/// <summary>Gets or sets the pitch of the raw image in bytes.</summary>
/// <remarks>The value may not always exactly match
/// <c><see cref="Width"/> * bytesPerPixelFromDxgiFormat</c>.
/// </remarks>
public int Pitch { get; set; }
/// <summary>Gets or sets the format of the raw image.</summary>
/// <remarks>See <a href="https://learn.microsoft.com/en-us/windows/win32/api/dxgiformat/ne-dxgiformat-dxgi_format">
/// DXGI_FORMAT</a>.</remarks>
public int DxgiFormat { get; set; }
/// <summary>Gets the number of bits per pixel.</summary>
/// <exception cref="NotSupportedException">Thrown if <see cref="DxgiFormat"/> is not supported.</exception>
public int BitsPerPixel =>
GetFormatInfo(this.Format, out var bitsPerPixel, out _)
? bitsPerPixel
: throw new NotSupportedException(FormatNotSupportedMessage);
/// <summary>Gets or sets the format (typed).</summary>
internal DXGI_FORMAT Format
{
get => (DXGI_FORMAT)this.DxgiFormat;
set => this.DxgiFormat = (int)value;
}
/// <summary>Gets the estimated number of bytes.</summary>
/// <remarks><c>-1</c> if failed.</remarks>
internal int EstimatedBytes =>
GetFormatInfo(this.Format, out var bitsPerPixel, out var isBlockCompression)
? isBlockCompression
? (((Math.Max(1, (this.Width + 3) / 4) * 2 * bitsPerPixel) + 63) / 64) * 64 *
Math.Max(1, (this.Height + 3) / 4)
: (((((bitsPerPixel * this.Width) + 7) / 8) + 63) / 64) * 64 * this.Height
: -1;
/// <summary>
/// Creates a new instance of <see cref="RawImageSpecification"/> record using the given resolution,
/// in B8G8R8A8(BGRA32) UNorm pixel format.
/// </summary>
/// <param name="width">The width.</param>
/// <param name="height">The height.</param>
/// <returns>The new instance.</returns>
public static RawImageSpecification Bgra32(int width, int height) =>
new(width, height, (int)DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM, width * 4);
/// <summary>
/// Creates a new instance of <see cref="RawImageSpecification"/> record using the given resolution,
/// in R8G8B8A8(RGBA32) UNorm pixel format.
/// </summary>
/// <param name="width">The width.</param>
/// <param name="height">The height.</param>
/// <returns>The new instance.</returns>
public static RawImageSpecification Rgba32(int width, int height) =>
new(width, height, (int)DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM, width * 4);
/// <summary>
/// Creates a new instance of <see cref="RawImageSpecification"/> record using the given resolution,
/// in A8 UNorm pixel format.
/// </summary>
/// <param name="width">The width.</param>
/// <param name="height">The height.</param>
/// <returns>The new instance.</returns>
public static RawImageSpecification A8(int width, int height) =>
new(width, height, (int)DXGI_FORMAT.DXGI_FORMAT_A8_UNORM, width);
/// <inheritdoc/>
public override string ToString() =>
$"{nameof(RawImageSpecification)}({this.Width}x{this.Height}, {this.Format}, {this.Pitch}b)";
private static bool GetFormatInfo(DXGI_FORMAT format, out int bitsPerPixel, out bool isBlockCompression)
{
switch (format)
{
case DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_FLOAT:
case DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_SINT:
bitsPerPixel = 128;
isBlockCompression = false;
return true;
case DXGI_FORMAT.DXGI_FORMAT_R32G32B32_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_R32G32B32_FLOAT:
case DXGI_FORMAT.DXGI_FORMAT_R32G32B32_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R32G32B32_SINT:
bitsPerPixel = 96;
isBlockCompression = false;
return true;
case DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_FLOAT:
case DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_SNORM:
case DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_SINT:
case DXGI_FORMAT.DXGI_FORMAT_R32G32_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_R32G32_FLOAT:
case DXGI_FORMAT.DXGI_FORMAT_R32G32_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R32G32_SINT:
case DXGI_FORMAT.DXGI_FORMAT_R32G8X24_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_D32_FLOAT_S8X24_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R32_FLOAT_X8X24_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_X32_TYPELESS_G8X24_UINT:
bitsPerPixel = 64;
isBlockCompression = false;
return true;
case DXGI_FORMAT.DXGI_FORMAT_R10G10B10A2_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_R10G10B10A2_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_R10G10B10A2_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R11G11B10_FLOAT:
case DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM_SRGB:
case DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_SNORM:
case DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_SINT:
case DXGI_FORMAT.DXGI_FORMAT_R16G16_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_R16G16_FLOAT:
case DXGI_FORMAT.DXGI_FORMAT_R16G16_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_R16G16_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R16G16_SNORM:
case DXGI_FORMAT.DXGI_FORMAT_R16G16_SINT:
case DXGI_FORMAT.DXGI_FORMAT_R32_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_D32_FLOAT:
case DXGI_FORMAT.DXGI_FORMAT_R32_FLOAT:
case DXGI_FORMAT.DXGI_FORMAT_R32_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R32_SINT:
case DXGI_FORMAT.DXGI_FORMAT_R24G8_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_D24_UNORM_S8_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R24_UNORM_X8_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_X24_TYPELESS_G8_UINT:
bitsPerPixel = 32;
isBlockCompression = false;
return true;
case DXGI_FORMAT.DXGI_FORMAT_R8G8_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_R8G8_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_R8G8_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R8G8_SNORM:
case DXGI_FORMAT.DXGI_FORMAT_R8G8_SINT:
case DXGI_FORMAT.DXGI_FORMAT_R16_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_R16_FLOAT:
case DXGI_FORMAT.DXGI_FORMAT_D16_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_R16_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_R16_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R16_SNORM:
case DXGI_FORMAT.DXGI_FORMAT_R16_SINT:
bitsPerPixel = 16;
isBlockCompression = false;
return true;
case DXGI_FORMAT.DXGI_FORMAT_R8_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_R8_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_R8_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R8_SNORM:
case DXGI_FORMAT.DXGI_FORMAT_R8_SINT:
case DXGI_FORMAT.DXGI_FORMAT_A8_UNORM:
bitsPerPixel = 8;
isBlockCompression = false;
return true;
case DXGI_FORMAT.DXGI_FORMAT_R1_UNORM:
bitsPerPixel = 1;
isBlockCompression = false;
return true;
case DXGI_FORMAT.DXGI_FORMAT_BC1_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM_SRGB:
bitsPerPixel = 4;
isBlockCompression = true;
return true;
case DXGI_FORMAT.DXGI_FORMAT_BC2_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_BC2_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_BC2_UNORM_SRGB:
case DXGI_FORMAT.DXGI_FORMAT_BC3_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_BC3_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_BC3_UNORM_SRGB:
bitsPerPixel = 8;
isBlockCompression = true;
return true;
case DXGI_FORMAT.DXGI_FORMAT_BC4_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_BC4_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_BC4_SNORM:
bitsPerPixel = 4;
isBlockCompression = true;
return true;
case DXGI_FORMAT.DXGI_FORMAT_BC5_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_BC5_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_BC5_SNORM:
bitsPerPixel = 8;
isBlockCompression = true;
return true;
case DXGI_FORMAT.DXGI_FORMAT_B5G6R5_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_B5G5R5A1_UNORM:
bitsPerPixel = 16;
isBlockCompression = false;
return true;
case DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM_SRGB:
case DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM_SRGB:
bitsPerPixel = 32;
isBlockCompression = false;
return true;
case DXGI_FORMAT.DXGI_FORMAT_BC6H_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_BC6H_UF16:
case DXGI_FORMAT.DXGI_FORMAT_BC6H_SF16:
case DXGI_FORMAT.DXGI_FORMAT_BC7_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_BC7_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_BC7_UNORM_SRGB:
bitsPerPixel = 8;
isBlockCompression = true;
return true;
case DXGI_FORMAT.DXGI_FORMAT_B4G4R4A4_UNORM:
bitsPerPixel = 16;
isBlockCompression = false;
return true;
default:
bitsPerPixel = 0;
isBlockCompression = false;
return false;
}
}
}

View file

@ -0,0 +1,126 @@
using System.Numerics;
using System.Text;
using Dalamud.Plugin.Services;
using TerraFX.Interop.DirectX;
namespace Dalamud.Interface.Textures;
/// <summary>Describes how to modify a texture.</summary>
public record struct TextureModificationArgs()
{
/// <summary>Gets or sets a value indicating whether to make the texture opaque.</summary>
/// <remarks>If <c>true</c>, then the alpha channel values will be filled with 1.0.</remarks>
public bool MakeOpaque { get; set; } = false;
/// <summary>Gets or sets the new DXGI format.</summary>
/// <remarks>
/// <para>Set to 0 (<see cref="DXGI_FORMAT.DXGI_FORMAT_UNKNOWN"/>) to use the source pixel format.</para>
/// <para>Supported values can be queried with
/// <see cref="ITextureProvider.IsDxgiFormatSupportedForCreateFromExistingTextureAsync"/>. This may not necessarily
/// match <see cref="ITextureProvider.IsDxgiFormatSupported"/>.
/// </para></remarks>
public int DxgiFormat { get; set; } = (int)DXGI_FORMAT.DXGI_FORMAT_UNKNOWN;
/// <summary>Gets or sets the new width.</summary>
/// <remarks>Set to 0 to automatically calculate according to the original texture size, <see cref="Uv0"/>, and
/// <see cref="Uv1"/>.</remarks>
public int NewWidth { get; set; }
/// <summary>Gets or sets the new height.</summary>
/// <remarks>Set to 0 to automatically calculate according to the original texture size, <see cref="Uv0"/>, and
/// <see cref="Uv1"/>.</remarks>
public int NewHeight { get; set; }
/// <summary>Gets or sets the left top coordinates relative to the size of the source texture.</summary>
/// <para>Coordinates should be in range between 0 and 1.</para>
public Vector2 Uv0 { get; set; } = Vector2.Zero;
/// <summary>Gets or sets the right bottom coordinates relative to the size of the source texture.</summary>
/// <para>Coordinates should be in range between 0 and 1.</para>
/// <remarks>If set to <see cref="Vector2.Zero"/>, then it will be interpreted as <see cref="Vector2.One"/>,
/// to accommodate the use of default value of this record struct.</remarks>
public Vector2 Uv1 { get; set; } = Vector2.One;
/// <summary>Gets or sets the format (typed).</summary>
internal DXGI_FORMAT Format
{
get => (DXGI_FORMAT)this.DxgiFormat;
set => this.DxgiFormat = (int)value;
}
/// <summary>Gets the effective value of <see cref="Uv1"/>.</summary>
internal Vector2 Uv1Effective => this.Uv1 == Vector2.Zero ? Vector2.One : this.Uv1;
/// <inheritdoc/>
public override string ToString()
{
var sb = new StringBuilder();
sb.Append(nameof(TextureModificationArgs)).Append('(');
if (this.MakeOpaque)
sb.Append($"{nameof(this.MakeOpaque)}, ");
if (this.Format != DXGI_FORMAT.DXGI_FORMAT_UNKNOWN)
sb.Append(Enum.GetName(this.Format) is { } name ? name[12..] : this.Format.ToString()).Append(", ");
if (this.NewWidth != 0 || this.NewHeight != 0)
{
sb.Append(this.NewWidth == 0 ? "?" : this.NewWidth.ToString())
.Append('x')
.Append(this.NewHeight == 0 ? "?" : this.NewHeight.ToString())
.Append(", ");
}
if (this.Uv0 != Vector2.Zero || this.Uv1Effective != Vector2.One)
{
sb.Append(this.Uv0.ToString())
.Append('-')
.Append(this.Uv1.ToString())
.Append(", ");
}
if (sb[^1] != '(')
sb.Remove(sb.Length - 2, 2);
return sb.Append(')').ToString();
}
/// <summary>Test if this instance of <see cref="TextureModificationArgs"/> does not instruct to change the
/// underlying data of a texture.</summary>
/// <param name="desc">The texture description to test against.</param>
/// <returns><c>true</c> if this instance of <see cref="TextureModificationArgs"/> does not instruct to
/// change the underlying data of a texture.</returns>
internal bool IsCompleteSourceCopy(in D3D11_TEXTURE2D_DESC desc) =>
this.Uv0 == Vector2.Zero
&& this.Uv1 == Vector2.One
&& (this.NewWidth == 0 || this.NewWidth == desc.Width)
&& (this.NewHeight == 0 || this.NewHeight == desc.Height)
&& !this.MakeOpaque
&& (this.Format == DXGI_FORMAT.DXGI_FORMAT_UNKNOWN || this.Format == desc.Format);
/// <summary>Checks the properties and throws an exception if values are invalid.</summary>
internal void ThrowOnInvalidValues()
{
if (this.Uv0.X is < 0 or > 1 or float.NaN)
throw new ArgumentException($"{nameof(this.Uv0)}.X is out of range.");
if (this.Uv0.Y is < 0 or > 1 or float.NaN)
throw new ArgumentException($"{nameof(this.Uv0)}.Y is out of range.");
if (this.Uv1Effective.X is < 0 or > 1 or float.NaN)
throw new ArgumentException($"{nameof(this.Uv1)}.X is out of range.");
if (this.Uv1Effective.Y is < 0 or > 1 or float.NaN)
throw new ArgumentException($"{nameof(this.Uv1)}.Y is out of range.");
if (this.Uv0.X >= this.Uv1Effective.X || this.Uv0.Y >= this.Uv1Effective.Y)
{
throw new ArgumentException(
$"{nameof(this.Uv0)} must be strictly less than {nameof(this.Uv1)} in a componentwise way.");
}
if (this.NewWidth < 0)
throw new ArgumentException($"{nameof(this.NewWidth)} cannot be a negative number.");
if (this.NewHeight < 0)
throw new ArgumentException($"{nameof(this.NewHeight)} cannot be a negative number.");
}
}

View file

@ -0,0 +1,46 @@
using Dalamud.Utility;
using ImGuiScene;
// ReSharper disable once CheckNamespace
namespace Dalamud.Interface.Internal;
/// <summary>Safety harness for ImGuiScene textures that will defer destruction until the end of the frame.</summary>
[Obsolete($"Use {nameof(IDalamudTextureWrap)}.")]
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
public class DalamudTextureWrap : IDalamudTextureWrap, IDeferredDisposable
{
private readonly TextureWrap wrappedWrap;
/// <summary>Initializes a new instance of the <see cref="DalamudTextureWrap"/> class.</summary>
/// <param name="wrappingWrap">The texture wrap to wrap.</param>
internal DalamudTextureWrap(TextureWrap wrappingWrap) => this.wrappedWrap = wrappingWrap;
/// <summary>Finalizes an instance of the <see cref="DalamudTextureWrap"/> class.</summary>
~DalamudTextureWrap() => this.Dispose(false);
/// <inheritdoc/>
public IntPtr ImGuiHandle => this.wrappedWrap.ImGuiHandle;
/// <inheritdoc/>
public int Width => this.wrappedWrap.Width;
/// <inheritdoc/>
public int Height => this.wrappedWrap.Height;
/// <summary>Queue the texture to be disposed once the frame ends.</summary>
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>Actually dispose the wrapped texture.</summary>
void IDeferredDisposable.RealDispose() => this.wrappedWrap.Dispose();
private void Dispose(bool disposing)
{
if (disposing)
Service<InterfaceManager>.GetNullable()?.EnqueueDeferredDispose(this);
}
}

View file

@ -0,0 +1,81 @@
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Runtime.CompilerServices;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Textures.TextureWraps.Internal;
using TerraFX.Interop.Windows;
namespace Dalamud.Interface.Textures.TextureWraps;
/// <summary>Base class for implementations of <see cref="IDalamudTextureWrap"/> that forwards to another.</summary>
public abstract class ForwardingTextureWrap : IDalamudTextureWrap
{
/// <inheritdoc/>
public IntPtr ImGuiHandle
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => this.GetWrap().ImGuiHandle;
}
/// <inheritdoc/>
public int Width
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => this.GetWrap().Width;
}
/// <inheritdoc/>
public int Height
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => this.GetWrap().Height;
}
/// <inheritdoc/>
public Vector2 Size
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => new(this.Width, this.Height);
}
/// <inheritdoc/>
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
/// <inheritdoc/>
public virtual unsafe IDalamudTextureWrap CreateWrapSharingLowLevelResource()
{
// Dalamud specific: IDalamudTextureWrap always points to an ID3D11ShaderResourceView.
var handle = (IUnknown*)this.ImGuiHandle;
return new UnknownTextureWrap(handle, this.Width, this.Height, true);
}
/// <inheritdoc/>
public override string ToString() => $"{this.GetType()}({(this.TryGetWrap(out var wrap) ? wrap : null)})";
/// <summary>Called on <see cref="IDisposable.Dispose"/>.</summary>
/// <param name="disposing"><c>true</c> if called from <see cref="IDisposable.Dispose"/>.</param>
/// <remarks>
/// <para>Base implementation will not dispose the result of <see cref="TryGetWrap"/>.</para>
/// <para>If you need to implement a finalizer, then make it call this function with <c>false</c>.</para>
/// </remarks>
protected virtual void Dispose(bool disposing)
{
}
/// <summary>Gets the inner wrap.</summary>
/// <param name="wrap">The inner wrap.</param>
/// <returns><c>true</c> if not disposed and <paramref name="wrap"/> is available.</returns>
protected abstract bool TryGetWrap([NotNullWhen(true)] out IDalamudTextureWrap? wrap);
/// <summary>Gets the inner wrap.</summary>
/// <returns>The inner wrap.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected IDalamudTextureWrap GetWrap() =>
this.TryGetWrap(out var wrap) ? wrap : throw new ObjectDisposedException(this.GetType().Name);
}

View file

@ -1,33 +1,34 @@
using System.Numerics;
using Dalamud.Interface.Textures;
using Dalamud.Interface.Textures.Internal;
using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Interface.Textures.TextureWraps.Internal;
using TerraFX.Interop.Windows;
// ReSharper disable once CheckNamespace
namespace Dalamud.Interface.Internal;
// TODO(api10): fix namespace maybe?
/// <summary>
/// Base TextureWrap interface for all Dalamud-owned texture wraps.
/// Used to avoid referencing ImGuiScene.
/// </summary>
/// <remarks>If you want to implement this, see if you're actually wrapping an existing instance of
/// <see cref="IDalamudTextureWrap"/>; if you are, then use <see cref="ForwardingTextureWrap"/>.</remarks>
public interface IDalamudTextureWrap : IDisposable
{
/// <summary>
/// Gets a texture handle suitable for direct use with ImGui functions.
/// </summary>
/// <summary>Gets a texture handle suitable for direct use with ImGui functions.</summary>
IntPtr ImGuiHandle { get; }
/// <summary>
/// Gets the width of the texture.
/// </summary>
/// <summary>Gets the width of the texture.</summary>
int Width { get; }
/// <summary>
/// Gets the height of the texture.
/// </summary>
/// <summary>Gets the height of the texture.</summary>
int Height { get; }
/// <summary>
/// Gets the size vector of the texture using Width, Height.
/// </summary>
/// <summary>Gets the size vector of the texture using Width, Height.</summary>
Vector2 Size => new(this.Width, this.Height);
/// <summary>

View file

@ -0,0 +1,20 @@
using Dalamud.Interface.Internal;
namespace Dalamud.Interface.Textures.TextureWraps.Internal;
/// <summary>A texture wrap that ignores <see cref="IDisposable.Dispose"/> calls.</summary>
internal class DisposeSuppressingTextureWrap : ForwardingTextureWrap
{
private readonly IDalamudTextureWrap innerWrap;
/// <summary>Initializes a new instance of the <see cref="DisposeSuppressingTextureWrap"/> class.</summary>
/// <param name="wrap">The inner wrap.</param>
public DisposeSuppressingTextureWrap(IDalamudTextureWrap wrap) => this.innerWrap = wrap;
/// <inheritdoc/>
protected override bool TryGetWrap(out IDalamudTextureWrap? wrap)
{
wrap = this.innerWrap;
return true;
}
}

View file

@ -1,21 +1,19 @@
using System.Threading;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Textures.Internal;
using Dalamud.Utility;
using TerraFX.Interop.Windows;
namespace Dalamud.Interface.Internal;
namespace Dalamud.Interface.Textures.TextureWraps.Internal;
/// <summary>
/// A texture wrap that is created by cloning the underlying <see cref="IDalamudTextureWrap.ImGuiHandle"/>.
/// </summary>
/// <summary>A texture wrap that is created from an <see cref="IUnknown"/>.</summary>
internal sealed unsafe class UnknownTextureWrap : IDalamudTextureWrap, IDeferredDisposable
{
private IntPtr imGuiHandle;
/// <summary>
/// Initializes a new instance of the <see cref="UnknownTextureWrap"/> class.
/// </summary>
/// <summary>Initializes a new instance of the <see cref="UnknownTextureWrap"/> class.</summary>
/// <param name="unknown">The pointer to <see cref="IUnknown"/> that is suitable for use with
/// <see cref="IDalamudTextureWrap.ImGuiHandle"/>.</param>
/// <param name="width">The width of the texture.</param>
@ -31,9 +29,7 @@ internal sealed unsafe class UnknownTextureWrap : IDalamudTextureWrap, IDeferred
unknown->AddRef();
}
/// <summary>
/// Finalizes an instance of the <see cref="UnknownTextureWrap"/> class.
/// </summary>
/// <summary>Finalizes an instance of the <see cref="UnknownTextureWrap"/> class.</summary>
~UnknownTextureWrap() => this.Dispose(false);
/// <inheritdoc/>
@ -48,18 +44,18 @@ internal sealed unsafe class UnknownTextureWrap : IDalamudTextureWrap, IDeferred
/// <inheritdoc/>
public int Height { get; }
/// <summary>
/// Queue the texture to be disposed once the frame ends.
/// </summary>
/// <summary>Queue the texture to be disposed once the frame ends.</summary>
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Actually dispose the wrapped texture.
/// </summary>
/// <inheritdoc/>
public override string ToString() =>
$"{nameof(UnknownTextureWrap)}({Service<TextureManager>.GetNullable()?.GetBlame(this)?.Name ?? $"{this.imGuiHandle:X}"})";
/// <summary>Actually dispose the wrapped texture.</summary>
void IDeferredDisposable.RealDispose()
{
var handle = Interlocked.Exchange(ref this.imGuiHandle, nint.Zero);

View file

@ -0,0 +1,276 @@
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Game;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Textures.Internal;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Storage.Assets;
using Dalamud.Utility;
using ImGuiNET;
using TerraFX.Interop.DirectX;
using TerraFX.Interop.Windows;
using NotSupportedException = System.NotSupportedException;
namespace Dalamud.Interface.Textures.TextureWraps.Internal;
/// <summary>A texture wrap that takes its buffer from the frame buffer (of swap chain).</summary>
internal sealed class ViewportTextureWrap : IDalamudTextureWrap, IDeferredDisposable
{
private readonly string? debugName;
private readonly LocalPlugin? ownerPlugin;
private readonly CancellationToken cancellationToken;
private readonly TaskCompletionSource<IDalamudTextureWrap> firstUpdateTaskCompletionSource = new();
private ImGuiViewportTextureArgs args;
private D3D11_TEXTURE2D_DESC desc;
private ComPtr<ID3D11Texture2D> tex;
private ComPtr<ID3D11ShaderResourceView> srv;
private ComPtr<ID3D11RenderTargetView> rtv;
private bool disposed;
/// <summary>Initializes a new instance of the <see cref="ViewportTextureWrap"/> class.</summary>
/// <param name="args">The arguments for creating a texture.</param>
/// <param name="debugName">Name for debug display purposes.</param>
/// <param name="ownerPlugin">The owner plugin.</param>
/// <param name="cancellationToken">The cancellation token.</param>
public ViewportTextureWrap(
ImGuiViewportTextureArgs args, string? debugName, LocalPlugin? ownerPlugin, CancellationToken cancellationToken)
{
this.args = args;
this.debugName = debugName;
this.ownerPlugin = ownerPlugin;
this.cancellationToken = cancellationToken;
}
/// <summary>Finalizes an instance of the <see cref="ViewportTextureWrap"/> class.</summary>
~ViewportTextureWrap() => this.Dispose(false);
/// <inheritdoc/>
public unsafe nint ImGuiHandle
{
get
{
var t = (nint)this.srv.Get();
return t == nint.Zero ? Service<DalamudAssetManager>.Get().Empty4X4.ImGuiHandle : t;
}
}
/// <inheritdoc/>
public int Width => (int)this.desc.Width;
/// <inheritdoc/>
public int Height => (int)this.desc.Height;
/// <summary>Gets the task representing the first <see cref="Update"/> call.</summary>
public Task<IDalamudTextureWrap> FirstUpdateTask => this.firstUpdateTaskCompletionSource.Task;
/// <summary>Updates the texture from the source viewport.</summary>
public unsafe void Update()
{
if (this.cancellationToken.IsCancellationRequested || this.disposed)
{
this.firstUpdateTaskCompletionSource.TrySetCanceled();
return;
}
try
{
ThreadSafety.AssertMainThread();
using var backBuffer = GetImGuiViewportBackBuffer(this.args.ViewportId);
D3D11_TEXTURE2D_DESC newDesc;
backBuffer.Get()->GetDesc(&newDesc);
if (newDesc.SampleDesc.Count > 1)
throw new NotSupportedException("Multisampling is not expected");
using var device = default(ComPtr<ID3D11Device>);
backBuffer.Get()->GetDevice(device.GetAddressOf());
using var context = default(ComPtr<ID3D11DeviceContext>);
device.Get()->GetImmediateContext(context.GetAddressOf());
var copyBox = new D3D11_BOX
{
left = (uint)MathF.Round(newDesc.Width * this.args.Uv0.X),
top = (uint)MathF.Round(newDesc.Height * this.args.Uv0.Y),
right = (uint)MathF.Round(newDesc.Width * this.args.Uv1Effective.X),
bottom = (uint)MathF.Round(newDesc.Height * this.args.Uv1Effective.Y),
front = 0,
back = 1,
};
if (this.desc.Width != copyBox.right - copyBox.left
|| this.desc.Height != copyBox.bottom - copyBox.top
|| this.desc.Format != newDesc.Format)
{
var texDesc = new D3D11_TEXTURE2D_DESC
{
Width = copyBox.right - copyBox.left,
Height = copyBox.bottom - copyBox.top,
MipLevels = 1,
ArraySize = 1,
Format = newDesc.Format,
SampleDesc = new(1, 0),
Usage = D3D11_USAGE.D3D11_USAGE_DEFAULT,
BindFlags = (uint)(D3D11_BIND_FLAG.D3D11_BIND_SHADER_RESOURCE |
D3D11_BIND_FLAG.D3D11_BIND_RENDER_TARGET),
CPUAccessFlags = 0u,
MiscFlags = 0u,
};
using var texTemp = default(ComPtr<ID3D11Texture2D>);
device.Get()->CreateTexture2D(&texDesc, null, texTemp.GetAddressOf()).ThrowOnError();
var rtvDesc = new D3D11_RENDER_TARGET_VIEW_DESC(
texTemp,
D3D11_RTV_DIMENSION.D3D11_RTV_DIMENSION_TEXTURE2D);
using var rtvTemp = default(ComPtr<ID3D11RenderTargetView>);
device.Get()->CreateRenderTargetView(
(ID3D11Resource*)texTemp.Get(),
&rtvDesc,
rtvTemp.GetAddressOf()).ThrowOnError();
var srvDesc = new D3D11_SHADER_RESOURCE_VIEW_DESC(
texTemp,
D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE2D);
using var srvTemp = default(ComPtr<ID3D11ShaderResourceView>);
device.Get()->CreateShaderResourceView(
(ID3D11Resource*)texTemp.Get(),
&srvDesc,
srvTemp.GetAddressOf())
.ThrowOnError();
this.desc = texDesc;
srvTemp.Swap(ref this.srv);
rtvTemp.Swap(ref this.rtv);
texTemp.Swap(ref this.tex);
Service<TextureManager>.Get().Blame(this, this.ownerPlugin);
Service<TextureManager>.Get().BlameSetName(
this,
this.debugName ?? $"{nameof(ViewportTextureWrap)}({this.args})");
}
// context.Get()->CopyResource((ID3D11Resource*)this.tex.Get(), (ID3D11Resource*)backBuffer.Get());
context.Get()->CopySubresourceRegion(
(ID3D11Resource*)this.tex.Get(),
0,
0,
0,
0,
(ID3D11Resource*)backBuffer.Get(),
0,
&copyBox);
if (!this.args.KeepTransparency)
{
var rtvLocal = this.rtv.Get();
context.Get()->OMSetRenderTargets(1u, &rtvLocal, null);
Service<TextureManager>.Get().SimpleDrawer.StripAlpha(context.Get());
var dummy = default(ID3D11RenderTargetView*);
context.Get()->OMSetRenderTargets(1u, &dummy, null);
}
this.firstUpdateTaskCompletionSource.TrySetResult(this);
}
catch (Exception e)
{
this.firstUpdateTaskCompletionSource.TrySetException(e);
}
if (this.args.AutoUpdate)
this.QueueUpdate();
}
/// <summary>Queues a call to <see cref="Update"/>.</summary>
public void QueueUpdate() =>
Service<Framework>.Get().RunOnTick(
() =>
{
if (this.args.TakeBeforeImGuiRender)
Service<InterfaceManager>.Get().RunBeforeImGuiRender(this.Update);
else
Service<InterfaceManager>.Get().RunAfterImGuiRender(this.Update);
},
cancellationToken: this.cancellationToken);
/// <summary>Queue the texture to be disposed once the frame ends. </summary>
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>Actually dispose the wrapped texture.</summary>
void IDeferredDisposable.RealDispose()
{
_ = this.FirstUpdateTask.Exception;
this.tex.Reset();
this.srv.Reset();
this.rtv.Reset();
}
private static unsafe ComPtr<ID3D11Texture2D> GetImGuiViewportBackBuffer(uint viewportId)
{
ThreadSafety.AssertMainThread();
var viewports = ImGui.GetPlatformIO().Viewports;
var viewportIndex = 0;
for (; viewportIndex < viewports.Size; viewportIndex++)
{
if (viewports[viewportIndex].ID == viewportId)
break;
}
if (viewportIndex >= viewports.Size)
{
throw new ArgumentOutOfRangeException(
nameof(viewportId),
viewportId,
"Could not find a viewport with the given ID.");
}
var texture = default(ComPtr<ID3D11Texture2D>);
Debug.Assert(viewports[0].ID == ImGui.GetMainViewport().ID, "ImGui has changed");
if (viewportId == viewports[0].ID)
{
var device = FFXIVClientStructs.FFXIV.Client.Graphics.Kernel.Device.Instance();
fixed (Guid* piid = &IID.IID_ID3D11Texture2D)
{
((IDXGISwapChain*)device->SwapChain->DXGISwapChain)->GetBuffer(0, piid, (void**)texture.GetAddressOf())
.ThrowOnError();
}
}
else
{
// See: ImGui_Impl_DX11.ImGuiViewportDataDx11
var rud = (nint*)viewports[viewportIndex].RendererUserData;
if (rud == null || rud[0] == nint.Zero || rud[1] == nint.Zero)
throw new InvalidOperationException();
using var resource = default(ComPtr<ID3D11Resource>);
((ID3D11RenderTargetView*)rud[1])->GetResource(resource.GetAddressOf());
resource.As(&texture).ThrowOnError();
}
return texture;
}
private void Dispose(bool disposing)
{
this.disposed = true;
this.args.AutoUpdate = false;
if (disposing)
Service<InterfaceManager>.GetNullable()?.EnqueueDeferredDispose(this);
else
((IDeferredDisposable)this).RealDispose();
}
}

View file

@ -12,12 +12,11 @@ using Dalamud.Interface.Internal;
using Dalamud.Interface.Internal.ManagedAsserts;
using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Interface.ManagedFontAtlas.Internals;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Utility;
using ImGuiNET;
using ImGuiScene;
using Serilog;
using SharpDX.Direct3D11;
@ -30,6 +29,7 @@ namespace Dalamud.Interface;
/// </summary>
public sealed class UiBuilder : IDisposable
{
private readonly LocalPlugin plugin;
private readonly Stopwatch stopwatch;
private readonly HitchDetector hitchDetector;
private readonly string namespaceName;
@ -53,14 +53,16 @@ public sealed class UiBuilder : IDisposable
/// Initializes a new instance of the <see cref="UiBuilder"/> class and registers it.
/// You do not have to call this manually.
/// </summary>
/// <param name="plugin">The plugin.</param>
/// <param name="namespaceName">The plugin namespace.</param>
internal UiBuilder(string namespaceName)
internal UiBuilder(LocalPlugin plugin, string namespaceName)
{
try
{
this.stopwatch = new Stopwatch();
this.hitchDetector = new HitchDetector($"UiBuilder({namespaceName})", this.configuration.UiBuilderHitch);
this.namespaceName = namespaceName;
this.plugin = plugin;
this.interfaceManager.Draw += this.OnDraw;
this.scopedFinalizer.Add(() => this.interfaceManager.Draw -= this.OnDraw);
@ -338,36 +340,6 @@ public sealed class UiBuilder : IDisposable
private Task<InterfaceManager> InterfaceManagerWithSceneAsync =>
Service<InterfaceManager.InterfaceManagerWithScene>.GetAsync().ContinueWith(task => task.Result.Manager);
/// <summary>
/// Loads an image from the specified file.
/// </summary>
/// <param name="filePath">The full filepath to the image.</param>
/// <returns>A <see cref="TextureWrap"/> object wrapping the created image. Use <see cref="TextureWrap.ImGuiHandle"/> inside ImGui.Image().</returns>
public IDalamudTextureWrap LoadImage(string filePath)
=> this.InterfaceManagerWithScene?.LoadImage(filePath)
?? throw new InvalidOperationException("Load failed.");
/// <summary>
/// Loads an image from a byte stream, such as a png downloaded into memory.
/// </summary>
/// <param name="imageData">A byte array containing the raw image data.</param>
/// <returns>A <see cref="TextureWrap"/> object wrapping the created image. Use <see cref="TextureWrap.ImGuiHandle"/> inside ImGui.Image().</returns>
public IDalamudTextureWrap LoadImage(byte[] imageData)
=> this.InterfaceManagerWithScene?.LoadImage(imageData)
?? throw new InvalidOperationException("Load failed.");
/// <summary>
/// Loads an image from raw unformatted pixel data, with no type or header information. To load formatted data, use <see cref="LoadImage(byte[])"/>.
/// </summary>
/// <param name="imageData">A byte array containing the raw pixel data.</param>
/// <param name="width">The width of the image contained in <paramref name="imageData"/>.</param>
/// <param name="height">The height of the image contained in <paramref name="imageData"/>.</param>
/// <param name="numChannels">The number of channels (bytes per pixel) of the image contained in <paramref name="imageData"/>. This should usually be 4.</param>
/// <returns>A <see cref="TextureWrap"/> object wrapping the created image. Use <see cref="TextureWrap.ImGuiHandle"/> inside ImGui.Image().</returns>
public IDalamudTextureWrap LoadImageRaw(byte[] imageData, int width, int height, int numChannels)
=> this.InterfaceManagerWithScene?.LoadImageRaw(imageData, width, height, numChannels)
?? throw new InvalidOperationException("Load failed.");
/// <summary>
/// Loads an ULD file that can load textures containing multiple icons in a single texture.
/// </summary>
@ -376,39 +348,6 @@ public sealed class UiBuilder : IDisposable
public UldWrapper LoadUld(string uldPath)
=> new(this, uldPath);
/// <summary>
/// Asynchronously loads an image from the specified file, when it's possible to do so.
/// </summary>
/// <param name="filePath">The full filepath to the image.</param>
/// <returns>A <see cref="TextureWrap"/> object wrapping the created image. Use <see cref="TextureWrap.ImGuiHandle"/> inside ImGui.Image().</returns>
public Task<IDalamudTextureWrap> LoadImageAsync(string filePath) => Task.Run(
async () =>
(await this.InterfaceManagerWithSceneAsync).LoadImage(filePath)
?? throw new InvalidOperationException("Load failed."));
/// <summary>
/// Asynchronously loads an image from a byte stream, such as a png downloaded into memory, when it's possible to do so.
/// </summary>
/// <param name="imageData">A byte array containing the raw image data.</param>
/// <returns>A <see cref="TextureWrap"/> object wrapping the created image. Use <see cref="TextureWrap.ImGuiHandle"/> inside ImGui.Image().</returns>
public Task<IDalamudTextureWrap> LoadImageAsync(byte[] imageData) => Task.Run(
async () =>
(await this.InterfaceManagerWithSceneAsync).LoadImage(imageData)
?? throw new InvalidOperationException("Load failed."));
/// <summary>
/// Asynchronously loads an image from raw unformatted pixel data, with no type or header information, when it's possible to do so. To load formatted data, use <see cref="LoadImage(byte[])"/>.
/// </summary>
/// <param name="imageData">A byte array containing the raw pixel data.</param>
/// <param name="width">The width of the image contained in <paramref name="imageData"/>.</param>
/// <param name="height">The height of the image contained in <paramref name="imageData"/>.</param>
/// <param name="numChannels">The number of channels (bytes per pixel) of the image contained in <paramref name="imageData"/>. This should usually be 4.</param>
/// <returns>A <see cref="TextureWrap"/> object wrapping the created image. Use <see cref="TextureWrap.ImGuiHandle"/> inside ImGui.Image().</returns>
public Task<IDalamudTextureWrap> LoadImageRawAsync(byte[] imageData, int width, int height, int numChannels) => Task.Run(
async () =>
(await this.InterfaceManagerWithSceneAsync).LoadImageRaw(imageData, width, height, numChannels)
?? throw new InvalidOperationException("Load failed."));
/// <summary>
/// Waits for UI to become available for use.
/// </summary>
@ -482,7 +421,8 @@ public sealed class UiBuilder : IDisposable
.CreateFontAtlas(
this.namespaceName + ":" + (debugName ?? "custom"),
autoRebuildMode,
isGlobalScaled));
isGlobalScaled,
this.plugin));
/// <summary>
/// Unregister the UiBuilder. Do not call this in plugin code.

View file

@ -4,6 +4,8 @@ using System.Linq;
using Dalamud.Data;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Textures;
using Dalamud.Interface.Textures.Internal;
using Dalamud.Utility;
using Lumina.Data.Files;
using Lumina.Data.Parsing.Uld;
@ -14,16 +16,17 @@ namespace Dalamud.Interface;
public class UldWrapper : IDisposable
{
private readonly DataManager data;
private readonly UiBuilder uiBuilder;
private readonly TextureManager textureManager;
private readonly Dictionary<string, (uint Id, int Width, int Height, bool HD, byte[] RgbaData)> textures = new();
/// <summary> Initializes a new instance of the <see cref="UldWrapper"/> class, wrapping an ULD file. </summary>
/// <param name="uiBuilder">The UiBuilder used to load textures.</param>
/// <param name="uldPath">The requested ULD file.</param>
internal UldWrapper(UiBuilder uiBuilder, string uldPath)
internal UldWrapper(UiBuilder? uiBuilder, string uldPath)
{
this.uiBuilder = uiBuilder;
_ = uiBuilder;
this.data = Service<DataManager>.Get();
this.textureManager = Service<TextureManager>.Get();
this.Uld = this.data.GetFile<UldFile>(uldPath);
}
@ -123,7 +126,10 @@ public class UldWrapper : IDisposable
inputSlice.CopyTo(outputSlice);
}
return this.uiBuilder.LoadImageRaw(imageData, part.W, part.H, 4);
return this.textureManager.CreateFromRaw(
RawImageSpecification.Rgba32(part.W, part.H),
imageData,
$"{nameof(UldWrapper)}({this.Uld?.FilePath.Path}: {part.TextureId})");
}
private (uint Id, int Width, int Height, bool HD, byte[] RgbaData)? GetTexture(string texturePath)

View file

@ -0,0 +1,164 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks;
using Dalamud.Interface.ImGuiFileDialog;
using Dalamud.Interface.ImGuiNotification;
using Dalamud.Interface.ImGuiNotification.Internal;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Textures.Internal;
using ImGuiNET;
using Serilog;
using TerraFX.Interop.Windows;
namespace Dalamud.Interface.Utility.Internal;
/// <summary>Utility function for saving textures.</summary>
[ServiceManager.EarlyLoadedService]
internal sealed class DevTextureSaveMenu : IInternalDisposableService
{
[ServiceManager.ServiceDependency]
private readonly InterfaceManager interfaceManager = Service<InterfaceManager>.Get();
private readonly FileDialogManager fileDialogManager;
[ServiceManager.ServiceConstructor]
private DevTextureSaveMenu()
{
this.fileDialogManager = new();
this.interfaceManager.Draw += this.InterfaceManagerOnDraw;
}
/// <inheritdoc/>
void IInternalDisposableService.DisposeService() => this.interfaceManager.Draw -= this.InterfaceManagerOnDraw;
/// <summary>Shows a context menu confirming texture save.</summary>
/// <param name="initiatorName">Name of the initiator.</param>
/// <param name="name">Suggested name of the file being saved.</param>
/// <param name="texture">A task returning the texture to save.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public async Task ShowTextureSaveMenuAsync(
string initiatorName,
string name,
Task<IDalamudTextureWrap> texture)
{
try
{
var initiatorScreenOffset = ImGui.GetMousePos();
using var textureWrap = await texture;
var textureManager = await Service<TextureManager>.GetAsync();
var popupName = $"{nameof(this.ShowTextureSaveMenuAsync)}_{textureWrap.ImGuiHandle:X}";
BitmapCodecInfo encoder;
{
var first = true;
var encoders = textureManager.Wic.GetSupportedEncoderInfos().ToList();
var tcs = new TaskCompletionSource<BitmapCodecInfo>();
Service<InterfaceManager>.Get().Draw += DrawChoices;
encoder = await tcs.Task;
[SuppressMessage("ReSharper", "AccessToDisposedClosure", Justification = "This shall not escape")]
void DrawChoices()
{
if (first)
{
ImGui.OpenPopup(popupName);
first = false;
}
ImGui.SetNextWindowPos(initiatorScreenOffset, ImGuiCond.Appearing);
if (!ImGui.BeginPopup(
popupName,
ImGuiWindowFlags.AlwaysAutoResize |
ImGuiWindowFlags.NoTitleBar |
ImGuiWindowFlags.NoSavedSettings))
{
Service<InterfaceManager>.Get().Draw -= DrawChoices;
tcs.TrySetCanceled();
return;
}
foreach (var encoder2 in encoders)
{
if (ImGui.Selectable(encoder2.Name))
tcs.TrySetResult(encoder2);
}
const float previewImageWidth = 320;
var size = textureWrap.Size;
if (size.X > previewImageWidth)
size *= previewImageWidth / size.X;
if (size.Y > previewImageWidth)
size *= previewImageWidth / size.Y;
ImGui.Image(textureWrap.ImGuiHandle, size);
if (tcs.Task.IsCompleted)
ImGui.CloseCurrentPopup();
ImGui.EndPopup();
}
}
string path;
{
var tcs = new TaskCompletionSource<string>();
this.fileDialogManager.SaveFileDialog(
"Save texture...",
$"{encoder.Name.Replace(',', '.')}{{{string.Join(',', encoder.Extensions)}}}",
name + encoder.Extensions.First(),
encoder.Extensions.First(),
(ok, path2) =>
{
if (!ok)
tcs.SetCanceled();
else
tcs.SetResult(path2);
});
path = await tcs.Task.ConfigureAwait(false);
}
var props = new Dictionary<string, object>();
if (encoder.ContainerGuid == GUID.GUID_ContainerFormatTiff)
props["CompressionQuality"] = 1.0f;
else if (encoder.ContainerGuid == GUID.GUID_ContainerFormatJpeg ||
encoder.ContainerGuid == GUID.GUID_ContainerFormatHeif ||
encoder.ContainerGuid == GUID.GUID_ContainerFormatWmp)
props["ImageQuality"] = 1.0f;
await textureManager.SaveToFileAsync(textureWrap, encoder.ContainerGuid, path, props: props);
var notif = Service<NotificationManager>.Get().AddNotification(
new()
{
Content = $"File saved to: {path}",
Title = initiatorName,
Type = NotificationType.Success,
});
notif.Click += n =>
{
Process.Start(new ProcessStartInfo(path) { UseShellExecute = true });
n.Notification.DismissNow();
};
}
catch (Exception e)
{
if (e is OperationCanceledException)
return;
Log.Error(
e,
$"{nameof(DalamudInterface)}.{nameof(this.ShowTextureSaveMenuAsync)}({initiatorName}, {name})");
Service<NotificationManager>.Get().AddNotification(
$"Failed to save file: {e}",
initiatorName,
NotificationType.Error);
}
}
private void InterfaceManagerOnDraw() => this.fileDialogManager.Draw();
}

View file

@ -37,8 +37,9 @@ internal class TaskTracker : IInternalDisposableService
/// <summary>
/// Gets a read-only list of tracked tasks.
/// Intended for use only from UI thread.
/// </summary>
public static IReadOnlyList<TaskInfo> Tasks => TrackedTasksInternal.ToArray();
public static IReadOnlyList<TaskInfo> Tasks => TrackedTasksInternal;
/// <summary>
/// Clear the list of tracked tasks.

View file

@ -54,7 +54,7 @@ public sealed class DalamudPluginInterface : IDisposable
var dataManager = Service<DataManager>.Get();
var localization = Service<Localization>.Get();
this.UiBuilder = new UiBuilder(plugin.Name);
this.UiBuilder = new(plugin, plugin.Name);
this.configs = Service<PluginManager>.Get().PluginConfigs;
this.Reason = reason;

View file

@ -1,95 +1,247 @@
using System.IO;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Internal.Windows.Data.Widgets;
using Dalamud.Interface.Textures;
using Lumina.Data.Files;
namespace Dalamud.Plugin.Services;
/// <summary>
/// Service that grants you access to textures you may render via ImGui.
/// </summary>
/// <summary>Service that grants you access to textures you may render via ImGui.</summary>
/// <remarks>
/// <para>
/// <b>Create</b> functions will return a new texture, and the returned instance of <see cref="IDalamudTextureWrap"/>
/// must be disposed after use.
/// </para>
/// <para>
/// <b>Get</b> functions will return a shared texture, and the returnd instance of <see cref="ISharedImmediateTexture"/>
/// do not require calling <see cref="IDisposable.Dispose"/>, unless a new reference has been created by calling
/// <see cref="ISharedImmediateTexture.RentAsync"/>.<br />
/// Use <see cref="ISharedImmediateTexture.TryGetWrap"/> and alike to obtain a reference of
/// <see cref="IDalamudTextureWrap"/> that will stay valid for the rest of the frame.
/// </para>
/// <para>
/// <c>debugName</c> parameter can be used to name your textures, to aid debugging resource leaks using
/// <see cref="TexWidget"/>.
/// </para>
/// </remarks>
public interface ITextureProvider
{
/// <summary>
/// Flags describing the icon you wish to receive.
/// <summary>Creates an empty texture.</summary>
/// <param name="specs">Texture specifications.</param>
/// <param name="cpuRead">Whether to support reading from CPU, while disabling reading from GPU.</param>
/// <param name="cpuWrite">Whether to support writing from CPU, while disabling writing from GPU.</param>
/// <param name="debugName">Name for debug display purposes.</param>
/// <returns>A new empty texture.</returns>
IDalamudTextureWrap CreateEmpty(
RawImageSpecification specs,
bool cpuRead,
bool cpuWrite,
string? debugName = null);
/// <summary>Creates a texture from the given existing texture, cropping and converting pixel format as needed.
/// </summary>
[Flags]
public enum IconFlags
{
/// <summary>
/// Low-resolution, standard quality icon.
/// </summary>
None = 0,
/// <summary>
/// If this icon is an item icon, and it has a high-quality variant, receive the high-quality version.
/// Null if the item does not have a high-quality variant.
/// </summary>
ItemHighQuality = 1 << 0,
/// <summary>
/// Get the hi-resolution version of the icon, if it exists.
/// </summary>
HiRes = 1 << 1,
}
/// <summary>
/// Get a texture handle for a specific icon.
/// </summary>
/// <param name="iconId">The ID of the icon to load.</param>
/// <param name="flags">Options to be considered when loading the icon.</param>
/// <param name="language">
/// 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.
/// </param>
/// <param name="keepAlive">
/// Not used. This parameter is ignored.
/// </param>
/// <returns>
/// Null, if the icon does not exist in the specified configuration, or a texture wrap that can be used
/// to render the icon.
/// </returns>
public IDalamudTextureWrap? GetIcon(uint iconId, IconFlags flags = IconFlags.HiRes, ClientLanguage? language = null, bool keepAlive = false);
/// <param name="wrap">The source texture wrap. The passed value may be disposed once this function returns,
/// without having to wait for the completion of the returned <see cref="Task{TResult}"/>.</param>
/// <param name="args">The texture modification arguments.</param>
/// <param name="leaveWrapOpen">Whether to leave <paramref name="wrap"/> non-disposed when the returned
/// <see cref="Task{TResult}"/> completes.</param>
/// <param name="debugName">Name for debug display purposes.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A <see cref="Task{TResult}"/> containing the copied texture on success. Dispose after use.</returns>
/// <remarks><para>This function may throw an exception.</para></remarks>
Task<IDalamudTextureWrap> CreateFromExistingTextureAsync(
IDalamudTextureWrap wrap,
TextureModificationArgs args = default,
bool leaveWrapOpen = false,
string? debugName = null,
CancellationToken cancellationToken = default);
/// <summary>Creates a texture from an ImGui viewport.</summary>
/// <param name="args">The arguments for creating a texture.</param>
/// <param name="debugName">Name for debug display purposes.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A <see cref="Task{TResult}"/> containing the copied texture on success. Dispose after use.</returns>
/// <remarks>
/// <para>Use <c>ImGui.GetMainViewport().ID</c> to capture the game screen with Dalamud rendered.</para>
/// <para>This function may throw an exception.</para>
/// </remarks>
Task<IDalamudTextureWrap> CreateFromImGuiViewportAsync(
ImGuiViewportTextureArgs args,
string? debugName = null,
CancellationToken cancellationToken = default);
/// <summary>Gets a texture from the given bytes, trying to interpret it as a .tex file or other well-known image
/// files, such as .png.</summary>
/// <param name="bytes">The bytes to load.</param>
/// <param name="debugName">Name for debug display purposes.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A <see cref="Task{TResult}"/> containing the loaded texture on success. Dispose after use.</returns>
/// <remarks><para>This function may throw an exception.</para></remarks>
Task<IDalamudTextureWrap> CreateFromImageAsync(
ReadOnlyMemory<byte> bytes,
string? debugName = null,
CancellationToken cancellationToken = default);
/// <summary>Gets a texture from the given stream, trying to interpret it as a .tex file or other well-known image
/// files, such as .png.</summary>
/// <param name="stream">The stream to load data from.</param>
/// <param name="leaveOpen">Whether to leave the stream open once the task completes, sucessfully or not.</param>
/// <param name="debugName">Name for debug display purposes.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A <see cref="Task{TResult}"/> containing the loaded texture on success. Dispose after use.</returns>
/// <remarks>
/// <para><paramref name="stream"/> will be closed or not only according to <paramref name="leaveOpen"/>;
/// <paramref name="cancellationToken"/> is irrelevant in closing the stream.</para>
/// <para>This function may throw an exception.</para></remarks>
Task<IDalamudTextureWrap> CreateFromImageAsync(
Stream stream,
bool leaveOpen = false,
string? debugName = null,
CancellationToken cancellationToken = default);
/// <summary>Gets a texture from the given bytes, interpreting it as a raw bitmap.</summary>
/// <param name="specs">The specifications for the raw bitmap.</param>
/// <param name="bytes">The bytes to load.</param>
/// <param name="debugName">Name for debug display purposes.</param>
/// <returns>The texture loaded from the supplied raw bitmap. Dispose after use.</returns>
/// <remarks><para>This function may throw an exception.</para></remarks>
IDalamudTextureWrap CreateFromRaw(
RawImageSpecification specs,
ReadOnlySpan<byte> bytes,
string? debugName = null);
/// <summary>Gets a texture from the given bytes, interpreting it as a raw bitmap.</summary>
/// <param name="specs">The specifications for the raw bitmap.</param>
/// <param name="bytes">The bytes to load.</param>
/// <param name="debugName">Name for debug display purposes.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A <see cref="Task{TResult}"/> containing the loaded texture on success. Dispose after use.</returns>
/// <remarks><para>This function may throw an exception.</para></remarks>
Task<IDalamudTextureWrap> CreateFromRawAsync(
RawImageSpecification specs,
ReadOnlyMemory<byte> bytes,
string? debugName = null,
CancellationToken cancellationToken = default);
/// <summary>Gets a texture from the given stream, interpreting the read data as a raw bitmap.</summary>
/// <param name="specs">The specifications for the raw bitmap.</param>
/// <param name="stream">The stream to load data from.</param>
/// <param name="leaveOpen">Whether to leave the stream open once the task completes, sucessfully or not.</param>
/// <param name="debugName">Name for debug display purposes.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A <see cref="Task{TResult}"/> containing the loaded texture on success. Dispose after use.</returns>
/// <remarks>
/// <para><paramref name="stream"/> will be closed or not only according to <paramref name="leaveOpen"/>;
/// <paramref name="cancellationToken"/> is irrelevant in closing the stream.</para>
/// <para>This function may throw an exception.</para>
/// </remarks>
Task<IDalamudTextureWrap> CreateFromRawAsync(
RawImageSpecification specs,
Stream stream,
bool leaveOpen = false,
string? debugName = null,
CancellationToken cancellationToken = default);
/// <summary>
/// Get a path for a specific icon's .tex file.
/// </summary>
/// <param name="iconId">The ID of the icon to look up.</param>
/// <param name="flags">Options to be considered when loading the icon.</param>
/// <param name="language">
/// 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.
/// </param>
/// <returns>
/// 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.
/// </returns>
public string? GetIconPath(uint iconId, IconFlags flags = IconFlags.HiRes, ClientLanguage? language = null);
/// <summary>
/// Get a texture handle for the texture at the specified path.
/// You may only specify paths in the game's VFS.
/// </summary>
/// <param name="path">The path to the texture in the game's VFS.</param>
/// <param name="keepAlive">Not used. This parameter is ignored.</param>
/// <returns>Null, if the icon does not exist, or a texture wrap that can be used to render the texture.</returns>
public IDalamudTextureWrap? GetTextureFromGame(string path, bool keepAlive = false);
/// <summary>
/// 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.
/// </summary>
/// <param name="file">The FileInfo describing the image or texture file.</param>
/// <param name="keepAlive">Not used. This parameter is ignored.</param>
/// <returns>Null, if the file does not exist, or a texture wrap that can be used to render the texture.</returns>
public IDalamudTextureWrap? GetTextureFromFile(FileInfo file, bool keepAlive = false);
/// <summary>
/// Get a texture handle for the specified Lumina TexFile.
/// Get a texture handle for the specified Lumina <see cref="TexFile"/>.
/// Alias for fetching <see cref="Task{TResult}.Result"/> from <see cref="CreateFromTexFileAsync"/>.
/// </summary>
/// <param name="file">The texture to obtain a handle to.</param>
/// <returns>A texture wrap that can be used to render the texture.</returns>
public IDalamudTextureWrap GetTexture(TexFile file);
/// <returns>A texture wrap that can be used to render the texture. Dispose after use.</returns>
/// <remarks><para>This function may throw an exception.</para></remarks>
IDalamudTextureWrap CreateFromTexFile(TexFile file);
/// <summary>Get a texture handle for the specified Lumina <see cref="TexFile"/>.</summary>
/// <param name="file">The texture to obtain a handle to.</param>
/// <param name="debugName">Name for debug display purposes.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A texture wrap that can be used to render the texture. Dispose after use.</returns>
/// <remarks><para>This function may throw an exception.</para></remarks>
Task<IDalamudTextureWrap> CreateFromTexFileAsync(
TexFile file,
string? debugName = null,
CancellationToken cancellationToken = default);
/// <summary>Gets the supported bitmap decoders.</summary>
/// <returns>The supported bitmap decoders.</returns>
/// <remarks>
/// <para>The following functions support the files of the container types pointed by yielded values.</para>
/// <ul>
/// <li><see cref="GetFromFile"/></li>
/// <li><see cref="GetFromManifestResource"/></li>
/// <li><see cref="CreateFromImageAsync(ReadOnlyMemory{byte},string?,CancellationToken)"/></li>
/// <li><see cref="CreateFromImageAsync(Stream,bool,string?,CancellationToken)"/></li>
/// </ul>
/// <para>This function may throw an exception.</para>
/// </remarks>
IEnumerable<IBitmapCodecInfo> GetSupportedImageDecoderInfos();
/// <summary>Gets a shared texture corresponding to the given game resource icon specifier.</summary>
/// <param name="lookup">A game icon specifier.</param>
/// <returns>The shared texture that you may use to obtain the loaded texture wrap and load states.</returns>
/// <remarks>
/// <para>This function is under the effect of <see cref="ITextureSubstitutionProvider.GetSubstitutedPath"/>.</para>
/// <para>This function does not throw exceptions.</para>
/// </remarks>
ISharedImmediateTexture GetFromGameIcon(in GameIconLookup lookup);
/// <summary>Gets a shared texture corresponding to the given path to a game resource.</summary>
/// <param name="path">A path to a game resource.</param>
/// <returns>The shared texture that you may use to obtain the loaded texture wrap and load states.</returns>
/// <remarks>
/// <para>This function is under the effect of <see cref="ITextureSubstitutionProvider.GetSubstitutedPath"/>.</para>
/// <para>This function does not throw exceptions.</para>
/// </remarks>
ISharedImmediateTexture GetFromGame(string path);
/// <summary>Gets a shared texture corresponding to the given file on the filesystem.</summary>
/// <param name="path">A path to a file on the filesystem.</param>
/// <returns>The shared texture that you may use to obtain the loaded texture wrap and load states.</returns>
/// <remarks><para>This function does not throw exceptions.</para></remarks>
ISharedImmediateTexture GetFromFile(string path);
/// <summary>Gets a shared texture corresponding to the given file of the assembly manifest resources.</summary>
/// <param name="assembly">The assembly containing manifest resources.</param>
/// <param name="name">The case-sensitive name of the manifest resource being requested.</param>
/// <returns>The shared texture that you may use to obtain the loaded texture wrap and load states.</returns>
/// <remarks><para>This function does not throw exceptions.</para></remarks>
ISharedImmediateTexture GetFromManifestResource(Assembly assembly, string name);
/// <summary>Get a path for a specific icon's .tex file.</summary>
/// <param name="lookup">The icon lookup.</param>
/// <returns>The path to the icon.</returns>
/// <exception cref="FileNotFoundException">If a corresponding file could not be found.</exception>
string GetIconPath(in GameIconLookup lookup);
/// <summary>
/// Gets the path of an icon.
/// </summary>
/// <param name="lookup">The icon lookup.</param>
/// <param name="path">The resolved path.</param>
/// <returns><c>true</c> if the corresponding file exists and <paramref name="path"/> has been set.</returns>
/// <remarks><para>This function does not throw exceptions.</para></remarks>
bool TryGetIconPath(in GameIconLookup lookup, [NotNullWhen(true)] out string? path);
/// <summary>
/// Determines whether the system supports the given DXGI format.
/// For use with <see cref="RawImageSpecification.DxgiFormat"/>.
/// </summary>
/// <param name="dxgiFormat">The DXGI format.</param>
/// <returns><c>true</c> if supported.</returns>
/// <remarks><para>This function does not throw exceptions.</para></remarks>
bool IsDxgiFormatSupported(int dxgiFormat);
/// <summary>Determines whether the system supports the given DXGI format for use with
/// <see cref="CreateFromExistingTextureAsync"/>.</summary>
/// <param name="dxgiFormat">The DXGI format.</param>
/// <returns><c>true</c> if supported.</returns>
/// <remarks><para>This function does not throw exceptions.</para></remarks>
bool IsDxgiFormatSupportedForCreateFromExistingTextureAsync(int dxgiFormat);
}

View file

@ -0,0 +1,105 @@
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Textures;
namespace Dalamud.Plugin.Services;
/// <summary>Service that grants you to read instances of <see cref="IDalamudTextureWrap"/>.</summary>
public interface ITextureReadbackProvider
{
/// <summary>Gets the raw data of a texture wrap.</summary>
/// <param name="wrap">The source texture wrap.</param>
/// <param name="args">The texture modification arguments.</param>
/// <param name="leaveWrapOpen">Whether to leave <paramref name="wrap"/> non-disposed when the returned
/// <see cref="Task{TResult}"/> completes.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The raw data and its specifications.</returns>
/// <remarks>
/// <para>The length of the returned <c>RawData</c> may not match
/// <see cref="RawImageSpecification.Height"/> * <see cref="RawImageSpecification.Pitch"/>.</para>
/// <para>This function may throw an exception.</para>
/// </remarks>
Task<(RawImageSpecification Specification, byte[] RawData)> GetRawImageAsync(
IDalamudTextureWrap wrap,
TextureModificationArgs args = default,
bool leaveWrapOpen = false,
CancellationToken cancellationToken = default);
/// <summary>Gets the supported bitmap encoders.</summary>
/// <returns>The supported bitmap encoders.</returns>
/// <remarks>
/// The following functions support the files of the container types pointed by yielded values.
/// <ul>
/// <li><see cref="SaveToStreamAsync"/></li>
/// <li><see cref="SaveToFileAsync"/></li>
/// </ul>
/// <para>This function may throw an exception.</para>
/// </remarks>
IEnumerable<IBitmapCodecInfo> GetSupportedImageEncoderInfos();
/// <summary>Saves a texture wrap to a stream in an image file format.</summary>
/// <param name="wrap">The texture wrap to save.</param>
/// <param name="containerGuid">The container GUID, obtained from <see cref="GetSupportedImageEncoderInfos"/>.</param>
/// <param name="stream">The stream to save to.</param>
/// <param name="props">Properties to pass to the encoder. See remarks for valid values.</param>
/// <param name="leaveWrapOpen">Whether to leave <paramref name="wrap"/> non-disposed when the returned
/// <see cref="Task{TResult}"/> completes.</param>
/// <param name="leaveStreamOpen">Whether to leave <paramref name="stream"/> open when the returned
/// <see cref="Task{TResult}"/> completes.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task representing the save process.</returns>
/// <remarks>
/// <para><paramref name="wrap"/> must not be disposed until the task finishes.</para>
/// <para>See the following webpages for the valid values for <paramref name="props"/> per
/// <paramref name="containerGuid"/>.</para>
/// <ul>
/// <li><a href="https://learn.microsoft.com/en-us/windows/win32/wic/native-wic-codecs">
/// WIC Codecs from Microsoft</a></li>
/// <li><a href="https://learn.microsoft.com/en-us/windows/win32/wic/-wic-creating-encoder#encoder-options">
/// Image Encoding Overview: Encoder options</a></li>
/// </ul>
/// <para>This function may throw an exception.</para>
/// </remarks>
Task SaveToStreamAsync(
IDalamudTextureWrap wrap,
Guid containerGuid,
Stream stream,
IReadOnlyDictionary<string, object>? props = null,
bool leaveWrapOpen = false,
bool leaveStreamOpen = false,
CancellationToken cancellationToken = default);
/// <summary>Saves a texture wrap to a file as an image file.</summary>
/// <param name="wrap">The texture wrap to save.</param>
/// <param name="containerGuid">The container GUID, obtained from <see cref="GetSupportedImageEncoderInfos"/>.</param>
/// <param name="path">The target file path. The target file will be overwritten if it exist.</param>
/// <param name="props">Properties to pass to the encoder. See remarks for valid values.</param>
/// <param name="leaveWrapOpen">Whether to leave <paramref name="wrap"/> non-disposed when the returned
/// <see cref="Task{TResult}"/> completes.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task representing the save process.</returns>
/// <remarks>
/// <para><paramref name="wrap"/> must not be disposed until the task finishes.</para>
/// <para>If the target file exists, it will be overwritten only if the save operation is successful.</para>
/// <para>See the following webpages for the valid values for <paramref name="props"/> per
/// <paramref name="containerGuid"/>.</para>
/// <ul>
/// <li><a href="https://learn.microsoft.com/en-us/windows/win32/wic/native-wic-codecs">
/// WIC Codecs from Microsoft</a></li>
/// <li><a href="https://learn.microsoft.com/en-us/windows/win32/wic/-wic-creating-encoder#encoder-options">
/// Image Encoding Overview: Encoder options</a></li>
/// </ul>
/// <para>This function may throw an exception.</para>
/// </remarks>
Task SaveToFileAsync(
IDalamudTextureWrap wrap,
Guid containerGuid,
string path,
IReadOnlyDictionary<string, object>? props = null,
bool leaveWrapOpen = false,
CancellationToken cancellationToken = default);
}

View file

@ -33,5 +33,8 @@ public interface ITextureSubstitutionProvider
/// and paths that are newly substituted.
/// </summary>
/// <param name="paths">The paths with a changed substitution status.</param>
/// <remarks>
/// This function will not invalidate the copies of the textures loaded from plugins.
/// </remarks>
public void InvalidatePaths(IEnumerable<string> paths);
}

View file

@ -7,6 +7,8 @@ using System.Threading;
using System.Threading.Tasks;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Textures.Internal;
using Dalamud.Interface.Textures.TextureWraps.Internal;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Networking.Http;
@ -58,7 +60,7 @@ internal sealed class DalamudAssetManager : IInternalDisposableService, IDalamud
this.fileStreams = Enum.GetValues<DalamudAsset>().ToDictionary(x => x, _ => (Task<FileStream>?)null);
this.textureWraps = Enum.GetValues<DalamudAsset>().ToDictionary(x => x, _ => (Task<IDalamudTextureWrap>?)null);
// Block until all the required assets to be ready.
var loadTimings = Timings.Start("DAM LoadAll");
registerStartupBlocker(
@ -78,17 +80,20 @@ internal sealed class DalamudAssetManager : IInternalDisposableService, IDalamud
"Prevent Dalamud from loading more stuff, until we've ensured that all required assets are available.");
Task.WhenAll(
Enum.GetValues<DalamudAsset>()
.Where(x => x is not DalamudAsset.Empty4X4)
.Where(x => x.GetAttribute<DalamudAssetAttribute>()?.Required is false)
.Select(this.CreateStreamAsync)
.Select(x => x.ToContentDisposedTask(true)))
Enum.GetValues<DalamudAsset>()
.Where(x => x is not DalamudAsset.Empty4X4)
.Where(x => x.GetAttribute<DalamudAssetAttribute>()?.Required is false)
.Select(this.CreateStreamAsync)
.Select(x => x.ToContentDisposedTask()))
.ContinueWith(r => Log.Verbose($"Optional assets load state: {r}"));
}
/// <inheritdoc/>
public IDalamudTextureWrap Empty4X4 => this.GetDalamudTextureWrap(DalamudAsset.Empty4X4);
/// <inheritdoc/>
public IDalamudTextureWrap White4X4 => this.GetDalamudTextureWrap(DalamudAsset.White4X4);
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
@ -310,17 +315,18 @@ internal sealed class DalamudAssetManager : IInternalDisposableService, IDalamud
var buf = Array.Empty<byte>();
try
{
var im = (await Service<InterfaceManager.InterfaceManagerWithScene>.GetAsync()).Manager;
var tm = await Service<TextureManager>.GetAsync();
await using var stream = await this.CreateStreamAsync(asset);
var length = checked((int)stream.Length);
buf = ArrayPool<byte>.Shared.Rent(length);
stream.ReadExactly(buf, 0, length);
var name = $"{nameof(DalamudAsset)}[{Enum.GetName(asset)}]";
var image = purpose switch
{
DalamudAssetPurpose.TextureFromPng => im.LoadImage(buf),
DalamudAssetPurpose.TextureFromPng => await tm.CreateFromImageAsync(buf, name),
DalamudAssetPurpose.TextureFromRaw =>
asset.GetAttribute<DalamudAssetRawTextureAttribute>() is { } raw
? im.LoadImageFromDxgiFormat(buf, raw.Pitch, raw.Width, raw.Height, raw.Format)
? await tm.CreateFromRawAsync(raw.Specification, buf, name)
: throw new InvalidOperationException(
"TextureFromRaw must accompany a DalamudAssetRawTextureAttribute."),
_ => null,
@ -328,7 +334,7 @@ internal sealed class DalamudAssetManager : IInternalDisposableService, IDalamud
var disposeDeferred =
this.scopedFinalizer.Add(image)
?? throw new InvalidOperationException("Something went wrong very badly");
return new DisposeSuppressingDalamudTextureWrap(disposeDeferred);
return new DisposeSuppressingTextureWrap(disposeDeferred);
}
catch (Exception e)
{
@ -350,26 +356,4 @@ internal sealed class DalamudAssetManager : IInternalDisposableService, IDalamud
return Task.FromException<TOut>(exc);
return task.ContinueWith(_ => this.TransformImmediate(task, transformer)).Unwrap();
}
private class DisposeSuppressingDalamudTextureWrap : IDalamudTextureWrap
{
private readonly IDalamudTextureWrap innerWrap;
public DisposeSuppressingDalamudTextureWrap(IDalamudTextureWrap wrap) => this.innerWrap = wrap;
/// <inheritdoc/>
public IntPtr ImGuiHandle => this.innerWrap.ImGuiHandle;
/// <inheritdoc/>
public int Width => this.innerWrap.Width;
/// <inheritdoc/>
public int Height => this.innerWrap.Height;
/// <inheritdoc/>
public void Dispose()
{
// suppressed
}
}
}

View file

@ -1,45 +1,23 @@
using SharpDX.DXGI;
using Dalamud.Interface.Textures;
using TerraFX.Interop.DirectX;
namespace Dalamud.Storage.Assets;
/// <summary>
/// Provide raw texture data directly.
/// </summary>
/// <summary>Provide raw texture data directly. </summary>
[AttributeUsage(AttributeTargets.Field)]
internal class DalamudAssetRawTextureAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="DalamudAssetRawTextureAttribute"/> class.
/// </summary>
/// <summary>Initializes a new instance of the <see cref="DalamudAssetRawTextureAttribute"/> class.</summary>
/// <param name="width">The width.</param>
/// <param name="pitch">The pitch.</param>
/// <param name="height">The height.</param>
/// <param name="format">The format.</param>
public DalamudAssetRawTextureAttribute(int width, int pitch, int height, Format format)
{
this.Width = width;
this.Pitch = pitch;
this.Height = height;
this.Format = format;
}
/// <param name="pitch">The pitch.</param>
public DalamudAssetRawTextureAttribute(int width, int height, DXGI_FORMAT format, int pitch) =>
this.Specification = new(width, height, (int)format, pitch);
/// <summary>
/// Gets the width.
/// Gets the specification.
/// </summary>
public int Width { get; }
/// <summary>
/// Gets the pitch.
/// </summary>
public int Pitch { get; }
/// <summary>
/// Gets the height.
/// </summary>
public int Height { get; }
/// <summary>
/// Gets the format.
/// </summary>
public Format Format { get; }
public RawImageSpecification Specification { get; }
}

View file

@ -23,6 +23,11 @@ public interface IDalamudAssetManager
/// </summary>
IDalamudTextureWrap Empty4X4 { get; }
/// <summary>
/// Gets the shared texture wrap for <see cref="DalamudAsset.White4X4"/>.
/// </summary>
IDalamudTextureWrap White4X4 { get; }
/// <summary>
/// Gets whether the stream for the asset is instantly available.
/// </summary>

View file

@ -0,0 +1,288 @@
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
namespace Dalamud.Utility;
/// <summary>Base class for loading resources in dynamic order.</summary>
internal class DynamicPriorityQueueLoader : IDisposable
{
private readonly CancellationTokenSource disposeCancellationTokenSource = new();
private readonly Task adderTask;
private readonly Task[] workerTasks;
private readonly Channel<WorkItem> newItemChannel;
private readonly Channel<object?> workTokenChannel;
private readonly List<WorkItem> workItemPending = new();
private bool disposing;
/// <summary>Initializes a new instance of the <see cref="DynamicPriorityQueueLoader"/> class.</summary>
/// <param name="concurrency">Maximum number of concurrent load tasks.</param>
public DynamicPriorityQueueLoader(int concurrency)
{
this.newItemChannel = Channel.CreateUnbounded<WorkItem>(new() { SingleReader = true });
this.workTokenChannel = Channel.CreateUnbounded<object?>(new() { SingleWriter = true });
this.adderTask = Task.Run(this.LoopAddWorkItemAsync);
this.workerTasks = new Task[concurrency];
foreach (ref var task in this.workerTasks.AsSpan())
task = Task.Run(this.LoopProcessWorkItemAsync);
}
/// <summary>Provider for priority metrics.</summary>
internal interface IThrottleBasisProvider
{
/// <summary>Gets a value indicating whether the resource is requested in an opportunistic way.</summary>
bool IsOpportunistic { get; }
/// <summary>Gets the first requested tick count from <see cref="Environment.TickCount64"/>.</summary>
long FirstRequestedTick { get; }
/// <summary>Gets the latest requested tick count from <see cref="Environment.TickCount64"/>.</summary>
long LatestRequestedTick { get; }
}
/// <inheritdoc/>
public void Dispose()
{
if (this.disposing)
return;
this.disposing = true;
this.newItemChannel.Writer.Complete();
this.workTokenChannel.Writer.Complete();
this.disposeCancellationTokenSource.Cancel();
this.adderTask.Wait();
Task.WaitAll(this.workerTasks);
_ = this.adderTask.Exception;
foreach (var t in this.workerTasks)
_ = t.Exception;
}
/// <summary>Loads a resource according to some order.</summary>
/// <typeparam name="T">The type of resource.</typeparam>
/// <param name="basis">The throttle basis. <c>null</c> may be used to create a new instance of
/// <see cref="IThrottleBasisProvider"/> that is not opportunistic with time values of now.</param>
/// <param name="immediateLoadFunction">The immediate load function.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="disposables">Disposables to dispose when the task completes.</param>
/// <returns>The task.</returns>
/// <remarks>
/// <paramref name="immediateLoadFunction"/> may throw immediately without returning anything, or the returned
/// <see cref="Task{TResult}"/> may complete in failure.
/// </remarks>
public Task<T> LoadAsync<T>(
IThrottleBasisProvider? basis,
Func<CancellationToken, Task<T>> immediateLoadFunction,
CancellationToken cancellationToken,
params IDisposable?[] disposables)
{
basis ??= new ReadOnlyThrottleBasisProvider();
var work = new WorkItem<T>(basis, immediateLoadFunction, cancellationToken, disposables);
if (this.newItemChannel.Writer.TryWrite(work))
return work.Task;
work.Dispose();
return Task.FromException<T>(new ObjectDisposedException(this.GetType().Name));
}
/// <summary>Continuously transfers work items added from <see cref="LoadAsync{T}"/> to
/// <see cref="workItemPending"/>, until all items are transferred and <see cref="Dispose"/> is called.</summary>
private async Task LoopAddWorkItemAsync()
{
const int batchAddSize = 64;
var newWorks = new List<WorkItem>(batchAddSize);
var reader = this.newItemChannel.Reader;
while (await reader.WaitToReadAsync())
{
while (newWorks.Count < batchAddSize && reader.TryRead(out var newWork))
newWorks.Add(newWork);
lock (this.workItemPending)
this.workItemPending.AddRange(newWorks);
for (var i = newWorks.Count; i > 0; i--)
this.workTokenChannel.Writer.TryWrite(null);
newWorks.Clear();
}
}
/// <summary>Continuously processes work items in <see cref="workItemPending"/>, until all items are processed and
/// <see cref="Dispose"/> is called.</summary>
private async Task LoopProcessWorkItemAsync()
{
var reader = this.workTokenChannel.Reader;
while (await reader.WaitToReadAsync())
{
if (!reader.TryRead(out _))
continue;
if (this.ExtractHighestPriorityWorkItem() is not { } work)
continue;
await work.Process(this.disposeCancellationTokenSource.Token);
work.Dispose();
}
}
/// <summary>Extracts the work item with the highest priority from <see cref="workItemPending"/>,
/// and removes cancelled items, if any.</summary>
/// <remarks>The order of items of <see cref="workItemPending"/> is undefined after this function.</remarks>
private WorkItem? ExtractHighestPriorityWorkItem()
{
lock (this.workItemPending)
{
for (var startIndex = 0; startIndex < this.workItemPending.Count - 1;)
{
var span = CollectionsMarshal.AsSpan(this.workItemPending)[startIndex..];
ref var lastRef = ref span[^1];
foreach (ref var itemRef in span[..^1])
{
if (itemRef.CancelAsRequested())
{
itemRef.Dispose();
itemRef = lastRef;
this.workItemPending.RemoveAt(this.workItemPending.Count - 1);
break;
}
if (itemRef.CompareTo(lastRef) < 0)
(itemRef, lastRef) = (lastRef, itemRef);
startIndex++;
}
}
if (this.workItemPending.Count == 0)
return null;
var last = this.workItemPending[^1];
this.workItemPending.RemoveAt(this.workItemPending.Count - 1);
if (last.CancelAsRequested())
{
last.Dispose();
return null;
}
return last;
}
}
/// <summary>A read-only implementation of <see cref="IThrottleBasisProvider"/>.</summary>
private class ReadOnlyThrottleBasisProvider : IThrottleBasisProvider
{
/// <inheritdoc/>
public bool IsOpportunistic { get; init; } = false;
/// <inheritdoc/>
public long FirstRequestedTick { get; init; } = Environment.TickCount64;
/// <inheritdoc/>
public long LatestRequestedTick { get; init; } = Environment.TickCount64;
}
/// <summary>Represents a work item added from <see cref="LoadAsync{T}"/>.</summary>
private abstract class WorkItem : IComparable<WorkItem>, IDisposable
{
private readonly IThrottleBasisProvider basis;
private readonly IDisposable?[] disposables;
protected WorkItem(
IThrottleBasisProvider basis,
CancellationToken cancellationToken,
params IDisposable?[] disposables)
{
this.basis = basis;
this.CancellationToken = cancellationToken;
this.disposables = disposables;
}
protected CancellationToken CancellationToken { get; }
public void Dispose()
{
foreach (ref var d in this.disposables.AsSpan())
Interlocked.Exchange(ref d, null)?.Dispose();
}
public int CompareTo(WorkItem other)
{
if (this.basis.IsOpportunistic != other.basis.IsOpportunistic)
return this.basis.IsOpportunistic ? 1 : -1;
if (this.basis.IsOpportunistic)
return -this.basis.LatestRequestedTick.CompareTo(other.basis.LatestRequestedTick);
return this.basis.FirstRequestedTick.CompareTo(other.basis.FirstRequestedTick);
}
public abstract bool CancelAsRequested();
public abstract ValueTask Process(CancellationToken serviceDisposeToken);
}
/// <summary>Typed version of <see cref="WorkItem"/>.</summary>
private sealed class WorkItem<T> : WorkItem
{
private readonly TaskCompletionSource<T> taskCompletionSource;
private readonly Func<CancellationToken, Task<T>> immediateLoadFunction;
public WorkItem(
IThrottleBasisProvider basis,
Func<CancellationToken, Task<T>> immediateLoadFunction,
CancellationToken cancellationToken,
params IDisposable?[] disposables)
: base(basis, cancellationToken, disposables)
{
this.taskCompletionSource = new();
this.immediateLoadFunction = immediateLoadFunction;
}
public Task<T> Task => this.taskCompletionSource.Task;
public override bool CancelAsRequested()
{
if (!this.CancellationToken.IsCancellationRequested)
return false;
// Cancel the load task and move on.
this.taskCompletionSource.TrySetCanceled(this.CancellationToken);
// Suppress the OperationCanceledException caused from the above.
_ = this.taskCompletionSource.Task.Exception;
return true;
}
public override async ValueTask Process(CancellationToken serviceDisposeToken)
{
try
{
T wrap;
if (this.CancellationToken.CanBeCanceled)
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(
serviceDisposeToken,
this.CancellationToken);
wrap = await this.immediateLoadFunction(cts.Token);
}
else
{
wrap = await this.immediateLoadFunction(serviceDisposeToken);
}
if (!this.taskCompletionSource.TrySetResult(wrap))
(wrap as IDisposable)?.Dispose();
}
catch (Exception e)
{
this.taskCompletionSource.TrySetException(e);
_ = this.taskCompletionSource.Task.Exception;
}
}
}
}

View file

@ -0,0 +1,59 @@
using System.Collections;
using System.Collections.Generic;
using TerraFX.Interop.Windows;
namespace Dalamud.Utility.TerraFxCom;
/// <summary>Managed iterator for <see cref="IEnumUnknown"/>.</summary>
/// <typeparam name="T">The unknown type.</typeparam>
internal sealed class ManagedIEnumUnknownEnumerator<T> : IEnumerator<ComPtr<T>>
where T : unmanaged, IUnknown.Interface
{
private ComPtr<IEnumUnknown> unknownEnumerator;
private ComPtr<T> current;
/// <summary>Initializes a new instance of the <see cref="ManagedIEnumUnknownEnumerator{T}"/> class.</summary>
/// <param name="unknownEnumerator">An instance of <see cref="IEnumUnknown"/>. Ownership is transferred.</param>
public ManagedIEnumUnknownEnumerator(ComPtr<IEnumUnknown> unknownEnumerator) =>
this.unknownEnumerator = unknownEnumerator;
/// <summary>Finalizes an instance of the <see cref="ManagedIEnumUnknownEnumerator{T}"/> class.</summary>
~ManagedIEnumUnknownEnumerator() => this.ReleaseUnmanagedResources();
/// <inheritdoc/>
public ComPtr<T> Current => this.current;
/// <inheritdoc/>
object IEnumerator.Current => this.current;
/// <inheritdoc/>
public void Dispose()
{
this.ReleaseUnmanagedResources();
GC.SuppressFinalize(this);
}
/// <inheritdoc/>
public unsafe bool MoveNext()
{
using var punk = default(ComPtr<IUnknown>);
var fetched = 0u;
while (this.unknownEnumerator.Get()->Next(1u, punk.ReleaseAndGetAddressOf(), &fetched) == S.S_OK && fetched == 1)
{
if (punk.As(ref this.current).SUCCEEDED)
return true;
}
return false;
}
/// <inheritdoc/>
public unsafe void Reset() => this.unknownEnumerator.Get()->Reset().ThrowOnError();
private void ReleaseUnmanagedResources()
{
this.unknownEnumerator.Reset();
this.current.Reset();
}
}

View file

@ -0,0 +1,454 @@
using System.Buffers;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using TerraFX.Interop;
using TerraFX.Interop.Windows;
namespace Dalamud.Utility.TerraFxCom;
/// <summary>An <see cref="IStream"/> wrapper for <see cref="Stream"/>.</summary>
[Guid("a620678b-56b9-4202-a1da-b821214dc972")]
internal sealed unsafe class ManagedIStream : IStream.Interface, IRefCountable
{
private static readonly Guid MyGuid = typeof(ManagedIStream).GUID;
private readonly Stream innerStream;
private readonly bool leaveOpen;
private readonly nint[] comObject;
private readonly IStream.Vtbl<IStream> vtbl;
private GCHandle gchThis;
private GCHandle gchComObject;
private GCHandle gchVtbl;
private int refCount;
private ManagedIStream(Stream innerStream, bool leaveOpen = false)
{
this.innerStream = innerStream ?? throw new NullReferenceException();
this.leaveOpen = leaveOpen;
this.comObject = new nint[2];
this.vtbl.QueryInterface = &QueryInterfaceStatic;
this.vtbl.AddRef = &AddRefStatic;
this.vtbl.Release = &ReleaseStatic;
this.vtbl.Read = &ReadStatic;
this.vtbl.Write = &WriteStatic;
this.vtbl.Seek = &SeekStatic;
this.vtbl.SetSize = &SetSizeStatic;
this.vtbl.CopyTo = &CopyToStatic;
this.vtbl.Commit = &CommitStatic;
this.vtbl.Revert = &RevertStatic;
this.vtbl.LockRegion = &LockRegionStatic;
this.vtbl.UnlockRegion = &UnlockRegionStatic;
this.vtbl.Stat = &StatStatic;
this.vtbl.Clone = &CloneStatic;
this.gchThis = GCHandle.Alloc(this);
this.gchVtbl = GCHandle.Alloc(this.vtbl, GCHandleType.Pinned);
this.gchComObject = GCHandle.Alloc(this.comObject, GCHandleType.Pinned);
this.comObject[0] = this.gchVtbl.AddrOfPinnedObject();
this.comObject[1] = GCHandle.ToIntPtr(this.gchThis);
this.refCount = 1;
return;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static ManagedIStream? ToManagedObject(void* pThis) =>
GCHandle.FromIntPtr(((nint*)pThis)[1]).Target as ManagedIStream;
[UnmanagedCallersOnly]
static int QueryInterfaceStatic(IStream* pThis, Guid* riid, void** ppvObject) =>
ToManagedObject(pThis)?.QueryInterface(riid, ppvObject) ?? E.E_UNEXPECTED;
[UnmanagedCallersOnly]
static uint AddRefStatic(IStream* pThis) => (uint)(ToManagedObject(pThis)?.AddRef() ?? 0);
[UnmanagedCallersOnly]
static uint ReleaseStatic(IStream* pThis) => (uint)(ToManagedObject(pThis)?.Release() ?? 0);
[UnmanagedCallersOnly]
static int ReadStatic(IStream* pThis, void* pv, uint cb, uint* pcbRead) =>
ToManagedObject(pThis)?.Read(pv, cb, pcbRead) ?? E.E_UNEXPECTED;
[UnmanagedCallersOnly]
static int WriteStatic(IStream* pThis, void* pv, uint cb, uint* pcbWritten) =>
ToManagedObject(pThis)?.Write(pv, cb, pcbWritten) ?? E.E_UNEXPECTED;
[UnmanagedCallersOnly]
static int SeekStatic(
IStream* pThis, LARGE_INTEGER dlibMove, uint dwOrigin, ULARGE_INTEGER* plibNewPosition) =>
ToManagedObject(pThis)?.Seek(dlibMove, dwOrigin, plibNewPosition) ?? E.E_UNEXPECTED;
[UnmanagedCallersOnly]
static int SetSizeStatic(IStream* pThis, ULARGE_INTEGER libNewSize) =>
ToManagedObject(pThis)?.SetSize(libNewSize) ?? E.E_UNEXPECTED;
[UnmanagedCallersOnly]
static int CopyToStatic(
IStream* pThis, IStream* pstm, ULARGE_INTEGER cb, ULARGE_INTEGER* pcbRead,
ULARGE_INTEGER* pcbWritten) =>
ToManagedObject(pThis)?.CopyTo(pstm, cb, pcbRead, pcbWritten) ?? E.E_UNEXPECTED;
[UnmanagedCallersOnly]
static int CommitStatic(IStream* pThis, uint grfCommitFlags) =>
ToManagedObject(pThis)?.Commit(grfCommitFlags) ?? E.E_UNEXPECTED;
[UnmanagedCallersOnly]
static int RevertStatic(IStream* pThis) => ToManagedObject(pThis)?.Revert() ?? E.E_UNEXPECTED;
[UnmanagedCallersOnly]
static int LockRegionStatic(IStream* pThis, ULARGE_INTEGER libOffset, ULARGE_INTEGER cb, uint dwLockType) =>
ToManagedObject(pThis)?.LockRegion(libOffset, cb, dwLockType) ?? E.E_UNEXPECTED;
[UnmanagedCallersOnly]
static int UnlockRegionStatic(
IStream* pThis, ULARGE_INTEGER libOffset, ULARGE_INTEGER cb, uint dwLockType) =>
ToManagedObject(pThis)?.UnlockRegion(libOffset, cb, dwLockType) ?? E.E_UNEXPECTED;
[UnmanagedCallersOnly]
static int StatStatic(IStream* pThis, STATSTG* pstatstg, uint grfStatFlag) =>
ToManagedObject(pThis)?.Stat(pstatstg, grfStatFlag) ?? E.E_UNEXPECTED;
[UnmanagedCallersOnly]
static int CloneStatic(IStream* pThis, IStream** ppstm) => ToManagedObject(pThis)?.Clone(ppstm) ?? E.E_UNEXPECTED;
}
/// <inheritdoc cref="INativeGuid.NativeGuid"/>
public static Guid* NativeGuid => (Guid*)Unsafe.AsPointer(ref Unsafe.AsRef(in MyGuid));
public static implicit operator IUnknown*(ManagedIStream mis) =>
(IUnknown*)mis.gchComObject.AddrOfPinnedObject();
public static implicit operator ISequentialStream*(ManagedIStream mis) =>
(ISequentialStream*)mis.gchComObject.AddrOfPinnedObject();
public static implicit operator IStream*(ManagedIStream mis) =>
(IStream*)mis.gchComObject.AddrOfPinnedObject();
/// <summary>Creates a new instance of <see cref="IStream"/> based on a managed <see cref="Stream"/>.</summary>
/// <param name="innerStream">The inner stream.</param>
/// <param name="leaveOpen">Whether to leave <paramref name="innerStream"/> open on final release.</param>
/// <returns>The new instance of <see cref="IStream"/> based on <paramref name="innerStream"/>.</returns>
public static ComPtr<IStream> Create(Stream innerStream, bool leaveOpen = false)
{
try
{
var res = default(ComPtr<IStream>);
res.Attach(new ManagedIStream(innerStream, leaveOpen));
return res;
}
catch
{
if (!leaveOpen)
innerStream.Dispose();
throw;
}
}
/// <inheritdoc/>
public HRESULT QueryInterface(Guid* riid, void** ppvObject)
{
if (ppvObject == null)
return E.E_POINTER;
if (*riid == IID.IID_IUnknown ||
*riid == IID.IID_ISequentialStream ||
*riid == IID.IID_IStream ||
*riid == MyGuid)
{
try
{
this.AddRef();
}
catch
{
return E.E_FAIL;
}
*ppvObject = (IUnknown*)this;
return S.S_OK;
}
*ppvObject = null;
return E.E_NOINTERFACE;
}
/// <inheritdoc/>
public int AddRef() => IRefCountable.AlterRefCount(1, ref this.refCount, out var newRefCount) switch
{
IRefCountable.RefCountResult.StillAlive => newRefCount,
IRefCountable.RefCountResult.AlreadyDisposed => throw new ObjectDisposedException(nameof(ManagedIStream)),
IRefCountable.RefCountResult.FinalRelease => throw new InvalidOperationException(),
_ => throw new InvalidOperationException(),
};
/// <inheritdoc/>
public int Release()
{
switch (IRefCountable.AlterRefCount(-1, ref this.refCount, out var newRefCount))
{
case IRefCountable.RefCountResult.StillAlive:
return newRefCount;
case IRefCountable.RefCountResult.FinalRelease:
this.gchThis.Free();
this.gchComObject.Free();
this.gchVtbl.Free();
if (!this.leaveOpen)
this.innerStream.Dispose();
return newRefCount;
case IRefCountable.RefCountResult.AlreadyDisposed:
throw new ObjectDisposedException(nameof(ManagedIStream));
default:
throw new InvalidOperationException();
}
}
/// <inheritdoc/>
uint IUnknown.Interface.AddRef()
{
try
{
return (uint)this.AddRef();
}
catch
{
return 0;
}
}
/// <inheritdoc/>
uint IUnknown.Interface.Release()
{
try
{
return (uint)this.Release();
}
catch
{
return 0;
}
}
/// <inheritdoc/>
public HRESULT Read(void* pv, uint cb, uint* pcbRead)
{
if (pcbRead == null)
{
var tmp = stackalloc uint[1];
pcbRead = tmp;
}
ref var read = ref *pcbRead;
for (read = 0u; read < cb;)
{
var chunkSize = unchecked((int)Math.Min(0x10000000u, cb));
var chunkRead = (uint)this.innerStream.Read(new(pv, chunkSize));
if (chunkRead == 0)
break;
pv = (byte*)pv + chunkRead;
read += chunkRead;
}
return read == cb ? S.S_OK : S.S_FALSE;
}
/// <inheritdoc/>
public HRESULT Write(void* pv, uint cb, uint* pcbWritten)
{
if (pcbWritten == null)
{
var tmp = stackalloc uint[1];
pcbWritten = tmp;
}
ref var written = ref *pcbWritten;
try
{
for (written = 0u; written < cb;)
{
var chunkSize = Math.Min(0x10000000u, cb);
this.innerStream.Write(new(pv, (int)chunkSize));
pv = (byte*)pv + chunkSize;
written += chunkSize;
}
return S.S_OK;
}
catch (Exception e) when (e.HResult == unchecked((int)(0x80070000u | ERROR.ERROR_HANDLE_DISK_FULL)))
{
return STG.STG_E_MEDIUMFULL;
}
catch (Exception e) when (e.HResult == unchecked((int)(0x80070000u | ERROR.ERROR_DISK_FULL)))
{
return STG.STG_E_MEDIUMFULL;
}
catch (IOException)
{
return STG.STG_E_CANTSAVE;
}
}
/// <inheritdoc/>
public HRESULT Seek(LARGE_INTEGER dlibMove, uint dwOrigin, ULARGE_INTEGER* plibNewPosition)
{
SeekOrigin seekOrigin;
switch ((STREAM_SEEK)dwOrigin)
{
case STREAM_SEEK.STREAM_SEEK_SET:
seekOrigin = SeekOrigin.Begin;
break;
case STREAM_SEEK.STREAM_SEEK_CUR:
seekOrigin = SeekOrigin.Current;
break;
case STREAM_SEEK.STREAM_SEEK_END:
seekOrigin = SeekOrigin.End;
break;
default:
return STG.STG_E_INVALIDFUNCTION;
}
try
{
var position = this.innerStream.Seek(dlibMove.QuadPart, seekOrigin);
if (plibNewPosition != null)
{
*plibNewPosition = new() { QuadPart = (ulong)position };
}
return S.S_OK;
}
catch
{
return STG.STG_E_INVALIDFUNCTION;
}
}
/// <inheritdoc/>
public HRESULT SetSize(ULARGE_INTEGER libNewSize)
{
try
{
this.innerStream.SetLength(checked((long)libNewSize.QuadPart));
return S.S_OK;
}
catch (Exception e) when (e.HResult == unchecked((int)(0x80070000u | ERROR.ERROR_HANDLE_DISK_FULL)))
{
return STG.STG_E_MEDIUMFULL;
}
catch (Exception e) when (e.HResult == unchecked((int)(0x80070000u | ERROR.ERROR_DISK_FULL)))
{
return STG.STG_E_MEDIUMFULL;
}
catch (IOException)
{
return STG.STG_E_INVALIDFUNCTION;
}
}
/// <inheritdoc/>
public HRESULT CopyTo(IStream* pstm, ULARGE_INTEGER cb, ULARGE_INTEGER* pcbRead, ULARGE_INTEGER* pcbWritten)
{
if (pcbRead == null)
{
var temp = stackalloc ULARGE_INTEGER[1];
pcbRead = temp;
}
if (pcbWritten == null)
{
var temp = stackalloc ULARGE_INTEGER[1];
pcbWritten = temp;
}
ref var cbRead = ref pcbRead->QuadPart;
ref var cbWritten = ref pcbWritten->QuadPart;
cbRead = cbWritten = 0;
var buf = ArrayPool<byte>.Shared.Rent(8192);
try
{
fixed (byte* pbuf = buf)
{
while (cbRead < cb)
{
var read = checked((uint)this.innerStream.Read(buf.AsSpan()));
if (read == 0)
break;
cbRead += read;
var written = 0u;
var writeResult = pstm->Write(pbuf, read, &written);
if (writeResult.FAILED)
return writeResult;
cbWritten += written;
}
}
return S.S_OK;
}
catch (Exception e) when (e.HResult == unchecked((int)(0x80070000u | ERROR.ERROR_HANDLE_DISK_FULL)))
{
return STG.STG_E_MEDIUMFULL;
}
catch (Exception e) when (e.HResult == unchecked((int)(0x80070000u | ERROR.ERROR_DISK_FULL)))
{
return STG.STG_E_MEDIUMFULL;
}
catch (Exception e)
{
// Undefined return value according to the documentation, but meh
return e.HResult < 0 ? e.HResult : E.E_FAIL;
}
finally
{
ArrayPool<byte>.Shared.Return(buf);
}
}
/// <inheritdoc/>
// On streams open in direct mode, this method has no effect.
public HRESULT Commit(uint grfCommitFlags) => S.S_OK;
/// <inheritdoc/>
// On streams open in direct mode, this method has no effect.
public HRESULT Revert() => S.S_OK;
/// <inheritdoc/>
// Locking is not supported at all or the specific type of lock requested is not supported.
public HRESULT LockRegion(ULARGE_INTEGER libOffset, ULARGE_INTEGER cb, uint dwLockType) =>
STG.STG_E_INVALIDFUNCTION;
/// <inheritdoc/>
// Locking is not supported at all or the specific type of lock requested is not supported.
public HRESULT UnlockRegion(ULARGE_INTEGER libOffset, ULARGE_INTEGER cb, uint dwLockType) =>
STG.STG_E_INVALIDFUNCTION;
/// <inheritdoc/>
public HRESULT Stat(STATSTG* pstatstg, uint grfStatFlag)
{
if (pstatstg is null)
return STG.STG_E_INVALIDPOINTER;
ref var streamStats = ref *pstatstg;
streamStats.type = (uint)STGTY.STGTY_STREAM;
streamStats.cbSize = (ulong)this.innerStream.Length;
streamStats.grfMode = 0;
if (this.innerStream.CanRead && this.innerStream.CanWrite)
streamStats.grfMode |= STGM.STGM_READWRITE;
else if (this.innerStream.CanRead)
streamStats.grfMode |= STGM.STGM_READ;
else if (this.innerStream.CanWrite)
streamStats.grfMode |= STGM.STGM_WRITE;
else
return STG.STG_E_REVERTED;
return S.S_OK;
}
/// <inheritdoc/>
// Undefined return value according to the documentation, but meh
public HRESULT Clone(IStream** ppstm) => E.E_NOTIMPL;
}

View file

@ -0,0 +1,175 @@
using System.IO;
using System.Runtime.InteropServices;
using TerraFX.Interop.Windows;
using static TerraFX.Interop.Windows.Windows;
namespace Dalamud.Utility.TerraFxCom;
/// <summary>Utilities for <see cref="IUnknown"/> and its derivatives.</summary>
internal static unsafe partial class TerraFxComInterfaceExtensions
{
/// <summary>Creates a new instance of <see cref="IStream"/> from a file path.</summary>
/// <param name="path">The file path.</param>
/// <param name="mode">The file open mode.</param>
/// <param name="access">The file access mode.</param>
/// <param name="share">The file share mode.</param>
/// <param name="attributes">The file attributes.</param>
/// <returns>The new instance of <see cref="IStream"/>.</returns>
public static ComPtr<IStream> CreateIStreamFromFile(
string path,
FileMode mode,
FileAccess access,
FileShare share,
FileAttributes attributes = FileAttributes.Normal)
{
var grfMode = 0u;
bool fCreate;
switch (mode)
{
case FileMode.CreateNew:
fCreate = true;
grfMode |= STGM.STGM_FAILIFTHERE;
break;
case FileMode.Create:
fCreate = true;
grfMode |= STGM.STGM_CREATE;
break;
case FileMode.Open:
fCreate = false;
grfMode |= STGM.STGM_FAILIFTHERE; // yes
break;
case FileMode.OpenOrCreate:
throw new NotSupportedException(
$"${FileMode.OpenOrCreate} is not supported. It might be, but it needs testing.");
case FileMode.Append:
throw new NotSupportedException($"${FileMode.Append} is not supported.");
case FileMode.Truncate:
throw new NotSupportedException($"${FileMode.Truncate} is not supported.");
default:
throw new ArgumentOutOfRangeException(nameof(mode), mode, null);
}
switch (access)
{
case FileAccess.Read:
grfMode |= STGM.STGM_READ;
break;
case FileAccess.Write:
grfMode |= STGM.STGM_WRITE;
break;
case FileAccess.ReadWrite:
grfMode |= STGM.STGM_READWRITE;
break;
default:
throw new ArgumentOutOfRangeException(nameof(access), access, null);
}
switch (share)
{
case FileShare.None:
grfMode |= STGM.STGM_SHARE_EXCLUSIVE;
break;
case FileShare.Read:
grfMode |= STGM.STGM_SHARE_DENY_WRITE;
break;
case FileShare.Write:
grfMode |= STGM.STGM_SHARE_DENY_READ;
break;
case FileShare.ReadWrite:
grfMode |= STGM.STGM_SHARE_DENY_NONE;
break;
default:
throw new NotSupportedException($"Only ${FileShare.Read} and ${FileShare.Write} are supported.");
}
using var stream = default(ComPtr<IStream>);
fixed (char* pPath = path)
{
SHCreateStreamOnFileEx(
(ushort*)pPath,
grfMode,
(uint)attributes,
fCreate,
null,
stream.GetAddressOf()).ThrowOnError();
}
var res = default(ComPtr<IStream>);
stream.As(ref res).ThrowOnError();
return res;
}
/// <summary>Calls <see cref="IPropertyBag2.Write"/>.</summary>
/// <param name="obj">The property bag.</param>
/// <param name="name">The name of the item to be interpreted as a VARIANT.</param>
/// <param name="value">The new value, to be interpreted as a <see cref="VARIANT"/>.</param>
/// <returns>Return value from <inheritdoc cref="IPropertyBag2.Write"/>.</returns>
public static HRESULT Write(ref this IPropertyBag2 obj, string name, object? value)
{
VARIANT varValue;
// Marshal calls VariantInit.
Marshal.GetNativeVariantForObject(value, (nint)(&varValue));
try
{
fixed (char* pName = name)
{
var option = new PROPBAG2 { pstrName = (ushort*)pName };
return obj.Write(1, &option, &varValue);
}
}
finally
{
VariantClear(&varValue);
}
}
/// <summary>Calls <inheritdoc cref="IWICMetadataQueryWriter.SetMetadataByName"/>.</summary>
/// <param name="obj">The object.</param>
/// <param name="name">The name of the metadata.</param>
/// <param name="value">The new value, to be interpreted as a <see cref="PROPVARIANT"/>.</param>
/// <returns>Return value from <inheritdoc cref="IWICMetadataQueryWriter.SetMetadataByName"/>.</returns>
public static HRESULT SetMetadataByName(ref this IWICMetadataQueryWriter obj, string name, object? value)
{
VARIANT varValue;
// Marshal calls VariantInit.
Marshal.GetNativeVariantForObject(value, (nint)(&varValue));
try
{
PROPVARIANT propVarValue;
var propVarRes = VariantToPropVariant(&varValue, &propVarValue);
if (propVarRes < 0)
return propVarRes;
try
{
fixed (char* pName = name)
return obj.SetMetadataByName((ushort*)pName, &propVarValue);
}
finally
{
_ = PropVariantClear(&propVarValue);
}
}
finally
{
_ = VariantClear(&varValue);
}
}
/// <summary>Calls <inheritdoc cref="IWICMetadataQueryWriter.SetMetadataByName"/>.</summary>
/// <param name="obj">The object.</param>
/// <param name="name">The name of the metadata.</param>
/// <returns>Return value from <inheritdoc cref="IWICMetadataQueryWriter.SetMetadataByName"/>.</returns>
public static HRESULT RemoveMetadataByName(ref this IWICMetadataQueryWriter obj, string name)
{
fixed (char* pName = name)
return obj.RemoveMetadataByName((ushort*)pName);
}
[LibraryImport("propsys.dll")]
private static partial int VariantToPropVariant(
void* pVarIn,
void* pPropVarOut);
}

View file

@ -0,0 +1,65 @@
using TerraFX.Interop.DirectX;
using TerraFX.Interop.Windows;
namespace Dalamud.Utility.TerraFxCom;
/// <summary>Extension methods for D3D11 TerraFX objects.</summary>
internal static class TerraFxD3D11Extensions
{
/// <summary>Creates a 2D texture with the given descriptor.</summary>
/// <param name="device">Device to copy from and to.</param>
/// <param name="desc">Resource descriptor.</param>
/// <param name="copyFrom">Optional initial data for the texture.</param>
/// <returns>New copied texture.</returns>
public static unsafe ComPtr<ID3D11Texture2D> CreateTexture2D(
this ComPtr<ID3D11Device> device,
D3D11_TEXTURE2D_DESC desc,
ComPtr<ID3D11Texture2D> copyFrom = default)
{
using var tmpTex = default(ComPtr<ID3D11Texture2D>);
device.Get()->CreateTexture2D(&desc, null, tmpTex.GetAddressOf()).ThrowOnError();
if (!copyFrom.IsEmpty())
{
using var context = default(ComPtr<ID3D11DeviceContext>);
device.Get()->GetImmediateContext(context.GetAddressOf());
context.Get()->CopyResource((ID3D11Resource*)tmpTex.Get(), (ID3D11Resource*)copyFrom.Get());
}
return new(tmpTex);
}
/// <summary>Creates a shader resource view for a resource.</summary>
/// <param name="device">Device to create the resource view into.</param>
/// <param name="resource">Resource to create a view on.</param>
/// <param name="desc">Resource view descriptor.</param>
/// <typeparam name="T">Type of the resource.</typeparam>
/// <returns>New shader resource view.</returns>
public static unsafe ComPtr<ID3D11ShaderResourceView> CreateShaderResourceView<T>(
this ComPtr<ID3D11Device> device,
ComPtr<T> resource,
in D3D11_SHADER_RESOURCE_VIEW_DESC desc)
where T : unmanaged, ID3D11Resource.Interface
{
fixed (D3D11_SHADER_RESOURCE_VIEW_DESC* pDesc = &desc)
{
var srv = default(ComPtr<ID3D11ShaderResourceView>);
device.Get()->CreateShaderResourceView(
(ID3D11Resource*)resource.Get(),
pDesc,
srv.GetAddressOf())
.ThrowOnError();
return srv;
}
}
/// <summary>Gets the descriptor for a <see cref="ID3D11Texture2D"/>.</summary>
/// <param name="texture">Texture.</param>
/// <returns>Texture descriptor.</returns>
public static unsafe D3D11_TEXTURE2D_DESC GetDesc(this ComPtr<ID3D11Texture2D> texture)
{
var desc = default(D3D11_TEXTURE2D_DESC);
texture.Get()->GetDesc(&desc);
return desc;
}
}

View file

@ -1,3 +1,7 @@
using System.Runtime.CompilerServices;
using Dalamud.Memory;
using ImGuiScene;
using Lumina.Data.Files;
@ -28,4 +32,25 @@ public static class TexFileExtensions
return dst;
}
/// <summary>Determines if the given data is possibly a <see cref="TexFile"/>.</summary>
/// <param name="data">The data.</param>
/// <returns><c>true</c> if it should be attempted to be interpreted as a <see cref="TexFile"/>.</returns>
internal static unsafe bool IsPossiblyTexFile2D(ReadOnlySpan<byte> data)
{
if (data.Length < Unsafe.SizeOf<TexFile.TexHeader>())
return false;
fixed (byte* ptr = data)
{
ref readonly var texHeader = ref MemoryHelper.Cast<TexFile.TexHeader>((nint)ptr);
if ((texHeader.Type & TexFile.Attribute.TextureTypeMask) != TexFile.Attribute.TextureType2D)
return false;
if (!Enum.IsDefined(texHeader.Format))
return false;
if (texHeader.Width == 0 || texHeader.Height == 0)
return false;
}
return true;
}
}

View file

@ -640,6 +640,30 @@ public static class Util
throw new Win32Exception();
}
/// <summary>Gets a temporary file name, for use as the sourceFileName in
/// <see cref="File.Replace(string,string,string?)"/>.</summary>
/// <param name="targetFile">The target file.</param>
/// <returns>A temporary file name that should be usable with <see cref="File.Replace(string,string,string?)"/>.
/// </returns>
/// <remarks>No write operation is done on the filesystem.</remarks>
public static string GetTempFileNameForFileReplacement(string targetFile)
{
Span<byte> buf = stackalloc byte[9];
Random.Shared.NextBytes(buf);
for (var i = 0; ; i++)
{
var tempName =
Path.GetFileName(targetFile) +
Convert.ToBase64String(buf)
.TrimEnd('=')
.Replace('+', '-')
.Replace('/', '_');
var tempPath = Path.Join(Path.GetDirectoryName(targetFile), tempName);
if (i >= 64 || !Path.Exists(tempPath))
return tempPath;
}
}
/// <summary>
/// Gets a random, inoffensive, human-friendly string.
/// </summary>
@ -657,12 +681,21 @@ public static class Util
/// Throws a corresponding exception if <see cref="HRESULT.FAILED"/> is true.
/// </summary>
/// <param name="hr">The result value.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void ThrowOnError(this HRESULT hr)
{
if (hr.FAILED)
Marshal.ThrowExceptionForHR(hr.Value);
}
/// <summary>Determines if the specified instance of <see cref="ComPtr{T}"/> points to null.</summary>
/// <param name="f">The pointer.</param>
/// <typeparam name="T">The COM interface type from TerraFX.</typeparam>
/// <returns><c>true</c> if not empty.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static unsafe bool IsEmpty<T>(in this ComPtr<T> f) where T : unmanaged, IUnknown.Interface =>
f.Get() is null;
/// <summary>
/// Calls <see cref="TaskCompletionSource.SetException(System.Exception)"/> if the task is incomplete.
/// </summary>