Skip to main content

typf_render_opixa/
glyph_cache.rs

1//! Glyph bitmap cache for efficient repeated rendering
2//!
3//! When rendering text, the same glyph often appears multiple times. This cache
4//! stores rendered glyph bitmaps keyed by (font, glyph_id, size, variations) to
5//! avoid redundant rasterization work.
6
7use std::collections::hash_map::DefaultHasher;
8use std::collections::HashMap;
9use std::hash::{Hash, Hasher};
10use std::sync::RwLock;
11
12use crate::rasterizer::GlyphBitmap;
13
14/// Cache key for rendered glyphs
15///
16/// Uniquely identifies a rendered glyph by its font, glyph ID, size, and variations.
17#[derive(Debug, Clone, PartialEq, Eq, Hash)]
18pub struct GlyphCacheKey {
19    /// Hash of font data (identifies the font)
20    pub font_id: u64,
21    /// Glyph ID within the font
22    pub glyph_id: u32,
23    /// Size in fixed-point (size * 100 for hash stability)
24    pub size: u32,
25    /// Hash of variation coordinates
26    pub variations_hash: u64,
27}
28
29impl GlyphCacheKey {
30    /// Create a new cache key
31    pub fn new(font_data: &[u8], glyph_id: u32, size: f32, variations: &[(String, f32)]) -> Self {
32        // Hash font data
33        let mut hasher = DefaultHasher::new();
34        font_data.hash(&mut hasher);
35        let font_id = hasher.finish();
36
37        // Hash variations
38        let mut var_hasher = DefaultHasher::new();
39        for (tag, val) in variations {
40            tag.hash(&mut var_hasher);
41            ((val * 1000.0) as i32).hash(&mut var_hasher);
42        }
43        let variations_hash = var_hasher.finish();
44
45        Self {
46            font_id,
47            glyph_id,
48            size: (size * 100.0) as u32,
49            variations_hash,
50        }
51    }
52}
53
54/// LRU-style glyph cache with configurable capacity
55pub struct GlyphCache {
56    cache: RwLock<HashMap<GlyphCacheKey, GlyphBitmap>>,
57    capacity: usize,
58    hits: RwLock<u64>,
59    misses: RwLock<u64>,
60}
61
62impl GlyphCache {
63    /// Create a new glyph cache with specified capacity
64    pub fn new(capacity: usize) -> Self {
65        Self {
66            cache: RwLock::new(HashMap::with_capacity(capacity)),
67            capacity,
68            hits: RwLock::new(0),
69            misses: RwLock::new(0),
70        }
71    }
72
73    /// Get a cached glyph bitmap if available
74    pub fn get(&self, key: &GlyphCacheKey) -> Option<GlyphBitmap> {
75        let cache = self.cache.read().ok()?;
76        if let Some(bitmap) = cache.get(key) {
77            if let Ok(mut hits) = self.hits.write() {
78                *hits += 1;
79            }
80            Some(bitmap.clone())
81        } else {
82            if let Ok(mut misses) = self.misses.write() {
83                *misses += 1;
84            }
85            None
86        }
87    }
88
89    /// Insert a glyph bitmap into the cache
90    pub fn insert(&self, key: GlyphCacheKey, bitmap: GlyphBitmap) {
91        let mut cache = match self.cache.write() {
92            Ok(c) => c,
93            Err(_) => return,
94        };
95
96        // Simple eviction: clear half the cache when full
97        if cache.len() >= self.capacity {
98            let keys_to_remove: Vec<_> = cache.keys().take(self.capacity / 2).cloned().collect();
99            for k in keys_to_remove {
100                cache.remove(&k);
101            }
102        }
103
104        cache.insert(key, bitmap);
105    }
106
107    /// Get cache hit rate (0.0 to 1.0)
108    pub fn hit_rate(&self) -> f64 {
109        let hits = self.hits.read().map(|h| *h).unwrap_or(0);
110        let misses = self.misses.read().map(|m| *m).unwrap_or(0);
111        let total = hits + misses;
112        if total == 0 {
113            0.0
114        } else {
115            hits as f64 / total as f64
116        }
117    }
118
119    /// Get cache statistics
120    pub fn stats(&self) -> GlyphCacheStats {
121        let cache = self.cache.read().ok();
122        GlyphCacheStats {
123            size: cache.map(|c| c.len()).unwrap_or(0),
124            capacity: self.capacity,
125            hits: self.hits.read().map(|h| *h).unwrap_or(0),
126            misses: self.misses.read().map(|m| *m).unwrap_or(0),
127        }
128    }
129
130    /// Clear the cache
131    pub fn clear(&self) {
132        if let Ok(mut cache) = self.cache.write() {
133            cache.clear();
134        }
135    }
136}
137
138/// Glyph cache statistics
139#[derive(Debug, Clone)]
140pub struct GlyphCacheStats {
141    pub size: usize,
142    pub capacity: usize,
143    pub hits: u64,
144    pub misses: u64,
145}
146
147impl GlyphCacheStats {
148    pub fn hit_rate(&self) -> f64 {
149        let total = self.hits + self.misses;
150        if total == 0 {
151            0.0
152        } else {
153            self.hits as f64 / total as f64
154        }
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    #[test]
163    fn test_cache_key_creation() {
164        let key1 = GlyphCacheKey::new(b"font1", 65, 16.0, &[]);
165        let key2 = GlyphCacheKey::new(b"font1", 65, 16.0, &[]);
166        let key3 = GlyphCacheKey::new(b"font2", 65, 16.0, &[]);
167
168        assert_eq!(key1, key2);
169        assert_ne!(key1, key3);
170    }
171
172    #[test]
173    fn test_variations_affect_key() {
174        let key1 = GlyphCacheKey::new(b"font", 65, 16.0, &[("wght".to_string(), 400.0)]);
175        let key2 = GlyphCacheKey::new(b"font", 65, 16.0, &[("wght".to_string(), 700.0)]);
176        let key3 = GlyphCacheKey::new(b"font", 65, 16.0, &[]);
177
178        assert_ne!(key1, key2, "Different weights should have different keys");
179        assert_ne!(key1, key3, "With/without variations should differ");
180    }
181
182    #[test]
183    fn test_cache_insert_and_get() {
184        let cache = GlyphCache::new(100);
185        let key = GlyphCacheKey::new(b"font", 65, 16.0, &[]);
186        let bitmap = GlyphBitmap {
187            width: 10,
188            height: 12,
189            left: 1,
190            top: 10,
191            data: vec![128; 120],
192        };
193
194        cache.insert(key.clone(), bitmap.clone());
195        let cached = cache.get(&key);
196
197        assert!(cached.is_some());
198        assert_eq!(cached.unwrap().width, 10);
199    }
200
201    #[test]
202    fn test_cache_miss() {
203        let cache = GlyphCache::new(100);
204        let key = GlyphCacheKey::new(b"font", 65, 16.0, &[]);
205
206        assert!(cache.get(&key).is_none());
207    }
208
209    #[test]
210    fn test_cache_eviction() {
211        let cache = GlyphCache::new(3);
212
213        for i in 0..5 {
214            let key = GlyphCacheKey::new(b"font", i, 16.0, &[]);
215            let bitmap = GlyphBitmap {
216                width: 10,
217                height: 12,
218                left: 1,
219                top: 10,
220                data: vec![128; 120],
221            };
222            cache.insert(key, bitmap);
223        }
224
225        // Should have evicted some entries
226        let stats = cache.stats();
227        assert!(stats.size <= 3, "Cache should not exceed capacity");
228    }
229
230    #[test]
231    fn test_cache_stats() {
232        let cache = GlyphCache::new(100);
233        let key = GlyphCacheKey::new(b"font", 65, 16.0, &[]);
234        let bitmap = GlyphBitmap {
235            width: 10,
236            height: 12,
237            left: 1,
238            top: 10,
239            data: vec![128; 120],
240        };
241
242        // Miss
243        cache.get(&key);
244
245        // Insert
246        cache.insert(key.clone(), bitmap);
247
248        // Hits
249        cache.get(&key);
250        cache.get(&key);
251
252        let stats = cache.stats();
253        assert_eq!(stats.misses, 1);
254        assert_eq!(stats.hits, 2);
255        assert!(stats.hit_rate() > 0.6);
256    }
257}