This commit is contained in:
Soreepeong 2024-03-02 23:43:47 +09:00
parent 0aa75306d4
commit 3415df5d40
32 changed files with 1718 additions and 1368 deletions

View file

@ -1,478 +0,0 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Interface.Internal.SharedImmediateTextures;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Lumina.Data;
using Lumina.Data.Files;
using TerraFX.Interop.DirectX;
using TerraFX.Interop.Windows;
using static TerraFX.Interop.Windows.Windows;
namespace Dalamud.Interface.Internal;
/// <summary>Service responsible for loading and disposing ImGui texture wraps.</summary>
internal sealed partial class TextureManager
{
private ComPtr<IWICImagingFactory> wicFactory;
/// <inheritdoc/>
[SuppressMessage(
"StyleCop.CSharp.LayoutRules",
"SA1519:Braces should not be omitted from multi-line child statement",
Justification = "Multiple fixed blocks")]
public Task SaveAsImageFormatToStreamAsync(
IDalamudTextureWrap wrap,
string extension,
Stream stream,
bool leaveOpen = false,
IReadOnlyDictionary<string, object>? props = null,
CancellationToken cancellationToken = default)
{
var container = GUID.GUID_ContainerFormatPng;
foreach (var (k, v) in this.GetSupportedContainerFormats(WICComponentType.WICEncoder))
{
if (v.Contains(extension, StringComparer.InvariantCultureIgnoreCase))
container = k;
}
return this.SaveToStreamUsingWicAsync(
wrap,
container,
pbag =>
{
if (props is null)
return;
unsafe
{
var nprop = 0u;
pbag.Get()->CountProperties(&nprop).ThrowOnError();
for (var i = 0u; i < nprop; i++)
{
var pbag2 = default(PROPBAG2);
var npropread = 0u;
pbag.Get()->GetPropertyInfo(i, 1, &pbag2, &npropread).ThrowOnError();
if (npropread == 0)
continue;
var propName = new string((char*)pbag2.pstrName);
if (props.TryGetValue(propName, out var untypedValue))
{
VARIANT val;
VariantInit(&val);
switch (untypedValue)
{
case null:
val.vt = (ushort)VARENUM.VT_EMPTY;
break;
case bool value:
val.vt = (ushort)VARENUM.VT_BOOL;
val.boolVal = (short)(value ? 1 : 0);
break;
case byte value:
val.vt = (ushort)VARENUM.VT_UI1;
val.bVal = value;
break;
case ushort value:
val.vt = (ushort)VARENUM.VT_UI2;
val.uiVal = value;
break;
case uint value:
val.vt = (ushort)VARENUM.VT_UI4;
val.uintVal = value;
break;
case ulong value:
val.vt = (ushort)VARENUM.VT_UI8;
val.ullVal = value;
break;
case sbyte value:
val.vt = (ushort)VARENUM.VT_I1;
val.cVal = value;
break;
case short value:
val.vt = (ushort)VARENUM.VT_I2;
val.iVal = value;
break;
case int value:
val.vt = (ushort)VARENUM.VT_I4;
val.intVal = value;
break;
case long value:
val.vt = (ushort)VARENUM.VT_I8;
val.llVal = value;
break;
case float value:
val.vt = (ushort)VARENUM.VT_R4;
val.fltVal = value;
break;
case double value:
val.vt = (ushort)VARENUM.VT_R8;
val.dblVal = value;
break;
default:
VariantClear(&val);
continue;
}
VariantChangeType(&val, &val, 0, pbag2.vt).ThrowOnError();
pbag.Get()->Write(1, &pbag2, &val).ThrowOnError();
VariantClear(&val);
}
CoTaskMemFree(pbag2.pstrName);
}
}
},
stream,
leaveOpen,
cancellationToken);
}
/// <inheritdoc/>
public IEnumerable<string[]> GetLoadSupportedImageExtensions() =>
this.GetSupportedContainerFormats(WICComponentType.WICDecoder).Values;
/// <inheritdoc/>
public IEnumerable<string[]> GetSaveSupportedImageExtensions() =>
this.GetSupportedContainerFormats(WICComponentType.WICEncoder).Values;
/// <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 unsafe IDalamudTextureWrap NoThrottleCreateFromImage(
ReadOnlyMemory<byte> bytes,
CancellationToken cancellationToken = default)
{
ObjectDisposedException.ThrowIf(this.disposing, this);
cancellationToken.ThrowIfCancellationRequested();
if (TexFileExtensions.IsPossiblyTexFile2D(bytes.Span))
{
var bytesArray = bytes.ToArray();
var tf = new TexFile();
typeof(TexFile).GetProperty(nameof(tf.Data))!.GetSetMethod(true)!.Invoke(
tf,
new object?[] { bytesArray });
typeof(TexFile).GetProperty(nameof(tf.Reader))!.GetSetMethod(true)!.Invoke(
tf,
new object?[] { new LuminaBinaryReader(bytesArray) });
// Note: FileInfo and FilePath are not used from TexFile; skip it.
try
{
return this.NoThrottleCreateFromTexFile(tf);
}
catch (Exception)
{
// ignore
}
}
fixed (byte* p = bytes.Span)
{
using var wicStream = default(ComPtr<IWICStream>);
this.wicFactory.Get()->CreateStream(wicStream.GetAddressOf()).ThrowOnError();
wicStream.Get()->InitializeFromMemory(p, checked((uint)bytes.Length)).ThrowOnError();
return this.NoThrottleCreateFromWicStream((IStream*)wicStream.Get(), cancellationToken);
}
}
/// <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
{
unsafe
{
fixed (char* pPath = path)
{
using var wicStream = default(ComPtr<IWICStream>);
this.wicFactory.Get()->CreateStream(wicStream.GetAddressOf()).ThrowOnError();
wicStream.Get()->InitializeFromFilename((ushort*)pPath, GENERIC_READ).ThrowOnError();
return this.NoThrottleCreateFromWicStream((IStream*)wicStream.Get(), cancellationToken);
}
}
}
catch
{
try
{
await using var fp = File.OpenRead(path);
if (fp.Length >= Unsafe.SizeOf<TexFile.TexHeader>())
{
var bytesArray = new byte[fp.Length];
await fp.ReadExactlyAsync(bytesArray, cancellationToken);
if (TexFileExtensions.IsPossiblyTexFile2D(bytesArray))
{
var tf = new TexFile();
typeof(TexFile).GetProperty(nameof(tf.Data))!.GetSetMethod(true)!.Invoke(
tf,
new object?[] { bytesArray });
typeof(TexFile).GetProperty(nameof(tf.Reader))!.GetSetMethod(true)!.Invoke(
tf,
new object?[] { new LuminaBinaryReader(bytesArray) });
// Note: FileInfo and FilePath are not used from TexFile; skip it.
return this.NoThrottleCreateFromTexFile(tf);
}
}
}
catch (Exception)
{
// ignore
}
throw;
}
}
/// <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>
private static DXGI_FORMAT GetCorrespondingDxgiFormat(Guid fmt) => 0 switch
{
// See https://github.com/microsoft/DirectXTex/wiki/WIC-I-O-Functions#savetowicmemory-savetowicfile
_ when fmt == GUID.GUID_WICPixelFormat128bppRGBAFloat => DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_FLOAT,
_ when fmt == GUID.GUID_WICPixelFormat64bppRGBAHalf => DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_FLOAT,
_ when fmt == GUID.GUID_WICPixelFormat64bppRGBA => DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_UNORM,
_ when fmt == GUID.GUID_WICPixelFormat32bppRGBA1010102XR => DXGI_FORMAT.DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM,
_ when fmt == GUID.GUID_WICPixelFormat32bppRGBA1010102 => DXGI_FORMAT.DXGI_FORMAT_R10G10B10A2_UNORM,
_ when fmt == GUID.GUID_WICPixelFormat16bppBGRA5551 => DXGI_FORMAT.DXGI_FORMAT_B5G5R5A1_UNORM,
_ when fmt == GUID.GUID_WICPixelFormat16bppBGR565 => DXGI_FORMAT.DXGI_FORMAT_B5G6R5_UNORM,
_ when fmt == GUID.GUID_WICPixelFormat32bppGrayFloat => DXGI_FORMAT.DXGI_FORMAT_R32_FLOAT,
_ when fmt == GUID.GUID_WICPixelFormat16bppGrayHalf => DXGI_FORMAT.DXGI_FORMAT_R16_FLOAT,
_ when fmt == GUID.GUID_WICPixelFormat16bppGray => DXGI_FORMAT.DXGI_FORMAT_R16_UNORM,
_ when fmt == GUID.GUID_WICPixelFormat8bppGray => DXGI_FORMAT.DXGI_FORMAT_R8_UNORM,
_ when fmt == GUID.GUID_WICPixelFormat8bppAlpha => DXGI_FORMAT.DXGI_FORMAT_A8_UNORM,
_ when fmt == GUID.GUID_WICPixelFormat32bppRGBA => DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM,
_ when fmt == GUID.GUID_WICPixelFormat32bppBGRA => DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM,
_ when fmt == GUID.GUID_WICPixelFormat32bppBGR => DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM,
_ => DXGI_FORMAT.DXGI_FORMAT_UNKNOWN,
};
private unsafe IDalamudTextureWrap NoThrottleCreateFromWicStream(
IStream* wicStream,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
using var decoder = default(ComPtr<IWICBitmapDecoder>);
this.wicFactory.Get()->CreateDecoderFromStream(
wicStream,
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.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.NoThrottleCreateFromRaw(
new RawImageSpecification(rcLock.Width, rcLock.Height, (int)stride, (int)dxgiFormat),
new(pbData, (int)cbBufferSize));
}
[SuppressMessage(
"StyleCop.CSharp.LayoutRules",
"SA1519:Braces should not be omitted from multi-line child statement",
Justification = "Multiple fixed blocks")]
private unsafe Dictionary<Guid, string[]> GetSupportedContainerFormats(WICComponentType componentType)
{
var result = new Dictionary<Guid, string[]>();
using var enumUnknown = default(ComPtr<IEnumUnknown>);
this.wicFactory.Get()->CreateComponentEnumerator(
(uint)componentType,
(uint)WICComponentEnumerateOptions.WICComponentEnumerateDefault,
enumUnknown.GetAddressOf()).ThrowOnError();
while (true)
{
using var entry = default(ComPtr<IUnknown>);
var fetched = 0u;
enumUnknown.Get()->Next(1, entry.GetAddressOf(), &fetched).ThrowOnError();
if (fetched == 0)
break;
using var codecInfo = default(ComPtr<IWICBitmapCodecInfo>);
if (entry.As(&codecInfo).FAILED)
continue;
Guid containerFormat;
if (codecInfo.Get()->GetContainerFormat(&containerFormat).FAILED)
continue;
var cch = 0u;
_ = codecInfo.Get()->GetFileExtensions(0, null, &cch);
var buf = new char[(int)cch + 1];
fixed (char* pBuf = buf)
{
if (codecInfo.Get()->GetFileExtensions(cch + 1, (ushort*)pBuf, &cch).FAILED)
continue;
}
result.Add(containerFormat, new string(buf, 0, buf.IndexOf('\0')).Split(","));
}
return result;
}
[SuppressMessage(
"StyleCop.CSharp.LayoutRules",
"SA1519:Braces should not be omitted from multi-line child statement",
Justification = "Multiple fixed blocks")]
private async Task SaveToStreamUsingWicAsync(
IDalamudTextureWrap wrap,
Guid containerFormat,
Action<ComPtr<IPropertyBag2>> propertyBackSetterDelegate,
Stream stream,
bool leaveOpen = false,
CancellationToken cancellationToken = default)
{
using var wrapCopy = wrap.CreateWrapSharingLowLevelResource();
await using var streamCloser = leaveOpen ? null : stream;
var (specs, bytes) = await this.GetRawDataAsync(
wrapCopy,
Vector2.Zero,
Vector2.One,
DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM,
cancellationToken).ConfigureAwait(false);
using var encoder = default(ComPtr<IWICBitmapEncoder>);
using var encoderFrame = default(ComPtr<IWICBitmapFrameEncode>);
using var wrappedStream = new ManagedIStream(stream);
var guidPixelFormat = GUID.GUID_WICPixelFormat32bppBGRA;
unsafe
{
this.wicFactory.Get()->CreateEncoder(&containerFormat, null, encoder.GetAddressOf()).ThrowOnError();
cancellationToken.ThrowIfCancellationRequested();
encoder.Get()->Initialize(wrappedStream, WICBitmapEncoderCacheOption.WICBitmapEncoderNoCache)
.ThrowOnError();
cancellationToken.ThrowIfCancellationRequested();
using var propertyBag = default(ComPtr<IPropertyBag2>);
encoder.Get()->CreateNewFrame(encoderFrame.GetAddressOf(), propertyBag.GetAddressOf()).ThrowOnError();
cancellationToken.ThrowIfCancellationRequested();
propertyBackSetterDelegate.Invoke(propertyBag);
encoderFrame.Get()->Initialize(propertyBag).ThrowOnError();
cancellationToken.ThrowIfCancellationRequested();
encoderFrame.Get()->SetPixelFormat(&guidPixelFormat).ThrowOnError();
encoderFrame.Get()->SetSize(checked((uint)specs.Width), checked((uint)specs.Height)).ThrowOnError();
using var tempBitmap = default(ComPtr<IWICBitmap>);
fixed (Guid* pGuid = &GUID.GUID_WICPixelFormat32bppBGRA)
fixed (byte* pBytes = bytes)
{
this.wicFactory.Get()->CreateBitmapFromMemory(
(uint)specs.Width,
(uint)specs.Height,
pGuid,
(uint)specs.Pitch,
checked((uint)bytes.Length),
pBytes,
tempBitmap.GetAddressOf()).ThrowOnError();
}
using var tempBitmap2 = default(ComPtr<IWICBitmapSource>);
WICConvertBitmapSource(
&guidPixelFormat,
(IWICBitmapSource*)tempBitmap.Get(),
tempBitmap2.GetAddressOf()).ThrowOnError();
encoderFrame.Get()->SetSize(checked((uint)specs.Width), checked((uint)specs.Height)).ThrowOnError();
encoderFrame.Get()->WriteSource(tempBitmap2.Get(), null).ThrowOnError();
cancellationToken.ThrowIfCancellationRequested();
encoderFrame.Get()->Commit().ThrowOnError();
cancellationToken.ThrowIfCancellationRequested();
encoder.Get()->Commit().ThrowOnError();
}
}
}

View file

@ -1,582 +0,0 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using BitFaster.Caching.Lru;
using Dalamud.Data;
using Dalamud.Game;
using Dalamud.Interface.Internal.SharedImmediateTextures;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Lumina.Data.Files;
using SharpDX;
using SharpDX.Direct3D;
using SharpDX.Direct3D11;
using SharpDX.DXGI;
using TerraFX.Interop.DirectX;
using TerraFX.Interop.Windows;
using static TerraFX.Interop.Windows.Windows;
namespace Dalamud.Interface.Internal;
/// <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 sealed partial class TextureManager : IServiceType, IDisposable, ITextureProvider, ITextureSubstitutionProvider
{
private const int PathLookupLruCount = 8192;
private const string IconFileFormat = "ui/icon/{0:D3}000/{1}{2:D6}.tex";
private const string HighResolutionIconFileFormat = "ui/icon/{0:D3}000/{1}{2:D6}_hr1.tex";
private static readonly ModuleLog Log = new(nameof(TextureManager));
[ServiceManager.ServiceDependency]
private readonly Dalamud dalamud = Service<Dalamud>.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();
[ServiceManager.ServiceDependency]
private readonly TextureLoadThrottler textureLoadThrottler = Service<TextureLoadThrottler>.Get();
private readonly ConcurrentLru<GameIconLookup, string> lookupToPath = new(PathLookupLruCount);
private readonly ConcurrentDictionary<string, SharedImmediateTexture> gamePathTextures = new();
private readonly ConcurrentDictionary<string, SharedImmediateTexture> fileSystemTextures = new();
private readonly ConcurrentDictionary<(Assembly Assembly, string Name), SharedImmediateTexture>
manifestResourceTextures = new();
private readonly HashSet<SharedImmediateTexture> invalidatedTextures = new();
private bool disposing;
[SuppressMessage(
"StyleCop.CSharp.LayoutRules",
"SA1519:Braces should not be omitted from multi-line child statement",
Justification = "Multiple fixed blocks")]
[ServiceManager.ServiceConstructor]
private TextureManager()
{
this.framework.Update += this.FrameworkOnUpdate;
unsafe
{
fixed (Guid* pclsidWicImagingFactory = &CLSID.CLSID_WICImagingFactory)
fixed (Guid* piidWicImagingFactory = &IID.IID_IWICImagingFactory)
{
CoCreateInstance(
pclsidWicImagingFactory,
null,
(uint)CLSCTX.CLSCTX_INPROC_SERVER,
piidWicImagingFactory,
(void**)this.wicFactory.GetAddressOf()).ThrowOnError();
}
}
}
/// <summary>Finalizes an instance of the <see cref="TextureManager"/> class.</summary>
~TextureManager() => this.Dispose();
/// <inheritdoc/>
public event ITextureSubstitutionProvider.TextureDataInterceptorDelegate? InterceptTexDataLoad;
/// <summary>Gets all the loaded textures from game resources.</summary>
public ICollection<SharedImmediateTexture> GamePathTexturesForDebug => this.gamePathTextures.Values;
/// <summary>Gets all the loaded textures from filesystem.</summary>
public ICollection<SharedImmediateTexture> FileSystemTexturesForDebug => this.fileSystemTextures.Values;
/// <summary>Gets all the loaded textures from assembly manifest resources.</summary>
public ICollection<SharedImmediateTexture> ManifestResourceTexturesForDebug => this.manifestResourceTextures.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> InvalidatedTexturesForDebug => this.invalidatedTextures;
/// <inheritdoc/>
public void Dispose()
{
if (this.disposing)
return;
this.disposing = true;
ReleaseSelfReferences(this.gamePathTextures);
ReleaseSelfReferences(this.fileSystemTextures);
ReleaseSelfReferences(this.manifestResourceTextures);
this.lookupToPath.Clear();
this.drawsOneSquare?.Dispose();
this.drawsOneSquare = null;
this.wicFactory.Reset();
return;
static void ReleaseSelfReferences<T>(ConcurrentDictionary<T, SharedImmediateTexture> dict)
{
foreach (var v in dict.Values)
v.ReleaseSelfReference(true);
dict.Clear();
}
}
#region API9 compat
#pragma warning disable CS0618 // Type or member is obsolete
/// <inheritdoc/>
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
[Obsolete("See interface definition.")]
string? ITextureProvider.GetIconPath(uint iconId, ITextureProvider.IconFlags flags, ClientLanguage? language)
=> this.TryGetIconPath(
new(
iconId,
(flags & ITextureProvider.IconFlags.ItemHighQuality) != 0,
(flags & ITextureProvider.IconFlags.HiRes) != 0,
language),
out var path)
? path
: null;
/// <inheritdoc/>
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
[Obsolete("See interface definition.")]
IDalamudTextureWrap? ITextureProvider.GetIcon(
uint iconId,
ITextureProvider.IconFlags flags,
ClientLanguage? language,
bool keepAlive) =>
this.GetFromGameIcon(
new(
iconId,
(flags & ITextureProvider.IconFlags.ItemHighQuality) != 0,
(flags & ITextureProvider.IconFlags.HiRes) != 0,
language))
.GetAvailableOnAccessWrapForApi9();
/// <inheritdoc/>
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
[Obsolete("See interface definition.")]
IDalamudTextureWrap? ITextureProvider.GetTextureFromGame(string path, bool keepAlive) =>
this.GetFromGame(path).GetAvailableOnAccessWrapForApi9();
/// <inheritdoc/>
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
[Obsolete("See interface definition.")]
IDalamudTextureWrap? ITextureProvider.GetTextureFromFile(FileInfo file, bool keepAlive) =>
this.GetFromFile(file.FullName).GetAvailableOnAccessWrapForApi9();
#pragma warning restore CS0618 // Type or member is obsolete
#endregion
/// <inheritdoc cref="ITextureProvider.GetFromGameIcon"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public SharedImmediateTexture GetFromGameIcon(in GameIconLookup lookup) =>
this.GetFromGame(this.lookupToPath.GetOrAdd(lookup, this.GetIconPathByValue));
/// <inheritdoc cref="ITextureProvider.GetFromGame"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public SharedImmediateTexture GetFromGame(string path)
{
ObjectDisposedException.ThrowIf(this.disposing, this);
return this.gamePathTextures.GetOrAdd(path, GamePathSharedImmediateTexture.CreatePlaceholder);
}
/// <inheritdoc cref="ITextureProvider.GetFromFile"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public SharedImmediateTexture GetFromFile(string path)
{
ObjectDisposedException.ThrowIf(this.disposing, this);
return this.fileSystemTextures.GetOrAdd(path, FileSystemSharedImmediateTexture.CreatePlaceholder);
}
/// <inheritdoc cref="ITextureProvider.GetFromFile"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public SharedImmediateTexture GetFromManifestResource(Assembly assembly, string name)
{
ObjectDisposedException.ThrowIf(this.disposing, this);
return this.manifestResourceTextures.GetOrAdd(
(assembly, name),
ManifestResourceSharedImmediateTexture.CreatePlaceholder);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
ISharedImmediateTexture ITextureProvider.GetFromGameIcon(in GameIconLookup lookup) => this.GetFromGameIcon(lookup);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
ISharedImmediateTexture ITextureProvider.GetFromGame(string path) => this.GetFromGame(path);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
ISharedImmediateTexture ITextureProvider.GetFromFile(string path) => this.GetFromFile(path);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
ISharedImmediateTexture ITextureProvider.GetFromManifestResource(Assembly assembly, string name) =>
this.GetFromManifestResource(assembly, name);
/// <inheritdoc/>
public Task<IDalamudTextureWrap> CreateFromImageAsync(
ReadOnlyMemory<byte> bytes,
CancellationToken cancellationToken = default) =>
this.textureLoadThrottler.LoadTextureAsync(
new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(),
ct => Task.Run(() => this.NoThrottleCreateFromImage(bytes.ToArray(), ct), ct),
cancellationToken);
/// <inheritdoc/>
public Task<IDalamudTextureWrap> CreateFromImageAsync(
Stream stream,
bool leaveOpen = false,
CancellationToken cancellationToken = default) =>
this.textureLoadThrottler.LoadTextureAsync(
new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(),
async ct =>
{
await using var ms = stream.CanSeek ? new MemoryStream((int)stream.Length) : new();
await stream.CopyToAsync(ms, ct).ConfigureAwait(false);
return this.NoThrottleCreateFromImage(ms.GetBuffer(), ct);
},
cancellationToken)
.ContinueWith(
r =>
{
if (!leaveOpen)
stream.Dispose();
return r;
},
default(CancellationToken))
.Unwrap();
/// <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) => this.NoThrottleCreateFromRaw(specs, bytes);
/// <inheritdoc/>
public Task<IDalamudTextureWrap> CreateFromRawAsync(
RawImageSpecification specs,
ReadOnlyMemory<byte> bytes,
CancellationToken cancellationToken = default) =>
this.textureLoadThrottler.LoadTextureAsync(
new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(),
_ => Task.FromResult(this.NoThrottleCreateFromRaw(specs, bytes.Span)),
cancellationToken);
/// <inheritdoc/>
public Task<IDalamudTextureWrap> CreateFromRawAsync(
RawImageSpecification specs,
Stream stream,
bool leaveOpen = false,
CancellationToken cancellationToken = default) =>
this.textureLoadThrottler.LoadTextureAsync(
new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(),
async ct =>
{
await using var ms = stream.CanSeek ? new MemoryStream((int)stream.Length) : new();
await stream.CopyToAsync(ms, ct).ConfigureAwait(false);
return this.NoThrottleCreateFromRaw(specs, ms.GetBuffer().AsSpan(0, (int)ms.Length));
},
cancellationToken)
.ContinueWith(
r =>
{
if (!leaveOpen)
stream.Dispose();
return r;
},
default(CancellationToken))
.Unwrap();
/// <inheritdoc/>
public IDalamudTextureWrap CreateFromTexFile(TexFile file) => this.CreateFromTexFileAsync(file).Result;
/// <inheritdoc/>
public Task<IDalamudTextureWrap> CreateFromTexFileAsync(
TexFile file,
CancellationToken cancellationToken = default) =>
this.textureLoadThrottler.LoadTextureAsync(
new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(),
ct => Task.Run(() => this.NoThrottleCreateFromTexFile(file), ct),
cancellationToken);
/// <inheritdoc/>
bool ITextureProvider.IsDxgiFormatSupported(int dxgiFormat) =>
this.IsDxgiFormatSupported((DXGI_FORMAT)dxgiFormat);
/// <inheritdoc cref="ITextureProvider.IsDxgiFormatSupported"/>
public bool IsDxgiFormatSupported(DXGI_FORMAT dxgiFormat)
{
if (this.interfaceManager.Scene is not { } scene)
{
_ = Service<InterfaceManager.InterfaceManagerWithScene>.Get();
scene = this.interfaceManager.Scene ?? throw new InvalidOperationException();
}
var format = (Format)dxgiFormat;
var support = scene.Device.CheckFormatSupport(format);
const FormatSupport required = FormatSupport.Texture2D;
return (support & required) == required;
}
/// <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)
{
if (this.gamePathTextures.TryRemove(path, out var r))
{
if (r.ReleaseSelfReference(true) != 0 || r.HasRevivalPossibility)
{
lock (this.invalidatedTextures)
this.invalidatedTextures.Add(r);
}
}
if (this.fileSystemTextures.TryRemove(path, out r))
{
if (r.ReleaseSelfReference(true) != 0 || r.HasRevivalPossibility)
{
lock (this.invalidatedTextures)
this.invalidatedTextures.Add(r);
}
}
}
}
/// <inheritdoc cref="ITextureProvider.CreateFromRaw"/>
internal IDalamudTextureWrap NoThrottleCreateFromRaw(
RawImageSpecification specs,
ReadOnlySpan<byte> bytes)
{
if (this.interfaceManager.Scene is not { } scene)
{
_ = Service<InterfaceManager.InterfaceManagerWithScene>.Get();
scene = this.interfaceManager.Scene ?? throw new InvalidOperationException();
}
ShaderResourceView resView;
unsafe
{
fixed (void* pData = bytes)
{
var texDesc = new Texture2DDescription
{
Width = specs.Width,
Height = specs.Height,
MipLevels = 1,
ArraySize = 1,
Format = (Format)specs.DxgiFormat,
SampleDescription = new(1, 0),
Usage = ResourceUsage.Immutable,
BindFlags = BindFlags.ShaderResource,
CpuAccessFlags = CpuAccessFlags.None,
OptionFlags = ResourceOptionFlags.None,
};
using var texture = new Texture2D(scene.Device, texDesc, new DataRectangle(new(pData), specs.Pitch));
resView = new(
scene.Device,
texture,
new()
{
Format = texDesc.Format,
Dimension = ShaderResourceViewDimension.Texture2D,
Texture2D = { MipLevels = texDesc.MipLevels },
});
}
}
// no sampler for now because the ImGui implementation we copied doesn't allow for changing it
return new DalamudTextureWrap(new D3DTextureWrap(resView, specs.Width, specs.Height));
}
/// <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)Format.B8G8R8A8_UNorm;
buffer = buffer.Filter(0, 0, TexFile.TextureFormat.B8G8R8A8);
}
return this.NoThrottleCreateFromRaw(
RawImageSpecification.From(buffer.Width, buffer.Height, dxgiFormat),
buffer.RawData);
}
private static string FormatIconPath(uint iconId, string? type, bool highResolution)
{
var format = highResolution ? HighResolutionIconFileFormat : IconFileFormat;
type ??= string.Empty;
if (type.Length > 0 && !type.EndsWith("/"))
type += "/";
return string.Format(format, iconId / 1000, type, iconId);
}
private void FrameworkOnUpdate(IFramework unused)
{
RemoveFinalReleased(this.gamePathTextures);
RemoveFinalReleased(this.fileSystemTextures);
RemoveFinalReleased(this.manifestResourceTextures);
// ReSharper disable once InconsistentlySynchronizedField
if (this.invalidatedTextures.Count != 0)
{
lock (this.invalidatedTextures)
this.invalidatedTextures.RemoveWhere(TextureFinalReleasePredicate);
}
return;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static void RemoveFinalReleased<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;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private string GetIconPathByValue(GameIconLookup lookup) =>
this.TryGetIconPath(lookup, out var path) ? path : throw new FileNotFoundException();
}

View file

@ -3,6 +3,7 @@ using System.Numerics;
using System.Threading.Tasks;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Textures.Internal;
using Dalamud.Interface.Utility;
using ImGuiNET;
@ -141,7 +142,7 @@ public class IconBrowserWidget : IDataWindowWidget
var texm = Service<TextureManager>.Get();
var cursor = ImGui.GetCursorScreenPos();
if (texm.GetFromGameIcon(new((uint)iconId)).TryGetWrap(out var texture, out var exc))
if (texm.Shared.GetFromGameIcon(new((uint)iconId)).TryGetWrap(out var texture, out var exc))
{
ImGui.Image(texture.ImGuiHandle, this.iconSize);

View file

@ -9,7 +9,8 @@ using System.Threading.Tasks;
using Dalamud.Interface.Components;
using Dalamud.Interface.ImGuiFileDialog;
using Dalamud.Interface.Internal.Notifications;
using Dalamud.Interface.Internal.SharedImmediateTextures;
using Dalamud.Interface.Textures.Internal;
using Dalamud.Interface.Textures.Internal.SharedImmediateTextures;
using Dalamud.Interface.Utility;
using Dalamud.Plugin.Services;
using Dalamud.Storage.Assets;
@ -21,6 +22,8 @@ using Serilog;
using TerraFX.Interop.DirectX;
using TextureManager = Dalamud.Interface.Textures.Internal.TextureManager;
namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
/// <summary>
@ -92,29 +95,29 @@ internal class TexWidget : IDataWindowWidget
ImGui.PushID("loadedGameTextures");
if (ImGui.CollapsingHeader(
$"Loaded Game Textures: {this.textureManager.GamePathTexturesForDebug.Count:g}###header"))
this.DrawLoadedTextures(this.textureManager.GamePathTexturesForDebug);
$"Loaded Game Textures: {this.textureManager.Shared.ForDebugGamePathTextures.Count:g}###header"))
this.DrawLoadedTextures(this.textureManager.Shared.ForDebugGamePathTextures);
ImGui.PopID();
ImGui.PushID("loadedFileTextures");
if (ImGui.CollapsingHeader(
$"Loaded File Textures: {this.textureManager.FileSystemTexturesForDebug.Count:g}###header"))
this.DrawLoadedTextures(this.textureManager.FileSystemTexturesForDebug);
$"Loaded File Textures: {this.textureManager.Shared.ForDebugFileSystemTextures.Count:g}###header"))
this.DrawLoadedTextures(this.textureManager.Shared.ForDebugFileSystemTextures);
ImGui.PopID();
ImGui.PushID("loadedManifestResourceTextures");
if (ImGui.CollapsingHeader(
$"Loaded Manifest Resource Textures: {this.textureManager.ManifestResourceTexturesForDebug.Count:g}###header"))
this.DrawLoadedTextures(this.textureManager.ManifestResourceTexturesForDebug);
$"Loaded Manifest Resource Textures: {this.textureManager.Shared.ForDebugManifestResourceTextures.Count:g}###header"))
this.DrawLoadedTextures(this.textureManager.Shared.ForDebugManifestResourceTextures);
ImGui.PopID();
lock (this.textureManager.InvalidatedTexturesForDebug)
lock (this.textureManager.Shared.ForDebugInvalidatedTextures)
{
ImGui.PushID("invalidatedTextures");
if (ImGui.CollapsingHeader(
$"Invalidated: {this.textureManager.InvalidatedTexturesForDebug.Count:g}###header"))
$"Invalidated: {this.textureManager.Shared.ForDebugInvalidatedTextures.Count:g}###header"))
{
this.DrawLoadedTextures(this.textureManager.InvalidatedTexturesForDebug);
this.DrawLoadedTextures(this.textureManager.Shared.ForDebugInvalidatedTextures);
}
ImGui.PopID();
@ -192,20 +195,9 @@ internal class TexWidget : IDataWindowWidget
ImGui.SameLine();
if (ImGui.Button("Save"))
{
this.fileDialogManager.SaveFileDialog(
"Save texture...",
string.Join(
',',
this.textureManager
.GetSaveSupportedImageExtensions()
.Select(x => $"{string.Join(" | ", x)}{{{string.Join(',', x)}}}")),
$"Texture {t.Id}.png",
".png",
(ok, path) =>
{
if (ok && t.GetTexture(this.textureManager) is { } source)
Task.Run(() => this.SaveTextureWrap(source, path));
});
this.SaveTextureAsync(
$"Texture {t.Id}",
() => t.CreateNewTextureWrapReference(this.textureManager));
}
ImGui.SameLine();
@ -244,7 +236,7 @@ internal class TexWidget : IDataWindowWidget
}
else
{
ImGui.TextUnformatted(t.DescribeError());
ImGui.TextUnformatted(t.DescribeError() ?? "Loading");
}
}
catch (Exception e)
@ -343,20 +335,8 @@ internal class TexWidget : IDataWindowWidget
ImGui.TableNextColumn();
if (ImGuiComponents.IconButton(FontAwesomeIcon.Save))
{
this.fileDialogManager.SaveFileDialog(
"Save texture...",
string.Join(
',',
this.textureManager
.GetSaveSupportedImageExtensions()
.Select(x => $"{string.Join(" | ", x)}{{{string.Join(',', x)}}}")),
Path.ChangeExtension(Path.GetFileName(texture.SourcePathForDebug), ".png"),
".png",
(ok, path) =>
{
if (ok)
Task.Run(() => this.SaveImmediateTexture(texture, path));
});
var name = Path.ChangeExtension(Path.GetFileName(texture.SourcePathForDebug), null);
this.SaveTextureAsync(name, () => texture.RentAsync());
}
if (ImGui.IsItemHovered() && texture.GetWrapOrDefault(null) is { } immediate)
@ -394,53 +374,6 @@ internal class TexWidget : IDataWindowWidget
ImGuiHelpers.ScaledDummy(10);
}
private async void SaveImmediateTexture(ISharedImmediateTexture texture, string path)
{
try
{
using var rented = await texture.RentAsync();
this.SaveTextureWrap(rented, path);
}
catch (Exception e)
{
Log.Error(e, $"{nameof(TexWidget)}.{nameof(this.SaveImmediateTexture)}");
Service<NotificationManager>.Get().AddNotification(
$"Failed to save file: {e}",
this.DisplayName,
NotificationType.Error);
}
}
private async void SaveTextureWrap(IDalamudTextureWrap texture, string path)
{
try
{
await this.textureManager.SaveAsImageFormatToStreamAsync(
texture,
Path.GetExtension(path),
File.Create(path),
props: new Dictionary<string, object>
{
["CompressionQuality"] = 1.0f,
["ImageQuality"] = 1.0f,
});
}
catch (Exception e)
{
Log.Error(e, $"{nameof(TexWidget)}.{nameof(this.SaveImmediateTexture)}");
Service<NotificationManager>.Get().AddNotification(
$"Failed to save file: {e}",
this.DisplayName,
NotificationType.Error);
return;
}
Service<NotificationManager>.Get().AddNotification(
$"File saved to: {path}",
this.DisplayName,
NotificationType.Success);
}
private void DrawGetFromGameIcon()
{
ImGui.InputText("Icon ID", ref this.iconId, 32);
@ -464,6 +397,7 @@ internal class TexWidget : IDataWindowWidget
this.addedTextures.Add(
new(
Api10: this.textureManager
.Shared
.GetFromGameIcon(new(uint.Parse(this.iconId), this.hq, this.hiRes))
.RentAsync()));
}
@ -486,7 +420,7 @@ internal class TexWidget : IDataWindowWidget
ImGui.SameLine();
if (ImGui.Button("Load Tex (Async)"))
this.addedTextures.Add(new(Api10: this.textureManager.GetFromGame(this.inputTexPath).RentAsync()));
this.addedTextures.Add(new(Api10: this.textureManager.Shared.GetFromGame(this.inputTexPath).RentAsync()));
ImGui.SameLine();
if (ImGui.Button("Load Tex (Immediate)"))
@ -506,7 +440,7 @@ internal class TexWidget : IDataWindowWidget
ImGui.SameLine();
if (ImGui.Button("Load File (Async)"))
this.addedTextures.Add(new(Api10: this.textureManager.GetFromFile(this.inputFilePath).RentAsync()));
this.addedTextures.Add(new(Api10: this.textureManager.Shared.GetFromFile(this.inputFilePath).RentAsync()));
ImGui.SameLine();
if (ImGui.Button("Load File (Immediate)"))
@ -579,7 +513,7 @@ internal class TexWidget : IDataWindowWidget
if (ImGui.Button("Load File (Async)"))
{
this.addedTextures.Add(
new(Api10: this.textureManager.GetFromManifestResource(assembly, name).RentAsync()));
new(Api10: this.textureManager.Shared.GetFromManifestResource(assembly, name).RentAsync()));
}
ImGui.SameLine();
@ -600,6 +534,100 @@ internal class TexWidget : IDataWindowWidget
ImGuiHelpers.ScaledDummy(10);
}
private async void SaveTextureAsync(string name, Func<Task<IDalamudTextureWrap>> textureGetter)
{
try
{
BitmapCodecInfo encoder;
{
var off = ImGui.GetCursorScreenPos();
var first = true;
var encoders = this.textureManager
.Wic
.GetSupportedEncoderInfos()
.ToList();
var tcs = new TaskCompletionSource<BitmapCodecInfo>();
Service<InterfaceManager>.Get().Draw += DrawChoices;
encoder = await tcs.Task;
void DrawChoices()
{
if (first)
{
ImGui.OpenPopup(nameof(this.SaveTextureAsync));
first = false;
}
ImGui.SetNextWindowPos(off, ImGuiCond.Appearing);
if (!ImGui.BeginPopup(
nameof(this.SaveTextureAsync),
ImGuiWindowFlags.AlwaysAutoResize |
ImGuiWindowFlags.NoTitleBar |
ImGuiWindowFlags.NoSavedSettings))
{
Service<InterfaceManager>.Get().Draw -= DrawChoices;
tcs.TrySetCanceled();
return;
}
foreach (var encoder2 in encoders)
{
if (ImGui.Selectable(encoder2.Name))
tcs.TrySetResult(encoder2);
}
if (tcs.Task.IsCompleted)
ImGui.CloseCurrentPopup();
ImGui.EndPopup();
}
}
string path;
{
var tcs = new TaskCompletionSource<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);
}
using var textureWrap = await textureGetter.Invoke();
await this.textureManager.SaveToFileAsync(
textureWrap,
encoder.ContainerGuid,
path,
props: new Dictionary<string, object>
{
["CompressionQuality"] = 1.0f,
["ImageQuality"] = 1.0f,
});
Service<NotificationManager>.Get().AddNotification(
$"File saved to: {path}",
this.DisplayName,
NotificationType.Success);
}
catch (Exception e)
{
Log.Error(e, $"{nameof(TexWidget)}.{nameof(this.SaveTextureAsync)}");
Service<NotificationManager>.Get().AddNotification(
$"Failed to save file: {e}",
this.DisplayName,
NotificationType.Error);
}
}
private void TextRightAlign(string s)
{
var width = ImGui.CalcTextSize(s).X;
@ -656,7 +684,7 @@ internal class TexWidget : IDataWindowWidget
_ = this.Api10?.ToContentDisposedTask();
}
public string DescribeError()
public string? DescribeError()
{
if (this.SharedResource is not null)
return "Unknown error";
@ -665,7 +693,7 @@ internal class TexWidget : IDataWindowWidget
if (this.Api10 is not null)
{
return !this.Api10.IsCompleted
? "Loading"
? null
: this.Api10.Exception?.ToString() ?? (this.Api10.IsCanceled ? "Canceled" : "Unknown error");
}
@ -704,6 +732,18 @@ internal class TexWidget : IDataWindowWidget
return null;
}
public async Task<IDalamudTextureWrap> CreateNewTextureWrapReference(ITextureProvider tp)
{
while (true)
{
if (this.GetTexture(tp) is { } textureWrap)
return textureWrap.CreateWrapSharingLowLevelResource();
if (this.DescribeError() is { } err)
throw new(err);
await Task.Delay(100);
}
}
public TextureEntry CreateFromSharedLowLevelResource(ITextureProvider tp) =>
new() { SharedResource = this.GetTexture(tp)?.CreateWrapSharingLowLevelResource() };

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;

View file

@ -16,6 +16,7 @@ using Dalamud.Interface.Animation.EasingFunctions;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Components;
using Dalamud.Interface.Internal.Notifications;
using Dalamud.Interface.Textures.Internal;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing;
@ -1745,7 +1746,7 @@ internal class PluginInstallerWindow : Window, IDisposable
if (!this.testerIconPath.IsNullOrEmpty())
{
this.testerIcon = tm.GetFromFile(this.testerIconPath).RentAsync();
this.testerIcon = tm.Shared.GetFromFile(this.testerIconPath).RentAsync();
}
this.testerImages = new Task<IDalamudTextureWrap>?[this.testerImagePaths.Length];
@ -1756,7 +1757,7 @@ internal class PluginInstallerWindow : Window, IDisposable
continue;
_ = this.testerImages[i]?.ToContentDisposedTask();
this.testerImages[i] = tm.GetFromFile(this.testerImagePaths[i]).RentAsync();
this.testerImages[i] = tm.Shared.GetFromFile(this.testerImagePaths[i]).RentAsync();
}
}
catch (Exception ex)

View file

@ -11,6 +11,7 @@ using Dalamud.Game;
using Dalamud.Interface.FontIdentifier;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Textures.Internal;
using Dalamud.Storage.Assets;
using Dalamud.Utility;

View file

@ -2,6 +2,7 @@
using ImGuiScene;
// ReSharper disable once CheckNamespace
namespace Dalamud.Interface.Internal;
/// <summary>

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

@ -1,7 +1,10 @@
using System.Numerics;
using Dalamud.Interface.Textures.Internal;
using TerraFX.Interop.Windows;
// ReSharper disable once CheckNamespace
namespace Dalamud.Interface.Internal;
/// <summary>

View file

@ -5,7 +5,7 @@ using System.Threading.Tasks;
using Dalamud.Interface.Internal;
using Dalamud.Utility;
namespace Dalamud.Interface;
namespace Dalamud.Interface.Textures;
/// <summary>A texture with a backing instance of <see cref="IDalamudTextureWrap"/> that is shared across multiple
/// requesters.</summary>

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

@ -1,4 +1,6 @@
namespace Dalamud.Interface.Internal;
using Dalamud.Interface.Internal;
namespace Dalamud.Interface.Textures.Internal;
/// <summary>
/// A texture wrap that ignores <see cref="IDisposable.Dispose"/> calls.

View file

@ -1,9 +1,10 @@
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Interface.Internal;
using Dalamud.Utility;
namespace Dalamud.Interface.Internal.SharedImmediateTextures;
namespace Dalamud.Interface.Textures.Internal.SharedImmediateTextures;
/// <summary>Represents a sharable texture, based on a file on the system filesystem.</summary>
internal sealed class FileSystemSharedImmediateTexture : SharedImmediateTexture
@ -42,7 +43,7 @@ internal sealed class FileSystemSharedImmediateTexture : SharedImmediateTexture
private async Task<IDalamudTextureWrap> CreateTextureAsync(CancellationToken cancellationToken)
{
var tm = await Service<TextureManager>.GetAsync();
var tm = await Service<Interface.Textures.Internal.TextureManager>.GetAsync();
return await tm.NoThrottleCreateFromFileAsync(this.path, cancellationToken);
}
}

View file

@ -3,11 +3,12 @@ using System.Threading;
using System.Threading.Tasks;
using Dalamud.Data;
using Dalamud.Interface.Internal;
using Dalamud.Utility;
using Lumina.Data.Files;
namespace Dalamud.Interface.Internal.SharedImmediateTextures;
namespace Dalamud.Interface.Textures.Internal.SharedImmediateTextures;
/// <summary>Represents a sharable texture, based on a file in game resources.</summary>
internal sealed class GamePathSharedImmediateTexture : SharedImmediateTexture
@ -46,8 +47,9 @@ internal sealed class GamePathSharedImmediateTexture : SharedImmediateTexture
private async Task<IDalamudTextureWrap> CreateTextureAsync(CancellationToken cancellationToken)
{
var dm = await Service<DataManager>.GetAsync();
var tm = await Service<TextureManager>.GetAsync();
if (dm.GetFile<TexFile>(this.path) is not { } file)
var tm = await Service<Interface.Textures.Internal.TextureManager>.GetAsync();
var substPath = tm.GetSubstitutedPath(this.path);
if (dm.GetFile<TexFile>(substPath) is not { } file)
throw new FileNotFoundException();
cancellationToken.ThrowIfCancellationRequested();
return tm.NoThrottleCreateFromTexFile(file);

View file

@ -3,9 +3,10 @@ using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Interface.Internal;
using Dalamud.Utility;
namespace Dalamud.Interface.Internal.SharedImmediateTextures;
namespace Dalamud.Interface.Textures.Internal.SharedImmediateTextures;
/// <summary>Represents a sharable texture, based on a manifest texture obtained from
/// <see cref="Assembly.GetManifestResourceStream(string)"/>.</summary>
@ -56,7 +57,7 @@ internal sealed class ManifestResourceSharedImmediateTexture : SharedImmediateTe
if (stream is null)
throw new FileNotFoundException("The resource file could not be found.");
var tm = await Service<TextureManager>.GetAsync();
var tm = await Service<Interface.Textures.Internal.TextureManager>.GetAsync();
var ms = new MemoryStream(stream.CanSeek ? (int)stream.Length : 0);
await stream.CopyToAsync(ms, cancellationToken);
return tm.NoThrottleCreateFromImage(ms.GetBuffer().AsMemory(0, (int)ms.Length), cancellationToken);

View file

@ -3,10 +3,11 @@ using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Interface.Internal;
using Dalamud.Storage.Assets;
using Dalamud.Utility;
namespace Dalamud.Interface.Internal.SharedImmediateTextures;
namespace Dalamud.Interface.Textures.Internal.SharedImmediateTextures;
/// <summary>Represents a texture that may have multiple reference holders (owners).</summary>
internal abstract class SharedImmediateTexture

View file

@ -4,7 +4,9 @@ using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
namespace Dalamud.Interface.Internal;
using Dalamud.Interface.Internal;
namespace Dalamud.Interface.Textures.Internal;
/// <summary>
/// Service for managing texture loads.

View file

@ -0,0 +1,55 @@
using System.IO;
using Dalamud.Interface.Internal;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
namespace Dalamud.Interface.Textures.Internal;
#pragma warning disable CS0618 // Type or member is obsolete
/// <summary>Service responsible for loading and disposing ImGui texture wraps.</summary>
internal sealed partial class TextureManager
{
/// <inheritdoc/>
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
[Obsolete("See interface definition.")]
string? ITextureProvider.GetIconPath(uint iconId, ITextureProvider.IconFlags flags, ClientLanguage? language)
=> this.TryGetIconPath(
new(
iconId,
(flags & ITextureProvider.IconFlags.ItemHighQuality) != 0,
(flags & ITextureProvider.IconFlags.HiRes) != 0,
language),
out var path)
? path
: null;
/// <inheritdoc/>
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
[Obsolete("See interface definition.")]
IDalamudTextureWrap? ITextureProvider.GetIcon(
uint iconId,
ITextureProvider.IconFlags flags,
ClientLanguage? language,
bool keepAlive) =>
this.Shared.GetFromGameIcon(
new(
iconId,
(flags & ITextureProvider.IconFlags.ItemHighQuality) != 0,
(flags & ITextureProvider.IconFlags.HiRes) != 0,
language))
.GetAvailableOnAccessWrapForApi9();
/// <inheritdoc/>
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
[Obsolete("See interface definition.")]
IDalamudTextureWrap? ITextureProvider.GetTextureFromGame(string path, bool keepAlive) =>
this.Shared.GetFromGame(path).GetAvailableOnAccessWrapForApi9();
/// <inheritdoc/>
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
[Obsolete("See interface definition.")]
IDalamudTextureWrap? ITextureProvider.GetTextureFromFile(FileInfo file, bool keepAlive) =>
this.Shared.GetFromFile(file.FullName).GetAvailableOnAccessWrapForApi9();
}

View file

@ -4,6 +4,7 @@ using System.Numerics;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Interface.Internal;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
@ -15,7 +16,7 @@ using SharpDX.DXGI;
using TerraFX.Interop.DirectX;
using TerraFX.Interop.Windows;
namespace Dalamud.Interface.Internal;
namespace Dalamud.Interface.Textures.Internal;
/// <summary>Service responsible for loading and disposing ImGui texture wraps.</summary>
internal sealed partial class TextureManager
@ -119,16 +120,16 @@ internal sealed partial class TextureManager
}
/// <inheritdoc/>
Task<(RawImageSpecification Specification, byte[] RawData)> ITextureProvider.GetRawDataAsync(
Task<(RawImageSpecification Specification, byte[] RawData)> ITextureProvider.GetRawDataFromExistingTextureAsync(
IDalamudTextureWrap wrap,
Vector2 uv0,
Vector2 uv1,
int dxgiFormat,
CancellationToken cancellationToken) =>
this.GetRawDataAsync(wrap, uv0, uv1, (DXGI_FORMAT)dxgiFormat, cancellationToken);
this.GetRawDataFromExistingTextureAsync(wrap, uv0, uv1, (DXGI_FORMAT)dxgiFormat, cancellationToken);
/// <inheritdoc cref="ITextureProvider.GetRawDataAsync"/>
public async Task<(RawImageSpecification Specification, byte[] RawData)> GetRawDataAsync(
/// <inheritdoc cref="ITextureProvider.GetRawDataFromExistingTextureAsync"/>
public async Task<(RawImageSpecification Specification, byte[] RawData)> GetRawDataFromExistingTextureAsync(
IDalamudTextureWrap wrap,
Vector2 uv0,
Vector2 uv1,

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,163 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using BitFaster.Caching.Lru;
using Dalamud.Interface.Textures.Internal.SharedImmediateTextures;
using Dalamud.Plugin.Services;
namespace Dalamud.Interface.Textures.Internal;
/// <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 GetFromGameIcon(in GameIconLookup lookup) =>
this.GetFromGame(this.lookupCache.GetOrAdd(lookup, this.GetIconPathByValue));
/// <inheritdoc cref="ITextureProvider.GetFromGame"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public SharedImmediateTexture GetFromGame(string path) =>
this.gameDict.GetOrAdd(path, GamePathSharedImmediateTexture.CreatePlaceholder);
/// <inheritdoc cref="ITextureProvider.GetFromFile"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public SharedImmediateTexture GetFromFile(string path) =>
this.fileDict.GetOrAdd(path, FileSystemSharedImmediateTexture.CreatePlaceholder);
/// <inheritdoc cref="ITextureProvider.GetFromFile"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public SharedImmediateTexture GetFromManifestResource(Assembly assembly, string name) =>
this.manifestResourceDict.GetOrAdd(
(assembly, name),
ManifestResourceSharedImmediateTexture.CreatePlaceholder);
/// <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,527 @@
using System.Buffers;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Numerics;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Textures.Internal.SharedImmediateTextures;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Dalamud.Utility.TerraFxCom;
using TerraFX.Interop.DirectX;
using TerraFX.Interop.Windows;
using static TerraFX.Interop.Windows.Windows;
namespace Dalamud.Interface.Textures.Internal;
/// <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/>
[SuppressMessage(
"StyleCop.CSharp.LayoutRules",
"SA1519:Braces should not be omitted from multi-line child statement",
Justification = "Multiple fixed blocks")]
public async Task SaveToStreamAsync(
IDalamudTextureWrap wrap,
Guid containerGuid,
Stream stream,
bool leaveOpen = false,
IReadOnlyDictionary<string, object>? props = null,
CancellationToken cancellationToken = default)
{
using var istream = ManagedIStream.Create(stream, leaveOpen);
RawImageSpecification specs;
byte[] bytes;
using (var wrapCopy = wrap.CreateWrapSharingLowLevelResource())
{
(specs, bytes) = await this.GetRawDataFromExistingTextureAsync(
wrapCopy,
Vector2.Zero,
Vector2.One,
DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM,
cancellationToken).ConfigureAwait(false);
}
this.Wic.SaveToStreamUsingWic(
specs,
bytes,
containerGuid,
istream,
props,
cancellationToken);
}
/// <inheritdoc/>
public async Task SaveToFileAsync(
IDalamudTextureWrap wrap,
Guid containerGuid,
string path,
IReadOnlyDictionary<string, object>? props = null,
CancellationToken cancellationToken = default)
{
var pathTemp = $"{path}.{GetCurrentThreadId():X08}{Environment.TickCount64:X16}.tmp";
try
{
await this.SaveToStreamAsync(wrap, containerGuid, File.Create(pathTemp), false, props, cancellationToken);
}
catch (Exception e)
{
try
{
if (File.Exists(pathTemp))
File.Delete(pathTemp);
}
catch (Exception e2)
{
throw new AggregateException(
"Failed to save the file, and failed to remove the temporary file.",
e,
e2);
}
throw;
}
try
{
try
{
File.Replace(pathTemp, path, null, true);
}
catch
{
File.Move(pathTemp, path, true);
}
}
catch (Exception e)
{
try
{
if (File.Exists(pathTemp))
File.Delete(pathTemp);
}
catch (Exception e2)
{
throw new AggregateException(
"Failed to move the temporary file to the target path, and failed to remove the temporary file.",
e,
e2);
}
throw;
}
}
/// <inheritdoc/>
IEnumerable<IBitmapCodecInfo> ITextureProvider.GetSupportedImageDecoderInfos() =>
this.Wic.GetSupportedDecoderInfos();
/// <inheritdoc/>
IEnumerable<IBitmapCodecInfo> ITextureProvider.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.CreateIStreamFromMemory(handle, bytes.Length);
return this.Wic.NoThrottleCreateFromWicStream(stream, cancellationToken);
}
catch (Exception e1)
{
try
{
return this.NoThrottleCreateFromTexFile(bytes.Span);
}
catch (Exception e2)
{
throw new AggregateException(e1, e2);
}
}
}
/// <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 = this.Wic.CreateIStreamFromFile(path);
return this.Wic.NoThrottleCreateFromWicStream(stream, cancellationToken);
}
catch (Exception e1)
{
try
{
return this.NoThrottleCreateFromTexFile(await File.ReadAllBytesAsync(path, cancellationToken));
}
catch (Exception e2)
{
throw new AggregateException(e1, e2);
}
}
}
/// <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;
/// <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)
{
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();
/// <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> CreateIStreamFromMemory(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="IStream"/> from a file path.</summary>
/// <param name="path">The file path.</param>
/// <returns>The new instance of <see cref="IStream"/>.</returns>
public unsafe ComPtr<IStream> CreateIStreamFromFile(string path)
{
using var wicStream = default(ComPtr<IWICStream>);
this.wicFactory.Get()->CreateStream(wicStream.GetAddressOf()).ThrowOnError();
fixed (char* pPath = path)
wicStream.Get()->InitializeFromFilename((ushort*)pPath, GENERIC_READ).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)stride, (int)dxgiFormat),
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,
byte[] bytes,
Guid containerFormat,
ComPtr<IStream> stream,
IReadOnlyDictionary<string, object>? props = null,
CancellationToken cancellationToken = default)
{
using var encoder = default(ComPtr<IWICBitmapEncoder>);
using var encoderFrame = default(ComPtr<IWICBitmapFrameEncode>);
var guidPixelFormat = GUID.GUID_WICPixelFormat32bppBGRA;
this.wicFactory.Get()->CreateEncoder(&containerFormat, null, encoder.GetAddressOf()).ThrowOnError();
cancellationToken.ThrowIfCancellationRequested();
encoder.Get()->Initialize(stream, WICBitmapEncoderCacheOption.WICBitmapEncoderNoCache)
.ThrowOnError();
cancellationToken.ThrowIfCancellationRequested();
using var propertyBag = default(ComPtr<IPropertyBag2>);
encoder.Get()->CreateNewFrame(encoderFrame.GetAddressOf(), propertyBag.GetAddressOf()).ThrowOnError();
cancellationToken.ThrowIfCancellationRequested();
if (props is not null)
{
var nprop = 0u;
propertyBag.Get()->CountProperties(&nprop).ThrowOnError();
for (var i = 0u; i < nprop; i++)
{
var pbag2 = default(PROPBAG2);
var npropread = 0u;
propertyBag.Get()->GetPropertyInfo(i, 1, &pbag2, &npropread).ThrowOnError();
if (npropread == 0)
continue;
try
{
var propName = new string((char*)pbag2.pstrName);
if (props.TryGetValue(propName, out var untypedValue))
{
VARIANT val;
// Marshal calls VariantInit.
Marshal.GetNativeVariantForObject(untypedValue, (nint)(&val));
VariantChangeType(&val, &val, 0, pbag2.vt).ThrowOnError();
propertyBag.Get()->Write(1, &pbag2, &val).ThrowOnError();
VariantClear(&val);
}
}
finally
{
CoTaskMemFree(pbag2.pstrName);
}
}
}
encoderFrame.Get()->Initialize(propertyBag).ThrowOnError();
cancellationToken.ThrowIfCancellationRequested();
encoderFrame.Get()->SetPixelFormat(&guidPixelFormat).ThrowOnError();
encoderFrame.Get()->SetSize(checked((uint)specs.Width), checked((uint)specs.Height)).ThrowOnError();
using var tempBitmap = default(ComPtr<IWICBitmap>);
fixed (Guid* pGuid = &GUID.GUID_WICPixelFormat32bppBGRA)
fixed (byte* pBytes = bytes)
{
this.wicFactory.Get()->CreateBitmapFromMemory(
(uint)specs.Width,
(uint)specs.Height,
pGuid,
(uint)specs.Pitch,
checked((uint)bytes.Length),
pBytes,
tempBitmap.GetAddressOf()).ThrowOnError();
}
using var tempBitmap2 = default(ComPtr<IWICBitmapSource>);
WICConvertBitmapSource(
&guidPixelFormat,
(IWICBitmapSource*)tempBitmap.Get(),
tempBitmap2.GetAddressOf()).ThrowOnError();
encoderFrame.Get()->SetSize(checked((uint)specs.Width), checked((uint)specs.Height)).ThrowOnError();
encoderFrame.Get()->WriteSource(tempBitmap2.Get(), null).ThrowOnError();
cancellationToken.ThrowIfCancellationRequested();
encoderFrame.Get()->Commit().ThrowOnError();
cancellationToken.ThrowIfCancellationRequested();
encoder.Get()->Commit().ThrowOnError();
}
/// <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>
private static DXGI_FORMAT GetCorrespondingDxgiFormat(Guid fmt) => 0 switch
{
// See https://github.com/microsoft/DirectXTex/wiki/WIC-I-O-Functions#savetowicmemory-savetowicfile
_ when fmt == GUID.GUID_WICPixelFormat128bppRGBAFloat => DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_FLOAT,
_ when fmt == GUID.GUID_WICPixelFormat64bppRGBAHalf => DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_FLOAT,
_ when fmt == GUID.GUID_WICPixelFormat64bppRGBA => DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_UNORM,
_ when fmt == GUID.GUID_WICPixelFormat32bppRGBA1010102XR => DXGI_FORMAT
.DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM,
_ when fmt == GUID.GUID_WICPixelFormat32bppRGBA1010102 => DXGI_FORMAT.DXGI_FORMAT_R10G10B10A2_UNORM,
_ when fmt == GUID.GUID_WICPixelFormat16bppBGRA5551 => DXGI_FORMAT.DXGI_FORMAT_B5G5R5A1_UNORM,
_ when fmt == GUID.GUID_WICPixelFormat16bppBGR565 => DXGI_FORMAT.DXGI_FORMAT_B5G6R5_UNORM,
_ when fmt == GUID.GUID_WICPixelFormat32bppGrayFloat => DXGI_FORMAT.DXGI_FORMAT_R32_FLOAT,
_ when fmt == GUID.GUID_WICPixelFormat16bppGrayHalf => DXGI_FORMAT.DXGI_FORMAT_R16_FLOAT,
_ when fmt == GUID.GUID_WICPixelFormat16bppGray => DXGI_FORMAT.DXGI_FORMAT_R16_UNORM,
_ when fmt == GUID.GUID_WICPixelFormat8bppGray => DXGI_FORMAT.DXGI_FORMAT_R8_UNORM,
_ when fmt == GUID.GUID_WICPixelFormat8bppAlpha => DXGI_FORMAT.DXGI_FORMAT_A8_UNORM,
_ when fmt == GUID.GUID_WICPixelFormat32bppRGBA => DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM,
_ when fmt == GUID.GUID_WICPixelFormat32bppBGRA => DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM,
_ when fmt == GUID.GUID_WICPixelFormat32bppBGR => DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM,
_ => DXGI_FORMAT.DXGI_FORMAT_UNKNOWN,
};
private void ReleaseUnmanagedResource() => this.wicFactory.Reset();
private readonly struct ComponentEnumerable<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,291 @@
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Data;
using Dalamud.Game;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Textures.Internal.SharedImmediateTextures;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Lumina.Data;
using Lumina.Data.Files;
using SharpDX;
using SharpDX.Direct3D;
using SharpDX.Direct3D11;
using SharpDX.DXGI;
using TerraFX.Interop.DirectX;
namespace Dalamud.Interface.Textures.Internal;
/// <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 sealed partial class TextureManager : IServiceType, IDisposable, ITextureProvider, ITextureSubstitutionProvider
{
private static readonly ModuleLog Log = new(nameof(TextureManager));
[ServiceManager.ServiceDependency]
private readonly Dalamud dalamud = Service<Dalamud>.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();
[ServiceManager.ServiceDependency]
private readonly TextureLoadThrottler textureLoadThrottler = Service<TextureLoadThrottler>.Get();
private SharedTextureManager? sharedTextureManager;
private WicManager? wicManager;
private bool disposing;
[SuppressMessage(
"StyleCop.CSharp.LayoutRules",
"SA1519:Braces should not be omitted from multi-line child statement",
Justification = "Multiple fixed blocks")]
[ServiceManager.ServiceConstructor]
private TextureManager()
{
this.sharedTextureManager = new(this);
this.wicManager = new(this);
}
/// <summary>Gets the shared texture manager.</summary>
public SharedTextureManager Shared =>
this.sharedTextureManager ??
throw new ObjectDisposedException(nameof(this.sharedTextureManager));
/// <summary>Gets the WIC manager.</summary>
public WicManager Wic =>
this.wicManager ??
throw new ObjectDisposedException(nameof(this.sharedTextureManager));
/// <inheritdoc/>
public void Dispose()
{
if (this.disposing)
return;
this.disposing = true;
this.drawsOneSquare?.Dispose();
this.drawsOneSquare = null;
Interlocked.Exchange(ref this.sharedTextureManager, null)?.Dispose();
Interlocked.Exchange(ref this.wicManager, null)?.Dispose();
}
/// <inheritdoc/>
public Task<IDalamudTextureWrap> CreateFromImageAsync(
ReadOnlyMemory<byte> bytes,
CancellationToken cancellationToken = default) =>
this.textureLoadThrottler.LoadTextureAsync(
new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(),
ct => Task.Run(() => this.NoThrottleCreateFromImage(bytes.ToArray(), ct), ct),
cancellationToken);
/// <inheritdoc/>
public Task<IDalamudTextureWrap> CreateFromImageAsync(
Stream stream,
bool leaveOpen = false,
CancellationToken cancellationToken = default) =>
this.textureLoadThrottler.LoadTextureAsync(
new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(),
async ct =>
{
await using var ms = stream.CanSeek ? new MemoryStream((int)stream.Length) : new();
await stream.CopyToAsync(ms, ct).ConfigureAwait(false);
return this.NoThrottleCreateFromImage(ms.GetBuffer(), ct);
},
cancellationToken)
.ContinueWith(
r =>
{
if (!leaveOpen)
stream.Dispose();
return r;
},
default(CancellationToken))
.Unwrap();
/// <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) => this.NoThrottleCreateFromRaw(specs, bytes);
/// <inheritdoc/>
public Task<IDalamudTextureWrap> CreateFromRawAsync(
RawImageSpecification specs,
ReadOnlyMemory<byte> bytes,
CancellationToken cancellationToken = default) =>
this.textureLoadThrottler.LoadTextureAsync(
new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(),
_ => Task.FromResult(this.NoThrottleCreateFromRaw(specs, bytes.Span)),
cancellationToken);
/// <inheritdoc/>
public Task<IDalamudTextureWrap> CreateFromRawAsync(
RawImageSpecification specs,
Stream stream,
bool leaveOpen = false,
CancellationToken cancellationToken = default) =>
this.textureLoadThrottler.LoadTextureAsync(
new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(),
async ct =>
{
await using var ms = stream.CanSeek ? new MemoryStream((int)stream.Length) : new();
await stream.CopyToAsync(ms, ct).ConfigureAwait(false);
return this.NoThrottleCreateFromRaw(specs, ms.GetBuffer().AsSpan(0, (int)ms.Length));
},
cancellationToken)
.ContinueWith(
r =>
{
if (!leaveOpen)
stream.Dispose();
return r;
},
default(CancellationToken))
.Unwrap();
/// <inheritdoc/>
public IDalamudTextureWrap CreateFromTexFile(TexFile file) => this.CreateFromTexFileAsync(file).Result;
/// <inheritdoc/>
public Task<IDalamudTextureWrap> CreateFromTexFileAsync(
TexFile file,
CancellationToken cancellationToken = default) =>
this.textureLoadThrottler.LoadTextureAsync(
new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(),
ct => Task.Run(() => this.NoThrottleCreateFromTexFile(file), ct),
cancellationToken);
/// <inheritdoc/>
bool ITextureProvider.IsDxgiFormatSupported(int dxgiFormat) =>
this.IsDxgiFormatSupported((DXGI_FORMAT)dxgiFormat);
/// <inheritdoc cref="ITextureProvider.IsDxgiFormatSupported"/>
public bool IsDxgiFormatSupported(DXGI_FORMAT dxgiFormat)
{
if (this.interfaceManager.Scene is not { } scene)
{
_ = Service<InterfaceManager.InterfaceManagerWithScene>.Get();
scene = this.interfaceManager.Scene ?? throw new InvalidOperationException();
}
var format = (Format)dxgiFormat;
var support = scene.Device.CheckFormatSupport(format);
const FormatSupport required = FormatSupport.Texture2D;
return (support & required) == required;
}
/// <inheritdoc cref="ITextureProvider.CreateFromRaw"/>
internal IDalamudTextureWrap NoThrottleCreateFromRaw(
RawImageSpecification specs,
ReadOnlySpan<byte> bytes)
{
if (this.interfaceManager.Scene is not { } scene)
{
_ = Service<InterfaceManager.InterfaceManagerWithScene>.Get();
scene = this.interfaceManager.Scene ?? throw new InvalidOperationException();
}
ShaderResourceView resView;
unsafe
{
fixed (void* pData = bytes)
{
var texDesc = new Texture2DDescription
{
Width = specs.Width,
Height = specs.Height,
MipLevels = 1,
ArraySize = 1,
Format = (Format)specs.DxgiFormat,
SampleDescription = new(1, 0),
Usage = ResourceUsage.Immutable,
BindFlags = BindFlags.ShaderResource,
CpuAccessFlags = CpuAccessFlags.None,
OptionFlags = ResourceOptionFlags.None,
};
using var texture = new Texture2D(scene.Device, texDesc, new DataRectangle(new(pData), specs.Pitch));
resView = new(
scene.Device,
texture,
new()
{
Format = texDesc.Format,
Dimension = ShaderResourceViewDimension.Texture2D,
Texture2D = { MipLevels = texDesc.MipLevels },
});
}
}
// no sampler for now because the ImGui implementation we copied doesn't allow for changing it
return new DalamudTextureWrap(new D3DTextureWrap(resView, specs.Width, specs.Height));
}
/// <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)Format.B8G8R8A8_UNorm;
buffer = buffer.Filter(0, 0, TexFile.TextureFormat.B8G8R8A8);
}
return this.NoThrottleCreateFromRaw(
RawImageSpecification.From(buffer.Width, buffer.Height, dxgiFormat),
buffer.RawData);
}
/// <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.
return this.NoThrottleCreateFromTexFile(tf);
}
}

View file

@ -1,10 +1,11 @@
using System.Threading;
using Dalamud.Interface.Internal;
using Dalamud.Utility;
using TerraFX.Interop.Windows;
namespace Dalamud.Interface.Internal;
namespace Dalamud.Interface.Textures.Internal;
/// <summary>
/// A texture wrap that is created by cloning the underlying <see cref="IDalamudTextureWrap.ImGuiHandle"/>.

View file

@ -14,6 +14,7 @@ using Dalamud.Interface.Internal.ManagedAsserts;
using Dalamud.Interface.Internal.Notifications;
using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Interface.ManagedFontAtlas.Internals;
using Dalamud.Interface.Textures.Internal;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Plugin.Services;
using Dalamud.Utility;

View file

@ -4,6 +4,7 @@ using System.Linq;
using Dalamud.Data;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Textures.Internal;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Lumina.Data.Files;

View file

@ -8,6 +8,7 @@ using System.Threading.Tasks;
using Dalamud.Interface;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Textures;
using Lumina.Data.Files;
@ -121,14 +122,41 @@ public partial interface ITextureProvider
TexFile file,
CancellationToken cancellationToken = default);
/// <summary>Gets the supported bitmap decoders.</summary>
/// <returns>The supported bitmap decoders.</returns>
/// <remarks>
/// The following functions support the files of the container types pointed by yielded values.
/// <ul>
/// <li><see cref="GetFromFile"/></li>
/// <li><see cref="GetFromManifestResource"/></li>
/// <li><see cref="CreateFromImageAsync(ReadOnlyMemory{byte},CancellationToken)"/></li>
/// <li><see cref="CreateFromImageAsync(Stream,bool,CancellationToken)"/></li>
/// </ul>
/// </remarks>
IEnumerable<IBitmapCodecInfo> GetSupportedImageDecoderInfos();
/// <summary>Gets the supported bitmap encoders.</summary>
/// <returns>The supported bitmap encoders.</returns>
/// <remarks>
/// The following function supports the files of the container types pointed by yielded values.
/// <ul>
/// <li><see cref="SaveToStreamAsync"/></li>
/// </ul>
/// </remarks>
IEnumerable<IBitmapCodecInfo> GetSupportedImageEncoderInfos();
/// <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>This function is under the effect of <see cref="ITextureSubstitutionProvider.GetSubstitutedPath"/>.
/// </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>This function is under the effect of <see cref="ITextureSubstitutionProvider.GetSubstitutedPath"/>.
/// </remarks>
ISharedImmediateTexture GetFromGame(string path);
/// <summary>Gets a shared texture corresponding to the given file on the filesystem.</summary>
@ -173,26 +201,16 @@ public partial interface ITextureProvider
/// then the source data will be returned.</para>
/// <para>This function can fail.</para>
/// </remarks>
Task<(RawImageSpecification Specification, byte[] RawData)> GetRawDataAsync(
Task<(RawImageSpecification Specification, byte[] RawData)> GetRawDataFromExistingTextureAsync(
IDalamudTextureWrap wrap,
Vector2 uv0,
Vector2 uv1,
int dxgiFormat = 0,
CancellationToken cancellationToken = default);
/// <summary>Gets the supported image file extensions available for loading.</summary>
/// <returns>The supported extensions. Each <c>string[]</c> entry indicates that there can be multiple extensions
/// that correspond to one container format.</returns>
IEnumerable<string[]> GetLoadSupportedImageExtensions();
/// <summary>Gets the supported image file extensions available for saving.</summary>
/// <returns>The supported extensions. Each <c>string[]</c> entry indicates that there can be multiple extensions
/// that correspond to one container format.</returns>
IEnumerable<string[]> GetSaveSupportedImageExtensions();
/// <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="extension">The extension of the file to deduce the file format with the leading dot.</param>
/// <param name="containerGuid">The container GUID, obtained from <see cref="GetSupportedImageEncoderInfos"/>.</param>
/// <param name="stream">The stream to save to.</param>
/// <param name="leaveOpen">Whether to leave <paramref name="stream"/> open.</param>
/// <param name="props">Properties to pass to the encoder. See
@ -202,21 +220,34 @@ public partial interface ITextureProvider
/// <returns>A task representing the save process.</returns>
/// <remarks>
/// <para><paramref name="wrap"/> may be disposed as soon as this function returns.</para>
/// <para>If no image container format corresponding to <paramref name="extension"/> is found, then the image will
/// be saved in png format.</para>
/// </remarks>
[SuppressMessage(
"StyleCop.CSharp.LayoutRules",
"SA1519:Braces should not be omitted from multi-line child statement",
Justification = "Multiple fixed blocks")]
Task SaveAsImageFormatToStreamAsync(
Task SaveToStreamAsync(
IDalamudTextureWrap wrap,
string extension,
Guid containerGuid,
Stream stream,
bool leaveOpen = false,
IReadOnlyDictionary<string, object>? props = null,
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
/// <a href="https://learn.microsoft.com/en-us/windows/win32/wic/-wic-creating-encoder#encoder-options">Microsoft
/// Learn</a> for available parameters.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task representing the save process.</returns>
/// <remarks>
/// <para><paramref name="wrap"/> may be disposed as soon as this function returns.</para>
/// </remarks>
Task SaveToFileAsync(
IDalamudTextureWrap wrap,
Guid containerGuid,
string path,
IReadOnlyDictionary<string, object>? props = null,
CancellationToken cancellationToken = default);
/// <summary>
/// Determines whether the system supports the given DXGI format.
/// For use with <see cref="RawImageSpecification.DxgiFormat"/>.

View file

@ -9,14 +9,24 @@ namespace Dalamud.Plugin.Services;
/// </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="Pitch">The pitch of the image in bytes. The value may not always exactly match
/// <c><paramref name="Width"/> * bytesPerPixelFromDxgiFormat</c>.</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)
public readonly record struct RawImageSpecification(int Width, int Height, int Pitch, int DxgiFormat)
{
private const string FormatNotSupportedMessage = $"{nameof(DxgiFormat)} is not supported.";
/// <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((DXGI_FORMAT)this.DxgiFormat, out var bitsPerPixel, out _)
? bitsPerPixel
: throw new NotSupportedException(FormatNotSupportedMessage);
/// <summary>
/// Creates a new instance of <see cref="RawImageSpecification"/> record using the given resolution and pixel
/// format. Pitch will be automatically calculated.
@ -25,167 +35,11 @@ public record struct RawImageSpecification(int Width, int Height, int Pitch, int
/// <param name="height">The height.</param>
/// <param name="format">The format.</param>
/// <returns>The new instance.</returns>
/// <exception cref="NotSupportedException">Thrown if <see cref="DxgiFormat"/> is not supported.</exception>
public static RawImageSpecification From(int width, int height, int format)
{
int bitsPerPixel;
var isBlockCompression = false;
switch ((DXGI_FORMAT)format)
{
case DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_FLOAT:
case DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_SINT:
bitsPerPixel = 128;
break;
case DXGI_FORMAT.DXGI_FORMAT_R32G32B32_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_R32G32B32_FLOAT:
case DXGI_FORMAT.DXGI_FORMAT_R32G32B32_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R32G32B32_SINT:
bitsPerPixel = 96;
break;
case DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_FLOAT:
case DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_SNORM:
case DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_SINT:
case DXGI_FORMAT.DXGI_FORMAT_R32G32_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_R32G32_FLOAT:
case DXGI_FORMAT.DXGI_FORMAT_R32G32_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R32G32_SINT:
case DXGI_FORMAT.DXGI_FORMAT_R32G8X24_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_D32_FLOAT_S8X24_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R32_FLOAT_X8X24_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_X32_TYPELESS_G8X24_UINT:
bitsPerPixel = 64;
break;
case DXGI_FORMAT.DXGI_FORMAT_R10G10B10A2_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_R10G10B10A2_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_R10G10B10A2_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R11G11B10_FLOAT:
case DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM_SRGB:
case DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_SNORM:
case DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_SINT:
case DXGI_FORMAT.DXGI_FORMAT_R16G16_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_R16G16_FLOAT:
case DXGI_FORMAT.DXGI_FORMAT_R16G16_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_R16G16_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R16G16_SNORM:
case DXGI_FORMAT.DXGI_FORMAT_R16G16_SINT:
case DXGI_FORMAT.DXGI_FORMAT_R32_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_D32_FLOAT:
case DXGI_FORMAT.DXGI_FORMAT_R32_FLOAT:
case DXGI_FORMAT.DXGI_FORMAT_R32_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R32_SINT:
case DXGI_FORMAT.DXGI_FORMAT_R24G8_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_D24_UNORM_S8_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R24_UNORM_X8_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_X24_TYPELESS_G8_UINT:
bitsPerPixel = 32;
break;
case DXGI_FORMAT.DXGI_FORMAT_R8G8_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_R8G8_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_R8G8_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R8G8_SNORM:
case DXGI_FORMAT.DXGI_FORMAT_R8G8_SINT:
case DXGI_FORMAT.DXGI_FORMAT_R16_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_R16_FLOAT:
case DXGI_FORMAT.DXGI_FORMAT_D16_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_R16_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_R16_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R16_SNORM:
case DXGI_FORMAT.DXGI_FORMAT_R16_SINT:
bitsPerPixel = 16;
break;
case DXGI_FORMAT.DXGI_FORMAT_R8_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_R8_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_R8_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R8_SNORM:
case DXGI_FORMAT.DXGI_FORMAT_R8_SINT:
case DXGI_FORMAT.DXGI_FORMAT_A8_UNORM:
bitsPerPixel = 8;
break;
case DXGI_FORMAT.DXGI_FORMAT_R1_UNORM:
bitsPerPixel = 1;
break;
case DXGI_FORMAT.DXGI_FORMAT_R9G9B9E5_SHAREDEXP:
case DXGI_FORMAT.DXGI_FORMAT_R8G8_B8G8_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_G8R8_G8B8_UNORM:
throw new NotSupportedException();
case DXGI_FORMAT.DXGI_FORMAT_BC1_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM_SRGB:
bitsPerPixel = 4;
isBlockCompression = true;
break;
case DXGI_FORMAT.DXGI_FORMAT_BC2_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_BC2_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_BC2_UNORM_SRGB:
case DXGI_FORMAT.DXGI_FORMAT_BC3_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_BC3_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_BC3_UNORM_SRGB:
bitsPerPixel = 8;
isBlockCompression = true;
break;
case DXGI_FORMAT.DXGI_FORMAT_BC4_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_BC4_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_BC4_SNORM:
bitsPerPixel = 4;
isBlockCompression = true;
break;
case DXGI_FORMAT.DXGI_FORMAT_BC5_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_BC5_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_BC5_SNORM:
bitsPerPixel = 8;
isBlockCompression = true;
break;
case DXGI_FORMAT.DXGI_FORMAT_B5G6R5_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_B5G5R5A1_UNORM:
bitsPerPixel = 16;
break;
case DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM_SRGB:
case DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM_SRGB:
bitsPerPixel = 32;
break;
case DXGI_FORMAT.DXGI_FORMAT_BC6H_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_BC6H_UF16:
case DXGI_FORMAT.DXGI_FORMAT_BC6H_SF16:
case DXGI_FORMAT.DXGI_FORMAT_BC7_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_BC7_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_BC7_UNORM_SRGB:
bitsPerPixel = 8;
isBlockCompression = true;
break;
case DXGI_FORMAT.DXGI_FORMAT_AYUV:
case DXGI_FORMAT.DXGI_FORMAT_Y410:
case DXGI_FORMAT.DXGI_FORMAT_Y416:
case DXGI_FORMAT.DXGI_FORMAT_NV12:
case DXGI_FORMAT.DXGI_FORMAT_P010:
case DXGI_FORMAT.DXGI_FORMAT_P016:
case DXGI_FORMAT.DXGI_FORMAT_420_OPAQUE:
case DXGI_FORMAT.DXGI_FORMAT_YUY2:
case DXGI_FORMAT.DXGI_FORMAT_Y210:
case DXGI_FORMAT.DXGI_FORMAT_Y216:
case DXGI_FORMAT.DXGI_FORMAT_NV11:
case DXGI_FORMAT.DXGI_FORMAT_AI44:
case DXGI_FORMAT.DXGI_FORMAT_IA44:
case DXGI_FORMAT.DXGI_FORMAT_P8:
case DXGI_FORMAT.DXGI_FORMAT_A8P8:
throw new NotSupportedException();
case DXGI_FORMAT.DXGI_FORMAT_B4G4R4A4_UNORM:
bitsPerPixel = 16;
break;
default:
throw new NotSupportedException();
}
if (!GetFormatInfo((DXGI_FORMAT)format, out var bitsPerPixel, out var isBlockCompression))
throw new NotSupportedException(FormatNotSupportedMessage);
var pitch = isBlockCompression
? Math.Max(1, (width + 3) / 4) * 2 * bitsPerPixel
@ -223,4 +77,157 @@ public record struct RawImageSpecification(int Width, int Height, int Pitch, int
/// <returns>The new instance.</returns>
public static RawImageSpecification A8(int width, int height) =>
new(width, height, width, (int)DXGI_FORMAT.DXGI_FORMAT_A8_UNORM);
private static bool GetFormatInfo(DXGI_FORMAT format, out int bitsPerPixel, out bool isBlockCompression)
{
switch (format)
{
case DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_FLOAT:
case DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_SINT:
bitsPerPixel = 128;
isBlockCompression = false;
return true;
case DXGI_FORMAT.DXGI_FORMAT_R32G32B32_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_R32G32B32_FLOAT:
case DXGI_FORMAT.DXGI_FORMAT_R32G32B32_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R32G32B32_SINT:
bitsPerPixel = 96;
isBlockCompression = false;
return true;
case DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_FLOAT:
case DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_SNORM:
case DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_SINT:
case DXGI_FORMAT.DXGI_FORMAT_R32G32_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_R32G32_FLOAT:
case DXGI_FORMAT.DXGI_FORMAT_R32G32_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R32G32_SINT:
case DXGI_FORMAT.DXGI_FORMAT_R32G8X24_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_D32_FLOAT_S8X24_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R32_FLOAT_X8X24_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_X32_TYPELESS_G8X24_UINT:
bitsPerPixel = 64;
isBlockCompression = false;
return true;
case DXGI_FORMAT.DXGI_FORMAT_R10G10B10A2_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_R10G10B10A2_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_R10G10B10A2_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R11G11B10_FLOAT:
case DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM_SRGB:
case DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_SNORM:
case DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_SINT:
case DXGI_FORMAT.DXGI_FORMAT_R16G16_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_R16G16_FLOAT:
case DXGI_FORMAT.DXGI_FORMAT_R16G16_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_R16G16_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R16G16_SNORM:
case DXGI_FORMAT.DXGI_FORMAT_R16G16_SINT:
case DXGI_FORMAT.DXGI_FORMAT_R32_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_D32_FLOAT:
case DXGI_FORMAT.DXGI_FORMAT_R32_FLOAT:
case DXGI_FORMAT.DXGI_FORMAT_R32_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R32_SINT:
case DXGI_FORMAT.DXGI_FORMAT_R24G8_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_D24_UNORM_S8_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R24_UNORM_X8_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_X24_TYPELESS_G8_UINT:
bitsPerPixel = 32;
isBlockCompression = false;
return true;
case DXGI_FORMAT.DXGI_FORMAT_R8G8_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_R8G8_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_R8G8_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R8G8_SNORM:
case DXGI_FORMAT.DXGI_FORMAT_R8G8_SINT:
case DXGI_FORMAT.DXGI_FORMAT_R16_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_R16_FLOAT:
case DXGI_FORMAT.DXGI_FORMAT_D16_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_R16_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_R16_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R16_SNORM:
case DXGI_FORMAT.DXGI_FORMAT_R16_SINT:
bitsPerPixel = 16;
isBlockCompression = false;
return true;
case DXGI_FORMAT.DXGI_FORMAT_R8_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_R8_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_R8_UINT:
case DXGI_FORMAT.DXGI_FORMAT_R8_SNORM:
case DXGI_FORMAT.DXGI_FORMAT_R8_SINT:
case DXGI_FORMAT.DXGI_FORMAT_A8_UNORM:
bitsPerPixel = 8;
isBlockCompression = false;
return true;
case DXGI_FORMAT.DXGI_FORMAT_R1_UNORM:
bitsPerPixel = 1;
isBlockCompression = false;
return true;
case DXGI_FORMAT.DXGI_FORMAT_BC1_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM_SRGB:
bitsPerPixel = 4;
isBlockCompression = true;
return true;
case DXGI_FORMAT.DXGI_FORMAT_BC2_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_BC2_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_BC2_UNORM_SRGB:
case DXGI_FORMAT.DXGI_FORMAT_BC3_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_BC3_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_BC3_UNORM_SRGB:
bitsPerPixel = 8;
isBlockCompression = true;
return true;
case DXGI_FORMAT.DXGI_FORMAT_BC4_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_BC4_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_BC4_SNORM:
bitsPerPixel = 4;
isBlockCompression = true;
return true;
case DXGI_FORMAT.DXGI_FORMAT_BC5_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_BC5_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_BC5_SNORM:
bitsPerPixel = 8;
isBlockCompression = true;
return true;
case DXGI_FORMAT.DXGI_FORMAT_B5G6R5_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_B5G5R5A1_UNORM:
bitsPerPixel = 16;
isBlockCompression = false;
return true;
case DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM_SRGB:
case DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM_SRGB:
bitsPerPixel = 32;
isBlockCompression = false;
return true;
case DXGI_FORMAT.DXGI_FORMAT_BC6H_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_BC6H_UF16:
case DXGI_FORMAT.DXGI_FORMAT_BC6H_SF16:
case DXGI_FORMAT.DXGI_FORMAT_BC7_TYPELESS:
case DXGI_FORMAT.DXGI_FORMAT_BC7_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_BC7_UNORM_SRGB:
bitsPerPixel = 8;
isBlockCompression = true;
return true;
case DXGI_FORMAT.DXGI_FORMAT_B4G4R4A4_UNORM:
bitsPerPixel = 16;
isBlockCompression = true;
return false;
default:
bitsPerPixel = 0;
isBlockCompression = false;
return false;
}
}
}

View file

@ -7,6 +7,7 @@ using System.Threading;
using System.Threading.Tasks;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Textures.Internal;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Networking.Http;

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

@ -1,5 +1,4 @@
using System.Buffers;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@ -7,7 +6,7 @@ using System.Runtime.InteropServices;
using TerraFX.Interop;
using TerraFX.Interop.Windows;
namespace Dalamud.Utility;
namespace Dalamud.Utility.TerraFxCom;
/// <summary>An <see cref="IStream"/> wrapper for <see cref="Stream"/>.</summary>
[Guid("a620678b-56b9-4202-a1da-b821214dc972")]
@ -15,7 +14,8 @@ internal sealed unsafe class ManagedIStream : IStream.Interface, IRefCountable
{
private static readonly Guid MyGuid = typeof(ManagedIStream).GUID;
private readonly Stream inner;
private readonly Stream innerStream;
private readonly bool leaveOpen;
private readonly nint[] comObject;
private readonly IStream.Vtbl<IStream> vtbl;
private GCHandle gchThis;
@ -23,11 +23,10 @@ internal sealed unsafe class ManagedIStream : IStream.Interface, IRefCountable
private GCHandle gchVtbl;
private int refCount;
/// <summary>Initializes a new instance of the <see cref="ManagedIStream"/> class.</summary>
/// <param name="inner">The inner stream.</param>
public ManagedIStream(Stream inner)
private ManagedIStream(Stream innerStream, bool leaveOpen = false)
{
this.inner = inner;
this.innerStream = innerStream ?? throw new NullReferenceException();
this.leaveOpen = leaveOpen;
this.comObject = new nint[2];
this.vtbl.QueryInterface = &QueryInterfaceStatic;
@ -127,6 +126,26 @@ internal sealed unsafe class ManagedIStream : IStream.Interface, IRefCountable
public static implicit operator IStream*(ManagedIStream mis) =>
(IStream*)mis.gchComObject.AddrOfPinnedObject();
/// <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)
{
@ -176,6 +195,8 @@ internal sealed unsafe class ManagedIStream : IStream.Interface, IRefCountable
this.gchThis.Free();
this.gchComObject.Free();
this.gchVtbl.Free();
if (!this.leaveOpen)
this.innerStream.Dispose();
return newRefCount;
case IRefCountable.RefCountResult.AlreadyDisposed:
@ -225,7 +246,7 @@ internal sealed unsafe class ManagedIStream : IStream.Interface, IRefCountable
for (read = 0u; read < cb;)
{
var chunkSize = unchecked((int)Math.Min(0x10000000u, cb));
var chunkRead = (uint)this.inner.Read(new(pv, chunkSize));
var chunkRead = (uint)this.innerStream.Read(new(pv, chunkSize));
if (chunkRead == 0)
break;
pv = (byte*)pv + chunkRead;
@ -250,7 +271,7 @@ internal sealed unsafe class ManagedIStream : IStream.Interface, IRefCountable
for (written = 0u; written < cb;)
{
var chunkSize = Math.Min(0x10000000u, cb);
this.inner.Write(new(pv, (int)chunkSize));
this.innerStream.Write(new(pv, (int)chunkSize));
pv = (byte*)pv + chunkSize;
written += chunkSize;
}
@ -293,7 +314,7 @@ internal sealed unsafe class ManagedIStream : IStream.Interface, IRefCountable
try
{
var position = this.inner.Seek(dlibMove.QuadPart, seekOrigin);
var position = this.innerStream.Seek(dlibMove.QuadPart, seekOrigin);
if (plibNewPosition != null)
{
*plibNewPosition = new() { QuadPart = (ulong)position };
@ -312,7 +333,7 @@ internal sealed unsafe class ManagedIStream : IStream.Interface, IRefCountable
{
try
{
this.inner.SetLength(checked((long)libNewSize.QuadPart));
this.innerStream.SetLength(checked((long)libNewSize.QuadPart));
return S.S_OK;
}
catch (Exception e) when (e.HResult == unchecked((int)(0x80070000u | ERROR.ERROR_HANDLE_DISK_FULL)))
@ -355,7 +376,7 @@ internal sealed unsafe class ManagedIStream : IStream.Interface, IRefCountable
{
while (cbRead < cb)
{
var read = checked((uint)this.inner.Read(buf.AsSpan()));
var read = checked((uint)this.innerStream.Read(buf.AsSpan()));
if (read == 0)
break;
cbRead += read;
@ -414,13 +435,13 @@ internal sealed unsafe class ManagedIStream : IStream.Interface, IRefCountable
return STG.STG_E_INVALIDPOINTER;
ref var streamStats = ref *pstatstg;
streamStats.type = (uint)STGTY.STGTY_STREAM;
streamStats.cbSize = (ulong)this.inner.Length;
streamStats.cbSize = (ulong)this.innerStream.Length;
streamStats.grfMode = 0;
if (this.inner.CanRead && this.inner.CanWrite)
if (this.innerStream.CanRead && this.innerStream.CanWrite)
streamStats.grfMode |= STGM.STGM_READWRITE;
else if (this.inner.CanRead)
else if (this.innerStream.CanRead)
streamStats.grfMode |= STGM.STGM_READ;
else if (this.inner.CanWrite)
else if (this.innerStream.CanWrite)
streamStats.grfMode |= STGM.STGM_WRITE;
else
return STG.STG_E_REVERTED;