From ed333b383936dd1e44c8d744ab86900f23852b46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Thu, 21 May 2026 19:19:47 -0300 Subject: [PATCH] Optimization in RichText and added benchmark. --- include/eepp/graphics/richtext.hpp | 1 + premake4.lua | 8 ++ premake5.lua | 14 ++++ src/benchmarks/inline_layout_benchmark.cpp | 85 ++++++++++++++++++++++ src/eepp/graphics/richtext.cpp | 69 ++++++++++++------ 5 files changed, 153 insertions(+), 24 deletions(-) create mode 100644 src/benchmarks/inline_layout_benchmark.cpp diff --git a/include/eepp/graphics/richtext.hpp b/include/eepp/graphics/richtext.hpp index 884399ae9..98b2f12ed 100644 --- a/include/eepp/graphics/richtext.hpp +++ b/include/eepp/graphics/richtext.hpp @@ -190,6 +190,7 @@ class EE_API RichText : public Drawable { Sizef size; Int64 startCharIndex{ 0 }; Int64 endCharIndex{ 0 }; + Int64 _leafIndex{ -1 }; // O(1) lookup index, assigned during layout }; /** @brief Structure representing a rendered paragraph (line). */ diff --git a/premake4.lua b/premake4.lua index 98763b653..59efd94d5 100644 --- a/premake4.lua +++ b/premake4.lua @@ -1883,6 +1883,14 @@ solution "eepp" includedirs { "src/thirdparty" } build_link_configuration( "eepp-ui-perf-test", true ) + project "eepp-benchmarks" + kind "ConsoleApp" + targetdir("./bin/benchmarks") + language "C++" + files { "src/benchmarks/*.cpp" } + includedirs { "src/thirdparty" } + build_link_configuration( "eepp-benchmarks", true ) + project "eepp-unit_tests" kind "ConsoleApp" targetdir("./bin/unit_tests") diff --git a/premake5.lua b/premake5.lua index a607bc2cc..5e9839ab5 100644 --- a/premake5.lua +++ b/premake5.lua @@ -1754,6 +1754,20 @@ workspace "eepp" incdirs { "src/thirdparty" } build_link_configuration( "eepp-ui-perf-test", true ) + project "eepp-benchmarks" + kind "ConsoleApp" + targetdir(_MAIN_SCRIPT_DIR .. "/bin/benchmarks") + language "C++" + files { "src/benchmarks/*.cpp" } + incdirs { "src/thirdparty" } + build_link_configuration( "eepp-benchmarks", true ) + if table.contains(backends, "SDL2") then + defines { "EE_BACKEND_SDL_ACTIVE", "EE_SDL_VERSION_2" } + end + if table.contains(backends, "SDL3") then + defines { "EE_BACKEND_SDL_ACTIVE", "EE_SDL_VERSION_3" } + end + project "eepp-unit_tests" kind "ConsoleApp" targetdir(_MAIN_SCRIPT_DIR .. "/bin/unit_tests") diff --git a/src/benchmarks/inline_layout_benchmark.cpp b/src/benchmarks/inline_layout_benchmark.cpp new file mode 100644 index 000000000..ad178f14e --- /dev/null +++ b/src/benchmarks/inline_layout_benchmark.cpp @@ -0,0 +1,85 @@ +#include "../tests/unit_tests/utest.hpp" + +#include +#include +#include +#include +#include +#include +#include + +using namespace EE; +using namespace EE::Graphics; +using namespace EE::Window; + +static constexpr int numBoxes = 100; +static constexpr int numSpansPerBox = 20; +static constexpr int layoutIterations = 50; +static constexpr Float maxWidth = 800; + +UTEST( Benchmark, InlineLayout ) { + Engine::instance()->createWindow( WindowSettings( + 800, 600, "bench", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ) ); + FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); + + FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ); + font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" ); + if ( !font->loaded() ) { + Engine::destroySingleton(); + UTEST_PRINT_INFO( "Failed to load font, skipping benchmark" ); + return; + } + FontFamily::loadFromRegular( font ); + + RichText rt; + rt.getFontStyleConfig().Font = font; + rt.getFontStyleConfig().CharacterSize = 12; + rt.setMaxWidth( maxWidth ); + + String lorem = "Lorem ipsum dolor sit amet consectetur adipiscing elit sed do " + "eiusmod tempor incididunt ut labore et dolore magna aliqua "; + String boldText = " **bold** "; + + for ( int b = 0; b < numBoxes; ++b ) { + rt.pushInlineBox( { 0, 0, 0, 0 }, { 4, 0, 4, 0 }, 0, + RichText::BaselineAlignValue( RichText::BaselineAlignment::Top ) ); + for ( int s = 0; s < numSpansPerBox; ++s ) { + rt.addInlineText( lorem, rt.getFontStyleConfig(), { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, 0, + RichText::BaselineAlignValue() ); + if ( s % 3 == 0 ) { + FontStyleConfig boldStyle = rt.getFontStyleConfig(); + boldStyle.Style = Text::Bold; + rt.addInlineText( boldText, boldStyle, { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, 0, + RichText::BaselineAlignValue() ); + } + } + rt.popInlineBox(); + rt.addInlineText( "\n", rt.getFontStyleConfig(), { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, 0, + RichText::BaselineAlignValue() ); + } + + rt.updateLayout(); + size_t lineCount = rt.getLines().size(); + size_t fragmentCount = rt.getInlineFragments().size(); + + Clock clock; + for ( int i = 0; i < layoutIterations; ++i ) { + rt.invalidateLayout(); + rt.updateLayout(); + } + Time elapsed = clock.getElapsedTime(); + + UTEST_PRINT_INFO( String::format( "Boxes: %d, spans/box: %d, iterations: %d", numBoxes, + numSpansPerBox, layoutIterations ) + .c_str() ); + UTEST_PRINT_INFO( + String::format( "Lines: %zu, fragments: %zu", lineCount, fragmentCount ).c_str() ); + UTEST_PRINT_INFO( String::format( "Total: %s", elapsed.toString().c_str() ).c_str() ); + UTEST_PRINT_INFO( + String::format( "Per iteration: %lld us", elapsed.asMicroseconds() / layoutIterations ) + .c_str() ); + + Engine::destroySingleton(); +} + +UTEST_MAIN() diff --git a/src/eepp/graphics/richtext.cpp b/src/eepp/graphics/richtext.cpp index 903eeff1a..0eedd5e67 100644 --- a/src/eepp/graphics/richtext.cpp +++ b/src/eepp/graphics/richtext.cpp @@ -1277,6 +1277,31 @@ class RichTextInlineLayouter { return result; } + // Resolve a render span to its source InlineItem leaf node. The fast + // path uses the sequential _leafIndex assigned during buildLayoutRuns: + // that index directly addresses the leaves vector built by collectLeaves + // because both walk the InlineItem tree in the same depth-first order. + // The fallback via path-based linear search handles legacy spans that + // lack a leaf index. + static const InlineLeafRef* resolveLeaf( const RichText::RenderSpan& span, + const SmallVector& leaves, + RichText::InlineFragment::Type type, + size_t& resolvedLeafIndex, size_t& leafIndex ) { + if ( span._leafIndex >= 0 && static_cast( span._leafIndex ) < leaves.size() ) { + resolvedLeafIndex = static_cast( span._leafIndex ); + const auto& leaf = leaves[resolvedLeafIndex]; + if ( leaf.type == type ) + return &leaf; + } + const InlineLeafRef* leaf = + findLeafByPath( leaves, span.inlinePath, type, resolvedLeafIndex ); + if ( leaf == nullptr && span.inlinePath.empty() ) { + resolvedLeafIndex = leafIndex; + leaf = findNextLeaf( leaves, resolvedLeafIndex, type ); + } + return leaf; + } + static std::vector rebuildFragments( const std::vector& inlineItems, const std::vector& lines ) { @@ -1307,7 +1332,7 @@ class RichTextInlineLayouter { isLastInlineLeafInBox( *ancestor.box, ancestor.path, leaf.path ); auto it = std::find_if( boxFragments.begin(), boxFragments.end(), [&]( const auto& f ) { - return f.lineIndex == lineIndex && sameInlinePath( f.path, ancestor.path ); + return f.lineIndex == lineIndex && f.box == ancestor.box; } ); if ( it == boxFragments.end() ) { InlineBoxFragmentAccumulator acc; @@ -1344,14 +1369,9 @@ class RichTextInlineLayouter { continue; size_t resolvedLeafIndex = 0; - const InlineLeafRef* leaf = findLeafByPath( - leaves, span.inlinePath, RichText::InlineFragment::Type::TextRun, - resolvedLeafIndex ); - if ( leaf == nullptr && span.inlinePath.empty() ) { - resolvedLeafIndex = leafIndex; - leaf = findNextLeaf( leaves, resolvedLeafIndex, - RichText::InlineFragment::Type::TextRun ); - } + const InlineLeafRef* leaf = + resolveLeaf( span, leaves, RichText::InlineFragment::Type::TextRun, + resolvedLeafIndex, leafIndex ); if ( leaf == nullptr ) continue; @@ -1389,14 +1409,9 @@ class RichTextInlineLayouter { continue; size_t resolvedLeafIndex = 0; - const InlineLeafRef* leaf = findLeafByPath( - leaves, span.inlinePath, RichText::InlineFragment::Type::AtomicBox, - resolvedLeafIndex ); - if ( leaf == nullptr && span.inlinePath.empty() ) { - resolvedLeafIndex = leafIndex; - leaf = findNextLeaf( leaves, resolvedLeafIndex, - RichText::InlineFragment::Type::AtomicBox ); - } + const InlineLeafRef* leaf = + resolveLeaf( span, leaves, RichText::InlineFragment::Type::AtomicBox, + resolvedLeafIndex, leafIndex ); if ( leaf == nullptr ) continue; @@ -1439,13 +1454,14 @@ class RichTextInlineLayouter { buildLayoutRuns( const std::vector& inlineItems ) { SmallVector runs; RichText::RenderSpan::InlinePath path; - appendLayoutRuns( inlineItems, path, runs ); + Int64 nextLeafIndex = 0; + appendLayoutRuns( inlineItems, path, runs, nextLeafIndex ); return runs; } static void appendLayoutRuns( const std::vector& items, RichText::RenderSpan::InlinePath& path, - SmallVector& runs ) { + SmallVector& runs, Int64& nextLeafIndex ) { for ( size_t i = 0; i < items.size(); ++i ) { path.push_back( i ); const auto& item = items[i]; @@ -1454,11 +1470,11 @@ class RichTextInlineLayouter { if ( box.children.empty() ) appendEmptyBoxLayoutRun( path, runs ); else - appendLayoutRuns( box.children, path, runs ); + appendLayoutRuns( box.children, path, runs, nextLeafIndex ); } else if ( item.isTextRun() ) { - appendTextLayoutRun( item.asTextRun(), path, runs ); + appendTextLayoutRun( item.asTextRun(), path, runs, nextLeafIndex ); } else { - appendAtomicLayoutRun( item.asAtomicBox(), path, runs ); + appendAtomicLayoutRun( item.asAtomicBox(), path, runs, nextLeafIndex ); } path.pop_back(); } @@ -1474,7 +1490,8 @@ class RichTextInlineLayouter { static void appendTextLayoutRun( const RichText::InlineItem::TextRun& textRun, const RichText::RenderSpan::InlinePath& path, - SmallVector& runs ) { + SmallVector& runs, + Int64& nextLeafIndex ) { InlineLayoutRun run; run.payload.type = RichText::RenderSpan::Type::Text; run.payload.text = textRun.text; @@ -1484,12 +1501,14 @@ class RichTextInlineLayouter { run.payload.baselineAlign = textRun.baselineAlign; run.payload.suppressBackground = textRun.suppressBackground; run.payload.inlinePath = path; + run.payload._leafIndex = nextLeafIndex++; runs.push_back( std::move( run ) ); } static void appendAtomicLayoutRun( const RichText::InlineItem::AtomicBox& box, const RichText::RenderSpan::InlinePath& path, - SmallVector& runs ) { + SmallVector& runs, + Int64& nextLeafIndex ) { InlineLayoutRun run; run.payload.type = box.drawable ? RichText::RenderSpan::Type::Drawable : RichText::RenderSpan::Type::AtomicBox; @@ -1501,6 +1520,7 @@ class RichTextInlineLayouter { run.payload.isLineBreak = box.isLineBreak; run.payload.baselineAlign = box.baselineAlign; run.payload.inlinePath = path; + run.payload._leafIndex = nextLeafIndex++; runs.push_back( std::move( run ) ); } @@ -1603,6 +1623,7 @@ class RichTextInlineLayouter { renderSpan.baselineAlign = payload.baselineAlign; renderSpan.suppressBackground = payload.suppressBackground; renderSpan.inlinePath = payload.inlinePath; + renderSpan._leafIndex = payload._leafIndex; renderSpan.position = { curX, 0 }; renderSpan.size = Sizef( spanWidth, height ); renderSpan.startCharIndex = curCharIdx;