#include "D3D12UiRenderer.h" #include #include #include #include #include #include #include #include #include #include #include #include #include namespace XCEngine::UI::Editor::Host { namespace { using ::XCEngine::RHI::BlendFactor; using ::XCEngine::RHI::BlendOp; using ::XCEngine::RHI::BufferDesc; using ::XCEngine::RHI::BufferType; using ::XCEngine::RHI::ColorWriteMask; using ::XCEngine::RHI::ComparisonFunc; using ::XCEngine::RHI::CullMode; using ::XCEngine::RHI::DescriptorHeapType; using ::XCEngine::RHI::DescriptorPoolDesc; using ::XCEngine::RHI::DescriptorSetLayoutBinding; using ::XCEngine::RHI::DescriptorSetLayoutDesc; using ::XCEngine::RHI::DescriptorType; using ::XCEngine::RHI::D3D12CommandList; using ::XCEngine::RHI::D3D12DescriptorSet; using ::XCEngine::RHI::D3D12DescriptorHeap; using ::XCEngine::RHI::FilterMode; using ::XCEngine::RHI::FillMode; using ::XCEngine::RHI::Format; using ::XCEngine::RHI::FrontFace; using ::XCEngine::RHI::GraphicsPipelineDesc; using ::XCEngine::RHI::PrimitiveTopology; using ::XCEngine::RHI::PrimitiveTopologyType; using ::XCEngine::RHI::RHICommandList; using ::XCEngine::RHI::Rect; using ::XCEngine::RHI::ResourceViewDesc; using ::XCEngine::RHI::ResourceViewDimension; using ::XCEngine::RHI::RHIPipelineLayoutDesc; using ::XCEngine::RHI::SamplerDesc; using ::XCEngine::RHI::ShaderVisibility; using ::XCEngine::RHI::TextureAddressMode; using ::XCEngine::RHI::Viewport; using ::XCEngine::Rendering::RenderContext; using ::XCEngine::Rendering::RenderSurface; using ::XCEngine::UI::UIColor; using ::XCEngine::UI::UIDrawCommand; using ::XCEngine::UI::UIDrawCommandType; using ::XCEngine::UI::UIDrawData; using ::XCEngine::UI::UIDrawList; using ::XCEngine::UI::UILinearGradientDirection; using ::XCEngine::UI::UIPoint; using ::XCEngine::UI::UIRect; using ::XCEngine::UI::UITextureHandle; constexpr std::uint64_t kMinDynamicVertexBufferBytes = 16u * 1024u; constexpr std::uint64_t kMinDynamicInstanceBufferBytes = 16u * 1024u; enum class UiPrimitiveKind : int { Textured = 0, RectFill = 1, RectOutline = 2, CircleFill = 3, CircleOutline = 4, TriangleFill = 5 }; struct UiQuadVertex { float corner[2] = {}; float uv[2] = {}; }; const UiQuadVertex kUnitQuadVertices[] = { { { 0.0f, 0.0f }, { 0.0f, 0.0f } }, { { 1.0f, 0.0f }, { 1.0f, 0.0f } }, { { 1.0f, 1.0f }, { 1.0f, 1.0f } }, { { 0.0f, 1.0f }, { 0.0f, 1.0f } } }; const std::uint32_t kUnitQuadIndices[] = { 0u, 1u, 2u, 0u, 2u, 3u }; const char kPrimitiveUiPassHlsl[] = R"( cbuffer UiConstants : register(b0) { float2 gInvViewportSize; float2 gPadding; }; struct VSInput { float2 corner : POSITION0; float2 unitUv : TEXCOORD0; float2 origin : POSITION1; float2 axisX : TEXCOORD1; float2 axisY : TEXCOORD2; float2 uvMin : TEXCOORD3; float2 uvMax : TEXCOORD4; float4 color : COLOR0; float4 secondaryColor : COLOR1; float4 params : TEXCOORD5; float4 aux0 : TEXCOORD6; float4 aux1 : TEXCOORD7; }; struct VSOutput { float4 position : SV_POSITION; float2 unitUv : TEXCOORD0; float2 localPos : TEXCOORD1; float2 size : TEXCOORD2; float2 sampleUv : TEXCOORD3; float4 color : COLOR0; float4 secondaryColor : COLOR1; float4 params : TEXCOORD4; float4 aux0 : TEXCOORD5; float4 aux1 : TEXCOORD6; }; Texture2D gTexture : register(t0); SamplerState gSampler : register(s0); float2 ToNdc(float2 pixelPos) { float2 ndc; ndc.x = pixelPos.x * gInvViewportSize.x * 2.0f - 1.0f; ndc.y = 1.0f - pixelPos.y * gInvViewportSize.y * 2.0f; return ndc; } VSOutput MainVS(VSInput input) { VSOutput output; const float2 pixelPos = input.origin + input.axisX * input.corner.x + input.axisY * input.corner.y; const float2 axisLength = float2(length(input.axisX), length(input.axisY)); output.position = float4(ToNdc(pixelPos), 0.0f, 1.0f); output.unitUv = input.corner; output.localPos = axisLength * input.corner; output.size = axisLength; output.sampleUv = lerp(input.uvMin, input.uvMax, input.corner); output.color = input.color; output.secondaryColor = input.secondaryColor; output.params = input.params; output.aux0 = input.aux0; output.aux1 = input.aux1; return output; } float CoverageFromSignedDistance(float distance) { const float aa = max(fwidth(distance), 1.0f); return saturate(0.5f - distance / aa); } float RoundedRectSignedDistance(float2 localPos, float2 size, float radius) { const float2 halfSize = max(size * 0.5f, float2(0.0f, 0.0f)); const float resolvedRadius = min(radius, min(halfSize.x, halfSize.y)); const float2 centered = localPos - halfSize; const float2 radiusVector = float2(resolvedRadius, resolvedRadius); const float2 q = abs(centered) - (halfSize - radiusVector); return length(max(q, float2(0.0f, 0.0f))) + min(max(q.x, q.y), 0.0f) - resolvedRadius; } float CircleSignedDistance(float2 localPos, float2 center, float radius) { return length(localPos - center) - radius; } float Cross2(float2 lhs, float2 rhs) { return lhs.x * rhs.y - lhs.y * rhs.x; } float TriangleSignedDistance(float2 localPos, float2 p0, float2 p1, float2 p2) { const float orientation = Cross2(p1 - p0, p2 - p0); if (abs(orientation) <= 1.0e-5f) { return 1.0e6f; } const float sign = orientation >= 0.0f ? 1.0f : -1.0f; const float2 edge0 = p1 - p0; const float2 edge1 = p2 - p1; const float2 edge2 = p0 - p2; const float distance0 = sign * Cross2(edge0, localPos - p0) / max(length(edge0), 1.0e-5f); const float distance1 = sign * Cross2(edge1, localPos - p1) / max(length(edge1), 1.0e-5f); const float distance2 = sign * Cross2(edge2, localPos - p2) / max(length(edge2), 1.0e-5f); return -min(distance0, min(distance1, distance2)); } float4 ResolveGradientColor(VSOutput input) { const bool verticalGradient = input.params.w > 0.5f; const float span = verticalGradient ? input.size.y : input.size.x; const float axisValue = verticalGradient ? input.localPos.y : input.localPos.x; const float t = span > 0.0f ? saturate(axisValue / span) : 0.0f; return lerp(input.color, input.secondaryColor, t); } float4 MainPS(VSOutput input) : SV_TARGET0 { const int quadKind = (int)round(input.params.x); if (quadKind == 0) { return gTexture.Sample(gSampler, input.sampleUv) * input.color; } float4 baseColor = ResolveGradientColor(input); float coverage = 1.0f; if (quadKind == 1) { const float distance = RoundedRectSignedDistance(input.localPos, input.size, input.params.y); coverage = CoverageFromSignedDistance(distance); } else if (quadKind == 2) { const float outerDistance = RoundedRectSignedDistance(input.localPos, input.size, input.params.y); const float innerRadius = max(0.0f, input.params.y - input.params.z); const float2 insetVector = float2(input.params.z, input.params.z); const float2 innerSize = max(input.size - insetVector * 2.0f, float2(0.0f, 0.0f)); const float2 innerLocalPos = input.localPos - insetVector; const float innerDistance = RoundedRectSignedDistance(innerLocalPos, innerSize, innerRadius); coverage = CoverageFromSignedDistance(outerDistance) * (1.0f - CoverageFromSignedDistance(innerDistance)); } else if (quadKind == 3) { const float2 center = input.size * 0.5f; const float distance = CircleSignedDistance(input.localPos, center, input.params.y); coverage = CoverageFromSignedDistance(distance); } else if (quadKind == 4) { const float2 center = input.size * 0.5f; const float distance = abs(length(input.localPos - center) - input.params.y) - input.params.z * 0.5f; coverage = CoverageFromSignedDistance(distance); } else if (quadKind == 5) { const float2 p0 = input.aux0.xy; const float2 p1 = input.aux0.zw; const float2 p2 = input.aux1.xy; const float distance = TriangleSignedDistance(input.localPos, p0, p1, p2); coverage = CoverageFromSignedDistance(distance); } return float4(baseColor.rgb, baseColor.a * coverage); } )"; struct FloatPoint { float x = 0.0f; float y = 0.0f; }; struct FloatRect { float left = 0.0f; float top = 0.0f; float right = 0.0f; float bottom = 0.0f; }; struct ClipState { Rect scissor = {}; FloatRect rect = {}; }; struct UiConstants { float invViewportSize[2] = {}; float padding[2] = {}; }; std::string HrToString(const char* operation, HRESULT hr) { char buffer[128] = {}; sprintf_s(buffer, "%s failed with hr=0x%08X.", operation, static_cast(hr)); return buffer; } float ClampDpiScale(float dpiScale) { return dpiScale > 0.0f ? dpiScale : 1.0f; } float ResolveFontSize(float fontSize) { return fontSize > 0.0f ? fontSize : 16.0f; } float SnapToPixel(float value, float dpiScale) { return std::round(value * ClampDpiScale(dpiScale)); } FloatRect ToPixelRect(const UIRect& rect, float dpiScale) { return FloatRect{ SnapToPixel(rect.x, dpiScale), SnapToPixel(rect.y, dpiScale), SnapToPixel(rect.x + rect.width, dpiScale), SnapToPixel(rect.y + rect.height, dpiScale) }; } FloatPoint ToPixelPoint(const UIPoint& point, float dpiScale) { return FloatPoint{ SnapToPixel(point.x, dpiScale), SnapToPixel(point.y, dpiScale) }; } FloatRect IntersectFloatRect(const FloatRect& lhs, const FloatRect& rhs) { return FloatRect{ (std::max)(lhs.left, rhs.left), (std::max)(lhs.top, rhs.top), (std::min)(lhs.right, rhs.right), (std::min)(lhs.bottom, rhs.bottom) }; } bool IsEmptyRect(const FloatRect& rect) { return rect.right <= rect.left || rect.bottom <= rect.top; } Rect ClampScissor(const FloatRect& rect, const Rect& renderArea) { Rect clamped = {}; clamped.left = (std::max)(renderArea.left, static_cast(std::floor(rect.left))); clamped.top = (std::max)(renderArea.top, static_cast(std::floor(rect.top))); clamped.right = (std::min)(renderArea.right, static_cast(std::ceil(rect.right))); clamped.bottom = (std::min)(renderArea.bottom, static_cast(std::ceil(rect.bottom))); if (clamped.right < clamped.left) { clamped.right = clamped.left; } if (clamped.bottom < clamped.top) { clamped.bottom = clamped.top; } return clamped; } bool IsEmptyScissor(const Rect& rect) { return rect.right <= rect.left || rect.bottom <= rect.top; } constexpr std::uint64_t kFnvOffsetBasis = 14695981039346656037ull; constexpr std::uint64_t kFnvPrime = 1099511628211ull; void HashBytes( std::uint64_t& hash, const void* data, std::size_t size) { const auto* bytes = static_cast(data); for (std::size_t index = 0u; index < size; ++index) { hash ^= static_cast(bytes[index]); hash *= kFnvPrime; } } void HashFloat(std::uint64_t& hash, float value) { std::uint32_t bits = 0u; static_assert(sizeof(bits) == sizeof(value)); std::memcpy(&bits, &value, sizeof(bits)); HashBytes(hash, &bits, sizeof(bits)); } void HashBool(std::uint64_t& hash, bool value) { const std::uint8_t encoded = value ? 1u : 0u; HashBytes(hash, &encoded, sizeof(encoded)); } void HashString(std::uint64_t& hash, std::string_view value) { const std::size_t size = value.size(); HashBytes(hash, &size, sizeof(size)); if (!value.empty()) { HashBytes(hash, value.data(), value.size()); } } void HashTextureHandle(std::uint64_t& hash, const UITextureHandle& texture) { HashBytes(hash, &texture.nativeHandle, sizeof(texture.nativeHandle)); HashBytes(hash, &texture.width, sizeof(texture.width)); HashBytes(hash, &texture.height, sizeof(texture.height)); HashBytes(hash, &texture.kind, sizeof(texture.kind)); HashBytes(hash, &texture.resourceHandle, sizeof(texture.resourceHandle)); } void HashPoint(std::uint64_t& hash, const UIPoint& point) { HashFloat(hash, point.x); HashFloat(hash, point.y); } void HashRect(std::uint64_t& hash, const UIRect& rect) { HashFloat(hash, rect.x); HashFloat(hash, rect.y); HashFloat(hash, rect.width); HashFloat(hash, rect.height); } std::uint64_t BuildDrawListContentHash(const UIDrawList& drawList) { std::uint64_t hash = kFnvOffsetBasis; HashString(hash, drawList.GetDebugName()); const std::size_t commandCount = drawList.GetCommandCount(); HashBytes(hash, &commandCount, sizeof(commandCount)); for (const UIDrawCommand& command : drawList.GetCommands()) { HashBytes(hash, &command.type, sizeof(command.type)); HashRect(hash, command.rect); HashPoint(hash, command.position); HashPoint(hash, command.uvMin); HashPoint(hash, command.uvMax); HashFloat(hash, command.color.r); HashFloat(hash, command.color.g); HashFloat(hash, command.color.b); HashFloat(hash, command.color.a); HashFloat(hash, command.secondaryColor.r); HashFloat(hash, command.secondaryColor.g); HashFloat(hash, command.secondaryColor.b); HashFloat(hash, command.secondaryColor.a); HashFloat(hash, command.thickness); HashFloat(hash, command.rounding); HashFloat(hash, command.radius); HashFloat(hash, command.fontSize); HashBool(hash, command.intersectWithCurrentClip); HashBytes(hash, &command.gradientDirection, sizeof(command.gradientDirection)); HashTextureHandle(hash, command.texture); HashString(hash, command.text); } return hash; } void WriteColor(float outColor[4], const UIColor& color) { outColor[0] = color.r; outColor[1] = color.g; outColor[2] = color.b; outColor[3] = color.a; } void AppendBatch( std::vector& batches, std::uint32_t firstInstance, std::uint32_t instanceCount, D3D12_GPU_DESCRIPTOR_HANDLE textureHandle, const Rect& scissor) { if (instanceCount == 0u || textureHandle.ptr == 0u || IsEmptyScissor(scissor)) { return; } if (!batches.empty()) { D3D12UiRenderer::UiBatch& lastBatch = batches.back(); if (lastBatch.textureHandle.ptr == textureHandle.ptr && lastBatch.scissorRect.left == scissor.left && lastBatch.scissorRect.top == scissor.top && lastBatch.scissorRect.right == scissor.right && lastBatch.scissorRect.bottom == scissor.bottom && lastBatch.firstInstance + lastBatch.instanceCount == firstInstance) { lastBatch.instanceCount += instanceCount; return; } } D3D12UiRenderer::UiBatch batch = {}; batch.firstInstance = firstInstance; batch.instanceCount = instanceCount; batch.textureHandle = textureHandle; batch.scissorRect = scissor; batches.push_back(batch); } void AppendPrimitiveInstance( std::vector& instances, std::vector& batches, const FloatPoint& origin, const FloatPoint& axisX, const FloatPoint& axisY, const UIPoint& uvMin, const UIPoint& uvMax, const UIColor& color, const UIColor& secondaryColor, UiPrimitiveKind primitiveKind, float radius, float thickness, float gradientDirection, const float aux0[4], const float aux1[4], D3D12_GPU_DESCRIPTOR_HANDLE textureHandle, const Rect& scissor) { if (textureHandle.ptr == 0u || IsEmptyScissor(scissor)) { return; } D3D12UiRenderer::UiPrimitiveInstance instance = {}; instance.origin[0] = origin.x; instance.origin[1] = origin.y; instance.axisX[0] = axisX.x; instance.axisX[1] = axisX.y; instance.axisY[0] = axisY.x; instance.axisY[1] = axisY.y; instance.uvMin[0] = uvMin.x; instance.uvMin[1] = uvMin.y; instance.uvMax[0] = uvMax.x; instance.uvMax[1] = uvMax.y; WriteColor(instance.color, color); WriteColor(instance.secondaryColor, secondaryColor); instance.params[0] = static_cast(static_cast(primitiveKind)); instance.params[1] = radius; instance.params[2] = thickness; instance.params[3] = gradientDirection; std::memcpy(instance.aux0, aux0, sizeof(instance.aux0)); std::memcpy(instance.aux1, aux1, sizeof(instance.aux1)); const std::uint32_t firstInstance = static_cast(instances.size()); instances.push_back(instance); AppendBatch(batches, firstInstance, 1u, textureHandle, scissor); } GraphicsPipelineDesc BuildBaseUiPipelineDesc( ::XCEngine::RHI::RHIPipelineLayout* pipelineLayout, const RenderSurface& surface) { GraphicsPipelineDesc pipelineDesc = {}; pipelineDesc.pipelineLayout = pipelineLayout; pipelineDesc.topologyType = static_cast(PrimitiveTopologyType::Triangle); ::XCEngine::Rendering::ApplySingleColorAttachmentPropertiesToGraphicsPipelineDesc( surface, pipelineDesc); pipelineDesc.inputLayout.elements = { { "POSITION", 0, static_cast(Format::R32G32_Float), 0, 0, 0, 0 }, { "TEXCOORD", 0, static_cast(Format::R32G32_Float), 0, 8, 0, 0 }, { "COLOR", 0, static_cast(Format::R32G32B32A32_Float), 0, 16, 0, 0 } }; pipelineDesc.rasterizerState.fillMode = static_cast(FillMode::Solid); pipelineDesc.rasterizerState.cullMode = static_cast(CullMode::None); pipelineDesc.rasterizerState.frontFace = static_cast(FrontFace::CounterClockwise); pipelineDesc.rasterizerState.depthClipEnable = true; pipelineDesc.rasterizerState.scissorTestEnable = true; pipelineDesc.blendState.blendEnable = true; pipelineDesc.blendState.srcBlend = static_cast(BlendFactor::SrcAlpha); pipelineDesc.blendState.dstBlend = static_cast(BlendFactor::InvSrcAlpha); pipelineDesc.blendState.srcBlendAlpha = static_cast(BlendFactor::One); pipelineDesc.blendState.dstBlendAlpha = static_cast(BlendFactor::InvSrcAlpha); pipelineDesc.blendState.blendOp = static_cast(BlendOp::Add); pipelineDesc.blendState.blendOpAlpha = static_cast(BlendOp::Add); pipelineDesc.blendState.colorWriteMask = static_cast(ColorWriteMask::All); pipelineDesc.depthStencilState.depthTestEnable = false; pipelineDesc.depthStencilState.depthWriteEnable = false; pipelineDesc.depthStencilState.depthFunc = static_cast(ComparisonFunc::Always); return pipelineDesc; } GraphicsPipelineDesc BuildPrimitiveUiPipelineDesc( ::XCEngine::RHI::RHIPipelineLayout* pipelineLayout, const RenderSurface& surface) { GraphicsPipelineDesc pipelineDesc = BuildBaseUiPipelineDesc(pipelineLayout, surface); pipelineDesc.inputLayout.elements = { { "POSITION", 0, static_cast(Format::R32G32_Float), 0, 0, 0, 0 }, { "TEXCOORD", 0, static_cast(Format::R32G32_Float), 0, 8, 0, 0 }, { "POSITION", 1, static_cast(Format::R32G32_Float), 1, 0, 1, 1 }, { "TEXCOORD", 1, static_cast(Format::R32G32_Float), 1, 8, 1, 1 }, { "TEXCOORD", 2, static_cast(Format::R32G32_Float), 1, 16, 1, 1 }, { "TEXCOORD", 3, static_cast(Format::R32G32_Float), 1, 24, 1, 1 }, { "TEXCOORD", 4, static_cast(Format::R32G32_Float), 1, 32, 1, 1 }, { "COLOR", 0, static_cast(Format::R32G32B32A32_Float), 1, 40, 1, 1 }, { "COLOR", 1, static_cast(Format::R32G32B32A32_Float), 1, 56, 1, 1 }, { "TEXCOORD", 5, static_cast(Format::R32G32B32A32_Float), 1, 72, 1, 1 }, { "TEXCOORD", 6, static_cast(Format::R32G32B32A32_Float), 1, 88, 1, 1 }, { "TEXCOORD", 7, static_cast(Format::R32G32B32A32_Float), 1, 104, 1, 1 } }; pipelineDesc.vertexShader.source.assign( kPrimitiveUiPassHlsl, kPrimitiveUiPassHlsl + std::strlen(kPrimitiveUiPassHlsl)); pipelineDesc.vertexShader.sourceLanguage = ::XCEngine::RHI::ShaderLanguage::HLSL; pipelineDesc.vertexShader.entryPoint = L"MainVS"; pipelineDesc.vertexShader.profile = L"vs_5_0"; pipelineDesc.fragmentShader.source.assign( kPrimitiveUiPassHlsl, kPrimitiveUiPassHlsl + std::strlen(kPrimitiveUiPassHlsl)); pipelineDesc.fragmentShader.sourceLanguage = ::XCEngine::RHI::ShaderLanguage::HLSL; pipelineDesc.fragmentShader.entryPoint = L"MainPS"; pipelineDesc.fragmentShader.profile = L"ps_5_0"; return pipelineDesc; } } // namespace bool D3D12UiRenderer::TextRunCacheKey::operator==(const TextRunCacheKey& other) const { return text == other.text && fontSizeTenths == other.fontSizeTenths && dpiScaleMilli == other.dpiScaleMilli; } std::size_t D3D12UiRenderer::TextRunCacheKeyHash::operator()( const TextRunCacheKey& key) const { const std::size_t textHash = std::hash{}(key.text); const std::size_t fontHash = std::hash{}(key.fontSizeTenths); const std::size_t dpiHash = std::hash{}(key.dpiScaleMilli); return textHash ^ (fontHash << 1u) ^ (dpiHash << 2u); } bool D3D12UiRenderer::CompiledDrawListKey::operator==( const CompiledDrawListKey& other) const { return contentHash == other.contentHash && renderWidth == other.renderWidth && renderHeight == other.renderHeight && dpiScaleMilli == other.dpiScaleMilli; } std::size_t D3D12UiRenderer::CompiledDrawListKeyHash::operator()( const CompiledDrawListKey& key) const { const std::size_t contentHashValue = std::hash{}(key.contentHash); const std::size_t widthHash = std::hash{}(key.renderWidth); const std::size_t heightHash = std::hash{}(key.renderHeight); const std::size_t dpiHash = std::hash{}(key.dpiScaleMilli); return contentHashValue ^ (widthHash << 1u) ^ (heightHash << 2u) ^ (dpiHash << 3u); } bool D3D12UiRenderer::Initialize( D3D12WindowRenderer& windowRenderer, D3D12UiTextureHost& textureHost, D3D12UiTextSystem& textSystem) { const float dpiScale = m_dpiScale; Shutdown(); m_windowRenderer = &windowRenderer; m_textureHost = &textureHost; m_textSystem = &textSystem; m_dpiScale = ClampDpiScale(dpiScale); m_lastError.clear(); return true; } void D3D12UiRenderer::Shutdown() { ReleaseTextRunCache(); ReleaseCompiledDrawListCache(); if (m_textureHost != nullptr) { m_textureHost->ReleaseTexture(m_whiteTexture); } else { m_whiteTexture = {}; } DestroyResources(); m_windowRenderer = nullptr; m_textureHost = nullptr; m_textSystem = nullptr; m_dpiScale = 1.0f; m_compiledDrawListFrameCounter = 0u; m_lastError.clear(); } void D3D12UiRenderer::SetDpiScale(float dpiScale) { const float resolvedScale = ClampDpiScale(dpiScale); if (std::abs(m_dpiScale - resolvedScale) > 0.0001f) { ReleaseTextRunCache(); ReleaseCompiledDrawListCache(); } m_dpiScale = resolvedScale; } float D3D12UiRenderer::GetDpiScale() const { return m_dpiScale; } const std::string& D3D12UiRenderer::GetLastError() const { return m_lastError; } bool D3D12UiRenderer::EnsureWhiteTexture() { if (m_whiteTexture.IsValid()) { return true; } if (m_textureHost == nullptr) { m_lastError = "EnsureWhiteTexture requires an initialized texture host."; return false; } const std::uint8_t whitePixel[4] = { 255u, 255u, 255u, 255u }; std::string error = {}; if (!m_textureHost->LoadTextureFromRgba( whitePixel, 1u, 1u, m_whiteTexture, error)) { m_lastError = error.empty() ? "Failed to create the UI white texture." : error; return false; } return true; } const D3D12UiRenderer::CachedTextRun* D3D12UiRenderer::ResolveTextRun( std::string_view text, float fontSize, std::uint64_t currentFrameId) { if (m_textureHost == nullptr || m_textSystem == nullptr) { return nullptr; } TextRunCacheKey cacheKey = {}; cacheKey.text.assign(text); cacheKey.fontSizeTenths = static_cast(std::lround(ResolveFontSize(fontSize) * 10.0f)); cacheKey.dpiScaleMilli = static_cast(std::lround(ClampDpiScale(m_dpiScale) * 1000.0f)); const auto found = m_textRunCache.find(cacheKey); if (found != m_textRunCache.end()) { found->second.lastUsedFrame = currentFrameId; return &found->second; } std::string error = {}; D3D12UiTextSystem::RasterizedTextRun rasterizedText = {}; if (!m_textSystem->RasterizeTextMask(text, fontSize, rasterizedText, error)) { m_lastError = error.empty() ? "Failed to rasterize the UI text run." : error; return nullptr; } CachedTextRun cachedText = {}; cachedText.offsetX = rasterizedText.offsetX; cachedText.offsetY = rasterizedText.offsetY; cachedText.width = static_cast(rasterizedText.width); cachedText.height = static_cast(rasterizedText.height); cachedText.lastUsedFrame = currentFrameId; cachedText.hasPixels = rasterizedText.width > 0u && rasterizedText.height > 0u && !rasterizedText.rgbaPixels.empty(); if (cachedText.hasPixels) { if (!m_textureHost->LoadTextureFromRgba( rasterizedText.rgbaPixels.data(), rasterizedText.width, rasterizedText.height, cachedText.texture, error)) { m_lastError = error.empty() ? "Failed to upload the UI text run texture." : error; return nullptr; } cachedText.textureHandle.ptr = static_cast(cachedText.texture.nativeHandle); } const auto [insertedIt, inserted] = m_textRunCache.emplace(std::move(cacheKey), std::move(cachedText)); (void)inserted; return &insertedIt->second; } void D3D12UiRenderer::ReleaseTextRunCache() { if (m_textureHost != nullptr) { for (auto& [key, cachedText] : m_textRunCache) { m_textureHost->ReleaseTexture(cachedText.texture); cachedText.textureHandle = {}; } } m_textRunCache.clear(); } void D3D12UiRenderer::PruneTextRunCache(std::uint64_t currentFrameId) { constexpr std::size_t kTextRunCacheMaxEntries = 512u; constexpr std::uint64_t kTextRunCacheRetentionFrames = 240u; if (m_textRunCache.size() <= kTextRunCacheMaxEntries) { return; } const std::uint64_t pruneBeforeFrame = currentFrameId > kTextRunCacheRetentionFrames ? currentFrameId - kTextRunCacheRetentionFrames : 0u; bool evictedAny = false; for (auto it = m_textRunCache.begin(); it != m_textRunCache.end();) { if (it->second.lastUsedFrame <= pruneBeforeFrame) { if (m_textureHost != nullptr) { m_textureHost->ReleaseTexture(it->second.texture); } it = m_textRunCache.erase(it); evictedAny = true; } else { ++it; } } if (evictedAny) { ReleaseCompiledDrawListCache(); } } void D3D12UiRenderer::ReleaseCompiledDrawListCache() { m_compiledDrawListCache.clear(); } bool D3D12UiRenderer::EnsureInitialized( const RenderContext& renderContext, const RenderSurface& surface) { const Format renderTargetFormat = ::XCEngine::Rendering::ResolveSurfaceColorFormat(surface, 0u); const std::uint32_t sampleCount = ::XCEngine::Rendering::ResolveSurfaceSampleCount(surface); const std::uint32_t sampleQuality = ::XCEngine::Rendering::ResolveSurfaceSampleQuality(surface); if (m_device == renderContext.device && m_backendType == renderContext.backendType && m_pipelineLayout != nullptr && m_primitivePipelineState != nullptr && m_constantPool != nullptr && m_constantSet != nullptr && m_samplerPool != nullptr && m_samplerSet != nullptr && m_sampler != nullptr && m_quadVertexBuffer != nullptr && m_quadVertexBufferView != nullptr && m_quadIndexBuffer != nullptr && m_quadIndexBufferView != nullptr && m_renderTargetFormat == renderTargetFormat && m_renderTargetSampleCount == sampleCount && m_renderTargetSampleQuality == sampleQuality) { return true; } DestroyResources(); return CreateResources(renderContext, surface); } bool D3D12UiRenderer::CreateResources( const RenderContext& renderContext, const RenderSurface& surface) { if (!renderContext.IsValid() || renderContext.device == nullptr || !::XCEngine::Rendering::HasSingleColorAttachment(surface) || ::XCEngine::Rendering::ResolveSurfaceColorFormat(surface, 0u) == Format::Unknown) { m_lastError = "CreateResources requires a valid single-color render surface."; return false; } m_device = renderContext.device; m_backendType = renderContext.backendType; m_renderTargetFormat = ::XCEngine::Rendering::ResolveSurfaceColorFormat(surface, 0u); m_renderTargetSampleCount = ::XCEngine::Rendering::ResolveSurfaceSampleCount(surface); m_renderTargetSampleQuality = ::XCEngine::Rendering::ResolveSurfaceSampleQuality(surface); DescriptorSetLayoutBinding constantBinding = {}; constantBinding.binding = 0u; constantBinding.type = static_cast(DescriptorType::CBV); constantBinding.count = 1u; constantBinding.visibility = static_cast(ShaderVisibility::All); DescriptorSetLayoutBinding textureBinding = {}; textureBinding.binding = 0u; textureBinding.type = static_cast(DescriptorType::SRV); textureBinding.count = 1u; textureBinding.visibility = static_cast(ShaderVisibility::All); DescriptorSetLayoutBinding samplerBinding = {}; samplerBinding.binding = 0u; samplerBinding.type = static_cast(DescriptorType::Sampler); samplerBinding.count = 1u; samplerBinding.visibility = static_cast(ShaderVisibility::All); DescriptorSetLayoutDesc setLayouts[3] = {}; setLayouts[0].bindings = &constantBinding; setLayouts[0].bindingCount = 1u; setLayouts[1].bindings = &textureBinding; setLayouts[1].bindingCount = 1u; setLayouts[2].bindings = &samplerBinding; setLayouts[2].bindingCount = 1u; RHIPipelineLayoutDesc pipelineLayoutDesc = {}; pipelineLayoutDesc.setLayouts = setLayouts; pipelineLayoutDesc.setLayoutCount = 3u; m_pipelineLayout = m_device->CreatePipelineLayout(pipelineLayoutDesc); if (m_pipelineLayout == nullptr) { m_lastError = "Failed to create the D3D12 UI pipeline layout."; DestroyResources(); return false; } DescriptorPoolDesc constantPoolDesc = {}; constantPoolDesc.type = DescriptorHeapType::CBV_SRV_UAV; constantPoolDesc.descriptorCount = 1u; constantPoolDesc.shaderVisible = false; m_constantPool = m_device->CreateDescriptorPool(constantPoolDesc); if (m_constantPool == nullptr) { m_lastError = "Failed to create the D3D12 UI constant descriptor pool."; DestroyResources(); return false; } m_constantSet = m_constantPool->AllocateSet(setLayouts[0]); if (m_constantSet == nullptr) { m_lastError = "Failed to allocate the D3D12 UI constant descriptor set."; DestroyResources(); return false; } SamplerDesc samplerDesc = {}; samplerDesc.filter = static_cast(FilterMode::Linear); samplerDesc.addressU = static_cast(TextureAddressMode::Clamp); samplerDesc.addressV = static_cast(TextureAddressMode::Clamp); samplerDesc.addressW = static_cast(TextureAddressMode::Clamp); samplerDesc.maxAnisotropy = 1u; samplerDesc.comparisonFunc = static_cast(ComparisonFunc::Always); samplerDesc.minLod = 0.0f; samplerDesc.maxLod = 16.0f; m_sampler = m_device->CreateSampler(samplerDesc); if (m_sampler == nullptr) { m_lastError = "Failed to create the D3D12 UI sampler."; DestroyResources(); return false; } DescriptorPoolDesc samplerPoolDesc = {}; samplerPoolDesc.type = DescriptorHeapType::Sampler; samplerPoolDesc.descriptorCount = 1u; samplerPoolDesc.shaderVisible = true; m_samplerPool = m_device->CreateDescriptorPool(samplerPoolDesc); if (m_samplerPool == nullptr) { m_lastError = "Failed to create the D3D12 UI sampler descriptor pool."; DestroyResources(); return false; } m_samplerSet = m_samplerPool->AllocateSet(setLayouts[2]); if (m_samplerSet == nullptr) { m_lastError = "Failed to allocate the D3D12 UI sampler descriptor set."; DestroyResources(); return false; } m_samplerSet->UpdateSampler(0u, m_sampler); auto* samplerDescriptorSet = static_cast(m_samplerSet); m_samplerGpuHandle = samplerDescriptorSet->GetGPUHandleForBinding(0u); if (m_samplerGpuHandle.ptr == 0u) { m_lastError = "Failed to resolve the D3D12 UI sampler GPU descriptor handle."; DestroyResources(); return false; } m_primitivePipelineState = m_device->CreatePipelineState( BuildPrimitiveUiPipelineDesc(m_pipelineLayout, surface)); if (m_primitivePipelineState == nullptr || !m_primitivePipelineState->IsValid()) { m_lastError = "Failed to create the D3D12 UI primitive pipeline state."; DestroyResources(); return false; } BufferDesc quadVertexBufferDesc = {}; quadVertexBufferDesc.size = sizeof(kUnitQuadVertices); quadVertexBufferDesc.stride = static_cast(sizeof(UiQuadVertex)); quadVertexBufferDesc.bufferType = static_cast(BufferType::Vertex); m_quadVertexBuffer = m_device->CreateBuffer(quadVertexBufferDesc, kUnitQuadVertices, sizeof(kUnitQuadVertices)); if (m_quadVertexBuffer == nullptr) { m_lastError = "Failed to create the D3D12 UI unit-quad vertex buffer."; DestroyResources(); return false; } m_quadVertexBuffer->SetStride(quadVertexBufferDesc.stride); m_quadVertexBuffer->SetBufferType(BufferType::Vertex); ResourceViewDesc quadVertexViewDesc = {}; quadVertexViewDesc.dimension = ResourceViewDimension::Buffer; quadVertexViewDesc.structureByteStride = quadVertexBufferDesc.stride; m_quadVertexBufferView = m_device->CreateVertexBufferView(m_quadVertexBuffer, quadVertexViewDesc); if (m_quadVertexBufferView == nullptr) { m_lastError = "Failed to create the D3D12 UI unit-quad vertex view."; DestroyResources(); return false; } BufferDesc quadIndexBufferDesc = {}; quadIndexBufferDesc.size = sizeof(kUnitQuadIndices); quadIndexBufferDesc.stride = static_cast(sizeof(std::uint32_t)); quadIndexBufferDesc.bufferType = static_cast(BufferType::Index); m_quadIndexBuffer = m_device->CreateBuffer(quadIndexBufferDesc, kUnitQuadIndices, sizeof(kUnitQuadIndices)); if (m_quadIndexBuffer == nullptr) { m_lastError = "Failed to create the D3D12 UI unit-quad index buffer."; DestroyResources(); return false; } m_quadIndexBuffer->SetStride(quadIndexBufferDesc.stride); m_quadIndexBuffer->SetBufferType(BufferType::Index); ResourceViewDesc quadIndexViewDesc = {}; quadIndexViewDesc.dimension = ResourceViewDimension::Buffer; quadIndexViewDesc.format = static_cast(Format::R32_UInt); m_quadIndexBufferView = m_device->CreateIndexBufferView(m_quadIndexBuffer, quadIndexViewDesc); if (m_quadIndexBufferView == nullptr) { m_lastError = "Failed to create the D3D12 UI unit-quad index view."; DestroyResources(); return false; } return true; } void D3D12UiRenderer::DestroyFrameResources(FrameResources& frameResources) { if (frameResources.primitiveInstanceBufferView != nullptr) { frameResources.primitiveInstanceBufferView->Shutdown(); delete frameResources.primitiveInstanceBufferView; frameResources.primitiveInstanceBufferView = nullptr; } if (frameResources.primitiveInstanceBuffer != nullptr) { frameResources.primitiveInstanceBuffer->Shutdown(); delete frameResources.primitiveInstanceBuffer; frameResources.primitiveInstanceBuffer = nullptr; } frameResources.primitiveInstanceCapacityBytes = 0u; } void D3D12UiRenderer::DestroyResources() { for (FrameResources& frameResources : m_frameResources) { DestroyFrameResources(frameResources); } if (m_quadIndexBufferView != nullptr) { m_quadIndexBufferView->Shutdown(); delete m_quadIndexBufferView; m_quadIndexBufferView = nullptr; } if (m_quadIndexBuffer != nullptr) { m_quadIndexBuffer->Shutdown(); delete m_quadIndexBuffer; m_quadIndexBuffer = nullptr; } if (m_quadVertexBufferView != nullptr) { m_quadVertexBufferView->Shutdown(); delete m_quadVertexBufferView; m_quadVertexBufferView = nullptr; } if (m_quadVertexBuffer != nullptr) { m_quadVertexBuffer->Shutdown(); delete m_quadVertexBuffer; m_quadVertexBuffer = nullptr; } if (m_primitivePipelineState != nullptr) { m_primitivePipelineState->Shutdown(); delete m_primitivePipelineState; m_primitivePipelineState = nullptr; } if (m_samplerSet != nullptr) { m_samplerSet->Shutdown(); delete m_samplerSet; m_samplerSet = nullptr; } if (m_samplerPool != nullptr) { m_samplerPool->Shutdown(); delete m_samplerPool; m_samplerPool = nullptr; } if (m_sampler != nullptr) { m_sampler->Shutdown(); delete m_sampler; m_sampler = nullptr; } if (m_constantSet != nullptr) { m_constantSet->Shutdown(); delete m_constantSet; m_constantSet = nullptr; } if (m_constantPool != nullptr) { m_constantPool->Shutdown(); delete m_constantPool; m_constantPool = nullptr; } if (m_pipelineLayout != nullptr) { m_pipelineLayout->Shutdown(); delete m_pipelineLayout; m_pipelineLayout = nullptr; } m_samplerGpuHandle = {}; m_device = nullptr; m_backendType = ::XCEngine::RHI::RHIType::D3D12; m_renderTargetFormat = Format::Unknown; m_renderTargetSampleCount = 1u; m_renderTargetSampleQuality = 0u; } bool D3D12UiRenderer::EnsureFrameBufferCapacity( std::uint32_t frameSlot, std::size_t primitiveInstanceBytes) { if (m_device == nullptr || frameSlot >= m_frameResources.size()) { return false; } FrameResources& frameResources = m_frameResources[frameSlot]; auto ensureVertexBuffer = [&](std::size_t requiredBytes, std::uint64_t minimumBytes, std::uint32_t stride, ::XCEngine::RHI::RHIBuffer*& buffer, ::XCEngine::RHI::RHIResourceView*& bufferView, std::uint64_t& capacityBytes) -> bool { if (requiredBytes == 0u) { return true; } const std::uint64_t resolvedRequiredBytes = (std::max)(static_cast(requiredBytes), minimumBytes); if (buffer != nullptr && capacityBytes >= resolvedRequiredBytes) { return true; } if (bufferView != nullptr) { bufferView->Shutdown(); delete bufferView; bufferView = nullptr; } if (buffer != nullptr) { buffer->Shutdown(); delete buffer; buffer = nullptr; } capacityBytes = resolvedRequiredBytes; BufferDesc bufferDesc = {}; bufferDesc.size = capacityBytes; bufferDesc.stride = stride; bufferDesc.bufferType = static_cast(BufferType::Vertex); buffer = m_device->CreateBuffer(bufferDesc); if (buffer == nullptr) { capacityBytes = 0u; return false; } buffer->SetStride(bufferDesc.stride); buffer->SetBufferType(BufferType::Vertex); ResourceViewDesc viewDesc = {}; viewDesc.dimension = ResourceViewDimension::Buffer; viewDesc.structureByteStride = bufferDesc.stride; bufferView = m_device->CreateVertexBufferView(buffer, viewDesc); if (bufferView == nullptr) { buffer->Shutdown(); delete buffer; buffer = nullptr; capacityBytes = 0u; return false; } return true; }; return ensureVertexBuffer( primitiveInstanceBytes, kMinDynamicInstanceBufferBytes, static_cast(sizeof(UiPrimitiveInstance)), frameResources.primitiveInstanceBuffer, frameResources.primitiveInstanceBufferView, frameResources.primitiveInstanceCapacityBytes); } bool D3D12UiRenderer::BuildDrawBatches( const UIDrawData& drawData, const RenderSurface& surface, std::vector& outPrimitiveInstances, std::vector& outBatches) { outPrimitiveInstances.clear(); outBatches.clear(); if (!EnsureWhiteTexture()) { return false; } const auto renderArea = surface.GetRenderArea(); const Rect fullScissor = { renderArea.x, renderArea.y, renderArea.x + renderArea.width, renderArea.y + renderArea.height }; const FloatRect fullRect = { static_cast(fullScissor.left), static_cast(fullScissor.top), static_cast(fullScissor.right), static_cast(fullScissor.bottom) }; const float dpiScale = ClampDpiScale(m_dpiScale); const std::uint64_t currentFrameId = ++m_compiledDrawListFrameCounter; const D3D12_GPU_DESCRIPTOR_HANDLE whiteTextureHandle = { static_cast(m_whiteTexture.nativeHandle) }; const float zeroAux[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; auto buildCompiledDrawList = [&](const UIDrawList& drawList, std::vector& compiledInstances, std::vector& compiledBatches) { compiledInstances.clear(); compiledBatches.clear(); std::vector localClipStack = {}; localClipStack.push_back({ fullScissor, fullRect }); auto appendAxisAlignedPrimitive = [&](const FloatRect& rect, const UIPoint& uvMin, const UIPoint& uvMax, const UIColor& color, const UIColor& secondaryColor, UiPrimitiveKind primitiveKind, float radius, float thickness, float gradientDirection, D3D12_GPU_DESCRIPTOR_HANDLE textureHandle, const Rect& scissor) { if (IsEmptyRect(rect) || textureHandle.ptr == 0u || IsEmptyScissor(scissor)) { return; } AppendPrimitiveInstance( compiledInstances, compiledBatches, FloatPoint{ rect.left, rect.top }, FloatPoint{ rect.right - rect.left, 0.0f }, FloatPoint{ 0.0f, rect.bottom - rect.top }, uvMin, uvMax, color, secondaryColor, primitiveKind, radius, thickness, gradientDirection, zeroAux, zeroAux, textureHandle, scissor); }; auto appendOrientedPrimitive = [&](const FloatPoint& origin, const FloatPoint& axisX, const FloatPoint& axisY, const UIColor& color, UiPrimitiveKind primitiveKind, float radius, float thickness, D3D12_GPU_DESCRIPTOR_HANDLE textureHandle, const Rect& scissor) { if (textureHandle.ptr == 0u || IsEmptyScissor(scissor)) { return; } AppendPrimitiveInstance( compiledInstances, compiledBatches, origin, axisX, axisY, UIPoint(0.0f, 0.0f), UIPoint(1.0f, 1.0f), color, color, primitiveKind, radius, thickness, 0.0f, zeroAux, zeroAux, textureHandle, scissor); }; auto appendTrianglePrimitive = [&](const FloatPoint& p0, const FloatPoint& p1, const FloatPoint& p2, const UIColor& color, D3D12_GPU_DESCRIPTOR_HANDLE textureHandle, const Rect& scissor) { if (textureHandle.ptr == 0u || IsEmptyScissor(scissor)) { return; } const float minX = (std::min)({ p0.x, p1.x, p2.x }); const float minY = (std::min)({ p0.y, p1.y, p2.y }); const float maxX = (std::max)({ p0.x, p1.x, p2.x }); const float maxY = (std::max)({ p0.y, p1.y, p2.y }); if (maxX <= minX || maxY <= minY) { return; } const FloatPoint origin = { minX, minY }; const FloatPoint axisX = { maxX - minX, 0.0f }; const FloatPoint axisY = { 0.0f, maxY - minY }; const float triangleAux0[4] = { p0.x - origin.x, p0.y - origin.y, p1.x - origin.x, p1.y - origin.y }; const float triangleAux1[4] = { p2.x - origin.x, p2.y - origin.y, 0.0f, 0.0f }; AppendPrimitiveInstance( compiledInstances, compiledBatches, origin, axisX, axisY, UIPoint(0.0f, 0.0f), UIPoint(1.0f, 1.0f), color, color, UiPrimitiveKind::TriangleFill, 0.0f, 0.0f, 0.0f, triangleAux0, triangleAux1, textureHandle, scissor); }; for (const UIDrawCommand& command : drawList.GetCommands()) { if (localClipStack.empty()) { localClipStack.push_back({ fullScissor, fullRect }); } if (command.type == UIDrawCommandType::PushClipRect) { FloatRect clipRect = ToPixelRect(command.rect, dpiScale); clipRect = command.intersectWithCurrentClip ? IntersectFloatRect(localClipStack.back().rect, clipRect) : IntersectFloatRect(fullRect, clipRect); ClipState clipState = {}; clipState.rect = clipRect; clipState.scissor = ClampScissor(clipRect, fullScissor); localClipStack.push_back(clipState); continue; } if (command.type == UIDrawCommandType::PopClipRect) { if (localClipStack.size() > 1u) { localClipStack.pop_back(); } continue; } const ClipState currentClip = localClipStack.back(); if (IsEmptyScissor(currentClip.scissor)) { continue; } switch (command.type) { case UIDrawCommandType::FilledRect: { appendAxisAlignedPrimitive( ToPixelRect(command.rect, dpiScale), UIPoint(0.0f, 0.0f), UIPoint(1.0f, 1.0f), command.color, command.color, UiPrimitiveKind::RectFill, command.rounding * dpiScale, 0.0f, 0.0f, whiteTextureHandle, currentClip.scissor); break; } case UIDrawCommandType::FilledRectLinearGradient: { appendAxisAlignedPrimitive( ToPixelRect(command.rect, dpiScale), UIPoint(0.0f, 0.0f), UIPoint(1.0f, 1.0f), command.color, command.secondaryColor, UiPrimitiveKind::RectFill, command.rounding * dpiScale, 0.0f, command.gradientDirection == UILinearGradientDirection::Vertical ? 1.0f : 0.0f, whiteTextureHandle, currentClip.scissor); break; } case UIDrawCommandType::RectOutline: { const FloatRect rect = ToPixelRect(command.rect, dpiScale); const float thickness = (command.thickness > 0.0f ? command.thickness : 1.0f) * dpiScale; const float halfThickness = thickness * 0.5f; appendAxisAlignedPrimitive( FloatRect{ rect.left - halfThickness, rect.top - halfThickness, rect.right + halfThickness, rect.bottom + halfThickness }, UIPoint(0.0f, 0.0f), UIPoint(1.0f, 1.0f), command.color, command.color, UiPrimitiveKind::RectOutline, command.rounding * dpiScale + halfThickness, thickness, 0.0f, whiteTextureHandle, currentClip.scissor); break; } case UIDrawCommandType::Line: { const FloatPoint start = ToPixelPoint(command.position, dpiScale); const FloatPoint end = ToPixelPoint(command.uvMin, dpiScale); const float dx = end.x - start.x; const float dy = end.y - start.y; const float length = std::sqrt(dx * dx + dy * dy); if (length <= 0.0f) { break; } const float thickness = (command.thickness > 0.0f ? command.thickness : 1.0f) * dpiScale; const float halfThickness = thickness * 0.5f; const float nx = -dy / length; const float ny = dx / length; appendOrientedPrimitive( FloatPoint{ start.x + nx * halfThickness, start.y + ny * halfThickness }, FloatPoint{ dx, dy }, FloatPoint{ -nx * thickness, -ny * thickness }, command.color, UiPrimitiveKind::RectFill, 0.0f, 0.0f, whiteTextureHandle, currentClip.scissor); break; } case UIDrawCommandType::FilledTriangle: { appendTrianglePrimitive( ToPixelPoint(command.position, dpiScale), ToPixelPoint(command.uvMin, dpiScale), ToPixelPoint(command.uvMax, dpiScale), command.color, whiteTextureHandle, currentClip.scissor); break; } case UIDrawCommandType::FilledCircle: { const FloatPoint center = ToPixelPoint(command.position, dpiScale); const float radius = command.radius * dpiScale; if (radius <= 0.0f) { break; } appendAxisAlignedPrimitive( FloatRect{ center.x - radius, center.y - radius, center.x + radius, center.y + radius }, UIPoint(0.0f, 0.0f), UIPoint(1.0f, 1.0f), command.color, command.color, UiPrimitiveKind::CircleFill, radius, 0.0f, 0.0f, whiteTextureHandle, currentClip.scissor); break; } case UIDrawCommandType::CircleOutline: { const FloatPoint center = ToPixelPoint(command.position, dpiScale); const float thickness = (command.thickness > 0.0f ? command.thickness : 1.0f) * dpiScale; const float radius = command.radius * dpiScale; const float outerRadius = radius + thickness * 0.5f; if (outerRadius <= 0.0f) { break; } appendAxisAlignedPrimitive( FloatRect{ center.x - outerRadius, center.y - outerRadius, center.x + outerRadius, center.y + outerRadius }, UIPoint(0.0f, 0.0f), UIPoint(1.0f, 1.0f), command.color, command.color, UiPrimitiveKind::CircleOutline, radius, thickness, 0.0f, whiteTextureHandle, currentClip.scissor); break; } case UIDrawCommandType::Text: { if (command.text.empty()) { break; } const CachedTextRun* textRun = ResolveTextRun(command.text, command.fontSize, currentFrameId); if (textRun == nullptr || !textRun->hasPixels || textRun->textureHandle.ptr == 0u || textRun->width <= 0.0f || textRun->height <= 0.0f) { break; } const FloatPoint position = ToPixelPoint(command.position, dpiScale); appendAxisAlignedPrimitive( FloatRect{ position.x + textRun->offsetX, position.y + textRun->offsetY, position.x + textRun->offsetX + textRun->width, position.y + textRun->offsetY + textRun->height }, UIPoint(0.0f, 0.0f), UIPoint(1.0f, 1.0f), command.color, command.color, UiPrimitiveKind::Textured, 0.0f, 0.0f, 0.0f, textRun->textureHandle, currentClip.scissor); break; } case UIDrawCommandType::Image: { if (!command.texture.IsValid()) { break; } appendAxisAlignedPrimitive( ToPixelRect(command.rect, dpiScale), command.uvMin, command.uvMax, command.color, command.color, UiPrimitiveKind::Textured, 0.0f, 0.0f, 0.0f, D3D12_GPU_DESCRIPTOR_HANDLE{ static_cast(command.texture.nativeHandle) }, currentClip.scissor); break; } default: break; } } return true; }; auto appendCompiledDrawList = [&](const CompiledDrawList& compiled) { if (compiled.batches.empty()) { return; } const std::uint32_t baseInstance = static_cast(outPrimitiveInstances.size()); if (!compiled.instances.empty()) { outPrimitiveInstances.insert( outPrimitiveInstances.end(), compiled.instances.begin(), compiled.instances.end()); } for (const UiBatch& batch : compiled.batches) { AppendBatch( outBatches, baseInstance + batch.firstInstance, batch.instanceCount, batch.textureHandle, batch.scissorRect); } }; const std::uint32_t renderWidth = renderArea.width > 0 ? renderArea.width : 0u; const std::uint32_t renderHeight = renderArea.height > 0 ? renderArea.height : 0u; const int dpiScaleMilli = static_cast(std::lround(dpiScale * 1000.0f)); for (const UIDrawList& drawList : drawData.GetDrawLists()) { if (drawList.Empty()) { continue; } CompiledDrawListKey key = {}; key.contentHash = BuildDrawListContentHash(drawList); key.renderWidth = renderWidth; key.renderHeight = renderHeight; key.dpiScaleMilli = dpiScaleMilli; auto found = m_compiledDrawListCache.find(key); if (found == m_compiledDrawListCache.end()) { CompiledDrawList compiled = {}; if (!buildCompiledDrawList( drawList, compiled.instances, compiled.batches)) { return false; } compiled.lastUsedFrame = currentFrameId; const auto [insertedIt, inserted] = m_compiledDrawListCache.emplace(key, std::move(compiled)); (void)inserted; found = insertedIt; } else { found->second.lastUsedFrame = currentFrameId; } appendCompiledDrawList(found->second); } constexpr std::size_t kCompiledDrawListCacheMaxEntries = 256u; constexpr std::uint64_t kCompiledDrawListCacheRetentionFrames = 240u; if (m_compiledDrawListCache.size() > kCompiledDrawListCacheMaxEntries) { const std::uint64_t pruneBeforeFrame = currentFrameId > kCompiledDrawListCacheRetentionFrames ? currentFrameId - kCompiledDrawListCacheRetentionFrames : 0u; for (auto it = m_compiledDrawListCache.begin(); it != m_compiledDrawListCache.end();) { if (it->second.lastUsedFrame <= pruneBeforeFrame) { it = m_compiledDrawListCache.erase(it); } else { ++it; } } } PruneTextRunCache(currentFrameId); return true; } bool D3D12UiRenderer::Render( const UIDrawData& drawData, const RenderContext& renderContext, const RenderSurface& surface) { if (m_windowRenderer == nullptr || m_textureHost == nullptr || m_textSystem == nullptr) { m_lastError = "Render requires an initialized D3D12 UI renderer."; return false; } if (!renderContext.IsValid() || renderContext.commandList == nullptr) { m_lastError = "Render requires a valid D3D12 render context."; return false; } if (!::XCEngine::Rendering::HasSingleColorAttachment(surface) || surface.GetColorAttachments().empty() || surface.GetColorAttachments()[0] == nullptr) { m_lastError = "Render requires a valid single-color render surface."; return false; } if (!EnsureInitialized(renderContext, surface)) { if (m_lastError.empty()) { m_lastError = "Failed to initialize the D3D12 UI renderer resources."; } return false; } m_textureHost->BeginFrame(m_windowRenderer->GetActiveFrameSlot()); std::vector primitiveInstances = {}; std::vector batches = {}; if (!BuildDrawBatches(drawData, surface, primitiveInstances, batches)) { if (m_lastError.empty()) { m_lastError = "Failed to build the D3D12 UI draw batches."; } return false; } if (batches.empty()) { m_lastError.clear(); return true; } const std::uint32_t frameSlot = m_windowRenderer->GetActiveFrameSlot(); if (!EnsureFrameBufferCapacity( frameSlot, primitiveInstances.size() * sizeof(UiPrimitiveInstance))) { m_lastError = "Failed to allocate the D3D12 UI dynamic buffers."; return false; } FrameResources& frameResources = m_frameResources[frameSlot]; if (!primitiveInstances.empty() && frameResources.primitiveInstanceBuffer != nullptr) { frameResources.primitiveInstanceBuffer->SetData( primitiveInstances.data(), primitiveInstances.size() * sizeof(UiPrimitiveInstance)); } const auto renderArea = surface.GetRenderArea(); UiConstants constants = {}; constants.invViewportSize[0] = renderArea.width > 0 ? 1.0f / static_cast(renderArea.width) : 0.0f; constants.invViewportSize[1] = renderArea.height > 0 ? 1.0f / static_cast(renderArea.height) : 0.0f; m_constantSet->WriteConstant(0u, &constants, sizeof(constants)); auto* d3d12CommandList = static_cast(renderContext.commandList); auto* renderTarget = surface.GetColorAttachments()[0]; RHICommandList* commandList = renderContext.commandList; commandList->SetRenderTargets(1u, &renderTarget, nullptr); commandList->SetViewport(Viewport{ static_cast(renderArea.x), static_cast(renderArea.y), static_cast(renderArea.width), static_cast(renderArea.height), 0.0f, 1.0f }); commandList->SetScissorRect(Rect{ renderArea.x, renderArea.y, renderArea.x + renderArea.width, renderArea.y + renderArea.height }); commandList->SetPrimitiveTopology(PrimitiveTopology::TriangleList); ::XCEngine::RHI::RHIDescriptorSet* constantSets[] = { m_constantSet }; commandList->SetGraphicsDescriptorSets(0u, 1u, constantSets, m_pipelineLayout); auto* textureHeap = m_windowRenderer->GetTextureDescriptorAllocator().GetDescriptorHeap(); auto* samplerHeap = static_cast(m_samplerPool)->GetDescriptorHeap(); ID3D12DescriptorHeap* descriptorHeaps[] = { textureHeap, samplerHeap }; d3d12CommandList->SetDescriptorHeaps(2u, descriptorHeaps); const auto* pipelineLayout = static_cast(m_pipelineLayout); const std::uint32_t textureRootIndex = pipelineLayout->GetShaderResourceTableRootParameterIndex(1u); const std::uint32_t samplerRootIndex = pipelineLayout->GetSamplerTableRootParameterIndex(2u); if (m_primitivePipelineState == nullptr || m_quadVertexBufferView == nullptr || m_quadIndexBufferView == nullptr || frameResources.primitiveInstanceBufferView == nullptr) { m_lastError = "D3D12 UI primitive rendering resources are unavailable."; return false; } commandList->SetPipelineState(m_primitivePipelineState); const std::uint64_t primitiveVertexOffsets[] = { 0u, 0u }; const std::uint32_t primitiveVertexStrides[] = { static_cast(sizeof(UiQuadVertex)), static_cast(sizeof(UiPrimitiveInstance)) }; ::XCEngine::RHI::RHIResourceView* primitiveVertexViews[] = { m_quadVertexBufferView, frameResources.primitiveInstanceBufferView }; commandList->SetVertexBuffers( 0u, 2u, primitiveVertexViews, primitiveVertexOffsets, primitiveVertexStrides); commandList->SetIndexBuffer(m_quadIndexBufferView, 0u); for (const UiBatch& batch : batches) { commandList->SetScissorRect(batch.scissorRect); d3d12CommandList->SetGraphicsRootDescriptorTable( textureRootIndex, batch.textureHandle); d3d12CommandList->SetGraphicsRootDescriptorTable( samplerRootIndex, m_samplerGpuHandle); commandList->DrawIndexed( static_cast(sizeof(kUnitQuadIndices) / sizeof(kUnitQuadIndices[0])), batch.instanceCount, 0u, 0, batch.firstInstance); } m_lastError.clear(); return true; } } // namespace XCEngine::UI::Editor::Host