LibWeb+LibGfx: Defer @font-face fetches until a codepoint renders

This commit is contained in:
Johan Dahlin
2026-04-21 22:18:29 +02:00
committed by Andreas Kling
parent 0de26af387
commit acabf765c1
Notes: github-actions[bot] 2026-04-25 15:07:59 +00:00
6 changed files with 108 additions and 10 deletions

View File

@@ -34,13 +34,45 @@ void FontCascadeList::add(NonnullRefPtr<Font const> font, Vector<UnicodeRange> u
} });
}
void FontCascadeList::add_pending_face(Vector<UnicodeRange> unicode_ranges, Function<void()> start_load)
{
if (unicode_ranges.is_empty())
return;
u32 lowest_code_point = 0xFFFFFFFF;
u32 highest_code_point = 0;
for (auto const& range : unicode_ranges) {
lowest_code_point = min(lowest_code_point, range.min_code_point());
highest_code_point = max(highest_code_point, range.max_code_point());
}
m_pending_faces.append(adopt_ref(*new PendingFace(
UnicodeRange { lowest_code_point, highest_code_point },
move(unicode_ranges),
move(start_load))));
}
void FontCascadeList::extend(FontCascadeList const& other)
{
m_fonts.extend(other.m_fonts);
m_pending_faces.extend(other.m_pending_faces);
}
Gfx::Font const& FontCascadeList::font_for_code_point(u32 code_point) const
{
// Walk pending entries first: if this codepoint falls in an unloaded face's
// unicode-range we kick off the fetch and drop the entry — a fallback font that
// happens to cover the codepoint shouldn't prevent the real face from loading.
// FontComputer::clear_computed_font_cache() rebuilds the cascade once the fetch
// completes, so later shapes pick up the loaded face. Run before the ASCII cache
// lookup so a previously-cached codepoint still triggers a newly-added face.
m_pending_faces.remove_all_matching([code_point](auto const& pending) {
if (!pending->covers(code_point))
return false;
pending->start_load();
return true;
});
if (code_point < m_ascii_cache.size()) {
if (auto const* cached = m_ascii_cache[code_point])
return *cached;

View File

@@ -8,6 +8,7 @@
#include <AK/Array.h>
#include <AK/Function.h>
#include <AK/RefCounted.h>
#include <LibGfx/Font/Font.h>
#include <LibGfx/Font/UnicodeRange.h>
@@ -23,7 +24,7 @@ public:
}
size_t size() const { return m_fonts.size(); }
bool is_empty() const { return m_fonts.is_empty() && !m_last_resort_font; }
bool is_empty() const { return m_fonts.is_empty() && m_pending_faces.is_empty() && !m_last_resort_font; }
Font const& first() const { return !m_fonts.is_empty() ? *m_fonts.first().font : *m_last_resort_font; }
template<typename Callback>
@@ -36,6 +37,10 @@ public:
void add(NonnullRefPtr<Font const> font);
void add(NonnullRefPtr<Font const> font, Vector<UnicodeRange> unicode_ranges);
// Register an unloaded face covering `unicode_ranges`. The cascade invokes
// `start_load` the first time a rendered codepoint falls within one of the ranges.
void add_pending_face(Vector<UnicodeRange> unicode_ranges, Function<void()> start_load);
void extend(FontCascadeList const& other);
Gfx::Font const& font_for_code_point(u32 code_point) const;
@@ -53,6 +58,34 @@ public:
Optional<RangeData> range_data;
};
class PendingFace : public RefCounted<PendingFace> {
public:
PendingFace(UnicodeRange enclosing, Vector<UnicodeRange> ranges, Function<void()> start_load)
: m_enclosing_range(enclosing)
, m_unicode_ranges(move(ranges))
, m_start_load(move(start_load))
{
}
bool covers(u32 code_point) const
{
if (!m_enclosing_range.contains(code_point))
return false;
for (auto const& range : m_unicode_ranges) {
if (range.contains(code_point))
return true;
}
return false;
}
void start_load() { m_start_load(); }
private:
UnicodeRange m_enclosing_range;
Vector<UnicodeRange> m_unicode_ranges;
Function<void()> m_start_load;
};
void set_last_resort_font(NonnullRefPtr<Font> font) { m_last_resort_font = move(font); }
void set_system_font_fallback_callback(SystemFontFallbackCallback callback) { m_system_font_fallback_callback = move(callback); }
@@ -67,6 +100,7 @@ public:
private:
RefPtr<Font const> m_last_resort_font;
mutable Vector<Entry> m_fonts;
mutable Vector<NonnullRefPtr<PendingFace>> m_pending_faces;
SystemFontFallbackCallback m_system_font_fallback_callback;
// OPTIMIZATION: Cache of resolved fonts for ASCII code points. Since m_fonts only grows and the cascade returns

View File

@@ -268,8 +268,18 @@ struct FontComputer::MatchingFontCandidate {
auto font_list = Gfx::FontCascadeList::create();
for (auto const& face : it->value) {
if (auto face_fonts = face->font_with_point_size(point_size, variations, shape_features))
if (auto face_fonts = face->font_with_point_size(point_size, variations, shape_features)) {
font_list->extend(*face_fonts);
continue;
}
// Unloaded subset face: surface it as a pending entry so the fetch only
// fires once font_for_code_point() sees a codepoint in its unicode-range.
if (face->has_urls() && face->has_non_default_unicode_range()) {
GC::Root<FontFace> rooted_face(*face);
font_list->add_pending_face(face->unicode_ranges(), [rooted_face = move(rooted_face)] {
const_cast<FontFace&>(*rooted_face).load();
});
}
}
if (font_list->is_empty())
return {};
@@ -771,12 +781,18 @@ void FontComputer::load_fonts_from_sheet(CSSStyleSheet& sheet)
auto font_face = FontFace::create_css_connected(document().realm(), *font_face_rule);
document().fonts()->add_css_connected_font(font_face);
// NB: Load via FontFace::load(), to satisfy this requirement:
// https://drafts.csswg.org/css-font-loading/#font-face-load
// User agents can initiate font loads on their own, whenever they determine that a given font face is
// necessary to render something on the page. When this happens, they must act as if they had called the
// corresponding FontFaces load() method described here.
font_face->load();
if (font_face->has_non_default_unicode_range()) {
// Register for matching, but defer loading until a rendered codepoint
// actually falls in this face's unicode-range.
register_font_face(font_face);
} else {
// NB: Load via FontFace::load(), to satisfy this requirement:
// https://drafts.csswg.org/css-font-loading/#font-face-load
// User agents can initiate font loads on their own, whenever they determine that a given font face is
// necessary to render something on the page. When this happens, they must act as if they had called the
// corresponding FontFaces load() method described here.
font_face->load();
}
}
if (auto* font_feature_values_rule = as_if<CSSFontFeatureValuesRule>(*rule))

View File

@@ -100,6 +100,17 @@ public:
RefPtr<Gfx::FontCascadeList const> font_with_point_size(float point_size, Gfx::FontVariationSettings const&, Gfx::ShapeFeatures const&) const;
Vector<Gfx::UnicodeRange> const& unicode_ranges() const { return m_unicode_ranges; }
bool has_urls() const { return !m_urls.is_empty(); }
bool has_non_default_unicode_range() const
{
if (m_unicode_ranges.size() != 1)
return true;
auto const& range = m_unicode_ranges.first();
return range.min_code_point() != 0 || range.max_code_point() != 0x10FFFF;
}
Bindings::FontFaceLoadStatus status() const { return m_status; }
GC::Ref<WebIDL::Promise> load();

View File

@@ -379,6 +379,11 @@ void FontFaceSet::set_is_pending_on_the_environment(bool is_pending_on_the_envir
// Spec issue: https://github.com/w3c/csswg-drafts/issues/13538#issuecomment-3933951987
if (m_set_entries->set_size() == 0 || (m_is_stuck_on_the_environment && m_loading_fonts.is_empty()))
switch_to_loaded();
// AD-HOC: Also switch when nothing has ever entered the LoadingFonts list — an empty set, or a set whose
// entries all have deferred unicode-ranges that no rendered codepoint matched. Without this the
// ready promise stays pending forever.
else if (m_loading_fonts.is_empty())
switch_to_loaded();
// 2. If the FontFaceSet is stuck on the environment, unmark it as such.
m_is_stuck_on_the_environment = false;

View File

@@ -1,4 +1,4 @@
MultiFont face count: 3
range=U+41 status=loaded
range=U+42 status=error
range=U+43 status=error
range=U+42 status=unloaded
range=U+43 status=unloaded