From 5367d288d60063fc407dabcf8256b7aae7bd5662 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sat, 2 Mar 2024 06:57:12 +0900 Subject: [PATCH] Use WIC to implement ITP.SaveAsImageFormatToStreamAsync --- .../Interface/Internal/InterfaceManager.cs | 42 +- .../Internal/TextureManager.FormatConvert.cs | 328 ++++++++----- .../Interface/Internal/TextureManager.Wic.cs | 273 +++++++++++ Dalamud/Interface/Internal/TextureManager.cs | 30 +- .../Windows/Data/Widgets/TexWidget.cs | 61 ++- Dalamud/Plugin/Services/ITextureProvider.cs | 67 ++- Dalamud/Utility/ManagedIStream.cs | 433 ++++++++++++++++++ 7 files changed, 1105 insertions(+), 129 deletions(-) create mode 100644 Dalamud/Interface/Internal/TextureManager.Wic.cs create mode 100644 Dalamud/Utility/ManagedIStream.cs diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 68a65ebd1..019b462cd 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -287,7 +287,47 @@ internal class InterfaceManager : IDisposable, IServiceType /// Queues an action to be run before Present call. /// The action. - public void RunBeforePresent(Action action) => this.runBeforePresent.Enqueue(action); + /// A that resolves once is run. + public Task RunBeforePresent(Action action) + { + var tcs = new TaskCompletionSource(); + this.runBeforePresent.Enqueue( + () => + { + try + { + action(); + tcs.SetResult(); + } + catch (Exception e) + { + tcs.SetException(e); + } + }); + return tcs.Task; + } + + /// Queues a function to be run before Present call. + /// The type of the return value. + /// The function. + /// A that resolves once is run. + public Task RunBeforePresent(Func func) + { + var tcs = new TaskCompletionSource(); + this.runBeforePresent.Enqueue( + () => + { + try + { + tcs.SetResult(func()); + } + catch (Exception e) + { + tcs.SetException(e); + } + }); + return tcs.Task; + } /// /// Get video memory information. diff --git a/Dalamud/Interface/Internal/TextureManager.FormatConvert.cs b/Dalamud/Interface/Internal/TextureManager.FormatConvert.cs index f35688998..900eb5627 100644 --- a/Dalamud/Interface/Internal/TextureManager.FormatConvert.cs +++ b/Dalamud/Interface/Internal/TextureManager.FormatConvert.cs @@ -75,57 +75,37 @@ internal sealed partial class TextureManager var wrapCopy = wrap.CreateWrapSharingLowLevelResource(); return this.textureLoadThrottler.LoadTextureAsync( new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), - ct => + async _ => { - var tcs = new TaskCompletionSource(); - this.interfaceManager.RunBeforePresent( - () => - { - try - { - ct.ThrowIfCancellationRequested(); - unsafe - { - using var tex = default(ComPtr); - tex.Attach( - this.NoThrottleCreateFromExistingTextureCore( - wrapCopy, - uv0, - uv1, - format, - false)); + using var tex = await this.NoThrottleCreateFromExistingTextureAsync( + wrapCopy, + uv0, + uv1, + format); + using var device = default(ComPtr); + using var srv = default(ComPtr); + var desc = default(D3D11_TEXTURE2D_DESC); + unsafe + { + tex.Get()->GetDevice(device.GetAddressOf()); - using var device = default(ComPtr); - tex.Get()->GetDevice(device.GetAddressOf()); + var srvDesc = new D3D11_SHADER_RESOURCE_VIEW_DESC( + tex, + D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE2D); + device.Get()->CreateShaderResourceView( + (ID3D11Resource*)tex.Get(), + &srvDesc, + srv.GetAddressOf()) + .ThrowOnError(); - using var srv = default(ComPtr); - var srvDesc = new D3D11_SHADER_RESOURCE_VIEW_DESC( - tex, - D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE2D); - device.Get()->CreateShaderResourceView( - (ID3D11Resource*)tex.Get(), - &srvDesc, - srv.GetAddressOf()) - .ThrowOnError(); + tex.Get()->GetDesc(&desc); - var desc = default(D3D11_TEXTURE2D_DESC); - tex.Get()->GetDesc(&desc); - - tcs.SetResult( - new UnknownTextureWrap( - (IUnknown*)srv.Get(), - (int)desc.Width, - (int)desc.Height, - true)); - } - } - catch (Exception e) - { - tcs.SetException(e); - } - }); - - return tcs.Task; + return new UnknownTextureWrap( + (IUnknown*)srv.Get(), + (int)desc.Width, + (int)desc.Height, + true); + } }, cancellationToken) .ContinueWith( @@ -138,95 +118,209 @@ internal sealed partial class TextureManager .Unwrap(); } - private unsafe ID3D11Texture2D* NoThrottleCreateFromExistingTextureCore( + /// + Task<(RawImageSpecification Specification, byte[] RawData)> ITextureProvider.GetRawDataAsync( IDalamudTextureWrap wrap, Vector2 uv0, Vector2 uv1, - DXGI_FORMAT format, - bool enableCpuRead) + int dxgiFormat, + CancellationToken cancellationToken) => + this.GetRawDataAsync(wrap, uv0, uv1, (DXGI_FORMAT)dxgiFormat, cancellationToken); + + /// + public async Task<(RawImageSpecification Specification, byte[] RawData)> GetRawDataAsync( + IDalamudTextureWrap wrap, + Vector2 uv0, + Vector2 uv1, + DXGI_FORMAT dxgiFormat, + CancellationToken cancellationToken) { - ThreadSafety.AssertMainThread(); - - using var resUnk = new ComPtr((IUnknown*)wrap.ImGuiHandle); - + using var resUnk = default(ComPtr); using var texSrv = default(ComPtr); - resUnk.As(&texSrv).ThrowOnError(); - using var device = default(ComPtr); - texSrv.Get()->GetDevice(device.GetAddressOf()); - - using var deviceContext = default(ComPtr); - device.Get()->GetImmediateContext(deviceContext.GetAddressOf()); - + using var context = default(ComPtr); using var tex2D = default(ComPtr); - using (var texRes = default(ComPtr)) + var texDesc = default(D3D11_TEXTURE2D_DESC); + + unsafe { - texSrv.Get()->GetResource(texRes.GetAddressOf()); - texRes.As(&tex2D).ThrowOnError(); + resUnk.Attach((IUnknown*)wrap.ImGuiHandle); + resUnk.Get()->AddRef(); + + resUnk.As(&texSrv).ThrowOnError(); + + texSrv.Get()->GetDevice(device.GetAddressOf()); + + device.Get()->GetImmediateContext(context.GetAddressOf()); + + using (var texRes = default(ComPtr)) + { + texSrv.Get()->GetResource(texRes.GetAddressOf()); + + using var tex2DTemp = default(ComPtr); + texRes.As(&tex2DTemp).ThrowOnError(); + tex2D.Swap(&tex2DTemp); + } + + tex2D.Get()->GetDesc(&texDesc); } + if (texDesc.Format != dxgiFormat && dxgiFormat != DXGI_FORMAT.DXGI_FORMAT_UNKNOWN) + { + using var tmp = await this.NoThrottleCreateFromExistingTextureAsync(wrap, uv0, uv1, dxgiFormat); + unsafe + { + tex2D.Swap(&tmp); + } + } + + cancellationToken.ThrowIfCancellationRequested(); + return await this.interfaceManager.RunBeforePresent( + () => ExtractMappedResource(device, context, tex2D, cancellationToken)); + + static unsafe (RawImageSpecification Specification, byte[] RawData) ExtractMappedResource( + ComPtr device, + ComPtr context, + ComPtr tex2D, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + ID3D11Resource* mapWhat = null; + try + { + using var tmpTex = default(ComPtr); + D3D11_TEXTURE2D_DESC desc; + tex2D.Get()->GetDesc(&desc); + if ((desc.CPUAccessFlags & (uint)D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_READ) == 0) + { + var tmpTexDesc = desc with + { + MipLevels = 1, + ArraySize = 1, + SampleDesc = new(1, 0), + Usage = D3D11_USAGE.D3D11_USAGE_STAGING, + BindFlags = 0u, + CPUAccessFlags = (uint)D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_READ, + MiscFlags = 0u, + }; + device.Get()->CreateTexture2D(&tmpTexDesc, null, tmpTex.GetAddressOf()).ThrowOnError(); + context.Get()->CopyResource((ID3D11Resource*)tmpTex.Get(), (ID3D11Resource*)tex2D.Get()); + + cancellationToken.ThrowIfCancellationRequested(); + } + + D3D11_MAPPED_SUBRESOURCE mapped; + mapWhat = (ID3D11Resource*)(tmpTex.IsEmpty() ? tex2D.Get() : tmpTex.Get()); + context.Get()->Map( + mapWhat, + 0, + D3D11_MAP.D3D11_MAP_READ, + 0, + &mapped).ThrowOnError(); + + var specs = RawImageSpecification.From((int)desc.Width, (int)desc.Height, (int)desc.Format); + var bytes = new Span(mapped.pData, checked((int)mapped.DepthPitch)).ToArray(); + return (specs, bytes); + } + finally + { + if (mapWhat is not null) + context.Get()->Unmap(mapWhat, 0); + } + } + } + + private async Task> NoThrottleCreateFromExistingTextureAsync( + IDalamudTextureWrap wrap, + Vector2 uv0, + Vector2 uv1, + DXGI_FORMAT format) + { + using var resUnk = default(ComPtr); + using var texSrv = default(ComPtr); + using var device = default(ComPtr); + using var context = default(ComPtr); + using var tex2D = default(ComPtr); var texDesc = default(D3D11_TEXTURE2D_DESC); - tex2D.Get()->GetDesc(&texDesc); + + unsafe + { + resUnk.Attach((IUnknown*)wrap.ImGuiHandle); + resUnk.Get()->AddRef(); + + using (var texSrv2 = default(ComPtr)) + { + resUnk.As(&texSrv2).ThrowOnError(); + texSrv.Attach(texSrv2); + texSrv2.Detach(); + } + + texSrv.Get()->GetDevice(device.GetAddressOf()); + + device.Get()->GetImmediateContext(context.GetAddressOf()); + + using (var texRes = default(ComPtr)) + { + texSrv.Get()->GetResource(texRes.GetAddressOf()); + texRes.As(&tex2D).ThrowOnError(); + } + + tex2D.Get()->GetDesc(&texDesc); + } + + var newWidth = checked((uint)MathF.Round((uv1.X - uv0.X) * texDesc.Width)); + var newHeight = checked((uint)MathF.Round((uv1.Y - uv0.Y) * texDesc.Height)); using var tex2DCopyTemp = default(ComPtr); - var tex2DCopyTempDesc = new D3D11_TEXTURE2D_DESC + unsafe { - Width = checked((uint)MathF.Round((uv1.X - uv0.X) * wrap.Width)), - Height = checked((uint)MathF.Round((uv1.Y - uv0.Y) * wrap.Height)), - MipLevels = 1, - ArraySize = 1, - Format = format, - SampleDesc = new(1, 0), - Usage = D3D11_USAGE.D3D11_USAGE_DEFAULT, - BindFlags = (uint)(D3D11_BIND_FLAG.D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_FLAG.D3D11_BIND_RENDER_TARGET), - CPUAccessFlags = 0u, - MiscFlags = 0u, - }; - device.Get()->CreateTexture2D(&tex2DCopyTempDesc, null, tex2DCopyTemp.GetAddressOf()).ThrowOnError(); - - using (var rtvCopyTemp = default(ComPtr)) - { - var rtvCopyTempDesc = new D3D11_RENDER_TARGET_VIEW_DESC( - tex2DCopyTemp, - D3D11_RTV_DIMENSION.D3D11_RTV_DIMENSION_TEXTURE2D); - device.Get()->CreateRenderTargetView( - (ID3D11Resource*)tex2DCopyTemp.Get(), - &rtvCopyTempDesc, - rtvCopyTemp.GetAddressOf()).ThrowOnError(); - - this.drawsOneSquare ??= new(); - this.drawsOneSquare.Setup(device.Get()); - - deviceContext.Get()->OMSetRenderTargets(1u, rtvCopyTemp.GetAddressOf(), null); - this.drawsOneSquare.Draw( - deviceContext.Get(), - texSrv.Get(), - (int)tex2DCopyTempDesc.Width, - (int)tex2DCopyTempDesc.Height, - uv0, - uv1); - deviceContext.Get()->OMSetRenderTargets(0, null, null); + var tex2DCopyTempDesc = new D3D11_TEXTURE2D_DESC + { + Width = newWidth, + Height = newHeight, + MipLevels = 1, + ArraySize = 1, + Format = format, + SampleDesc = new(1, 0), + Usage = D3D11_USAGE.D3D11_USAGE_DEFAULT, + BindFlags = (uint)(D3D11_BIND_FLAG.D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_FLAG.D3D11_BIND_RENDER_TARGET), + CPUAccessFlags = 0u, + MiscFlags = 0u, + }; + device.Get()->CreateTexture2D(&tex2DCopyTempDesc, null, tex2DCopyTemp.GetAddressOf()).ThrowOnError(); } - if (!enableCpuRead) - { - tex2DCopyTemp.Get()->AddRef(); - return tex2DCopyTemp.Get(); - } + await this.interfaceManager.RunBeforePresent( + () => + { + unsafe + { + using var rtvCopyTemp = default(ComPtr); + var rtvCopyTempDesc = new D3D11_RENDER_TARGET_VIEW_DESC( + tex2DCopyTemp, + D3D11_RTV_DIMENSION.D3D11_RTV_DIMENSION_TEXTURE2D); + device.Get()->CreateRenderTargetView( + (ID3D11Resource*)tex2DCopyTemp.Get(), + &rtvCopyTempDesc, + rtvCopyTemp.GetAddressOf()).ThrowOnError(); - using var tex2DTarget = default(ComPtr); - var tex2DTargetDesc = tex2DCopyTempDesc with - { - Usage = D3D11_USAGE.D3D11_USAGE_DYNAMIC, - BindFlags = 0u, - CPUAccessFlags = (uint)D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_READ, - }; - device.Get()->CreateTexture2D(&tex2DTargetDesc, null, tex2DTarget.GetAddressOf()).ThrowOnError(); + this.drawsOneSquare ??= new(); + this.drawsOneSquare.Setup(device.Get()); - deviceContext.Get()->CopyResource((ID3D11Resource*)tex2DTarget.Get(), (ID3D11Resource*)tex2DCopyTemp.Get()); + context.Get()->OMSetRenderTargets(1u, rtvCopyTemp.GetAddressOf(), null); + this.drawsOneSquare.Draw( + context.Get(), + texSrv.Get(), + (int)newWidth, + (int)newHeight, + uv0, + uv1); + context.Get()->OMSetRenderTargets(0, null, null); + } + }); - tex2DTarget.Get()->AddRef(); - return tex2DTarget.Get(); + return new(tex2DCopyTemp); } [SuppressMessage( diff --git a/Dalamud/Interface/Internal/TextureManager.Wic.cs b/Dalamud/Interface/Internal/TextureManager.Wic.cs new file mode 100644 index 000000000..1adeccede --- /dev/null +++ b/Dalamud/Interface/Internal/TextureManager.Wic.cs @@ -0,0 +1,273 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Numerics; +using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Utility; + +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 factory; + + /// + [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()) + { + 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 GetSupportedImageExtensions() => this.GetSupportedContainerFormats().Values; + + [SuppressMessage( + "StyleCop.CSharp.LayoutRules", + "SA1519:Braces should not be omitted from multi-line child statement", + Justification = "Multiple fixed blocks")] + private unsafe Dictionary GetSupportedContainerFormats() + { + var result = new Dictionary(); + using var enumUnknown = default(ComPtr); + this.factory.Get()->CreateComponentEnumerator( + (uint)WICComponentType.WICEncoder, + (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.factory.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(); + + if (guidPixelFormat == GUID.GUID_WICPixelFormat32bppBGRA) + { + fixed (byte* pByte = bytes) + { + encoderFrame.Get()->WritePixels( + (uint)specs.Height, + (uint)specs.Pitch, + checked((uint)bytes.Length), + pByte).ThrowOnError(); + } + } + else + { + using var tempBitmap = default(ComPtr); + fixed (Guid* pGuid = &GUID.GUID_WICPixelFormat32bppBGRA) + fixed (byte* pBytes = bytes) + { + this.factory.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 index 1041fc00c..8e61e42b0 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -27,6 +27,9 @@ using SharpDX.Direct3D11; using SharpDX.DXGI; using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +using static TerraFX.Interop.Windows.Windows; namespace Dalamud.Interface.Internal; @@ -75,8 +78,31 @@ internal sealed partial class TextureManager : IServiceType, IDisposable, ITextu 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; + 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.factory.GetAddressOf()).ThrowOnError(); + } + } + } + + /// Finalizes an instance of the class. + ~TextureManager() => this.Dispose(); /// public event ITextureSubstitutionProvider.TextureDataInterceptorDelegate? InterceptTexDataLoad; @@ -114,6 +140,8 @@ internal sealed partial class TextureManager : IServiceType, IDisposable, ITextu this.drawsOneSquare?.Dispose(); this.drawsOneSquare = null; + this.factory.Reset(); + return; static void ReleaseSelfReferences(ConcurrentDictionary dict) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs index 9a63dbcb9..a39a48f66 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -7,6 +7,7 @@ using System.Runtime.Loader; 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.Utility; @@ -16,6 +17,8 @@ using Dalamud.Utility; using ImGuiNET; +using Serilog; + using TerraFX.Interop.DirectX; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; @@ -42,6 +45,7 @@ internal class TexWidget : IDataWindowWidget private Vector4 inputTintCol = Vector4.One; private Vector2 inputTexScale = Vector2.Zero; private TextureManager textureManager = null!; + private FileDialogManager fileDialogManager = null!; private string[]? supportedRenderTargetFormatNames; private DXGI_FORMAT[]? supportedRenderTargetFormats; @@ -74,6 +78,7 @@ internal class TexWidget : IDataWindowWidget this.inputManifestResourceNameIndex = 0; this.supportedRenderTargetFormats = null; this.supportedRenderTargetFormatNames = null; + this.fileDialogManager = new(); this.Ready = true; } @@ -233,6 +238,8 @@ internal class TexWidget : IDataWindowWidget } runLater?.Invoke(); + + this.fileDialogManager.Draw(); } private unsafe void DrawLoadedTextures(ICollection textures) @@ -241,11 +248,11 @@ internal class TexWidget : IDataWindowWidget if (!ImGui.BeginTable("##table", 6)) return; - const int numIcons = 3; + const int numIcons = 4; float iconWidths; using (im.IconFontHandle?.Push()) { - iconWidths = ImGui.CalcTextSize(FontAwesomeIcon.Image.ToIconString()).X; + iconWidths = ImGui.CalcTextSize(FontAwesomeIcon.Save.ToIconString()).X; iconWidths += ImGui.CalcTextSize(FontAwesomeIcon.Sync.ToIconString()).X; iconWidths += ImGui.CalcTextSize(FontAwesomeIcon.Trash.ToIconString()).X; } @@ -315,7 +322,24 @@ internal class TexWidget : IDataWindowWidget ImGui.TextUnformatted(texture.HasRevivalPossibility ? "Yes" : "No"); ImGui.TableNextColumn(); - ImGuiComponents.IconButton(FontAwesomeIcon.Image); + if (ImGuiComponents.IconButton(FontAwesomeIcon.Save)) + { + this.fileDialogManager.SaveFileDialog( + "Save texture...", + string.Join( + ',', + this.textureManager + .GetSupportedImageExtensions() + .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)); + }); + } + if (ImGui.IsItemHovered() && texture.GetWrapOrDefault(null) is { } immediate) { ImGui.BeginTooltip(); @@ -351,6 +375,37 @@ internal class TexWidget : IDataWindowWidget ImGuiHelpers.ScaledDummy(10); } + private async void SaveImmediateTexture(ISharedImmediateTexture texture, string path) + { + try + { + using var rented = await texture.RentAsync(); + await this.textureManager.SaveAsImageFormatToStreamAsync( + rented, + 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); diff --git a/Dalamud/Plugin/Services/ITextureProvider.cs b/Dalamud/Plugin/Services/ITextureProvider.cs index a18ac5b2a..723801c9d 100644 --- a/Dalamud/Plugin/Services/ITextureProvider.cs +++ b/Dalamud/Plugin/Services/ITextureProvider.cs @@ -1,4 +1,5 @@ -using System.Diagnostics.CodeAnalysis; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Numerics; using System.Reflection; @@ -112,9 +113,7 @@ public partial interface ITextureProvider /// A texture wrap that can be used to render the texture. Dispose after use. IDalamudTextureWrap CreateFromTexFile(TexFile file); - /// - /// Get a texture handle for the specified Lumina . - /// + /// Get a texture handle for the specified Lumina . /// The texture to obtain a handle to. /// The cancellation token. /// A texture wrap that can be used to render the texture. Dispose after use. @@ -143,9 +142,7 @@ public partial interface ITextureProvider /// The shared texture that you may use to obtain the loaded texture wrap and load states. ISharedImmediateTexture GetFromManifestResource(Assembly assembly, string name); - /// - /// Get a path for a specific icon's .tex file. - /// + /// Get a path for a specific icon's .tex file. /// The icon lookup. /// The path to the icon. /// If a corresponding file could not be found. @@ -159,6 +156,62 @@ public partial interface ITextureProvider /// true if the corresponding file exists and has been set. bool TryGetIconPath(in GameIconLookup lookup, [NotNullWhen(true)] out string? path); + /// Gets the raw data of a texture wrap. + /// The source texture wrap. The passed value may be disposed once this function returns, + /// without having to wait for the completion of the returned . + /// The left top coordinates relative to the size of the source texture. + /// The right bottom coordinates relative to the size of the source texture. + /// The desired target format. + /// If 0 (unknown) is passed, then the format will not be converted. + /// The cancellation token. + /// The raw data and its specifications. + /// + /// The length of the returned RawData may not match + /// * . + /// If is , + /// is , and is 0, + /// then the source data will be returned. + /// This function can fail. + /// + Task<(RawImageSpecification Specification, byte[] RawData)> GetRawDataAsync( + IDalamudTextureWrap wrap, + Vector2 uv0, + Vector2 uv1, + int dxgiFormat = 0, + CancellationToken cancellationToken = default); + + /// Gets the supported image file extensions. + /// The supported extensions. Each string[] entry indicates that there can be multiple extensions + /// that correspond to one container format. + IEnumerable GetSupportedImageExtensions(); + + /// 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 stream to save to. + /// Whether to leave open. + /// 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. + /// 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( + IDalamudTextureWrap wrap, + string extension, + Stream stream, + bool leaveOpen = false, + IReadOnlyDictionary? props = null, + CancellationToken cancellationToken = default); + /// /// Determines whether the system supports the given DXGI format. /// For use with . diff --git a/Dalamud/Utility/ManagedIStream.cs b/Dalamud/Utility/ManagedIStream.cs new file mode 100644 index 000000000..33c05111c --- /dev/null +++ b/Dalamud/Utility/ManagedIStream.cs @@ -0,0 +1,433 @@ +using System.Buffers; +using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +using TerraFX.Interop; +using TerraFX.Interop.Windows; + +namespace Dalamud.Utility; + +/// An wrapper for . +[Guid("a620678b-56b9-4202-a1da-b821214dc972")] +internal sealed unsafe class ManagedIStream : IStream.Interface, IRefCountable +{ + private static readonly Guid MyGuid = typeof(ManagedIStream).GUID; + + private readonly Stream inner; + private readonly nint[] comObject; + private readonly IStream.Vtbl vtbl; + private GCHandle gchThis; + private GCHandle gchComObject; + private GCHandle gchVtbl; + private int refCount; + + /// Initializes a new instance of the class. + /// The inner stream. + public ManagedIStream(Stream inner) + { + this.inner = inner; + this.comObject = new nint[2]; + + this.vtbl.QueryInterface = &QueryInterfaceStatic; + this.vtbl.AddRef = &AddRefStatic; + this.vtbl.Release = &ReleaseStatic; + this.vtbl.Read = &ReadStatic; + this.vtbl.Write = &WriteStatic; + this.vtbl.Seek = &SeekStatic; + this.vtbl.SetSize = &SetSizeStatic; + this.vtbl.CopyTo = &CopyToStatic; + this.vtbl.Commit = &CommitStatic; + this.vtbl.Revert = &RevertStatic; + this.vtbl.LockRegion = &LockRegionStatic; + this.vtbl.UnlockRegion = &UnlockRegionStatic; + this.vtbl.Stat = &StatStatic; + this.vtbl.Clone = &CloneStatic; + + this.gchThis = GCHandle.Alloc(this); + this.gchVtbl = GCHandle.Alloc(this.vtbl, GCHandleType.Pinned); + this.gchComObject = GCHandle.Alloc(this.comObject, GCHandleType.Pinned); + this.comObject[0] = this.gchVtbl.AddrOfPinnedObject(); + this.comObject[1] = GCHandle.ToIntPtr(this.gchThis); + this.refCount = 1; + + return; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static IStream.Interface? ToManagedObject(void* pThis) => + GCHandle.FromIntPtr(((nint*)pThis)[1]).Target as IStream.Interface; + + [UnmanagedCallersOnly] + static int QueryInterfaceStatic(IStream* pThis, Guid* riid, void** ppvObject) => + ToManagedObject(pThis)?.QueryInterface(riid, ppvObject) ?? E.E_FAIL; + + [UnmanagedCallersOnly] + static uint AddRefStatic(IStream* pThis) => ToManagedObject(pThis)?.AddRef() ?? 0; + + [UnmanagedCallersOnly] + static uint ReleaseStatic(IStream* pThis) => ToManagedObject(pThis)?.Release() ?? 0; + + [UnmanagedCallersOnly] + static int ReadStatic(IStream* pThis, void* pv, uint cb, uint* pcbRead) => + ToManagedObject(pThis)?.Read(pv, cb, pcbRead) ?? E.E_FAIL; + + [UnmanagedCallersOnly] + static int WriteStatic(IStream* pThis, void* pv, uint cb, uint* pcbWritten) => + ToManagedObject(pThis)?.Write(pv, cb, pcbWritten) ?? E.E_FAIL; + + [UnmanagedCallersOnly] + static int SeekStatic( + IStream* pThis, LARGE_INTEGER dlibMove, uint dwOrigin, ULARGE_INTEGER* plibNewPosition) => + ToManagedObject(pThis)?.Seek(dlibMove, dwOrigin, plibNewPosition) ?? E.E_FAIL; + + [UnmanagedCallersOnly] + static int SetSizeStatic(IStream* pThis, ULARGE_INTEGER libNewSize) => + ToManagedObject(pThis)?.SetSize(libNewSize) ?? E.E_FAIL; + + [UnmanagedCallersOnly] + static int CopyToStatic( + IStream* pThis, IStream* pstm, ULARGE_INTEGER cb, ULARGE_INTEGER* pcbRead, + ULARGE_INTEGER* pcbWritten) => + ToManagedObject(pThis)?.CopyTo(pstm, cb, pcbRead, pcbWritten) ?? E.E_FAIL; + + [UnmanagedCallersOnly] + static int CommitStatic(IStream* pThis, uint grfCommitFlags) => + ToManagedObject(pThis)?.Commit(grfCommitFlags) ?? E.E_FAIL; + + [UnmanagedCallersOnly] + static int RevertStatic(IStream* pThis) => ToManagedObject(pThis)?.Revert() ?? E.E_FAIL; + + [UnmanagedCallersOnly] + static int LockRegionStatic(IStream* pThis, ULARGE_INTEGER libOffset, ULARGE_INTEGER cb, uint dwLockType) => + ToManagedObject(pThis)?.LockRegion(libOffset, cb, dwLockType) ?? E.E_FAIL; + + [UnmanagedCallersOnly] + static int UnlockRegionStatic( + IStream* pThis, ULARGE_INTEGER libOffset, ULARGE_INTEGER cb, uint dwLockType) => + ToManagedObject(pThis)?.UnlockRegion(libOffset, cb, dwLockType) ?? E.E_FAIL; + + [UnmanagedCallersOnly] + static int StatStatic(IStream* pThis, STATSTG* pstatstg, uint grfStatFlag) => + ToManagedObject(pThis)?.Stat(pstatstg, grfStatFlag) ?? E.E_FAIL; + + [UnmanagedCallersOnly] + static int CloneStatic(IStream* pThis, IStream** ppstm) => ToManagedObject(pThis)?.Clone(ppstm) ?? E.E_FAIL; + } + + /// + public static Guid* NativeGuid => (Guid*)Unsafe.AsPointer(ref Unsafe.AsRef(in MyGuid)); + + public static implicit operator IUnknown*(ManagedIStream mis) => + (IUnknown*)mis.gchComObject.AddrOfPinnedObject(); + + public static implicit operator ISequentialStream*(ManagedIStream mis) => + (ISequentialStream*)mis.gchComObject.AddrOfPinnedObject(); + + public static implicit operator IStream*(ManagedIStream mis) => + (IStream*)mis.gchComObject.AddrOfPinnedObject(); + + /// + public HRESULT QueryInterface(Guid* riid, void** ppvObject) + { + if (ppvObject == null) + return E.E_POINTER; + + if (*riid == IID.IID_IUnknown || + *riid == IID.IID_ISequentialStream || + *riid == IID.IID_IStream || + *riid == MyGuid) + { + try + { + this.AddRef(); + } + catch + { + return E.E_FAIL; + } + + *ppvObject = (IUnknown*)this; + return S.S_OK; + } + + *ppvObject = null; + return E.E_NOINTERFACE; + } + + /// + public int AddRef() => IRefCountable.AlterRefCount(1, ref this.refCount, out var newRefCount) switch + { + IRefCountable.RefCountResult.StillAlive => newRefCount, + IRefCountable.RefCountResult.AlreadyDisposed => throw new ObjectDisposedException(nameof(ManagedIStream)), + IRefCountable.RefCountResult.FinalRelease => throw new InvalidOperationException(), + _ => throw new InvalidOperationException(), + }; + + /// + public int Release() + { + switch (IRefCountable.AlterRefCount(-1, ref this.refCount, out var newRefCount)) + { + case IRefCountable.RefCountResult.StillAlive: + return newRefCount; + + case IRefCountable.RefCountResult.FinalRelease: + this.gchThis.Free(); + this.gchComObject.Free(); + this.gchVtbl.Free(); + return newRefCount; + + case IRefCountable.RefCountResult.AlreadyDisposed: + throw new ObjectDisposedException(nameof(ManagedIStream)); + + default: + throw new InvalidOperationException(); + } + } + + /// + uint IUnknown.Interface.AddRef() + { + try + { + return (uint)this.AddRef(); + } + catch + { + return 0; + } + } + + /// + uint IUnknown.Interface.Release() + { + try + { + return (uint)this.Release(); + } + catch + { + return 0; + } + } + + /// + public HRESULT Read(void* pv, uint cb, uint* pcbRead) + { + if (pcbRead == null) + { + var tmp = stackalloc uint[1]; + pcbRead = tmp; + } + + ref var read = ref *pcbRead; + for (read = 0u; read < cb;) + { + var chunkSize = unchecked((int)Math.Min(0x10000000u, cb)); + var chunkRead = (uint)this.inner.Read(new(pv, chunkSize)); + if (chunkRead == 0) + break; + pv = (byte*)pv + chunkRead; + read += chunkRead; + } + + return read == cb ? S.S_OK : S.S_FALSE; + } + + /// + public HRESULT Write(void* pv, uint cb, uint* pcbWritten) + { + if (pcbWritten == null) + { + var tmp = stackalloc uint[1]; + pcbWritten = tmp; + } + + ref var written = ref *pcbWritten; + try + { + for (written = 0u; written < cb;) + { + var chunkSize = Math.Min(0x10000000u, cb); + this.inner.Write(new(pv, (int)chunkSize)); + pv = (byte*)pv + chunkSize; + written += chunkSize; + } + + return S.S_OK; + } + catch (Exception e) when (e.HResult == unchecked((int)(0x80070000u | ERROR.ERROR_HANDLE_DISK_FULL))) + { + return STG.STG_E_MEDIUMFULL; + } + catch (Exception e) when (e.HResult == unchecked((int)(0x80070000u | ERROR.ERROR_DISK_FULL))) + { + return STG.STG_E_MEDIUMFULL; + } + catch (IOException) + { + return STG.STG_E_CANTSAVE; + } + } + + /// + public HRESULT Seek(LARGE_INTEGER dlibMove, uint dwOrigin, ULARGE_INTEGER* plibNewPosition) + { + SeekOrigin seekOrigin; + + switch ((STREAM_SEEK)dwOrigin) + { + case STREAM_SEEK.STREAM_SEEK_SET: + seekOrigin = SeekOrigin.Begin; + break; + case STREAM_SEEK.STREAM_SEEK_CUR: + seekOrigin = SeekOrigin.Current; + break; + case STREAM_SEEK.STREAM_SEEK_END: + seekOrigin = SeekOrigin.End; + break; + default: + return STG.STG_E_INVALIDFUNCTION; + } + + try + { + var position = this.inner.Seek(dlibMove.QuadPart, seekOrigin); + if (plibNewPosition != null) + { + *plibNewPosition = new() { QuadPart = (ulong)position }; + } + + return S.S_OK; + } + catch + { + return STG.STG_E_INVALIDFUNCTION; + } + } + + /// + public HRESULT SetSize(ULARGE_INTEGER libNewSize) + { + try + { + this.inner.SetLength(checked((long)libNewSize.QuadPart)); + return S.S_OK; + } + catch (Exception e) when (e.HResult == unchecked((int)(0x80070000u | ERROR.ERROR_HANDLE_DISK_FULL))) + { + return STG.STG_E_MEDIUMFULL; + } + catch (Exception e) when (e.HResult == unchecked((int)(0x80070000u | ERROR.ERROR_DISK_FULL))) + { + return STG.STG_E_MEDIUMFULL; + } + catch (IOException) + { + return STG.STG_E_INVALIDFUNCTION; + } + } + + /// + public HRESULT CopyTo(IStream* pstm, ULARGE_INTEGER cb, ULARGE_INTEGER* pcbRead, ULARGE_INTEGER* pcbWritten) + { + if (pcbRead == null) + { + var temp = stackalloc ULARGE_INTEGER[1]; + pcbRead = temp; + } + + if (pcbWritten == null) + { + var temp = stackalloc ULARGE_INTEGER[1]; + pcbWritten = temp; + } + + ref var cbRead = ref pcbRead->QuadPart; + ref var cbWritten = ref pcbWritten->QuadPart; + cbRead = cbWritten = 0; + + var buf = ArrayPool.Shared.Rent(8192); + try + { + fixed (byte* pbuf = buf) + { + while (cbRead < cb) + { + var read = checked((uint)this.inner.Read(buf.AsSpan())); + if (read == 0) + break; + cbRead += read; + + var written = 0u; + var writeResult = pstm->Write(pbuf, read, &written); + if (writeResult.FAILED) + return writeResult; + cbWritten += written; + } + } + + return S.S_OK; + } + catch (Exception e) when (e.HResult == unchecked((int)(0x80070000u | ERROR.ERROR_HANDLE_DISK_FULL))) + { + return STG.STG_E_MEDIUMFULL; + } + catch (Exception e) when (e.HResult == unchecked((int)(0x80070000u | ERROR.ERROR_DISK_FULL))) + { + return STG.STG_E_MEDIUMFULL; + } + catch (Exception e) + { + // Undefined return value according to the documentation, but meh + return e.HResult < 0 ? e.HResult : E.E_FAIL; + } + finally + { + ArrayPool.Shared.Return(buf); + } + } + + /// + // On streams open in direct mode, this method has no effect. + public HRESULT Commit(uint grfCommitFlags) => S.S_OK; + + /// + // On streams open in direct mode, this method has no effect. + public HRESULT Revert() => S.S_OK; + + /// + // Locking is not supported at all or the specific type of lock requested is not supported. + public HRESULT LockRegion(ULARGE_INTEGER libOffset, ULARGE_INTEGER cb, uint dwLockType) => + STG.STG_E_INVALIDFUNCTION; + + /// + // Locking is not supported at all or the specific type of lock requested is not supported. + public HRESULT UnlockRegion(ULARGE_INTEGER libOffset, ULARGE_INTEGER cb, uint dwLockType) => + STG.STG_E_INVALIDFUNCTION; + + /// + public HRESULT Stat(STATSTG* pstatstg, uint grfStatFlag) + { + if (pstatstg is null) + return STG.STG_E_INVALIDPOINTER; + ref var streamStats = ref *pstatstg; + streamStats.type = (uint)STGTY.STGTY_STREAM; + streamStats.cbSize = (ulong)this.inner.Length; + streamStats.grfMode = 0; + if (this.inner.CanRead && this.inner.CanWrite) + streamStats.grfMode |= STGM.STGM_READWRITE; + else if (this.inner.CanRead) + streamStats.grfMode |= STGM.STGM_READ; + else if (this.inner.CanWrite) + streamStats.grfMode |= STGM.STGM_WRITE; + else + return STG.STG_E_REVERTED; + return S.S_OK; + } + + /// + // Undefined return value according to the documentation, but meh + public HRESULT Clone(IStream** ppstm) => E.E_NOTIMPL; +}