Prewarm Linux font match caches (#63158)

Prewarming cosmic-text font cache completely eliminated expensive
`get_font_matches` calls and thus reducing the time it takes to call
`cosmic_text::shape::ShapeLine::new`.

Release Notes:

- Improved rendering performance on Linux by prewarming font match
caches.
This commit is contained in:
Jakub Konka authored and GitHub committed 2026-08-24 18:27:55 +00:00
1 parent cff8de9239
commit 55fed7ab8d
4 files changed
+141 -43

No files matched your search

+2
View File
@@ -1075,6 +1075,8 @@ pub trait PlatformTextSystem: Send + Sync {
fn all_font_names(&self) -> Vec<String>;
/// Get the font ID for a font descriptor.
fn font_id(&self, descriptor: &Font) -> Result<FontId>;
/// Prewarm any system font caches needed to shape text.
fn prewarm_fonts(&self, _font_ids: &[FontId]) {}
/// Get metrics for a font.
fn font_metrics(&self, font_id: FontId) -> FontMetrics;
/// Get typographic bounds for a glyph.
+16
View File
@@ -165,6 +165,22 @@ impl TextSystem {
);
}
/// Prewarm any system font caches needed to shape text.
///
/// This may be expensive, so callers should generally invoke it on a
/// background executor. Missing entries are still populated on demand by
/// the normal shaping path.
pub fn prewarm_fonts(&self, fonts: &[Font]) {
let mut font_ids = SmallVec::<[FontId; 8]>::new();
for font in fonts {
let font_id = self.resolve_font(font);
if !font_ids.contains(&font_id) {
font_ids.push(font_id);
}
}
self.platform_text_system.prewarm_fonts(&font_ids);
}
/// Get the bounding box for the given font and font size.
/// A font's bounding box is the smallest rectangle that could enclose all glyphs
/// in the font. superimposed over one another.
+77 -43
View File
@@ -2,7 +2,7 @@ use anyhow::{Context as _, Ok, Result};
use collections::HashMap;
use cosmic_text::{
Attrs, AttrsList, Ellipsize, Family, Font as CosmicTextFont,
FontFeatures as CosmicFontFeatures, FontSystem, ShapeBuffer, ShapeLine,
FontFeatures as CosmicFontFeatures, FontSystem, ShapeBuffer, ShapeLine, Stretch, Style, Weight,
};
use gpui::{
Bounds, DevicePixels, Font, FontFallbacks, FontFeatures, FontId, FontMetrics, FontRun, GlyphId,
@@ -61,6 +61,27 @@ struct LoadedFont {
user_fallback_chain: Arc<[(FontId, SharedString)]>,
}
struct FontMatchProperties {
primary_family_name: SharedString,
stretch: Stretch,
style: Style,
weight: Weight,
features: CosmicFontFeatures,
fallback_chain: Arc<[(FontId, SharedString)]>,
}
impl FontMatchProperties {
fn attributes<'a>(&'a self, font_id: FontId, family_name: &'a str) -> Attrs<'a> {
Attrs::new()
.metadata(font_id.0)
.family(Family::Name(family_name))
.stretch(self.stretch)
.style(self.style)
.weight(self.weight)
.font_features(self.features.clone())
}
}
impl CosmicTextSystem {
pub fn new(system_font_fallback: &str) -> Self {
let font_system = FontSystem::new();
@@ -132,6 +153,10 @@ impl PlatformTextSystem for CosmicTextSystem {
Ok(candidates[ix])
}
fn prewarm_fonts(&self, font_ids: &[FontId]) {
self.0.write().prewarm_fonts(font_ids);
}
fn font_metrics(&self, font_id: FontId) -> FontMetrics {
let metrics = self
.0
@@ -208,6 +233,43 @@ impl CosmicTextSystemState {
&self.loaded_fonts[font_id.0]
}
fn font_match_properties(&self, font_id: FontId) -> Option<FontMatchProperties> {
let loaded_font = self.loaded_font(font_id);
let Some(face) = self.font_system.db().face(loaded_font.font.id()) else {
log::warn!("font face not found in database for font_id {:?}", font_id);
return None;
};
let Some(first_family) = face.families.first() else {
log::warn!("font face has no family names for font_id {:?}", font_id);
return None;
};
Some(FontMatchProperties {
primary_family_name: first_family.0.clone().into(),
stretch: face.stretch,
style: face.style,
weight: face.weight,
features: loaded_font.features.clone(),
fallback_chain: Arc::clone(&loaded_font.user_fallback_chain),
})
}
fn prewarm_fonts(&mut self, font_ids: &[FontId]) {
for &font_id in font_ids {
let Some(properties) = self.font_match_properties(font_id) else {
continue;
};
let primary_attributes =
properties.attributes(font_id, &properties.primary_family_name);
self.font_system.get_font_matches(&primary_attributes);
for (fallback_id, fallback_name) in &*properties.fallback_chain {
let fallback_attributes = properties.attributes(*fallback_id, fallback_name);
self.font_system.get_font_matches(&fallback_attributes);
}
}
}
#[profiling::function]
fn add_fonts(&mut self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
let db = self.font_system.db_mut();
@@ -552,54 +614,19 @@ impl CosmicTextSystemState {
for run in font_runs {
let run_end = offs + run.len;
let loaded_font = self.loaded_font(run.font_id);
let Some(face) = self.font_system.db().face(loaded_font.font.id()) else {
log::warn!(
"font face not found in database for font_id {:?}",
run.font_id
);
offs = run_end;
continue;
};
let Some(first_family) = face.families.first() else {
log::warn!(
"font face has no family names for font_id {:?}",
run.font_id
);
let Some(properties) = self.font_match_properties(run.font_id) else {
offs = run_end;
continue;
};
let primary_family_name: SharedString = first_family.0.clone().into();
let primary_stretch = face.stretch;
let primary_style = face.style;
let primary_weight = face.weight;
let primary_features = loaded_font.features.clone();
let fallback_chain = Arc::clone(&loaded_font.user_fallback_chain);
// build one `Attrs` per slot up front. each clone of span attrs
// would otherwise re-allocate the `font_features` Vec.
let primary_attrs = Attrs::new()
.metadata(run.font_id.0)
.family(Family::Name(&primary_family_name))
.stretch(primary_stretch)
.style(primary_style)
.weight(primary_weight)
.font_features(primary_features.clone());
let fallback_attrs: SmallVec<[Attrs<'_>; 4]> = fallback_chain
let primary_attrs = properties.attributes(run.font_id, &properties.primary_family_name);
let fallback_attrs: SmallVec<[Attrs<'_>; 4]> = properties
.fallback_chain
.iter()
.map(|(fb_id, fb_name)| {
Attrs::new()
.metadata(fb_id.0)
.family(Family::Name(fb_name))
.stretch(primary_stretch)
.style(primary_style)
.weight(primary_weight)
.font_features(primary_features.clone())
})
.map(|(font_id, family_name)| properties.attributes(*font_id, family_name))
.collect();
let spans = if fallback_chain.is_empty() {
let spans = if properties.fallback_chain.is_empty() {
let mut spans = SmallVec::<[RunSpan; 4]>::new();
spans.push(RunSpan {
start: offs,
@@ -611,7 +638,14 @@ impl CosmicTextSystemState {
} else {
let loaded_fonts = &self.loaded_fonts;
let covers = |id: FontId, ch: char| charmap_covers(loaded_fonts, id, ch);
compute_run_spans(text, offs, run.len, run.font_id, &fallback_chain, &covers)
compute_run_spans(
text,
offs,
run.len,
run.font_id,
&properties.fallback_chain,
&covers,
)
};
for span in spans {
+46
View File
@@ -732,6 +732,8 @@ fn main() {
dev_container::init(cx);
load_embedded_fonts(cx);
#[cfg(target_os = "linux")]
prewarm_fonts(cx);
editor::init(cx);
image_viewer::init(cx);
@@ -1855,6 +1857,50 @@ fn load_embedded_fonts(cx: &App) {
.unwrap();
}
#[cfg(target_os = "linux")]
fn prewarm_fonts(cx: &mut App) {
let theme_settings = theme::theme_settings(cx);
let ui_font = theme_settings.ui_font(cx).clone();
let buffer_font = theme_settings.buffer_font(cx).clone();
let mut fonts = vec![ui_font.clone(), buffer_font.clone()];
let mut add_variant = |base_font: &gpui::Font, weight, style| {
let mut font = base_font.clone();
font.weight = weight;
font.style = style;
fonts.push(font);
};
for weight in [
gpui::FontWeight::MEDIUM,
gpui::FontWeight::SEMIBOLD,
gpui::FontWeight::BOLD,
] {
add_variant(&ui_font, weight, gpui::FontStyle::Normal);
}
add_variant(&ui_font, gpui::FontWeight::NORMAL, gpui::FontStyle::Italic);
add_variant(
&buffer_font,
gpui::FontWeight::BOLD,
gpui::FontStyle::Normal,
);
add_variant(
&buffer_font,
gpui::FontWeight::NORMAL,
gpui::FontStyle::Italic,
);
add_variant(
&buffer_font,
gpui::FontWeight::BOLD,
gpui::FontStyle::Italic,
);
let text_system = cx.text_system().clone();
cx.background_spawn(async move {
text_system.prewarm_fonts(&fonts);
})
.detach();
}
/// Spawns a background task to load the user themes from the themes directory.
fn load_user_themes_in_background(fs: Arc<dyn fs::Fs>, cx: &mut App) {
cx.spawn({