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 entries: HashMap<GlyphCacheKey, CachedGlyph>,
44    generation: u64,
45}
46
47/// Number of frames a glyph can go unused before being evicted.
48const MAX_IDLE_FRAMES: u64 = 120; // ~2 seconds at 60fps
49
50impl Default for GlyphCache {
51    fn default() -> Self {
52        Self::new()
53    }
54}
55
56impl GlyphCache {
57    pub fn new() -> Self {
58        Self {
59            entries: HashMap::new(),
60            generation: 0,
61        }
62    }
63
64    /// Advance the frame generation counter. Call once per render frame.
65    pub fn advance_generation(&mut self) {
66        self.generation += 1;
67    }
68
69    pub fn generation(&self) -> u64 {
70        self.generation
71    }
72
73    /// Look up a cached glyph, marking it as used in the current generation.
74    pub fn get(&mut self, key: &GlyphCacheKey) -> Option<&CachedGlyph> {
75        if let Some(entry) = self.entries.get_mut(key) {
76            entry.last_used = self.generation;
77            Some(entry)
78        } else {
79            None
80        }
81    }
82
83    /// Look up without marking as used (for read-only queries).
84    pub fn peek(&self, key: &GlyphCacheKey) -> Option<&CachedGlyph> {
85        self.entries.get(key)
86    }
87
88    pub fn insert(&mut self, key: GlyphCacheKey, mut glyph: CachedGlyph) {
89        glyph.last_used = self.generation;
90        self.entries.insert(key, glyph);
91    }
92
93    /// Evict glyphs unused for MAX_IDLE_FRAMES generations.
94    /// Returns the AllocIds that should be deallocated from the atlas.
95    pub fn evict_unused(&mut self) -> Vec<AllocId> {
96        let threshold = self.generation.saturating_sub(MAX_IDLE_FRAMES);
97        let mut evicted = Vec::new();
98
99        self.entries.retain(|_key, glyph| {
100            if glyph.last_used < threshold {
101                evicted.push(glyph.alloc_id);
102                false
103            } else {
104                true
105            }
106        });
107
108        evicted
109    }
110
111    pub fn len(&self) -> usize {
112        self.entries.len()
113    }
114
115    pub fn is_empty(&self) -> bool {
116        self.entries.is_empty()
117    }
118}