Update ITextureProvider

This commit is contained in:
Soreepeong 2024-02-22 00:53:07 +09:00
parent 8e5a84792e
commit 3fe2920e92
14 changed files with 1222 additions and 498 deletions

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

@ -0,0 +1,30 @@
namespace Dalamud.Interface.Internal;
/// <summary>
/// A texture wrap that ignores <see cref="IDisposable.Dispose"/> calls.
/// </summary>
internal sealed class DisposeSuppressingTextureWrap : IDalamudTextureWrap
{
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/>
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

@ -0,0 +1,31 @@
namespace Dalamud.Interface.Internal;
/// <summary>
/// A disposed texture wrap.
/// </summary>
internal sealed class DisposedDalamudTextureWrap : IDalamudTextureWrap
{
/// <summary>
/// Gets the singleton instance.
/// </summary>
public static readonly DisposedDalamudTextureWrap Instance = new();
private DisposedDalamudTextureWrap()
{
}
/// <inheritdoc/>
public IntPtr ImGuiHandle => throw new ObjectDisposedException(nameof(DisposedDalamudTextureWrap));
/// <inheritdoc/>
public int Width => throw new ObjectDisposedException(nameof(DisposedDalamudTextureWrap));
/// <inheritdoc/>
public int Height => throw new ObjectDisposedException(nameof(DisposedDalamudTextureWrap));
/// <inheritdoc/>
public void Dispose()
{
// suppressed
}
}

View file

@ -25,6 +25,10 @@ using Dalamud.Utility;
using Dalamud.Utility.Timing;
using ImGuiNET;
using ImGuiScene;
using Lumina.Data.Files;
using Lumina.Data.Parsing.Tex.Buffers;
using PInvoke;
using Serilog;
using SharpDX;
@ -63,7 +67,7 @@ internal class InterfaceManager : IDisposable, IServiceType
public const float DefaultFontSizePx = (DefaultFontSizePt * 4.0f) / 3.0f;
private readonly ConcurrentBag<IDeferredDisposable> deferredDisposeTextures = new();
private readonly ConcurrentBag<ILockedImFont> deferredDisposeImFontLockeds = new();
private readonly ConcurrentBag<IDisposable> deferredDisposeDisposables = new();
[ServiceManager.ServiceDependency]
private readonly WndProcHookManager wndProcHookManager = Service<WndProcHookManager>.Get();
@ -349,7 +353,7 @@ internal class InterfaceManager : IDisposable, IServiceType
/// <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)
public DalamudTextureWrap LoadImageFromDxgiFormat(ReadOnlySpan<byte> data, int pitch, int width, int height, Format dxgiFormat)
{
if (this.scene == null)
throw new InvalidOperationException("Scene isn't ready.");
@ -389,6 +393,37 @@ internal class InterfaceManager : IDisposable, IServiceType
#nullable restore
/// <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 DalamudTextureWrap LoadImageFromTexFile(TexFile file)
{
if (!this.IsReady)
throw new InvalidOperationException("Cannot create textures before scene is ready");
var buffer = file.TextureBuffer;
var bpp = 1 << (((int)file.Header.Format & (int)TexFile.TextureFormat.BppMask) >>
(int)TexFile.TextureFormat.BppShift);
var (dxgiFormat, conversion) = TexFile.GetDxgiFormatFromTextureFormat(file.Header.Format, false);
if (conversion != TexFile.DxgiFormatConversion.NoConversion || !this.SupportsDxgiFormat((Format)dxgiFormat))
{
dxgiFormat = (int)Format.B8G8R8A8_UNorm;
buffer = buffer.Filter(0, 0, TexFile.TextureFormat.B8G8R8A8);
bpp = 32;
}
var pitch = buffer is BlockCompressionTextureBuffer
? Math.Max(1, (buffer.Width + 3) / 4) * 2 * bpp
: ((buffer.Width * bpp) + 7) / 8;
return this.LoadImageFromDxgiFormat(buffer.RawData, pitch, buffer.Width, buffer.Height, (Format)dxgiFormat);
}
/// <summary>
/// Sets up a deferred invocation of font rebuilding, before the next render frame.
/// </summary>
@ -411,9 +446,9 @@ internal class InterfaceManager : IDisposable, IServiceType
/// 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>
@ -683,12 +718,12 @@ internal class InterfaceManager : IDisposable, IServiceType
Log.Verbose("[IM] Disposing {Count} textures", count);
}
if (!this.deferredDisposeImFontLockeds.IsEmpty)
if (!this.deferredDisposeDisposables.IsEmpty)
{
// Not logging; the main purpose of this is to keep resources used for rendering the frame to be kept
// referenced until the resources are actually done being used, and it is expected that this will be
// frequent.
while (this.deferredDisposeImFontLockeds.TryTake(out var d))
while (this.deferredDisposeDisposables.TryTake(out var d))
d.Dispose();
}
}

View file

@ -0,0 +1,54 @@
using System.Threading.Tasks;
using Dalamud.Utility;
namespace Dalamud.Interface.Internal.SharableTextures;
/// <summary>
/// Represents a sharable texture, based on a file on the system filesystem.
/// </summary>
internal sealed class FileSystemSharableTexture : SharableTexture
{
private readonly string path;
/// <summary>
/// Initializes a new instance of the <see cref="FileSystemSharableTexture"/> class.
/// </summary>
/// <param name="path">The path.</param>
public FileSystemSharableTexture(string path)
{
this.path = path;
this.UnderlyingWrap = this.CreateTextureAsync();
}
/// <inheritdoc/>
protected override Task<IDalamudTextureWrap> UnderlyingWrap { get; set; }
/// <inheritdoc/>
public override string ToString() =>
$"{nameof(FileSystemSharableTexture)}#{this.InstanceIdForDebug}({this.path})";
/// <inheritdoc/>
protected override void FinalRelease()
{
this.DisposeSuppressingWrap = null;
_ = this.UnderlyingWrap.ToContentDisposedTask(true);
this.UnderlyingWrap =
Task.FromException<IDalamudTextureWrap>(new ObjectDisposedException(nameof(GamePathSharableTexture)));
_ = this.UnderlyingWrap.Exception;
}
/// <inheritdoc/>
protected override void Revive() =>
this.UnderlyingWrap = this.CreateTextureAsync();
private Task<IDalamudTextureWrap> CreateTextureAsync() =>
Task.Run(
() =>
{
var w = (IDalamudTextureWrap)Service<InterfaceManager>.Get().LoadImage(this.path)
?? throw new("Failed to load image because of an unknown reason.");
this.DisposeSuppressingWrap = new(w);
return w;
});
}

View file

@ -0,0 +1,59 @@
using System.IO;
using System.Threading.Tasks;
using Dalamud.Data;
using Dalamud.Utility;
using Lumina.Data.Files;
namespace Dalamud.Interface.Internal.SharableTextures;
/// <summary>
/// Represents a sharable texture, based on a file in game resources.
/// </summary>
internal sealed class GamePathSharableTexture : SharableTexture
{
private readonly string path;
/// <summary>
/// Initializes a new instance of the <see cref="GamePathSharableTexture"/> class.
/// </summary>
/// <param name="path">The path.</param>
public GamePathSharableTexture(string path)
{
this.path = path;
this.UnderlyingWrap = this.CreateTextureAsync();
}
/// <inheritdoc/>
protected override Task<IDalamudTextureWrap> UnderlyingWrap { get; set; }
/// <inheritdoc/>
public override string ToString() => $"{nameof(GamePathSharableTexture)}#{this.InstanceIdForDebug}({this.path})";
/// <inheritdoc/>
protected override void FinalRelease()
{
this.DisposeSuppressingWrap = null;
_ = this.UnderlyingWrap.ToContentDisposedTask(true);
this.UnderlyingWrap =
Task.FromException<IDalamudTextureWrap>(new ObjectDisposedException(nameof(GamePathSharableTexture)));
_ = this.UnderlyingWrap.Exception;
}
/// <inheritdoc/>
protected override void Revive() =>
this.UnderlyingWrap = this.CreateTextureAsync();
private Task<IDalamudTextureWrap> CreateTextureAsync() =>
Task.Run(
async () =>
{
var dm = await Service<DataManager>.GetAsync();
var im = await Service<InterfaceManager>.GetAsync();
var file = dm.GetFile<TexFile>(this.path);
var t = (IDalamudTextureWrap)im.LoadImageFromTexFile(file ?? throw new FileNotFoundException());
this.DisposeSuppressingWrap = new(t);
return t;
});
}

View file

@ -0,0 +1,333 @@
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Storage.Assets;
using Dalamud.Utility;
namespace Dalamud.Interface.Internal.SharableTextures;
/// <summary>
/// Represents a texture that may have multiple reference holders (owners).
/// </summary>
internal abstract class SharableTexture : IRefCountable
{
private const int SelfReferenceDurationTicks = 5000;
private const long SelfReferenceExpiryExpired = long.MaxValue;
private static long instanceCounter;
private readonly object reviveLock = new();
private int refCount;
private long selfReferenceExpiry;
private IDalamudTextureWrap? availableOnAccessWrapForApi9;
/// <summary>
/// Initializes a new instance of the <see cref="SharableTexture"/> class.
/// </summary>
protected SharableTexture()
{
this.InstanceIdForDebug = Interlocked.Increment(ref instanceCounter);
this.refCount = 1;
this.selfReferenceExpiry = Environment.TickCount64 + SelfReferenceDurationTicks;
this.ReviveEnabled = true;
}
/// <summary>
/// Gets 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>
public WeakReference<IDalamudTextureWrap>? RevivalPossibility { get; private set; }
/// <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 or sets the underlying texture wrap.
/// </summary>
protected abstract Task<IDalamudTextureWrap> UnderlyingWrap { get; set; }
/// <summary>
/// Gets or sets the dispose-suppressing wrap for <see cref="UnderlyingWrap"/>.
/// </summary>
protected DisposeSuppressingTextureWrap? DisposeSuppressingWrap { get; set; }
/// <summary>
/// Gets a value indicating whether this instance of <see cref="SharableTexture"/> supports reviving.
/// </summary>
protected bool ReviveEnabled { get; private set; }
/// <inheritdoc/>
public int AddRef() => this.TryAddRef(out var newRefCount) switch
{
IRefCountable.RefCountResult.StillAlive => newRefCount,
IRefCountable.RefCountResult.AlreadyDisposed => throw new ObjectDisposedException(nameof(SharableTexture)),
IRefCountable.RefCountResult.FinalRelease => throw new InvalidOperationException(),
_ => throw new InvalidOperationException(),
};
/// <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.FinalRelease();
return newRefCount;
case IRefCountable.RefCountResult.AlreadyDisposed:
throw new ObjectDisposedException(nameof(SharableTexture));
default:
throw new InvalidOperationException();
}
}
/// <summary>
/// Releases self-reference, if it should expire.
/// </summary>
/// <returns>Number of the new reference count that may or may not have changed.</returns>
public int ReleaseSelfReferenceIfExpired()
{
while (true)
{
var exp = this.selfReferenceExpiry;
if (exp > Environment.TickCount64)
return this.refCount;
if (exp != Interlocked.CompareExchange(ref this.selfReferenceExpiry, SelfReferenceExpiryExpired, exp))
continue;
this.availableOnAccessWrapForApi9 = null;
return this.Release();
}
}
/// <summary>
/// Disables revival.
/// </summary>
public void DisableReviveAndReleaseSelfReference()
{
this.ReviveEnabled = false;
while (true)
{
var exp = this.selfReferenceExpiry;
if (exp == SelfReferenceExpiryExpired)
return;
if (exp != Interlocked.CompareExchange(ref this.selfReferenceExpiry, SelfReferenceExpiryExpired, exp))
continue;
this.availableOnAccessWrapForApi9 = null;
this.Release();
break;
}
}
/// <summary>
/// Gets the texture if immediately available. The texture is guarnateed to be available for the rest of the frame.
/// Invocation from non-main thread will exhibit an undefined behavior.
/// </summary>
/// <returns>The texture if available; <c>null</c> if not.</returns>
public IDalamudTextureWrap? GetImmediate()
{
if (this.TryAddRef(out _) != IRefCountable.RefCountResult.StillAlive)
return null;
var nexp = Environment.TickCount64 + SelfReferenceDurationTicks;
while (true)
{
var exp = this.selfReferenceExpiry;
if (exp != Interlocked.CompareExchange(ref this.selfReferenceExpiry, nexp, exp))
continue;
// If below condition is met, the additional reference from above is for the self-reference.
if (exp == SelfReferenceExpiryExpired)
_ = this.AddRef();
// Release the reference for rendering, after rendering ImGui.
Service<InterfaceManager>.Get().EnqueueDeferredDispose(this);
return this.DisposeSuppressingWrap;
}
}
/// <summary>
/// Creates a new reference to this texture. The texture is guaranteed to be available until
/// <see cref="IDisposable.Dispose"/> is called.
/// </summary>
/// <returns>The task containing the texture.</returns>
public Task<IDalamudTextureWrap> CreateNewReference()
{
this.AddRef();
return this.UnderlyingWrap.ContinueWith(
r => (IDalamudTextureWrap)new RefCountableWrappingTextureWrap(r.Result, 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;
this.UnderlyingWrap.Wait();
if (this.UnderlyingWrap.Exception is not null)
return null;
this.availableOnAccessWrapForApi9 = new AvailableOnAccessTextureWrap(this);
this.RevivalPossibility = new(this.availableOnAccessWrapForApi9);
}
return this.availableOnAccessWrapForApi9;
}
/// <summary>
/// Cleans up this instance of <see cref="SharableTexture"/>.
/// </summary>
protected abstract void FinalRelease();
/// <summary>
/// Attempts to restore the reference to this texture.
/// </summary>
protected abstract void Revive();
private IRefCountable.RefCountResult TryAddRef(out int newRefCount)
{
var alterResult = IRefCountable.AlterRefCount(1, ref this.refCount, out newRefCount);
if (alterResult != IRefCountable.RefCountResult.AlreadyDisposed || !this.ReviveEnabled)
return alterResult;
lock (this.reviveLock)
{
alterResult = IRefCountable.AlterRefCount(1, ref this.refCount, out newRefCount);
if (alterResult != IRefCountable.RefCountResult.AlreadyDisposed)
return alterResult;
this.Revive();
Interlocked.Increment(ref this.refCount);
if (this.RevivalPossibility?.TryGetTarget(out var target) is true)
this.availableOnAccessWrapForApi9 = target;
return IRefCountable.RefCountResult.StillAlive;
}
}
private sealed class RefCountableWrappingTextureWrap : IDalamudTextureWrap
{
private IDalamudTextureWrap? innerWrap;
private IRefCountable? owner;
/// <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;
}
~RefCountableWrappingTextureWrap() => this.Dispose();
/// <inheritdoc/>
public IntPtr ImGuiHandle => this.InnerWrapNonDisposed.ImGuiHandle;
/// <inheritdoc/>
public int Width => this.InnerWrapNonDisposed.Width;
/// <inheritdoc/>
public int Height => this.InnerWrapNonDisposed.Height;
private IDalamudTextureWrap InnerWrapNonDisposed =>
this.innerWrap ?? throw new ObjectDisposedException(nameof(RefCountableWrappingTextureWrap));
/// <inheritdoc/>
public void Dispose()
{
while (true)
{
if (this.owner is not { } ownerCopy)
return;
if (ownerCopy != Interlocked.CompareExchange(ref this.owner, null, ownerCopy))
continue;
this.innerWrap = null;
ownerCopy.Release();
GC.SuppressFinalize(this);
}
}
/// <inheritdoc/>
public override string ToString() => $"{nameof(RefCountableWrappingTextureWrap)}({this.owner})";
}
private sealed class AvailableOnAccessTextureWrap : IDalamudTextureWrap
{
private readonly SharableTexture inner;
public AvailableOnAccessTextureWrap(SharableTexture inner) => this.inner = inner;
/// <inheritdoc/>
public IntPtr ImGuiHandle => this.GetActualTexture().ImGuiHandle;
/// <inheritdoc/>
public int Width => this.GetActualTexture().Width;
/// <inheritdoc/>
public int Height => this.GetActualTexture().Height;
/// <inheritdoc/>
public void Dispose()
{
// ignore
}
/// <inheritdoc/>
public override string ToString() => $"{nameof(AvailableOnAccessTextureWrap)}({this.inner})";
private IDalamudTextureWrap GetActualTexture()
{
if (this.inner.GetImmediate() is { } t)
return t;
this.inner.UnderlyingWrap.Wait();
return this.inner.DisposeSuppressingWrap ?? Service<DalamudAssetManager>.Get().Empty4X4;
}
}
}

View file

@ -1,17 +1,21 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Numerics;
using System.Threading.Tasks;
using BitFaster.Caching.Lru;
using Dalamud.Data;
using Dalamud.Game;
using Dalamud.Interface.Internal.SharableTextures;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Services;
using Dalamud.Storage.Assets;
using Dalamud.Utility;
using Lumina.Data.Files;
using Lumina.Data.Parsing.Tex.Buffers;
using SharpDX.DXGI;
namespace Dalamud.Interface.Internal;
@ -27,218 +31,263 @@ namespace Dalamud.Interface.Internal;
[ResolveVia<ITextureProvider>]
[ResolveVia<ITextureSubstitutionProvider>]
#pragma warning restore SA1015
internal class TextureManager : IDisposable, IServiceType, ITextureProvider, ITextureSubstitutionProvider
internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvider, ITextureSubstitutionProvider
{
private const int PathLookupLruCount = 8192;
private const string IconFileFormat = "ui/icon/{0:D3}000/{1}{2:D6}.tex";
private const string HighResolutionIconFileFormat = "ui/icon/{0:D3}000/{1}{2:D6}_hr1.tex";
private const uint MillisecondsEvictionTime = 2000;
private static readonly ModuleLog Log = new("TEXM");
private readonly Framework framework;
private readonly DataManager dataManager;
private readonly InterfaceManager im;
[ServiceManager.ServiceDependency]
private readonly Dalamud dalamud = Service<Dalamud>.Get();
private readonly ClientLanguage language;
private readonly Dictionary<string, TextureInfo> activeTextures = new();
[ServiceManager.ServiceDependency]
private readonly DalamudAssetManager dalamudAssetManager = Service<DalamudAssetManager>.Get();
private IDalamudTextureWrap? fallbackTextureWrap;
[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 readonly ConcurrentLru<GameIconLookup, string> lookupToPath = new(PathLookupLruCount);
private readonly ConcurrentDictionary<string, SharableTexture> gamePathTextures = new();
private readonly ConcurrentDictionary<string, SharableTexture> fileSystemTextures = new();
private bool disposing;
/// <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)
private TextureManager()
{
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.
/// Gets all the loaded textures from the game resources. Debug use only.
/// </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)
public IReadOnlyDictionary<string, SharableTexture> GamePathTextures => this.gamePathTextures;
/// <summary>
/// Gets all the loaded textures from the game resources. Debug use only.
/// </summary>
public IReadOnlyDictionary<string, SharableTexture> FileSystemTextures => this.fileSystemTextures;
/// <inheritdoc/>
public void Dispose()
{
if (this.disposing)
return;
this.disposing = true;
foreach (var v in this.gamePathTextures.Values)
v.DisableReviveAndReleaseSelfReference();
foreach (var v in this.fileSystemTextures.Values)
v.DisableReviveAndReleaseSelfReference();
this.lookupToPath.Clear();
this.gamePathTextures.Clear();
this.fileSystemTextures.Clear();
}
#pragma warning disable CS0618 // Type or member is obsolete
/// <inheritdoc/>
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
[Obsolete("See interface definition.")]
public string? GetIconPath(
uint iconId,
ITextureProvider.IconFlags flags = ITextureProvider.IconFlags.HiRes,
ClientLanguage? language = null)
=> this.TryGetIconPath(
new(
iconId,
(flags & ITextureProvider.IconFlags.ItemHighQuality) != 0,
(flags & ITextureProvider.IconFlags.HiRes) != 0,
language),
out var path)
? path
: null;
/// <inheritdoc/>
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
[Obsolete("See interface definition.")]
public IDalamudTextureWrap? GetIcon(
uint iconId,
ITextureProvider.IconFlags flags = ITextureProvider.IconFlags.HiRes,
ClientLanguage? language = null,
bool keepAlive = false) =>
this.GetTextureFromGame(
this.lookupToPath.GetOrAdd(
new(
iconId,
(flags & ITextureProvider.IconFlags.ItemHighQuality) != 0,
(flags & ITextureProvider.IconFlags.HiRes) != 0,
language),
this.GetIconPathByValue));
/// <inheritdoc/>
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
[Obsolete("See interface definition.")]
public IDalamudTextureWrap? GetTextureFromGame(string path, bool keepAlive = false) =>
this.gamePathTextures.GetOrAdd(path, CreateGamePathSharableTexture).GetAvailableOnAccessWrapForApi9();
/// <inheritdoc/>
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
[Obsolete("See interface definition.")]
public IDalamudTextureWrap? GetTextureFromFile(FileInfo file, bool keepAlive = false) =>
this.fileSystemTextures.GetOrAdd(file.FullName, CreateFileSystemSharableTexture).GetAvailableOnAccessWrapForApi9();
#pragma warning restore CS0618 // Type or member is obsolete
/// <inheritdoc/>
public IDalamudTextureWrap ImmediateGetFromGameIcon(in GameIconLookup lookup) =>
this.ImmediateGetFromGame(this.lookupToPath.GetOrAdd(lookup, this.GetIconPathByValue));
/// <inheritdoc/>
public IDalamudTextureWrap ImmediateGetFromGame(string path) =>
this.gamePathTextures.GetOrAdd(path, CreateGamePathSharableTexture).GetImmediate()
?? this.dalamudAssetManager.Empty4X4;
/// <inheritdoc/>
public IDalamudTextureWrap ImmediateGetFromFile(string file) =>
this.fileSystemTextures.GetOrAdd(file, CreateFileSystemSharableTexture).GetImmediate()
?? this.dalamudAssetManager.Empty4X4;
/// <inheritdoc/>
public Task<IDalamudTextureWrap> GetFromGameIconAsync(in GameIconLookup lookup) =>
this.GetFromGameAsync(this.lookupToPath.GetOrAdd(lookup, this.GetIconPathByValue));
/// <inheritdoc/>
public Task<IDalamudTextureWrap> GetFromGameAsync(string path) =>
this.gamePathTextures.GetOrAdd(path, CreateGamePathSharableTexture).CreateNewReference();
/// <inheritdoc/>
public Task<IDalamudTextureWrap> GetFromFileAsync(string file) =>
this.fileSystemTextures.GetOrAdd(file, CreateFileSystemSharableTexture).CreateNewReference();
/// <inheritdoc/>
public Task<IDalamudTextureWrap> GetFromImageAsync(ReadOnlyMemory<byte> bytes) =>
Task.Run(
() => this.interfaceManager.LoadImage(bytes.ToArray())
?? throw new("Failed to load image because of an unknown reason."));
/// <inheritdoc/>
public async Task<IDalamudTextureWrap> GetFromImageAsync(Stream stream, bool leaveOpen = false)
{
await using var streamDispose = leaveOpen ? null : stream;
await using var ms = stream.CanSeek ? new MemoryStream((int)stream.Length) : new();
await stream.CopyToAsync(ms).ConfigureAwait(false);
return await this.GetFromImageAsync(ms.GetBuffer());
}
/// <inheritdoc/>
public IDalamudTextureWrap GetFromRaw(RawImageSpecification specs, ReadOnlySpan<byte> bytes) =>
this.interfaceManager.LoadImageFromDxgiFormat(
bytes,
specs.Pitch,
specs.Width,
specs.Height,
(SharpDX.DXGI.Format)specs.DxgiFormat);
/// <inheritdoc/>
public Task<IDalamudTextureWrap> GetFromRawAsync(RawImageSpecification specs, ReadOnlyMemory<byte> bytes) =>
Task.Run(() => this.GetFromRaw(specs, bytes.Span));
/// <inheritdoc/>
public async Task<IDalamudTextureWrap> GetFromRawAsync(
RawImageSpecification specs,
Stream stream,
bool leaveOpen = false)
{
await using var streamDispose = leaveOpen ? null : stream;
await using var ms = stream.CanSeek ? new MemoryStream((int)stream.Length) : new();
await stream.CopyToAsync(ms).ConfigureAwait(false);
return await this.GetFromRawAsync(specs, ms.GetBuffer());
}
/// <inheritdoc/>
public IDalamudTextureWrap GetTexture(TexFile file) => this.interfaceManager.LoadImageFromTexFile(file);
/// <inheritdoc/>
public bool TryGetIconPath(in GameIconLookup lookup, out string path)
{
var hiRes = flags.HasFlag(ITextureProvider.IconFlags.HiRes);
// 1. Item
var path = FormatIconPath(
iconId,
flags.HasFlag(ITextureProvider.IconFlags.ItemHighQuality) ? "hq/" : string.Empty,
hiRes);
path = FormatIconPath(
lookup.IconId,
lookup.ItemHq ? "hq/" : string.Empty,
lookup.HiRes);
if (this.dataManager.FileExists(path))
return path;
language ??= this.language;
var languageFolder = language switch
return true;
var languageFolder = (lookup.Language ?? (ClientLanguage)(int)this.dalamud.StartInfo.Language) switch
{
ClientLanguage.Japanese => "ja/",
ClientLanguage.English => "en/",
ClientLanguage.German => "de/",
ClientLanguage.French => "fr/",
_ => throw new ArgumentOutOfRangeException(nameof(language), $"Unknown Language: {language}"),
_ => null,
};
// 2. Regular icon, with language, hi-res
path = FormatIconPath(
iconId,
languageFolder,
hiRes);
if (this.dataManager.FileExists(path))
return path;
if (hiRes)
if (languageFolder is not null)
{
// 3. Regular icon, with language, no hi-res
// 2. Regular icon, with language, hi-res
path = FormatIconPath(
iconId,
lookup.IconId,
languageFolder,
false);
lookup.HiRes);
if (this.dataManager.FileExists(path))
return path;
return true;
if (lookup.HiRes)
{
// 3. Regular icon, with language, no hi-res
path = FormatIconPath(
lookup.IconId,
languageFolder,
false);
if (this.dataManager.FileExists(path))
return true;
}
}
// 4. Regular icon, without language, hi-res
path = FormatIconPath(
iconId,
lookup.IconId,
null,
hiRes);
lookup.HiRes);
if (this.dataManager.FileExists(path))
return path;
return true;
// 4. Regular icon, without language, no hi-res
if (hiRes)
if (lookup.HiRes)
{
path = FormatIconPath(
iconId,
lookup.IconId,
null,
false);
if (this.dataManager.FileExists(path))
return path;
return true;
}
return null;
return false;
}
/// <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 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);
@ -254,261 +303,45 @@ internal class TextureManager : IDisposable, IServiceType, ITextureProvider, ITe
/// <inheritdoc/>
public void InvalidatePaths(IEnumerable<string> paths)
{
lock (this.activeTextures)
foreach (var path in paths)
{
foreach (var path in paths)
{
if (!this.activeTextures.TryGetValue(path, out var info) || info == null)
continue;
info.Wrap?.Dispose();
info.Wrap = null;
}
if (this.gamePathTextures.TryRemove(path, out var r))
r.DisableReviveAndReleaseSelfReference();
}
}
/// <inheritdoc/>
public void Dispose()
{
this.fallbackTextureWrap?.Dispose();
this.framework.Update -= this.FrameworkOnUpdate;
if (this.activeTextures.Count == 0)
return;
Log.Verbose("Disposing {Num} left behind textures.", this.activeTextures.Count);
foreach (var activeTexture in this.activeTextures)
{
activeTexture.Value.Wrap?.Dispose();
}
this.activeTextures.Clear();
}
private static SharableTexture CreateGamePathSharableTexture(string gamePath) =>
new GamePathSharableTexture(gamePath);
/// <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 SharableTexture CreateFileSystemSharableTexture(string fileSystemPath) =>
new FileSystemSharableTexture(fileSystemPath);
private static string FormatIconPath(uint iconId, string? type, bool highResolution)
{
var format = highResolution ? HighResolutionIconFileFormat : IconFileFormat;
type ??= string.Empty;
if (type.Length > 0 && !type.EndsWith("/"))
type += "/";
return string.Format(format, iconId / 1000, type, iconId);
}
private TextureManagerTextureWrap? CreateWrap(string path)
private void FrameworkOnUpdate(IFramework unused)
{
lock (this.activeTextures)
foreach (var (k, v) in this.gamePathTextures)
{
// This will create the texture.
// That's fine, it's probably used immediately and this will let the plugin catch load errors.
var info = this.GetInfo(path, rethrow: true);
return new TextureManagerTextureWrap(path, info.Extents, this);
if (v.ReleaseSelfReferenceIfExpired() == 0 && v.RevivalPossibility?.TryGetTarget(out _) is not true)
_ = this.gamePathTextures.TryRemove(k, out _);
}
foreach (var (k, v) in this.fileSystemTextures)
{
if (v.ReleaseSelfReferenceIfExpired() == 0 && v.RevivalPossibility?.TryGetTarget(out _) is not true)
_ = this.fileSystemTextures.TryRemove(k, out _);
}
}
private void FrameworkOnUpdate(IFramework fw)
{
lock (this.activeTextures)
{
var toRemove = new List<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.
}
private string GetIconPathByValue(GameIconLookup lookup) =>
this.TryGetIconPath(lookup, out var path) ? path : throw new FileNotFoundException();
}

View file

@ -15,6 +15,7 @@ using Dalamud.Interface.Internal.Notifications;
using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Interface.ManagedFontAtlas.Internals;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using ImGuiNET;
using ImGuiScene;
@ -383,6 +384,8 @@ public sealed class UiBuilder : IDisposable
/// </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>
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
[Obsolete($"Use {nameof(ITextureProvider.GetFromFileAsync)}.")]
public IDalamudTextureWrap LoadImage(string filePath)
=> this.InterfaceManagerWithScene?.LoadImage(filePath)
?? throw new InvalidOperationException("Load failed.");
@ -392,6 +395,8 @@ public sealed class UiBuilder : IDisposable
/// </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>
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
[Obsolete($"Use {nameof(ITextureProvider.GetFromImageAsync)}.")]
public IDalamudTextureWrap LoadImage(byte[] imageData)
=> this.InterfaceManagerWithScene?.LoadImage(imageData)
?? throw new InvalidOperationException("Load failed.");
@ -404,6 +409,8 @@ public sealed class UiBuilder : IDisposable
/// <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>
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
[Obsolete($"Use {nameof(ITextureProvider.GetFromRaw)} or {nameof(ITextureProvider.GetFromRawAsync)}.")]
public IDalamudTextureWrap LoadImageRaw(byte[] imageData, int width, int height, int numChannels)
=> this.InterfaceManagerWithScene?.LoadImageRaw(imageData, width, height, numChannels)
?? throw new InvalidOperationException("Load failed.");
@ -421,6 +428,8 @@ public sealed class UiBuilder : IDisposable
/// </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>
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
[Obsolete($"Use {nameof(ITextureProvider.GetFromFileAsync)}.")]
public Task<IDalamudTextureWrap> LoadImageAsync(string filePath) => Task.Run(
async () =>
(await this.InterfaceManagerWithSceneAsync).LoadImage(filePath)
@ -431,6 +440,8 @@ public sealed class UiBuilder : IDisposable
/// </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>
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
[Obsolete($"Use {nameof(ITextureProvider.GetFromImageAsync)}.")]
public Task<IDalamudTextureWrap> LoadImageAsync(byte[] imageData) => Task.Run(
async () =>
(await this.InterfaceManagerWithSceneAsync).LoadImage(imageData)
@ -444,6 +455,8 @@ public sealed class UiBuilder : IDisposable
/// <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>
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
[Obsolete($"Use {nameof(ITextureProvider.GetFromRawAsync)}.")]
public Task<IDalamudTextureWrap> LoadImageRawAsync(byte[] imageData, int width, int height, int numChannels) => Task.Run(
async () =>
(await this.InterfaceManagerWithSceneAsync).LoadImageRaw(imageData, width, height, numChannels)

View file

@ -0,0 +1,20 @@
using System.Diagnostics.CodeAnalysis;
namespace Dalamud.Plugin.Services;
/// <summary>
/// Represents a lookup for a game icon.
/// </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>
[SuppressMessage(
"StyleCop.CSharp.NamingRules",
"SA1313:Parameter names should begin with lower-case letter",
Justification = "no")]
public record struct GameIconLookup(
uint IconId,
bool ItemHq = false,
bool HiRes = true,
ClientLanguage? Language = null);

View file

@ -0,0 +1,98 @@
using System.IO;
using Dalamud.Interface.Internal;
using Dalamud.Utility;
namespace Dalamud.Plugin.Services;
/// <summary>
/// Service that grants you access to textures you may render via ImGui.
/// </summary>
public partial interface ITextureProvider
{
/// <summary>
/// Flags describing the icon you wish to receive.
/// </summary>
[Obsolete($"Use {nameof(GameIconLookup)}.")]
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
[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>
[Obsolete($"Use {nameof(ImmediateGetFromGameIcon)} or {nameof(GetFromGameIconAsync)}.")]
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
public IDalamudTextureWrap? GetIcon(uint iconId, IconFlags flags = IconFlags.HiRes, ClientLanguage? language = null, bool keepAlive = false);
/// <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>
[Obsolete($"Use {nameof(TryGetIconPath)} or {nameof(GetIconPath)}({nameof(GameIconLookup)}).")]
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
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>
[Obsolete($"Use {nameof(ImmediateGetFromGame)} or {nameof(GetFromGameAsync)}.")]
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
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>
[Obsolete($"Use {nameof(ImmediateGetFromFile)} or {nameof(GetFromFileAsync)}.")]
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
public IDalamudTextureWrap? GetTextureFromFile(FileInfo file, bool keepAlive = false);
}

View file

@ -1,7 +1,9 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Threading.Tasks;
using Dalamud.Interface.Internal;
using Lumina.Data.Files;
namespace Dalamud.Plugin.Services;
@ -9,86 +11,107 @@ namespace Dalamud.Plugin.Services;
/// <summary>
/// Service that grants you access to textures you may render via ImGui.
/// </summary>
public interface ITextureProvider
public partial interface ITextureProvider
{
/// <summary>
/// Flags describing the icon you wish to receive.
/// <summary>Gets the corresponding game icon for use with the current frame.</summary>
/// <param name="lookup">The icon specifier.</param>
/// <returns>An instance of <see cref="IDalamudTextureWrap"/> that is guaranteed to be available for the current
/// frame being drawn.</returns>
/// <remarks><see cref="IDisposable.Dispose"/> will be ignored.<br />
/// If the file is unavailable, then the returned instance of <see cref="IDalamudTextureWrap"/> will point to an
/// empty texture instead.</remarks>
/// <exception cref="InvalidOperationException">Thrown when called outside the UI thread.</exception>
public IDalamudTextureWrap ImmediateGetFromGameIcon(in GameIconLookup lookup);
/// <summary>Gets a texture from a file shipped as a part of the game resources for use with the current frame.
/// </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="path">The game-internal path to a .tex, .atex, or an image file such as .png.</param>
/// <returns>An instance of <see cref="IDalamudTextureWrap"/> that is guaranteed to be available for the current
/// frame being drawn.</returns>
/// <remarks><see cref="IDisposable.Dispose"/> will be ignored.<br />
/// If the file is unavailable, then the returned instance of <see cref="IDalamudTextureWrap"/> will point to an
/// empty texture instead.</remarks>
/// <exception cref="InvalidOperationException">Thrown when called outside the UI thread.</exception>
public IDalamudTextureWrap ImmediateGetFromGame(string path);
/// <summary>Gets a texture from a file on the filesystem for use with the current frame.</summary>
/// <param name="file">The filesystem path to a .tex, .atex, or an image file such as .png.</param>
/// <returns>An instance of <see cref="IDalamudTextureWrap"/> that is guaranteed to be available for the current
/// frame being drawn.</returns>
/// <remarks><see cref="IDisposable.Dispose"/> will be ignored.<br />
/// If the file is unavailable, then the returned instance of <see cref="IDalamudTextureWrap"/> will point to an
/// empty texture instead.</remarks>
/// <exception cref="InvalidOperationException">Thrown when called outside the UI thread.</exception>
public IDalamudTextureWrap ImmediateGetFromFile(string file);
/// <summary>Gets the corresponding game icon for use with the current frame.</summary>
/// <param name="lookup">The icon specifier.</param>
/// <returns>A <see cref="Task{TResult}"/> containing the loaded texture on success. Dispose after use.</returns>
public Task<IDalamudTextureWrap> GetFromGameIconAsync(in GameIconLookup lookup);
/// <summary>Gets a texture from a file shipped as a part of the game resources.</summary>
/// <param name="path">The game-internal path to a .tex, .atex, or an image file such as .png.</param>
/// <returns>A <see cref="Task{TResult}"/> containing the loaded texture on success. Dispose after use.</returns>
public Task<IDalamudTextureWrap> GetFromGameAsync(string path);
/// <summary>Gets a texture from a file on the filesystem.</summary>
/// <param name="file">The filesystem path to a .tex, .atex, or an image file such as .png.</param>
/// <returns>A <see cref="Task{TResult}"/> containing the loaded texture on success. Dispose after use.</returns>
public Task<IDalamudTextureWrap> GetFromFileAsync(string file);
/// <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>
/// <returns>A <see cref="Task{TResult}"/> containing the loaded texture on success. Dispose after use.</returns>
public Task<IDalamudTextureWrap> GetFromImageAsync(ReadOnlyMemory<byte> bytes);
/// <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>
/// <returns>A <see cref="Task{TResult}"/> containing the loaded texture on success. Dispose after use.</returns>
public Task<IDalamudTextureWrap> GetFromImageAsync(Stream stream, bool leaveOpen = false);
/// <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>
/// <returns>The texture loaded from the supplied raw bitmap. Dispose after use.</returns>
public IDalamudTextureWrap GetFromRaw(RawImageSpecification specs, ReadOnlySpan<byte> bytes);
/// <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>
/// <returns>A <see cref="Task{TResult}"/> containing the loaded texture on success. Dispose after use.</returns>
public Task<IDalamudTextureWrap> GetFromRawAsync(RawImageSpecification specs, ReadOnlyMemory<byte> bytes);
/// <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>
/// <returns>A <see cref="Task{TResult}"/> containing the loaded texture on success. Dispose after use.</returns>
public Task<IDalamudTextureWrap> GetFromRawAsync(
RawImageSpecification specs,
Stream stream,
bool leaveOpen = false);
/// <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);
/// <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>
public string GetIconPath(in GameIconLookup lookup);
/// <summary>
/// Get a texture handle for the texture at the specified path.
/// You may only specify paths in the game's VFS.
/// Gets the path of an icon.
/// </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);
/// <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>
public bool TryGetIconPath(in GameIconLookup lookup, [NotNullWhen(true)] out string? 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);
/// <summary>
/// Get a texture handle for the specified Lumina TexFile.
/// Get a texture handle for the specified Lumina <see cref="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>

View file

@ -0,0 +1,216 @@
using System.Diagnostics.CodeAnalysis;
using TerraFX.Interop.DirectX;
namespace Dalamud.Plugin.Services;
/// <summary>
/// Describes a raw image.
/// </summary>
/// <param name="Width">The width of the image.</param>
/// <param name="Height">The height of the image.</param>
/// <param name="Pitch">The pitch of the image.</param>
/// <param name="DxgiFormat">The format of the image. See <a href="https://learn.microsoft.com/en-us/windows/win32/api/dxgiformat/ne-dxgiformat-dxgi_format">DXGI_FORMAT</a>.</param>
[SuppressMessage(
"StyleCop.CSharp.NamingRules",
"SA1313:Parameter names should begin with lower-case letter",
Justification = "no")]
public record struct RawImageSpecification(int Width, int Height, int Pitch, int DxgiFormat)
{
/// <summary>
/// Creates a new instance of <see cref="RawImageSpecification"/> record using the given resolution and pixel
/// format. Pitch will be automatically calculated.
/// </summary>
/// <param name="width">The width.</param>
/// <param name="height">The height.</param>
/// <param name="format">The format.</param>
/// <returns>The new instance.</returns>
public static RawImageSpecification From(int width, int height, int format)
{
int bitsPerPixel;
var isBlockCompression = false;
switch ((DXGI_FORMAT)format)
{
case DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_FLOAT:
case DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_SINT:
bitsPerPixel = 128;
break;
case DXGI_FORMAT.DXGI_FORMAT_R32G32B32_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_R32G32B32_FLOAT:
case DXGI_FORMAT.DXGI_FORMAT_R32G32B32_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R32G32B32_SINT:
bitsPerPixel = 96;
break;
case DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_FLOAT:
case DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_SNORM:
case DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_SINT:
case DXGI_FORMAT.DXGI_FORMAT_R32G32_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_R32G32_FLOAT:
case DXGI_FORMAT.DXGI_FORMAT_R32G32_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R32G32_SINT:
case DXGI_FORMAT.DXGI_FORMAT_R32G8X24_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_D32_FLOAT_S8X24_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R32_FLOAT_X8X24_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_X32_TYPELESS_G8X24_UINT:
bitsPerPixel = 64;
break;
case DXGI_FORMAT.DXGI_FORMAT_R10G10B10A2_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_R10G10B10A2_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_R10G10B10A2_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R11G11B10_FLOAT:
case DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM_SRGB:
case DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_SNORM:
case DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_SINT:
case DXGI_FORMAT.DXGI_FORMAT_R16G16_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_R16G16_FLOAT:
case DXGI_FORMAT.DXGI_FORMAT_R16G16_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_R16G16_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R16G16_SNORM:
case DXGI_FORMAT.DXGI_FORMAT_R16G16_SINT:
case DXGI_FORMAT.DXGI_FORMAT_R32_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_D32_FLOAT:
case DXGI_FORMAT.DXGI_FORMAT_R32_FLOAT:
case DXGI_FORMAT.DXGI_FORMAT_R32_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R32_SINT:
case DXGI_FORMAT.DXGI_FORMAT_R24G8_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_D24_UNORM_S8_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R24_UNORM_X8_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_X24_TYPELESS_G8_UINT:
bitsPerPixel = 32;
break;
case DXGI_FORMAT.DXGI_FORMAT_R8G8_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_R8G8_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_R8G8_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R8G8_SNORM:
case DXGI_FORMAT.DXGI_FORMAT_R8G8_SINT:
case DXGI_FORMAT.DXGI_FORMAT_R16_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_R16_FLOAT:
case DXGI_FORMAT.DXGI_FORMAT_D16_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_R16_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_R16_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R16_SNORM:
case DXGI_FORMAT.DXGI_FORMAT_R16_SINT:
bitsPerPixel = 16;
break;
case DXGI_FORMAT.DXGI_FORMAT_R8_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_R8_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_R8_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R8_SNORM:
case DXGI_FORMAT.DXGI_FORMAT_R8_SINT:
case DXGI_FORMAT.DXGI_FORMAT_A8_UNORM:
bitsPerPixel = 8;
break;
case DXGI_FORMAT.DXGI_FORMAT_R1_UNORM:
bitsPerPixel = 1;
break;
case DXGI_FORMAT.DXGI_FORMAT_R9G9B9E5_SHAREDEXP:
case DXGI_FORMAT.DXGI_FORMAT_R8G8_B8G8_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_G8R8_G8B8_UNORM:
throw new NotSupportedException();
case DXGI_FORMAT.DXGI_FORMAT_BC1_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM_SRGB:
bitsPerPixel = 4;
isBlockCompression = true;
break;
case DXGI_FORMAT.DXGI_FORMAT_BC2_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_BC2_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_BC2_UNORM_SRGB:
case DXGI_FORMAT.DXGI_FORMAT_BC3_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_BC3_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_BC3_UNORM_SRGB:
bitsPerPixel = 8;
isBlockCompression = true;
break;
case DXGI_FORMAT.DXGI_FORMAT_BC4_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_BC4_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_BC4_SNORM:
bitsPerPixel = 4;
isBlockCompression = true;
break;
case DXGI_FORMAT.DXGI_FORMAT_BC5_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_BC5_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_BC5_SNORM:
bitsPerPixel = 8;
isBlockCompression = true;
break;
case DXGI_FORMAT.DXGI_FORMAT_B5G6R5_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_B5G5R5A1_UNORM:
bitsPerPixel = 16;
break;
case DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM_SRGB:
case DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM_SRGB:
bitsPerPixel = 32;
break;
case DXGI_FORMAT.DXGI_FORMAT_BC6H_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_BC6H_UF16:
case DXGI_FORMAT.DXGI_FORMAT_BC6H_SF16:
case DXGI_FORMAT.DXGI_FORMAT_BC7_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_BC7_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_BC7_UNORM_SRGB:
bitsPerPixel = 8;
isBlockCompression = true;
break;
case DXGI_FORMAT.DXGI_FORMAT_AYUV:
case DXGI_FORMAT.DXGI_FORMAT_Y410:
case DXGI_FORMAT.DXGI_FORMAT_Y416:
case DXGI_FORMAT.DXGI_FORMAT_NV12:
case DXGI_FORMAT.DXGI_FORMAT_P010:
case DXGI_FORMAT.DXGI_FORMAT_P016:
case DXGI_FORMAT.DXGI_FORMAT_420_OPAQUE:
case DXGI_FORMAT.DXGI_FORMAT_YUY2:
case DXGI_FORMAT.DXGI_FORMAT_Y210:
case DXGI_FORMAT.DXGI_FORMAT_Y216:
case DXGI_FORMAT.DXGI_FORMAT_NV11:
case DXGI_FORMAT.DXGI_FORMAT_AI44:
case DXGI_FORMAT.DXGI_FORMAT_IA44:
case DXGI_FORMAT.DXGI_FORMAT_P8:
case DXGI_FORMAT.DXGI_FORMAT_A8P8:
throw new NotSupportedException();
case DXGI_FORMAT.DXGI_FORMAT_B4G4R4A4_UNORM:
bitsPerPixel = 16;
break;
default:
throw new NotSupportedException();
}
var pitch = isBlockCompression
? Math.Max(1, (width + 3) / 4) * 2 * bitsPerPixel
: ((width * bitsPerPixel) + 7) / 8;
return new(width, height, pitch, format);
}
/// <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, width * 4, (int)DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM);
/// <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, width * 4, (int)DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM);
}

View file

@ -320,7 +320,7 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA
var disposeDeferred =
this.scopedFinalizer.Add(image)
?? throw new InvalidOperationException("Something went wrong very badly");
return new DisposeSuppressingDalamudTextureWrap(disposeDeferred);
return new DisposeSuppressingTextureWrap(disposeDeferred);
}
catch (Exception e)
{
@ -342,26 +342,4 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA
return Task.FromException<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
}
}
}