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.
This commit is contained in:
Tim Ledbetter 2026-05-08 10:34:00 +01:00 committed by Jelle Raaijmakers
parent e3b359ccab
commit 0be802797a
4 changed files with 88 additions and 45 deletions

View file

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

View file

@ -11,6 +11,8 @@
#include <AK/AtomicRefCounted.h>
#include <AK/FlyString.h>
#include <AK/HashMap.h>
#include <AK/OwnPtr.h>
#include <AK/RefPtr.h>
#include <AK/Utf16String.h>
#include <LibGfx/Font/Font.h>
@ -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<Utf16String, hb_buffer_t*> map;
hb_buffer_t* single_ascii_character_map[128] { nullptr };
HashMap<ShapingCacheKey, OwnPtr<ShapedGlyphs>> map;
OwnPtr<ShapedGlyphs> single_ascii_character_map[128];
~ShapingCache();
void clear();
@ -110,3 +122,15 @@ private:
};
}
namespace AK {
template<>
struct Traits<Gfx::ShapingCacheKey> : public DefaultTraits<Gfx::ShapingCacheKey> {
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));
}
};
}

View file

@ -7,6 +7,8 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/BitCast.h>
#include <AK/HashFunctions.h>
#include <AK/Utf16String.h>
#include <AK/Utf16View.h>
#include <LibGfx/Font/Font.h>
@ -195,41 +197,18 @@ static hb_buffer_t* setup_text_shaping(Utf16View const& string, Font const& font
return buffer;
}
NonnullRefPtr<GlyphRun> shape_text(FloatPoint baseline_start, float letter_spacing, Utf16View const& string, Font const& font, GlyphRun::TextType text_type)
static NonnullOwnPtr<ShapedGlyphs> 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<DrawGlyph> glyph_run;
glyph_run.ensure_capacity(glyph_count);
FloatPoint point = baseline_start;
Vector<DrawGlyph> 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<GlyphRun> 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<GlyphRun> 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<ShapedGlyphs>(move(glyphs), point.x());
}
NonnullRefPtr<GlyphRun> 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<GlyphRun> {
Vector<DrawGlyph> 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<u8>(to_underlying(text_type));
auto letter_spacing_bit_pattern = bit_cast<u32>(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)

View file

@ -30,6 +30,11 @@ struct DrawGlyph {
u32 glyph_id { 0 };
};
struct ShapedGlyphs {
Vector<DrawGlyph> glyphs;
float width { 0 };
};
class GlyphRun : public AtomicRefCounted<GlyphRun> {
public:
enum class TextType {