1use 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#[derive(Debug, Clone, PartialEq, Eq, Hash)]
18pub struct GlyphCacheKey {
19 pub font_id: u64,
21 pub glyph_id: u32,
23 pub size: u32,
25 pub variations_hash: u64,
27}
28
29impl GlyphCacheKey {
30 pub fn new(font_data: &[u8], glyph_id: u32, size: f32, variations: &[(String, f32)]) -> Self {
32 let mut hasher = DefaultHasher::new();
34 font_data.hash(&mut hasher);
35 let font_id = hasher.finish();
36
37 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
54pub struct GlyphCache {
56 cache: RwLock<HashMap<GlyphCacheKey, GlyphBitmap>>,
57 capacity: usize,
58 hits: RwLock<u64>,
59 misses: RwLock<u64>,
60}
61
62impl GlyphCache {
63 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 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 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 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 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 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 pub fn clear(&self) {
132 if let Ok(mut cache) = self.cache.write() {
133 cache.clear();
134 }
135 }
136}
137
138#[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 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 cache.get(&key);
244
245 cache.insert(key.clone(), bitmap);
247
248 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}