From 68ce4c35cdbb2d61aa499c8ade279222cff923e0 Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl Date: Fri, 8 May 2026 09:52:49 +0200 Subject: [PATCH 1/6] glifo: Add basic sanity unit tests for atlas caching --- glifo/src/glyph.rs | 223 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 215 insertions(+), 8 deletions(-) diff --git a/glifo/src/glyph.rs b/glifo/src/glyph.rs index 1ebfe0f833..7e71088938 100644 --- a/glifo/src/glyph.rs +++ b/glifo/src/glyph.rs @@ -1592,14 +1592,6 @@ impl OutlinePen for OutlinePath { /// the need for updates only to align Skrifa versions. pub type NormalizedCoord = i16; -#[cfg(test)] -mod tests { - use super::*; - - const _NORMALISED_COORD_SIZE_MATCHES: () = - assert!(size_of::() == size_of::()); -} - /// Caches used for glyph rendering. /// /// Contains renderer-agnostic caches (outline paths, hinting instances) @@ -2052,3 +2044,218 @@ fn x_y_advances(transform: &Affine) -> (Vec2, Vec2) { Vec2::new(y_advance.x, y_advance.y), ) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::atlas::{AtlasConfig, AtlasPaint}; + use crate::interface::{DrawSink, GlyphRenderer}; + use crate::peniko::BlendMode; + use crate::peniko::Blob; + use crate::peniko::color::{AlphaColor, Srgb}; + use alloc::sync::Arc; + use vello_common::paint::{Image, ImageId, ImageSource, Tint}; + + const _NORMALISED_COORD_SIZE_MATCHES: () = + assert!(size_of::() == size_of::()); + + const ROBOTO_FONT: &[u8] = include_bytes!("../../examples/assets/roboto/Roboto-Regular.ttf"); + const NOTO_COLR_FONT: &[u8] = + include_bytes!("../../examples/assets/noto_color_emoji/NotoColorEmoji-Subset.ttf"); + #[cfg(feature = "png")] + const NOTO_CBTF_FONT: &[u8] = + include_bytes!("../../examples/assets/noto_color_emoji/NotoColorEmoji-CBTF-Subset.ttf"); + + #[derive(Clone, Copy)] + enum TestGlyphKind { + Outline, + Colr, + #[cfg(feature = "png")] + Bitmap, + } + + #[derive(Default)] + struct NoopRenderer; + + struct TestResources { + renderer: NoopRenderer, + prep_cache: GlyphPrepCache, + glyph_atlas: GlyphAtlas, + image_cache: ImageCache, + } + + impl Default for TestResources { + fn default() -> Self { + Self { + renderer: NoopRenderer, + prep_cache: GlyphPrepCache::default(), + glyph_atlas: GlyphAtlas::default(), + image_cache: ImageCache::new_with_config(AtlasConfig { + atlas_size: (512, 512), + ..AtlasConfig::default() + }), + } + } + } + + impl DrawSink for NoopRenderer { + fn set_transform(&mut self, _t: Affine) {} + + fn set_paint(&mut self, _paint: AtlasPaint) {} + + fn set_paint_transform(&mut self, _t: Affine) {} + + fn fill_path(&mut self, _path: &BezPath) {} + + fn fill_rect(&mut self, _rect: &Rect) {} + + fn push_clip_layer(&mut self, _clip: &BezPath) {} + + fn push_blend_layer(&mut self, _blend_mode: BlendMode) {} + + fn pop_layer(&mut self) {} + + fn width(&self) -> u16 { + 512 + } + + fn height(&self) -> u16 { + 512 + } + } + + impl GlyphRenderer for NoopRenderer { + type SavedState = (); + + fn save_state(&mut self) -> Self::SavedState {} + + fn restore_state(&mut self, _state: Self::SavedState) {} + + fn stroke_path(&mut self, _path: &BezPath) {} + + fn set_paint_image(&mut self, _image: Image) {} + + fn set_tint(&mut self, _tint: Option) {} + + fn get_context_color(&self) -> AlphaColor { + BLACK + } + + fn atlas_image_source(&self, atlas_slot: &AtlasSlot) -> ImageSource { + ImageSource::opaque_id(ImageId::new(atlas_slot.page_index)) + } + + fn atlas_paint_transform(&self, atlas_slot: &AtlasSlot) -> Affine { + Affine::translate((-(atlas_slot.x as f64), -(atlas_slot.y as f64))) + } + } + + fn test_font(kind: TestGlyphKind) -> FontData { + let bytes = match kind { + TestGlyphKind::Outline => ROBOTO_FONT, + TestGlyphKind::Colr => NOTO_COLR_FONT, + #[cfg(feature = "png")] + TestGlyphKind::Bitmap => NOTO_CBTF_FONT, + }; + FontData::new(Blob::new(Arc::new(bytes)), 0) + } + + fn test_glyph(font: &FontData, kind: TestGlyphKind) -> Glyph { + let ch = match kind { + TestGlyphKind::Outline => 'H', + TestGlyphKind::Colr => '✅', + #[cfg(feature = "png")] + TestGlyphKind::Bitmap => '✅', + }; + let glyph_id = font.as_skrifa().charmap().map(ch).unwrap(); + Glyph { + id: glyph_id.to_u32(), + x: 0.0, + y: 0.0, + } + } + + fn draw_test_glyph( + font: &FontData, + glyph: Glyph, + atlas_cache_enabled: bool, + resources: &mut TestResources, + ) { + let atlas_cacher = if atlas_cache_enabled { + AtlasCacher::Enabled(&mut resources.glyph_atlas, &mut resources.image_cache) + } else { + AtlasCacher::Disabled + }; + + GlyphRun { + font: font.clone(), + font_size: 20.0, + transform: Affine::translate((0.0, 20.0)), + glyph_transform: None, + normalized_coords: &[], + hint: false, + } + .build( + core::iter::once(glyph), + resources.prep_cache.as_mut(), + atlas_cacher, + ) + .fill_glyphs(&mut resources.renderer); + } + + fn ensure_cache(kind: TestGlyphKind) { + let font = test_font(kind); + let glyph = test_glyph(&font, kind); + let mut resources = TestResources::default(); + + draw_test_glyph(&font, glyph, true, &mut resources); + + assert_eq!(resources.glyph_atlas.len(), 1); + assert_eq!(resources.glyph_atlas.cache_hits(), 0); + assert!(resources.glyph_atlas.cache_misses() > 0); + } + + fn ensure_no_cache(kind: TestGlyphKind) { + let font = test_font(kind); + let glyph = test_glyph(&font, kind); + let mut resources = TestResources::default(); + + draw_test_glyph(&font, glyph, false, &mut resources); + + assert_eq!(resources.glyph_atlas.len(), 0); + assert_eq!(resources.glyph_atlas.cache_hits(), 0); + assert_eq!(resources.glyph_atlas.cache_misses(), 0); + } + + #[test] + fn outline_glyph_is_cached_when_atlas_cache_is_enabled() { + ensure_cache(TestGlyphKind::Outline); + } + + #[test] + fn outline_glyph_is_not_cached_when_atlas_cache_is_disabled() { + ensure_no_cache(TestGlyphKind::Outline); + } + + #[test] + fn colr_glyph_is_cached_when_atlas_cache_is_enabled() { + ensure_cache(TestGlyphKind::Colr); + } + + #[test] + fn colr_glyph_is_not_cached_when_atlas_cache_is_disabled() { + ensure_no_cache(TestGlyphKind::Colr); + } + + #[cfg(feature = "png")] + #[test] + fn bitmap_glyph_is_cached_when_atlas_cache_is_enabled() { + ensure_cache(TestGlyphKind::Bitmap); + } + + #[cfg(feature = "png")] + #[test] + fn bitmap_glyph_is_not_cached_when_atlas_cache_is_disabled() { + ensure_no_cache(TestGlyphKind::Bitmap); + } +} From ad3d3e9108f27c9d5c5d102a56915f038abb8add Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl Date: Thu, 21 May 2026 22:04:39 +0200 Subject: [PATCH 2/6] Address review part 1 --- glifo/src/glyph.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/glifo/src/glyph.rs b/glifo/src/glyph.rs index 7e71088938..80904b6a94 100644 --- a/glifo/src/glyph.rs +++ b/glifo/src/glyph.rs @@ -2212,6 +2212,16 @@ mod tests { assert_eq!(resources.glyph_atlas.len(), 1); assert_eq!(resources.glyph_atlas.cache_hits(), 0); + // Note that we are checking > 0 instead of == 1 here because + // COLR actually has slightly different cache access behavior than + // normal outlines (we first perform a speculative check for normal outlines + // and then get a second cache miss for the actual COLR glyph). + assert!(resources.glyph_atlas.cache_misses() > 0); + + draw_test_glyph(&font, glyph, true, &mut resources); + + assert_eq!(resources.glyph_atlas.len(), 1); + assert_eq!(resources.glyph_atlas.cache_hits(), 1); assert!(resources.glyph_atlas.cache_misses() > 0); } @@ -2225,6 +2235,12 @@ mod tests { assert_eq!(resources.glyph_atlas.len(), 0); assert_eq!(resources.glyph_atlas.cache_hits(), 0); assert_eq!(resources.glyph_atlas.cache_misses(), 0); + + draw_test_glyph(&font, glyph, false, &mut resources); + + assert_eq!(resources.glyph_atlas.len(), 0); + assert_eq!(resources.glyph_atlas.cache_hits(), 0); + assert_eq!(resources.glyph_atlas.cache_misses(), 0); } #[test] From 4e96d56e10e659205968e0e458c66725e878dfcd Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl Date: Thu, 21 May 2026 22:09:14 +0200 Subject: [PATCH 3/6] Address review part 2 --- glifo/src/glyph.rs | 45 ++++++++++++++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/glifo/src/glyph.rs b/glifo/src/glyph.rs index 80904b6a94..b4fffcb636 100644 --- a/glifo/src/glyph.rs +++ b/glifo/src/glyph.rs @@ -2179,6 +2179,7 @@ mod tests { font: &FontData, glyph: Glyph, atlas_cache_enabled: bool, + style: Style, resources: &mut TestResources, ) { let atlas_cacher = if atlas_cache_enabled { @@ -2187,7 +2188,7 @@ mod tests { AtlasCacher::Disabled }; - GlyphRun { + let mut run = GlyphRun { font: font.clone(), font_size: 20.0, transform: Affine::translate((0.0, 20.0)), @@ -2199,16 +2200,20 @@ mod tests { core::iter::once(glyph), resources.prep_cache.as_mut(), atlas_cacher, - ) - .fill_glyphs(&mut resources.renderer); + ); + + match style { + Style::Fill => run.fill_glyphs(&mut resources.renderer), + Style::Stroke => run.stroke_glyphs(&mut resources.renderer), + } } - fn ensure_cache(kind: TestGlyphKind) { + fn ensure_cache(kind: TestGlyphKind, style: Style) { let font = test_font(kind); let glyph = test_glyph(&font, kind); let mut resources = TestResources::default(); - draw_test_glyph(&font, glyph, true, &mut resources); + draw_test_glyph(&font, glyph, true, style, &mut resources); assert_eq!(resources.glyph_atlas.len(), 1); assert_eq!(resources.glyph_atlas.cache_hits(), 0); @@ -2218,25 +2223,25 @@ mod tests { // and then get a second cache miss for the actual COLR glyph). assert!(resources.glyph_atlas.cache_misses() > 0); - draw_test_glyph(&font, glyph, true, &mut resources); + draw_test_glyph(&font, glyph, true, style, &mut resources); assert_eq!(resources.glyph_atlas.len(), 1); assert_eq!(resources.glyph_atlas.cache_hits(), 1); assert!(resources.glyph_atlas.cache_misses() > 0); } - fn ensure_no_cache(kind: TestGlyphKind) { + fn ensure_no_cache(kind: TestGlyphKind, style: Style, atlas_cache_enabled: bool) { let font = test_font(kind); let glyph = test_glyph(&font, kind); let mut resources = TestResources::default(); - draw_test_glyph(&font, glyph, false, &mut resources); + draw_test_glyph(&font, glyph, atlas_cache_enabled, style, &mut resources); assert_eq!(resources.glyph_atlas.len(), 0); assert_eq!(resources.glyph_atlas.cache_hits(), 0); assert_eq!(resources.glyph_atlas.cache_misses(), 0); - draw_test_glyph(&font, glyph, false, &mut resources); + draw_test_glyph(&font, glyph, atlas_cache_enabled, style, &mut resources); assert_eq!(resources.glyph_atlas.len(), 0); assert_eq!(resources.glyph_atlas.cache_hits(), 0); @@ -2245,33 +2250,43 @@ mod tests { #[test] fn outline_glyph_is_cached_when_atlas_cache_is_enabled() { - ensure_cache(TestGlyphKind::Outline); + ensure_cache(TestGlyphKind::Outline, Style::Fill); } #[test] fn outline_glyph_is_not_cached_when_atlas_cache_is_disabled() { - ensure_no_cache(TestGlyphKind::Outline); + ensure_no_cache(TestGlyphKind::Outline, Style::Fill, false); + } + + #[test] + fn stroked_outline_glyph_is_not_cached_when_atlas_cache_is_enabled() { + ensure_no_cache(TestGlyphKind::Outline, Style::Stroke, true); + } + + #[test] + fn stroked_outline_glyph_is_not_cached_when_atlas_cache_is_disabled() { + ensure_no_cache(TestGlyphKind::Outline, Style::Stroke, false); } #[test] fn colr_glyph_is_cached_when_atlas_cache_is_enabled() { - ensure_cache(TestGlyphKind::Colr); + ensure_cache(TestGlyphKind::Colr, Style::Fill); } #[test] fn colr_glyph_is_not_cached_when_atlas_cache_is_disabled() { - ensure_no_cache(TestGlyphKind::Colr); + ensure_no_cache(TestGlyphKind::Colr, Style::Fill, false); } #[cfg(feature = "png")] #[test] fn bitmap_glyph_is_cached_when_atlas_cache_is_enabled() { - ensure_cache(TestGlyphKind::Bitmap); + ensure_cache(TestGlyphKind::Bitmap, Style::Fill); } #[cfg(feature = "png")] #[test] fn bitmap_glyph_is_not_cached_when_atlas_cache_is_disabled() { - ensure_no_cache(TestGlyphKind::Bitmap); + ensure_no_cache(TestGlyphKind::Bitmap, Style::Fill, false); } } From bbd999215b17d21c7386e472f3b50dc7525e861b Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl Date: Thu, 21 May 2026 22:11:21 +0200 Subject: [PATCH 4/6] Add comment --- glifo/src/glyph.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/glifo/src/glyph.rs b/glifo/src/glyph.rs index b4fffcb636..55788fa6e6 100644 --- a/glifo/src/glyph.rs +++ b/glifo/src/glyph.rs @@ -2258,6 +2258,7 @@ mod tests { ensure_no_cache(TestGlyphKind::Outline, Style::Fill, false); } + // This might change in the future, but for now they are not cached. #[test] fn stroked_outline_glyph_is_not_cached_when_atlas_cache_is_enabled() { ensure_no_cache(TestGlyphKind::Outline, Style::Stroke, true); From c9669e5fc1a34917dccde8a44252e59b24494131 Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl Date: Fri, 22 May 2026 07:57:23 +0200 Subject: [PATCH 5/6] rebase --- glifo/src/glyph.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/glifo/src/glyph.rs b/glifo/src/glyph.rs index 55788fa6e6..a4109ad6c5 100644 --- a/glifo/src/glyph.rs +++ b/glifo/src/glyph.rs @@ -2191,6 +2191,7 @@ mod tests { let mut run = GlyphRun { font: font.clone(), font_size: 20.0, + font_embolden: Default::default(), transform: Affine::translate((0.0, 20.0)), glyph_transform: None, normalized_coords: &[], From 86a4a070bcaf6ba6cd1af243e294c542d0f3bee0 Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl Date: Fri, 22 May 2026 19:42:07 +0200 Subject: [PATCH 6/6] Fix clippy --- glifo/src/glyph.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/glifo/src/glyph.rs b/glifo/src/glyph.rs index a4109ad6c5..fd5373f285 100644 --- a/glifo/src/glyph.rs +++ b/glifo/src/glyph.rs @@ -2191,7 +2191,7 @@ mod tests { let mut run = GlyphRun { font: font.clone(), font_size: 20.0, - font_embolden: Default::default(), + font_embolden: FontEmbolden::default(), transform: Affine::translate((0.0, 20.0)), glyph_transform: None, normalized_coords: &[],