diff --git a/Dalamud/Interface/Internal/TextureManager.Wic.cs b/Dalamud/Interface/Internal/TextureManager.Wic.cs deleted file mode 100644 index 66be9ca58..000000000 --- a/Dalamud/Interface/Internal/TextureManager.Wic.cs +++ /dev/null @@ -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; - -/// Service responsible for loading and disposing ImGui texture wraps. -internal sealed partial class TextureManager -{ - private ComPtr wicFactory; - - /// - [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? 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); - } - - /// - public IEnumerable GetLoadSupportedImageExtensions() => - this.GetSupportedContainerFormats(WICComponentType.WICDecoder).Values; - - /// - public IEnumerable GetSaveSupportedImageExtensions() => - this.GetSupportedContainerFormats(WICComponentType.WICEncoder).Values; - - /// Creates a texture from the given bytes of an image file. Skips the load throttler; intended to be used - /// from implementation of s. - /// The data. - /// The cancellation token. - /// The loaded texture. - internal unsafe IDalamudTextureWrap NoThrottleCreateFromImage( - ReadOnlyMemory 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); - this.wicFactory.Get()->CreateStream(wicStream.GetAddressOf()).ThrowOnError(); - wicStream.Get()->InitializeFromMemory(p, checked((uint)bytes.Length)).ThrowOnError(); - return this.NoThrottleCreateFromWicStream((IStream*)wicStream.Get(), cancellationToken); - } - } - - /// Creates a texture from the given path to an image file. Skips the load throttler; intended to be used - /// from implementation of s. - /// The path of the file.. - /// The cancellation token. - /// The loaded texture. - internal async Task NoThrottleCreateFromFileAsync( - string path, - CancellationToken cancellationToken = default) - { - ObjectDisposedException.ThrowIf(this.disposing, this); - - cancellationToken.ThrowIfCancellationRequested(); - - try - { - unsafe - { - fixed (char* pPath = path) - { - using var wicStream = default(ComPtr); - 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()) - { - 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; - } - } - - /// - /// Gets the corresponding from a containing a WIC pixel format. - /// - /// The WIC pixel format. - /// The corresponding , or if - /// unavailable. - 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); - this.wicFactory.Get()->CreateDecoderFromStream( - wicStream, - null, - WICDecodeOptions.WICDecodeMetadataCacheOnDemand, - decoder.GetAddressOf()).ThrowOnError(); - - cancellationToken.ThrowIfCancellationRequested(); - - using var frame = default(ComPtr); - decoder.Get()->GetFrame(0, frame.GetAddressOf()).ThrowOnError(); - var pixelFormat = default(Guid); - frame.Get()->GetPixelFormat(&pixelFormat).ThrowOnError(); - var dxgiFormat = GetCorrespondingDxgiFormat(pixelFormat); - - cancellationToken.ThrowIfCancellationRequested(); - - using var bitmapSource = default(ComPtr); - if (dxgiFormat == DXGI_FORMAT.DXGI_FORMAT_UNKNOWN || !this.IsDxgiFormatSupported(dxgiFormat)) - { - dxgiFormat = DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM; - pixelFormat = GUID.GUID_WICPixelFormat32bppBGRA; - WICConvertBitmapSource(&pixelFormat, (IWICBitmapSource*)frame.Get(), bitmapSource.GetAddressOf()) - .ThrowOnError(); - } - else - { - frame.As(&bitmapSource); - } - - cancellationToken.ThrowIfCancellationRequested(); - - using var bitmap = default(ComPtr); - using var bitmapLock = default(ComPtr); - WICRect rcLock; - uint stride; - uint cbBufferSize; - byte* pbData; - if (bitmapSource.As(&bitmap).FAILED) - { - bitmapSource.Get()->GetSize((uint*)&rcLock.Width, (uint*)&rcLock.Height).ThrowOnError(); - this.wicFactory.Get()->CreateBitmap( - (uint)rcLock.Width, - (uint)rcLock.Height, - &pixelFormat, - WICBitmapCreateCacheOption.WICBitmapCacheOnDemand, - bitmap.GetAddressOf()).ThrowOnError(); - - bitmap.Get()->Lock( - &rcLock, - (uint)WICBitmapLockFlags.WICBitmapLockWrite, - bitmapLock.ReleaseAndGetAddressOf()) - .ThrowOnError(); - bitmapLock.Get()->GetStride(&stride).ThrowOnError(); - bitmapLock.Get()->GetDataPointer(&cbBufferSize, &pbData).ThrowOnError(); - bitmapSource.Get()->CopyPixels(null, stride, cbBufferSize, pbData).ThrowOnError(); - } - - cancellationToken.ThrowIfCancellationRequested(); - - bitmap.Get()->Lock( - &rcLock, - (uint)WICBitmapLockFlags.WICBitmapLockRead, - bitmapLock.ReleaseAndGetAddressOf()) - .ThrowOnError(); - bitmapSource.Get()->GetSize((uint*)&rcLock.Width, (uint*)&rcLock.Height).ThrowOnError(); - bitmapLock.Get()->GetStride(&stride).ThrowOnError(); - bitmapLock.Get()->GetDataPointer(&cbBufferSize, &pbData).ThrowOnError(); - bitmapSource.Get()->CopyPixels(null, stride, cbBufferSize, pbData).ThrowOnError(); - return this.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 GetSupportedContainerFormats(WICComponentType componentType) - { - var result = new Dictionary(); - using var enumUnknown = default(ComPtr); - this.wicFactory.Get()->CreateComponentEnumerator( - (uint)componentType, - (uint)WICComponentEnumerateOptions.WICComponentEnumerateDefault, - enumUnknown.GetAddressOf()).ThrowOnError(); - - while (true) - { - using var entry = default(ComPtr); - var fetched = 0u; - enumUnknown.Get()->Next(1, entry.GetAddressOf(), &fetched).ThrowOnError(); - if (fetched == 0) - break; - - using var codecInfo = default(ComPtr); - 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> 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); - using var encoderFrame = default(ComPtr); - 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); - 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); - 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); - 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(); - } - } -} diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs deleted file mode 100644 index ce9f2a22d..000000000 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ /dev/null @@ -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; - -/// Service responsible for loading and disposing ImGui texture wraps. -[PluginInterface] -[InterfaceVersion("1.0")] -[ServiceManager.EarlyLoadedService] -#pragma warning disable SA1015 -[ResolveVia] -[ResolveVia] -#pragma warning restore SA1015 -internal 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.Get(); - - [ServiceManager.ServiceDependency] - private readonly DataManager dataManager = Service.Get(); - - [ServiceManager.ServiceDependency] - private readonly Framework framework = Service.Get(); - - [ServiceManager.ServiceDependency] - private readonly InterfaceManager interfaceManager = Service.Get(); - - [ServiceManager.ServiceDependency] - private readonly TextureLoadThrottler textureLoadThrottler = Service.Get(); - - private readonly ConcurrentLru lookupToPath = new(PathLookupLruCount); - - private readonly ConcurrentDictionary gamePathTextures = new(); - - private readonly ConcurrentDictionary fileSystemTextures = new(); - - private readonly ConcurrentDictionary<(Assembly Assembly, string Name), SharedImmediateTexture> - manifestResourceTextures = new(); - - private readonly HashSet 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(); - } - } - } - - /// Finalizes an instance of the class. - ~TextureManager() => this.Dispose(); - - /// - public event ITextureSubstitutionProvider.TextureDataInterceptorDelegate? InterceptTexDataLoad; - - /// Gets all the loaded textures from game resources. - public ICollection GamePathTexturesForDebug => this.gamePathTextures.Values; - - /// Gets all the loaded textures from filesystem. - public ICollection FileSystemTexturesForDebug => this.fileSystemTextures.Values; - - /// Gets all the loaded textures from assembly manifest resources. - public ICollection ManifestResourceTexturesForDebug => this.manifestResourceTextures.Values; - - /// Gets all the loaded textures that are invalidated from . - /// lock on use of the value returned from this property. - [SuppressMessage( - "ReSharper", - "InconsistentlySynchronizedField", - Justification = "Debug use only; users are expected to lock around this")] - public ICollection InvalidatedTexturesForDebug => this.invalidatedTextures; - - /// - 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(ConcurrentDictionary dict) - { - foreach (var v in dict.Values) - v.ReleaseSelfReference(true); - dict.Clear(); - } - } - - #region API9 compat - -#pragma warning disable CS0618 // Type or member is obsolete - /// - [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; - - /// - [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(); - - /// - [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] - [Obsolete("See interface definition.")] - IDalamudTextureWrap? ITextureProvider.GetTextureFromGame(string path, bool keepAlive) => - this.GetFromGame(path).GetAvailableOnAccessWrapForApi9(); - - /// - [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 - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public SharedImmediateTexture GetFromGameIcon(in GameIconLookup lookup) => - this.GetFromGame(this.lookupToPath.GetOrAdd(lookup, this.GetIconPathByValue)); - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public SharedImmediateTexture GetFromGame(string path) - { - ObjectDisposedException.ThrowIf(this.disposing, this); - return this.gamePathTextures.GetOrAdd(path, GamePathSharedImmediateTexture.CreatePlaceholder); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public SharedImmediateTexture GetFromFile(string path) - { - ObjectDisposedException.ThrowIf(this.disposing, this); - return this.fileSystemTextures.GetOrAdd(path, FileSystemSharedImmediateTexture.CreatePlaceholder); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public SharedImmediateTexture GetFromManifestResource(Assembly assembly, string name) - { - ObjectDisposedException.ThrowIf(this.disposing, this); - return this.manifestResourceTextures.GetOrAdd( - (assembly, name), - ManifestResourceSharedImmediateTexture.CreatePlaceholder); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - ISharedImmediateTexture ITextureProvider.GetFromGameIcon(in GameIconLookup lookup) => this.GetFromGameIcon(lookup); - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - ISharedImmediateTexture ITextureProvider.GetFromGame(string path) => this.GetFromGame(path); - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - ISharedImmediateTexture ITextureProvider.GetFromFile(string path) => this.GetFromFile(path); - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - ISharedImmediateTexture ITextureProvider.GetFromManifestResource(Assembly assembly, string name) => - this.GetFromManifestResource(assembly, name); - - /// - public Task CreateFromImageAsync( - ReadOnlyMemory bytes, - CancellationToken cancellationToken = default) => - this.textureLoadThrottler.LoadTextureAsync( - new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), - ct => Task.Run(() => this.NoThrottleCreateFromImage(bytes.ToArray(), ct), ct), - cancellationToken); - - /// - public Task 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(); - - /// - // It probably doesn't make sense to throttle this, as it copies the passed bytes to GPU without any transformation. - public IDalamudTextureWrap CreateFromRaw( - RawImageSpecification specs, - ReadOnlySpan bytes) => this.NoThrottleCreateFromRaw(specs, bytes); - - /// - public Task CreateFromRawAsync( - RawImageSpecification specs, - ReadOnlyMemory bytes, - CancellationToken cancellationToken = default) => - this.textureLoadThrottler.LoadTextureAsync( - new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), - _ => Task.FromResult(this.NoThrottleCreateFromRaw(specs, bytes.Span)), - cancellationToken); - - /// - public Task 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(); - - /// - public IDalamudTextureWrap CreateFromTexFile(TexFile file) => this.CreateFromTexFileAsync(file).Result; - - /// - public Task CreateFromTexFileAsync( - TexFile file, - CancellationToken cancellationToken = default) => - this.textureLoadThrottler.LoadTextureAsync( - new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), - ct => Task.Run(() => this.NoThrottleCreateFromTexFile(file), ct), - cancellationToken); - - /// - bool ITextureProvider.IsDxgiFormatSupported(int dxgiFormat) => - this.IsDxgiFormatSupported((DXGI_FORMAT)dxgiFormat); - - /// - public bool IsDxgiFormatSupported(DXGI_FORMAT dxgiFormat) - { - if (this.interfaceManager.Scene is not { } scene) - { - _ = Service.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; - } - - /// - public bool TryGetIconPath(in GameIconLookup lookup, out string path) - { - // 1. Item - path = FormatIconPath( - lookup.IconId, - lookup.ItemHq ? "hq/" : string.Empty, - lookup.HiRes); - if (this.dataManager.FileExists(path)) - return true; - - var languageFolder = (lookup.Language ?? (ClientLanguage)(int)this.dalamud.StartInfo.Language) switch - { - ClientLanguage.Japanese => "ja/", - ClientLanguage.English => "en/", - ClientLanguage.German => "de/", - ClientLanguage.French => "fr/", - _ => null, - }; - - if (languageFolder is not null) - { - // 2. Regular icon, with language, hi-res - path = FormatIconPath( - lookup.IconId, - languageFolder, - lookup.HiRes); - if (this.dataManager.FileExists(path)) - return true; - - if (lookup.HiRes) - { - // 3. Regular icon, with language, no hi-res - path = FormatIconPath( - lookup.IconId, - languageFolder, - false); - if (this.dataManager.FileExists(path)) - return true; - } - } - - // 4. Regular icon, without language, hi-res - path = FormatIconPath( - lookup.IconId, - null, - lookup.HiRes); - if (this.dataManager.FileExists(path)) - return true; - - // 4. Regular icon, without language, no hi-res - if (lookup.HiRes) - { - path = FormatIconPath( - lookup.IconId, - null, - false); - if (this.dataManager.FileExists(path)) - return true; - } - - return false; - } - - /// - public string GetIconPath(in GameIconLookup lookup) => - this.TryGetIconPath(lookup, out var path) ? path : throw new FileNotFoundException(); - - /// - public string GetSubstitutedPath(string originalPath) - { - if (this.InterceptTexDataLoad == null) - return originalPath; - - string? interceptPath = null; - this.InterceptTexDataLoad.Invoke(originalPath, ref interceptPath); - - if (interceptPath != null) - { - Log.Verbose("Intercept: {OriginalPath} => {ReplacePath}", originalPath, interceptPath); - return interceptPath; - } - - return originalPath; - } - - /// - public void InvalidatePaths(IEnumerable paths) - { - foreach (var path in paths) - { - 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); - } - } - } - } - - /// - internal IDalamudTextureWrap NoThrottleCreateFromRaw( - RawImageSpecification specs, - ReadOnlySpan bytes) - { - if (this.interfaceManager.Scene is not { } scene) - { - _ = Service.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)); - } - - /// Creates a texture from the given . Skips the load throttler; intended to be used - /// from implementation of s. - /// The data. - /// The loaded texture. - internal IDalamudTextureWrap NoThrottleCreateFromTexFile(TexFile file) - { - ObjectDisposedException.ThrowIf(this.disposing, this); - - var buffer = file.TextureBuffer; - var (dxgiFormat, conversion) = TexFile.GetDxgiFormatFromTextureFormat(file.Header.Format, false); - if (conversion != TexFile.DxgiFormatConversion.NoConversion || - !this.IsDxgiFormatSupported((DXGI_FORMAT)dxgiFormat)) - { - dxgiFormat = (int)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(ConcurrentDictionary dict) - { - if (!dict.IsEmpty) - { - foreach (var (k, v) in dict) - { - if (TextureFinalReleasePredicate(v)) - _ = dict.TryRemove(k, out _); - } - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - static bool TextureFinalReleasePredicate(SharedImmediateTexture v) => - v.ContentQueried && v.ReleaseSelfReference(false) == 0 && !v.HasRevivalPossibility; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private string GetIconPathByValue(GameIconLookup lookup) => - this.TryGetIconPath(lookup, out var path) ? path : throw new FileNotFoundException(); -} diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs index 1f4e5f29d..b7b897e68 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs @@ -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.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); diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs index 45a3a5331..6779d0d60 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -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; /// @@ -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.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 - { - ["CompressionQuality"] = 1.0f, - ["ImageQuality"] = 1.0f, - }); - } - catch (Exception e) - { - Log.Error(e, $"{nameof(TexWidget)}.{nameof(this.SaveImmediateTexture)}"); - Service.Get().AddNotification( - $"Failed to save file: {e}", - this.DisplayName, - NotificationType.Error); - return; - } - - Service.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> textureGetter) + { + try + { + BitmapCodecInfo encoder; + { + var off = ImGui.GetCursorScreenPos(); + var first = true; + var encoders = this.textureManager + .Wic + .GetSupportedEncoderInfos() + .ToList(); + var tcs = new TaskCompletionSource(); + Service.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.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(); + 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 + { + ["CompressionQuality"] = 1.0f, + ["ImageQuality"] = 1.0f, + }); + + Service.Get().AddNotification( + $"File saved to: {path}", + this.DisplayName, + NotificationType.Success); + } + catch (Exception e) + { + Log.Error(e, $"{nameof(TexWidget)}.{nameof(this.SaveTextureAsync)}"); + Service.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 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() }; diff --git a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs index 50450aaae..1042b0741 100644 --- a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs +++ b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs @@ -7,6 +7,7 @@ using System.Threading; using System.Threading.Tasks; using Dalamud.Game; +using Dalamud.Interface.Textures.Internal; using Dalamud.Networking.Http; using Dalamud.Plugin.Internal; using Dalamud.Plugin.Internal.Types; diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 0e30658ef..186263783 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -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?[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) diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs index 6f2a56394..87776f53a 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs @@ -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; diff --git a/Dalamud/Interface/Internal/DalamudTextureWrap.cs b/Dalamud/Interface/Textures/DalamudTextureWrap.cs similarity index 97% rename from Dalamud/Interface/Internal/DalamudTextureWrap.cs rename to Dalamud/Interface/Textures/DalamudTextureWrap.cs index b49c6f07b..3795abad2 100644 --- a/Dalamud/Interface/Internal/DalamudTextureWrap.cs +++ b/Dalamud/Interface/Textures/DalamudTextureWrap.cs @@ -2,6 +2,7 @@ using ImGuiScene; +// ReSharper disable once CheckNamespace namespace Dalamud.Interface.Internal; /// diff --git a/Dalamud/Interface/Textures/IBitmapCodecInfo.cs b/Dalamud/Interface/Textures/IBitmapCodecInfo.cs new file mode 100644 index 000000000..7a6f300ca --- /dev/null +++ b/Dalamud/Interface/Textures/IBitmapCodecInfo.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; + +namespace Dalamud.Interface.Textures; + +/// Represents an available bitmap codec. +public interface IBitmapCodecInfo +{ + /// Gets the friendly name for the codec. + string Name { get; } + + /// Gets the representing the container. + Guid ContainerGuid { get; } + + /// Gets the suggested file extensions. + IReadOnlyCollection Extensions { get; } + + /// Gets the corresponding mime types. + IReadOnlyCollection MimeTypes { get; } +} diff --git a/Dalamud/Interface/Internal/IDalamudTextureWrap.cs b/Dalamud/Interface/Textures/IDalamudTextureWrap.cs similarity index 96% rename from Dalamud/Interface/Internal/IDalamudTextureWrap.cs rename to Dalamud/Interface/Textures/IDalamudTextureWrap.cs index 8e2e56c26..d2915b5a0 100644 --- a/Dalamud/Interface/Internal/IDalamudTextureWrap.cs +++ b/Dalamud/Interface/Textures/IDalamudTextureWrap.cs @@ -1,7 +1,10 @@ using System.Numerics; +using Dalamud.Interface.Textures.Internal; + using TerraFX.Interop.Windows; +// ReSharper disable once CheckNamespace namespace Dalamud.Interface.Internal; /// diff --git a/Dalamud/Interface/ISharedImmediateTexture.cs b/Dalamud/Interface/Textures/ISharedImmediateTexture.cs similarity index 99% rename from Dalamud/Interface/ISharedImmediateTexture.cs rename to Dalamud/Interface/Textures/ISharedImmediateTexture.cs index f6c63ee10..f8c727557 100644 --- a/Dalamud/Interface/ISharedImmediateTexture.cs +++ b/Dalamud/Interface/Textures/ISharedImmediateTexture.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; using Dalamud.Interface.Internal; using Dalamud.Utility; -namespace Dalamud.Interface; +namespace Dalamud.Interface.Textures; /// A texture with a backing instance of that is shared across multiple /// requesters. diff --git a/Dalamud/Interface/Textures/Internal/BitmapCodecInfo.cs b/Dalamud/Interface/Textures/Internal/BitmapCodecInfo.cs new file mode 100644 index 000000000..3d5456500 --- /dev/null +++ b/Dalamud/Interface/Textures/Internal/BitmapCodecInfo.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Runtime.InteropServices; + +using Dalamud.Utility; + +using TerraFX.Interop.Windows; + +namespace Dalamud.Interface.Textures.Internal; + +/// Represents an available bitmap codec. +internal sealed class BitmapCodecInfo : IBitmapCodecInfo +{ + /// Initializes a new instance of the class. + /// The source codec info. Ownership is not transferred. + public unsafe BitmapCodecInfo(ComPtr codecInfo) + { + this.Name = ReadStringUsing( + codecInfo, + ((IWICBitmapCodecInfo.Vtbl*)codecInfo.Get()->lpVtbl)->GetFriendlyName); + Guid temp; + codecInfo.Get()->GetContainerFormat(&temp).ThrowOnError(); + this.ContainerGuid = temp; + this.Extensions = ReadStringUsing( + codecInfo, + ((IWICBitmapCodecInfo.Vtbl*)codecInfo.Get()->lpVtbl)->GetFileExtensions) + .Split(','); + this.MimeTypes = ReadStringUsing( + codecInfo, + ((IWICBitmapCodecInfo.Vtbl*)codecInfo.Get()->lpVtbl)->GetMimeTypes) + .Split(','); + } + + /// Gets the friendly name for the codec. + public string Name { get; } + + /// Gets the representing the container. + public Guid ContainerGuid { get; } + + /// Gets the suggested file extensions. + public IReadOnlyCollection Extensions { get; } + + /// Gets the corresponding mime types. + public IReadOnlyCollection MimeTypes { get; } + + private static unsafe string ReadStringUsing( + IWICBitmapCodecInfo* codecInfo, + delegate* unmanaged readFuncPtr) + { + var cch = 0u; + _ = readFuncPtr(codecInfo, 0, null, &cch); + var buf = stackalloc char[(int)cch + 1]; + Marshal.ThrowExceptionForHR(readFuncPtr(codecInfo, cch + 1, (ushort*)buf, &cch)); + return new(buf, 0, (int)cch); + } +} diff --git a/Dalamud/Interface/Internal/DisposeSuppressingTextureWrap.cs b/Dalamud/Interface/Textures/Internal/DisposeSuppressingTextureWrap.cs similarity index 90% rename from Dalamud/Interface/Internal/DisposeSuppressingTextureWrap.cs rename to Dalamud/Interface/Textures/Internal/DisposeSuppressingTextureWrap.cs index d099bae69..17a88e270 100644 --- a/Dalamud/Interface/Internal/DisposeSuppressingTextureWrap.cs +++ b/Dalamud/Interface/Textures/Internal/DisposeSuppressingTextureWrap.cs @@ -1,4 +1,6 @@ -namespace Dalamud.Interface.Internal; +using Dalamud.Interface.Internal; + +namespace Dalamud.Interface.Textures.Internal; /// /// A texture wrap that ignores calls. diff --git a/Dalamud/Interface/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs similarity index 89% rename from Dalamud/Interface/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs rename to Dalamud/Interface/Textures/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs index 06c366601..6cdd0aa25 100644 --- a/Dalamud/Interface/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs +++ b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs @@ -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; /// Represents a sharable texture, based on a file on the system filesystem. internal sealed class FileSystemSharedImmediateTexture : SharedImmediateTexture @@ -42,7 +43,7 @@ internal sealed class FileSystemSharedImmediateTexture : SharedImmediateTexture private async Task CreateTextureAsync(CancellationToken cancellationToken) { - var tm = await Service.GetAsync(); + var tm = await Service.GetAsync(); return await tm.NoThrottleCreateFromFileAsync(this.path, cancellationToken); } } diff --git a/Dalamud/Interface/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs similarity index 85% rename from Dalamud/Interface/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs rename to Dalamud/Interface/Textures/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs index e22998813..a0562f1ef 100644 --- a/Dalamud/Interface/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs +++ b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs @@ -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; /// Represents a sharable texture, based on a file in game resources. internal sealed class GamePathSharedImmediateTexture : SharedImmediateTexture @@ -46,8 +47,9 @@ internal sealed class GamePathSharedImmediateTexture : SharedImmediateTexture private async Task CreateTextureAsync(CancellationToken cancellationToken) { var dm = await Service.GetAsync(); - var tm = await Service.GetAsync(); - if (dm.GetFile(this.path) is not { } file) + var tm = await Service.GetAsync(); + var substPath = tm.GetSubstitutedPath(this.path); + if (dm.GetFile(substPath) is not { } file) throw new FileNotFoundException(); cancellationToken.ThrowIfCancellationRequested(); return tm.NoThrottleCreateFromTexFile(file); diff --git a/Dalamud/Interface/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs similarity index 93% rename from Dalamud/Interface/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs rename to Dalamud/Interface/Textures/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs index c017a0764..c9bdea067 100644 --- a/Dalamud/Interface/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs +++ b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs @@ -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; /// Represents a sharable texture, based on a manifest texture obtained from /// . @@ -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.GetAsync(); + var tm = await Service.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); diff --git a/Dalamud/Interface/Internal/SharedImmediateTextures/SharedImmediateTexture.cs b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/SharedImmediateTexture.cs similarity index 99% rename from Dalamud/Interface/Internal/SharedImmediateTextures/SharedImmediateTexture.cs rename to Dalamud/Interface/Textures/Internal/SharedImmediateTextures/SharedImmediateTexture.cs index 426c61b2c..f730637e4 100644 --- a/Dalamud/Interface/Internal/SharedImmediateTextures/SharedImmediateTexture.cs +++ b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/SharedImmediateTexture.cs @@ -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; /// Represents a texture that may have multiple reference holders (owners). internal abstract class SharedImmediateTexture diff --git a/Dalamud/Interface/Internal/TextureLoadThrottler.cs b/Dalamud/Interface/Textures/Internal/TextureLoadThrottler.cs similarity index 99% rename from Dalamud/Interface/Internal/TextureLoadThrottler.cs rename to Dalamud/Interface/Textures/Internal/TextureLoadThrottler.cs index 043906782..a996a2890 100644 --- a/Dalamud/Interface/Internal/TextureLoadThrottler.cs +++ b/Dalamud/Interface/Textures/Internal/TextureLoadThrottler.cs @@ -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; /// /// Service for managing texture loads. diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.Api9.cs b/Dalamud/Interface/Textures/Internal/TextureManager.Api9.cs new file mode 100644 index 000000000..8e08b43f8 --- /dev/null +++ b/Dalamud/Interface/Textures/Internal/TextureManager.Api9.cs @@ -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 + +/// Service responsible for loading and disposing ImGui texture wraps. +internal sealed partial class TextureManager +{ + /// + [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; + + /// + [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(); + + /// + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + [Obsolete("See interface definition.")] + IDalamudTextureWrap? ITextureProvider.GetTextureFromGame(string path, bool keepAlive) => + this.Shared.GetFromGame(path).GetAvailableOnAccessWrapForApi9(); + + /// + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + [Obsolete("See interface definition.")] + IDalamudTextureWrap? ITextureProvider.GetTextureFromFile(FileInfo file, bool keepAlive) => + this.Shared.GetFromFile(file.FullName).GetAvailableOnAccessWrapForApi9(); +} diff --git a/Dalamud/Interface/Internal/TextureManager.FormatConvert.cs b/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs similarity index 98% rename from Dalamud/Interface/Internal/TextureManager.FormatConvert.cs rename to Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs index 99474dca2..e3a130a14 100644 --- a/Dalamud/Interface/Internal/TextureManager.FormatConvert.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs @@ -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; /// Service responsible for loading and disposing ImGui texture wraps. internal sealed partial class TextureManager @@ -119,16 +120,16 @@ internal sealed partial class TextureManager } /// - 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); - /// - public async Task<(RawImageSpecification Specification, byte[] RawData)> GetRawDataAsync( + /// + public async Task<(RawImageSpecification Specification, byte[] RawData)> GetRawDataFromExistingTextureAsync( IDalamudTextureWrap wrap, Vector2 uv0, Vector2 uv1, diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.GamePath.cs b/Dalamud/Interface/Textures/Internal/TextureManager.GamePath.cs new file mode 100644 index 000000000..455b6f504 --- /dev/null +++ b/Dalamud/Interface/Textures/Internal/TextureManager.GamePath.cs @@ -0,0 +1,120 @@ +using System.Collections.Generic; +using System.IO; + +using Dalamud.Plugin.Services; + +namespace Dalamud.Interface.Textures.Internal; + +/// Service responsible for loading and disposing ImGui texture wraps. +internal sealed partial class TextureManager +{ + private const string IconFileFormat = "ui/icon/{0:D3}000/{1}{2:D6}.tex"; + private const string HighResolutionIconFileFormat = "ui/icon/{0:D3}000/{1}{2:D6}_hr1.tex"; + + /// + public event ITextureSubstitutionProvider.TextureDataInterceptorDelegate? InterceptTexDataLoad; + + /// + public bool TryGetIconPath(in GameIconLookup lookup, out string path) + { + // 1. Item + path = FormatIconPath( + lookup.IconId, + lookup.ItemHq ? "hq/" : string.Empty, + lookup.HiRes); + if (this.dataManager.FileExists(path)) + return true; + + var languageFolder = (lookup.Language ?? (ClientLanguage)(int)this.dalamud.StartInfo.Language) switch + { + ClientLanguage.Japanese => "ja/", + ClientLanguage.English => "en/", + ClientLanguage.German => "de/", + ClientLanguage.French => "fr/", + _ => null, + }; + + if (languageFolder is not null) + { + // 2. Regular icon, with language, hi-res + path = FormatIconPath( + lookup.IconId, + languageFolder, + lookup.HiRes); + if (this.dataManager.FileExists(path)) + return true; + + if (lookup.HiRes) + { + // 3. Regular icon, with language, no hi-res + path = FormatIconPath( + lookup.IconId, + languageFolder, + false); + if (this.dataManager.FileExists(path)) + return true; + } + } + + // 4. Regular icon, without language, hi-res + path = FormatIconPath( + lookup.IconId, + null, + lookup.HiRes); + if (this.dataManager.FileExists(path)) + return true; + + // 4. Regular icon, without language, no hi-res + if (lookup.HiRes) + { + path = FormatIconPath( + lookup.IconId, + null, + false); + if (this.dataManager.FileExists(path)) + return true; + } + + return false; + } + + /// + public string GetIconPath(in GameIconLookup lookup) => + this.TryGetIconPath(lookup, out var path) ? path : throw new FileNotFoundException(); + + /// + public string GetSubstitutedPath(string originalPath) + { + if (this.InterceptTexDataLoad == null) + return originalPath; + + string? interceptPath = null; + this.InterceptTexDataLoad.Invoke(originalPath, ref interceptPath); + + if (interceptPath != null) + { + Log.Verbose("Intercept: {OriginalPath} => {ReplacePath}", originalPath, interceptPath); + return interceptPath; + } + + return originalPath; + } + + /// + public void InvalidatePaths(IEnumerable paths) + { + foreach (var path in paths) + this.Shared.FlushFromGameCache(path); + } + + private static string FormatIconPath(uint iconId, string? type, bool highResolution) + { + var format = highResolution ? HighResolutionIconFileFormat : IconFileFormat; + + type ??= string.Empty; + if (type.Length > 0 && !type.EndsWith("/")) + type += "/"; + + return string.Format(format, iconId / 1000, type, iconId); + } +} diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.SharedTextures.cs b/Dalamud/Interface/Textures/Internal/TextureManager.SharedTextures.cs new file mode 100644 index 000000000..3a121c4c5 --- /dev/null +++ b/Dalamud/Interface/Textures/Internal/TextureManager.SharedTextures.cs @@ -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; + +/// Service responsible for loading and disposing ImGui texture wraps. +internal sealed partial class TextureManager +{ + /// + ISharedImmediateTexture ITextureProvider.GetFromGameIcon(in GameIconLookup lookup) => + this.Shared.GetFromGameIcon(lookup); + + /// + ISharedImmediateTexture ITextureProvider.GetFromGame(string path) => + this.Shared.GetFromGame(path); + + /// + ISharedImmediateTexture ITextureProvider.GetFromFile(string path) => + this.Shared.GetFromFile(path); + + /// + ISharedImmediateTexture ITextureProvider.GetFromManifestResource(Assembly assembly, string name) => + this.Shared.GetFromManifestResource(assembly, name); + + /// A part of texture manager that deals with s. + internal sealed class SharedTextureManager : IDisposable + { + private const int PathLookupLruCount = 8192; + + private readonly TextureManager textureManager; + private readonly ConcurrentLru lookupCache = new(PathLookupLruCount); + private readonly ConcurrentDictionary gameDict = new(); + private readonly ConcurrentDictionary fileDict = new(); + private readonly ConcurrentDictionary<(Assembly, string), SharedImmediateTexture> manifestResourceDict = new(); + private readonly HashSet invalidatedTextures = new(); + + /// Initializes a new instance of the class. + /// An instance of . + public SharedTextureManager(TextureManager textureManager) + { + this.textureManager = textureManager; + this.textureManager.framework.Update += this.FrameworkOnUpdate; + } + + /// Gets all the loaded textures from game resources. + public ICollection ForDebugGamePathTextures => this.gameDict.Values; + + /// Gets all the loaded textures from filesystem. + public ICollection ForDebugFileSystemTextures => this.fileDict.Values; + + /// Gets all the loaded textures from assembly manifest resources. + public ICollection ForDebugManifestResourceTextures => this.manifestResourceDict.Values; + + /// Gets all the loaded textures that are invalidated from . + /// lock on use of the value returned from this property. + [SuppressMessage( + "ReSharper", + "InconsistentlySynchronizedField", + Justification = "Debug use only; users are expected to lock around this")] + public ICollection ForDebugInvalidatedTextures => this.invalidatedTextures; + + /// + public void Dispose() + { + this.textureManager.framework.Update -= this.FrameworkOnUpdate; + this.lookupCache.Clear(); + ReleaseSelfReferences(this.gameDict); + ReleaseSelfReferences(this.fileDict); + ReleaseSelfReferences(this.manifestResourceDict); + return; + + static void ReleaseSelfReferences(ConcurrentDictionary dict) + { + foreach (var v in dict.Values) + v.ReleaseSelfReference(true); + dict.Clear(); + } + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public SharedImmediateTexture GetFromGameIcon(in GameIconLookup lookup) => + this.GetFromGame(this.lookupCache.GetOrAdd(lookup, this.GetIconPathByValue)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public SharedImmediateTexture GetFromGame(string path) => + this.gameDict.GetOrAdd(path, GamePathSharedImmediateTexture.CreatePlaceholder); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public SharedImmediateTexture GetFromFile(string path) => + this.fileDict.GetOrAdd(path, FileSystemSharedImmediateTexture.CreatePlaceholder); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public SharedImmediateTexture GetFromManifestResource(Assembly assembly, string name) => + this.manifestResourceDict.GetOrAdd( + (assembly, name), + ManifestResourceSharedImmediateTexture.CreatePlaceholder); + + /// Invalidates a cached item from and . + /// + /// The path to invalidate. + public void FlushFromGameCache(string path) + { + if (this.gameDict.TryRemove(path, out var r)) + { + if (r.ReleaseSelfReference(true) != 0 || r.HasRevivalPossibility) + { + lock (this.invalidatedTextures) + this.invalidatedTextures.Add(r); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private string GetIconPathByValue(GameIconLookup lookup) => + this.textureManager.TryGetIconPath(lookup, out var path) ? path : throw new FileNotFoundException(); + + private void FrameworkOnUpdate(IFramework unused) + { + RemoveFinalReleased(this.gameDict); + RemoveFinalReleased(this.fileDict); + RemoveFinalReleased(this.manifestResourceDict); + + // ReSharper disable once InconsistentlySynchronizedField + if (this.invalidatedTextures.Count != 0) + { + lock (this.invalidatedTextures) + this.invalidatedTextures.RemoveWhere(TextureFinalReleasePredicate); + } + + return; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static void RemoveFinalReleased(ConcurrentDictionary dict) + { + if (!dict.IsEmpty) + { + foreach (var (k, v) in dict) + { + if (TextureFinalReleasePredicate(v)) + _ = dict.TryRemove(k, out _); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static bool TextureFinalReleasePredicate(SharedImmediateTexture v) => + v.ContentQueried && v.ReleaseSelfReference(false) == 0 && !v.HasRevivalPossibility; + } + } +} diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs b/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs new file mode 100644 index 000000000..6da68b7e0 --- /dev/null +++ b/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs @@ -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; + +/// Service responsible for loading and disposing ImGui texture wraps. +[SuppressMessage( + "StyleCop.CSharp.LayoutRules", + "SA1519:Braces should not be omitted from multi-line child statement", + Justification = "Multiple fixed blocks")] +internal sealed partial class TextureManager +{ + /// + [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? 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); + } + + /// + public async Task SaveToFileAsync( + IDalamudTextureWrap wrap, + Guid containerGuid, + string path, + IReadOnlyDictionary? 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; + } + } + + /// + IEnumerable ITextureProvider.GetSupportedImageDecoderInfos() => + this.Wic.GetSupportedDecoderInfos(); + + /// + IEnumerable ITextureProvider.GetSupportedImageEncoderInfos() => + this.Wic.GetSupportedEncoderInfos(); + + /// Creates a texture from the given bytes of an image file. Skips the load throttler; intended to be used + /// from implementation of s. + /// The data. + /// The cancellation token. + /// The loaded texture. + internal IDalamudTextureWrap NoThrottleCreateFromImage( + ReadOnlyMemory bytes, + CancellationToken cancellationToken = default) + { + ObjectDisposedException.ThrowIf(this.disposing, this); + cancellationToken.ThrowIfCancellationRequested(); + + try + { + using var handle = bytes.Pin(); + using var stream = this.Wic.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); + } + } + } + + /// Creates a texture from the given path to an image file. Skips the load throttler; intended to be used + /// from implementation of s. + /// The path of the file.. + /// The cancellation token. + /// The loaded texture. + internal async Task NoThrottleCreateFromFileAsync( + string path, + CancellationToken cancellationToken = default) + { + ObjectDisposedException.ThrowIf(this.disposing, this); + cancellationToken.ThrowIfCancellationRequested(); + + try + { + using var stream = 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); + } + } + } + + /// A part of texture manager that uses Windows Imaging Component under the hood. + internal sealed class WicManager : IDisposable + { + private readonly TextureManager textureManager; + private ComPtr wicFactory; + + /// Initializes a new instance of the class. + /// An instance of . + public WicManager(TextureManager textureManager) + { + this.textureManager = textureManager; + unsafe + { + fixed (Guid* pclsidWicImagingFactory = &CLSID.CLSID_WICImagingFactory) + fixed (Guid* piidWicImagingFactory = &IID.IID_IWICImagingFactory) + { + CoCreateInstance( + pclsidWicImagingFactory, + null, + (uint)CLSCTX.CLSCTX_INPROC_SERVER, + piidWicImagingFactory, + (void**)this.wicFactory.GetAddressOf()).ThrowOnError(); + } + } + } + + /// + /// Finalizes an instance of the class. + /// + ~WicManager() => this.ReleaseUnmanagedResource(); + + /// + public void Dispose() + { + this.ReleaseUnmanagedResource(); + GC.SuppressFinalize(this); + } + + /// Creates a new instance of from a . + /// An instance of . + /// The number of bytes in the memory. + /// The new instance of . + public unsafe ComPtr CreateIStreamFromMemory(MemoryHandle handle, int length) + { + using var wicStream = default(ComPtr); + this.wicFactory.Get()->CreateStream(wicStream.GetAddressOf()).ThrowOnError(); + wicStream.Get()->InitializeFromMemory((byte*)handle.Pointer, checked((uint)length)).ThrowOnError(); + + var res = default(ComPtr); + wicStream.As(ref res).ThrowOnError(); + return res; + } + + /// Creates a new instance of from a file path. + /// The file path. + /// The new instance of . + public unsafe ComPtr CreateIStreamFromFile(string path) + { + using var wicStream = default(ComPtr); + this.wicFactory.Get()->CreateStream(wicStream.GetAddressOf()).ThrowOnError(); + fixed (char* pPath = path) + wicStream.Get()->InitializeFromFilename((ushort*)pPath, GENERIC_READ).ThrowOnError(); + + var res = default(ComPtr); + wicStream.As(ref res).ThrowOnError(); + return res; + } + + /// Creates a new instance of from a . + /// The stream that will NOT be closed after. + /// The cancellation token. + /// The newly loaded texture. + public unsafe IDalamudTextureWrap NoThrottleCreateFromWicStream( + ComPtr stream, + CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + using var decoder = default(ComPtr); + this.wicFactory.Get()->CreateDecoderFromStream( + stream, + null, + WICDecodeOptions.WICDecodeMetadataCacheOnDemand, + decoder.GetAddressOf()).ThrowOnError(); + + cancellationToken.ThrowIfCancellationRequested(); + + using var frame = default(ComPtr); + decoder.Get()->GetFrame(0, frame.GetAddressOf()).ThrowOnError(); + var pixelFormat = default(Guid); + frame.Get()->GetPixelFormat(&pixelFormat).ThrowOnError(); + var dxgiFormat = GetCorrespondingDxgiFormat(pixelFormat); + + cancellationToken.ThrowIfCancellationRequested(); + + using var bitmapSource = default(ComPtr); + if (dxgiFormat == DXGI_FORMAT.DXGI_FORMAT_UNKNOWN || !this.textureManager.IsDxgiFormatSupported(dxgiFormat)) + { + dxgiFormat = DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM; + pixelFormat = GUID.GUID_WICPixelFormat32bppBGRA; + WICConvertBitmapSource(&pixelFormat, (IWICBitmapSource*)frame.Get(), bitmapSource.GetAddressOf()) + .ThrowOnError(); + } + else + { + frame.As(&bitmapSource); + } + + cancellationToken.ThrowIfCancellationRequested(); + + using var bitmap = default(ComPtr); + using var bitmapLock = default(ComPtr); + WICRect rcLock; + uint stride; + uint cbBufferSize; + byte* pbData; + if (bitmapSource.As(&bitmap).FAILED) + { + bitmapSource.Get()->GetSize((uint*)&rcLock.Width, (uint*)&rcLock.Height).ThrowOnError(); + this.wicFactory.Get()->CreateBitmap( + (uint)rcLock.Width, + (uint)rcLock.Height, + &pixelFormat, + WICBitmapCreateCacheOption.WICBitmapCacheOnDemand, + bitmap.GetAddressOf()).ThrowOnError(); + + bitmap.Get()->Lock( + &rcLock, + (uint)WICBitmapLockFlags.WICBitmapLockWrite, + bitmapLock.ReleaseAndGetAddressOf()) + .ThrowOnError(); + bitmapLock.Get()->GetStride(&stride).ThrowOnError(); + bitmapLock.Get()->GetDataPointer(&cbBufferSize, &pbData).ThrowOnError(); + bitmapSource.Get()->CopyPixels(null, stride, cbBufferSize, pbData).ThrowOnError(); + } + + cancellationToken.ThrowIfCancellationRequested(); + + bitmap.Get()->Lock( + &rcLock, + (uint)WICBitmapLockFlags.WICBitmapLockRead, + bitmapLock.ReleaseAndGetAddressOf()) + .ThrowOnError(); + bitmapSource.Get()->GetSize((uint*)&rcLock.Width, (uint*)&rcLock.Height).ThrowOnError(); + bitmapLock.Get()->GetStride(&stride).ThrowOnError(); + bitmapLock.Get()->GetDataPointer(&cbBufferSize, &pbData).ThrowOnError(); + bitmapSource.Get()->CopyPixels(null, stride, cbBufferSize, pbData).ThrowOnError(); + return this.textureManager.NoThrottleCreateFromRaw( + new(rcLock.Width, rcLock.Height, (int)stride, (int)dxgiFormat), + new(pbData, (int)cbBufferSize)); + } + + /// Gets the supported bitmap codecs. + /// The supported encoders. + public IEnumerable GetSupportedEncoderInfos() + { + foreach (var ptr in new ComponentEnumerable( + this.wicFactory, + WICComponentType.WICEncoder)) + yield return new(ptr); + } + + /// Gets the supported bitmap codecs. + /// The supported decoders. + public IEnumerable GetSupportedDecoderInfos() + { + foreach (var ptr in new ComponentEnumerable( + this.wicFactory, + WICComponentType.WICDecoder)) + yield return new(ptr); + } + + /// Saves the given raw bitmap to a stream. + /// The raw bitmap specifications. + /// The raw bitmap bytes. + /// The container format from . + /// The stream to write to. The ownership is not transferred. + /// The encoder properties. + /// The cancellation token. + public unsafe void SaveToStreamUsingWic( + RawImageSpecification specs, + byte[] bytes, + Guid containerFormat, + ComPtr stream, + IReadOnlyDictionary? props = null, + CancellationToken cancellationToken = default) + { + using var encoder = default(ComPtr); + using var encoderFrame = default(ComPtr); + 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); + 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); + 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); + 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(); + } + + /// + /// Gets the corresponding from a containing a WIC pixel format. + /// + /// The WIC pixel format. + /// The corresponding , or if + /// unavailable. + 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 : IEnumerable> + where T : unmanaged, IWICComponentInfo.Interface + { + private readonly ComPtr factory; + private readonly WICComponentType componentType; + + /// Initializes a new instance of the struct. + /// The WIC factory. Ownership is not transferred. + /// + /// The component type to enumerate. + public ComponentEnumerable(ComPtr factory, WICComponentType componentType) + { + this.factory = factory; + this.componentType = componentType; + } + + public unsafe ManagedIEnumUnknownEnumerator GetEnumerator() + { + var enumUnknown = default(ComPtr); + this.factory.Get()->CreateComponentEnumerator( + (uint)this.componentType, + (uint)WICComponentEnumerateOptions.WICComponentEnumerateDefault, + enumUnknown.GetAddressOf()).ThrowOnError(); + return new(enumUnknown); + } + + IEnumerator> IEnumerable>.GetEnumerator() => this.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + } + } +} diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.cs b/Dalamud/Interface/Textures/Internal/TextureManager.cs new file mode 100644 index 000000000..2a17f4d73 --- /dev/null +++ b/Dalamud/Interface/Textures/Internal/TextureManager.cs @@ -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; + +/// Service responsible for loading and disposing ImGui texture wraps. +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.EarlyLoadedService] +#pragma warning disable SA1015 +[ResolveVia] +[ResolveVia] +#pragma warning restore SA1015 +internal sealed partial class TextureManager : IServiceType, IDisposable, ITextureProvider, ITextureSubstitutionProvider +{ + private static readonly ModuleLog Log = new(nameof(TextureManager)); + + [ServiceManager.ServiceDependency] + private readonly Dalamud dalamud = Service.Get(); + + [ServiceManager.ServiceDependency] + private readonly DataManager dataManager = Service.Get(); + + [ServiceManager.ServiceDependency] + private readonly Framework framework = Service.Get(); + + [ServiceManager.ServiceDependency] + private readonly InterfaceManager interfaceManager = Service.Get(); + + [ServiceManager.ServiceDependency] + private readonly TextureLoadThrottler textureLoadThrottler = Service.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); + } + + /// Gets the shared texture manager. + public SharedTextureManager Shared => + this.sharedTextureManager ?? + throw new ObjectDisposedException(nameof(this.sharedTextureManager)); + + /// Gets the WIC manager. + public WicManager Wic => + this.wicManager ?? + throw new ObjectDisposedException(nameof(this.sharedTextureManager)); + + /// + 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(); + } + + /// + public Task CreateFromImageAsync( + ReadOnlyMemory bytes, + CancellationToken cancellationToken = default) => + this.textureLoadThrottler.LoadTextureAsync( + new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), + ct => Task.Run(() => this.NoThrottleCreateFromImage(bytes.ToArray(), ct), ct), + cancellationToken); + + /// + public Task 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(); + + /// + // It probably doesn't make sense to throttle this, as it copies the passed bytes to GPU without any transformation. + public IDalamudTextureWrap CreateFromRaw( + RawImageSpecification specs, + ReadOnlySpan bytes) => this.NoThrottleCreateFromRaw(specs, bytes); + + /// + public Task CreateFromRawAsync( + RawImageSpecification specs, + ReadOnlyMemory bytes, + CancellationToken cancellationToken = default) => + this.textureLoadThrottler.LoadTextureAsync( + new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), + _ => Task.FromResult(this.NoThrottleCreateFromRaw(specs, bytes.Span)), + cancellationToken); + + /// + public Task 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(); + + /// + public IDalamudTextureWrap CreateFromTexFile(TexFile file) => this.CreateFromTexFileAsync(file).Result; + + /// + public Task CreateFromTexFileAsync( + TexFile file, + CancellationToken cancellationToken = default) => + this.textureLoadThrottler.LoadTextureAsync( + new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), + ct => Task.Run(() => this.NoThrottleCreateFromTexFile(file), ct), + cancellationToken); + + /// + bool ITextureProvider.IsDxgiFormatSupported(int dxgiFormat) => + this.IsDxgiFormatSupported((DXGI_FORMAT)dxgiFormat); + + /// + public bool IsDxgiFormatSupported(DXGI_FORMAT dxgiFormat) + { + if (this.interfaceManager.Scene is not { } scene) + { + _ = Service.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; + } + + /// + internal IDalamudTextureWrap NoThrottleCreateFromRaw( + RawImageSpecification specs, + ReadOnlySpan bytes) + { + if (this.interfaceManager.Scene is not { } scene) + { + _ = Service.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)); + } + + /// Creates a texture from the given . Skips the load throttler; intended to be used + /// from implementation of s. + /// The data. + /// The loaded texture. + internal IDalamudTextureWrap NoThrottleCreateFromTexFile(TexFile file) + { + ObjectDisposedException.ThrowIf(this.disposing, this); + + var buffer = file.TextureBuffer; + var (dxgiFormat, conversion) = TexFile.GetDxgiFormatFromTextureFormat(file.Header.Format, false); + if (conversion != TexFile.DxgiFormatConversion.NoConversion || + !this.IsDxgiFormatSupported((DXGI_FORMAT)dxgiFormat)) + { + dxgiFormat = (int)Format.B8G8R8A8_UNorm; + buffer = buffer.Filter(0, 0, TexFile.TextureFormat.B8G8R8A8); + } + + return this.NoThrottleCreateFromRaw( + RawImageSpecification.From(buffer.Width, buffer.Height, dxgiFormat), + buffer.RawData); + } + + /// Creates a texture from the given , trying to interpret it as a + /// . + /// The file bytes. + /// The loaded texture. + internal IDalamudTextureWrap NoThrottleCreateFromTexFile(ReadOnlySpan fileBytes) + { + ObjectDisposedException.ThrowIf(this.disposing, this); + + if (!TexFileExtensions.IsPossiblyTexFile2D(fileBytes)) + throw new InvalidDataException("The file is not a TexFile."); + + var bytesArray = fileBytes.ToArray(); + var tf = new TexFile(); + typeof(TexFile).GetProperty(nameof(tf.Data))!.GetSetMethod(true)!.Invoke( + tf, + new object?[] { bytesArray }); + typeof(TexFile).GetProperty(nameof(tf.Reader))!.GetSetMethod(true)!.Invoke( + tf, + new object?[] { new LuminaBinaryReader(bytesArray) }); + // Note: FileInfo and FilePath are not used from TexFile; skip it. + return this.NoThrottleCreateFromTexFile(tf); + } +} diff --git a/Dalamud/Interface/Internal/UnknownTextureWrap.cs b/Dalamud/Interface/Textures/Internal/UnknownTextureWrap.cs similarity index 96% rename from Dalamud/Interface/Internal/UnknownTextureWrap.cs rename to Dalamud/Interface/Textures/Internal/UnknownTextureWrap.cs index 41164f2c3..24e9a8bc1 100644 --- a/Dalamud/Interface/Internal/UnknownTextureWrap.cs +++ b/Dalamud/Interface/Textures/Internal/UnknownTextureWrap.cs @@ -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; /// /// A texture wrap that is created by cloning the underlying . diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 3a718ef4c..f28f400c1 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -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; diff --git a/Dalamud/Interface/UldWrapper.cs b/Dalamud/Interface/UldWrapper.cs index 289db6faf..507730662 100644 --- a/Dalamud/Interface/UldWrapper.cs +++ b/Dalamud/Interface/UldWrapper.cs @@ -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; diff --git a/Dalamud/Plugin/Services/ITextureProvider.cs b/Dalamud/Plugin/Services/ITextureProvider.cs index ac6ab8baf..7715bd5d0 100644 --- a/Dalamud/Plugin/Services/ITextureProvider.cs +++ b/Dalamud/Plugin/Services/ITextureProvider.cs @@ -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); + /// Gets the supported bitmap decoders. + /// The supported bitmap decoders. + /// + /// The following functions support the files of the container types pointed by yielded values. + ///
    + ///
  • + ///
  • + ///
  • + ///
  • + ///
+ ///
+ IEnumerable GetSupportedImageDecoderInfos(); + + /// Gets the supported bitmap encoders. + /// The supported bitmap encoders. + /// + /// The following function supports the files of the container types pointed by yielded values. + ///
    + ///
  • + ///
+ ///
+ IEnumerable GetSupportedImageEncoderInfos(); + /// Gets a shared texture corresponding to the given game resource icon specifier. /// A game icon specifier. /// The shared texture that you may use to obtain the loaded texture wrap and load states. + /// This function is under the effect of . + /// ISharedImmediateTexture GetFromGameIcon(in GameIconLookup lookup); /// Gets a shared texture corresponding to the given path to a game resource. /// A path to a game resource. /// The shared texture that you may use to obtain the loaded texture wrap and load states. + /// This function is under the effect of . + /// ISharedImmediateTexture GetFromGame(string path); /// Gets a shared texture corresponding to the given file on the filesystem. @@ -173,26 +201,16 @@ public partial interface ITextureProvider /// then the source data will be returned. /// This function can fail. /// - Task<(RawImageSpecification Specification, byte[] RawData)> GetRawDataAsync( + Task<(RawImageSpecification Specification, byte[] RawData)> GetRawDataFromExistingTextureAsync( IDalamudTextureWrap wrap, Vector2 uv0, Vector2 uv1, int dxgiFormat = 0, CancellationToken cancellationToken = default); - /// Gets the supported image file extensions available for loading. - /// The supported extensions. Each string[] entry indicates that there can be multiple extensions - /// that correspond to one container format. - IEnumerable GetLoadSupportedImageExtensions(); - - /// Gets the supported image file extensions available for saving. - /// The supported extensions. Each string[] entry indicates that there can be multiple extensions - /// that correspond to one container format. - IEnumerable GetSaveSupportedImageExtensions(); - /// Saves a texture wrap to a stream in an image file format. /// The texture wrap to save. - /// The extension of the file to deduce the file format with the leading dot. + /// The container GUID, obtained from . /// The stream to save to. /// Whether to leave open. /// Properties to pass to the encoder. See @@ -202,20 +220,33 @@ public partial interface ITextureProvider /// A task representing the save process. /// /// may be disposed as soon as this function returns. - /// If no image container format corresponding to is found, then the image will - /// be saved in png format. /// - [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? props = null, CancellationToken cancellationToken = default); + + /// Saves a texture wrap to a file as an image file. + /// The texture wrap to save. + /// The container GUID, obtained from . + /// The target file path. The target file will be overwritten if it exist. + /// Properties to pass to the encoder. See + /// Microsoft + /// Learn for available parameters. + /// The cancellation token. + /// A task representing the save process. + /// + /// may be disposed as soon as this function returns. + /// + Task SaveToFileAsync( + IDalamudTextureWrap wrap, + Guid containerGuid, + string path, + IReadOnlyDictionary? props = null, + CancellationToken cancellationToken = default); /// /// Determines whether the system supports the given DXGI format. diff --git a/Dalamud/Plugin/Services/RawImageSpecification.cs b/Dalamud/Plugin/Services/RawImageSpecification.cs index 206ce578e..4d0aa2e9e 100644 --- a/Dalamud/Plugin/Services/RawImageSpecification.cs +++ b/Dalamud/Plugin/Services/RawImageSpecification.cs @@ -9,14 +9,24 @@ namespace Dalamud.Plugin.Services; /// /// The width of the image. /// The height of the image. -/// The pitch of the image. +/// The pitch of the image in bytes. The value may not always exactly match +/// * bytesPerPixelFromDxgiFormat. /// The format of the image. See DXGI_FORMAT. [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."; + + /// Gets the number of bits per pixel. + /// Thrown if is not supported. + public int BitsPerPixel => + GetFormatInfo((DXGI_FORMAT)this.DxgiFormat, out var bitsPerPixel, out _) + ? bitsPerPixel + : throw new NotSupportedException(FormatNotSupportedMessage); + /// /// Creates a new instance of 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 /// The height. /// The format. /// The new instance. + /// Thrown if is not supported. 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 /// The new instance. 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; + } + } } diff --git a/Dalamud/Storage/Assets/DalamudAssetManager.cs b/Dalamud/Storage/Assets/DalamudAssetManager.cs index 9db6d55a4..a9293eb6d 100644 --- a/Dalamud/Storage/Assets/DalamudAssetManager.cs +++ b/Dalamud/Storage/Assets/DalamudAssetManager.cs @@ -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; diff --git a/Dalamud/Utility/TerraFxCom/ManagedIEnumUnknownEnumerator.cs b/Dalamud/Utility/TerraFxCom/ManagedIEnumUnknownEnumerator.cs new file mode 100644 index 000000000..1f1ac9ffb --- /dev/null +++ b/Dalamud/Utility/TerraFxCom/ManagedIEnumUnknownEnumerator.cs @@ -0,0 +1,59 @@ +using System.Collections; +using System.Collections.Generic; + +using TerraFX.Interop.Windows; + +namespace Dalamud.Utility.TerraFxCom; + +/// Managed iterator for . +/// The unknown type. +internal sealed class ManagedIEnumUnknownEnumerator : IEnumerator> + where T : unmanaged, IUnknown.Interface +{ + private ComPtr unknownEnumerator; + private ComPtr current; + + /// Initializes a new instance of the class. + /// An instance of . Ownership is transferred. + public ManagedIEnumUnknownEnumerator(ComPtr unknownEnumerator) => + this.unknownEnumerator = unknownEnumerator; + + /// Finalizes an instance of the class. + ~ManagedIEnumUnknownEnumerator() => this.ReleaseUnmanagedResources(); + + /// + public ComPtr Current => this.current; + + /// + object IEnumerator.Current => this.current; + + /// + public void Dispose() + { + this.ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + + /// + public unsafe bool MoveNext() + { + using var punk = default(ComPtr); + var fetched = 0u; + while (this.unknownEnumerator.Get()->Next(1u, punk.ReleaseAndGetAddressOf(), &fetched) == S.S_OK && fetched == 1) + { + if (punk.As(ref this.current).SUCCEEDED) + return true; + } + + return false; + } + + /// + public unsafe void Reset() => this.unknownEnumerator.Get()->Reset().ThrowOnError(); + + private void ReleaseUnmanagedResources() + { + this.unknownEnumerator.Reset(); + this.current.Reset(); + } +} diff --git a/Dalamud/Utility/ManagedIStream.cs b/Dalamud/Utility/TerraFxCom/ManagedIStream.cs similarity index 88% rename from Dalamud/Utility/ManagedIStream.cs rename to Dalamud/Utility/TerraFxCom/ManagedIStream.cs index 33c05111c..942a9baf3 100644 --- a/Dalamud/Utility/ManagedIStream.cs +++ b/Dalamud/Utility/TerraFxCom/ManagedIStream.cs @@ -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; /// An wrapper for . [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 vtbl; private GCHandle gchThis; @@ -23,11 +23,10 @@ internal sealed unsafe class ManagedIStream : IStream.Interface, IRefCountable private GCHandle gchVtbl; private int refCount; - /// Initializes a new instance of the class. - /// The inner stream. - 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(); + /// Creates a new instance of based on a managed . + /// The inner stream. + /// Whether to leave open on final release. + /// The new instance of based on . + public static ComPtr Create(Stream innerStream, bool leaveOpen = false) + { + try + { + var res = default(ComPtr); + res.Attach(new ManagedIStream(innerStream, leaveOpen)); + return res; + } + catch + { + if (!leaveOpen) + innerStream.Dispose(); + throw; + } + } + /// public HRESULT QueryInterface(Guid* riid, void** ppvObject) { @@ -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;