vello_common/
glyph.rs

1// Copyright 2025 the Vello Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Processing and drawing glyphs.
5
6use crate::kurbo::{Affine, BezPath, Vec2};
7use crate::peniko::FontData;
8use alloc::boxed::Box;
9use alloc::vec::Vec;
10use core::fmt::{Debug, Formatter};
11use hashbrown::hash_map::{Entry, RawEntryMut};
12use hashbrown::{Equivalent, HashMap};
13use skrifa::instance::{LocationRef, Size};
14use skrifa::outline::{DrawSettings, OutlineGlyphFormat};
15use skrifa::raw::TableProvider;
16use skrifa::{FontRef, OutlineGlyphCollection};
17use skrifa::{
18    GlyphId, MetadataProvider,
19    outline::{HintingInstance, HintingOptions, OutlinePen},
20};
21
22use crate::colr::convert_bounding_box;
23use crate::encode::x_y_advances;
24use crate::kurbo::Rect;
25use crate::pixmap::Pixmap;
26use skrifa::bitmap::{BitmapData, BitmapFormat, BitmapStrikes, Origin};
27
28#[cfg(not(feature = "std"))]
29use peniko::kurbo::common::FloatFuncs as _;
30
31/// Positioned glyph.
32#[derive(Copy, Clone, Default, Debug)]
33pub struct Glyph {
34    /// The font-specific identifier for this glyph.
35    ///
36    /// This ID is specific to the font being used and corresponds to the
37    /// glyph index within that font. It is *not* a Unicode code point.
38    pub id: u32,
39    /// X-offset in run, relative to transform.
40    pub x: f32,
41    /// Y-offset in run, relative to transform.
42    pub y: f32,
43}
44
45/// A type of glyph.
46#[derive(Debug)]
47pub enum GlyphType<'a> {
48    /// An outline glyph.
49    Outline(OutlineGlyph<'a>),
50    /// A bitmap glyph.
51    Bitmap(BitmapGlyph),
52    /// A COLR glyph.
53    Colr(Box<ColorGlyph<'a>>),
54}
55
56/// A simplified representation of a glyph, prepared for easy rendering.
57#[derive(Debug)]
58pub struct PreparedGlyph<'a> {
59    /// The type of glyph.
60    pub glyph_type: GlyphType<'a>,
61    /// The global transform of the glyph.
62    pub transform: Affine,
63}
64
65/// A glyph defined by a path (its outline) and a local transform.
66#[derive(Debug)]
67pub struct OutlineGlyph<'a> {
68    /// The path of the glyph.
69    pub path: &'a BezPath,
70}
71
72/// A glyph defined by a bitmap.
73#[derive(Debug)]
74pub struct BitmapGlyph {
75    /// The pixmap of the glyph.
76    pub pixmap: Pixmap,
77    /// The rectangular area that should be filled with the bitmap when painting.
78    pub area: Rect,
79}
80
81/// A glyph defined by a COLR glyph description.
82///
83/// Clients are supposed to first draw the glyph into an intermediate image texture/pixmap
84/// and then render that into the actual scene, in a similar fashion to
85/// bitmap glyphs.
86pub struct ColorGlyph<'a> {
87    pub(crate) skrifa_glyph: skrifa::color::ColorGlyph<'a>,
88    pub(crate) location: LocationRef<'a>,
89    pub(crate) font_ref: &'a FontRef<'a>,
90    pub(crate) draw_transform: Affine,
91    /// The rectangular area that should be filled with the rendered representation of the
92    /// COLR glyph when painting.
93    pub area: Rect,
94    /// The width of the pixmap/texture in pixels to which the glyph should be rendered to.
95    pub pix_width: u16,
96    /// The height of the pixmap/texture in pixels to which the glyph should be rendered to.
97    pub pix_height: u16,
98}
99
100impl Debug for ColorGlyph<'_> {
101    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
102        write!(f, "ColorGlyph")
103    }
104}
105
106/// Trait for types that can render glyphs.
107pub trait GlyphRenderer {
108    /// Fill glyphs with the current paint and fill rule.
109    fn fill_glyph(&mut self, glyph: PreparedGlyph<'_>);
110
111    /// Stroke glyphs with the current paint and stroke settings.
112    fn stroke_glyph(&mut self, glyph: PreparedGlyph<'_>);
113
114    /// Takes the glyph caches from the renderer for use in a glyph run.
115    ///
116    /// NOTE: The caller must restore the caches after the glyph run is done.
117    fn take_glyph_caches(&mut self) -> GlyphCaches;
118
119    /// Restores the glyph caches after a glyph run.
120    ///
121    /// The caches must have been previously taken with `take_glyph_caches`.
122    fn restore_glyph_caches(&mut self, caches: GlyphCaches);
123}
124
125/// A builder for configuring and drawing glyphs.
126#[derive(Debug)]
127#[must_use = "Methods on the builder don't do anything until `render` is called."]
128pub struct GlyphRunBuilder<'a, T: GlyphRenderer + 'a> {
129    run: GlyphRun<'a>,
130    renderer: &'a mut T,
131}
132
133impl<'a, T: GlyphRenderer + 'a> GlyphRunBuilder<'a, T> {
134    /// Creates a new builder for drawing glyphs.
135    pub fn new(font: FontData, transform: Affine, renderer: &'a mut T) -> Self {
136        Self {
137            run: GlyphRun {
138                font,
139                font_size: 16.0,
140                transform,
141                glyph_transform: None,
142                hint: true,
143                normalized_coords: &[],
144            },
145            renderer,
146        }
147    }
148
149    /// Set the font size in pixels per em.
150    pub fn font_size(mut self, size: f32) -> Self {
151        self.run.font_size = size;
152        self
153    }
154
155    /// Set the per-glyph transform. Use `Affine::skew` with a horizontal-only skew to simulate
156    /// italic text.
157    pub fn glyph_transform(mut self, transform: Affine) -> Self {
158        self.run.glyph_transform = Some(transform);
159        self
160    }
161
162    /// Set whether font hinting is enabled.
163    ///
164    /// This performs vertical hinting only. Hinting is performed only if the combined `transform`
165    /// and `glyph_transform` have a uniform scale and no vertical skew or rotation.
166    pub fn hint(mut self, hint: bool) -> Self {
167        self.run.hint = hint;
168        self
169    }
170
171    /// Set normalized variation coordinates for variable fonts.
172    pub fn normalized_coords(mut self, coords: &'a [NormalizedCoord]) -> Self {
173        self.run.normalized_coords = bytemuck::cast_slice(coords);
174        self
175    }
176
177    /// Consumes the builder and fills the glyphs with the current configuration.
178    pub fn fill_glyphs(self, glyphs: impl Iterator<Item = Glyph>) {
179        self.render(glyphs, Style::Fill);
180    }
181
182    /// Consumes the builder and strokes the glyphs with the current configuration.
183    pub fn stroke_glyphs(self, glyphs: impl Iterator<Item = Glyph>) {
184        self.render(glyphs, Style::Stroke);
185    }
186
187    fn render(self, glyphs: impl Iterator<Item = Glyph>, style: Style) {
188        let font_ref =
189            FontRef::from_index(self.run.font.data.as_ref(), self.run.font.index).unwrap();
190
191        let upem: f32 = font_ref.head().map(|h| h.units_per_em()).unwrap().into();
192
193        let outlines = font_ref.outline_glyphs();
194        let color_glyphs = font_ref.color_glyphs();
195        let bitmaps = font_ref.bitmap_strikes();
196
197        // TODO: Consider using a drop guard so that panics return the caches to the renderer.
198        let GlyphCaches {
199            mut hinting_cache,
200            mut outline_cache,
201        } = self.renderer.take_glyph_caches();
202        let mut outline_cache_session =
203            OutlineCacheSession::new(&mut outline_cache, VarLookupKey(self.run.normalized_coords));
204        let PreparedGlyphRun {
205            transform: initial_transform,
206            size,
207            normalized_coords,
208            hinting_instance,
209        } = prepare_glyph_run(&self.run, &outlines, &mut hinting_cache);
210
211        let render_glyph = match style {
212            Style::Fill => GlyphRenderer::fill_glyph,
213            Style::Stroke => GlyphRenderer::stroke_glyph,
214        };
215
216        for glyph in glyphs {
217            let bitmap_data = bitmaps
218                .glyph_for_size(Size::new(self.run.font_size), GlyphId::new(glyph.id))
219                .and_then(|g| match g.data {
220                    #[cfg(feature = "png")]
221                    BitmapData::Png(data) => Pixmap::from_png(data).ok().map(|d| (g, d)),
222                    #[cfg(not(feature = "png"))]
223                    BitmapData::Png(_) => None,
224                    // The others are not worth implementing for now (unless we can find a test case),
225                    // they should be very rare.
226                    BitmapData::Bgra(_) => None,
227                    BitmapData::Mask(_) => None,
228                });
229
230            let (glyph_type, transform) =
231                if let Some(color_glyph) = color_glyphs.get(GlyphId::new(glyph.id)) {
232                    prepare_colr_glyph(
233                        &font_ref,
234                        glyph,
235                        self.run.font_size,
236                        upem,
237                        initial_transform,
238                        color_glyph,
239                        normalized_coords,
240                    )
241                } else if let Some((bitmap_glyph, pixmap)) = bitmap_data {
242                    prepare_bitmap_glyph(
243                        &bitmaps,
244                        glyph,
245                        pixmap,
246                        self.run.font_size,
247                        upem,
248                        initial_transform,
249                        bitmap_glyph,
250                    )
251                } else {
252                    let Some(outline) = outlines.get(GlyphId::new(glyph.id)) else {
253                        continue;
254                    };
255
256                    prepare_outline_glyph(
257                        glyph,
258                        self.run.font.data.id(),
259                        self.run.font.index,
260                        &mut outline_cache_session,
261                        size,
262                        initial_transform,
263                        self.run.transform,
264                        &outline,
265                        hinting_instance,
266                        normalized_coords,
267                    )
268                };
269
270            let prepared_glyph = PreparedGlyph {
271                glyph_type,
272                transform,
273            };
274
275            render_glyph(self.renderer, prepared_glyph);
276        }
277
278        self.renderer.restore_glyph_caches(GlyphCaches {
279            outline_cache,
280            hinting_cache,
281        });
282    }
283}
284
285fn prepare_outline_glyph<'a>(
286    glyph: Glyph,
287    font_id: u64,
288    font_index: u32,
289    outline_cache: &'a mut OutlineCacheSession<'_>,
290    size: Size,
291    // The transform of the run + the per-glyph transform.
292    initial_transform: Affine,
293    // The transform of the run, without the per-glyph transform.
294    run_transform: Affine,
295    outline_glyph: &skrifa::outline::OutlineGlyph<'a>,
296    hinting_instance: Option<&HintingInstance>,
297    normalized_coords: &[skrifa::instance::NormalizedCoord],
298) -> (GlyphType<'a>, Affine) {
299    let path = outline_cache.get_or_insert(
300        glyph.id,
301        font_id,
302        font_index,
303        size,
304        VarLookupKey(normalized_coords),
305        outline_glyph,
306        hinting_instance,
307    );
308
309    // Calculate the global glyph translation based on the glyph's local position within
310    // the run and the run's global transform.
311    //
312    // This is a partial affine matrix multiplication, calculating only the translation
313    // component that we need. It is added below to calculate the total transform of this
314    // glyph.
315    let [a, b, c, d, _, _] = run_transform.as_coeffs();
316    let translation = Vec2::new(
317        a * f64::from(glyph.x) + c * f64::from(glyph.y),
318        b * f64::from(glyph.x) + d * f64::from(glyph.y),
319    );
320
321    // When hinting, ensure the y-offset is integer. The x-offset doesn't matter, as we
322    // perform vertical-only hinting.
323    let mut final_transform = initial_transform
324        .then_translate(translation)
325        // Account for the fact that the coordinate system of fonts
326        // is upside down.
327        .pre_scale_non_uniform(1.0, -1.0)
328        .as_coeffs();
329
330    if hinting_instance.is_some() {
331        final_transform[5] = final_transform[5].round();
332    }
333
334    (
335        GlyphType::Outline(OutlineGlyph { path: &path.0 }),
336        Affine::new(final_transform),
337    )
338}
339
340fn prepare_bitmap_glyph<'a>(
341    bitmaps: &BitmapStrikes<'_>,
342    glyph: Glyph,
343    pixmap: Pixmap,
344    font_size: f32,
345    upem: f32,
346    initial_transform: Affine,
347    bitmap_glyph: skrifa::bitmap::BitmapGlyph<'a>,
348) -> (GlyphType<'a>, Affine) {
349    let x_scale_factor = font_size / bitmap_glyph.ppem_x;
350    let y_scale_factor = font_size / bitmap_glyph.ppem_y;
351    let font_units_to_size = font_size / upem;
352
353    // CoreText appears to special case Apple Color Emoji, adding
354    // a 100 font unit vertical offset. We do the same but only
355    // when both vertical offsets are 0 to avoid incorrect
356    // rendering if Apple ever does encode the offset directly in
357    // the font.
358    let bearing_y = if bitmap_glyph.bearing_y == 0.0 && bitmaps.format() == Some(BitmapFormat::Sbix)
359    {
360        100.0
361    } else {
362        bitmap_glyph.bearing_y
363    };
364
365    let origin_shift = match bitmap_glyph.placement_origin {
366        Origin::TopLeft => Vec2::default(),
367        Origin::BottomLeft => Vec2 {
368            x: 0.,
369            y: -f64::from(pixmap.height()),
370        },
371    };
372
373    let transform = initial_transform
374        .pre_translate(Vec2::new(glyph.x.into(), glyph.y.into()))
375        // Apply outer bearings.
376        .pre_translate(Vec2 {
377            x: (-bitmap_glyph.bearing_x * font_units_to_size).into(),
378            y: (bearing_y * font_units_to_size).into(),
379        })
380        // Scale to pixel-space.
381        .pre_scale_non_uniform(f64::from(x_scale_factor), f64::from(y_scale_factor))
382        // Apply inner bearings.
383        .pre_translate(Vec2 {
384            x: (-bitmap_glyph.inner_bearing_x).into(),
385            y: (-bitmap_glyph.inner_bearing_y).into(),
386        })
387        .pre_translate(origin_shift);
388
389    // Scale factor already accounts for ppem, so we can just draw in the size of the
390    // actual image
391    let area = Rect::new(
392        0.0,
393        0.0,
394        f64::from(pixmap.width()),
395        f64::from(pixmap.height()),
396    );
397
398    (GlyphType::Bitmap(BitmapGlyph { pixmap, area }), transform)
399}
400
401fn prepare_colr_glyph<'a>(
402    font_ref: &'a FontRef<'a>,
403    glyph: Glyph,
404    font_size: f32,
405    upem: f32,
406    run_transform: Affine,
407    color_glyph: skrifa::color::ColorGlyph<'a>,
408    normalized_coords: &'a [skrifa::instance::NormalizedCoord],
409) -> (GlyphType<'a>, Affine) {
410    // A couple of notes on the implementation here:
411    //
412    // Firstly, COLR glyphs, similarly to normal outline
413    // glyphs, are by default specified in an upside-down coordinate system. They operate
414    // on a layer-based push/pop system, where you push new clip or blend layers and then
415    // fill the whole available area (within the current clipping area) with a specific paint.
416    // Rendering those glyphs in the main scene would be very expensive, as we have to push/pop
417    // layers on the whole canvas just to draw a small glyph (at least with the current architecture).
418    // Because of this, clients are instead supposed to create an intermediate texture to render the
419    // glyph onto and then render it similarly to a bitmap glyph. This also makes it possible to cache
420    // the glyphs.
421    //
422    // Next, there is a problem when rendering COLR glyphs to an intermediate pixmap: The bounding box
423    // of a glyph can reach into the negative, meaning that parts of it might be cut off when
424    // rendering it directly. Because of this, before drawing we first apply a shift transform so
425    // that the bounding box of the glyph starts at (0, 0), then we draw the whole glyph, and
426    // finally when positioning the actual pixmap in the scene, we reverse that transform so that
427    // the position stays the same as the original one.
428
429    let scale = font_size / upem;
430
431    let transform = run_transform.pre_translate(Vec2::new(glyph.x.into(), glyph.y.into()));
432
433    // Estimate the size of the intermediate pixmap. Ideally, the intermediate bitmap should have
434    // exactly one pixel (or more) per device pixel, to ensure that no quality is lost. Therefore,
435    // we simply use the scaling/skewing factor to calculate how much to scale by, and use the
436    // maximum of both dimensions.
437    let scale_factor = {
438        let (x_vec, y_vec) = x_y_advances(&transform.pre_scale(f64::from(scale)));
439        x_vec.length().max(y_vec.length())
440    };
441
442    let bbox = color_glyph
443        .bounding_box(LocationRef::default(), Size::unscaled())
444        .map(convert_bounding_box)
445        .unwrap_or(Rect::new(0.0, 0.0, f64::from(upem), f64::from(upem)));
446
447    // Calculate the position of the rectangle that will contain the rendered pixmap in device
448    // coordinates.
449    let scaled_bbox = bbox.scale_from_origin(scale_factor);
450
451    let glyph_transform = transform
452        // There are two things going on here:
453        // - On the one hand, for images, the position (0, 0) will be at the top-left, while
454        //   for images, the position will be at the bottom-left.
455        // - COLR glyphs have a flipped y-axis, so in the intermediate image they will be
456        //   upside down.
457        // Because of both of these, all we simply need to do is to flip the image on the y-axis.
458        // This will ensure that the glyph in the image isn't upside down anymore, and at the same
459        // time also flips from having the origin in the top-left to having the origin in the
460        // bottom-right.
461        * Affine::scale_non_uniform(1.0, -1.0)
462        // Shift the pixmap back so that the bbox aligns with the original position
463        // of where the glyph should be placed.
464        * Affine::translate((scaled_bbox.x0, scaled_bbox.y0));
465
466    let (pix_width, pix_height) = (
467        scaled_bbox.width().ceil() as u16,
468        scaled_bbox.height().ceil() as u16,
469    );
470
471    let draw_transform =
472        // Shift everything so that the bbox starts at (0, 0) and the whole visible area of
473        // the glyph will be contained in the intermediate pixmap.
474        Affine::translate((-scaled_bbox.x0, -scaled_bbox.y0)) *
475        // Scale down to the actual size that the COLR glyph will have in device units.
476        Affine::scale(scale_factor);
477
478    // The shift-back happens in `glyph_transform`, so here we can assume (0.0, 0.0) as the origin
479    // of the area we want to draw to.
480    let area = Rect::new(0.0, 0.0, scaled_bbox.width(), scaled_bbox.height());
481
482    (
483        GlyphType::Colr(Box::new(ColorGlyph {
484            skrifa_glyph: color_glyph,
485            font_ref,
486            location: LocationRef::new(normalized_coords),
487            area,
488            pix_width,
489            pix_height,
490            draw_transform,
491        })),
492        glyph_transform,
493    )
494}
495
496/// Rendering style for glyphs.
497#[derive(Debug, Clone, Copy, PartialEq, Eq)]
498pub enum Style {
499    /// Fill the glyph.
500    Fill,
501    /// Stroke the glyph.
502    Stroke,
503}
504
505/// A sequence of glyphs with shared rendering properties.
506#[derive(Clone, Debug)]
507struct GlyphRun<'a> {
508    /// Font for all glyphs in the run.
509    font: FontData,
510    /// Size of the font in pixels per em.
511    font_size: f32,
512    /// Global transform.
513    transform: Affine,
514    /// Per-glyph transform. Use [`Affine::skew`] with horizontal-skew only to simulate italic
515    /// text.
516    glyph_transform: Option<Affine>,
517    /// Normalized variation coordinates for variable fonts.
518    normalized_coords: &'a [skrifa::instance::NormalizedCoord],
519    /// Controls whether font hinting is enabled.
520    hint: bool,
521}
522
523struct PreparedGlyphRun<'a> {
524    /// The total transform (`global_transform * glyph_transform`), not accounting for glyph
525    /// translation.
526    transform: Affine,
527    /// The font size to generate glyph outlines for.
528    size: Size,
529    normalized_coords: &'a [skrifa::instance::NormalizedCoord],
530    hinting_instance: Option<&'a HintingInstance>,
531}
532
533/// Prepare a glyph run for rendering.
534///
535/// This function calculates the appropriate transform, size, and scaling parameters
536/// for proper font hinting when enabled and possible.
537fn prepare_glyph_run<'a>(
538    run: &GlyphRun<'a>,
539    outlines: &OutlineGlyphCollection<'_>,
540    hint_cache: &'a mut HintCache,
541) -> PreparedGlyphRun<'a> {
542    if !run.hint {
543        return PreparedGlyphRun {
544            transform: run.transform * run.glyph_transform.unwrap_or(Affine::IDENTITY),
545            size: Size::new(run.font_size),
546            normalized_coords: run.normalized_coords,
547            hinting_instance: None,
548        };
549    }
550
551    // We perform vertical-only hinting.
552    //
553    // Hinting doesn't make sense if we later scale the glyphs via some transform. So we extract
554    // the scale from the global transform and glyph transform and apply it to the font size for
555    // hinting. We do require the scaling to be uniform: simply using the vertical scale as font
556    // size and then transforming by the relative horizontal scale can cause, e.g., overlapping
557    // glyphs. Note that this extracted scale should be later applied to the glyph's position.
558    //
559    // As the hinting is vertical-only, we can handle horizontal skew, but not vertical skew or
560    // rotations.
561    let total_transform = run.transform * run.glyph_transform.unwrap_or(Affine::IDENTITY);
562    let [t_a, t_b, t_c, t_d, t_e, t_f] = total_transform.as_coeffs();
563
564    let uniform_scale = t_a == t_d;
565    let vertically_uniform = t_b == 0.;
566
567    if uniform_scale && vertically_uniform {
568        let vertical_font_size = run.font_size * t_d as f32;
569        let size = Size::new(vertical_font_size);
570
571        let hinting_instance = hint_cache.get(&HintKey {
572            font_id: run.font.data.id(),
573            font_index: run.font.index,
574            outlines,
575            size,
576            coords: run.normalized_coords,
577        });
578
579        PreparedGlyphRun {
580            transform: Affine::new([1., 0., t_c, 1., t_e, t_f]),
581            size,
582            normalized_coords: run.normalized_coords,
583            hinting_instance,
584        }
585    } else {
586        PreparedGlyphRun {
587            transform: total_transform,
588            size: Size::new(run.font_size),
589            normalized_coords: run.normalized_coords,
590            hinting_instance: None,
591        }
592    }
593}
594
595// TODO: Although these are sane defaults, we might want to make them
596// configurable.
597const HINTING_OPTIONS: HintingOptions = HintingOptions {
598    engine: skrifa::outline::Engine::AutoFallback,
599    target: skrifa::outline::Target::Smooth {
600        mode: skrifa::outline::SmoothMode::Lcd,
601        symmetric_rendering: false,
602        preserve_linear_metrics: true,
603    },
604};
605
606#[derive(Clone, Default)]
607pub(crate) struct OutlinePath(pub(crate) BezPath);
608
609impl OutlinePath {
610    pub(crate) fn new() -> Self {
611        Self(BezPath::new())
612    }
613}
614
615// Note that we flip the y-axis to match our coordinate system.
616impl OutlinePen for OutlinePath {
617    #[inline]
618    fn move_to(&mut self, x: f32, y: f32) {
619        self.0.move_to((x, y));
620    }
621
622    #[inline]
623    fn line_to(&mut self, x: f32, y: f32) {
624        self.0.line_to((x, y));
625    }
626
627    #[inline]
628    fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
629        self.0.curve_to((cx0, cy0), (cx1, cy1), (x, y));
630    }
631
632    #[inline]
633    fn quad_to(&mut self, cx: f32, cy: f32, x: f32, y: f32) {
634        self.0.quad_to((cx, cy), (x, y));
635    }
636
637    #[inline]
638    fn close(&mut self) {
639        self.0.close_path();
640    }
641}
642
643/// A normalized variation coordinate (for variable fonts) in 2.14 fixed point format.
644///
645/// In most cases, this can be [cast](bytemuck::cast_slice) from the
646/// normalised coords provided by your text layout library.
647///
648/// Equivalent to [`skrifa::instance::NormalizedCoord`], but defined
649/// in Vello so that Skrifa is not part of Vello's public API.
650/// This allows Vello to update its Skrifa in a patch release, and limits
651/// the need for updates only to align Skrifa versions.
652pub type NormalizedCoord = i16;
653
654#[cfg(test)]
655mod tests {
656    use super::*;
657
658    const _NORMALISED_COORD_SIZE_MATCHES: () =
659        assert!(size_of::<skrifa::instance::NormalizedCoord>() == size_of::<NormalizedCoord>());
660}
661
662/// Caches used for glyph rendering.
663// TODO: Consider capturing cache performance metrics like hit rate, etc.
664#[derive(Debug, Default)]
665pub struct GlyphCaches {
666    outline_cache: OutlineCache,
667    hinting_cache: HintCache,
668}
669
670impl GlyphCaches {
671    /// Creates a new `GlyphCaches` instance.
672    pub fn new() -> Self {
673        Default::default()
674    }
675
676    /// Clears the glyph caches.
677    pub fn clear(&mut self) {
678        self.outline_cache.clear();
679        self.hinting_cache.clear();
680    }
681
682    /// Maintains the glyph caches by evicting unused cache entries.
683    ///
684    /// Should be called once per scene rendering.
685    pub fn maintain(&mut self) {
686        self.outline_cache.maintain();
687    }
688}
689
690#[derive(Copy, Clone, PartialEq, Eq, Hash, Default, Debug)]
691struct OutlineKey {
692    font_id: u64,
693    font_index: u32,
694    glyph_id: u32,
695    size_bits: u32,
696    hint: bool,
697}
698
699struct OutlineEntry {
700    path: OutlinePath,
701    serial: u32,
702}
703
704impl OutlineEntry {
705    const fn new(path: OutlinePath, serial: u32) -> Self {
706        Self { path, serial }
707    }
708}
709
710/// Caches glyph outlines for reuse.
711/// Heavily inspired by `vello_encoding::glyph_cache`.
712#[derive(Default)]
713struct OutlineCache {
714    free_list: Vec<OutlinePath>,
715    static_map: HashMap<OutlineKey, OutlineEntry>,
716    variable_map: HashMap<VarKey, HashMap<OutlineKey, OutlineEntry>>,
717    cached_count: usize,
718    serial: u32,
719    last_prune_serial: u32,
720}
721
722impl Debug for OutlineCache {
723    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
724        f.debug_struct("OutlineCache")
725            .field("free_list", &self.free_list.len())
726            .field("static_map", &self.static_map.len())
727            .field("variable_map", &self.variable_map.len())
728            .field("cached_count", &self.cached_count)
729            .field("serial", &self.serial)
730            .field("last_prune_serial", &self.last_prune_serial)
731            .finish()
732    }
733}
734
735impl OutlineCache {
736    fn maintain(&mut self) {
737        // Maximum number of full renders where we'll retain an unused glyph
738        const MAX_ENTRY_AGE: u32 = 64;
739        // Maximum number of full renders before we force a prune
740        const PRUNE_FREQUENCY: u32 = 64;
741        // Always prune if the cached count is greater than this value
742        const CACHED_COUNT_THRESHOLD: usize = 256;
743        // Number of encoding buffers we'll keep on the free list
744        const MAX_FREE_LIST_SIZE: usize = 128;
745
746        let free_list = &mut self.free_list;
747        let serial = self.serial;
748        self.serial += 1;
749        // Don't iterate over the whole cache every frame
750        if serial - self.last_prune_serial < PRUNE_FREQUENCY
751            && self.cached_count < CACHED_COUNT_THRESHOLD
752        {
753            return;
754        }
755        self.last_prune_serial = serial;
756        self.static_map.retain(|_, entry| {
757            if serial - entry.serial > MAX_ENTRY_AGE {
758                if free_list.len() < MAX_FREE_LIST_SIZE {
759                    free_list.push(core::mem::take(&mut entry.path));
760                }
761                self.cached_count -= 1;
762                false
763            } else {
764                true
765            }
766        });
767        self.variable_map.retain(|_, map| {
768            map.retain(|_, entry| {
769                if serial - entry.serial > MAX_ENTRY_AGE {
770                    if free_list.len() < MAX_FREE_LIST_SIZE {
771                        free_list.push(core::mem::take(&mut entry.path));
772                    }
773                    self.cached_count -= 1;
774                    false
775                } else {
776                    true
777                }
778            });
779            !map.is_empty()
780        });
781    }
782
783    fn clear(&mut self) {
784        self.free_list.clear();
785        self.static_map.clear();
786        self.variable_map.clear();
787        self.cached_count = 0;
788        self.serial = 0;
789        self.last_prune_serial = 0;
790    }
791}
792
793struct OutlineCacheSession<'a> {
794    map: &'a mut HashMap<OutlineKey, OutlineEntry>,
795    free_list: &'a mut Vec<OutlinePath>,
796    serial: u32,
797    cached_count: &'a mut usize,
798}
799
800impl<'a> OutlineCacheSession<'a> {
801    fn new(outline_cache: &'a mut OutlineCache, var_key: VarLookupKey<'_>) -> Self {
802        let map = if var_key.0.is_empty() {
803            &mut outline_cache.static_map
804        } else {
805            match outline_cache
806                .variable_map
807                .raw_entry_mut()
808                .from_key(&var_key)
809            {
810                RawEntryMut::Occupied(entry) => entry.into_mut(),
811                RawEntryMut::Vacant(entry) => entry.insert(var_key.into(), HashMap::new()).1,
812            }
813        };
814        Self {
815            map,
816            free_list: &mut outline_cache.free_list,
817            serial: outline_cache.serial,
818            cached_count: &mut outline_cache.cached_count,
819        }
820    }
821
822    fn get_or_insert(
823        &mut self,
824        glyph_id: u32,
825        font_id: u64,
826        font_index: u32,
827        size: Size,
828        var_key: VarLookupKey<'_>,
829        outline_glyph: &skrifa::outline::OutlineGlyph<'_>,
830        hinting_instance: Option<&HintingInstance>,
831    ) -> &OutlinePath {
832        let key = OutlineKey {
833            glyph_id,
834            font_id,
835            font_index,
836            size_bits: size.ppem().unwrap().to_bits(),
837            hint: hinting_instance.is_some(),
838        };
839
840        match self.map.entry(key) {
841            Entry::Occupied(mut entry) => {
842                entry.get_mut().serial = self.serial;
843                &entry.into_mut().path
844            }
845            Entry::Vacant(entry) => {
846                let mut path = self.free_list.pop().unwrap_or_default();
847
848                let draw_settings = if let Some(hinting_instance) = hinting_instance {
849                    DrawSettings::hinted(hinting_instance, false)
850                } else {
851                    DrawSettings::unhinted(size, var_key.0)
852                };
853
854                path.0.truncate(0);
855                outline_glyph.draw(draw_settings, &mut path).unwrap();
856
857                let entry = entry.insert(OutlineEntry::new(path, self.serial));
858                *self.cached_count += 1;
859                &entry.path
860            }
861        }
862    }
863}
864
865/// Key for variable font caches.
866type VarKey = Vec<skrifa::instance::NormalizedCoord>;
867
868/// Lookup key for variable font caches.
869#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
870struct VarLookupKey<'a>(&'a [skrifa::instance::NormalizedCoord]);
871
872impl Equivalent<VarKey> for VarLookupKey<'_> {
873    fn equivalent(&self, other: &VarKey) -> bool {
874        self.0 == *other
875    }
876}
877
878impl From<VarLookupKey<'_>> for VarKey {
879    fn from(key: VarLookupKey<'_>) -> Self {
880        key.0.to_vec()
881    }
882}
883
884/// We keep this small to enable a simple LRU cache with a linear
885/// search. Regenerating hinting data is low to medium cost so it's fine
886/// to redo it occasionally.
887const MAX_CACHED_HINT_INSTANCES: usize = 16;
888
889struct HintKey<'a> {
890    font_id: u64,
891    font_index: u32,
892    outlines: &'a OutlineGlyphCollection<'a>,
893    size: Size,
894    coords: &'a [skrifa::instance::NormalizedCoord],
895}
896
897impl HintKey<'_> {
898    fn instance(&self) -> Option<HintingInstance> {
899        HintingInstance::new(self.outlines, self.size, self.coords, HINTING_OPTIONS).ok()
900    }
901}
902
903/// LRU cache for hinting instances.
904///
905/// Heavily inspired by `vello_encoding::glyph_cache`.
906#[derive(Default)]
907struct HintCache {
908    // Split caches for glyf/cff because the instance type can reuse
909    // internal memory when reconfigured for the same format.
910    glyf_entries: Vec<HintEntry>,
911    cff_entries: Vec<HintEntry>,
912    serial: u64,
913}
914
915impl Debug for HintCache {
916    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
917        f.debug_struct("HintCache")
918            .field("glyf_entries", &self.glyf_entries.len())
919            .field("cff_entries", &self.cff_entries.len())
920            .field("serial", &self.serial)
921            .finish()
922    }
923}
924
925impl HintCache {
926    fn get(&mut self, key: &HintKey<'_>) -> Option<&HintingInstance> {
927        let entries = match key.outlines.format()? {
928            OutlineGlyphFormat::Glyf => &mut self.glyf_entries,
929            OutlineGlyphFormat::Cff | OutlineGlyphFormat::Cff2 => &mut self.cff_entries,
930        };
931        let (entry_ix, is_current) = find_hint_entry(entries, key)?;
932        let entry = entries.get_mut(entry_ix)?;
933        self.serial += 1;
934        entry.serial = self.serial;
935        if !is_current {
936            entry.font_id = key.font_id;
937            entry.font_index = key.font_index;
938            entry
939                .instance
940                .reconfigure(key.outlines, key.size, key.coords, HINTING_OPTIONS)
941                .ok()?;
942        }
943        Some(&entry.instance)
944    }
945
946    fn clear(&mut self) {
947        self.glyf_entries.clear();
948        self.cff_entries.clear();
949        self.serial = 0;
950    }
951}
952
953struct HintEntry {
954    font_id: u64,
955    font_index: u32,
956    instance: HintingInstance,
957    serial: u64,
958}
959
960fn find_hint_entry(entries: &mut Vec<HintEntry>, key: &HintKey<'_>) -> Option<(usize, bool)> {
961    let mut found_serial = u64::MAX;
962    let mut found_index = 0;
963    for (ix, entry) in entries.iter().enumerate() {
964        if entry.font_id == key.font_id
965            && entry.font_index == key.font_index
966            && entry.instance.size() == key.size
967            && entry.instance.location().coords() == key.coords
968        {
969            return Some((ix, true));
970        }
971        if entry.serial < found_serial {
972            found_serial = entry.serial;
973            found_index = ix;
974        }
975    }
976    if entries.len() < MAX_CACHED_HINT_INSTANCES {
977        let instance = key.instance()?;
978        let ix = entries.len();
979        entries.push(HintEntry {
980            font_id: key.font_id,
981            font_index: key.font_index,
982            instance,
983            // This should be updated by the caller.
984            serial: 0,
985        });
986        Some((ix, true))
987    } else {
988        Some((found_index, false))
989    }
990}