diff --git a/Dalamud/DalamudAsset.cs b/Dalamud/DalamudAsset.cs index a7b35b196..15d342962 100644 --- a/Dalamud/DalamudAsset.cs +++ b/Dalamud/DalamudAsset.cs @@ -1,5 +1,7 @@ using Dalamud.Storage.Assets; +using TerraFX.Interop.DirectX; + namespace Dalamud; /// @@ -19,9 +21,16 @@ public enum DalamudAsset /// : The fallback empty texture. /// [DalamudAsset(DalamudAssetPurpose.TextureFromRaw, data: new byte[] { 0, 0, 0, 0, 0, 0, 0, 0 })] - [DalamudAssetRawTexture(4, 8, 4, SharpDX.DXGI.Format.BC1_UNorm)] + [DalamudAssetRawTexture(4, 4, DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM, 8)] Empty4X4 = 1000, + /// + /// : The fallback empty texture. + /// + [DalamudAsset(DalamudAssetPurpose.TextureFromRaw, data: new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, 0, 0, 0, 0 })] + [DalamudAssetRawTexture(4, 4, DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM, 8)] + White4X4 = 1014, + /// /// : The Dalamud logo. /// diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 019b462cd..65c10d3a0 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -70,7 +70,8 @@ internal class InterfaceManager : IDisposable, IServiceType [ServiceManager.ServiceDependency] private readonly DalamudIme dalamudIme = Service.Get(); - private readonly ConcurrentQueue runBeforePresent = new(); + private readonly ConcurrentQueue runBeforeImGuiRender = new(); + private readonly ConcurrentQueue runAfterImGuiRender = new(); private readonly SwapChainVtableResolver address = new(); private readonly Hook setCursorHook; @@ -285,13 +286,13 @@ internal class InterfaceManager : IDisposable, IServiceType this.deferredDisposeDisposables.Add(locked); } - /// Queues an action to be run before Present call. + /// Queues an action to be run before call. /// The action. /// A that resolves once is run. - public Task RunBeforePresent(Action action) + public Task RunBeforeImGuiRender(Action action) { var tcs = new TaskCompletionSource(); - this.runBeforePresent.Enqueue( + this.runBeforeImGuiRender.Enqueue( () => { try @@ -307,14 +308,58 @@ internal class InterfaceManager : IDisposable, IServiceType return tcs.Task; } - /// Queues a function to be run before Present call. + /// Queues a function to be run before call. /// The type of the return value. /// The function. /// A that resolves once is run. - public Task RunBeforePresent(Func func) + public Task RunBeforeImGuiRender(Func func) { var tcs = new TaskCompletionSource(); - this.runBeforePresent.Enqueue( + this.runBeforeImGuiRender.Enqueue( + () => + { + try + { + tcs.SetResult(func()); + } + catch (Exception e) + { + tcs.SetException(e); + } + }); + return tcs.Task; + } + + /// Queues an action to be run after call. + /// The action. + /// A that resolves once is run. + public Task RunAfterImGuiRender(Action action) + { + var tcs = new TaskCompletionSource(); + this.runAfterImGuiRender.Enqueue( + () => + { + try + { + action(); + tcs.SetResult(); + } + catch (Exception e) + { + tcs.SetException(e); + } + }); + return tcs.Task; + } + + /// Queues a function to be run after call. + /// The type of the return value. + /// The function. + /// A that resolves once is run. + public Task RunAfterImGuiRender(Func func) + { + var tcs = new TaskCompletionSource(); + this.runAfterImGuiRender.Enqueue( () => { try @@ -566,7 +611,7 @@ internal class InterfaceManager : IDisposable, IServiceType if (!this.dalamudAtlas!.HasBuiltAtlas) return this.presentHook!.Original(swapChain, syncInterval, presentFlags); - while (this.runBeforePresent.TryDequeue(out var action)) + while (this.runBeforeImGuiRender.TryDequeue(out var action)) action.InvokeSafely(); if (this.address.IsReshade) @@ -574,19 +619,22 @@ internal class InterfaceManager : IDisposable, IServiceType var pRes = this.presentHook!.Original(swapChain, syncInterval, presentFlags); RenderImGui(this.scene!); - this.CleanupPostImGuiRender(); + this.PostImGuiRender(); return pRes; } RenderImGui(this.scene!); - this.CleanupPostImGuiRender(); + this.PostImGuiRender(); return this.presentHook!.Original(swapChain, syncInterval, presentFlags); } - private void CleanupPostImGuiRender() + private void PostImGuiRender() { + while (this.runAfterImGuiRender.TryDequeue(out var action)) + action.InvokeSafely(); + if (!this.deferredDisposeTextures.IsEmpty) { var count = 0; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs index fb4330bea..769df073b 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Dalamud.Interface.Components; using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.Textures; using Dalamud.Interface.Textures.Internal; using Dalamud.Interface.Textures.Internal.SharedImmediateTextures; using Dalamud.Interface.Utility; @@ -21,6 +22,7 @@ using ImGuiNET; using Serilog; using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; using TextureManager = Dalamud.Interface.Textures.Internal.TextureManager; @@ -49,6 +51,7 @@ internal class TexWidget : IDataWindowWidget private Vector2 inputTexScale = Vector2.Zero; private TextureManager textureManager = null!; private FileDialogManager fileDialogManager = null!; + private ExistingTextureModificationArgs existingTextureModificationArgs; private string[]? supportedRenderTargetFormatNames; private DXGI_FORMAT[]? supportedRenderTargetFormats; @@ -82,6 +85,13 @@ internal class TexWidget : IDataWindowWidget this.supportedRenderTargetFormats = null; this.supportedRenderTargetFormatNames = null; this.fileDialogManager = new(); + this.existingTextureModificationArgs = new() + { + Uv0 = new(0.25f), + Uv1 = new(0.75f), + NewWidth = 320, + NewHeight = 240, + }; this.Ready = true; } @@ -123,6 +133,31 @@ internal class TexWidget : IDataWindowWidget ImGui.PopID(); } + ImGui.Dummy(new(ImGui.GetTextLineHeightWithSpacing())); + + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Capture: "); + if (ImGui.Button("Game")) + this.addedTextures.Add(new() { Api10 = this.textureManager.CreateFromGameScreen() }); + + ImGui.SameLine(); + if (ImGui.Button("Game (Auto)")) + this.addedTextures.Add(new() { Api10 = this.textureManager.CreateFromGameScreen(true) }); + + ImGui.SameLine(); + if (ImGui.Button("Main Viewport")) + { + this.addedTextures.Add( + new() { Api10 = this.textureManager.CreateFromImGuiViewport(ImGui.GetMainViewport().ID) }); + } + + ImGui.SameLine(); + if (ImGui.Button("Main Viewport (Auto)")) + { + this.addedTextures.Add( + new() { Api10 = this.textureManager.CreateFromImGuiViewport(ImGui.GetMainViewport().ID, true) }); + } + if (ImGui.CollapsingHeader(nameof(ITextureProvider.GetFromGameIcon), ImGuiTreeNodeFlags.DefaultOpen)) { ImGui.PushID(nameof(this.DrawGetFromGameIcon)); @@ -158,24 +193,14 @@ internal class TexWidget : IDataWindowWidget ImGui.PopID(); } - ImGui.Dummy(new(ImGui.GetTextLineHeightWithSpacing())); - - if (this.supportedRenderTargetFormats is null) + if (ImGui.CollapsingHeader($"CropCopy##{this.DrawExistingTextureModificationArgs}")) { - this.supportedRenderTargetFormatNames = null; - this.supportedRenderTargetFormats = - Enum.GetValues() - .Where(this.textureManager.IsDxgiFormatSupportedForCreateFromExistingTextureAsync) - .ToArray(); - this.renderTargetChoiceInt = 0; + ImGui.PushID(nameof(this.DrawExistingTextureModificationArgs)); + this.DrawExistingTextureModificationArgs(); + ImGui.PopID(); } - this.supportedRenderTargetFormatNames ??= this.supportedRenderTargetFormats.Select(Enum.GetName).ToArray(); - ImGui.Combo( - "CropCopy Format", - ref this.renderTargetChoiceInt, - this.supportedRenderTargetFormatNames, - this.supportedRenderTargetFormatNames.Length); + ImGui.Dummy(new(ImGui.GetTextLineHeightWithSpacing())); Action? runLater = null; foreach (var t in this.addedTextures) @@ -217,13 +242,14 @@ internal class TexWidget : IDataWindowWidget return; var texTask = this.textureManager.CreateFromExistingTextureAsync( source.CreateWrapSharingLowLevelResource(), - new(0.25f), - new(0.75f), - supportedFormats[this.renderTargetChoiceInt]); + this.existingTextureModificationArgs with + { + Format = supportedFormats[this.renderTargetChoiceInt], + }); this.addedTextures.Add(new() { Api10 = texTask }); }; } - + ImGui.SameLine(); ImGui.AlignTextToFramePadding(); unsafe @@ -233,7 +259,7 @@ internal class TexWidget : IDataWindowWidget var psrv = (ID3D11ShaderResourceView*)source.ImGuiHandle; var rcsrv = psrv->AddRef() - 1; psrv->Release(); - + var pres = default(ID3D11Resource*); psrv->GetResource(&pres); var rcres = pres->AddRef() - 1; @@ -558,6 +584,49 @@ internal class TexWidget : IDataWindowWidget ImGuiHelpers.ScaledDummy(10); } + private void DrawExistingTextureModificationArgs() + { + var vec2 = this.existingTextureModificationArgs.Uv0; + if (ImGui.InputFloat2("UV0", ref vec2)) + this.existingTextureModificationArgs.Uv0 = vec2; + + vec2 = this.existingTextureModificationArgs.Uv1; + if (ImGui.InputFloat2("UV1", ref vec2)) + this.existingTextureModificationArgs.Uv1 = vec2; + + Span wh = stackalloc int[2]; + wh[0] = this.existingTextureModificationArgs.NewWidth; + wh[1] = this.existingTextureModificationArgs.NewHeight; + if (ImGui.InputInt2("New Size", ref wh[0])) + { + this.existingTextureModificationArgs.NewWidth = wh[0]; + this.existingTextureModificationArgs.NewHeight = wh[1]; + } + + var b = this.existingTextureModificationArgs.MakeOpaque; + if (ImGui.Checkbox("Make Opaque", ref b)) + this.existingTextureModificationArgs.MakeOpaque = b; + + 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( + "Format", + ref this.renderTargetChoiceInt, + this.supportedRenderTargetFormatNames, + this.supportedRenderTargetFormatNames.Length); + + ImGuiHelpers.ScaledDummy(10); + } + private async void SaveTextureAsync(string name, Func> textureGetter) { try @@ -627,15 +696,18 @@ internal class TexWidget : IDataWindowWidget } using var textureWrap = await textureGetter.Invoke(); + var props = new Dictionary(); + if (encoder.ContainerGuid == GUID.GUID_ContainerFormatTiff) + props["CompressionQuality"] = 1.0f; + else if (encoder.ContainerGuid == GUID.GUID_ContainerFormatJpeg || + encoder.ContainerGuid == GUID.GUID_ContainerFormatHeif || + encoder.ContainerGuid == GUID.GUID_ContainerFormatWmp) + props["ImageQuality"] = 1.0f; await this.textureManager.SaveToFileAsync( textureWrap, encoder.ContainerGuid, path, - props: new Dictionary - { - ["CompressionQuality"] = 1.0f, - ["ImageQuality"] = 1.0f, - }); + props: props); Service.Get().AddNotification( $"File saved to: {path}", diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs index 73530bf0e..316174c4e 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs @@ -638,8 +638,8 @@ internal sealed partial class FontAtlasFactory new( width, height, - width * bpp, - (int)(use4 ? Format.B4G4R4A4_UNorm : Format.B8G8R8A8_UNorm)), + (int)(use4 ? Format.B4G4R4A4_UNorm : Format.B8G8R8A8_UNorm), + width * bpp), buf); this.data.AddExistingTexture(wrap); texture.TexID = wrap.ImGuiHandle; diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs index 27888bb0b..39e969fb8 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs @@ -360,10 +360,10 @@ internal sealed partial class FontAtlasFactory new( texFile.Header.Width, texFile.Header.Height, - texFile.Header.Width * bpp, (int)(targetIsB4G4R4A4 ? DXGI_FORMAT.DXGI_FORMAT_B4G4R4A4_UNORM - : DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM)), + : DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM), + texFile.Header.Width * bpp), buffer)); } finally diff --git a/Dalamud/Interface/Textures/ExistingTextureModificationArgs.cs b/Dalamud/Interface/Textures/ExistingTextureModificationArgs.cs new file mode 100644 index 000000000..9bcb9f34d --- /dev/null +++ b/Dalamud/Interface/Textures/ExistingTextureModificationArgs.cs @@ -0,0 +1,95 @@ +using System.Numerics; + +using Dalamud.Plugin.Services; + +using TerraFX.Interop.DirectX; + +namespace Dalamud.Interface.Textures; + +/// Describes how to modify an existing texture. +public record struct ExistingTextureModificationArgs() +{ + /// Gets or sets the left top coordinates relative to the size of the source texture. + /// Coordinates should be in range between 0 and 1. + public Vector2 Uv0 { get; set; } = Vector2.Zero; + + /// Gets or sets the right bottom coordinates relative to the size of the source texture. + /// Coordinates should be in range between 0 and 1. + /// If set to , then it will be interpreted as , + /// to accommodate the use of default value of this record struct. + public Vector2 Uv1 { get; set; } = Vector2.One; + + /// Gets or sets the new width. + /// Set to 0 to automatically calculate according to the original texture size, , and + /// . + public int NewWidth { get; set; } + + /// Gets or sets the new height. + /// Set to 0 to automatically calculate according to the original texture size, , and + /// . + public int NewHeight { get; set; } + + /// Gets or sets a value indicating whether to make the texture opaque. + /// Alpha channel values will be filled with 1.0. + public bool MakeOpaque { get; set; } = false; + + /// Gets or sets the new DXGI format. + /// + /// Set to 0 () to use the source pixel format. + /// Supported values can be queried with + /// . This may not necessarily + /// match . + /// + public int DxgiFormat { get; set; } = (int)DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM; + + /// Gets or sets the format (typed). + internal DXGI_FORMAT Format + { + get => (DXGI_FORMAT)this.DxgiFormat; + set => this.DxgiFormat = (int)value; + } + + /// Gets the effective value of . + internal Vector2 Uv1Effective => this.Uv1 == Vector2.Zero ? Vector2.One : this.Uv1; + + /// Test if this instance of does not instruct to change the + /// underlying data of a texture. + /// The texture description to test against. + /// true if this instance of does not instruct to + /// change the underlying data of a texture. + internal bool IsCompleteSourceCopy(in D3D11_TEXTURE2D_DESC desc) => + this.Uv0 == Vector2.Zero + && this.Uv1 == Vector2.One + && (this.NewWidth == 0 || this.NewWidth == desc.Width) + && (this.NewHeight == 0 || this.NewHeight == desc.Height) + && !this.MakeOpaque + && (this.Format == DXGI_FORMAT.DXGI_FORMAT_UNKNOWN || this.Format == desc.Format); + + /// Checks the properties and throws an exception if values are invalid. + internal void ThrowOnInvalidValues() + { + if (this.Uv0.X is < 0 or > 1 or float.NaN) + throw new ArgumentException($"{nameof(this.Uv0)}.X is out of range."); + + if (this.Uv0.Y is < 0 or > 1 or float.NaN) + throw new ArgumentException($"{nameof(this.Uv0)}.Y is out of range."); + + if (this.Uv1Effective.X is < 0 or > 1 or float.NaN) + throw new ArgumentException($"{nameof(this.Uv1)}.X is out of range."); + + if (this.Uv1Effective.Y is < 0 or > 1 or float.NaN) + throw new ArgumentException($"{nameof(this.Uv1)}.Y is out of range."); + + if (this.Uv0.X >= this.Uv1Effective.X || this.Uv0.Y >= this.Uv1Effective.Y) + { + throw new ArgumentException( + $"{nameof(this.Uv0)} must be strictly less than {nameof(this.Uv1)} in a componentwise way."); + } + + if (this.NewWidth < 0) + throw new ArgumentException($"{nameof(this.NewWidth)} cannot be a negative number."); + + if (this.NewHeight < 0) + throw new ArgumentException($"{nameof(this.NewHeight)} cannot be a negative number."); + } +} diff --git a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs index a0562f1ef..1dfe96fe2 100644 --- a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs +++ b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs @@ -28,7 +28,8 @@ internal sealed class GamePathSharedImmediateTexture : SharedImmediateTexture public static SharedImmediateTexture CreatePlaceholder(string path) => new GamePathSharedImmediateTexture(path); /// - public override string ToString() => $"{nameof(GamePathSharedImmediateTexture)}#{this.InstanceIdForDebug}({this.path})"; + public override string ToString() => + $"{nameof(GamePathSharedImmediateTexture)}#{this.InstanceIdForDebug}({this.path})"; /// protected override void ReleaseResources() diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.Drawer.cs b/Dalamud/Interface/Textures/Internal/TextureManager.Drawer.cs new file mode 100644 index 000000000..c1043e914 --- /dev/null +++ b/Dalamud/Interface/Textures/Internal/TextureManager.Drawer.cs @@ -0,0 +1,406 @@ +using System.Buffers; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; + +using Dalamud.Interface.Internal; +using Dalamud.Storage.Assets; +using Dalamud.Utility; + +using ImGuiNET; + +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +namespace Dalamud.Interface.Textures.Internal; + +/// Service responsible for loading and disposing ImGui texture wraps. +internal sealed partial class TextureManager +{ + private SimpleDrawerImpl? simpleDrawer; + + [ServiceManager.CallWhenServicesReady("Need device")] + private unsafe void ContinueConstructionFromExistingTextures(InterfaceManager.InterfaceManagerWithScene withScene) + { + this.simpleDrawer = new(); + this.simpleDrawer.Setup(this.Device.Get()); + } + + /// A class for drawing simple stuff. + [SuppressMessage( + "StyleCop.CSharp.LayoutRules", + "SA1519:Braces should not be omitted from multi-line child statement", + Justification = "Multiple fixed blocks")] + internal sealed unsafe class SimpleDrawerImpl : IDisposable + { + private ComPtr sampler; + private ComPtr vertexShader; + private ComPtr pixelShader; + private ComPtr inputLayout; + private ComPtr vertexConstantBuffer; + private ComPtr blendState; + private ComPtr blendStateForStrippingAlpha; + private ComPtr rasterizerState; + private ComPtr vertexBufferFill; + private ComPtr vertexBufferMutable; + private ComPtr indexBuffer; + + /// Finalizes an instance of the class. + ~SimpleDrawerImpl() => this.Dispose(); + + /// + public void Dispose() + { + this.sampler.Reset(); + this.vertexShader.Reset(); + this.pixelShader.Reset(); + this.inputLayout.Reset(); + this.vertexConstantBuffer.Reset(); + this.blendState.Reset(); + this.blendStateForStrippingAlpha.Reset(); + this.rasterizerState.Reset(); + this.vertexBufferFill.Reset(); + this.vertexBufferMutable.Reset(); + this.indexBuffer.Reset(); + GC.SuppressFinalize(this); + } + + /// Sets up this instance of . + /// The device. + public void Setup(ID3D11Device* device) + { + 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(); + } + + if (this.blendStateForStrippingAlpha.IsEmpty()) + { + var blendStateDesc = new D3D11_BLEND_DESC + { + RenderTarget = + { + e0 = + { + BlendEnable = true, + SrcBlend = D3D11_BLEND.D3D11_BLEND_ZERO, + DestBlend = D3D11_BLEND.D3D11_BLEND_ONE, + BlendOp = D3D11_BLEND_OP.D3D11_BLEND_OP_ADD, + SrcBlendAlpha = D3D11_BLEND.D3D11_BLEND_ONE, + DestBlendAlpha = D3D11_BLEND.D3D11_BLEND_ZERO, + BlendOpAlpha = D3D11_BLEND_OP.D3D11_BLEND_OP_ADD, + RenderTargetWriteMask = (byte)D3D11_COLOR_WRITE_ENABLE.D3D11_COLOR_WRITE_ENABLE_ALPHA, + }, + }, + }; + fixed (ID3D11BlendState** ppBlendState = &this.blendStateForStrippingAlpha.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); + } + } + + /// Draws the given shader resource view to the current render target. + /// An instance of . + /// The shader resource view. + /// The left top coordinates relative to the size of the source texture. + /// The right bottom coordinates relative to the size of the source texture. + /// This function does not throw. + public void Draw( + ID3D11DeviceContext* ctx, + ID3D11ShaderResourceView* srv, + Vector2 uv0, + Vector2 uv1) + { + using var rtv = default(ComPtr); + ctx->OMGetRenderTargets(1, rtv.GetAddressOf(), null); + if (rtv.IsEmpty()) + return; + + using var rtvRes = default(ComPtr); + rtv.Get()->GetResource(rtvRes.GetAddressOf()); + + using var rtvTex = default(ComPtr); + if (rtvRes.As(&rtvTex).FAILED) + return; + + D3D11_TEXTURE2D_DESC texDesc; + rtvTex.Get()->GetDesc(&texDesc); + + ID3D11Buffer* buffer; + if (uv0 == Vector2.Zero && uv1 == Vector2.One) + { + buffer = this.vertexBufferFill.Get(); + } + else + { + buffer = this.vertexBufferMutable.Get(); + var mapped = default(D3D11_MAPPED_SUBRESOURCE); + if (ctx->Map((ID3D11Resource*)buffer, 0, D3D11_MAP.D3D11_MAP_WRITE_DISCARD, 0u, &mapped).FAILED) + return; + _ = 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, texDesc.Width, texDesc.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); + + var ppn = default(ID3D11ShaderResourceView*); + ctx->PSSetShaderResources(0, 1, &ppn); + } + + /// Fills alpha channel to 1.0 from the current render target. + /// An instance of . + /// This function does not throw. + public void StripAlpha(ID3D11DeviceContext* ctx) + { + using var rtv = default(ComPtr); + ctx->OMGetRenderTargets(1, rtv.GetAddressOf(), null); + if (rtv.IsEmpty()) + return; + + using var rtvRes = default(ComPtr); + rtv.Get()->GetResource(rtvRes.GetAddressOf()); + + using var rtvTex = default(ComPtr); + if (rtvRes.As(&rtvTex).FAILED) + return; + + D3D11_TEXTURE2D_DESC texDesc; + rtvTex.Get()->GetDesc(&texDesc); + + var buffer = this.vertexBufferFill.Get(); + 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, texDesc.Width, texDesc.Height); + ctx->RSSetState(this.rasterizerState); + ctx->RSSetViewports(1, &viewport); + + var blendColor = default(Vector4); + ctx->OMSetBlendState(this.blendStateForStrippingAlpha, (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); + var ppn = (ID3D11ShaderResourceView*)Service.Get().White4X4.ImGuiHandle; + ctx->PSSetShaderResources(0, 1, &ppn); + + 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); + + ppn = default; + ctx->PSSetShaderResources(0, 1, &ppn); + } + } +} diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs b/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs index fc2a9e70f..56b0356e5 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs @@ -1,6 +1,3 @@ -using System.Buffers; -using System.Diagnostics.CodeAnalysis; -using System.Numerics; using System.Threading; using System.Threading.Tasks; @@ -18,8 +15,6 @@ namespace Dalamud.Interface.Textures.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); @@ -50,27 +45,9 @@ internal sealed partial class TextureManager } /// - Task ITextureProvider.CreateFromExistingTextureAsync( - IDalamudTextureWrap wrap, - Vector2 uv0, - Vector2 uv1, - int dxgiFormat, - bool leaveWrapOpen, - CancellationToken cancellationToken) => - this.CreateFromExistingTextureAsync( - wrap, - uv0, - uv1, - (DXGI_FORMAT)dxgiFormat, - leaveWrapOpen, - cancellationToken); - - /// public Task CreateFromExistingTextureAsync( IDalamudTextureWrap wrap, - Vector2 uv0, - Vector2 uv1, - DXGI_FORMAT format = DXGI_FORMAT.DXGI_FORMAT_UNKNOWN, + ExistingTextureModificationArgs args = default, bool leaveWrapOpen = false, CancellationToken cancellationToken = default) { @@ -82,7 +59,7 @@ internal sealed partial class TextureManager async Task ImmediateLoadFunction(CancellationToken ct) { - using var tex = await this.NoThrottleCreateFromExistingTextureAsync(wrap, uv0, uv1, format); + using var tex = await this.NoThrottleCreateFromExistingTextureAsync(wrap, args); unsafe { @@ -108,27 +85,52 @@ internal sealed partial class TextureManager } /// - Task<(RawImageSpecification Specification, byte[] RawData)> ITextureProvider.GetRawDataFromExistingTextureAsync( - IDalamudTextureWrap wrap, - Vector2 uv0, - Vector2 uv1, - int dxgiFormat, - bool leaveWrapOpen, - CancellationToken cancellationToken) => - this.GetRawDataFromExistingTextureAsync( - wrap, - uv0, - uv1, - (DXGI_FORMAT)dxgiFormat, - leaveWrapOpen, - cancellationToken); + public Task CreateFromGameScreen( + bool autoUpdate = false, + CancellationToken cancellationToken = default) => + this.interfaceManager.RunBeforeImGuiRender( + () => + { + cancellationToken.ThrowIfCancellationRequested(); + var t = new ViewportTextureWrap(ImGui.GetMainViewport().ID, true, autoUpdate, cancellationToken); + t.Update(); + try + { + return t.FirstUpdateTask.Result; + } + catch + { + t.Dispose(); + throw; + } + }); - /// + /// + public Task CreateFromImGuiViewport( + uint viewportId, + bool autoUpdate = false, + CancellationToken cancellationToken = default) => + this.interfaceManager.RunBeforeImGuiRender( + () => + { + cancellationToken.ThrowIfCancellationRequested(); + var t = new ViewportTextureWrap(viewportId, false, autoUpdate, cancellationToken); + t.Update(); + try + { + return t.FirstUpdateTask.Result; + } + catch + { + t.Dispose(); + throw; + } + }); + + /// public async Task<(RawImageSpecification Specification, byte[] RawData)> GetRawDataFromExistingTextureAsync( IDalamudTextureWrap wrap, - Vector2 uv0, - Vector2 uv1, - DXGI_FORMAT dxgiFormat = DXGI_FORMAT.DXGI_FORMAT_UNKNOWN, + ExistingTextureModificationArgs args = default, bool leaveWrapOpen = false, CancellationToken cancellationToken = default) { @@ -157,9 +159,9 @@ internal sealed partial class TextureManager tex2D.Get()->GetDesc(&texDesc); } - if (texDesc.Format != dxgiFormat && dxgiFormat != DXGI_FORMAT.DXGI_FORMAT_UNKNOWN) + if (!args.IsCompleteSourceCopy(texDesc)) { - using var tmp = await this.NoThrottleCreateFromExistingTextureAsync(wrap, uv0, uv1, dxgiFormat); + using var tmp = await this.NoThrottleCreateFromExistingTextureAsync(wrap, args); unsafe { tex2D.Swap(&tmp); @@ -167,7 +169,7 @@ internal sealed partial class TextureManager } cancellationToken.ThrowIfCancellationRequested(); - return await this.interfaceManager.RunBeforePresent( + return await this.interfaceManager.RunBeforeImGuiRender( () => ExtractMappedResource(this.Device, context, tex2D, cancellationToken)); static unsafe (RawImageSpecification Specification, byte[] RawData) ExtractMappedResource( @@ -214,8 +216,8 @@ internal sealed partial class TextureManager var specs = new RawImageSpecification( (int)desc.Width, (int)desc.Height, - (int)mapped.RowPitch, - (int)desc.Format); + (int)desc.Format, + (int)mapped.RowPitch); var bytes = new Span(mapped.pData, checked((int)mapped.DepthPitch)).ToArray(); return (specs, bytes); } @@ -229,10 +231,10 @@ internal sealed partial class TextureManager private async Task> NoThrottleCreateFromExistingTextureAsync( IDalamudTextureWrap wrap, - Vector2 uv0, - Vector2 uv1, - DXGI_FORMAT format) + ExistingTextureModificationArgs args) { + args.ThrowOnInvalidValues(); + using var texSrv = default(ComPtr); using var context = default(ComPtr); using var tex2D = default(ComPtr); @@ -254,33 +256,34 @@ internal sealed partial class TextureManager tex2D.Get()->GetDesc(&texDesc); } - if (format == DXGI_FORMAT.DXGI_FORMAT_UNKNOWN) - format = texDesc.Format; - - var newWidth = checked((uint)MathF.Round((uv1.X - uv0.X) * texDesc.Width)); - var newHeight = checked((uint)MathF.Round((uv1.Y - uv0.Y) * texDesc.Height)); + if (args.Format == DXGI_FORMAT.DXGI_FORMAT_UNKNOWN) + args = args with { Format = texDesc.Format }; + if (args.NewWidth == 0) + args = args with { NewWidth = (int)MathF.Round((args.Uv1Effective.X - args.Uv0.X) * texDesc.Width) }; + if (args.NewHeight == 0) + args = args with { NewHeight = (int)MathF.Round((args.Uv1Effective.Y - args.Uv0.Y) * texDesc.Height) }; using var tex2DCopyTemp = default(ComPtr); unsafe { var tex2DCopyTempDesc = new D3D11_TEXTURE2D_DESC { - Width = newWidth, - Height = newHeight, + Width = (uint)args.NewWidth, + Height = (uint)args.NewHeight, MipLevels = 1, ArraySize = 1, - Format = format, + Format = args.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), + BindFlags = (uint)(D3D11_BIND_FLAG.D3D11_BIND_SHADER_RESOURCE | + D3D11_BIND_FLAG.D3D11_BIND_RENDER_TARGET), CPUAccessFlags = 0u, MiscFlags = 0u, }; this.Device.Get()->CreateTexture2D(&tex2DCopyTempDesc, null, tex2DCopyTemp.GetAddressOf()).ThrowOnError(); } - await this.interfaceManager.RunBeforePresent( + await this.interfaceManager.RunBeforeImGuiRender( () => { unsafe @@ -294,292 +297,20 @@ internal sealed partial class TextureManager &rtvCopyTempDesc, rtvCopyTemp.GetAddressOf()).ThrowOnError(); - this.drawsOneSquare ??= new(); - this.drawsOneSquare.Setup(this.Device.Get()); - context.Get()->OMSetRenderTargets(1u, rtvCopyTemp.GetAddressOf(), null); - this.drawsOneSquare.Draw( + this.SimpleDrawer.Draw( context.Get(), texSrv.Get(), - (int)newWidth, - (int)newHeight, - uv0, - uv1); - context.Get()->OMSetRenderTargets(0, null, null); + args.Uv0, + args.Uv1Effective); + if (args.MakeOpaque) + this.SimpleDrawer.StripAlpha(context.Get()); + + var dummy = default(ID3D11RenderTargetView*); + context.Get()->OMSetRenderTargets(1u, &dummy, null); } }); return new(tex2DCopyTemp); } - - [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); - - var ppn = default(ID3D11ShaderResourceView*); - ctx->PSSetShaderResources(0, 1, &ppn); - } - } } diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.SharedTextures.cs b/Dalamud/Interface/Textures/Internal/TextureManager.SharedTextures.cs index 3a121c4c5..71f300479 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.SharedTextures.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.SharedTextures.cs @@ -50,7 +50,7 @@ internal sealed partial class TextureManager this.textureManager = textureManager; this.textureManager.framework.Update += this.FrameworkOnUpdate; } - + /// Gets all the loaded textures from game resources. public ICollection ForDebugGamePathTextures => this.gameDict.Values; diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs b/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs index 54e30dab0..b35864a17 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs @@ -3,8 +3,6 @@ using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; -using System.Numerics; -using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; @@ -43,13 +41,42 @@ internal sealed partial class TextureManager CancellationToken cancellationToken = default) { using var wrapDispose = leaveWrapOpen ? null : wrap; + var texDesc = default(D3D11_TEXTURE2D_DESC); + + unsafe + { + using var texSrv = default(ComPtr); + using var context = default(ComPtr); + using var tex2D = default(ComPtr); + fixed (Guid* piid = &IID.IID_ID3D11ShaderResourceView) + ((IUnknown*)wrap.ImGuiHandle)->QueryInterface(piid, (void**)texSrv.GetAddressOf()).ThrowOnError(); + + this.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); + } + + var dxgiFormat = texDesc.Format; + if (!WicManager.GetCorrespondingWicPixelFormat(dxgiFormat, out _, out _)) + dxgiFormat = DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM; + using var istream = ManagedIStream.Create(stream, leaveStreamOpen); var (specs, bytes) = await this.GetRawDataFromExistingTextureAsync( wrap, - Vector2.Zero, - Vector2.One, - DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM, + new() + { + Format = dxgiFormat, + }, true, cancellationToken).ConfigureAwait(false); @@ -80,9 +107,9 @@ internal sealed partial class TextureManager containerGuid, File.Create(pathTemp), props, - true, - false, - cancellationToken); + leaveWrapOpen: true, + leaveStreamOpen: false, + cancellationToken: cancellationToken); } catch (Exception e) { @@ -206,6 +233,7 @@ internal sealed partial class TextureManager { private readonly TextureManager textureManager; private ComPtr wicFactory; + private ComPtr wicFactory2; /// Initializes a new instance of the class. /// An instance of . @@ -216,13 +244,27 @@ internal sealed partial class TextureManager { fixed (Guid* pclsidWicImagingFactory = &CLSID.CLSID_WICImagingFactory) fixed (Guid* piidWicImagingFactory = &IID.IID_IWICImagingFactory) + fixed (Guid* pclsidWicImagingFactory2 = &CLSID.CLSID_WICImagingFactory2) + fixed (Guid* piidWicImagingFactory2 = &IID.IID_IWICImagingFactory2) { - CoCreateInstance( - pclsidWicImagingFactory, - null, - (uint)CLSCTX.CLSCTX_INPROC_SERVER, - piidWicImagingFactory, - (void**)this.wicFactory.GetAddressOf()).ThrowOnError(); + if (CoCreateInstance( + pclsidWicImagingFactory2, + null, + (uint)CLSCTX.CLSCTX_INPROC_SERVER, + piidWicImagingFactory2, + (void**)this.wicFactory2.GetAddressOf()).SUCCEEDED) + { + this.wicFactory2.As(ref this.wicFactory).ThrowOnError(); + } + else + { + CoCreateInstance( + pclsidWicImagingFactory, + null, + (uint)CLSCTX.CLSCTX_INPROC_SERVER, + piidWicImagingFactory, + (void**)this.wicFactory.GetAddressOf()).ThrowOnError(); + } } } } @@ -232,6 +274,76 @@ internal sealed partial class TextureManager /// ~WicManager() => this.ReleaseUnmanagedResource(); + /// + /// Gets the corresponding from a containing a WIC pixel format. + /// + /// The WIC pixel format. + /// The corresponding , or if + /// unavailable. + public static DXGI_FORMAT GetCorrespondingDxgiFormat(Guid fmt) => 0 switch + { + // See https://github.com/microsoft/DirectXTex/wiki/WIC-I-O-Functions#savetowicmemory-savetowicfile + _ when fmt == GUID.GUID_WICPixelFormat128bppRGBAFloat => DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_FLOAT, + _ when fmt == GUID.GUID_WICPixelFormat64bppRGBAHalf => DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_FLOAT, + _ when fmt == GUID.GUID_WICPixelFormat64bppRGBA => DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_UNORM, + _ when fmt == GUID.GUID_WICPixelFormat32bppRGBA1010102XR => DXGI_FORMAT + .DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM, + _ when fmt == GUID.GUID_WICPixelFormat32bppRGBA1010102 => DXGI_FORMAT.DXGI_FORMAT_R10G10B10A2_UNORM, + _ when fmt == GUID.GUID_WICPixelFormat16bppBGRA5551 => DXGI_FORMAT.DXGI_FORMAT_B5G5R5A1_UNORM, + _ when fmt == GUID.GUID_WICPixelFormat16bppBGR565 => DXGI_FORMAT.DXGI_FORMAT_B5G6R5_UNORM, + _ when fmt == GUID.GUID_WICPixelFormat32bppGrayFloat => DXGI_FORMAT.DXGI_FORMAT_R32_FLOAT, + _ when fmt == GUID.GUID_WICPixelFormat16bppGrayHalf => DXGI_FORMAT.DXGI_FORMAT_R16_FLOAT, + _ when fmt == GUID.GUID_WICPixelFormat16bppGray => DXGI_FORMAT.DXGI_FORMAT_R16_UNORM, + _ when fmt == GUID.GUID_WICPixelFormat8bppGray => DXGI_FORMAT.DXGI_FORMAT_R8_UNORM, + _ when fmt == GUID.GUID_WICPixelFormat8bppAlpha => DXGI_FORMAT.DXGI_FORMAT_A8_UNORM, + _ when fmt == GUID.GUID_WICPixelFormat32bppRGBA => DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM, + _ when fmt == GUID.GUID_WICPixelFormat32bppBGRA => DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM, + _ when fmt == GUID.GUID_WICPixelFormat32bppBGR => DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM, + _ => DXGI_FORMAT.DXGI_FORMAT_UNKNOWN, + }; + + /// + /// Gets the corresponding containing a WIC pixel format from a . + /// + /// The DXGI pixel format. + /// The corresponding . + /// Whether the image is in SRGB. + /// true if a corresponding pixel format exists. + public static bool GetCorrespondingWicPixelFormat( + DXGI_FORMAT dxgiPixelFormat, + out Guid wicPixelFormat, + out bool srgb) + { + wicPixelFormat = dxgiPixelFormat switch + { + // See https://github.com/microsoft/DirectXTex/wiki/WIC-I-O-Functions#savetowicmemory-savetowicfile + DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_FLOAT => GUID.GUID_WICPixelFormat128bppRGBAFloat, + DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_FLOAT => GUID.GUID_WICPixelFormat64bppRGBAHalf, + DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_UNORM => GUID.GUID_WICPixelFormat64bppRGBA, + DXGI_FORMAT.DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM => GUID.GUID_WICPixelFormat32bppRGBA1010102XR, + DXGI_FORMAT.DXGI_FORMAT_R10G10B10A2_UNORM => GUID.GUID_WICPixelFormat32bppRGBA1010102, + DXGI_FORMAT.DXGI_FORMAT_B5G5R5A1_UNORM => GUID.GUID_WICPixelFormat16bppBGRA5551, + DXGI_FORMAT.DXGI_FORMAT_B5G6R5_UNORM => GUID.GUID_WICPixelFormat16bppBGR565, + DXGI_FORMAT.DXGI_FORMAT_R32_FLOAT => GUID.GUID_WICPixelFormat32bppGrayFloat, + DXGI_FORMAT.DXGI_FORMAT_R16_FLOAT => GUID.GUID_WICPixelFormat16bppGrayHalf, + DXGI_FORMAT.DXGI_FORMAT_R16_UNORM => GUID.GUID_WICPixelFormat16bppGray, + DXGI_FORMAT.DXGI_FORMAT_R8_UNORM => GUID.GUID_WICPixelFormat8bppGray, + DXGI_FORMAT.DXGI_FORMAT_A8_UNORM => GUID.GUID_WICPixelFormat8bppAlpha, + DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM => GUID.GUID_WICPixelFormat32bppRGBA, + DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM_SRGB => GUID.GUID_WICPixelFormat32bppRGBA, + DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM => GUID.GUID_WICPixelFormat32bppBGRA, + DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM_SRGB => GUID.GUID_WICPixelFormat32bppBGRA, + DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM => GUID.GUID_WICPixelFormat32bppBGR, + DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM_SRGB => GUID.GUID_WICPixelFormat32bppBGR, + _ => Guid.Empty, + }; + srgb = dxgiPixelFormat + is DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM_SRGB + or DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM_SRGB + or DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM_SRGB; + return wicPixelFormat != Guid.Empty; + } + /// public void Dispose() { @@ -349,7 +461,7 @@ internal sealed partial class TextureManager bitmapLock.Get()->GetDataPointer(&cbBufferSize, &pbData).ThrowOnError(); bitmapSource.Get()->CopyPixels(null, stride, cbBufferSize, pbData).ThrowOnError(); return this.textureManager.NoThrottleCreateFromRaw( - new(rcLock.Width, rcLock.Height, (int)stride, (int)dxgiFormat), + new(rcLock.Width, rcLock.Height, (int)dxgiFormat, (int)stride), new(pbData, (int)cbBufferSize)); } @@ -382,15 +494,13 @@ internal sealed partial class TextureManager /// The cancellation token. public unsafe void SaveToStreamUsingWic( RawImageSpecification specs, - byte[] bytes, + ReadOnlySpan bytes, Guid containerFormat, ComPtr stream, IReadOnlyDictionary? props = null, CancellationToken cancellationToken = default) { - var outPixelFormat = GUID.GUID_WICPixelFormat32bppBGRA; - var inPixelFormat = GetCorrespondingWicPixelFormat((DXGI_FORMAT)specs.DxgiFormat); - if (inPixelFormat == Guid.Empty) + if (!GetCorrespondingWicPixelFormat((DXGI_FORMAT)specs.DxgiFormat, out var inPixelFormat, out var srgb)) throw new NotSupportedException("DXGI_FORMAT from specs is not supported by WIC."); using var encoder = default(ComPtr); @@ -398,6 +508,24 @@ internal sealed partial class TextureManager this.wicFactory.Get()->CreateEncoder(&containerFormat, null, encoder.GetAddressOf()).ThrowOnError(); cancellationToken.ThrowIfCancellationRequested(); + // See: DirectXTK/Src/ScreenGrab.cpp + var outPixelFormat = (DXGI_FORMAT)specs.DxgiFormat switch + { + DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_FLOAT => GUID.GUID_WICPixelFormat128bppRGBAFloat, + DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_FLOAT when !this.wicFactory2.IsEmpty() => + GUID.GUID_WICPixelFormat128bppRGBAFloat, + DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_FLOAT => GUID.GUID_WICPixelFormat32bppBGRA, + DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_UNORM => GUID.GUID_WICPixelFormat64bppBGRA, + DXGI_FORMAT.DXGI_FORMAT_B5G5R5A1_UNORM => GUID.GUID_WICPixelFormat16bppBGRA5551, + DXGI_FORMAT.DXGI_FORMAT_B5G6R5_UNORM => GUID.GUID_WICPixelFormat16bppBGR565, + DXGI_FORMAT.DXGI_FORMAT_R32_FLOAT => GUID.GUID_WICPixelFormat8bppGray, + DXGI_FORMAT.DXGI_FORMAT_R16_FLOAT => GUID.GUID_WICPixelFormat8bppGray, + DXGI_FORMAT.DXGI_FORMAT_R16_UNORM => GUID.GUID_WICPixelFormat8bppGray, + DXGI_FORMAT.DXGI_FORMAT_R8_UNORM => GUID.GUID_WICPixelFormat8bppGray, + DXGI_FORMAT.DXGI_FORMAT_A8_UNORM => GUID.GUID_WICPixelFormat8bppGray, + _ => GUID.GUID_WICPixelFormat24bppBGR, + }; + encoder.Get()->Initialize(stream, WICBitmapEncoderCacheOption.WICBitmapEncoderNoCache) .ThrowOnError(); cancellationToken.ThrowIfCancellationRequested(); @@ -406,35 +534,14 @@ internal sealed partial class TextureManager encoder.Get()->CreateNewFrame(encoderFrame.GetAddressOf(), propertyBag.GetAddressOf()).ThrowOnError(); cancellationToken.ThrowIfCancellationRequested(); + // Opt-in to the WIC2 support for writing 32-bit Windows BMP files with an alpha channel + if (containerFormat == GUID.GUID_ContainerFormatBmp && !this.wicFactory2.IsEmpty()) + propertyBag.Get()->Write("EnableV5Header32bppBGRA", true).ThrowOnError(); + if (props is not null) { - var nprop = 0u; - propertyBag.Get()->CountProperties(&nprop).ThrowOnError(); - for (var i = 0u; i < nprop; i++) - { - var pbag2 = default(PROPBAG2); - var npropread = 0u; - propertyBag.Get()->GetPropertyInfo(i, 1, &pbag2, &npropread).ThrowOnError(); - if (npropread == 0) - continue; - try - { - var propName = new string((char*)pbag2.pstrName); - if (props.TryGetValue(propName, out var untypedValue)) - { - VARIANT val; - // Marshal calls VariantInit. - Marshal.GetNativeVariantForObject(untypedValue, (nint)(&val)); - VariantChangeType(&val, &val, 0, pbag2.vt).ThrowOnError(); - propertyBag.Get()->Write(1, &pbag2, &val).ThrowOnError(); - VariantClear(&val); - } - } - finally - { - CoTaskMemFree(pbag2.pstrName); - } - } + foreach (var (name, untypedValue) in props) + propertyBag.Get()->Write(name, untypedValue).ThrowOnError(); } encoderFrame.Get()->Initialize(propertyBag).ThrowOnError(); @@ -442,6 +549,34 @@ internal sealed partial class TextureManager encoderFrame.Get()->SetPixelFormat(&outPixelFormat).ThrowOnError(); encoderFrame.Get()->SetSize(checked((uint)specs.Width), checked((uint)specs.Height)).ThrowOnError(); + using (var metaWriter = default(ComPtr)) + { + if (encoderFrame.Get()->GetMetadataQueryWriter(metaWriter.GetAddressOf()).SUCCEEDED) + { + if (containerFormat == GUID.GUID_ContainerFormatPng) + { + // Set sRGB chunk + if (srgb) + { + _ = metaWriter.Get()->SetMetadataByName("/sRGB/RenderingIntent", (byte)0); + } + else + { + // add gAMA chunk with gamma 1.0 + // gama value * 100,000 -- i.e. gamma 1.0 + _ = metaWriter.Get()->SetMetadataByName("/sRGB/RenderingIntent", 100000U); + + // remove sRGB chunk which is added by default. + _ = metaWriter.Get()->RemoveMetadataByName("/sRGB/RenderingIntent"); + } + } + else + { + // Set EXIF Colorspace of sRGB + _ = metaWriter.Get()->SetMetadataByName("System.Image.ColorSpace", (ushort)0); + } + } + } using var tempBitmap = default(ComPtr); fixed (byte* pBytes = bytes) @@ -480,61 +615,11 @@ internal sealed partial class TextureManager encoder.Get()->Commit().ThrowOnError(); } - /// - /// Gets the corresponding from a containing a WIC pixel format. - /// - /// The WIC pixel format. - /// The corresponding , or if - /// unavailable. - private static DXGI_FORMAT GetCorrespondingDxgiFormat(Guid fmt) => 0 switch + private void ReleaseUnmanagedResource() { - // See https://github.com/microsoft/DirectXTex/wiki/WIC-I-O-Functions#savetowicmemory-savetowicfile - _ when fmt == GUID.GUID_WICPixelFormat128bppRGBAFloat => DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_FLOAT, - _ when fmt == GUID.GUID_WICPixelFormat64bppRGBAHalf => DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_FLOAT, - _ when fmt == GUID.GUID_WICPixelFormat64bppRGBA => DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_UNORM, - _ when fmt == GUID.GUID_WICPixelFormat32bppRGBA1010102XR => DXGI_FORMAT - .DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM, - _ when fmt == GUID.GUID_WICPixelFormat32bppRGBA1010102 => DXGI_FORMAT.DXGI_FORMAT_R10G10B10A2_UNORM, - _ when fmt == GUID.GUID_WICPixelFormat16bppBGRA5551 => DXGI_FORMAT.DXGI_FORMAT_B5G5R5A1_UNORM, - _ when fmt == GUID.GUID_WICPixelFormat16bppBGR565 => DXGI_FORMAT.DXGI_FORMAT_B5G6R5_UNORM, - _ when fmt == GUID.GUID_WICPixelFormat32bppGrayFloat => DXGI_FORMAT.DXGI_FORMAT_R32_FLOAT, - _ when fmt == GUID.GUID_WICPixelFormat16bppGrayHalf => DXGI_FORMAT.DXGI_FORMAT_R16_FLOAT, - _ when fmt == GUID.GUID_WICPixelFormat16bppGray => DXGI_FORMAT.DXGI_FORMAT_R16_UNORM, - _ when fmt == GUID.GUID_WICPixelFormat8bppGray => DXGI_FORMAT.DXGI_FORMAT_R8_UNORM, - _ when fmt == GUID.GUID_WICPixelFormat8bppAlpha => DXGI_FORMAT.DXGI_FORMAT_A8_UNORM, - _ when fmt == GUID.GUID_WICPixelFormat32bppRGBA => DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM, - _ when fmt == GUID.GUID_WICPixelFormat32bppBGRA => DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM, - _ when fmt == GUID.GUID_WICPixelFormat32bppBGR => DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM, - _ => DXGI_FORMAT.DXGI_FORMAT_UNKNOWN, - }; - - /// - /// Gets the corresponding containing a WIC pixel format from a . - /// - /// The DXGI pixel format. - /// The corresponding , or if unavailable. - private static Guid GetCorrespondingWicPixelFormat(DXGI_FORMAT fmt) => fmt switch - { - // See https://github.com/microsoft/DirectXTex/wiki/WIC-I-O-Functions#savetowicmemory-savetowicfile - DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_FLOAT => GUID.GUID_WICPixelFormat128bppRGBAFloat, - DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_FLOAT => GUID.GUID_WICPixelFormat64bppRGBAHalf, - DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_UNORM => GUID.GUID_WICPixelFormat64bppRGBA, - DXGI_FORMAT.DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM => GUID.GUID_WICPixelFormat32bppRGBA1010102XR, - DXGI_FORMAT.DXGI_FORMAT_R10G10B10A2_UNORM => GUID.GUID_WICPixelFormat32bppRGBA1010102, - DXGI_FORMAT.DXGI_FORMAT_B5G5R5A1_UNORM => GUID.GUID_WICPixelFormat16bppBGRA5551, - DXGI_FORMAT.DXGI_FORMAT_B5G6R5_UNORM => GUID.GUID_WICPixelFormat16bppBGR565, - DXGI_FORMAT.DXGI_FORMAT_R32_FLOAT => GUID.GUID_WICPixelFormat32bppGrayFloat, - DXGI_FORMAT.DXGI_FORMAT_R16_FLOAT => GUID.GUID_WICPixelFormat16bppGrayHalf, - DXGI_FORMAT.DXGI_FORMAT_R16_UNORM => GUID.GUID_WICPixelFormat16bppGray, - DXGI_FORMAT.DXGI_FORMAT_R8_UNORM => GUID.GUID_WICPixelFormat8bppGray, - DXGI_FORMAT.DXGI_FORMAT_A8_UNORM => GUID.GUID_WICPixelFormat8bppAlpha, - DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM => GUID.GUID_WICPixelFormat32bppRGBA, - DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM => GUID.GUID_WICPixelFormat32bppBGRA, - DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM => GUID.GUID_WICPixelFormat32bppBGR, - _ => Guid.Empty, - }; - - private void ReleaseUnmanagedResource() => this.wicFactory.Reset(); + this.wicFactory.Reset(); + this.wicFactory2.Reset(); + } private readonly struct ComponentEnumerable : IEnumerable> where T : unmanaged, IWICComponentInfo.Interface diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.cs b/Dalamud/Interface/Textures/Internal/TextureManager.cs index 522167c55..921febe94 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.cs @@ -63,7 +63,7 @@ internal sealed partial class TextureManager : IServiceType, IDisposable, ITextu this.wicManager = new(this); } - /// Gets the D3D11 Device used to create textures. + /// Gets the D3D11 Device used to create textures. Ownership is not transferred. public unsafe ComPtr Device { get @@ -80,15 +80,19 @@ internal sealed partial class TextureManager : IServiceType, IDisposable, ITextu } } + /// Gets a simpler drawer. + public SimpleDrawerImpl SimpleDrawer => + this.simpleDrawer ?? throw new ObjectDisposedException(nameof(TextureManager)); + /// Gets the shared texture manager. public SharedTextureManager Shared => this.sharedTextureManager ?? - throw new ObjectDisposedException(nameof(this.sharedTextureManager)); + throw new ObjectDisposedException(nameof(TextureManager)); /// Gets the WIC manager. public WicManager Wic => this.wicManager ?? - throw new ObjectDisposedException(nameof(this.sharedTextureManager)); + throw new ObjectDisposedException(nameof(TextureManager)); /// public void Dispose() @@ -98,9 +102,7 @@ internal sealed partial class TextureManager : IServiceType, IDisposable, ITextu this.disposing = true; - this.drawsOneSquare?.Dispose(); - this.drawsOneSquare = null; - + Interlocked.Exchange(ref this.simpleDrawer, null)?.Dispose(); Interlocked.Exchange(ref this.sharedTextureManager, null)?.Dispose(); Interlocked.Exchange(ref this.wicManager, null)?.Dispose(); } @@ -201,7 +203,7 @@ internal sealed partial class TextureManager : IServiceType, IDisposable, ITextu D3D11_FORMAT_SUPPORT supported; if (this.Device.Get()->CheckFormatSupport(dxgiFormat, (uint*)&supported).FAILED) return false; - + const D3D11_FORMAT_SUPPORT required = D3D11_FORMAT_SUPPORT.D3D11_FORMAT_SUPPORT_TEXTURE2D; return (supported & required) == required; } @@ -232,7 +234,7 @@ internal sealed partial class TextureManager : IServiceType, IDisposable, ITextu var subrdata = new D3D11_SUBRESOURCE_DATA { pSysMem = dataPtr, SysMemPitch = (uint)specs.Pitch }; device.Get()->CreateTexture2D(&texd, &subrdata, texture.GetAddressOf()).ThrowOnError(); } - + var viewDesc = new D3D11_SHADER_RESOURCE_VIEW_DESC { Format = texd.Format, @@ -264,7 +266,7 @@ internal sealed partial class TextureManager : IServiceType, IDisposable, ITextu } return this.NoThrottleCreateFromRaw( - RawImageSpecification.From(buffer.Width, buffer.Height, dxgiFormat), + new(buffer.Width, buffer.Height, dxgiFormat), buffer.RawData); } diff --git a/Dalamud/Interface/Textures/Internal/ViewportTextureWrap.cs b/Dalamud/Interface/Textures/Internal/ViewportTextureWrap.cs new file mode 100644 index 000000000..8a0a17183 --- /dev/null +++ b/Dalamud/Interface/Textures/Internal/ViewportTextureWrap.cs @@ -0,0 +1,239 @@ +using System.Diagnostics; +using System.Numerics; +using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Game; +using Dalamud.Interface.Internal; +using Dalamud.Utility; + +using ImGuiNET; + +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +using NotSupportedException = System.NotSupportedException; + +namespace Dalamud.Interface.Textures.Internal; + +/// A texture wrap that takes its buffer from the frame buffer (of swap chain). +internal sealed class ViewportTextureWrap : IDalamudTextureWrap, IDeferredDisposable +{ + private readonly uint viewportId; + private readonly bool beforeImGuiRender; + private readonly CancellationToken cancellationToken; + private readonly TaskCompletionSource firstUpdateTaskCompletionSource = new(); + + private D3D11_TEXTURE2D_DESC desc; + private ComPtr tex; + private ComPtr srv; + private ComPtr rtv; + + private bool autoUpdate; + private bool disposed; + + /// Initializes a new instance of the class. + /// The source viewport ID. + /// Capture before calling . + /// If true, automatically update the underlying texture. + /// The cancellation token. + public ViewportTextureWrap( + uint viewportId, + bool beforeImGuiRender, + bool autoUpdate, + CancellationToken cancellationToken) + { + this.viewportId = viewportId; + this.beforeImGuiRender = beforeImGuiRender; + this.autoUpdate = autoUpdate; + this.cancellationToken = cancellationToken; + } + + /// Finalizes an instance of the class. + ~ViewportTextureWrap() => this.Dispose(false); + + /// + public unsafe nint ImGuiHandle => (nint)this.srv.Get(); + + /// + public int Width => (int)this.desc.Width; + + /// + public int Height => (int)this.desc.Height; + + /// Gets the task representing the first call. + public Task FirstUpdateTask => this.firstUpdateTaskCompletionSource.Task; + + /// Updates the texture from the source viewport. + public unsafe void Update() + { + if (this.cancellationToken.IsCancellationRequested || this.disposed) + { + this.firstUpdateTaskCompletionSource.TrySetCanceled(); + return; + } + + try + { + ThreadSafety.AssertMainThread(); + + using var backBuffer = GetImGuiViewportBackBuffer(this.viewportId); + D3D11_TEXTURE2D_DESC newDesc; + backBuffer.Get()->GetDesc(&newDesc); + + if (newDesc.SampleDesc.Count > 1) + throw new NotSupportedException("Multisampling is not expected"); + + using var device = default(ComPtr); + backBuffer.Get()->GetDevice(device.GetAddressOf()); + + using var context = default(ComPtr); + device.Get()->GetImmediateContext(context.GetAddressOf()); + + if (this.desc.Width != newDesc.Width + || this.desc.Height != newDesc.Height + || this.desc.Format != newDesc.Format) + { + var texDesc = new D3D11_TEXTURE2D_DESC + { + Width = newDesc.Width, + Height = newDesc.Height, + MipLevels = 1, + ArraySize = 1, + Format = newDesc.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, + }; + + using var texTemp = default(ComPtr); + device.Get()->CreateTexture2D(&texDesc, null, texTemp.GetAddressOf()).ThrowOnError(); + + var rtvDesc = new D3D11_RENDER_TARGET_VIEW_DESC( + texTemp, + D3D11_RTV_DIMENSION.D3D11_RTV_DIMENSION_TEXTURE2D); + using var rtvTemp = default(ComPtr); + device.Get()->CreateRenderTargetView( + (ID3D11Resource*)texTemp.Get(), + &rtvDesc, + rtvTemp.GetAddressOf()).ThrowOnError(); + + var srvDesc = new D3D11_SHADER_RESOURCE_VIEW_DESC( + texTemp, + D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE2D); + using var srvTemp = default(ComPtr); + device.Get()->CreateShaderResourceView( + (ID3D11Resource*)texTemp.Get(), + &srvDesc, + srvTemp.GetAddressOf()) + .ThrowOnError(); + + this.desc = newDesc; + srvTemp.Swap(ref this.srv); + rtvTemp.Swap(ref this.rtv); + texTemp.Swap(ref this.tex); + } + + context.Get()->CopyResource((ID3D11Resource*)this.tex.Get(), (ID3D11Resource*)backBuffer.Get()); + var rtvLocal = this.rtv.Get(); + context.Get()->OMSetRenderTargets(1u, &rtvLocal, null); + Service.Get().SimpleDrawer.StripAlpha(context.Get()); + + var dummy = default(ID3D11RenderTargetView*); + context.Get()->OMSetRenderTargets(1u, &dummy, null); + + this.firstUpdateTaskCompletionSource.TrySetResult(this); + } + catch (Exception e) + { + this.firstUpdateTaskCompletionSource.TrySetException(e); + } + + if (this.autoUpdate) + { + Service.Get().RunOnTick( + () => + { + if (this.beforeImGuiRender) + Service.Get().RunBeforeImGuiRender(this.Update); + else + Service.Get().RunAfterImGuiRender(this.Update); + }, + cancellationToken: this.cancellationToken); + } + } + + /// Queue the texture to be disposed once the frame ends. + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// Actually dispose the wrapped texture. + void IDeferredDisposable.RealDispose() + { + _ = this.FirstUpdateTask.Exception; + this.tex.Reset(); + this.srv.Reset(); + } + + private static unsafe ComPtr GetImGuiViewportBackBuffer(uint viewportId) + { + ThreadSafety.AssertMainThread(); + var viewports = ImGui.GetPlatformIO().Viewports; + var viewportIndex = 0; + for (; viewportIndex < viewports.Size; viewportIndex++) + { + if (viewports[viewportIndex].ID == viewportId) + break; + } + + if (viewportIndex >= viewports.Size) + { + throw new ArgumentOutOfRangeException( + nameof(viewportId), + viewportId, + "Could not find a viewport with the given ID."); + } + + var texture = default(ComPtr); + + Debug.Assert(viewports[0].ID == ImGui.GetMainViewport().ID, "ImGui has changed"); + if (viewportId == viewports[0].ID) + { + var device = FFXIVClientStructs.FFXIV.Client.Graphics.Kernel.Device.Instance(); + fixed (Guid* piid = &IID.IID_ID3D11Texture2D) + { + ((IDXGISwapChain*)device->SwapChain->DXGISwapChain)->GetBuffer(0, piid, (void**)texture.GetAddressOf()) + .ThrowOnError(); + } + } + else + { + // See: ImGui_Impl_DX11.ImGuiViewportDataDx11 + var rud = (nint*)viewports[viewportIndex].RendererUserData; + if (rud == null || rud[0] == nint.Zero || rud[1] == nint.Zero) + throw new InvalidOperationException(); + + using var resource = default(ComPtr); + ((ID3D11RenderTargetView*)rud[viewportIndex])->GetResource(resource.GetAddressOf()); + resource.As(&texture).ThrowOnError(); + } + + return texture; + } + + private void Dispose(bool disposing) + { + this.disposed = true; + this.autoUpdate = false; + if (disposing) + Service.GetNullable()?.EnqueueDeferredDispose(this); + else + ((IDeferredDisposable)this).RealDispose(); + } +} diff --git a/Dalamud/Plugin/Services/ITextureProvider.cs b/Dalamud/Plugin/Services/ITextureProvider.cs index 854579bcd..79b2a7ad3 100644 --- a/Dalamud/Plugin/Services/ITextureProvider.cs +++ b/Dalamud/Plugin/Services/ITextureProvider.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; -using System.Numerics; using System.Reflection; using System.Threading; using System.Threading.Tasks; @@ -34,32 +33,47 @@ public partial interface ITextureProvider /// /// 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. Use 0 to use the source format. + /// The texture modification arguments. /// Whether to leave non-disposed when the returned /// completes. /// 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 - /// . - /// + /// This function may throw an exception. Task CreateFromExistingTextureAsync( IDalamudTextureWrap wrap, - Vector2 uv0, - Vector2 uv1, - int dxgiFormat = 0, + ExistingTextureModificationArgs args = default, bool leaveWrapOpen = false, CancellationToken cancellationToken = default); + /// Creates a texture from the game screen, before rendering Dalamud. + /// If true, automatically update the underlying texture. + /// The cancellation token. + /// A containing the copied texture on success. Dispose after use. + /// This function may throw an exception. + Task CreateFromGameScreen( + bool autoUpdate = false, + CancellationToken cancellationToken = default); + + /// Creates a texture from the game screen, before rendering Dalamud. + /// The viewport ID. + /// If true, automatically update the underlying texture. + /// The cancellation token. + /// A containing the copied texture on success. Dispose after use. + /// + /// Use ImGui.GetMainViewport().ID to capture the game screen with Dalamud rendered. + /// This function may throw an exception. + /// + Task CreateFromImGuiViewport( + uint viewportId, + bool autoUpdate = false, + 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. /// The cancellation token. /// A containing the loaded texture on success. Dispose after use. + /// This function may throw an exception. Task CreateFromImageAsync( ReadOnlyMemory bytes, CancellationToken cancellationToken = default); @@ -70,8 +84,10 @@ public partial interface ITextureProvider /// Whether to leave the stream open once the task completes, sucessfully or not. /// The cancellation token. /// A containing the loaded texture on success. Dispose after use. - /// will be closed or not only according to ; - /// is irrelevant in closing the stream. + /// + /// will be closed or not only according to ; + /// is irrelevant in closing the stream. + /// This function may throw an exception. Task CreateFromImageAsync( Stream stream, bool leaveOpen = false, @@ -81,6 +97,7 @@ public partial interface ITextureProvider /// The specifications for the raw bitmap. /// The bytes to load. /// The texture loaded from the supplied raw bitmap. Dispose after use. + /// This function may throw an exception. IDalamudTextureWrap CreateFromRaw( RawImageSpecification specs, ReadOnlySpan bytes); @@ -90,6 +107,7 @@ public partial interface ITextureProvider /// The bytes to load. /// The cancellation token. /// A containing the loaded texture on success. Dispose after use. + /// This function may throw an exception. Task CreateFromRawAsync( RawImageSpecification specs, ReadOnlyMemory bytes, @@ -101,8 +119,11 @@ public partial interface ITextureProvider /// Whether to leave the stream open once the task completes, sucessfully or not. /// The cancellation token. /// A containing the loaded texture on success. Dispose after use. - /// will be closed or not only according to ; - /// is irrelevant in closing the stream. + /// + /// will be closed or not only according to ; + /// is irrelevant in closing the stream. + /// This function may throw an exception. + /// Task CreateFromRawAsync( RawImageSpecification specs, Stream stream, @@ -115,12 +136,14 @@ public partial interface ITextureProvider /// /// The texture to obtain a handle to. /// A texture wrap that can be used to render the texture. Dispose after use. + /// This function may throw an exception. IDalamudTextureWrap CreateFromTexFile(TexFile file); /// 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. + /// This function may throw an exception. Task CreateFromTexFileAsync( TexFile file, CancellationToken cancellationToken = default); @@ -128,13 +151,14 @@ public partial interface ITextureProvider /// Gets the supported bitmap decoders. /// The supported bitmap decoders. /// - /// The following functions support the files of the container types pointed by yielded values. + /// The following functions support the files of the container types pointed by yielded values. ///
    ///
  • ///
  • ///
  • ///
  • ///
+ /// This function may throw an exception. ///
IEnumerable GetSupportedImageDecoderInfos(); @@ -145,32 +169,39 @@ public partial interface ITextureProvider ///
    ///
  • ///
+ /// This function may throw an exception. /// IEnumerable GetSupportedImageEncoderInfos(); /// Gets a shared texture corresponding to the given game resource icon specifier. /// A game icon specifier. /// The shared texture that you may use to obtain the loaded texture wrap and load states. - /// This function is under the effect of . + /// + /// This function is under the effect of . + /// This function does not throw exceptions. /// ISharedImmediateTexture GetFromGameIcon(in GameIconLookup lookup); /// Gets a shared texture corresponding to the given path to a game resource. /// A path to a game resource. /// The shared texture that you may use to obtain the loaded texture wrap and load states. - /// This function is under the effect of . + /// + /// This function is under the effect of . + /// This function does not throw exceptions. /// ISharedImmediateTexture GetFromGame(string path); /// Gets a shared texture corresponding to the given file on the filesystem. /// A path to a file on the filesystem. /// The shared texture that you may use to obtain the loaded texture wrap and load states. + /// This function does not throw exceptions. ISharedImmediateTexture GetFromFile(string path); /// Gets a shared texture corresponding to the given file of the assembly manifest resources. /// The assembly containing manifest resources. /// The case-sensitive name of the manifest resource being requested. /// The shared texture that you may use to obtain the loaded texture wrap and load states. + /// This function does not throw exceptions. ISharedImmediateTexture GetFromManifestResource(Assembly assembly, string name); /// Get a path for a specific icon's .tex file. @@ -185,14 +216,12 @@ public partial interface ITextureProvider /// The icon lookup. /// The resolved path. /// true if the corresponding file exists and has been set. + /// This function does not throw exceptions. bool TryGetIconPath(in GameIconLookup lookup, [NotNullWhen(true)] out string? path); /// Gets the raw data of a texture wrap. /// The source texture wrap. - /// 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 texture modification arguments. /// Whether to leave non-disposed when the returned /// completes. /// The cancellation token. @@ -200,16 +229,11 @@ public partial interface ITextureProvider /// /// 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. + /// This function may throw an exception. /// Task<(RawImageSpecification Specification, byte[] RawData)> GetRawDataFromExistingTextureAsync( IDalamudTextureWrap wrap, - Vector2 uv0, - Vector2 uv1, - int dxgiFormat = 0, + ExistingTextureModificationArgs args = default, bool leaveWrapOpen = false, CancellationToken cancellationToken = default); @@ -217,9 +241,7 @@ public partial interface ITextureProvider /// The texture wrap to save. /// The container GUID, obtained from . /// The stream to save to. - /// Properties to pass to the encoder. See - /// Microsoft - /// Learn for available parameters. + /// Properties to pass to the encoder. See remarks for valid values. /// Whether to leave non-disposed when the returned /// completes. /// Whether to leave open when the returned @@ -228,6 +250,15 @@ public partial interface ITextureProvider /// A task representing the save process. /// /// must not be disposed until the task finishes. + /// See the following webpages for the valid values for per + /// . + /// + /// This function may throw an exception. /// Task SaveToStreamAsync( IDalamudTextureWrap wrap, @@ -242,15 +273,22 @@ public partial interface ITextureProvider /// The texture wrap to save. /// The container GUID, obtained from . /// The target file path. The target file will be overwritten if it exist. - /// Properties to pass to the encoder. See - /// Microsoft - /// Learn for available parameters. + /// Properties to pass to the encoder. See remarks for valid values. /// Whether to leave non-disposed when the returned /// completes. /// The cancellation token. /// A task representing the save process. /// /// must not be disposed until the task finishes. + /// See the following webpages for the valid values for per + /// . + /// + /// This function may throw an exception. /// Task SaveToFileAsync( IDalamudTextureWrap wrap, @@ -266,11 +304,13 @@ public partial interface ITextureProvider /// /// The DXGI format. /// true if supported. + /// This function does not throw exceptions. bool IsDxgiFormatSupported(int dxgiFormat); /// Determines whether the system supports the given DXGI format for use with /// . /// The DXGI format. /// true if supported. + /// This function does not throw exceptions. bool IsDxgiFormatSupportedForCreateFromExistingTextureAsync(int dxgiFormat); } diff --git a/Dalamud/Plugin/Services/RawImageSpecification.cs b/Dalamud/Plugin/Services/RawImageSpecification.cs index 4d0aa2e9e..7c17ee9fb 100644 --- a/Dalamud/Plugin/Services/RawImageSpecification.cs +++ b/Dalamud/Plugin/Services/RawImageSpecification.cs @@ -1,25 +1,53 @@ -using System.Diagnostics.CodeAnalysis; - using TerraFX.Interop.DirectX; namespace Dalamud.Plugin.Services; -/// -/// Describes a raw image. -/// -/// The width of the image. -/// The height of the image. -/// The pitch of the image in bytes. The value may not always exactly match -/// * bytesPerPixelFromDxgiFormat. -/// The format of the image. See DXGI_FORMAT. -[SuppressMessage( - "StyleCop.CSharp.NamingRules", - "SA1313:Parameter names should begin with lower-case letter", - Justification = "no")] -public readonly record struct RawImageSpecification(int Width, int Height, int Pitch, int DxgiFormat) +/// Describes a raw image. +public record struct RawImageSpecification { private const string FormatNotSupportedMessage = $"{nameof(DxgiFormat)} is not supported."; + /// Initializes a new instance of the class. + /// The width of the raw image. + /// The height of the raw image. + /// The DXGI format of the raw image. + /// The pitch of the raw image in bytes. + /// Specify -1 to calculate it from other parameters. + public RawImageSpecification(int width, int height, int dxgiFormat, int pitch = -1) + { + if (pitch < 0) + { + if (!GetFormatInfo((DXGI_FORMAT)dxgiFormat, out var bitsPerPixel, out var isBlockCompression)) + throw new NotSupportedException(FormatNotSupportedMessage); + + pitch = isBlockCompression + ? Math.Max(1, (width + 3) / 4) * 2 * bitsPerPixel + : ((width * bitsPerPixel) + 7) / 8; + } + + this.Width = width; + this.Height = height; + this.Pitch = pitch; + this.DxgiFormat = dxgiFormat; + } + + /// Gets or sets the width of the raw image. + public int Width { get; set; } + + /// Gets or sets the height of the raw image. + public int Height { get; set; } + + /// Gets or sets the pitch of the raw image in bytes. + /// The value may not always exactly match + /// * bytesPerPixelFromDxgiFormat. + /// + public int Pitch { get; set; } + + /// Gets or sets the format of the raw image. + /// See + /// DXGI_FORMAT. + public int DxgiFormat { get; set; } + /// Gets the number of bits per pixel. /// Thrown if is not supported. public int BitsPerPixel => @@ -27,27 +55,6 @@ public readonly record struct RawImageSpecification(int Width, int Height, int P ? bitsPerPixel : throw new NotSupportedException(FormatNotSupportedMessage); - /// - /// Creates a new instance of record using the given resolution and pixel - /// format. Pitch will be automatically calculated. - /// - /// The width. - /// The height. - /// The format. - /// The new instance. - /// Thrown if is not supported. - public static RawImageSpecification From(int width, int height, int format) - { - if (!GetFormatInfo((DXGI_FORMAT)format, out var bitsPerPixel, out var isBlockCompression)) - throw new NotSupportedException(FormatNotSupportedMessage); - - var pitch = isBlockCompression - ? Math.Max(1, (width + 3) / 4) * 2 * bitsPerPixel - : ((width * bitsPerPixel) + 7) / 8; - - return new(width, height, pitch, format); - } - /// /// Creates a new instance of record using the given resolution, /// in B8G8R8A8(BGRA32) UNorm pixel format. @@ -56,7 +63,7 @@ public readonly record struct RawImageSpecification(int Width, int Height, int P /// The height. /// The new instance. public static RawImageSpecification Bgra32(int width, int height) => - new(width, height, width * 4, (int)DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM); + new(width, height, (int)DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM, width * 4); /// /// Creates a new instance of record using the given resolution, @@ -66,7 +73,7 @@ public readonly record struct RawImageSpecification(int Width, int Height, int P /// The height. /// The new instance. public static RawImageSpecification Rgba32(int width, int height) => - new(width, height, width * 4, (int)DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM); + new(width, height, (int)DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM, width * 4); /// /// Creates a new instance of record using the given resolution, @@ -76,7 +83,7 @@ public readonly record struct RawImageSpecification(int Width, int Height, int P /// The height. /// The new instance. public static RawImageSpecification A8(int width, int height) => - new(width, height, width, (int)DXGI_FORMAT.DXGI_FORMAT_A8_UNORM); + new(width, height, (int)DXGI_FORMAT.DXGI_FORMAT_A8_UNORM, width); private static bool GetFormatInfo(DXGI_FORMAT format, out int bitsPerPixel, out bool isBlockCompression) { diff --git a/Dalamud/Storage/Assets/DalamudAssetManager.cs b/Dalamud/Storage/Assets/DalamudAssetManager.cs index a9293eb6d..e51788c0b 100644 --- a/Dalamud/Storage/Assets/DalamudAssetManager.cs +++ b/Dalamud/Storage/Assets/DalamudAssetManager.cs @@ -83,6 +83,9 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA /// public IDalamudTextureWrap Empty4X4 => this.GetDalamudTextureWrap(DalamudAsset.Empty4X4); + /// + public IDalamudTextureWrap White4X4 => this.GetDalamudTextureWrap(DalamudAsset.White4X4); + /// public void Dispose() { diff --git a/Dalamud/Storage/Assets/DalamudAssetRawTextureAttribute.cs b/Dalamud/Storage/Assets/DalamudAssetRawTextureAttribute.cs index 99253411b..4fba51a2f 100644 --- a/Dalamud/Storage/Assets/DalamudAssetRawTextureAttribute.cs +++ b/Dalamud/Storage/Assets/DalamudAssetRawTextureAttribute.cs @@ -2,25 +2,21 @@ using SharpDX.DXGI; +using TerraFX.Interop.DirectX; + namespace Dalamud.Storage.Assets; -/// -/// Provide raw texture data directly. -/// +/// Provide raw texture data directly. [AttributeUsage(AttributeTargets.Field)] internal class DalamudAssetRawTextureAttribute : Attribute { - /// - /// Initializes a new instance of the class. - /// + /// Initializes a new instance of the class. /// The width. - /// The pitch. /// The height. /// The format. - public DalamudAssetRawTextureAttribute(int width, int pitch, int height, Format format) - { - this.Specification = new(width, height, pitch, (int)format); - } + /// The pitch. + public DalamudAssetRawTextureAttribute(int width, int height, DXGI_FORMAT format, int pitch) => + this.Specification = new(width, height, (int)format, pitch); /// /// Gets the specification. diff --git a/Dalamud/Storage/Assets/IDalamudAssetManager.cs b/Dalamud/Storage/Assets/IDalamudAssetManager.cs index 643eef18c..f402c9f93 100644 --- a/Dalamud/Storage/Assets/IDalamudAssetManager.cs +++ b/Dalamud/Storage/Assets/IDalamudAssetManager.cs @@ -23,6 +23,11 @@ public interface IDalamudAssetManager /// IDalamudTextureWrap Empty4X4 { get; } + /// + /// Gets the shared texture wrap for . + /// + IDalamudTextureWrap White4X4 { get; } + /// /// Gets whether the stream for the asset is instantly available. /// diff --git a/Dalamud/Utility/TerraFxCom/IPropertyBag2Extensions.cs b/Dalamud/Utility/TerraFxCom/IPropertyBag2Extensions.cs new file mode 100644 index 000000000..cfb11ae41 --- /dev/null +++ b/Dalamud/Utility/TerraFxCom/IPropertyBag2Extensions.cs @@ -0,0 +1,83 @@ +using System.Runtime.InteropServices; + +using TerraFX.Interop.Windows; + +using static TerraFX.Interop.Windows.Windows; + +namespace Dalamud.Utility.TerraFxCom; + +/// Utilities for and its derivatives. +internal static unsafe partial class TerraFxComInterfaceExtensions +{ + /// Calls . + /// The property bag. + /// The name of the item to be interpreted as a VARIANT. + /// The new value, to be interpreted as a . + /// Return value from . + public static HRESULT Write(ref this IPropertyBag2 obj, string name, object? value) + { + VARIANT varValue; + // Marshal calls VariantInit. + Marshal.GetNativeVariantForObject(value, (nint)(&varValue)); + try + { + fixed (char* pName = name) + { + var option = new PROPBAG2 { pstrName = (ushort*)pName }; + return obj.Write(1, &option, &varValue); + } + } + finally + { + VariantClear(&varValue); + } + } + + /// Calls . + /// The object. + /// The name of the metadata. + /// The new value, to be interpreted as a . + /// Return value from . + public static HRESULT SetMetadataByName(ref this IWICMetadataQueryWriter obj, string name, object? value) + { + VARIANT varValue; + // Marshal calls VariantInit. + Marshal.GetNativeVariantForObject(value, (nint)(&varValue)); + try + { + PROPVARIANT propVarValue; + var propVarRes = VariantToPropVariant(&varValue, &propVarValue); + if (propVarRes < 0) + return propVarRes; + + try + { + fixed (char* pName = name) + return obj.SetMetadataByName((ushort*)pName, &propVarValue); + } + finally + { + _ = PropVariantClear(&propVarValue); + } + } + finally + { + _ = VariantClear(&varValue); + } + } + + /// Calls . + /// The object. + /// The name of the metadata. + /// Return value from . + public static HRESULT RemoveMetadataByName(ref this IWICMetadataQueryWriter obj, string name) + { + fixed (char* pName = name) + return obj.RemoveMetadataByName((ushort*)pName); + } + + [LibraryImport("propsys.dll")] + private static partial int VariantToPropVariant( + void* pVarIn, + void* pPropVarOut); +}