#include "XCUIBackend/XCUIRHICommandCompiler.h" #include #include 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& 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& 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& 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& 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& 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(*cursor++); if (lead < 0x80u) { outCodepoint = static_cast(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(lead & 0x1Fu) : continuationCount == 2 ? static_cast(lead & 0x0Fu) : static_cast(lead & 0x07u); for (int index = 0; index < continuationCount; ++index) { const unsigned char continuation = static_cast(*cursor); if ((continuation & 0xC0u) != 0x80u) { outCodepoint = 0xFFFDu; return true; } ++cursor; codepoint = (codepoint << 6u) | static_cast(continuation & 0x3Fu); } outCodepoint = codepoint; return true; } void AppendOrMergeBatch( std::vector& 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(lastBatch.firstVertex + lastBatch.vertexCount) == firstVertex) { lastBatch.vertexCount += static_cast(vertexCount); lastBatch.commandCount += 1u; return; } } Compiler::Batch batch = {}; batch.kind = kind; batch.drawListIndex = static_cast(drawListIndex); batch.firstCommandIndex = static_cast(commandIndex); batch.commandCount = 1u; batch.firstVertex = static_cast(firstVertex); batch.vertexCount = static_cast(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& 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 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