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;
+}