Files
XCEngine/editor/app/Rendering/D3D12/D3D12UiRenderer.cpp
2026-04-25 16:46:01 +08:00

1828 lines
66 KiB
C++

#include "D3D12UiRenderer.h"
#include <XCEngine/Rendering/RenderSurfacePipelineUtils.h>
#include <XCEngine/RHI/D3D12/D3D12CommandList.h>
#include <XCEngine/RHI/D3D12/D3D12DescriptorHeap.h>
#include <XCEngine/RHI/D3D12/D3D12DescriptorSet.h>
#include <XCEngine/RHI/D3D12/D3D12PipelineLayout.h>
#include <XCEngine/RHI/RHICommandList.h>
#include <XCEngine/RHI/RHIEnums.h>
#include <XCEngine/RHI/RHITypes.h>
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <utility>
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<unsigned int>(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<int>(std::floor(rect.left)));
clamped.top = (std::max)(renderArea.top, static_cast<int>(std::floor(rect.top)));
clamped.right = (std::min)(renderArea.right, static_cast<int>(std::ceil(rect.right)));
clamped.bottom = (std::min)(renderArea.bottom, static_cast<int>(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<const unsigned char*>(data);
for (std::size_t index = 0u; index < size; ++index) {
hash ^= static_cast<std::uint64_t>(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<D3D12UiRenderer::UiBatch>& 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<D3D12UiRenderer::UiPrimitiveInstance>& instances,
std::vector<D3D12UiRenderer::UiBatch>& 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<float>(static_cast<int>(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<std::uint32_t>(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<std::uint32_t>(PrimitiveTopologyType::Triangle);
::XCEngine::Rendering::ApplySingleColorAttachmentPropertiesToGraphicsPipelineDesc(
surface,
pipelineDesc);
pipelineDesc.inputLayout.elements = {
{ "POSITION", 0, static_cast<std::uint32_t>(Format::R32G32_Float), 0, 0, 0, 0 },
{ "TEXCOORD", 0, static_cast<std::uint32_t>(Format::R32G32_Float), 0, 8, 0, 0 },
{ "COLOR", 0, static_cast<std::uint32_t>(Format::R32G32B32A32_Float), 0, 16, 0, 0 }
};
pipelineDesc.rasterizerState.fillMode =
static_cast<std::uint32_t>(FillMode::Solid);
pipelineDesc.rasterizerState.cullMode =
static_cast<std::uint32_t>(CullMode::None);
pipelineDesc.rasterizerState.frontFace =
static_cast<std::uint32_t>(FrontFace::CounterClockwise);
pipelineDesc.rasterizerState.depthClipEnable = true;
pipelineDesc.rasterizerState.scissorTestEnable = true;
pipelineDesc.blendState.blendEnable = true;
pipelineDesc.blendState.srcBlend =
static_cast<std::uint32_t>(BlendFactor::SrcAlpha);
pipelineDesc.blendState.dstBlend =
static_cast<std::uint32_t>(BlendFactor::InvSrcAlpha);
pipelineDesc.blendState.srcBlendAlpha =
static_cast<std::uint32_t>(BlendFactor::One);
pipelineDesc.blendState.dstBlendAlpha =
static_cast<std::uint32_t>(BlendFactor::InvSrcAlpha);
pipelineDesc.blendState.blendOp =
static_cast<std::uint32_t>(BlendOp::Add);
pipelineDesc.blendState.blendOpAlpha =
static_cast<std::uint32_t>(BlendOp::Add);
pipelineDesc.blendState.colorWriteMask =
static_cast<std::uint8_t>(ColorWriteMask::All);
pipelineDesc.depthStencilState.depthTestEnable = false;
pipelineDesc.depthStencilState.depthWriteEnable = false;
pipelineDesc.depthStencilState.depthFunc =
static_cast<std::uint32_t>(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<std::uint32_t>(Format::R32G32_Float), 0, 0, 0, 0 },
{ "TEXCOORD", 0, static_cast<std::uint32_t>(Format::R32G32_Float), 0, 8, 0, 0 },
{ "POSITION", 1, static_cast<std::uint32_t>(Format::R32G32_Float), 1, 0, 1, 1 },
{ "TEXCOORD", 1, static_cast<std::uint32_t>(Format::R32G32_Float), 1, 8, 1, 1 },
{ "TEXCOORD", 2, static_cast<std::uint32_t>(Format::R32G32_Float), 1, 16, 1, 1 },
{ "TEXCOORD", 3, static_cast<std::uint32_t>(Format::R32G32_Float), 1, 24, 1, 1 },
{ "TEXCOORD", 4, static_cast<std::uint32_t>(Format::R32G32_Float), 1, 32, 1, 1 },
{ "COLOR", 0, static_cast<std::uint32_t>(Format::R32G32B32A32_Float), 1, 40, 1, 1 },
{ "COLOR", 1, static_cast<std::uint32_t>(Format::R32G32B32A32_Float), 1, 56, 1, 1 },
{ "TEXCOORD", 5, static_cast<std::uint32_t>(Format::R32G32B32A32_Float), 1, 72, 1, 1 },
{ "TEXCOORD", 6, static_cast<std::uint32_t>(Format::R32G32B32A32_Float), 1, 88, 1, 1 },
{ "TEXCOORD", 7, static_cast<std::uint32_t>(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<std::string>{}(key.text);
const std::size_t fontHash = std::hash<int>{}(key.fontSizeTenths);
const std::size_t dpiHash = std::hash<int>{}(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<std::uint64_t>{}(key.contentHash);
const std::size_t widthHash = std::hash<std::uint32_t>{}(key.renderWidth);
const std::size_t heightHash = std::hash<std::uint32_t>{}(key.renderHeight);
const std::size_t dpiHash = std::hash<int>{}(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<int>(std::lround(ResolveFontSize(fontSize) * 10.0f));
cacheKey.dpiScaleMilli =
static_cast<int>(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<float>(rasterizedText.width);
cachedText.height = static_cast<float>(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<UINT64>(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<std::uint32_t>(DescriptorType::CBV);
constantBinding.count = 1u;
constantBinding.visibility = static_cast<std::uint32_t>(ShaderVisibility::All);
DescriptorSetLayoutBinding textureBinding = {};
textureBinding.binding = 0u;
textureBinding.type = static_cast<std::uint32_t>(DescriptorType::SRV);
textureBinding.count = 1u;
textureBinding.visibility = static_cast<std::uint32_t>(ShaderVisibility::All);
DescriptorSetLayoutBinding samplerBinding = {};
samplerBinding.binding = 0u;
samplerBinding.type = static_cast<std::uint32_t>(DescriptorType::Sampler);
samplerBinding.count = 1u;
samplerBinding.visibility = static_cast<std::uint32_t>(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<std::uint32_t>(FilterMode::Linear);
samplerDesc.addressU = static_cast<std::uint32_t>(TextureAddressMode::Clamp);
samplerDesc.addressV = static_cast<std::uint32_t>(TextureAddressMode::Clamp);
samplerDesc.addressW = static_cast<std::uint32_t>(TextureAddressMode::Clamp);
samplerDesc.maxAnisotropy = 1u;
samplerDesc.comparisonFunc = static_cast<std::uint32_t>(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<D3D12DescriptorSet*>(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<std::uint32_t>(sizeof(UiQuadVertex));
quadVertexBufferDesc.bufferType = static_cast<std::uint32_t>(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<std::uint32_t>(sizeof(std::uint32_t));
quadIndexBufferDesc.bufferType = static_cast<std::uint32_t>(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<std::uint32_t>(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<std::uint64_t>)(static_cast<std::uint64_t>(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<std::uint32_t>(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<std::uint32_t>(sizeof(UiPrimitiveInstance)),
frameResources.primitiveInstanceBuffer,
frameResources.primitiveInstanceBufferView,
frameResources.primitiveInstanceCapacityBytes);
}
bool D3D12UiRenderer::BuildDrawBatches(
const UIDrawData& drawData,
const RenderSurface& surface,
std::vector<UiPrimitiveInstance>& outPrimitiveInstances,
std::vector<UiBatch>& 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<float>(fullScissor.left),
static_cast<float>(fullScissor.top),
static_cast<float>(fullScissor.right),
static_cast<float>(fullScissor.bottom)
};
const float dpiScale = ClampDpiScale(m_dpiScale);
const std::uint64_t currentFrameId = ++m_compiledDrawListFrameCounter;
const D3D12_GPU_DESCRIPTOR_HANDLE whiteTextureHandle = {
static_cast<UINT64>(m_whiteTexture.nativeHandle)
};
const float zeroAux[4] = { 0.0f, 0.0f, 0.0f, 0.0f };
auto buildCompiledDrawList =
[&](const UIDrawList& drawList,
std::vector<UiPrimitiveInstance>& compiledInstances,
std::vector<UiBatch>& compiledBatches) {
compiledInstances.clear();
compiledBatches.clear();
std::vector<ClipState> 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<UINT64>(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<std::uint32_t>(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<int>(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<UiPrimitiveInstance> primitiveInstances = {};
std::vector<UiBatch> 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<float>(renderArea.width) : 0.0f;
constants.invViewportSize[1] =
renderArea.height > 0 ? 1.0f / static_cast<float>(renderArea.height) : 0.0f;
m_constantSet->WriteConstant(0u, &constants, sizeof(constants));
auto* d3d12CommandList =
static_cast<D3D12CommandList*>(renderContext.commandList);
auto* renderTarget = surface.GetColorAttachments()[0];
RHICommandList* commandList = renderContext.commandList;
commandList->SetRenderTargets(1u, &renderTarget, nullptr);
commandList->SetViewport(Viewport{
static_cast<float>(renderArea.x),
static_cast<float>(renderArea.y),
static_cast<float>(renderArea.width),
static_cast<float>(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<D3D12DescriptorHeap*>(m_samplerPool)->GetDescriptorHeap();
ID3D12DescriptorHeap* descriptorHeaps[] = { textureHeap, samplerHeap };
d3d12CommandList->SetDescriptorHeaps(2u, descriptorHeaps);
const auto* pipelineLayout =
static_cast<const ::XCEngine::RHI::D3D12PipelineLayout*>(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<std::uint32_t>(sizeof(UiQuadVertex)),
static_cast<std::uint32_t>(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<std::uint32_t>(sizeof(kUnitQuadIndices) / sizeof(kUnitQuadIndices[0])),
batch.instanceCount,
0u,
0,
batch.firstInstance);
}
m_lastError.clear();
return true;
}
} // namespace XCEngine::UI::Editor::Host