Optimization in RichText and added benchmark.

This commit is contained in:
Martín Lucas Golini
2026-05-21 19:19:47 -03:00
parent ca56d7d935
commit ed333b3839
5 changed files with 153 additions and 24 deletions

View File

@@ -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). */

View File

@@ -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")

View File

@@ -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")

View File

@@ -0,0 +1,85 @@
#include "../tests/unit_tests/utest.hpp"
#include <eepp/graphics/fontfamily.hpp>
#include <eepp/graphics/fonttruetype.hpp>
#include <eepp/graphics/richtext.hpp>
#include <eepp/system/clock.hpp>
#include <eepp/system/filesystem.hpp>
#include <eepp/system/sys.hpp>
#include <eepp/window/engine.hpp>
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()

View File

@@ -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<InlineLeafRef, 32>& leaves,
RichText::InlineFragment::Type type,
size_t& resolvedLeafIndex, size_t& leafIndex ) {
if ( span._leafIndex >= 0 && static_cast<size_t>( span._leafIndex ) < leaves.size() ) {
resolvedLeafIndex = static_cast<size_t>( 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<RichText::InlineFragment>
rebuildFragments( const std::vector<RichText::InlineItem>& inlineItems,
const std::vector<RichText::RenderParagraph>& 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<RichText::InlineItem>& inlineItems ) {
SmallVector<InlineLayoutRun, 32> 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<RichText::InlineItem>& items,
RichText::RenderSpan::InlinePath& path,
SmallVector<InlineLayoutRun, 32>& runs ) {
SmallVector<InlineLayoutRun, 32>& 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<InlineLayoutRun, 32>& runs ) {
SmallVector<InlineLayoutRun, 32>& 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<InlineLayoutRun, 32>& runs ) {
SmallVector<InlineLayoutRun, 32>& 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;