diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 7e9c8eed0..68a65ebd1 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -70,6 +70,8 @@ internal class InterfaceManager : IDisposable, IServiceType [ServiceManager.ServiceDependency] private readonly DalamudIme dalamudIme = Service.Get(); + private readonly ConcurrentQueue runBeforePresent = new(); + private readonly SwapChainVtableResolver address = new(); private readonly Hook setCursorHook; private RawDX11Scene? scene; @@ -283,6 +285,10 @@ internal class InterfaceManager : IDisposable, IServiceType this.deferredDisposeDisposables.Add(locked); } + /// Queues an action to be run before Present call. + /// The action. + public void RunBeforePresent(Action action) => this.runBeforePresent.Enqueue(action); + /// /// Get video memory information. /// @@ -520,6 +526,9 @@ internal class InterfaceManager : IDisposable, IServiceType if (!this.dalamudAtlas!.HasBuiltAtlas) return this.presentHook!.Original(swapChain, syncInterval, presentFlags); + while (this.runBeforePresent.TryDequeue(out var action)) + action.InvokeSafely(); + if (this.address.IsReshade) { var pRes = this.presentHook!.Original(swapChain, syncInterval, presentFlags); diff --git a/Dalamud/Interface/Internal/TextureManager.FormatConvert.cs b/Dalamud/Interface/Internal/TextureManager.FormatConvert.cs new file mode 100644 index 000000000..a11cf6c53 --- /dev/null +++ b/Dalamud/Interface/Internal/TextureManager.FormatConvert.cs @@ -0,0 +1,484 @@ +using System.Buffers; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Plugin.Services; +using Dalamud.Utility; + +using ImGuiNET; + +using SharpDX.Direct3D11; +using SharpDX.DXGI; + +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +namespace Dalamud.Interface.Internal; + +/// Service responsible for loading and disposing ImGui texture wraps. +internal sealed partial class TextureManager +{ + private DrawsOneSquare? drawsOneSquare; + + /// + bool ITextureProvider.IsDxgiFormatSupportedForCreateFromExistingTextureAsync(int dxgiFormat) => + this.IsDxgiFormatSupportedForCreateFromExistingTextureAsync((DXGI_FORMAT)dxgiFormat); + + /// + public bool IsDxgiFormatSupportedForCreateFromExistingTextureAsync(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); + return (support & FormatSupport.RenderTarget) != 0 + && (support & FormatSupport.Texture2D) != 0; + } + + /// + Task ITextureProvider.CreateFromExistingTextureAsync( + IDalamudTextureWrap wrap, + Vector2 uv0, + Vector2 uv1, + int dxgiFormat, + CancellationToken cancellationToken) => + this.CreateFromExistingTextureAsync(wrap, uv0, uv1, (DXGI_FORMAT)dxgiFormat, cancellationToken); + + /// + public Task CreateFromExistingTextureAsync( + IDalamudTextureWrap wrap, + Vector2 uv0, + Vector2 uv1, + DXGI_FORMAT format, + CancellationToken cancellationToken = default) + { + var wrapCopy = wrap.CreateWrapSharingLowLevelResource(); + return this.textureLoadThrottler.LoadTextureAsync( + new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), + ct => + { + var tcs = new TaskCompletionSource(); + this.interfaceManager.RunBeforePresent( + () => + { + try + { + ct.ThrowIfCancellationRequested(); + unsafe + { + using var tex = new ComPtr( + this.NoThrottleCreateFromExistingTextureCore( + wrapCopy, + uv0, + uv1, + format, + false)); + + using var device = default(ComPtr); + tex.Get()->GetDevice(device.GetAddressOf()); + + 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(); + + 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; + }, + cancellationToken) + .ContinueWith( + r => + { + wrapCopy.Dispose(); + return r; + }, + default(CancellationToken)) + .Unwrap(); + } + + private unsafe ID3D11Texture2D* NoThrottleCreateFromExistingTextureCore( + IDalamudTextureWrap wrap, + Vector2 uv0, + Vector2 uv1, + DXGI_FORMAT format, + bool enableCpuRead) + { + ThreadSafety.AssertMainThread(); + + using var resUnk = new ComPtr((IUnknown*)wrap.ImGuiHandle); + + 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 tex2D = default(ComPtr); + using (var texRes = default(ComPtr)) + { + texSrv.Get()->GetResource(texRes.GetAddressOf()); + texRes.As(&tex2D).ThrowOnError(); + } + + var texDesc = default(D3D11_TEXTURE2D_DESC); + tex2D.Get()->GetDesc(&texDesc); + + using var tex2DCopyTemp = default(ComPtr); + var tex2DCopyTempDesc = new D3D11_TEXTURE2D_DESC + { + 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); + } + + if (!enableCpuRead) + { + tex2DCopyTemp.Get()->AddRef(); + return tex2DCopyTemp.Get(); + } + + 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(); + + deviceContext.Get()->CopyResource((ID3D11Resource*)tex2DTarget.Get(), (ID3D11Resource*)tex2DCopyTemp.Get()); + + tex2DTarget.Get()->AddRef(); + return tex2DTarget.Get(); + } + + [SuppressMessage( + "StyleCop.CSharp.LayoutRules", + "SA1519:Braces should not be omitted from multi-line child statement", + Justification = "Multiple fixed blocks")] + private sealed unsafe class DrawsOneSquare : IDisposable + { + private ComPtr sampler; + private ComPtr vertexShader; + private ComPtr pixelShader; + private ComPtr inputLayout; + private ComPtr vertexConstantBuffer; + private ComPtr blendState; + private ComPtr rasterizerState; + private ComPtr vertexBufferFill; + private ComPtr vertexBufferMutable; + private ComPtr indexBuffer; + + ~DrawsOneSquare() => this.Dispose(); + + public void Dispose() + { + this.sampler.Reset(); + this.vertexShader.Reset(); + this.pixelShader.Reset(); + this.inputLayout.Reset(); + this.vertexConstantBuffer.Reset(); + this.blendState.Reset(); + this.rasterizerState.Reset(); + this.vertexBufferFill.Reset(); + this.vertexBufferMutable.Reset(); + this.indexBuffer.Reset(); + } + + public void Setup(T* device) where T : unmanaged, ID3D11Device.Interface + { + var assembly = typeof(ImGuiScene.ImGui_Impl_DX11).Assembly; + + // Create the vertex shader + if (this.vertexShader.IsEmpty() || this.inputLayout.IsEmpty()) + { + this.vertexShader.Reset(); + this.inputLayout.Reset(); + + using var stream = assembly.GetManifestResourceStream("imgui-vertex.hlsl.bytes")!; + var array = ArrayPool.Shared.Rent((int)stream.Length); + stream.ReadExactly(array, 0, (int)stream.Length); + fixed (byte* pArray = array) + fixed (ID3D11VertexShader** ppShader = &this.vertexShader.GetPinnableReference()) + fixed (ID3D11InputLayout** ppInputLayout = &this.inputLayout.GetPinnableReference()) + fixed (void* pszPosition = "POSITION"u8) + fixed (void* pszTexCoord = "TEXCOORD"u8) + fixed (void* pszColor = "COLOR"u8) + { + device->CreateVertexShader(pArray, (nuint)stream.Length, null, ppShader).ThrowOnError(); + + var ied = stackalloc D3D11_INPUT_ELEMENT_DESC[] + { + new() + { + SemanticName = (sbyte*)pszPosition, + Format = DXGI_FORMAT.DXGI_FORMAT_R32G32_FLOAT, + AlignedByteOffset = uint.MaxValue, + }, + new() + { + SemanticName = (sbyte*)pszTexCoord, + Format = DXGI_FORMAT.DXGI_FORMAT_R32G32_FLOAT, + AlignedByteOffset = uint.MaxValue, + }, + new() + { + SemanticName = (sbyte*)pszColor, + Format = DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM, + AlignedByteOffset = uint.MaxValue, + }, + }; + device->CreateInputLayout(ied, 3, pArray, (nuint)stream.Length, ppInputLayout).ThrowOnError(); + } + + ArrayPool.Shared.Return(array); + } + + // Create the constant buffer + if (this.vertexConstantBuffer.IsEmpty()) + { + var bufferDesc = new D3D11_BUFFER_DESC( + (uint)sizeof(Matrix4x4), + (uint)D3D11_BIND_FLAG.D3D11_BIND_CONSTANT_BUFFER, + D3D11_USAGE.D3D11_USAGE_IMMUTABLE); + var data = Matrix4x4.Identity; + var subr = new D3D11_SUBRESOURCE_DATA { pSysMem = &data }; + fixed (ID3D11Buffer** ppBuffer = &this.vertexConstantBuffer.GetPinnableReference()) + device->CreateBuffer(&bufferDesc, &subr, ppBuffer).ThrowOnError(); + } + + // Create the pixel shader + if (this.pixelShader.IsEmpty()) + { + using var stream = assembly.GetManifestResourceStream("imgui-frag.hlsl.bytes")!; + var array = ArrayPool.Shared.Rent((int)stream.Length); + stream.ReadExactly(array, 0, (int)stream.Length); + fixed (byte* pArray = array) + fixed (ID3D11PixelShader** ppShader = &this.pixelShader.GetPinnableReference()) + device->CreatePixelShader(pArray, (nuint)stream.Length, null, ppShader).ThrowOnError(); + + ArrayPool.Shared.Return(array); + } + + // Create the blending setup + if (this.blendState.IsEmpty()) + { + var blendStateDesc = new D3D11_BLEND_DESC + { + RenderTarget = + { + e0 = + { + BlendEnable = true, + SrcBlend = D3D11_BLEND.D3D11_BLEND_SRC_ALPHA, + DestBlend = D3D11_BLEND.D3D11_BLEND_INV_SRC_ALPHA, + BlendOp = D3D11_BLEND_OP.D3D11_BLEND_OP_ADD, + SrcBlendAlpha = D3D11_BLEND.D3D11_BLEND_INV_DEST_ALPHA, + DestBlendAlpha = D3D11_BLEND.D3D11_BLEND_ONE, + BlendOpAlpha = D3D11_BLEND_OP.D3D11_BLEND_OP_ADD, + RenderTargetWriteMask = (byte)D3D11_COLOR_WRITE_ENABLE.D3D11_COLOR_WRITE_ENABLE_ALL, + }, + }, + }; + fixed (ID3D11BlendState** ppBlendState = &this.blendState.GetPinnableReference()) + device->CreateBlendState(&blendStateDesc, ppBlendState).ThrowOnError(); + } + + // Create the rasterizer state + if (this.rasterizerState.IsEmpty()) + { + var rasterizerDesc = new D3D11_RASTERIZER_DESC + { + FillMode = D3D11_FILL_MODE.D3D11_FILL_SOLID, + CullMode = D3D11_CULL_MODE.D3D11_CULL_NONE, + }; + fixed (ID3D11RasterizerState** ppRasterizerState = &this.rasterizerState.GetPinnableReference()) + device->CreateRasterizerState(&rasterizerDesc, ppRasterizerState).ThrowOnError(); + } + + // Create the font sampler + if (this.sampler.IsEmpty()) + { + var samplerDesc = new D3D11_SAMPLER_DESC( + D3D11_FILTER.D3D11_FILTER_MIN_MAG_MIP_LINEAR, + D3D11_TEXTURE_ADDRESS_MODE.D3D11_TEXTURE_ADDRESS_WRAP, + D3D11_TEXTURE_ADDRESS_MODE.D3D11_TEXTURE_ADDRESS_WRAP, + D3D11_TEXTURE_ADDRESS_MODE.D3D11_TEXTURE_ADDRESS_WRAP, + 0f, + 0, + D3D11_COMPARISON_FUNC.D3D11_COMPARISON_ALWAYS, + null, + 0, + 0); + fixed (ID3D11SamplerState** ppSampler = &this.sampler.GetPinnableReference()) + device->CreateSamplerState(&samplerDesc, ppSampler).ThrowOnError(); + } + + if (this.vertexBufferFill.IsEmpty()) + { + var data = stackalloc ImDrawVert[] + { + new() { col = uint.MaxValue, pos = new(-1, 1), uv = new(0, 0) }, + new() { col = uint.MaxValue, pos = new(-1, -1), uv = new(0, 1) }, + new() { col = uint.MaxValue, pos = new(1, 1), uv = new(1, 0) }, + new() { col = uint.MaxValue, pos = new(1, -1), uv = new(1, 1) }, + }; + var desc = new D3D11_BUFFER_DESC( + (uint)(sizeof(ImDrawVert) * 4), + (uint)D3D11_BIND_FLAG.D3D11_BIND_VERTEX_BUFFER, + D3D11_USAGE.D3D11_USAGE_IMMUTABLE); + var subr = new D3D11_SUBRESOURCE_DATA { pSysMem = data }; + var buffer = default(ID3D11Buffer*); + device->CreateBuffer(&desc, &subr, &buffer).ThrowOnError(); + this.vertexBufferFill.Attach(buffer); + } + + if (this.vertexBufferMutable.IsEmpty()) + { + var desc = new D3D11_BUFFER_DESC( + (uint)(sizeof(ImDrawVert) * 4), + (uint)D3D11_BIND_FLAG.D3D11_BIND_VERTEX_BUFFER, + D3D11_USAGE.D3D11_USAGE_DYNAMIC, + (uint)D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_WRITE); + var buffer = default(ID3D11Buffer*); + device->CreateBuffer(&desc, null, &buffer).ThrowOnError(); + this.vertexBufferMutable.Attach(buffer); + } + + if (this.indexBuffer.IsEmpty()) + { + var data = stackalloc ushort[] { 0, 1, 2, 1, 2, 3 }; + var desc = new D3D11_BUFFER_DESC( + sizeof(ushort) * 6, + (uint)D3D11_BIND_FLAG.D3D11_BIND_INDEX_BUFFER, + D3D11_USAGE.D3D11_USAGE_IMMUTABLE); + var subr = new D3D11_SUBRESOURCE_DATA { pSysMem = data }; + var buffer = default(ID3D11Buffer*); + device->CreateBuffer(&desc, &subr, &buffer).ThrowOnError(); + this.indexBuffer.Attach(buffer); + } + } + + public void Draw( + ID3D11DeviceContext* ctx, + ID3D11ShaderResourceView* srv, + int width, + int height, + Vector2 uv0, + Vector2 uv1) + { + ID3D11Buffer* buffer; + if (uv0 == Vector2.Zero && uv1 == Vector2.One) + { + buffer = this.vertexBufferFill.Get(); + } + else + { + buffer = this.vertexBufferMutable.Get(); + var mapped = default(D3D11_MAPPED_SUBRESOURCE); + ctx->Map((ID3D11Resource*)buffer, 0, D3D11_MAP.D3D11_MAP_WRITE_DISCARD, 0u, &mapped).ThrowOnError(); + _ = new Span(mapped.pData, 4) + { + [0] = new() { col = uint.MaxValue, pos = new(-1, 1), uv = uv0 }, + [1] = new() { col = uint.MaxValue, pos = new(-1, -1), uv = new(uv0.X, uv1.Y) }, + [2] = new() { col = uint.MaxValue, pos = new(1, 1), uv = new(uv1.X, uv0.Y) }, + [3] = new() { col = uint.MaxValue, pos = new(1, -1), uv = uv1 }, + }; + ctx->Unmap((ID3D11Resource*)buffer, 0u); + } + + var stride = (uint)sizeof(ImDrawVert); + var offset = 0u; + + ctx->IASetInputLayout(this.inputLayout); + ctx->IASetVertexBuffers(0, 1, &buffer, &stride, &offset); + ctx->IASetIndexBuffer(this.indexBuffer, DXGI_FORMAT.DXGI_FORMAT_R16_UINT, 0); + ctx->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY.D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + + var viewport = new D3D11_VIEWPORT(0, 0, width, height); + ctx->RSSetState(this.rasterizerState); + ctx->RSSetViewports(1, &viewport); + + var blendColor = default(Vector4); + ctx->OMSetBlendState(this.blendState, (float*)&blendColor, 0xffffffff); + ctx->OMSetDepthStencilState(null, 0); + + ctx->VSSetShader(this.vertexShader.Get(), null, 0); + buffer = this.vertexConstantBuffer.Get(); + ctx->VSSetConstantBuffers(0, 1, &buffer); + + ctx->PSSetShader(this.pixelShader, null, 0); + var simp = this.sampler.Get(); + ctx->PSSetSamplers(0, 1, &simp); + ctx->PSSetShaderResources(0, 1, &srv); + + ctx->GSSetShader(null, null, 0); + ctx->HSSetShader(null, null, 0); + ctx->DSSetShader(null, null, 0); + ctx->CSSetShader(null, null, 0); + ctx->DrawIndexed(6, 0, 0); + } + } +} diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index 8d80a08d0..52253ff3c 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -26,6 +26,8 @@ using SharpDX.Direct3D; using SharpDX.Direct3D11; using SharpDX.DXGI; +using TerraFX.Interop.DirectX; + namespace Dalamud.Interface.Internal; /// Service responsible for loading and disposing ImGui texture wraps. @@ -36,7 +38,7 @@ namespace Dalamud.Interface.Internal; [ResolveVia] [ResolveVia] #pragma warning restore SA1015 -internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvider, ITextureSubstitutionProvider +internal sealed partial class TextureManager : IServiceType, IDisposable, ITextureProvider, ITextureSubstitutionProvider { private const int PathLookupLruCount = 8192; @@ -109,6 +111,9 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid ReleaseSelfReferences(this.manifestResourceTextures); this.lookupToPath.Clear(); + this.drawsOneSquare?.Dispose(); + this.drawsOneSquare = null; + return; static void ReleaseSelfReferences(ConcurrentDictionary dict) @@ -302,7 +307,11 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid cancellationToken); /// - public bool IsDxgiFormatSupported(int dxgiFormat) + bool ITextureProvider.IsDxgiFormatSupported(int dxgiFormat) => + this.IsDxgiFormatSupported((DXGI_FORMAT)dxgiFormat); + + /// + public bool IsDxgiFormatSupported(DXGI_FORMAT dxgiFormat) { if (this.interfaceManager.Scene is not { } scene) { @@ -310,7 +319,9 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid scene = this.interfaceManager.Scene ?? throw new InvalidOperationException(); } - return scene.Device.CheckFormatSupport((Format)dxgiFormat).HasFlag(FormatSupport.Texture2D); + var format = (Format)dxgiFormat; + var support = scene.Device.CheckFormatSupport(format); + return (support & FormatSupport.Texture2D) != 0; } /// @@ -522,7 +533,8 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid var buffer = file.TextureBuffer; var (dxgiFormat, conversion) = TexFile.GetDxgiFormatFromTextureFormat(file.Header.Format, false); - if (conversion != TexFile.DxgiFormatConversion.NoConversion || !this.IsDxgiFormatSupported(dxgiFormat)) + if (conversion != TexFile.DxgiFormatConversion.NoConversion || + !this.IsDxgiFormatSupported((DXGI_FORMAT)dxgiFormat)) { dxgiFormat = (int)Format.B8G8R8A8_UNorm; buffer = buffer.Filter(0, 0, TexFile.TextureFormat.B8G8R8A8); diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs index 4254b9082..9a63dbcb9 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -16,6 +16,8 @@ using Dalamud.Utility; using ImGuiNET; +using TerraFX.Interop.DirectX; + namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// @@ -41,6 +43,10 @@ internal class TexWidget : IDataWindowWidget private Vector2 inputTexScale = Vector2.Zero; private TextureManager textureManager = null!; + private string[]? supportedRenderTargetFormatNames; + private DXGI_FORMAT[]? supportedRenderTargetFormats; + private int renderTargetChoiceInt; + /// public string[]? CommandShortcuts { get; init; } = { "tex", "texture" }; @@ -66,6 +72,8 @@ internal class TexWidget : IDataWindowWidget this.inputManifestResourceAssemblyIndex = 0; this.inputManifestResourceNameCandidates = null; this.inputManifestResourceNameIndex = 0; + this.supportedRenderTargetFormats = null; + this.supportedRenderTargetFormatNames = null; this.Ready = true; } @@ -142,19 +150,63 @@ internal class TexWidget : IDataWindowWidget ImGui.PopID(); } - TextureEntry? toRemove = null; - TextureEntry? toCopy = null; + ImGui.Dummy(new(ImGui.GetTextLineHeightWithSpacing())); + + if (this.supportedRenderTargetFormats is null) + { + this.supportedRenderTargetFormatNames = null; + this.supportedRenderTargetFormats = + Enum.GetValues() + .Where(this.textureManager.IsDxgiFormatSupportedForCreateFromExistingTextureAsync) + .ToArray(); + this.renderTargetChoiceInt = 0; + } + + this.supportedRenderTargetFormatNames ??= this.supportedRenderTargetFormats.Select(Enum.GetName).ToArray(); + ImGui.Combo( + "CropCopy Format", + ref this.renderTargetChoiceInt, + this.supportedRenderTargetFormatNames, + this.supportedRenderTargetFormatNames.Length); + + Action? runLater = null; foreach (var t in this.addedTextures) { ImGui.PushID(t.Id); if (ImGui.CollapsingHeader($"Tex #{t.Id} {t}###header", ImGuiTreeNodeFlags.DefaultOpen)) { if (ImGui.Button("X")) - toRemove = t; + { + runLater = () => + { + t.Dispose(); + this.addedTextures.Remove(t); + }; + } ImGui.SameLine(); - if (ImGui.Button("Copy")) - toCopy = t; + if (ImGui.Button("Copy Reference")) + runLater = () => this.addedTextures.Add(t.CreateFromSharedLowLevelResource(this.textureManager)); + + ImGui.SameLine(); + if (ImGui.Button("CropCopy")) + { + runLater = () => + { + if (t.GetTexture(this.textureManager) is not { } source) + return; + if (this.supportedRenderTargetFormats is not { } supportedFormats) + return; + if (this.renderTargetChoiceInt < 0 || this.renderTargetChoiceInt >= supportedFormats.Length) + return; + var texTask = this.textureManager.CreateFromExistingTextureAsync( + source, + new(0.25f), + new(0.75f), + supportedFormats[this.renderTargetChoiceInt]); + this.addedTextures.Add(new() { Api10 = texTask }); + }; + } try { @@ -162,7 +214,7 @@ internal class TexWidget : IDataWindowWidget { var scale = new Vector2(tex.Width, tex.Height); if (this.inputTexScale != Vector2.Zero) - scale = this.inputTexScale; + scale *= this.inputTexScale; ImGui.Image(tex.ImGuiHandle, scale, this.inputTexUv0, this.inputTexUv1, this.inputTintCol); } @@ -180,16 +232,7 @@ internal class TexWidget : IDataWindowWidget ImGui.PopID(); } - if (toRemove != null) - { - toRemove.Dispose(); - this.addedTextures.Remove(toRemove); - } - - if (toCopy != null) - { - this.addedTextures.Add(toCopy.CreateFromSharedLowLevelResource(this.textureManager)); - } + runLater?.Invoke(); } private unsafe void DrawLoadedTextures(ICollection textures) diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs index 89a47c552..73530bf0e 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs @@ -18,6 +18,8 @@ using ImGuiNET; using SharpDX.DXGI; +using TerraFX.Interop.DirectX; + namespace Dalamud.Interface.ManagedFontAtlas.Internals; /// @@ -580,7 +582,7 @@ internal sealed partial class FontAtlasFactory var buf = Array.Empty(); try { - var use4 = this.factory.TextureManager.IsDxgiFormatSupported((int)Format.B4G4R4A4_UNorm); + var use4 = this.factory.TextureManager.IsDxgiFormatSupported(DXGI_FORMAT.DXGI_FORMAT_B4G4R4A4_UNORM); var bpp = use4 ? 2 : 4; var width = this.NewImAtlas.TexWidth; var height = this.NewImAtlas.TexHeight; diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs index 532e223fc..a632f14e4 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs @@ -24,6 +24,8 @@ using SharpDX; using SharpDX.Direct3D11; using SharpDX.DXGI; +using TerraFX.Interop.DirectX; + namespace Dalamud.Interface.ManagedFontAtlas.Internals; /// @@ -353,7 +355,7 @@ internal sealed partial class FontAtlasFactory var numPixels = texFile.Header.Width * texFile.Header.Height; _ = Service.Get(); - var targetIsB4G4R4A4 = this.TextureManager.IsDxgiFormatSupported((int)Format.B4G4R4A4_UNorm); + var targetIsB4G4R4A4 = this.TextureManager.IsDxgiFormatSupported(DXGI_FORMAT.DXGI_FORMAT_B4G4R4A4_UNORM); var bpp = targetIsB4G4R4A4 ? 2 : 4; var buffer = ArrayPool.Shared.Rent(numPixels * bpp); try diff --git a/Dalamud/Plugin/Services/ITextureProvider.cs b/Dalamud/Plugin/Services/ITextureProvider.cs index 46ad0fb46..a18ac5b2a 100644 --- a/Dalamud/Plugin/Services/ITextureProvider.cs +++ b/Dalamud/Plugin/Services/ITextureProvider.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Numerics; using System.Reflection; using System.Threading; using System.Threading.Tasks; @@ -27,6 +28,28 @@ namespace Dalamud.Plugin.Services; /// public partial interface ITextureProvider { + /// Creates a texture from the given existing texture, cropping and converting pixel format as needed. + /// + /// 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. + /// The cancellation token. + /// A containing the copied texture on success. Dispose after use. + /// + /// Coordinates in and should be in range between 0 and 1. + /// + /// Supported values for may not necessarily match + /// . + /// + Task CreateFromExistingTextureAsync( + IDalamudTextureWrap wrap, + Vector2 uv0, + Vector2 uv1, + int dxgiFormat, + CancellationToken cancellationToken = default); + /// Gets a texture from the given bytes, trying to interpret it as a .tex file or other well-known image /// files, such as .png. /// The bytes to load. @@ -143,4 +166,10 @@ public partial interface ITextureProvider /// The DXGI format. /// true if supported. bool IsDxgiFormatSupported(int dxgiFormat); + + /// Determines whether the system supports the given DXGI format for use with + /// . + /// The DXGI format. + /// true if supported. + bool IsDxgiFormatSupportedForCreateFromExistingTextureAsync(int dxgiFormat); } diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index 65196b3ee..f0d58dd3c 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -692,12 +692,21 @@ public static class Util /// Throws a corresponding exception if is true. /// /// The result value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static void ThrowOnError(this HRESULT hr) { if (hr.FAILED) Marshal.ThrowExceptionForHR(hr.Value); } + /// Determines if the specified instance of points to null. + /// The pointer. + /// The COM interface type from TerraFX. + /// true if not empty. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static unsafe bool IsEmpty(in this ComPtr f) where T : unmanaged, IUnknown.Interface => + f.Get() is null; + /// /// Calls if the task is incomplete. ///