From 783fe921aac285cdf969c3222e3ed5289550dc43 Mon Sep 17 00:00:00 2001 From: Andy Terra <152812+airstrike@users.noreply.github.com> Date: Sun, 7 Jun 2026 14:20:45 -0400 Subject: [PATCH] Invalidate buffer cache when fonts are loaded The text BufferCache keys on (content, font, size, ...) but does not account for fonts loaded after a buffer is first shaped, because it assumes all fonts are loaded at startup. As a result, a buffer using a given missing font but shaped with a fallback font would persist in the cache even after said font was loaded, because the key hash hadn't changed. Fix: pass the FontSystem version into `Cache::allocate` so when the version advances after a font is loaded, the cache is invalidated and buffers are reshaped correctly with the right font. --- graphics/src/text/cache.rs | 76 ++++++++++++++++++++++++++++++++++++++ tiny_skia/src/text.rs | 3 +- wgpu/src/text.rs | 2 + 3 files changed, 80 insertions(+), 1 deletion(-) diff --git a/graphics/src/text/cache.rs b/graphics/src/text/cache.rs index e63c3a1cc7..3596de6aeb 100644 --- a/graphics/src/text/cache.rs +++ b/graphics/src/text/cache.rs @@ -12,6 +12,7 @@ pub struct Cache { entries: FxHashMap, aliases: FxHashMap, recently_used: FxHashSet, + version: text::Version, } impl Cache { @@ -30,7 +31,15 @@ impl Cache { &mut self, font_system: &mut cosmic_text::FontSystem, key: Key<'_>, + version: text::Version, ) -> (KeyHash, &mut Entry) { + if version != self.version { + self.entries.clear(); + self.aliases.clear(); + self.recently_used.clear(); + self.version = version; + } + let hash = key.hash(FxHasher::default()); if let Some(hash) = self.aliases.get(&hash) { @@ -149,3 +158,70 @@ pub struct Entry { /// The minimum bounds of the text. pub min_bounds: Size, } + +#[cfg(test)] +mod tests { + use super::*; + use crate::core::text::{Alignment, Ellipsis, Shaping, Wrapping}; + + use std::sync::Arc; + + // The icon font is loaded up front so the font system can always shape. + const ICED_ICONS: &[u8] = include_bytes!("../../fonts/Iced-Icons.ttf"); + // Fira Sans is the font the text asks for, loaded only later. + const FIRA_SANS: &[u8] = include_bytes!("../../fonts/FiraSans-Regular.ttf"); + + fn key() -> Key<'static> { + Key { + content: "Hello, world!", + size: 16.0, + line_height: 20.0, + font: Font::new("Fira Sans"), + bounds: Size::new(1000.0, 1000.0), + shaping: Shaping::Advanced, + align_x: Alignment::default(), + wrapping: Wrapping::default(), + ellipsis: Ellipsis::default(), + } + } + + #[test] + fn reshapes_text_when_its_font_is_loaded() { + // A font system that can shape, but does not yet know about Fira Sans. + let mut db = cosmic_text::fontdb::Database::new(); + let _ = db.load_font_source(cosmic_text::fontdb::Source::Binary(Arc::new( + ICED_ICONS.to_vec(), + ))); + let mut font_system = + cosmic_text::FontSystem::new_with_locale_and_db("en-US".to_owned(), db); + let mut cache = Cache::new(); + let version = text::Version::default(); + + // Its font missing, the text falls back to whatever is available. + let (_, entry) = cache.allocate(&mut font_system, key(), version); + let unshaped = entry.min_bounds; + + // Re-requesting the same text without loading a font returns the same + // buffer. Nothing changed, so neither did its shape. + let (_, entry) = cache.allocate(&mut font_system, key(), version); + assert_eq!( + entry.min_bounds, unshaped, + "text was reshaped without a font being loaded" + ); + + // Loading Fira Sans advances the font system version. + let _ = font_system + .db_mut() + .load_font_source(cosmic_text::fontdb::Source::Binary(Arc::new( + FIRA_SANS.to_vec(), + ))); + let version = text::Version(version.0 + 1); + + // The text is now reshaped against Fira Sans, so its buffer changes. + let (_, entry) = cache.allocate(&mut font_system, key(), version); + assert_ne!( + entry.min_bounds, unshaped, + "text was not reshaped after its font was loaded" + ); + } +} diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs index b9e3eb46cc..09395c9b5a 100644 --- a/tiny_skia/src/text.rs +++ b/tiny_skia/src/text.rs @@ -110,6 +110,7 @@ impl Pipeline { let line_height = f32::from(line_height); let mut font_system = font_system().write().expect("Write font system"); + let version = font_system.version(); let font_system = font_system.raw(); let key = cache::Key { @@ -124,7 +125,7 @@ impl Pipeline { align_x, }; - let (_, entry) = self.cache.get_mut().allocate(font_system, key); + let (_, entry) = self.cache.get_mut().allocate(font_system, key, version); let width = entry.min_bounds.width; let height = entry.min_bounds.height; diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index a3df7c7a5c..af006eea2f 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -442,6 +442,7 @@ fn prepare( layer_transformation: Transformation, ) -> Result<(), cryoglyph::PrepareError> { let mut font_system = font_system().write().expect("Write font system"); + let version = font_system.version(); let font_system = font_system.raw(); enum Allocation { @@ -484,6 +485,7 @@ fn prepare( wrapping: *wrapping, ellipsis: *ellipsis, }, + version, ); Some(Allocation::Cache(key))