From 0be802797acea3d7a9504731a27020a55c7d03f1 Mon Sep 17 00:00:00 2001 From: Tim Ledbetter Date: Fri, 8 May 2026 10:34:00 +0100 Subject: [PATCH] LibGfx: Cache font-independent shape data in the font shaping cache The shaping cache previously stored HarfBuzz buffers keyed by string, and `shape_text()` rebuilt a fresh `GlyphRun` on every call. Cache the font-independent shape data instead, so repeated shaping of the same input skips both the HarfBuzz call and the glyph-vector build. --- Libraries/LibGfx/Font/Font.cpp | 16 ++----- Libraries/LibGfx/Font/Font.h | 28 ++++++++++- Libraries/LibGfx/TextLayout.cpp | 84 +++++++++++++++++++++------------ Libraries/LibGfx/TextLayout.h | 5 ++ 4 files changed, 88 insertions(+), 45 deletions(-) diff --git a/Libraries/LibGfx/Font/Font.cpp b/Libraries/LibGfx/Font/Font.cpp index 37c3208645..1179518d05 100644 --- a/Libraries/LibGfx/Font/Font.cpp +++ b/Libraries/LibGfx/Font/Font.cpp @@ -138,23 +138,13 @@ SkFont Font::skia_font(float scale) const return sk_font; } -Font::ShapingCache::~ShapingCache() -{ - clear(); -} +Font::ShapingCache::~ShapingCache() = default; void Font::ShapingCache::clear() { - for (auto& it : map) { - hb_buffer_destroy(it.value); - } map.clear(); - for (auto& buffer : single_ascii_character_map) { - if (buffer) { - hb_buffer_destroy(buffer); - buffer = nullptr; - } - } + for (auto& slot : single_ascii_character_map) + slot = nullptr; } static bool hb_face_has_table(hb_face_t* face, hb_tag_t tag) diff --git a/Libraries/LibGfx/Font/Font.h b/Libraries/LibGfx/Font/Font.h index 87dfcf041f..70f287dab8 100644 --- a/Libraries/LibGfx/Font/Font.h +++ b/Libraries/LibGfx/Font/Font.h @@ -11,6 +11,8 @@ #include #include +#include +#include #include #include #include @@ -23,6 +25,16 @@ struct hb_buffer_t; namespace Gfx { +struct ShapedGlyphs; + +struct ShapingCacheKey { + Utf16String text; + u8 text_type { 0 }; + u32 letter_spacing_bit_pattern { 0 }; + + bool operator==(ShapingCacheKey const&) const = default; +}; + struct FontPixelMetrics { float x_height { 0 }; float advance_of_ascii_zero { 0 }; @@ -79,8 +91,8 @@ public: ShapeFeatures const& features() const { return m_shape_features; } struct ShapingCache { - HashMap map; - hb_buffer_t* single_ascii_character_map[128] { nullptr }; + HashMap> map; + OwnPtr single_ascii_character_map[128]; ~ShapingCache(); void clear(); @@ -110,3 +122,15 @@ private: }; } + +namespace AK { + +template<> +struct Traits : public DefaultTraits { + static unsigned hash(Gfx::ShapingCacheKey const& key) + { + return pair_int_hash(key.text.hash(), pair_int_hash(key.text_type, key.letter_spacing_bit_pattern)); + } +}; + +} diff --git a/Libraries/LibGfx/TextLayout.cpp b/Libraries/LibGfx/TextLayout.cpp index d05dca6897..f89623c9ac 100644 --- a/Libraries/LibGfx/TextLayout.cpp +++ b/Libraries/LibGfx/TextLayout.cpp @@ -7,6 +7,8 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include +#include #include #include #include @@ -195,41 +197,18 @@ static hb_buffer_t* setup_text_shaping(Utf16View const& string, Font const& font return buffer; } -NonnullRefPtr shape_text(FloatPoint baseline_start, float letter_spacing, Utf16View const& string, Font const& font, GlyphRun::TextType text_type) +static NonnullOwnPtr build_origin_relative_shape(Utf16View const& string, Font const& font, GlyphRun::TextType text_type, float letter_spacing) { auto const& metrics = font.pixel_metrics(); - auto& shaping_cache = font.shaping_cache(); + auto* buffer = setup_text_shaping(string, font, text_type); - // FIXME: The cache currently grows unbounded. We should have some limit and LRU mechanism. - auto get_or_create_buffer = [&] -> hb_buffer_t* { - if (string.length_in_code_units() == 1) { - auto code_unit = string.code_unit_at(0); - if (code_unit < 128) { - auto*& cache_slot = shaping_cache.single_ascii_character_map[code_unit]; - if (!cache_slot) { - cache_slot = setup_text_shaping(string, font, text_type); - } - return cache_slot; - } - } - if (auto it = shaping_cache.map.find( - string.hash(), [&](auto& candidate) { return candidate.key == string; }); - it != shaping_cache.map.end()) { - return it->value; - } - auto* buffer = setup_text_shaping(string, font, text_type); - shaping_cache.map.set(Utf16String::from_utf16(string), buffer); - return buffer; - }; - - hb_buffer_t* buffer = get_or_create_buffer(); u32 glyph_count; auto const* glyph_info = hb_buffer_get_glyph_infos(buffer, &glyph_count); auto const* positions = hb_buffer_get_glyph_positions(buffer, &glyph_count); - Vector glyph_run; - glyph_run.ensure_capacity(glyph_count); - FloatPoint point = baseline_start; + Vector glyphs; + glyphs.ensure_capacity(glyph_count); + FloatPoint point; // We track the code unit length rather than just the code unit offset because LibWeb may later collapse glyph runs. // Updating the offset of each glyph gets tricky when handling text direction (LTR/RTL). So rather than doing that, @@ -253,7 +232,7 @@ NonnullRefPtr shape_text(FloatPoint baseline_start, float letter_spaci - FloatPoint { 0, metrics.ascent } + FloatPoint { positions[i].x_offset, positions[i].y_offset } / text_shaping_resolution; - glyph_run.unchecked_append({ + glyphs.unchecked_append({ .position = position, .length_in_code_units = glyph_length_in_code_units(i), .glyph_width = positions[i].x_advance / text_shaping_resolution + letter_spacing, @@ -267,7 +246,52 @@ NonnullRefPtr shape_text(FloatPoint baseline_start, float letter_spaci point.translate_by(letter_spacing, 0); } - return adopt_ref(*new GlyphRun(move(glyph_run), font, text_type, point.x() - baseline_start.x())); + hb_buffer_destroy(buffer); + + return make(move(glyphs), point.x()); +} + +NonnullRefPtr shape_text(FloatPoint baseline_start, float letter_spacing, Utf16View const& string, Font const& font, GlyphRun::TextType text_type) +{ + auto& shaping_cache = font.shaping_cache(); + + auto build_glyph_run = [&](ShapedGlyphs const& shape) -> NonnullRefPtr { + Vector glyphs = shape.glyphs; + if (!baseline_start.is_zero()) { + for (auto& glyph : glyphs) + glyph.position.translate_by(baseline_start); + } + return adopt_ref(*new GlyphRun(move(glyphs), font, text_type, shape.width)); + }; + + // FIXME: The cache currently grows unbounded. We should have some limit and LRU mechanism. + if (string.length_in_code_units() == 1 && letter_spacing == 0.f && text_type == GlyphRun::TextType::Common) { + auto code_unit = string.code_unit_at(0); + if (code_unit < 128) { + auto& cache_slot = shaping_cache.single_ascii_character_map[code_unit]; + if (!cache_slot) + cache_slot = build_origin_relative_shape(string, font, text_type, letter_spacing); + return build_glyph_run(*cache_slot); + } + } + + auto text_type_bits = static_cast(to_underlying(text_type)); + auto letter_spacing_bit_pattern = bit_cast(letter_spacing); + auto key_hash = pair_int_hash(string.hash(), pair_int_hash(text_type_bits, letter_spacing_bit_pattern)); + + if (auto it = shaping_cache.map.find(key_hash, [&](auto const& candidate) { + return candidate.key.text_type == text_type_bits + && candidate.key.letter_spacing_bit_pattern == letter_spacing_bit_pattern + && candidate.key.text == string; + }); + it != shaping_cache.map.end()) { + return build_glyph_run(*it->value); + } + + auto shape = build_origin_relative_shape(string, font, text_type, letter_spacing); + auto run = build_glyph_run(*shape); + shaping_cache.map.set({ Utf16String::from_utf16(string), text_type_bits, letter_spacing_bit_pattern }, move(shape)); + return run; } float measure_text_width(Utf16View const& string, Font const& font, float letter_spacing) diff --git a/Libraries/LibGfx/TextLayout.h b/Libraries/LibGfx/TextLayout.h index 6d2ab169cb..b0ed31412f 100644 --- a/Libraries/LibGfx/TextLayout.h +++ b/Libraries/LibGfx/TextLayout.h @@ -30,6 +30,11 @@ struct DrawGlyph { u32 glyph_id { 0 }; }; +struct ShapedGlyphs { + Vector glyphs; + float width { 0 }; +}; + class GlyphRun : public AtomicRefCounted { public: enum class TextType {