1use std::collections::hash_map::DefaultHasher;
12use std::hash::{Hash, Hasher};
13use std::sync::{Arc, RwLock};
14
15use crate::cache::MultiLevelCache;
16use crate::cache_config;
17use crate::types::ShapingResult;
18
19#[derive(Debug, Clone, PartialEq, Eq, Hash)]
25pub struct ShapingCacheKey {
26 pub text: String,
28 pub backend: String,
30 pub font_id: u64,
32 pub size: u32,
34 pub language: Option<String>,
36 pub script: Option<String>,
38 pub features: Vec<(String, u32)>,
40 pub variations: Vec<(String, i32)>,
42}
43
44impl ShapingCacheKey {
45 #[allow(clippy::too_many_arguments)]
53 pub fn new(
54 text: impl Into<String>,
55 backend: impl Into<String>,
56 font_data: &[u8],
57 size: f32,
58 language: Option<String>,
59 script: Option<String>,
60 features: Vec<(String, u32)>,
61 variations: Vec<(String, f32)>,
62 ) -> Self {
63 let mut hasher = DefaultHasher::new();
65 font_data.hash(&mut hasher);
66 let font_id = hasher.finish();
67
68 let variations_int: Vec<(String, i32)> = variations
70 .into_iter()
71 .map(|(tag, val)| (tag, (val * 100.0) as i32))
72 .collect();
73
74 Self {
75 text: text.into(),
76 backend: backend.into(),
77 font_id,
78 size: (size * 100.0) as u32, language,
80 script,
81 features,
82 variations: variations_int,
83 }
84 }
85}
86
87pub struct ShapingCache {
92 cache: MultiLevelCache<ShapingCacheKey, ShapingResult>,
93}
94
95impl std::fmt::Debug for ShapingCache {
96 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97 f.debug_struct("ShapingCache")
98 .field("cache", &self.cache)
99 .finish()
100 }
101}
102
103impl ShapingCache {
104 pub fn new() -> Self {
109 Self {
110 cache: MultiLevelCache::new(100, 500),
111 }
112 }
113
114 pub fn with_capacity(l1_size: usize, l2_size: usize) -> Self {
116 Self {
117 cache: MultiLevelCache::new(l1_size, l2_size),
118 }
119 }
120
121 pub fn get(&self, key: &ShapingCacheKey) -> Option<ShapingResult> {
126 if !cache_config::is_caching_enabled() {
127 return None;
128 }
129 self.cache.get(key)
130 }
131
132 pub fn insert(&self, key: ShapingCacheKey, result: ShapingResult) {
137 if !cache_config::is_caching_enabled() {
138 return;
139 }
140 self.cache.insert(key, result);
141 }
142
143 pub fn hit_rate(&self) -> f64 {
145 self.cache.hit_rate()
146 }
147
148 pub fn stats(&self) -> CacheStats {
150 let metrics = self.cache.metrics();
151 let total = metrics.l1_hits + metrics.l2_hits + metrics.misses;
152 CacheStats {
153 hits: (metrics.l1_hits + metrics.l2_hits) as usize,
154 misses: metrics.misses as usize,
155 evictions: 0, hit_rate: if total > 0 {
157 (metrics.l1_hits + metrics.l2_hits) as f64 / total as f64
158 } else {
159 0.0
160 },
161 }
162 }
163}
164
165impl Default for ShapingCache {
166 fn default() -> Self {
167 Self::new()
168 }
169}
170
171#[derive(Debug, Clone)]
173pub struct CacheStats {
174 pub hits: usize,
175 pub misses: usize,
176 pub evictions: usize,
177 pub hit_rate: f64,
178}
179
180pub type SharedShapingCache = Arc<RwLock<ShapingCache>>;
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186 use crate::types::{Direction, PositionedGlyph};
187
188 #[test]
189 fn test_cache_key_creation() {
190 let key = ShapingCacheKey::new(
191 "Hello",
192 "hb",
193 b"font_data",
194 16.0,
195 Some("en".to_string()),
196 Some("latn".to_string()),
197 vec![("liga".to_string(), 1)],
198 vec![("wght".to_string(), 700.0)],
199 );
200
201 assert_eq!(key.text, "Hello");
202 assert_eq!(key.size, 1600); assert_eq!(key.language, Some("en".to_string()));
204 assert_eq!(key.variations, vec![("wght".to_string(), 70000)]); }
206
207 #[test]
208 fn test_cache_insert_and_get() {
209 let _guard = crate::cache_config::scoped_caching_enabled(true);
210
211 let cache = ShapingCache::new();
212
213 let key = ShapingCacheKey::new("Test", "hb", b"font", 12.0, None, None, vec![], vec![]);
214
215 let result = ShapingResult {
216 glyphs: vec![PositionedGlyph {
217 id: 1,
218 x: 0.0,
219 y: 0.0,
220 advance: 10.0,
221 cluster: 0,
222 }],
223 advance_width: 10.0,
224 advance_height: 12.0,
225 direction: Direction::LeftToRight,
226 };
227
228 cache.insert(key.clone(), result.clone());
229 let cached = match cache.get(&key) {
230 Some(cached) => cached,
231 None => unreachable!("cache should return inserted value"),
232 };
233 assert_eq!(cached.glyphs.len(), 1);
234 }
235
236 #[test]
237 fn test_cache_miss() {
238 let cache = ShapingCache::new();
239
240 let key = ShapingCacheKey::new("Missing", "hb", b"font", 16.0, None, None, vec![], vec![]);
241 assert!(cache.get(&key).is_none());
242 }
243
244 #[test]
245 fn test_cache_stats() {
246 let _guard = crate::cache_config::scoped_caching_enabled(true);
247
248 let cache = ShapingCache::new();
249
250 let key = ShapingCacheKey::new("Text", "hb", b"font", 16.0, None, None, vec![], vec![]);
251 let result = ShapingResult {
252 glyphs: vec![],
253 advance_width: 0.0,
254 advance_height: 16.0,
255 direction: Direction::LeftToRight,
256 };
257
258 cache.get(&key);
260
261 cache.insert(key.clone(), result);
263
264 cache.get(&key);
266 cache.get(&key);
267
268 let stats = cache.stats();
269
270 assert!(stats.hit_rate >= 0.0);
272 }
273
274 #[test]
275 fn test_different_keys() {
276 let key1 = ShapingCacheKey::new("Hello", "hb", b"font1", 16.0, None, None, vec![], vec![]);
277 let key2 = ShapingCacheKey::new("Hello", "hb", b"font2", 16.0, None, None, vec![], vec![]);
278 let key3 = ShapingCacheKey::new("World", "hb", b"font1", 16.0, None, None, vec![], vec![]);
279
280 assert_ne!(key1, key2);
282
283 assert_ne!(key1, key3);
285 }
286
287 #[test]
288 fn test_different_variations_produce_different_keys() {
289 let key_400 = ShapingCacheKey::new(
291 "Test",
292 "hb",
293 b"font",
294 16.0,
295 None,
296 None,
297 vec![],
298 vec![("wght".to_string(), 400.0)],
299 );
300 let key_700 = ShapingCacheKey::new(
301 "Test",
302 "hb",
303 b"font",
304 16.0,
305 None,
306 None,
307 vec![],
308 vec![("wght".to_string(), 700.0)],
309 );
310 let key_no_var =
311 ShapingCacheKey::new("Test", "hb", b"font", 16.0, None, None, vec![], vec![]);
312
313 assert_ne!(
315 key_400, key_700,
316 "wght=400 and wght=700 should have different cache keys"
317 );
318 assert_ne!(
319 key_400, key_no_var,
320 "wght=400 and no variations should have different keys"
321 );
322 }
323}