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}
13
14impl GlyphCacheKey {
15    pub fn new(font_face_id: FontFaceId, glyph_id: u16, size_px: f32) -> Self {
16        Self {
17            font_face_id,
18            glyph_id,
19            size_bits: size_px.to_bits(),
20        }
21    }
22}
23
24pub struct CachedGlyph {
25    pub alloc_id: AllocId,
26    pub atlas_x: u32,
27    pub atlas_y: u32,
28    pub width: u32,
29    pub height: u32,
30    pub placement_left: i32,
31    pub placement_top: i32,
32    pub is_color: bool,
33    /// Frame generation when this glyph was last used.
34    pub last_used: u64,
35}
36
37/// Glyph cache with LRU eviction.
38///
39/// Tracks a frame generation counter. Each `get` marks the glyph as used
40/// in the current generation. `evict_unused` removes glyphs not used
41/// for `max_idle_frames` generations and deallocates their atlas space.
42pub struct GlyphCache {
43    pub(crate) entries: HashMap<GlyphCacheKey, CachedGlyph>,
44    generation: u64,
45    last_eviction_generation: u64,
46}
47
48/// Number of frames a glyph can go unused before being evicted.
49const MAX_IDLE_FRAMES: u64 = 120; // ~2 seconds at 60fps
50
51impl Default for GlyphCache {
52    fn default() -> Self {
53        Self::new()
54    }
55}
56
57impl GlyphCache {
58    pub fn new() -> Self {
59        Self {
60            entries: HashMap::new(),
61            generation: 0,
62            last_eviction_generation: 0,
63        }
64    }
65
66    /// Advance the frame generation counter. Call once per render frame.
67    pub fn advance_generation(&mut self) {
68        self.generation += 1;
69    }
70
71    pub fn generation(&self) -> u64 {
72        self.generation
73    }
74
75    /// Look up a cached glyph, marking it as used in the current generation.
76    pub fn get(&mut self, key: &GlyphCacheKey) -> Option<&CachedGlyph> {
77        if let Some(entry) = self.entries.get_mut(key) {
78            entry.last_used = self.generation;
79            Some(entry)
80        } else {
81            None
82        }
83    }
84
85    /// Look up without marking as used (for read-only queries).
86    pub fn peek(&self, key: &GlyphCacheKey) -> Option<&CachedGlyph> {
87        self.entries.get(key)
88    }
89
90    pub fn insert(&mut self, key: GlyphCacheKey, mut glyph: CachedGlyph) {
91        glyph.last_used = self.generation;
92        self.entries.insert(key, glyph);
93    }
94
95    /// Evict glyphs unused for MAX_IDLE_FRAMES generations.
96    /// Returns the AllocIds that should be deallocated from the atlas.
97    /// Only runs the actual eviction scan every 60 calls (~1 second at 60fps)
98    /// to avoid iterating the entire cache on every render.
99    pub fn evict_unused(&mut self) -> Vec<AllocId> {
100        // Only scan every 60 generations (~1 second at 60fps)
101        if self.generation - self.last_eviction_generation < 60 {
102            return Vec::new();
103        }
104        self.last_eviction_generation = self.generation;
105
106        let threshold = self.generation.saturating_sub(MAX_IDLE_FRAMES);
107        let mut evicted = Vec::new();
108
109        self.entries.retain(|_key, glyph| {
110            if glyph.last_used < threshold {
111                evicted.push(glyph.alloc_id);
112                false
113            } else {
114                true
115            }
116        });
117
118        evicted
119    }
120
121    pub fn len(&self) -> usize {
122        self.entries.len()
123    }
124
125    pub fn is_empty(&self) -> bool {
126        self.entries.is_empty()
127    }
128}