591 lines
21 KiB
C++
591 lines
21 KiB
C++
#include "XCUIBackend/XCUIRHICommandCompiler.h"
|
|
|
|
#include <algorithm>
|
|
#include <iterator>
|
|
|
|
namespace XCEngine {
|
|
namespace Editor {
|
|
namespace XCUIBackend {
|
|
|
|
namespace {
|
|
|
|
using Compiler = XCUIRHICommandCompiler;
|
|
|
|
UI::UIRect NormalizeRect(const UI::UIRect& rect) {
|
|
UI::UIRect normalized = rect;
|
|
if (normalized.width < 0.0f) {
|
|
normalized.x += normalized.width;
|
|
normalized.width = -normalized.width;
|
|
}
|
|
if (normalized.height < 0.0f) {
|
|
normalized.y += normalized.height;
|
|
normalized.height = -normalized.height;
|
|
}
|
|
return normalized;
|
|
}
|
|
|
|
bool IsRectEmpty(const UI::UIRect& rect) {
|
|
return rect.width <= 0.0f || rect.height <= 0.0f;
|
|
}
|
|
|
|
bool IntersectRects(
|
|
const UI::UIRect& lhs,
|
|
const UI::UIRect& rhs,
|
|
UI::UIRect& outIntersection) {
|
|
const UI::UIRect left = NormalizeRect(lhs);
|
|
const UI::UIRect right = NormalizeRect(rhs);
|
|
const float minX = (std::max)(left.x, right.x);
|
|
const float minY = (std::max)(left.y, right.y);
|
|
const float maxX = (std::min)(left.x + left.width, right.x + right.width);
|
|
const float maxY = (std::min)(left.y + left.height, right.y + right.height);
|
|
if (maxX <= minX || maxY <= minY) {
|
|
outIntersection = {};
|
|
return false;
|
|
}
|
|
|
|
outIntersection = UI::UIRect(minX, minY, maxX - minX, maxY - minY);
|
|
return true;
|
|
}
|
|
|
|
bool RectEquals(const UI::UIRect& lhs, const UI::UIRect& rhs) {
|
|
return lhs.x == rhs.x &&
|
|
lhs.y == rhs.y &&
|
|
lhs.width == rhs.width &&
|
|
lhs.height == rhs.height;
|
|
}
|
|
|
|
bool TextureEquals(const UI::UITextureHandle& lhs, const UI::UITextureHandle& rhs) {
|
|
return lhs.nativeHandle == rhs.nativeHandle &&
|
|
lhs.width == rhs.width &&
|
|
lhs.height == rhs.height &&
|
|
lhs.kind == rhs.kind;
|
|
}
|
|
|
|
void AppendFilledRectVertices(
|
|
std::vector<Compiler::ColorVertex>& outVertices,
|
|
const UI::UIRect& rect,
|
|
const UI::UIColor& color) {
|
|
const Compiler::ColorVertex vertices[6] = {
|
|
{ { rect.x, rect.y }, { color.r, color.g, color.b, color.a } },
|
|
{ { rect.x + rect.width, rect.y }, { color.r, color.g, color.b, color.a } },
|
|
{ { rect.x + rect.width, rect.y + rect.height }, { color.r, color.g, color.b, color.a } },
|
|
{ { rect.x, rect.y }, { color.r, color.g, color.b, color.a } },
|
|
{ { rect.x + rect.width, rect.y + rect.height }, { color.r, color.g, color.b, color.a } },
|
|
{ { rect.x, rect.y + rect.height }, { color.r, color.g, color.b, color.a } }
|
|
};
|
|
outVertices.insert(outVertices.end(), std::begin(vertices), std::end(vertices));
|
|
}
|
|
|
|
bool AppendClippedFilledRect(
|
|
std::vector<Compiler::ColorVertex>& outVertices,
|
|
const UI::UIRect& rect,
|
|
const UI::UIColor& color,
|
|
const UI::UIRect& clipRect) {
|
|
UI::UIRect clipped = {};
|
|
if (!IntersectRects(rect, clipRect, clipped)) {
|
|
return false;
|
|
}
|
|
|
|
AppendFilledRectVertices(outVertices, clipped, color);
|
|
return true;
|
|
}
|
|
|
|
bool AppendClippedRectOutline(
|
|
std::vector<Compiler::ColorVertex>& outVertices,
|
|
const UI::UIRect& rect,
|
|
const UI::UIColor& color,
|
|
float thickness,
|
|
const UI::UIRect& clipRect) {
|
|
const UI::UIRect normalized = NormalizeRect(rect);
|
|
if (IsRectEmpty(normalized)) {
|
|
return false;
|
|
}
|
|
|
|
const float resolvedThickness = (std::max)(1.0f, thickness);
|
|
const float halfWidth = normalized.width * 0.5f;
|
|
const float halfHeight = normalized.height * 0.5f;
|
|
const float edgeThickness = (std::min)(resolvedThickness, (std::min)(halfWidth, halfHeight));
|
|
if (edgeThickness <= 0.0f) {
|
|
return false;
|
|
}
|
|
|
|
bool rendered = false;
|
|
rendered |= AppendClippedFilledRect(
|
|
outVertices,
|
|
UI::UIRect(normalized.x, normalized.y, normalized.width, edgeThickness),
|
|
color,
|
|
clipRect);
|
|
rendered |= AppendClippedFilledRect(
|
|
outVertices,
|
|
UI::UIRect(
|
|
normalized.x,
|
|
normalized.y + normalized.height - edgeThickness,
|
|
normalized.width,
|
|
edgeThickness),
|
|
color,
|
|
clipRect);
|
|
rendered |= AppendClippedFilledRect(
|
|
outVertices,
|
|
UI::UIRect(
|
|
normalized.x,
|
|
normalized.y + edgeThickness,
|
|
edgeThickness,
|
|
normalized.height - edgeThickness * 2.0f),
|
|
color,
|
|
clipRect);
|
|
rendered |= AppendClippedFilledRect(
|
|
outVertices,
|
|
UI::UIRect(
|
|
normalized.x + normalized.width - edgeThickness,
|
|
normalized.y + edgeThickness,
|
|
edgeThickness,
|
|
normalized.height - edgeThickness * 2.0f),
|
|
color,
|
|
clipRect);
|
|
return rendered;
|
|
}
|
|
|
|
void AppendTexturedRectVertices(
|
|
std::vector<Compiler::TexturedVertex>& outVertices,
|
|
const UI::UIRect& rect,
|
|
float u0,
|
|
float v0,
|
|
float u1,
|
|
float v1,
|
|
const UI::UIColor& color) {
|
|
const Compiler::TexturedVertex vertices[6] = {
|
|
{ { rect.x, rect.y }, { u0, v0 }, { color.r, color.g, color.b, color.a } },
|
|
{ { rect.x + rect.width, rect.y }, { u1, v0 }, { color.r, color.g, color.b, color.a } },
|
|
{ { rect.x + rect.width, rect.y + rect.height }, { u1, v1 }, { color.r, color.g, color.b, color.a } },
|
|
{ { rect.x, rect.y }, { u0, v0 }, { color.r, color.g, color.b, color.a } },
|
|
{ { rect.x + rect.width, rect.y + rect.height }, { u1, v1 }, { color.r, color.g, color.b, color.a } },
|
|
{ { rect.x, rect.y + rect.height }, { u0, v1 }, { color.r, color.g, color.b, color.a } }
|
|
};
|
|
outVertices.insert(outVertices.end(), std::begin(vertices), std::end(vertices));
|
|
}
|
|
|
|
bool AppendClippedTexturedRect(
|
|
std::vector<Compiler::TexturedVertex>& outVertices,
|
|
const UI::UIRect& rect,
|
|
float u0,
|
|
float v0,
|
|
float u1,
|
|
float v1,
|
|
const UI::UIColor& color,
|
|
const UI::UIRect& clipRect) {
|
|
const bool flipU = rect.width < 0.0f;
|
|
const bool flipV = rect.height < 0.0f;
|
|
const UI::UIRect normalized = NormalizeRect(rect);
|
|
if (IsRectEmpty(normalized)) {
|
|
return false;
|
|
}
|
|
|
|
UI::UIRect clipped = {};
|
|
if (!IntersectRects(normalized, clipRect, clipped)) {
|
|
return false;
|
|
}
|
|
|
|
const float leftT = (clipped.x - normalized.x) / normalized.width;
|
|
const float rightT = (clipped.x + clipped.width - normalized.x) / normalized.width;
|
|
const float topT = (clipped.y - normalized.y) / normalized.height;
|
|
const float bottomT = (clipped.y + clipped.height - normalized.y) / normalized.height;
|
|
const float resolvedU0 = flipU ? u1 : u0;
|
|
const float resolvedU1 = flipU ? u0 : u1;
|
|
const float resolvedV0 = flipV ? v1 : v0;
|
|
const float resolvedV1 = flipV ? v0 : v1;
|
|
const float clippedU0 = resolvedU0 + (resolvedU1 - resolvedU0) * leftT;
|
|
const float clippedU1 = resolvedU0 + (resolvedU1 - resolvedU0) * rightT;
|
|
const float clippedV0 = resolvedV0 + (resolvedV1 - resolvedV0) * topT;
|
|
const float clippedV1 = resolvedV0 + (resolvedV1 - resolvedV0) * bottomT;
|
|
AppendTexturedRectVertices(
|
|
outVertices,
|
|
clipped,
|
|
clippedU0,
|
|
clippedV0,
|
|
clippedU1,
|
|
clippedV1,
|
|
color);
|
|
return true;
|
|
}
|
|
|
|
bool DecodeNextUtf8(const char*& cursor, const char* end, std::uint32_t& outCodepoint) {
|
|
if (cursor >= end) {
|
|
return false;
|
|
}
|
|
|
|
const unsigned char lead = static_cast<unsigned char>(*cursor++);
|
|
if (lead < 0x80u) {
|
|
outCodepoint = static_cast<std::uint32_t>(lead);
|
|
return true;
|
|
}
|
|
|
|
const int continuationCount =
|
|
(lead & 0xE0u) == 0xC0u ? 1 :
|
|
(lead & 0xF0u) == 0xE0u ? 2 :
|
|
(lead & 0xF8u) == 0xF0u ? 3 :
|
|
-1;
|
|
if (continuationCount < 0 || cursor + continuationCount > end) {
|
|
outCodepoint = 0xFFFDu;
|
|
cursor = end;
|
|
return true;
|
|
}
|
|
|
|
std::uint32_t codepoint =
|
|
continuationCount == 1 ? static_cast<std::uint32_t>(lead & 0x1Fu) :
|
|
continuationCount == 2 ? static_cast<std::uint32_t>(lead & 0x0Fu) :
|
|
static_cast<std::uint32_t>(lead & 0x07u);
|
|
for (int index = 0; index < continuationCount; ++index) {
|
|
const unsigned char continuation = static_cast<unsigned char>(*cursor);
|
|
if ((continuation & 0xC0u) != 0x80u) {
|
|
outCodepoint = 0xFFFDu;
|
|
return true;
|
|
}
|
|
++cursor;
|
|
codepoint = (codepoint << 6u) | static_cast<std::uint32_t>(continuation & 0x3Fu);
|
|
}
|
|
|
|
outCodepoint = codepoint;
|
|
return true;
|
|
}
|
|
|
|
void AppendOrMergeBatch(
|
|
std::vector<Compiler::Batch>& outBatches,
|
|
Compiler::BatchKind kind,
|
|
std::size_t drawListIndex,
|
|
std::size_t commandIndex,
|
|
std::size_t firstVertex,
|
|
std::size_t vertexCount,
|
|
const UI::UIRect& clipRect,
|
|
const UI::UITextureHandle& texture,
|
|
bool allowMerge) {
|
|
if (vertexCount == 0u) {
|
|
return;
|
|
}
|
|
|
|
if (allowMerge && !outBatches.empty()) {
|
|
Compiler::Batch& lastBatch = outBatches.back();
|
|
if (lastBatch.kind == kind &&
|
|
lastBatch.drawListIndex == drawListIndex &&
|
|
lastBatch.firstCommandIndex + lastBatch.commandCount == commandIndex &&
|
|
RectEquals(lastBatch.clipRect, clipRect) &&
|
|
TextureEquals(lastBatch.texture, texture) &&
|
|
static_cast<std::size_t>(lastBatch.firstVertex + lastBatch.vertexCount) == firstVertex) {
|
|
lastBatch.vertexCount += static_cast<std::uint32_t>(vertexCount);
|
|
lastBatch.commandCount += 1u;
|
|
return;
|
|
}
|
|
}
|
|
|
|
Compiler::Batch batch = {};
|
|
batch.kind = kind;
|
|
batch.drawListIndex = static_cast<std::uint32_t>(drawListIndex);
|
|
batch.firstCommandIndex = static_cast<std::uint32_t>(commandIndex);
|
|
batch.commandCount = 1u;
|
|
batch.firstVertex = static_cast<std::uint32_t>(firstVertex);
|
|
batch.vertexCount = static_cast<std::uint32_t>(vertexCount);
|
|
batch.clipRect = clipRect;
|
|
batch.texture = texture;
|
|
outBatches.push_back(batch);
|
|
}
|
|
|
|
bool CompileTextCommand(
|
|
const UI::UIDrawCommand& command,
|
|
const UI::UIRect& clipRect,
|
|
const Compiler::TextGlyphProvider& glyphProvider,
|
|
std::vector<Compiler::TexturedVertex>& outVertices,
|
|
Compiler::TextRunContext& outContext,
|
|
std::size_t& outAddedVertexCount) {
|
|
outContext = {};
|
|
if (!glyphProvider.BeginText(command.fontSize, outContext)) {
|
|
outAddedVertexCount = 0u;
|
|
return false;
|
|
}
|
|
|
|
if (!outContext.texture.IsValid() || outContext.lineHeight <= 0.0f) {
|
|
outAddedVertexCount = 0u;
|
|
return false;
|
|
}
|
|
|
|
const std::size_t startVertex = outVertices.size();
|
|
const float startX = command.position.x;
|
|
float cursorX = startX;
|
|
float cursorY = command.position.y;
|
|
|
|
const char* cursor = command.text.c_str();
|
|
const char* const end = cursor + command.text.size();
|
|
while (cursor < end) {
|
|
std::uint32_t codepoint = 0u;
|
|
if (!DecodeNextUtf8(cursor, end, codepoint)) {
|
|
break;
|
|
}
|
|
|
|
if (codepoint == '\r') {
|
|
continue;
|
|
}
|
|
if (codepoint == '\n') {
|
|
cursorX = startX;
|
|
cursorY += outContext.lineHeight;
|
|
continue;
|
|
}
|
|
|
|
Compiler::TextGlyph glyph = {};
|
|
if (!glyphProvider.ResolveGlyph(outContext, codepoint, glyph)) {
|
|
continue;
|
|
}
|
|
|
|
if (glyph.visible) {
|
|
const UI::UIRect glyphRect(
|
|
cursorX + glyph.x0,
|
|
cursorY + glyph.y0,
|
|
glyph.x1 - glyph.x0,
|
|
glyph.y1 - glyph.y0);
|
|
AppendClippedTexturedRect(
|
|
outVertices,
|
|
glyphRect,
|
|
glyph.u0,
|
|
glyph.v0,
|
|
glyph.u1,
|
|
glyph.v1,
|
|
command.color,
|
|
clipRect);
|
|
}
|
|
|
|
cursorX += glyph.advanceX;
|
|
}
|
|
|
|
outAddedVertexCount = outVertices.size() - startVertex;
|
|
return true;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void XCUIRHICommandCompiler::CompiledDrawData::Clear() {
|
|
colorVertices.clear();
|
|
texturedVertices.clear();
|
|
batches.clear();
|
|
stats = {};
|
|
}
|
|
|
|
bool XCUIRHICommandCompiler::CompiledDrawData::Empty() const {
|
|
return colorVertices.empty() && texturedVertices.empty() && batches.empty();
|
|
}
|
|
|
|
void XCUIRHICommandCompiler::Compile(
|
|
const UI::UIDrawData& drawData,
|
|
const CompileConfig& config,
|
|
CompiledDrawData& outCompiledDrawData) const {
|
|
outCompiledDrawData.Clear();
|
|
outCompiledDrawData.stats.drawListCount = drawData.GetDrawListCount();
|
|
outCompiledDrawData.stats.commandCount = drawData.GetTotalCommandCount();
|
|
if (drawData.Empty()) {
|
|
return;
|
|
}
|
|
|
|
UI::UIRect surfaceClipRect = NormalizeRect(config.surfaceClipRect);
|
|
if (IsRectEmpty(surfaceClipRect)) {
|
|
surfaceClipRect = {};
|
|
}
|
|
|
|
outCompiledDrawData.colorVertices.reserve(drawData.GetTotalCommandCount() * 24u);
|
|
outCompiledDrawData.texturedVertices.reserve(drawData.GetTotalCommandCount() * 36u);
|
|
outCompiledDrawData.batches.reserve(drawData.GetTotalCommandCount());
|
|
|
|
std::vector<UI::UIRect> clipStack = {};
|
|
UI::UIRect currentClipRect = surfaceClipRect;
|
|
|
|
std::size_t drawListIndex = 0u;
|
|
for (const UI::UIDrawList& drawList : drawData.GetDrawLists()) {
|
|
std::size_t commandIndex = 0u;
|
|
for (const UI::UIDrawCommand& command : drawList.GetCommands()) {
|
|
bool compiled = false;
|
|
bool culled = false;
|
|
bool unsupported = false;
|
|
|
|
switch (command.type) {
|
|
case UI::UIDrawCommandType::FilledRect: {
|
|
++outCompiledDrawData.stats.filledRectCommandCount;
|
|
const std::size_t firstVertex = outCompiledDrawData.colorVertices.size();
|
|
compiled = AppendClippedFilledRect(
|
|
outCompiledDrawData.colorVertices,
|
|
command.rect,
|
|
command.color,
|
|
currentClipRect);
|
|
if (compiled) {
|
|
AppendOrMergeBatch(
|
|
outCompiledDrawData.batches,
|
|
BatchKind::Color,
|
|
drawListIndex,
|
|
commandIndex,
|
|
firstVertex,
|
|
outCompiledDrawData.colorVertices.size() - firstVertex,
|
|
currentClipRect,
|
|
{},
|
|
config.mergeBatchesWithinDrawList);
|
|
} else {
|
|
culled = true;
|
|
}
|
|
break;
|
|
}
|
|
case UI::UIDrawCommandType::RectOutline: {
|
|
++outCompiledDrawData.stats.rectOutlineCommandCount;
|
|
const std::size_t firstVertex = outCompiledDrawData.colorVertices.size();
|
|
compiled = AppendClippedRectOutline(
|
|
outCompiledDrawData.colorVertices,
|
|
command.rect,
|
|
command.color,
|
|
command.thickness,
|
|
currentClipRect);
|
|
if (compiled) {
|
|
AppendOrMergeBatch(
|
|
outCompiledDrawData.batches,
|
|
BatchKind::Color,
|
|
drawListIndex,
|
|
commandIndex,
|
|
firstVertex,
|
|
outCompiledDrawData.colorVertices.size() - firstVertex,
|
|
currentClipRect,
|
|
{},
|
|
config.mergeBatchesWithinDrawList);
|
|
} else {
|
|
culled = true;
|
|
}
|
|
break;
|
|
}
|
|
case UI::UIDrawCommandType::Text: {
|
|
++outCompiledDrawData.stats.textCommandCount;
|
|
if (command.text.empty()) {
|
|
culled = true;
|
|
break;
|
|
}
|
|
if (config.textGlyphProvider == nullptr) {
|
|
unsupported = true;
|
|
break;
|
|
}
|
|
|
|
const std::size_t firstVertex = outCompiledDrawData.texturedVertices.size();
|
|
std::size_t addedVertexCount = 0u;
|
|
TextRunContext textRunContext = {};
|
|
compiled = CompileTextCommand(
|
|
command,
|
|
currentClipRect,
|
|
*config.textGlyphProvider,
|
|
outCompiledDrawData.texturedVertices,
|
|
textRunContext,
|
|
addedVertexCount);
|
|
if (compiled && addedVertexCount > 0u) {
|
|
AppendOrMergeBatch(
|
|
outCompiledDrawData.batches,
|
|
BatchKind::Textured,
|
|
drawListIndex,
|
|
commandIndex,
|
|
firstVertex,
|
|
addedVertexCount,
|
|
currentClipRect,
|
|
textRunContext.texture,
|
|
config.mergeBatchesWithinDrawList);
|
|
} else if (!compiled) {
|
|
unsupported = true;
|
|
} else {
|
|
compiled = false;
|
|
culled = true;
|
|
}
|
|
break;
|
|
}
|
|
case UI::UIDrawCommandType::Image: {
|
|
++outCompiledDrawData.stats.imageCommandCount;
|
|
if (!command.texture.IsValid()) {
|
|
unsupported = true;
|
|
break;
|
|
}
|
|
|
|
const std::size_t firstVertex = outCompiledDrawData.texturedVertices.size();
|
|
compiled = AppendClippedTexturedRect(
|
|
outCompiledDrawData.texturedVertices,
|
|
command.rect,
|
|
command.uvMin.x,
|
|
command.uvMin.y,
|
|
command.uvMax.x,
|
|
command.uvMax.y,
|
|
command.color,
|
|
currentClipRect);
|
|
if (compiled) {
|
|
AppendOrMergeBatch(
|
|
outCompiledDrawData.batches,
|
|
BatchKind::Textured,
|
|
drawListIndex,
|
|
commandIndex,
|
|
firstVertex,
|
|
outCompiledDrawData.texturedVertices.size() - firstVertex,
|
|
currentClipRect,
|
|
command.texture,
|
|
config.mergeBatchesWithinDrawList);
|
|
} else {
|
|
culled = true;
|
|
}
|
|
break;
|
|
}
|
|
case UI::UIDrawCommandType::PushClipRect: {
|
|
++outCompiledDrawData.stats.clipPushCommandCount;
|
|
UI::UIRect nextClipRect = NormalizeRect(command.rect);
|
|
UI::UIRect intersection = {};
|
|
if (command.intersectWithCurrentClip) {
|
|
if (!IntersectRects(currentClipRect, nextClipRect, intersection)) {
|
|
intersection = {};
|
|
}
|
|
} else if (!IntersectRects(surfaceClipRect, nextClipRect, intersection)) {
|
|
intersection = {};
|
|
}
|
|
clipStack.push_back(currentClipRect);
|
|
currentClipRect = intersection;
|
|
compiled = true;
|
|
outCompiledDrawData.stats.maxClipDepth =
|
|
(std::max)(outCompiledDrawData.stats.maxClipDepth, clipStack.size());
|
|
break;
|
|
}
|
|
case UI::UIDrawCommandType::PopClipRect:
|
|
++outCompiledDrawData.stats.clipPopCommandCount;
|
|
if (!clipStack.empty()) {
|
|
currentClipRect = clipStack.back();
|
|
clipStack.pop_back();
|
|
} else {
|
|
currentClipRect = surfaceClipRect;
|
|
++outCompiledDrawData.stats.clipStackUnderflowCount;
|
|
}
|
|
compiled = true;
|
|
break;
|
|
default:
|
|
unsupported = true;
|
|
break;
|
|
}
|
|
|
|
if (compiled) {
|
|
++outCompiledDrawData.stats.compiledCommandCount;
|
|
} else {
|
|
++outCompiledDrawData.stats.skippedCommandCount;
|
|
if (culled) {
|
|
++outCompiledDrawData.stats.culledCommandCount;
|
|
}
|
|
if (unsupported) {
|
|
++outCompiledDrawData.stats.unsupportedCommandCount;
|
|
}
|
|
}
|
|
|
|
++commandIndex;
|
|
}
|
|
++drawListIndex;
|
|
}
|
|
|
|
outCompiledDrawData.stats.colorVertexCount = outCompiledDrawData.colorVertices.size();
|
|
outCompiledDrawData.stats.texturedVertexCount = outCompiledDrawData.texturedVertices.size();
|
|
outCompiledDrawData.stats.triangleCount =
|
|
(outCompiledDrawData.stats.colorVertexCount +
|
|
outCompiledDrawData.stats.texturedVertexCount) /
|
|
3u;
|
|
outCompiledDrawData.stats.batchCount = outCompiledDrawData.batches.size();
|
|
outCompiledDrawData.stats.danglingClipDepth = clipStack.size();
|
|
}
|
|
|
|
} // namespace XCUIBackend
|
|
} // namespace Editor
|
|
} // namespace XCEngine
|