Skip to main content

text_typeset/atlas/
cache.rs

1use std::collections::HashMap;
2
3use etagere::AllocId;
4
5use crate::types::FontFaceId;
6
7#[derive(Clone, Copy, Eq, PartialEq, Hash)]
8pub struct GlyphCacheKey {
9    pub font_face_id: FontFaceId,
10    pub glyph_id: u16,
11    pub size_bits: u32,
12    /// Font weight (variation axis) so that e.g. Inter Regular and Inter
13    /// Bold produce separate cache entries even though they share the
14    /// same `font_face_id` and `glyph_id` in a variable font.
15    pub weight: u32,
16}
17
18impl GlyphCacheKey {
19    pub fn new(font_face_id: FontFaceId, glyph_id: u16, size_px: f32) -> Self {
20        Self {
21            font_face_id,
22            glyph_id,
23            size_bits: size_px.to_bits(),
24            weight: 400,
25        }
26    }
27
28    pub fn with_weight(font_face_id: FontFaceId, glyph_id: u16, size_px: f32, weight: u32) -> Self {
29        Self {
30            font_face_id,
31            glyph_id,
32            size_bits: size_px.to_bits(),
33            weight,
34        }
35    }
36}
37
38pub struct CachedGlyph {
39    pub alloc_id: AllocId,
40    pub atlas_x: u32,
41    pub atlas_y: u32,
42    pub width: u32,
43    pub height: u32,
44    pub placement_left: i32,
45    pub placement_top: i32,
46    pub is_color: bool,
47    /// Frame generation when this glyph was last used.
48    pub last_used: u64,
49}
50
51/// Glyph cache with LRU eviction.
52///
53/// Tracks a frame generation counter. Each `get` marks the glyph as used
54/// in the current generation. `evict_unused` removes glyphs not used
55/// for `max_idle_frames` generations and deallocates their atlas space.
56pub struct GlyphCache {
57    pub(crate) entries: HashMap<GlyphCacheKey, CachedGlyph>,
58    generation: u64,
59    last_eviction_generation: u64,
60}
61
62/// Number of frames a glyph can go unused before being evicted.
63const MAX_IDLE_FRAMES: u64 = 120; // ~2 seconds at 60fps
64
65impl Default for GlyphCache {
66    fn default() -> Self {
67        Self::new()
68    }
69}
70
71impl GlyphCache {
72    pub fn new() -> Self {
73        Self {
74            entries: HashMap::new(),
75            generation: 0,
76            last_eviction_generation: 0,
77        }
78    }
79
80    /// Advance the frame generation counter. Call once per render frame.
81    pub fn advance_generation(&mut self) {
82        self.generation += 1;
83    }
84
85    pub fn generation(&self) -> u64 {
86        self.generation
87    }
88
89    /// Look up a cached glyph, marking it as used in the current generation.
90    pub fn get(&mut self, key: &GlyphCacheKey) -> Option<&CachedGlyph> {
91        if let Some(entry) = self.entries.get_mut(key) {
92            entry.last_used = self.generation;
93            Some(entry)
94        } else {
95            None
96        }
97    }
98
99    /// Look up without marking as used (for read-only queries).
100    pub fn peek(&self, key: &GlyphCacheKey) -> Option<&CachedGlyph> {
101        self.entries.get(key)
102    }
103
104    pub fn insert(&mut self, key: GlyphCacheKey, mut glyph: CachedGlyph) {
105        glyph.last_used = self.generation;
106        self.entries.insert(key, glyph);
107    }
108
109    /// Evict glyphs unused for MAX_IDLE_FRAMES generations.
110    /// Returns the AllocIds that should be deallocated from the atlas.
111    /// Only runs the actual eviction scan every 60 calls (~1 second at 60fps)
112    /// to avoid iterating the entire cache on every render.
113    pub fn evict_unused(&mut self) -> Vec<AllocId> {
114        // Only scan every 60 generations (~1 second at 60fps)
115        if self.generation - self.last_eviction_generation < 60 {
116            return Vec::new();
117        }
118        self.last_eviction_generation = self.generation;
119
120        let threshold = self.generation.saturating_sub(MAX_IDLE_FRAMES);
121        let mut evicted = Vec::new();
122
123        self.entries.retain(|_key, glyph| {
124            if glyph.last_used < threshold {
125                evicted.push(glyph.alloc_id);
126                false
127            } else {
128                true
129            }
130        });
131
132        evicted
133    }
134
135    pub fn len(&self) -> usize {
136        self.entries.len()
137    }
138
139    pub fn is_empty(&self) -> bool {
140        self.entries.is_empty()
141    }
142
143    /// Mark multiple glyphs as used in the current generation without
144    /// returning their data. Used by callers that cache glyph output
145    /// externally (e.g. per-widget paint caches) and need to keep the
146    /// glyphs alive in the atlas even though they don't re-measure them
147    /// every frame.
148    pub fn touch(&mut self, keys: &[GlyphCacheKey]) {
149        let current = self.generation;
150        for key in keys {
151            if let Some(entry) = self.entries.get_mut(key) {
152                entry.last_used = current;
153            }
154        }
155    }
156}