glifo: Add proper support for caching stroked glyphs (#1550)

As in https://github.com/linebender/parley/pull/582, but now for here
since we did the migration.

---------

Co-authored-by: Laurenz Stampfl <laurenz@canva.com>
diff --git a/glifo/src/glyph.rs b/glifo/src/glyph.rs
index 30cf30e..e2952bd 100644
--- a/glifo/src/glyph.rs
+++ b/glifo/src/glyph.rs
@@ -285,8 +285,13 @@
         let font_index = self.prepared_run.font.index;
         let hinted = hinting_instance.is_some();
 
-        let cache_enabled = self.atlas_cache_enabled
+        let colr_bitmap_cache_enabled = self.atlas_cache_enabled
             && hinted_size <= self.glyph_atlas.config().max_cached_font_size;
+        let outline_cache_enabled = colr_bitmap_cache_enabled
+            // Due to the various parameters that would need to be considered in the cache key,
+            // we never cache stroked outlines for now. For COLR and bitmap, this doesn't matter
+            // because they are always filled anyway.
+            && style == Style::Fill;
 
         let render_glyph: fn(&mut R, PreparedGlyph<'_>, &mut C, &mut ImageCache) = match style {
             Style::Fill => R::fill_glyph,
@@ -310,7 +315,7 @@
                 self.prepared_run.run_transform,
                 hinting_instance,
             );
-            let outline_cache_key = cache_enabled.then(|| {
+            let outline_cache_key = outline_cache_enabled.then(|| {
                 let fractional_x = outline_transform.translation().x.fract() as f32;
                 GlyphCacheKey::new(
                     font_id,
@@ -349,7 +354,7 @@
 
                 // COLR glyphs are never hinted and have no sub-pixel offset;
                 // context_color is part of the key because it affects painted layers.
-                let cache_key = cache_enabled.then(|| GlyphCacheKey {
+                let cache_key = colr_bitmap_cache_enabled.then(|| GlyphCacheKey {
                     font_id,
                     font_index,
                     glyph_id: glyph.id,
@@ -428,7 +433,7 @@
 
                 // Bitmaps are not hinted and have no sub-pixel offset or
                 // context color; variation coords are irrelevant for fixed strikes.
-                let cache_key = cache_enabled.then(|| GlyphCacheKey {
+                let cache_key = colr_bitmap_cache_enabled.then(|| GlyphCacheKey {
                     font_id,
                     font_index,
                     glyph_id: glyph.id,
diff --git a/sparse_strips/vello_sparse_tests/snapshots/glyphs_bitmap_noto_stroked.png b/sparse_strips/vello_sparse_tests/snapshots/glyphs_bitmap_noto_stroked.png
new file mode 100644
index 0000000..5751b9d
--- /dev/null
+++ b/sparse_strips/vello_sparse_tests/snapshots/glyphs_bitmap_noto_stroked.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:42c171625da5fcbbb9037fc45ec81c884abbda617bea8b8f41496f60456e3a78
+size 10033
diff --git a/sparse_strips/vello_sparse_tests/snapshots/glyphs_colr_noto_stroked.png b/sparse_strips/vello_sparse_tests/snapshots/glyphs_colr_noto_stroked.png
new file mode 100644
index 0000000..5e46b0f
--- /dev/null
+++ b/sparse_strips/vello_sparse_tests/snapshots/glyphs_colr_noto_stroked.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1b8e91836dcb499bbfc71170e12583de5ee8f5faf1c24d97b11918433a8fe97a
+size 9927
diff --git a/sparse_strips/vello_sparse_tests/snapshots/glyphs_filled_then_stroked.png b/sparse_strips/vello_sparse_tests/snapshots/glyphs_filled_then_stroked.png
new file mode 100644
index 0000000..1a82977
--- /dev/null
+++ b/sparse_strips/vello_sparse_tests/snapshots/glyphs_filled_then_stroked.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f9ca8a3f2d206c18b80afa1fce8ef9e0e6ee28e449a18baa680d02a022916937
+size 6098
diff --git a/sparse_strips/vello_sparse_tests/snapshots/glyphs_large_stroke_width.png b/sparse_strips/vello_sparse_tests/snapshots/glyphs_large_stroke_width.png
new file mode 100644
index 0000000..c8e79a4
--- /dev/null
+++ b/sparse_strips/vello_sparse_tests/snapshots/glyphs_large_stroke_width.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a409728ccaf05d8c21b23ef7a98f4e888ff4a925b4f8a5fa10841aaba64333b0
+size 3671
diff --git a/sparse_strips/vello_sparse_tests/snapshots/glyphs_stroked_then_filled.png b/sparse_strips/vello_sparse_tests/snapshots/glyphs_stroked_then_filled.png
new file mode 100644
index 0000000..bfceb20
--- /dev/null
+++ b/sparse_strips/vello_sparse_tests/snapshots/glyphs_stroked_then_filled.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:688c2b64098483aef590957dd368b8da98d6c0c123ba2e3fd6162b8b340adfb7
+size 6069
diff --git a/sparse_strips/vello_sparse_tests/tests/glyph.rs b/sparse_strips/vello_sparse_tests/tests/glyph.rs
index 73badd5..b1f366d 100644
--- a/sparse_strips/vello_sparse_tests/tests/glyph.rs
+++ b/sparse_strips/vello_sparse_tests/tests/glyph.rs
@@ -11,7 +11,7 @@
 use std::sync::Arc;
 use vello_common::color::palette::css::{BLACK, BLUE, GREEN, REBECCA_PURPLE};
 use vello_common::glyph::Glyph;
-use vello_common::kurbo::Affine;
+use vello_common::kurbo::{Affine, Stroke};
 use vello_common::peniko::{Blob, FontData};
 use vello_dev_macros::vello_test;
 
@@ -68,6 +68,68 @@
 }
 
 #[vello_test(width = 300, height = 70)]
+fn glyphs_large_stroke_width(ctx: &mut impl Renderer) {
+    let font_size: f32 = 50_f32;
+    let (font, glyphs) = layout_glyphs_roboto("Hello, world!", font_size);
+
+    ctx.set_transform(Affine::translate((0., f64::from(font_size))));
+    ctx.set_paint(REBECCA_PURPLE.with_alpha(0.5));
+    ctx.set_stroke(Stroke {
+        width: 3.0,
+        ..Stroke::default()
+    });
+    ctx.glyph_run(&font)
+        .font_size(font_size)
+        .stroke_glyphs(glyphs.into_iter());
+}
+
+#[vello_test(width = 300, height = 120)]
+fn glyphs_stroked_then_filled(ctx: &mut impl Renderer) {
+    let font_size: f32 = 50_f32;
+    let (font, glyphs) = layout_glyphs_roboto("Hello, world!", font_size);
+
+    render_roboto_with_mode(
+        ctx,
+        &font,
+        font_size,
+        glyphs.iter().copied(),
+        Affine::translate((0., f64::from(font_size))),
+        DrawMode::Stroke,
+    );
+    render_roboto_with_mode(
+        ctx,
+        &font,
+        font_size,
+        glyphs.into_iter(),
+        Affine::translate((0., f64::from(font_size * 2.0))),
+        DrawMode::Fill,
+    );
+}
+
+#[vello_test(width = 300, height = 120)]
+fn glyphs_filled_then_stroked(ctx: &mut impl Renderer) {
+    let font_size: f32 = 50_f32;
+    let (font, glyphs) = layout_glyphs_roboto("Hello, world!", font_size);
+
+    render_roboto_with_mode(
+        ctx,
+        &font,
+        font_size,
+        glyphs.iter().copied(),
+        Affine::translate((0., f64::from(font_size))),
+        DrawMode::Fill,
+    );
+    render_roboto_with_mode(
+        ctx,
+        &font,
+        font_size,
+        glyphs.into_iter(),
+        Affine::translate((0., f64::from(font_size * 2.0))),
+        DrawMode::Stroke,
+    );
+}
+
+#[vello_test(width = 300, height = 70)]
 fn glyphs_skewed(ctx: &mut impl Renderer) {
     let font_size: f32 = 50_f32;
     let (font, glyphs) = layout_glyphs_roboto("Hello, world!", font_size);
@@ -254,19 +316,64 @@
         .fill_glyphs(glyphs.into_iter());
 }
 
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+enum DrawMode {
+    Fill,
+    Stroke,
+}
+
+fn render_roboto_with_mode(
+    ctx: &mut impl Renderer,
+    font: &FontData,
+    font_size: f32,
+    glyphs: impl Iterator<Item = Glyph>,
+    transform: Affine,
+    mode: DrawMode,
+) {
+    ctx.set_transform(transform);
+    ctx.set_paint(REBECCA_PURPLE);
+    ctx.set_stroke(Stroke {
+        width: 3.0,
+        ..Stroke::default()
+    });
+    let builder = ctx.glyph_run(font).font_size(font_size);
+
+    match mode {
+        DrawMode::Fill => {
+            builder.fill_glyphs(glyphs);
+        }
+        DrawMode::Stroke => {
+            builder.stroke_glyphs(glyphs);
+        }
+    }
+}
+
 #[vello_test(width = 250, height = 70, skip_hybrid, cpu_u8_tolerance = 1)]
 fn glyphs_colr_noto(ctx: &mut impl Renderer) {
-    render_colr_noto_with_transform(ctx, Affine::translate((0., 50.)));
+    render_colr_noto_with_transform(ctx, Affine::translate((0., 50.)), DrawMode::Fill);
+}
+
+#[vello_test(width = 250, height = 70, skip_hybrid, cpu_u8_tolerance = 1)]
+fn glyphs_colr_noto_stroked(ctx: &mut impl Renderer) {
+    render_colr_noto_with_transform(ctx, Affine::translate((0., 50.)), DrawMode::Stroke);
 }
 
 #[vello_test(width = 500, height = 140, skip_hybrid, cpu_u8_tolerance = 1)]
 fn glyphs_colr_noto_scaled_2x(ctx: &mut impl Renderer) {
-    render_colr_noto_with_transform(ctx, Affine::translate((0., 50.)).then_scale(2.0));
+    render_colr_noto_with_transform(
+        ctx,
+        Affine::translate((0., 50.)).then_scale(2.0),
+        DrawMode::Fill,
+    );
 }
 
 #[vello_test(width = 125, height = 35, skip_hybrid, cpu_u8_tolerance = 1)]
 fn glyphs_colr_noto_scaled_half(ctx: &mut impl Renderer) {
-    render_colr_noto_with_transform(ctx, Affine::translate((0., 50.)).then_scale(0.5));
+    render_colr_noto_with_transform(
+        ctx,
+        Affine::translate((0., 50.)).then_scale(0.5),
+        DrawMode::Fill,
+    );
 }
 
 #[vello_test(width = 350, height = 350, skip_hybrid, cpu_u8_tolerance = 3)]
@@ -274,6 +381,7 @@
     render_colr_noto_with_transform(
         ctx,
         Affine::translate((175., 100.)) * Affine::rotate(std::f64::consts::FRAC_PI_4),
+        DrawMode::Fill,
     );
 }
 
@@ -284,6 +392,7 @@
         Affine::translate((300., 150.))
             * Affine::rotate(std::f64::consts::FRAC_PI_4)
             * Affine::scale(2.0),
+        DrawMode::Fill,
     );
 }
 
@@ -292,6 +401,7 @@
     render_colr_noto_with_transform(
         ctx,
         Affine::translate((0., 50.)) * Affine::scale_non_uniform(1.0, 2.0),
+        DrawMode::Fill,
     );
 }
 
@@ -302,9 +412,21 @@
         Affine::translate((150., 150.))
             * Affine::rotate(std::f64::consts::FRAC_PI_4)
             * Affine::scale_non_uniform(1.0, 2.0),
+        DrawMode::Fill,
     );
 }
 
+#[vello_test(width = 250, height = 70, skip_hybrid)]
+fn glyphs_bitmap_noto_stroked(ctx: &mut impl Renderer) {
+    let font_size: f32 = 50_f32;
+    let (font, glyphs) = layout_glyphs_noto_cbtf("✅👀🎉🤠", font_size);
+
+    ctx.set_transform(Affine::translate((0., f64::from(font_size))));
+    ctx.glyph_run(&font)
+        .font_size(font_size)
+        .stroke_glyphs(glyphs.into_iter());
+}
+
 #[cfg(target_os = "macos")]
 #[vello_test(width = 200, height = 70, skip_hybrid, cpu_u8_tolerance = 2)]
 fn glyphs_bitmap_apple(ctx: &mut impl Renderer) {
@@ -385,13 +507,16 @@
 }
 
 /// Hinting is disabled to preserve transforms passed to `prepare_colr_glyph`.
-fn render_colr_noto_with_transform(ctx: &mut impl Renderer, transform: Affine) {
+fn render_colr_noto_with_transform(ctx: &mut impl Renderer, transform: Affine, mode: DrawMode) {
     let font_size: f32 = 50_f32;
     let (font, glyphs) = layout_glyphs_noto_colr("✅👀🎉🤠", font_size);
 
     ctx.set_transform(transform);
-    ctx.glyph_run(&font)
-        .font_size(font_size)
-        .hint(false)
-        .fill_glyphs(glyphs.into_iter());
+    let run = ctx.glyph_run(&font).font_size(font_size).hint(false);
+
+    if mode == DrawMode::Stroke {
+        run.stroke_glyphs(glyphs.into_iter());
+    } else {
+        run.fill_glyphs(glyphs.into_iter());
+    }
 }