From 55fed7ab8dacd8e2bd04999a3935e4d1bc4ea02c Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Mon, 24 Aug 2026 18:27:55 +0000 Subject: [PATCH] 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. --- crates/gpui/src/platform.rs | 2 + crates/gpui/src/text_system.rs | 16 +++ crates/gpui_wgpu/src/cosmic_text_system.rs | 120 +++++++++++++-------- crates/zed/src/main.rs | 46 ++++++++ 4 files changed, 141 insertions(+), 43 deletions(-) diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index bdafe8cf56..16ca199d90 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -1075,6 +1075,8 @@ pub trait PlatformTextSystem: Send + Sync { fn all_font_names(&self) -> Vec; /// Get the font ID for a font descriptor. fn font_id(&self, descriptor: &Font) -> Result; + /// 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. diff --git a/crates/gpui/src/text_system.rs b/crates/gpui/src/text_system.rs index f3dc95bc57..3346389319 100644 --- a/crates/gpui/src/text_system.rs +++ b/crates/gpui/src/text_system.rs @@ -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. diff --git a/crates/gpui_wgpu/src/cosmic_text_system.rs b/crates/gpui_wgpu/src/cosmic_text_system.rs index 19064867bc..7d28896140 100644 --- a/crates/gpui_wgpu/src/cosmic_text_system.rs +++ b/crates/gpui_wgpu/src/cosmic_text_system.rs @@ -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 { + 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>) -> 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 { diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index b8eba3bf97..e96f97508c 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -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, cx: &mut App) { cx.spawn({