From 2ab6d30aba5bf8cd49ebfbbb773b7604e24635ff Mon Sep 17 00:00:00 2001 From: aphrody-dev Date: Fri, 9 Oct 2026 19:36:42 +0200 Subject: [PATCH] =?UTF-8?q?feat(atlas):=20BackendD3D12=20natif=20(atlas=20?= =?UTF-8?q?DirectWrite/D2D=20via=20D3D11On12,=20swap=20chain=20flip=20?= =?UTF-8?q?=C3=A0=20file=20D3D12,=203=20trames=20en=20vol,=20anneau=20d'up?= =?UTF-8?q?load,=20root=20signatures=20et=20PSO=20pr=C3=A9compil=C3=A9s,?= =?UTF-8?q?=20shaders=20personnalis=C3=A9s=20multi-passes=20t0-t3=20et=20e?= =?UTF-8?q?ntr=C3=A9es=20temps/curseur,=20repli=20D3D11)=20et=20param?= =?UTF-8?q?=C3=A8tre=20direct3d12=20(sch=C3=A9ma,=20UI=20en-US/fr-FR,=20te?= =?UTF-8?q?st=20de=20s=C3=A9rialisation)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/cascadia/profiles.schema.json | 5 +- .../Resources/en-US/Resources.resw | 5 +- .../Resources/fr-FR/Resources.resw | 5 +- .../SerializationTests.cpp | 26 + src/renderer/atlas/AtlasEngine.r.cpp | 40 +- src/renderer/atlas/BackendD3D12.cpp | 1299 +++++++++++++++++ src/renderer/atlas/BackendD3D12.h | 176 +++ src/renderer/atlas/README.md | 70 + src/renderer/atlas/atlas.vcxproj | 21 + .../atlas/custom_shader_ps_d3d12.hlsl | 5 + .../atlas/custom_shader_vs_d3d12.hlsl | 16 + src/renderer/atlas/makefile.inc | 18 + src/renderer/atlas/shader_d3d12_rootsig.hlsl | 23 + src/renderer/atlas/shader_ps_d3d12.hlsl | 5 + src/renderer/atlas/shader_vs_d3d12.hlsl | 16 + src/renderer/atlas/sources | 5 + src/renderer/atlas/wic.cpp | 34 +- src/renderer/atlas/wic.h | 12 + 18 files changed, 1762 insertions(+), 19 deletions(-) create mode 100644 src/renderer/atlas/BackendD3D12.cpp create mode 100644 src/renderer/atlas/BackendD3D12.h create mode 100644 src/renderer/atlas/custom_shader_ps_d3d12.hlsl create mode 100644 src/renderer/atlas/custom_shader_vs_d3d12.hlsl create mode 100644 src/renderer/atlas/shader_d3d12_rootsig.hlsl create mode 100644 src/renderer/atlas/shader_ps_d3d12.hlsl create mode 100644 src/renderer/atlas/shader_vs_d3d12.hlsl diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json index 33888aa9d..347deec06 100644 --- a/doc/cascadia/profiles.schema.json +++ b/doc/cascadia/profiles.schema.json @@ -2507,12 +2507,13 @@ "type": "array" }, "rendering.graphicsAPI": { - "description": "Direct3D 11 provides a more performant and feature-rich experience, whereas Direct2D is more stable. The default option \"Automatic\" will pick the API that best fits your graphics hardware. If you experience significant issues, consider using Direct2D.", + "description": "Direct3D 11 provides a more performant and feature-rich experience, whereas Direct2D is more stable. Direct3D 12 renders like Direct3D 11 and additionally supports multi-pass custom shaders. The default option \"Automatic\" will pick the API that best fits your graphics hardware. If you experience significant issues, consider using Direct2D.", "type": "string", "enum": [ "automatic", "direct2d", - "direct3d11" + "direct3d11", + "direct3d12" ] }, "rendering.disablePartialInvalidation": { diff --git a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw index 68be6b835..52cb2a6e7 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw @@ -305,7 +305,7 @@ This text is shown next to a list of choices. - Direct3D 11 provides a more performant and feature-rich experience, whereas Direct2D is more stable. The default option "Automatic" will pick the API that best fits your graphics hardware. If you experience significant issues, consider using Direct2D. + Direct3D 11 provides a more performant and feature-rich experience, whereas Direct2D is more stable. Direct3D 12 renders like Direct3D 11 and additionally supports multi-pass custom shaders. The default option "Automatic" will pick the API that best fits your graphics hardware. If you experience significant issues, consider using Direct2D. Automatic @@ -317,6 +317,9 @@ Direct3D 11 + + Direct3D 12 + Disable partial Swap Chain invalidation "Swap Chain" is an official technical term by Microsoft. This text is shown next to a toggle. diff --git a/src/cascadia/TerminalSettingsEditor/Resources/fr-FR/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/fr-FR/Resources.resw index a26ea5217..56b7c585f 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/fr-FR/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/fr-FR/Resources.resw @@ -305,7 +305,7 @@ This text is shown next to a list of choices. - Direct3D 11 offre une expérience plus performante et riche en fonctionnalités, tandis que Direct2D est plus stable. L’option par défaut « Automatique » sélectionne l’API qui correspond le mieux à votre matériel graphique. Si vous rencontrez des problèmes importants, envisagez d’utiliser Direct2D. + Direct3D 11 offre une expérience plus performante et riche en fonctionnalités, tandis que Direct2D est plus stable. Direct3D 12 offre le même rendu que Direct3D 11 et prend en charge en plus les nuanceurs personnalisés à plusieurs passes. L’option par défaut « Automatique » sélectionne l’API qui correspond le mieux à votre matériel graphique. Si vous rencontrez des problèmes importants, envisagez d’utiliser Direct2D. Automatique @@ -317,6 +317,9 @@ Direct3D 11 + + Direct3D 12 + Désactiver l’invalidation partielle de la chaîne d’échange "Swap Chain" is an official technical term by Microsoft. This text is shown next to a toggle. diff --git a/src/cascadia/UnitTests_SettingsModel/SerializationTests.cpp b/src/cascadia/UnitTests_SettingsModel/SerializationTests.cpp index dd2c6e794..f37ba1110 100644 --- a/src/cascadia/UnitTests_SettingsModel/SerializationTests.cpp +++ b/src/cascadia/UnitTests_SettingsModel/SerializationTests.cpp @@ -37,6 +37,7 @@ namespace SettingsModelUnitTests TEST_CLASS(SerializationTests); TEST_METHOD(GlobalSettings); + TEST_METHOD(GraphicsAPI); TEST_METHOD(Profile); TEST_METHOD(ColorScheme); TEST_METHOD(Actions); @@ -150,6 +151,31 @@ namespace SettingsModelUnitTests RoundtripTest(smallGlobalsString); } + void SerializationTests::GraphicsAPI() + { + static constexpr std::pair cases[]{ + { R"({ "rendering.graphicsAPI": "automatic", "actions": [], "keybindings": [] })", winrt::Microsoft::Terminal::Control::GraphicsAPI::Automatic }, + { R"({ "rendering.graphicsAPI": "direct2d", "actions": [], "keybindings": [] })", winrt::Microsoft::Terminal::Control::GraphicsAPI::Direct2D }, + { R"({ "rendering.graphicsAPI": "direct3d11", "actions": [], "keybindings": [] })", winrt::Microsoft::Terminal::Control::GraphicsAPI::Direct3D11 }, + { R"({ "rendering.graphicsAPI": "direct3d12", "actions": [], "keybindings": [] })", winrt::Microsoft::Terminal::Control::GraphicsAPI::Direct3D12 }, + }; + + for (const auto& [jsonString, expected] : cases) + { + RoundtripTest(jsonString); + + const auto settings = implementation::GlobalAppSettings::FromJson(VerifyParseSucceeded(jsonString)); + VERIFY_IS_TRUE(settings->HasGraphicsAPI()); + VERIFY_ARE_EQUAL(expected, settings->GraphicsAPI()); + } + + // Setting the value from code (Settings UI) writes the JSON name back out. + const auto settings = implementation::GlobalAppSettings::FromJson(VerifyParseSucceeded(R"({ "actions": [], "keybindings": [] })")); + settings->GraphicsAPI(winrt::Microsoft::Terminal::Control::GraphicsAPI::Direct3D12); + const auto result = settings->ToJson(); + VERIFY_ARE_EQUAL("direct3d12", result["rendering.graphicsAPI"].asString()); + } + void SerializationTests::Profile() { static constexpr std::string_view profileString{ R"( diff --git a/src/renderer/atlas/AtlasEngine.r.cpp b/src/renderer/atlas/AtlasEngine.r.cpp index 65cb7b0d7..db02fb95e 100644 --- a/src/renderer/atlas/AtlasEngine.r.cpp +++ b/src/renderer/atlas/AtlasEngine.r.cpp @@ -6,6 +6,7 @@ #include "BackendD2D.h" #include "BackendD3D.h" +#include "BackendD3D12.h" // #### NOTE #### // If you see any code in here that contains "_api." you might be seeing a race condition. @@ -173,9 +174,31 @@ void AtlasEngine::_recreateBackend() // D3D11 defers the destruction of objects and only one swap chain can be associated with a // HWND, IWindow, or composition surface at a time. --> Destroy it while we still have the old device. _destroySwapChain(); + _p.d3d12 = {}; auto graphicsAPI = _p.s->target->graphicsAPI; + if (graphicsAPI == GraphicsAPI::Direct3D12) + { + try + { + BackendD3D12::CreateDevice(_p); + _p.device = nullptr; + _p.deviceContext = nullptr; + _b = std::make_unique(_p); + _p.MarkAllAsDirty(); + return; + } + catch (...) + { + // Direct3D 12 is unavailable (or broken) on this adapter. Fall back to Direct3D 11. + LOG_CAUGHT_EXCEPTION(); + _b.reset(); + _p.d3d12 = {}; + graphicsAPI = GraphicsAPI::Direct3D11; + } + } + auto deviceFlags = D3D11_CREATE_DEVICE_SINGLETHREADED #ifndef NDEBUG @@ -358,11 +381,13 @@ bool AtlasEngine::_createSwapChain(HANDLE shutdownEvent) wil::com_ptr swapChain1; wil::unique_handle handle; + // Direct3D 12 swap chains are created on the command queue instead of the device. + IUnknown* const swapChainDevice = _p.d3d12.queue ? static_cast(_p.d3d12.queue.get()) : static_cast(_p.device.get()); if (_p.s->target->hwnd) { desc.AlphaMode = DXGI_ALPHA_MODE_IGNORE; - THROW_IF_FAILED(_p.dxgi.factory->CreateSwapChainForHwnd(_p.device.get(), _p.s->target->hwnd, &desc, nullptr, nullptr, swapChain1.addressof())); + THROW_IF_FAILED(_p.dxgi.factory->CreateSwapChainForHwnd(swapChainDevice, _p.s->target->hwnd, &desc, nullptr, nullptr, swapChain1.addressof())); } else { @@ -373,7 +398,7 @@ bool AtlasEngine::_createSwapChain(HANDLE shutdownEvent) // As per: https://docs.microsoft.com/en-us/windows/win32/api/dcomp/nf-dcomp-dcompositioncreatesurfacehandle static constexpr DWORD COMPOSITIONSURFACE_ALL_ACCESS = 0x0003L; THROW_IF_FAILED(DCompositionCreateSurfaceHandle(COMPOSITIONSURFACE_ALL_ACCESS, nullptr, handle.addressof())); - THROW_IF_FAILED(_p.dxgi.factory.query()->CreateSwapChainForCompositionSurfaceHandle(_p.device.get(), handle.get(), &desc, nullptr, swapChain1.addressof())); + THROW_IF_FAILED(_p.dxgi.factory.query()->CreateSwapChainForCompositionSurfaceHandle(swapChainDevice, handle.get(), &desc, nullptr, swapChain1.addressof())); } _p.swapChain.swapChain = swapChain1.query(); @@ -424,7 +449,10 @@ void AtlasEngine::_destroySwapChain() void AtlasEngine::_resizeBuffers() { _b->ReleaseResources(); - _p.deviceContext->ClearState(); + if (_p.deviceContext) + { + _p.deviceContext->ClearState(); + } THROW_IF_FAILED(_p.swapChain.swapChain->ResizeBuffers(0, _p.s->targetSize.x, _p.s->targetSize.y, DXGI_FORMAT_UNKNOWN, swapChainFlags)); _p.swapChain.targetSize = _p.s->targetSize; @@ -496,7 +524,11 @@ void AtlasEngine::_present() { // Render() has already issued GPU commands. If we don't Present() now, the GPU's // command queue would just fill up and effectively leak memory. Flush() it. - _p.deviceContext->Flush(); + // BackendD3D12 submits its command lists itself. + if (_p.deviceContext) + { + _p.deviceContext->Flush(); + } return; } diff --git a/src/renderer/atlas/BackendD3D12.cpp b/src/renderer/atlas/BackendD3D12.cpp new file mode 100644 index 000000000..95c1cd935 --- /dev/null +++ b/src/renderer/atlas/BackendD3D12.cpp @@ -0,0 +1,1299 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "BackendD3D12.h" + +#if TIL_FEATURE_CONHOSTATLASENGINECUSTOMSHADERS_ENABLED +#include +#include +#endif + +#include +#include + +#include "dwrite_helpers.h" +#include "wic.h" +#include "../../types/inc/convert.hpp" + +TIL_FAST_MATH_BEGIN + +#pragma warning(disable : 4100) // '...': unreferenced formal parameter +#pragma warning(disable : 26440) // Function '...' can be declared 'noexcept'(f.6). +#pragma warning(disable : 4242) // '=': conversion from '...' to '...', possible loss of data +#pragma warning(disable : 4244) // 'initializing': conversion from '...' to '...', possible loss of data +#pragma warning(disable : 4267) // 'argument': conversion from '...' to '...', possible loss of data +#pragma warning(disable : 4838) // conversion from '...' to '...' requires a narrowing conversion +#pragma warning(disable : 26472) // Don't use a static_cast for arithmetic conversions. Use brace initialization, gsl::narrow_cast or gsl::narrow (type.1). +#pragma warning(disable : 26429) // Symbol 'data' is never tested for nullness, it can be marked as not_null (f.23). +#pragma warning(disable : 26446) // Prefer to use gsl::at() instead of unchecked subscript operator (bounds.4). +#pragma warning(disable : 26459) // You called an STL function '...' with a raw pointer parameter at position '...' that may be unsafe [...]. +#pragma warning(disable : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1). +#pragma warning(disable : 26482) // Only index into arrays using constant expressions (bounds.2). +#pragma warning(disable : 26485) // Expression '...': No array to pointer decay (bounds.3). +#pragma warning(disable : 26490) // Don't use reinterpret_cast (type.1). + +using namespace Microsoft::Console::Render::Atlas; + +static_assert(sizeof(BackendD3D::VSConstBuffer) == 4 * 4); +static_assert(sizeof(BackendD3D::PSConstBuffer) == 20 * 4); +static_assert(sizeof(BackendD3D12::CustomConstBuffer12) == 96); +static_assert(offsetof(BackendD3D12::CustomConstBuffer12, background) == offsetof(BackendD3D::CustomConstBuffer, background)); + +static constexpr D2D1_COLOR_F whiteColor{ 1, 1, 1, 1 }; +// Intermediate passes of multi-pass custom shaders render into half-float targets. +static constexpr DXGI_FORMAT customIntermediateFormat = DXGI_FORMAT_R16G16B16A16_FLOAT; +static constexpr u64 customConstantsStride = (sizeof(BackendD3D12::CustomConstBuffer12) + D3D12_CONSTANT_BUFFER_DATA_PLACEMENT_ALIGNMENT - 1) & ~u64{ D3D12_CONSTANT_BUFFER_DATA_PLACEMENT_ALIGNMENT - 1 }; +static constexpr u64 initialRingSize = 1024 * 1024; + +static u64 queryPerfFreq() noexcept +{ + LARGE_INTEGER li; + QueryPerformanceFrequency(&li); + return std::bit_cast(li.QuadPart); +} + +static u64 queryPerfCount() noexcept +{ + LARGE_INTEGER li; + QueryPerformanceCounter(&li); + return std::bit_cast(li.QuadPart); +} + +static D3D12_GRAPHICS_PIPELINE_STATE_DESC defaultPipelineDesc() noexcept +{ + D3D12_GRAPHICS_PIPELINE_STATE_DESC desc{}; + for (auto& rt : desc.BlendState.RenderTarget) + { + rt = { + .BlendEnable = FALSE, + .LogicOpEnable = FALSE, + .SrcBlend = D3D12_BLEND_ONE, + .DestBlend = D3D12_BLEND_ZERO, + .BlendOp = D3D12_BLEND_OP_ADD, + .SrcBlendAlpha = D3D12_BLEND_ONE, + .DestBlendAlpha = D3D12_BLEND_ZERO, + .BlendOpAlpha = D3D12_BLEND_OP_ADD, + .LogicOp = D3D12_LOGIC_OP_NOOP, + .RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL, + }; + } + desc.SampleMask = UINT_MAX; + desc.RasterizerState = { + .FillMode = D3D12_FILL_MODE_SOLID, + .CullMode = D3D12_CULL_MODE_NONE, + .DepthClipEnable = TRUE, + }; + static constexpr D3D12_DEPTH_STENCILOP_DESC stencilOp{ + .StencilFailOp = D3D12_STENCIL_OP_KEEP, + .StencilDepthFailOp = D3D12_STENCIL_OP_KEEP, + .StencilPassOp = D3D12_STENCIL_OP_KEEP, + .StencilFunc = D3D12_COMPARISON_FUNC_ALWAYS, + }; + desc.DepthStencilState = { + .DepthEnable = FALSE, + .DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ZERO, + .DepthFunc = D3D12_COMPARISON_FUNC_ALWAYS, + .StencilEnable = FALSE, + .StencilReadMask = D3D12_DEFAULT_STENCIL_READ_MASK, + .StencilWriteMask = D3D12_DEFAULT_STENCIL_WRITE_MASK, + .FrontFace = stencilOp, + .BackFace = stencilOp, + }; + desc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE; + desc.NumRenderTargets = 1; + desc.RTVFormats[0] = DXGI_FORMAT_B8G8R8A8_UNORM; + desc.SampleDesc = { 1, 0 }; + return desc; +} + +static wil::com_ptr createUploadBuffer(ID3D12Device* device, u64 size) +{ + static constexpr D3D12_HEAP_PROPERTIES heap{ .Type = D3D12_HEAP_TYPE_UPLOAD }; + const D3D12_RESOURCE_DESC desc{ + .Dimension = D3D12_RESOURCE_DIMENSION_BUFFER, + .Width = size, + .Height = 1, + .DepthOrArraySize = 1, + .MipLevels = 1, + .Format = DXGI_FORMAT_UNKNOWN, + .SampleDesc = { 1, 0 }, + .Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR, + }; + wil::com_ptr buffer; + THROW_IF_FAILED(device->CreateCommittedResource(&heap, D3D12_HEAP_FLAG_NONE, &desc, D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(buffer.addressof()))); + return buffer; +} + +void BackendD3D12::CreateDevice(RenderingPayload& p) +{ + // Loaded dynamically, so that the consumers of the atlas library don't need to link d3d12.lib. + static const auto d3d12 = LoadLibraryExW(L"d3d12.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32); + THROW_LAST_ERROR_IF_NULL(d3d12); + const auto d3d12CreateDevice = GetProcAddressByFunctionDeclaration(d3d12, D3D12CreateDevice); + THROW_LAST_ERROR_IF_NULL(d3d12CreateDevice); + +#ifndef NDEBUG + if (IsDebuggerPresent()) + { + if (const auto d3d12GetDebugInterface = GetProcAddressByFunctionDeclaration(d3d12, D3D12GetDebugInterface)) + { + if (wil::com_ptr debug; SUCCEEDED(d3d12GetDebugInterface(IID_PPV_ARGS(debug.addressof())))) + { + debug->EnableDebugLayer(); + } + } + } +#endif + + wil::com_ptr device; + THROW_IF_FAILED(d3d12CreateDevice(p.dxgi.adapter.get(), D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(device.addressof()))); + + static constexpr D3D12_COMMAND_QUEUE_DESC queueDesc{ .Type = D3D12_COMMAND_LIST_TYPE_DIRECT }; + wil::com_ptr queue; + THROW_IF_FAILED(device->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(queue.addressof()))); + + p.d3d12.device = std::move(device); + p.d3d12.queue = std::move(queue); +} + +BackendD3D12::BackendD3D12(const RenderingPayload& p) : + _device{ p.d3d12.device }, + _queue{ p.d3d12.queue } +{ + THROW_HR_IF_NULL(E_UNEXPECTED, _device); + THROW_HR_IF_NULL(E_UNEXPECTED, _queue); + + THROW_IF_FAILED(_device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(_fence.addressof()))); + _fenceEvent.create(); + + for (auto& frame : _frames) + { + THROW_IF_FAILED(_device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(frame.allocator.addressof()))); + } + THROW_IF_FAILED(_device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(_immediateAllocator.addressof()))); + THROW_IF_FAILED(_device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, _frames[0].allocator.get(), nullptr, IID_PPV_ARGS(_commandList.addressof()))); + THROW_IF_FAILED(_commandList->Close()); + + { + static constexpr D3D12_DESCRIPTOR_HEAP_DESC desc{ + .Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV, + .NumDescriptors = SrvCount, + .Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE, + }; + THROW_IF_FAILED(_device->CreateDescriptorHeap(&desc, IID_PPV_ARGS(_srvHeap.addressof()))); + _srvDescriptorSize = _device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV); + } + { + static constexpr D3D12_DESCRIPTOR_HEAP_DESC desc{ + .Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV, + .NumDescriptors = RtvCount, + }; + THROW_IF_FAILED(_device->CreateDescriptorHeap(&desc, IID_PPV_ARGS(_rtvHeap.addressof()))); + _rtvDescriptorSize = _device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV); + } + for (u32 i = 0; i < SrvCount; ++i) + { + _writeSrv(nullptr, DXGI_FORMAT_B8G8R8A8_UNORM, i); + } + + // The root signatures are embedded in the vertex shaders (see shader_d3d12_rootsig.hlsl). + THROW_IF_FAILED(_device->CreateRootSignature(0, &shader_vs_d3d12[0], sizeof(shader_vs_d3d12), IID_PPV_ARGS(_rootSignature.addressof()))); +#if TIL_FEATURE_CONHOSTATLASENGINECUSTOMSHADERS_ENABLED + THROW_IF_FAILED(_device->CreateRootSignature(0, &custom_shader_vs_d3d12[0], sizeof(custom_shader_vs_d3d12), IID_PPV_ARGS(_customRootSignature.addressof()))); +#endif + + { + static constexpr D3D12_INPUT_ELEMENT_DESC layout[]{ + { "SV_Position", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }, + { "shadingType", 0, DXGI_FORMAT_R16_UINT, 1, offsetof(QuadInstance, shadingType), D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1 }, + { "renditionScale", 0, DXGI_FORMAT_R8G8_UINT, 1, offsetof(QuadInstance, renditionScale), D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1 }, + { "position", 0, DXGI_FORMAT_R16G16_SINT, 1, offsetof(QuadInstance, position), D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1 }, + { "size", 0, DXGI_FORMAT_R16G16_UINT, 1, offsetof(QuadInstance, size), D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1 }, + { "texcoord", 0, DXGI_FORMAT_R16G16_UINT, 1, offsetof(QuadInstance, texcoord), D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1 }, + { "color", 0, DXGI_FORMAT_R8G8B8A8_UNORM, 1, offsetof(QuadInstance, color), D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1 }, + }; + + auto desc = defaultPipelineDesc(); + desc.pRootSignature = _rootSignature.get(); + desc.VS = { &shader_vs_d3d12[0], sizeof(shader_vs_d3d12) }; + desc.PS = { &shader_ps_d3d12[0], sizeof(shader_ps_d3d12) }; + // Same blending as BackendD3D. See the explanation in its constructor. + desc.BlendState.RenderTarget[0] = { + .BlendEnable = TRUE, + .LogicOpEnable = FALSE, + .SrcBlend = D3D12_BLEND_ONE, + .DestBlend = D3D12_BLEND_INV_SRC1_COLOR, + .BlendOp = D3D12_BLEND_OP_ADD, + .SrcBlendAlpha = D3D12_BLEND_ONE, + .DestBlendAlpha = D3D12_BLEND_INV_SRC1_ALPHA, + .BlendOpAlpha = D3D12_BLEND_OP_ADD, + .LogicOp = D3D12_LOGIC_OP_NOOP, + .RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL, + }; + desc.InputLayout = { &layout[0], static_cast(std::size(layout)) }; + THROW_IF_FAILED(_device->CreateGraphicsPipelineState(&desc, IID_PPV_ARGS(_pipelineState.addressof()))); + } + + { + static constexpr f32x2 vertices[]{ + { 0, 0 }, + { 1, 0 }, + { 1, 1 }, + { 0, 1 }, + }; + static constexpr u16 indices[]{ 0, 1, 2, 2, 3, 0 }; + + _staticBuffer = createUploadBuffer(_device.get(), sizeof(vertices) + sizeof(indices)); + + void* data = nullptr; + static constexpr D3D12_RANGE noRead{ 0, 0 }; + THROW_IF_FAILED(_staticBuffer->Map(0, &noRead, &data)); + memcpy(data, &vertices[0], sizeof(vertices)); + memcpy(static_cast(data) + sizeof(vertices), &indices[0], sizeof(indices)); + _staticBuffer->Unmap(0, nullptr); + + const auto gpu = _staticBuffer->GetGPUVirtualAddress(); + _vertexBufferView = { gpu, sizeof(vertices), sizeof(f32x2) }; + _indexBufferView = { gpu + sizeof(vertices), sizeof(indices), DXGI_FORMAT_R16_UINT }; + } + + // Direct2D can't target D3D12 resources. Our glyph rasterizer (and only that) goes through + // a D3D11On12 device, which submits its work to our own queue in the right order. + { + static const auto d3d11 = LoadLibraryExW(L"d3d11.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32); + THROW_LAST_ERROR_IF_NULL(d3d11); + const auto d3d11On12CreateDevice = GetProcAddressByFunctionDeclaration(d3d11, D3D11On12CreateDevice); + THROW_LAST_ERROR_IF_NULL(d3d11On12CreateDevice); + + IUnknown* queues[]{ _queue.get() }; + THROW_IF_FAILED(d3d11On12CreateDevice( + /* pDevice */ _device.get(), + /* Flags */ D3D11_CREATE_DEVICE_BGRA_SUPPORT | D3D11_CREATE_DEVICE_SINGLETHREADED, + /* pFeatureLevels */ nullptr, + /* FeatureLevels */ 0, + /* ppCommandQueues */ &queues[0], + /* NumQueues */ 1, + /* NodeMask */ 0, + /* ppDevice */ _d3d11Device.addressof(), + /* ppImmediateContext */ _d3d11Context.addressof(), + /* pChosenFeatureLevel */ nullptr)); + _d3d11On12 = _d3d11Device.query(); + } + + _growRing(initialRingSize); +} + +#pragma warning(suppress : 26432) // If you define or delete any default operation in the type '...', define or delete them all (c.21). +BackendD3D12::~BackendD3D12() +{ + if (_d2dBeganDrawing) + { +#pragma warning(suppress : 26447) // The function is declared 'noexcept' but calls function '...' which may throw exceptions (f.6). + LOG_IF_FAILED(_d2dRenderTarget->EndDraw()); + _d2dBeganDrawing = false; + if (_glyphAtlasWrapped) + { + ID3D11Resource* resources[]{ _glyphAtlasWrapped.get() }; + _d3d11On12->ReleaseWrappedResources(&resources[0], 1); + } + } + + // The D2D objects live in BackendD3D, which is destroyed after us. They reference the wrapped + // glyph atlas and thus the D3D11On12 device, so they have to go first. + _d2dRenderTarget.reset(); + _d2dRenderTarget4.reset(); + _brush.reset(); + _emojiBrush.reset(); + _softFontBitmap.reset(); + _glyphAtlasWrapped.reset(); + if (_d3d11Context) + { + _d3d11Context->Flush(); + } + + _waitForGpuIdleNoThrow(); +} + +void BackendD3D12::ReleaseResources() noexcept +{ + _waitForGpuIdleNoThrow(); + for (auto& buffer : _backBuffers) + { + buffer.reset(); + } + _backBufferCount = 0; + _swapChain3.reset(); + _releaseCustomTargets(); + // Ensure _handleSettingsUpdate12() is called so that the back buffers get recreated. + _generation = {}; +} + +void BackendD3D12::Render(RenderingPayload& p) +{ + // An exception may have interrupted the previous frame. + if (_commandListOpen) + { + LOG_IF_FAILED(_commandList->Close()); + _commandListOpen = false; + } + + if (_generation != p.s.generation()) + { + _handleSettingsUpdate12(p); + } + + _beginFrame(); + _drawBackground(p); + _drawCursorBackground(p); + _drawText(p); + _debugShowDirty(p); + _flushQuads(p); + + if (!_customPasses.empty()) + { + _executeCustomShader12(p); + } + + _endFrame(); +} + +void BackendD3D12::_handleSettingsUpdate12(const RenderingPayload& p) +{ + // Most of the resources below are referenced by in-flight frames. + _waitForGpuIdle(); + + if (!_backBufferCount) + { + _createBackBuffers(p); + } + + const auto fontChanged = _fontGeneration != p.s->font.generation(); + const auto miscChanged = _miscGeneration != p.s->misc.generation(); + const auto cellCountChanged = _viewportCellCount != p.s->viewportCellCount; + + if (fontChanged) + { + _updateFontDependents(p); + } + if (miscChanged) + { + _recreateCustomShader12(p); + } + if (cellCountChanged || !_backgroundBitmap12) + { + _recreateBackgroundBitmap12(p); + } + if (!_customPasses.empty() && !_customOffscreen) + { + _recreateCustomTargets(p); + } + + _recreateConstants(p); + + _generation = p.s.generation(); + _fontGeneration = p.s->font.generation(); + _miscGeneration = p.s->misc.generation(); + _targetSize = p.s->targetSize; + _viewportCellCount = p.s->viewportCellCount; +} + +void BackendD3D12::_createBackBuffers(const RenderingPayload& p) +{ + _swapChain3 = p.swapChain.swapChain.query(); + + DXGI_SWAP_CHAIN_DESC1 desc{}; + THROW_IF_FAILED(_swapChain3->GetDesc1(&desc)); + _backBufferCount = std::min(desc.BufferCount, MaxBackBuffers); + + for (u32 i = 0; i < _backBufferCount; ++i) + { + THROW_IF_FAILED(_swapChain3->GetBuffer(i, IID_PPV_ARGS(_backBuffers[i].put()))); + _device->CreateRenderTargetView(_backBuffers[i].get(), nullptr, _rtvCpu(RtvBackBufferFirst + i)); + } +} + +void BackendD3D12::_recreateBackgroundBitmap12(const RenderingPayload& p) +{ + // Avoid memory usage spikes by releasing memory first. + _backgroundBitmap12.reset(); + + const auto width = std::max(1, p.s->viewportCellCount.x); + const auto height = std::max(1, p.s->viewportCellCount.y); + _backgroundBitmap12 = _createTexture(width, height, DXGI_FORMAT_R8G8B8A8_UNORM, D3D12_RESOURCE_FLAG_NONE, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE); + _writeSrv(_backgroundBitmap12.get(), DXGI_FORMAT_R8G8B8A8_UNORM, SrvBackground); + _backgroundBitmapGeneration = {}; +} + +void BackendD3D12::_recreateConstants(const RenderingPayload& p) +{ + _vsConstants = {}; + _vsConstants.positionScale = { 2.0f / p.s->targetSize.x, -2.0f / p.s->targetSize.y }; + + _psConstants = {}; + _psConstants.backgroundColor = colorFromU32Premultiply(p.s->misc->backgroundColor); + _psConstants.backgroundCellSize = { static_cast(p.s->font->cellSize.x), static_cast(p.s->font->cellSize.y) }; + _psConstants.backgroundCellCount = { static_cast(p.s->viewportCellCount.x), static_cast(p.s->viewportCellCount.y) }; + DWrite_GetGammaRatios(_gamma, _psConstants.gammaRatios); + _psConstants.enhancedContrast = p.s->font->antialiasingMode == AntialiasingMode::ClearType ? _cleartypeEnhancedContrast : _grayscaleEnhancedContrast; + _psConstants.underlineWidth = p.s->font->underline.height; + _psConstants.doubleUnderlineWidth = p.s->font->doubleUnderline[0].height; + _psConstants.curlyLineHalfHeight = _curlyLineHalfHeight; + _psConstants.shadedGlyphDotSize = std::max(1.0f, std::roundf(std::max(p.s->font->cellSize.x / 12.0f, p.s->font->cellSize.y / 24.0f))); +} + +void BackendD3D12::_recreateCustomShader12(const RenderingPayload& p) +{ + UNREFERENCED_PARAMETER(p); + +#if TIL_FEATURE_CONHOSTATLASENGINECUSTOMSHADERS_ENABLED + _releaseCustomTargets(); + _customPasses.clear(); + _customImage.reset(); + _customUsesHistory = false; + _customFrameCount = 0; + _customLastFrameTime = 0; + _requiresContinuousRedraw = false; + + const auto& shaderPath = p.s->misc->customPixelShaderPath; + std::vector> blobs; + std::vector bytecodes; + + if (!shaderPath.empty()) + { + static constexpr auto flags = + D3DCOMPILE_PACK_MATRIX_COLUMN_MAJOR +#ifdef NDEBUG + | D3DCOMPILE_OPTIMIZATION_LEVEL3; +#else + | D3DCOMPILE_ENABLE_STRICTNESS | D3DCOMPILE_WARNINGS_ARE_ERRORS | D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION; +#endif + + const auto compile = [&](const char* entrypoint, wil::com_ptr& blob, wil::com_ptr& error) { + return D3DCompileFromFile( + /* pFileName */ shaderPath.c_str(), + /* pDefines */ nullptr, + /* pInclude */ D3D_COMPILE_STANDARD_FILE_INCLUDE, + /* pEntrypoint */ entrypoint, + /* pTarget */ "ps_5_1", + /* Flags1 */ flags, + /* Flags2 */ 0, + /* ppCode */ blob.put(), + /* ppErrorMsgs */ error.put()); + }; + const auto errorText = [](const wil::com_ptr& error) { + return error ? std::string_view{ static_cast(error->GetBufferPointer()), error->GetBufferSize() } : std::string_view{}; + }; + const auto reportError = [&](HRESULT hr, const wil::com_ptr& error) { + if (!p.warningCallback) + { + return; + } + if (error) + { + p.warningCallback(D2DERR_SHADER_COMPILE_FAILED, ConvertToW(CP_ACP, errorText(error))); + } + else + { + p.warningCallback(hr, shaderPath); + } + }; + + // Optional extra passes "pass1" to "pass7" run in that order before "main". + auto ok = true; + for (u32 i = 1; i < MaxCustomPasses; ++i) + { + char entrypoint[]{ 'p', 'a', 's', 's', static_cast('0' + i), '\0' }; + wil::com_ptr blob; + wil::com_ptr error; + const auto hr = compile(&entrypoint[0], blob, error); + if (SUCCEEDED(hr)) + { + blobs.emplace_back(std::move(blob)); + continue; + } + // X3501: entrypoint not found + if (errorText(error).find("X3501") == std::string_view::npos) + { + reportError(hr, error); + ok = false; + } + break; + } + + if (ok) + { + wil::com_ptr blob; + wil::com_ptr error; + const auto hr = compile("main", blob, error); + if (SUCCEEDED(hr)) + { + blobs.emplace_back(std::move(blob)); + } + else + { + reportError(hr, error); + ok = false; + } + } + + if (!ok) + { + blobs.clear(); + } + + for (const auto& blob : blobs) + { + bytecodes.push_back({ blob->GetBufferPointer(), blob->GetBufferSize() }); + + wil::com_ptr reflector; + D3D12_SHADER_DESC shaderDesc{}; + if (FAILED_LOG(D3DReflect(blob->GetBufferPointer(), blob->GetBufferSize(), IID_PPV_ARGS(reflector.addressof()))) || + FAILED(reflector->GetDesc(&shaderDesc))) + { + // Unless we can determine otherwise, assume this shader requires evaluation every frame. + _requiresContinuousRedraw = true; + continue; + } + + for (UINT i = 0; i < shaderDesc.BoundResources; ++i) + { + D3D12_SHADER_INPUT_BIND_DESC bind{}; + if (FAILED(reflector->GetResourceBindingDesc(i, &bind))) + { + continue; + } + + if (bind.Type == D3D_SIT_TEXTURE && bind.BindPoint <= 3 && bind.BindPoint + std::max(1u, bind.BindCount) > 3) + { + _customUsesHistory = true; + } + + if (bind.Type == D3D_SIT_CBUFFER && bind.BindPoint == 0) + { + // Like in BackendD3D, any failure here means that the cbuffer is unused. + const auto constantBuffer = reflector->GetConstantBufferByName(bind.Name); + D3D12_SHADER_BUFFER_DESC bufferDesc{}; + if (!constantBuffer || FAILED(constantBuffer->GetDesc(&bufferDesc))) + { + continue; + } + + for (UINT v = 0; v < bufferDesc.Variables; ++v) + { + const auto variable = constantBuffer->GetVariableByIndex(v); + D3D12_SHADER_VARIABLE_DESC variableDesc{}; + if (!variable || FAILED(variable->GetDesc(&variableDesc)) || WI_IsFlagClear(variableDesc.uFlags, D3D_SVF_USED)) + { + continue; + } + + // time, as well as deltaTime, frameCount and cursorTime change every frame. + const auto begin = variableDesc.StartOffset; + const auto end = begin + variableDesc.Size; + if (begin < offsetof(CustomConstBuffer12, scale) || + (begin < offsetof(CustomConstBuffer12, passIndex) && end > offsetof(CustomConstBuffer12, deltaTime))) + { + _requiresContinuousRedraw = true; + } + } + } + } + } + + // A history-based effect (trails, decay, ...) only converges if it keeps getting drawn. + if (_customUsesHistory) + { + _requiresContinuousRedraw = true; + } + + if (!bytecodes.empty() && !p.s->misc->customPixelShaderImagePath.empty()) + { + try + { + _customImage = _uploadImage(p, p.s->misc->customPixelShaderImagePath.c_str()); + } + catch (...) + { + LOG_CAUGHT_EXCEPTION(); + bytecodes.clear(); + if (p.warningCallback) + { + p.warningCallback(D2DERR_SHADER_COMPILE_FAILED, p.s->misc->customPixelShaderImagePath); + } + } + } + } + else if (p.s->misc->useRetroTerminalEffect) + { + bytecodes.push_back({ &custom_shader_ps_d3d12[0], sizeof(custom_shader_ps_d3d12) }); + } + + for (size_t i = 0; i < bytecodes.size(); ++i) + { + const auto last = i + 1 == bytecodes.size(); + auto desc = defaultPipelineDesc(); + desc.pRootSignature = _customRootSignature.get(); + desc.VS = { &custom_shader_vs_d3d12[0], sizeof(custom_shader_vs_d3d12) }; + desc.PS = bytecodes[i]; + desc.RTVFormats[0] = last ? DXGI_FORMAT_B8G8R8A8_UNORM : customIntermediateFormat; + + wil::com_ptr pipelineState; + const auto hr = _device->CreateGraphicsPipelineState(&desc, IID_PPV_ARGS(pipelineState.addressof())); + if (FAILED(hr)) + { + // Most likely the shader binds something outside of the root signature (b0, t0-t3, s0-s3). + LOG_HR(hr); + _customPasses.clear(); + _customImage.reset(); + _customUsesHistory = false; + _requiresContinuousRedraw = false; + if (p.warningCallback) + { + p.warningCallback(hr, shaderPath); + } + return; + } + _customPasses.emplace_back(std::move(pipelineState)); + } + + if (!_customPasses.empty()) + { + // See BackendD3D::_recreateCustomShader() for an explanation of the wraparound. + const auto freq = queryPerfFreq(); + _customShaderPerfTickMod = freq * 1000; + _customShaderSecsPerPerfTick = 1.0f / freq; + } +#endif +} + +void BackendD3D12::_recreateCustomTargets(const RenderingPayload& p) +{ + _releaseCustomTargets(); + + const u32 width = p.s->targetSize.x; + const u32 height = p.s->targetSize.y; + const auto passCount = gsl::narrow_cast(_customPasses.size()); + + _customOffscreen = _createTexture(width, height, DXGI_FORMAT_B8G8R8A8_UNORM, D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE); + _device->CreateRenderTargetView(_customOffscreen.get(), nullptr, _rtvCpu(RtvCustomOffscreen)); + + if (passCount > 1) + { + for (u32 i = 0; i < 2; ++i) + { + _customPingPong[i] = _createTexture(width, height, customIntermediateFormat, D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE); + _device->CreateRenderTargetView(_customPingPong[i].get(), nullptr, _rtvCpu(RtvCustomPingPong + i)); + } + } + + if (_customUsesHistory) + { + _customHistory = _createTexture(width, height, DXGI_FORMAT_B8G8R8A8_UNORM, D3D12_RESOURCE_FLAG_NONE, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE); + } + + for (u32 i = 0; i < passCount; ++i) + { + const auto base = SrvCustomFirst + 4 * i; + if (i == 0) + { + _writeSrv(_customOffscreen.get(), DXGI_FORMAT_B8G8R8A8_UNORM, base + 0); + } + else + { + _writeSrv(_customPingPong[(i - 1) % 2].get(), customIntermediateFormat, base + 0); + } + _writeSrv(_customImage.get(), DXGI_FORMAT_B8G8R8A8_UNORM, base + 1); + _writeSrv(_customOffscreen.get(), DXGI_FORMAT_B8G8R8A8_UNORM, base + 2); + _writeSrv(_customHistory.get(), DXGI_FORMAT_B8G8R8A8_UNORM, base + 3); + } +} + +void BackendD3D12::_releaseCustomTargets() noexcept +{ + _customOffscreen.reset(); + _customPingPong[0].reset(); + _customPingPong[1].reset(); + _customHistory.reset(); +} + +wil::com_ptr BackendD3D12::_uploadImage(const RenderingPayload& p, const wchar_t* path) +{ + assert(!_commandListOpen); + + const auto image = WIC::LoadImageFromFile(path); + auto texture = _createTexture(image.width, image.height, DXGI_FORMAT_B8G8R8A8_UNORM, D3D12_RESOURCE_FLAG_NONE, D3D12_RESOURCE_STATE_COPY_DEST); + + const auto rowPitch = alignForward(image.width * sizeof(u32), D3D12_TEXTURE_DATA_PITCH_ALIGNMENT); + const auto upload = createUploadBuffer(_device.get(), u64{ rowPitch } * image.height); + { + void* data = nullptr; + static constexpr D3D12_RANGE noRead{ 0, 0 }; + THROW_IF_FAILED(upload->Map(0, &noRead, &data)); + for (u32 y = 0; y < image.height; ++y) + { + memcpy(static_cast(data) + u64{ y } * rowPitch, image.pixels.data() + u64{ y } * image.stride, image.width * sizeof(u32)); + } + upload->Unmap(0, nullptr); + } + + THROW_IF_FAILED(_immediateAllocator->Reset()); + THROW_IF_FAILED(_commandList->Reset(_immediateAllocator.get(), nullptr)); + _commandListOpen = true; + { + D3D12_TEXTURE_COPY_LOCATION dst{}; + dst.pResource = texture.get(); + dst.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; + dst.SubresourceIndex = 0; + + D3D12_TEXTURE_COPY_LOCATION src{}; + src.pResource = upload.get(); + src.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT; + src.PlacedFootprint.Offset = 0; + src.PlacedFootprint.Footprint = { DXGI_FORMAT_B8G8R8A8_UNORM, image.width, image.height, 1, rowPitch }; + + _commandList->CopyTextureRegion(&dst, 0, 0, 0, &src, nullptr); + _transition(texture.get(), D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE); + } + THROW_IF_FAILED(_commandList->Close()); + _commandListOpen = false; + + ID3D12CommandList* lists[]{ _commandList.get() }; + _queue->ExecuteCommandLists(1, &lists[0]); + _waitForGpuIdle(); + return texture; +} + +void BackendD3D12::_executeCustomShader12(RenderingPayload& p) +{ +#if TIL_FEATURE_CONHOSTATLASENGINECUSTOMSHADERS_ENABLED + const auto now = queryPerfCount(); + const auto passCount = gsl::narrow_cast(_customPasses.size()); + + if (p.cursorRect != _customLastCursorRect) + { + _customLastCursorRect = p.cursorRect; + _customCursorChangeTime = now; + } + + CustomConstBuffer12 data{}; + data.time = (now % _customShaderPerfTickMod) * _customShaderSecsPerPerfTick; + data.scale = static_cast(p.s->font->dpi) / static_cast(USER_DEFAULT_SCREEN_DPI); + data.resolution = { + static_cast(_viewportCellCount.x * p.s->font->cellSize.x), + static_cast(_viewportCellCount.y * p.s->font->cellSize.y), + }; + data.background = colorFromU32Premultiply(p.s->misc->backgroundColor); + data.cursorRect = {}; + data.cursorColor = {}; + if (!p.cursorRect.empty()) + { + data.cursorRect = { + static_cast(p.s->font->cellSize.x * p.cursorRect.left), + static_cast(p.s->font->cellSize.y * p.cursorRect.top), + static_cast(p.s->font->cellSize.x * p.cursorRect.right), + static_cast(p.s->font->cellSize.y * p.cursorRect.bottom), + }; + data.cursorColor = colorFromU32(p.s->cursor->cursorColor); + } + data.targetSize = { static_cast(_targetSize.x), static_cast(_targetSize.y) }; + data.deltaTime = _customLastFrameTime ? std::min(1.0f, (now - _customLastFrameTime) * _customShaderSecsPerPerfTick) : 0.0f; + data.frameCount = _customFrameCount++; + data.cursorTime = std::min(1000.0f, (now - _customCursorChangeTime) * _customShaderSecsPerPerfTick); + data.passCount = passCount; + _customLastFrameTime = now; + + // Allocate all constants before switching to the custom root signature: _ringAlloc() may + // submit the command list and restore the state of the main pipeline. + const auto constants = _ringAlloc(customConstantsStride * passCount, D3D12_CONSTANT_BUFFER_DATA_PLACEMENT_ALIGNMENT); + for (u32 i = 0; i < passCount; ++i) + { + data.passIndex = i; + memcpy(constants.cpu + i * customConstantsStride, &data, sizeof(data)); + } + + _transition(_customOffscreen.get(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE); + + _commandList->SetGraphicsRootSignature(_customRootSignature.get()); + _commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP); + + for (u32 i = 0; i < passCount; ++i) + { + const auto last = i + 1 == passCount; + const auto target = last ? _backBuffers[_backBufferIndex].get() : _customPingPong[i % 2].get(); + const auto rtv = last ? _rtvCpu(RtvBackBufferFirst + _backBufferIndex) : _rtvCpu(RtvCustomPingPong + i % 2); + + if (!last) + { + _transition(target, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_RENDER_TARGET); + } + + _commandList->SetPipelineState(_customPasses[i].get()); + _commandList->SetGraphicsRootConstantBufferView(0, constants.gpu + i * customConstantsStride); + _commandList->SetGraphicsRootDescriptorTable(1, _srvGpu(SrvCustomFirst + 4 * i)); + _commandList->OMSetRenderTargets(1, &rtv, FALSE, nullptr); + _commandList->DrawInstanced(4, 1, 0, 0); + + if (!last) + { + _transition(target, D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE); + } + } + + if (_customHistory) + { + const auto backBuffer = _backBuffers[_backBufferIndex].get(); + _transition(backBuffer, D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_COPY_SOURCE); + _transition(_customHistory.get(), D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_COPY_DEST); + _commandList->CopyResource(_customHistory.get(), backBuffer); + _transition(_customHistory.get(), D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE); + _transition(backBuffer, D3D12_RESOURCE_STATE_COPY_SOURCE, D3D12_RESOURCE_STATE_RENDER_TARGET); + } + + // With custom shaders, everything might be invalidated, so we have to + // indirectly disable Present1() and its dirty rects this way. + p.dirtyRectInPx = { 0, 0, p.s->targetSize.x, p.s->targetSize.y }; +#endif +} + +void BackendD3D12::_d2dBeginDrawing() +{ + if (!_d2dBeganDrawing) + { + // Direct2D is about to write into the glyph atlas. Draw calls recorded so far read + // its current contents and must reach the queue before the D3D11On12 commands do. + if (_atlasReadsPending && _commandListOpen) + { + _submit(true); + } + + ID3D11Resource* resources[]{ _glyphAtlasWrapped.get() }; + _d3d11On12->AcquireWrappedResources(&resources[0], 1); + _d2dRenderTarget->BeginDraw(); + _d2dBeganDrawing = true; + } +} + +void BackendD3D12::_d2dEndDrawing() +{ + if (_d2dBeganDrawing) + { + _d2dBeganDrawing = false; + const auto hr = _d2dRenderTarget->EndDraw(); + ID3D11Resource* resources[]{ _glyphAtlasWrapped.get() }; + _d3d11On12->ReleaseWrappedResources(&resources[0], 1); + // Submits the Direct2D work to our queue, ahead of the command list that's currently being recorded. + _d3d11Context->Flush(); + THROW_IF_FAILED(hr); + } +} + +void BackendD3D12::_resizeGlyphAtlas(const RenderingPayload& p, const u16 u, const u16 v) +{ +#if defined(_M_X64) || defined(_M_IX86) + static const auto faultyMacTypeVersion = _checkMacTypeVersion(p); +#else + // The affected versions of MacType are unavailable on ARM. + static constexpr auto faultyMacTypeVersion = false; +#endif + + // The old atlas and its descriptor are referenced by recorded and in-flight draw calls. + if (_commandListOpen) + { + _submit(true); + } + _waitForGpuIdle(); + + _d2dRenderTarget.reset(); + _d2dRenderTarget4.reset(); + _brush.reset(); + _emojiBrush.reset(); + _softFontBitmap.reset(); + _glyphAtlasWrapped.reset(); + // Destroys the wrapped resource for real. + _d3d11Context->Flush(); + _glyphAtlas12.reset(); + + _glyphAtlas12 = _createTexture(u, v, DXGI_FORMAT_B8G8R8A8_UNORM, D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE); + _writeSrv(_glyphAtlas12.get(), DXGI_FORMAT_B8G8R8A8_UNORM, SrvGlyphAtlas); + + { + static constexpr D3D11_RESOURCE_FLAGS flags{ .BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE }; + THROW_IF_FAILED(_d3d11On12->CreateWrappedResource( + _glyphAtlas12.get(), + &flags, + D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, + D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, + IID_PPV_ARGS(_glyphAtlasWrapped.addressof()))); + } + + { + const auto surface = _glyphAtlasWrapped.query(); + + static constexpr D2D1_RENDER_TARGET_PROPERTIES props{ + .type = D2D1_RENDER_TARGET_TYPE_DEFAULT, + .pixelFormat = { DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED }, + }; + THROW_IF_FAILED(p.d2dFactory->CreateDxgiSurfaceRenderTarget(surface.get(), &props, reinterpret_cast(_d2dRenderTarget.addressof()))); + _d2dRenderTarget.try_query_to(_d2dRenderTarget4.addressof()); + + _d2dRenderTarget->SetUnitMode(D2D1_UNIT_MODE_PIXELS); + // Ensure that D2D uses the exact same gamma as our shader uses. + _d2dRenderTarget->SetTextRenderingParams(_textRenderingParams.get()); + + _d2dRenderTargetUpdateFontSettings(p); + } + + // We have our own glyph cache so Direct2D's cache doesn't help much. + if (_d2dRenderTarget4) + { + wil::com_ptr device; + _d2dRenderTarget4->GetDevice(device.addressof()); + + device->SetMaximumTextureMemory(0); + + if (!faultyMacTypeVersion) + { + if (const auto device4 = device.try_query()) + { + device4->SetMaximumColorGlyphCacheMemory(0); + } + } + } + + { + THROW_IF_FAILED(_d2dRenderTarget->CreateSolidColorBrush(&whiteColor, nullptr, _emojiBrush.put())); + THROW_IF_FAILED(_d2dRenderTarget->CreateSolidColorBrush(&whiteColor, nullptr, _brush.put())); + } + + _rectPackerData = Buffer{ u }; +} + +void BackendD3D12::_flushQuads(const RenderingPayload& p) +{ + if (!_instancesCount) + { + return; + } + + if (!_cursorRects.empty()) + { + _drawCursorForeground(); + } + + const auto bytes = _instancesCount * sizeof(QuadInstance); + const auto instances = _ringAlloc(bytes, 16); + memcpy(instances.cpu, _instances.data(), bytes); + + const D3D12_VERTEX_BUFFER_VIEW views[]{ + _vertexBufferView, + { instances.gpu, gsl::narrow(bytes), sizeof(QuadInstance) }, + }; + _commandList->IASetVertexBuffers(0, 2, &views[0]); + _commandList->DrawIndexedInstanced(6, static_cast(_instancesCount), 0, 0, 0); + + _instancesCount = 0; + _atlasReadsPending = true; +} + +void BackendD3D12::_uploadBackgroundBitmap(const RenderingPayload& p) +{ + const u32 width = p.s->viewportCellCount.x; + const u32 height = p.s->viewportCellCount.y; + if (!width || !height) + { + _backgroundBitmapGeneration = p.colorBitmapGenerations[0]; + return; + } + + const auto rowPitch = alignForward(width * sizeof(u32), D3D12_TEXTURE_DATA_PITCH_ALIGNMENT); + const auto upload = _ringAlloc(u64{ rowPitch } * height, D3D12_TEXTURE_DATA_PLACEMENT_ALIGNMENT); + + auto src = std::bit_cast(p.backgroundBitmap.data()); + const auto srcWidth = width * sizeof(u32); + const auto srcStride = p.colorBitmapRowStride * sizeof(u32); + auto dst = std::bit_cast(upload.cpu); + + for (u32 y = 0; y < height; ++y) + { + memcpy(dst, src, srcWidth); + src += srcStride; + dst += rowPitch; + } + + D3D12_TEXTURE_COPY_LOCATION dstLocation{}; + dstLocation.pResource = _backgroundBitmap12.get(); + dstLocation.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; + dstLocation.SubresourceIndex = 0; + + D3D12_TEXTURE_COPY_LOCATION srcLocation{}; + srcLocation.pResource = _ring.get(); + srcLocation.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT; + srcLocation.PlacedFootprint.Offset = upload.offset; + srcLocation.PlacedFootprint.Footprint = { DXGI_FORMAT_R8G8B8A8_UNORM, width, height, 1, rowPitch }; + + _transition(_backgroundBitmap12.get(), D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_COPY_DEST); + _commandList->CopyTextureRegion(&dstLocation, 0, 0, 0, &srcLocation, nullptr); + _transition(_backgroundBitmap12.get(), D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE); + + _backgroundBitmapGeneration = p.colorBitmapGenerations[0]; +} + +void BackendD3D12::_beginFrame() +{ + auto& frame = _frames[_frameIndex]; + _waitForFence(frame.fenceValue); + _ringTail = std::max(_ringTail, frame.ringEnd); + + THROW_IF_FAILED(frame.allocator->Reset()); + THROW_IF_FAILED(_commandList->Reset(frame.allocator.get(), nullptr)); + _commandListOpen = true; + + _backBufferIndex = _swapChain3->GetCurrentBackBufferIndex(); + THROW_HR_IF(E_UNEXPECTED, _backBufferIndex >= _backBufferCount); + + _transition(_backBuffers[_backBufferIndex].get(), D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET); + if (_customOffscreen) + { + _transition(_customOffscreen.get(), D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_RENDER_TARGET); + } + + _setCommandListState(); +} + +void BackendD3D12::_endFrame() +{ + _transition(_backBuffers[_backBufferIndex].get(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT); + _submit(false); + _frames[_frameIndex].ringEnd = _ringHead; + _frameIndex = (_frameIndex + 1) % FrameCount; +} + +void BackendD3D12::_setCommandListState() +{ + ID3D12DescriptorHeap* heaps[]{ _srvHeap.get() }; + _commandList->SetDescriptorHeaps(1, &heaps[0]); + + _commandList->SetGraphicsRootSignature(_rootSignature.get()); + _commandList->SetPipelineState(_pipelineState.get()); + _commandList->SetGraphicsRoot32BitConstants(0, sizeof(_vsConstants) / 4, &_vsConstants, 0); + _commandList->SetGraphicsRoot32BitConstants(1, sizeof(_psConstants) / 4, &_psConstants, 0); + _commandList->SetGraphicsRootDescriptorTable(2, _srvGpu(SrvBackground)); + + _commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + _commandList->IASetIndexBuffer(&_indexBufferView); + _commandList->IASetVertexBuffers(0, 1, &_vertexBufferView); + + const D3D12_VIEWPORT viewport{ 0, 0, static_cast(_targetSize.x), static_cast(_targetSize.y), 0, 1 }; + const D3D12_RECT scissor{ 0, 0, _targetSize.x, _targetSize.y }; + _commandList->RSSetViewports(1, &viewport); + _commandList->RSSetScissorRects(1, &scissor); + + const auto rtv = _mainTargetRtv(); + _commandList->OMSetRenderTargets(1, &rtv, FALSE, nullptr); +} + +void BackendD3D12::_submit(bool reopen) +{ + THROW_IF_FAILED(_commandList->Close()); + _commandListOpen = false; + + ID3D12CommandList* lists[]{ _commandList.get() }; + _queue->ExecuteCommandLists(1, &lists[0]); + THROW_IF_FAILED(_queue->Signal(_fence.get(), ++_fenceValue)); + _frames[_frameIndex].fenceValue = _fenceValue; + _atlasReadsPending = false; + + if (reopen) + { + // An allocator can back the next recording while its previous commands are still executing. + THROW_IF_FAILED(_commandList->Reset(_frames[_frameIndex].allocator.get(), nullptr)); + _commandListOpen = true; + _setCommandListState(); + } +} + +void BackendD3D12::_waitForFence(u64 value) +{ + for (;;) + { + const auto completed = _fence->GetCompletedValue(); + // GetCompletedValue() returns UINT64_MAX once the device has been removed. + if (completed == UINT64_MAX) + { + LOG_IF_FAILED(_device->GetDeviceRemovedReason()); + THROW_HR(DXGI_ERROR_DEVICE_REMOVED); + } + if (completed >= value) + { + return; + } + + THROW_IF_FAILED(_fence->SetEventOnCompletion(value, _fenceEvent.get())); + if (WaitForSingleObject(_fenceEvent.get(), 1000) == WAIT_TIMEOUT && FAILED(_device->GetDeviceRemovedReason())) + { + THROW_HR(DXGI_ERROR_DEVICE_REMOVED); + } + } +} + +void BackendD3D12::_waitForGpuIdle() +{ + THROW_IF_FAILED(_queue->Signal(_fence.get(), ++_fenceValue)); + _waitForFence(_fenceValue); + if (!_commandListOpen) + { + _ringTail = _ringHead; + } +} + +void BackendD3D12::_waitForGpuIdleNoThrow() noexcept +{ + try + { + if (_queue && _fence) + { + _waitForGpuIdle(); + } + } + CATCH_LOG() +} + +BackendD3D12::RingAllocation BackendD3D12::_ringAlloc(u64 size, u64 alignment) +{ + for (;;) + { + auto offset = alignForward(_ringHead, alignment); + auto pos = offset % _ringSize; + // Allocations don't wrap around the end of the buffer. Skip to the start instead. + if (pos + size > _ringSize) + { + offset += _ringSize - pos; + pos = 0; + } + + if (offset + size - _ringTail <= _ringSize) + { + _ringHead = offset + size; + return { _ringData + pos, _ringGpu + pos, pos }; + } + + // Retire the oldest frame that still holds parts of the ring. + FrameContext* oldest = nullptr; + for (auto& frame : _frames) + { + if (&frame != &_frames[_frameIndex] && frame.ringEnd > _ringTail && (!oldest || frame.fenceValue < oldest->fenceValue)) + { + oldest = &frame; + } + } + + if (oldest) + { + _waitForFence(oldest->fenceValue); + _ringTail = oldest->ringEnd; + } + else + { + // The current frame alone doesn't fit. + _growRing(size); + } + } +} + +void BackendD3D12::_growRing(u64 minSize) +{ + // Everything recorded so far may reference the old buffer. + if (_commandListOpen) + { + _submit(true); + } + _waitForGpuIdle(); + + const auto newSize = alignForward(std::max({ initialRingSize, _ringSize * 2, minSize * 2 }), 64 * 1024); + + _ring.reset(); + _ringData = nullptr; + _ring = createUploadBuffer(_device.get(), newSize); + + void* data = nullptr; + static constexpr D3D12_RANGE noRead{ 0, 0 }; + THROW_IF_FAILED(_ring->Map(0, &noRead, &data)); + _ringData = static_cast(data); + _ringGpu = _ring->GetGPUVirtualAddress(); + _ringSize = newSize; + _ringHead = 0; + _ringTail = 0; + for (auto& frame : _frames) + { + frame.ringEnd = 0; + } +} + +void BackendD3D12::_transition(ID3D12Resource* resource, D3D12_RESOURCE_STATES before, D3D12_RESOURCE_STATES after) const noexcept +{ + D3D12_RESOURCE_BARRIER barrier{}; + barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; + barrier.Transition.pResource = resource; + barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; + barrier.Transition.StateBefore = before; + barrier.Transition.StateAfter = after; + _commandList->ResourceBarrier(1, &barrier); +} + +D3D12_CPU_DESCRIPTOR_HANDLE BackendD3D12::_srvCpu(u32 index) const noexcept +{ + auto handle = _srvHeap->GetCPUDescriptorHandleForHeapStart(); + handle.ptr += size_t{ index } * _srvDescriptorSize; + return handle; +} + +D3D12_GPU_DESCRIPTOR_HANDLE BackendD3D12::_srvGpu(u32 index) const noexcept +{ + auto handle = _srvHeap->GetGPUDescriptorHandleForHeapStart(); + handle.ptr += u64{ index } * _srvDescriptorSize; + return handle; +} + +D3D12_CPU_DESCRIPTOR_HANDLE BackendD3D12::_rtvCpu(u32 index) const noexcept +{ + auto handle = _rtvHeap->GetCPUDescriptorHandleForHeapStart(); + handle.ptr += size_t{ index } * _rtvDescriptorSize; + return handle; +} + +D3D12_CPU_DESCRIPTOR_HANDLE BackendD3D12::_mainTargetRtv() const noexcept +{ + return _customOffscreen ? _rtvCpu(RtvCustomOffscreen) : _rtvCpu(RtvBackBufferFirst + _backBufferIndex); +} + +wil::com_ptr BackendD3D12::_createTexture(u32 width, u32 height, DXGI_FORMAT format, D3D12_RESOURCE_FLAGS flags, D3D12_RESOURCE_STATES state) const +{ + static constexpr D3D12_HEAP_PROPERTIES heap{ .Type = D3D12_HEAP_TYPE_DEFAULT }; + const D3D12_RESOURCE_DESC desc{ + .Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D, + .Width = width, + .Height = height, + .DepthOrArraySize = 1, + .MipLevels = 1, + .Format = format, + .SampleDesc = { 1, 0 }, + .Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN, + .Flags = flags, + }; + wil::com_ptr texture; + THROW_IF_FAILED(_device->CreateCommittedResource(&heap, D3D12_HEAP_FLAG_NONE, &desc, state, nullptr, IID_PPV_ARGS(texture.addressof()))); + return texture; +} + +void BackendD3D12::_writeSrv(ID3D12Resource* resource, DXGI_FORMAT format, u32 index) const noexcept +{ + D3D12_SHADER_RESOURCE_VIEW_DESC desc{}; + desc.Format = format; + desc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D; + desc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING; + desc.Texture2D.MipLevels = 1; + _device->CreateShaderResourceView(resource, &desc, _srvCpu(index)); +} + +TIL_FAST_MATH_END diff --git a/src/renderer/atlas/BackendD3D12.h b/src/renderer/atlas/BackendD3D12.h new file mode 100644 index 000000000..cbe86b232 --- /dev/null +++ b/src/renderer/atlas/BackendD3D12.h @@ -0,0 +1,176 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include "BackendD3D.h" + +namespace Microsoft::Console::Render::Atlas +{ + // A Direct3D 12 port of BackendD3D. The CPU side (text shaping results -> quads, glyph atlas packing, + // cursor, selection, gridlines, bitmaps) is inherited unchanged. Glyphs are still rasterized by Direct2D, + // which writes into the D3D12 glyph atlas through a D3D11On12 device sharing our command queue. +#pragma warning(push) +#pragma warning(disable : 4324) // 'BackendD3D12': structure was padded due to alignment specifier + struct BackendD3D12 final : BackendD3D + { + // Creates the ID3D12Device and its direct command queue in p.d3d12. Throws if Direct3D 12 is unavailable. + static void CreateDevice(RenderingPayload& p); + + BackendD3D12(const RenderingPayload& p); + ~BackendD3D12() override; + + void ReleaseResources() noexcept override; + void Render(RenderingPayload& payload) override; + + // The first 32 bytes are identical to BackendD3D::CustomConstBuffer, so shaders written for + // Direct3D 11 work unchanged. See README.md ("Custom shaders with Direct3D 12") for the contract. + // WARNING: The same packing rules as for VSConstBuffer apply. + struct alignas(16) CustomConstBuffer12 + { + alignas(sizeof(f32)) f32 time = 0; + alignas(sizeof(f32)) f32 scale = 0; + alignas(sizeof(f32x2)) f32x2 resolution; + alignas(sizeof(f32x4)) f32x4 background; + alignas(sizeof(f32x4)) f32x4 cursorRect; + alignas(sizeof(f32x4)) f32x4 cursorColor; + alignas(sizeof(f32x2)) f32x2 targetSize; + alignas(sizeof(f32)) f32 deltaTime = 0; + alignas(sizeof(u32)) u32 frameCount = 0; + alignas(sizeof(f32)) f32 cursorTime = 0; + alignas(sizeof(u32)) u32 passIndex = 0; + alignas(sizeof(u32)) u32 passCount = 0; +#pragma warning(suppress : 4324) // 'CustomConstBuffer12': structure was padded due to alignment specifier + }; + + private: + static constexpr u32 FrameCount = 3; + static constexpr u32 MaxBackBuffers = 8; + static constexpr u32 MaxCustomPasses = 8; + + // Shader visible CBV/SRV/UAV heap layout. + static constexpr u32 SrvBackground = 0; + static constexpr u32 SrvGlyphAtlas = 1; + static constexpr u32 SrvCustomFirst = 2; // + 4 descriptors (t0-t3) per custom shader pass + static constexpr u32 SrvCount = SrvCustomFirst + 4 * MaxCustomPasses; + + // RTV heap layout. + static constexpr u32 RtvBackBufferFirst = 0; + static constexpr u32 RtvCustomOffscreen = MaxBackBuffers; + static constexpr u32 RtvCustomPingPong = MaxBackBuffers + 1; // + 2 + static constexpr u32 RtvCount = MaxBackBuffers + 3; + + struct FrameContext + { + wil::com_ptr allocator; + u64 fenceValue = 0; + u64 ringEnd = 0; + }; + + struct RingAllocation + { + u8* cpu = nullptr; + D3D12_GPU_VIRTUAL_ADDRESS gpu = 0; + u64 offset = 0; // relative to the start of _ring + }; + + void _d2dBeginDrawing() override; + void _d2dEndDrawing() override; + void _resizeGlyphAtlas(const RenderingPayload& p, u16 u, u16 v) override; + void _flushQuads(const RenderingPayload& p) override; + void _uploadBackgroundBitmap(const RenderingPayload& p) override; + + ATLAS_ATTR_COLD void _handleSettingsUpdate12(const RenderingPayload& p); + ATLAS_ATTR_COLD void _createBackBuffers(const RenderingPayload& p); + ATLAS_ATTR_COLD void _recreateBackgroundBitmap12(const RenderingPayload& p); + ATLAS_ATTR_COLD void _recreateConstants(const RenderingPayload& p); + ATLAS_ATTR_COLD void _recreateCustomShader12(const RenderingPayload& p); + ATLAS_ATTR_COLD void _recreateCustomTargets(const RenderingPayload& p); + ATLAS_ATTR_COLD void _releaseCustomTargets() noexcept; + ATLAS_ATTR_COLD wil::com_ptr _uploadImage(const RenderingPayload& p, const wchar_t* path); + void _executeCustomShader12(RenderingPayload& p); + + void _beginFrame(); + void _endFrame(); + void _setCommandListState(); + void _submit(bool reopen); + void _waitForFence(u64 value); + void _waitForGpuIdle(); + void _waitForGpuIdleNoThrow() noexcept; + RingAllocation _ringAlloc(u64 size, u64 alignment); + ATLAS_ATTR_COLD void _growRing(u64 minSize); + void _transition(ID3D12Resource* resource, D3D12_RESOURCE_STATES before, D3D12_RESOURCE_STATES after) const noexcept; + D3D12_CPU_DESCRIPTOR_HANDLE _srvCpu(u32 index) const noexcept; + D3D12_GPU_DESCRIPTOR_HANDLE _srvGpu(u32 index) const noexcept; + D3D12_CPU_DESCRIPTOR_HANDLE _rtvCpu(u32 index) const noexcept; + D3D12_CPU_DESCRIPTOR_HANDLE _mainTargetRtv() const noexcept; + ATLAS_ATTR_COLD wil::com_ptr _createTexture(u32 width, u32 height, DXGI_FORMAT format, D3D12_RESOURCE_FLAGS flags, D3D12_RESOURCE_STATES state) const; + ATLAS_ATTR_COLD void _writeSrv(ID3D12Resource* resource, DXGI_FORMAT format, u32 index) const noexcept; + + // Device + wil::com_ptr _device; + wil::com_ptr _queue; + wil::com_ptr _commandList; + wil::com_ptr _immediateAllocator; + wil::com_ptr _fence; + wil::unique_event _fenceEvent; + u64 _fenceValue = 0; + FrameContext _frames[FrameCount]; + u32 _frameIndex = 0; + bool _commandListOpen = false; + bool _atlasReadsPending = false; + + // Direct2D interop + wil::com_ptr _d3d11Device; + wil::com_ptr _d3d11Context; + wil::com_ptr _d3d11On12; + wil::com_ptr _glyphAtlasWrapped; + + // Descriptors + wil::com_ptr _srvHeap; + wil::com_ptr _rtvHeap; + u32 _srvDescriptorSize = 0; + u32 _rtvDescriptorSize = 0; + + // Main pipeline + wil::com_ptr _rootSignature; + wil::com_ptr _pipelineState; + wil::com_ptr _staticBuffer; // vertices + indices + D3D12_VERTEX_BUFFER_VIEW _vertexBufferView{}; + D3D12_INDEX_BUFFER_VIEW _indexBufferView{}; + VSConstBuffer _vsConstants{}; + PSConstBuffer _psConstants{}; + + // Upload ring buffer (persistently mapped) + wil::com_ptr _ring; + u8* _ringData = nullptr; + D3D12_GPU_VIRTUAL_ADDRESS _ringGpu = 0; + u64 _ringSize = 0; + u64 _ringHead = 0; // monotonic + u64 _ringTail = 0; // monotonic + + // Swap chain + wil::com_ptr _swapChain3; + wil::com_ptr _backBuffers[MaxBackBuffers]; + u32 _backBufferCount = 0; + u32 _backBufferIndex = 0; + + // Textures + wil::com_ptr _glyphAtlas12; + wil::com_ptr _backgroundBitmap12; + + // Custom shaders + wil::com_ptr _customRootSignature; + std::vector> _customPasses; + wil::com_ptr _customOffscreen; + wil::com_ptr _customPingPong[2]; + wil::com_ptr _customHistory; + wil::com_ptr _customImage; + bool _customUsesHistory = false; + u64 _customLastFrameTime = 0; + u64 _customCursorChangeTime = 0; + til::rect _customLastCursorRect; + u32 _customFrameCount = 0; + }; +#pragma warning(pop) +} diff --git a/src/renderer/atlas/README.md b/src/renderer/atlas/README.md index f15f1195b..3634f9896 100644 --- a/src/renderer/atlas/README.md +++ b/src/renderer/atlas/README.md @@ -113,3 +113,73 @@ graph TD graph TD Render --> _executeCustomShader ``` + +## BackendD3D12 + +`"rendering.graphicsAPI": "direct3d12"` selects `BackendD3D12`. `automatic` never selects it: it isn't faster than `BackendD3D` on the hardware it was measured on (see `tools/aphrody/bench-render.md`). + +```mermaid +graph TD + AtlasEngine.r.cpp -->|CreateDevice, falls back to D3D11 on failure| BackendD3D12.cpp + BackendD3D12.cpp -.->|inherits the CPU side:
quads, glyph atlas packing, cursor,
selection, gridlines, bitmaps| BackendD3D.cpp + BackendD3D12.cpp -->|D3D11On12, glyph atlas only| Direct2D +``` + +* `BackendD3D12` derives from `BackendD3D` and overrides the parts that touch the GPU: `_flushQuads`, `_uploadBackgroundBitmap`, `_resizeGlyphAtlas` and `_d2dBeginDrawing`/`_d2dEndDrawing`. Everything that turns rows into quads is shared, so both backends render identically. +* `d3d12.dll` is loaded at runtime. If device creation fails, `AtlasEngine` logs the error and uses Direct3D 11. +* The swap chain is created on the direct command queue. It keeps the flip model, the frame latency waitable object and the `Present1` dirty rects of `AtlasEngine::_present`. +* 3 frames in flight, a single fence. Instances, the background bitmap and the custom shader constants are written into a persistently mapped upload ring buffer, which grows when a single frame doesn't fit. +* Root signatures are written in HLSL (`shader_d3d12_rootsig.hlsl`) and embedded in the `*_d3d12.hlsl` vertex shaders, which wrap the regular shaders and are compiled for shader model 5.1. The pipeline states are created once. +* Direct2D can't draw into D3D12 resources, so the glyph atlas is a D3D12 texture wrapped by a D3D11On12 device that submits to the same queue. Draw calls recorded before Direct2D writes to the atlas again are submitted first, because they read its previous contents. + +### Custom shaders with Direct3D 12 + +Shaders written for Direct3D 11 work unchanged. They're compiled for `ps_5_1`, and the following is available on top: + +* **Multiple passes.** Optional entry points `pass1` to `pass7` run in that order before `main`. Intermediate passes render into `R16G16B16A16_FLOAT` targets (the size of the swap chain), `main` renders into the swap chain. +* **Textures.** + * `t0`: output of the previous pass. For the first pass that's the terminal contents (same as with Direct3D 11). + * `t1`: `experimental.pixelShaderImagePath` (same as with Direct3D 11). + * `t2`: the terminal contents, in every pass. + * `t3`: the final output of the previous frame, for trails or decay effects. Using it turns on continuous redraw. +* **Samplers.** + * `s0`: linear, border, transparent black (same as with Direct3D 11). + * `s1`: point, clamp. + * `s2`: linear, clamp. + * `s3`: linear, wrap. +* **Constant buffer `b0`.** The first 32 bytes are the Direct3D 11 layout. + + | Offset | Type | Name | Meaning | + |-------:|----------|---------------|--------------------------------------------------------------| + | 0 | `float` | `time` | seconds, wraps around after 1000 s | + | 4 | `float` | `scale` | DPI / 96 | + | 8 | `float2` | `resolution` | size of the text area in pixels | + | 16 | `float4` | `background` | premultiplied background color | + | 32 | `float4` | `cursorRect` | cursor left, top, right, bottom in pixels (0 when hidden) | + | 48 | `float4` | `cursorColor` | cursor color | + | 64 | `float2` | `targetSize` | size of the swap chain in pixels | + | 72 | `float` | `deltaTime` | seconds since the previous frame (at most 1) | + | 76 | `uint` | `frameCount` | frames drawn since the shader was loaded | + | 80 | `float` | `cursorTime` | seconds since the cursor last moved (at most 1000) | + | 84 | `uint` | `passIndex` | index of the running pass (`main` is `passCount - 1`) | + | 88 | `uint` | `passCount` | number of passes | + + ```hlsl + cbuffer PixelShaderSettings : register(b0) + { + float time; + float scale; + float2 resolution; + float4 background; + float4 cursorRect; + float4 cursorColor; + float2 targetSize; + float deltaTime; + uint frameCount; + float cursorTime; + uint passIndex; + uint passCount; + }; + ``` +* **Continuous redraw** is enabled when any pass uses `time`, `deltaTime`, `frameCount`, `cursorTime` or `t3`, or when the shader can't be reflected. Otherwise the terminal is only redrawn when its contents change. +* Binding anything outside of `b0`, `t0`-`t3` and `s0`-`s3` makes pipeline creation fail. The error is reported like a compilation error. diff --git a/src/renderer/atlas/atlas.vcxproj b/src/renderer/atlas/atlas.vcxproj index 01085f529..ffe2092d6 100644 --- a/src/renderer/atlas/atlas.vcxproj +++ b/src/renderer/atlas/atlas.vcxproj @@ -17,6 +17,7 @@ + @@ -31,6 +32,7 @@ + @@ -45,18 +47,37 @@ Vertex + + Pixel + 5.1 + + + Vertex + 5.1 + true true + + true + Pixel + + Pixel + 5.1 + Vertex + + Vertex + 5.1 + diff --git a/src/renderer/atlas/custom_shader_ps_d3d12.hlsl b/src/renderer/atlas/custom_shader_ps_d3d12.hlsl new file mode 100644 index 000000000..968ad40fd --- /dev/null +++ b/src/renderer/atlas/custom_shader_ps_d3d12.hlsl @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +// Same retro pixel shader as BackendD3D, compiled for shader model 5.1. +#include "custom_shader_ps.hlsl" diff --git a/src/renderer/atlas/custom_shader_vs_d3d12.hlsl b/src/renderer/atlas/custom_shader_vs_d3d12.hlsl new file mode 100644 index 000000000..c1f8a85a6 --- /dev/null +++ b/src/renderer/atlas/custom_shader_vs_d3d12.hlsl @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "shader_d3d12_rootsig.hlsl" + +#define main atlas_custom_vs_main +#include "custom_shader_vs.hlsl" +#undef main + +// clang-format off +[RootSignature(ATLAS_CUSTOM_ROOT_SIGNATURE)] +VS_OUTPUT main(uint id : SV_VERTEXID) +// clang-format on +{ + return atlas_custom_vs_main(id); +} diff --git a/src/renderer/atlas/makefile.inc b/src/renderer/atlas/makefile.inc index 6c935369a..eaa71f8dd 100644 --- a/src/renderer/atlas/makefile.inc +++ b/src/renderer/atlas/makefile.inc @@ -19,3 +19,21 @@ $(OBJ_PATH)\$(O)\custom_shader_ps.h: custom_shader_ps.hlsl $(OBJ_PATH)\$(O)\shader_ps.h: shader_ps.hlsl @$(ECHO) Compiling shader $(*F)... $(FXC) $(FXC_FLAGS) /Tps_$(FXC_PROFILE_VERSION) /Fh"$@" /Vn$(*B) $? + +# Direct3D 12 variants (BackendD3D12): shader model 5.1 with embedded root signatures + +$(OBJ_PATH)\$(O)\custom_shader_vs_d3d12.h: custom_shader_vs_d3d12.hlsl custom_shader_vs.hlsl shader_d3d12_rootsig.hlsl + @$(ECHO) Compiling shader $(*F)... + $(FXC) $(FXC_FLAGS) /Tvs_5_1 /Fh"$@" /Vn$(*B) custom_shader_vs_d3d12.hlsl + +$(OBJ_PATH)\$(O)\shader_vs_d3d12.h: shader_vs_d3d12.hlsl shader_vs.hlsl shader_d3d12_rootsig.hlsl + @$(ECHO) Compiling shader $(*F)... + $(FXC) $(FXC_FLAGS) /Tvs_5_1 /Fh"$@" /Vn$(*B) shader_vs_d3d12.hlsl + +$(OBJ_PATH)\$(O)\custom_shader_ps_d3d12.h: custom_shader_ps_d3d12.hlsl custom_shader_ps.hlsl + @$(ECHO) Compiling shader $(*F)... + $(FXC) $(FXC_FLAGS) /Tps_5_1 /Fh"$@" /Vn$(*B) custom_shader_ps_d3d12.hlsl + +$(OBJ_PATH)\$(O)\shader_ps_d3d12.h: shader_ps_d3d12.hlsl shader_ps.hlsl + @$(ECHO) Compiling shader $(*F)... + $(FXC) $(FXC_FLAGS) /Tps_5_1 /Fh"$@" /Vn$(*B) shader_ps_d3d12.hlsl diff --git a/src/renderer/atlas/shader_d3d12_rootsig.hlsl b/src/renderer/atlas/shader_d3d12_rootsig.hlsl new file mode 100644 index 000000000..70bd1dabd --- /dev/null +++ b/src/renderer/atlas/shader_d3d12_rootsig.hlsl @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +// Root signatures of BackendD3D12. They're embedded into the vertex shaders (shader_vs_d3d12.hlsl and +// custom_shader_vs_d3d12.hlsl) and the C++ side creates them straight from that bytecode. + +// b0 (VS) = VSConstBuffer, b0 (PS) = PSConstBuffer, t0 = background bitmap, t1 = glyph atlas. +#define ATLAS_MAIN_ROOT_SIGNATURE \ + "RootFlags(ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT | DENY_HULL_SHADER_ROOT_ACCESS | DENY_DOMAIN_SHADER_ROOT_ACCESS | DENY_GEOMETRY_SHADER_ROOT_ACCESS)," \ + "RootConstants(num32BitConstants=4, b0, visibility=SHADER_VISIBILITY_VERTEX)," \ + "RootConstants(num32BitConstants=20, b0, visibility=SHADER_VISIBILITY_PIXEL)," \ + "DescriptorTable(SRV(t0, numDescriptors=2, flags=DESCRIPTORS_VOLATILE | DATA_VOLATILE), visibility=SHADER_VISIBILITY_PIXEL)" + +// b0 = CustomConstBuffer12, t0 = previous pass (or the terminal), t1 = experimental.pixelShaderImagePath, +// t2 = the terminal, t3 = the previous frame. s0 matches the sampler of BackendD3D, s1-s3 are extras. +#define ATLAS_CUSTOM_ROOT_SIGNATURE \ + "RootFlags(DENY_HULL_SHADER_ROOT_ACCESS | DENY_DOMAIN_SHADER_ROOT_ACCESS | DENY_GEOMETRY_SHADER_ROOT_ACCESS)," \ + "CBV(b0, visibility=SHADER_VISIBILITY_PIXEL)," \ + "DescriptorTable(SRV(t0, numDescriptors=4, flags=DESCRIPTORS_VOLATILE | DATA_VOLATILE), visibility=SHADER_VISIBILITY_PIXEL)," \ + "StaticSampler(s0, filter=FILTER_MIN_MAG_MIP_LINEAR, addressU=TEXTURE_ADDRESS_BORDER, addressV=TEXTURE_ADDRESS_BORDER, addressW=TEXTURE_ADDRESS_BORDER, maxAnisotropy=1, comparisonFunc=COMPARISON_ALWAYS, borderColor=STATIC_BORDER_COLOR_TRANSPARENT_BLACK, visibility=SHADER_VISIBILITY_PIXEL)," \ + "StaticSampler(s1, filter=FILTER_MIN_MAG_MIP_POINT, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_ALWAYS, visibility=SHADER_VISIBILITY_PIXEL)," \ + "StaticSampler(s2, filter=FILTER_MIN_MAG_MIP_LINEAR, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_ALWAYS, visibility=SHADER_VISIBILITY_PIXEL)," \ + "StaticSampler(s3, filter=FILTER_MIN_MAG_MIP_LINEAR, addressU=TEXTURE_ADDRESS_WRAP, addressV=TEXTURE_ADDRESS_WRAP, addressW=TEXTURE_ADDRESS_WRAP, maxAnisotropy=1, comparisonFunc=COMPARISON_ALWAYS, visibility=SHADER_VISIBILITY_PIXEL)" diff --git a/src/renderer/atlas/shader_ps_d3d12.hlsl b/src/renderer/atlas/shader_ps_d3d12.hlsl new file mode 100644 index 000000000..858c42017 --- /dev/null +++ b/src/renderer/atlas/shader_ps_d3d12.hlsl @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +// Same pixel shader as BackendD3D, compiled for shader model 5.1. +#include "shader_ps.hlsl" diff --git a/src/renderer/atlas/shader_vs_d3d12.hlsl b/src/renderer/atlas/shader_vs_d3d12.hlsl new file mode 100644 index 000000000..e4a5a5510 --- /dev/null +++ b/src/renderer/atlas/shader_vs_d3d12.hlsl @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "shader_d3d12_rootsig.hlsl" + +#define main atlas_vs_main +#include "shader_vs.hlsl" +#undef main + +// clang-format off +[RootSignature(ATLAS_MAIN_ROOT_SIGNATURE)] +PSData main(VSData data) +// clang-format on +{ + return atlas_vs_main(data); +} diff --git a/src/renderer/atlas/sources b/src/renderer/atlas/sources index ed3edd4dc..ce72e5a55 100644 --- a/src/renderer/atlas/sources +++ b/src/renderer/atlas/sources @@ -32,6 +32,7 @@ SOURCES = \ Backend.cpp \ BackendD2D.cpp \ BackendD3D.cpp \ + BackendD3D12.cpp \ BuiltinGlyphs.cpp \ dwrite_helpers.cpp \ DWriteTextAnalysis.cpp \ @@ -44,3 +45,7 @@ NTTARGETFILE0 = \ $(OBJ_PATH)\$(O)\custom_shader_ps.h \ $(OBJ_PATH)\$(O)\shader_vs.h \ $(OBJ_PATH)\$(O)\shader_ps.h \ + $(OBJ_PATH)\$(O)\custom_shader_vs_d3d12.h \ + $(OBJ_PATH)\$(O)\custom_shader_ps_d3d12.h \ + $(OBJ_PATH)\$(O)\shader_vs_d3d12.h \ + $(OBJ_PATH)\$(O)\shader_ps_d3d12.h \ diff --git a/src/renderer/atlas/wic.cpp b/src/renderer/atlas/wic.cpp index 320c6c2e2..56cf3b7a4 100644 --- a/src/renderer/atlas/wic.cpp +++ b/src/renderer/atlas/wic.cpp @@ -66,12 +66,9 @@ void WIC::SaveTextureToPNG(ID3D11DeviceContext* deviceContext, ID3D11Resource* s THROW_IF_FAILED(encoder->Commit()); } -void WIC::LoadTextureFromFile(ID3D11Device* device, const wchar_t* fileName, ID3D11Texture2D** out_texture, ID3D11ShaderResourceView** out_textureView) +Image WIC::LoadImageFromFile(const wchar_t* fileName) { - __assume(device != nullptr); __assume(fileName != nullptr); - __assume(out_texture != nullptr); - __assume(out_textureView != nullptr); const auto wicFactory = getWicFactory(); @@ -124,12 +121,27 @@ void WIC::LoadTextureFromFile(ID3D11Device* device, const wchar_t* fileName, ID3 const uint64_t bytes = stride * static_cast(tgtHeight); THROW_HR_IF(ERROR_ARITHMETIC_OVERFLOW, bytes > UINT32_MAX); - Buffer buffer{ gsl::narrow_cast(bytes) }; - THROW_IF_FAILED(converter->CopyPixels(nullptr, gsl::narrow_cast(stride), gsl::narrow_cast(bytes), buffer.data())); + Image image{ + .pixels = Buffer{ gsl::narrow_cast(bytes) }, + .width = tgtWidth, + .height = tgtHeight, + .stride = gsl::narrow_cast(stride), + }; + THROW_IF_FAILED(converter->CopyPixels(nullptr, image.stride, gsl::narrow_cast(bytes), image.pixels.data())); + return image; +} + +void WIC::LoadTextureFromFile(ID3D11Device* device, const wchar_t* fileName, ID3D11Texture2D** out_texture, ID3D11ShaderResourceView** out_textureView) +{ + __assume(device != nullptr); + __assume(out_texture != nullptr); + __assume(out_textureView != nullptr); + + const auto image = LoadImageFromFile(fileName); const D3D11_TEXTURE2D_DESC desc = { - .Width = tgtWidth, - .Height = tgtHeight, + .Width = image.width, + .Height = image.height, .MipLevels = 1, .ArraySize = 1, .Format = DXGI_FORMAT_B8G8R8A8_UNORM, @@ -138,9 +150,9 @@ void WIC::LoadTextureFromFile(ID3D11Device* device, const wchar_t* fileName, ID3 .BindFlags = D3D11_BIND_SHADER_RESOURCE, }; const D3D11_SUBRESOURCE_DATA initData = { - .pSysMem = buffer.data(), - .SysMemPitch = gsl::narrow_cast(stride), - .SysMemSlicePitch = gsl::narrow_cast(bytes), + .pSysMem = image.pixels.data(), + .SysMemPitch = image.stride, + .SysMemSlicePitch = gsl::narrow_cast(image.pixels.size()), }; wil::com_ptr texture; THROW_IF_FAILED(device->CreateTexture2D(&desc, &initData, texture.addressof())); diff --git a/src/renderer/atlas/wic.h b/src/renderer/atlas/wic.h index 777b64cf8..79c63c1e4 100644 --- a/src/renderer/atlas/wic.h +++ b/src/renderer/atlas/wic.h @@ -3,8 +3,20 @@ #pragma once +#include "common.h" + namespace Microsoft::Console::Render::Atlas::WIC { + // 32bpp premultiplied BGRA pixels, clamped to D3D11_REQ_TEXTURE2D_U_OR_V_DIMENSION. + struct Image + { + Buffer pixels; + u32 width = 0; + u32 height = 0; + u32 stride = 0; + }; + void SaveTextureToPNG(ID3D11DeviceContext* deviceContext, ID3D11Resource* source, double dpi, const wchar_t* fileName); + Image LoadImageFromFile(const wchar_t* fileName); void LoadTextureFromFile(ID3D11Device* device, const wchar_t* fileName, ID3D11Texture2D** out_texture, ID3D11ShaderResourceView** out_textureView); }