Skip to main content

typf_core/
glyph_cache.rs

1//! Backend-neutral glyph/render cache with byte-weighted eviction
2//!
3//! Caches complete `RenderOutput` values keyed by the shaped glyph stream,
4//! render parameters, font identity, and renderer backend name. This sits in
5//! `typf-core` so every renderer can benefit without bespoke cache logic.
6//!
7//! **Memory safety**: Uses byte-weighted eviction to prevent memory explosions.
8//! A 4MB emoji bitmap consumes 4000x more cache quota than a 1KB glyph.
9
10// this_file: crates/typf-core/src/glyph_cache.rs
11
12use std::collections::hash_map::DefaultHasher;
13use std::hash::{Hash, Hasher};
14use std::sync::{Arc, RwLock};
15
16use crate::cache::RenderOutputCache;
17use crate::cache_config;
18use crate::types::{RenderOutput, ShapingResult};
19use crate::RenderParams;
20
21/// Stable key for render output caching
22#[derive(Debug, Clone, PartialEq, Eq, Hash)]
23pub struct GlyphCacheKey {
24    /// Renderer/backend identity
25    pub renderer: String,
26    /// Font identity hash
27    pub font_id: u64,
28    /// Hash of shaped glyph sequence (positions + ids + direction)
29    pub shaped_hash: u64,
30    /// Hash of render parameters (colors, AA, palette, variations, glyph sources)
31    pub render_hash: u64,
32}
33
34impl GlyphCacheKey {
35    /// Build a key from runtime inputs
36    pub fn new(
37        renderer: impl Into<String>,
38        font_data: &[u8],
39        shaped: &ShapingResult,
40        render_params: &RenderParams,
41    ) -> Self {
42        let font_id = hash_bytes(font_data);
43        let shaped_hash = hash_shaping_result(shaped);
44        let render_hash = hash_render_params(render_params);
45
46        Self {
47            renderer: renderer.into(),
48            font_id,
49            shaped_hash,
50            render_hash,
51        }
52    }
53}
54
55fn hash_bytes(bytes: &[u8]) -> u64 {
56    let mut hasher = DefaultHasher::new();
57    bytes.hash(&mut hasher);
58    hasher.finish()
59}
60
61fn hash_shaping_result(shaped: &ShapingResult) -> u64 {
62    let mut hasher = DefaultHasher::new();
63
64    shaped.direction.hash(&mut hasher);
65    shaped.advance_width.to_bits().hash(&mut hasher);
66    shaped.advance_height.to_bits().hash(&mut hasher);
67
68    for glyph in &shaped.glyphs {
69        glyph.id.hash(&mut hasher);
70        glyph.cluster.hash(&mut hasher);
71        glyph.x.to_bits().hash(&mut hasher);
72        glyph.y.to_bits().hash(&mut hasher);
73        glyph.advance.to_bits().hash(&mut hasher);
74    }
75
76    hasher.finish()
77}
78
79fn hash_render_params(params: &RenderParams) -> u64 {
80    let mut hasher = DefaultHasher::new();
81
82    params.padding.hash(&mut hasher);
83    params.antialias.hash(&mut hasher);
84    params.color_palette.hash(&mut hasher);
85    params.output.hash(&mut hasher);
86    params.foreground.hash(&mut hasher);
87    params.background.hash(&mut hasher);
88
89    for (tag, value) in &params.variations {
90        tag.hash(&mut hasher);
91        value.to_bits().hash(&mut hasher);
92    }
93
94    for source in params.glyph_sources.effective_order() {
95        source.hash(&mut hasher);
96    }
97
98    let mut denied: Vec<_> = params.glyph_sources.deny.iter().copied().collect();
99    denied.sort();
100    for deny in denied {
101        deny.hash(&mut hasher);
102    }
103
104    hasher.finish()
105}
106
107/// Byte-weighted render output cache
108///
109/// Uses byte-weighted eviction (not entry count) to prevent memory explosions.
110/// Default limit is 512 MB, configurable via `TYPF_CACHE_MAX_BYTES`.
111pub struct GlyphCache {
112    cache: RenderOutputCache<GlyphCacheKey>,
113}
114
115impl GlyphCache {
116    /// Create a cache with the default byte limit (512 MB or env override).
117    pub fn new() -> Self {
118        Self {
119            cache: RenderOutputCache::with_default_limit(),
120        }
121    }
122
123    /// Create a cache with a specific byte limit.
124    pub fn with_max_bytes(max_bytes: u64) -> Self {
125        Self {
126            cache: RenderOutputCache::new(max_bytes),
127        }
128    }
129
130    /// Get a cached render output.
131    ///
132    /// Returns `None` if not found or if caching is globally disabled.
133    pub fn get(&self, key: &GlyphCacheKey) -> Option<RenderOutput> {
134        if !cache_config::is_caching_enabled() {
135            return None;
136        }
137        self.cache.get(key)
138    }
139
140    /// Insert a render output into the cache.
141    ///
142    /// Does nothing if caching is globally disabled.
143    /// Large outputs may be evicted sooner due to byte-weighted eviction.
144    pub fn insert(&self, key: GlyphCacheKey, output: RenderOutput) {
145        if !cache_config::is_caching_enabled() {
146            return;
147        }
148        self.cache.insert(key, output);
149    }
150
151    pub fn hit_rate(&self) -> f64 {
152        self.cache.hit_rate()
153    }
154
155    pub fn metrics(&self) -> crate::cache::CacheMetrics {
156        self.cache.metrics()
157    }
158
159    /// Current weighted size in bytes.
160    pub fn weighted_size(&self) -> u64 {
161        self.cache.weighted_size()
162    }
163
164    /// Number of entries in cache.
165    pub fn entry_count(&self) -> u64 {
166        self.cache.entry_count()
167    }
168
169    /// Clear all cached entries.
170    pub fn clear(&self) {
171        self.cache.clear();
172    }
173}
174
175impl Default for GlyphCache {
176    fn default() -> Self {
177        Self::new()
178    }
179}
180
181/// Thread-safe shared glyph cache
182pub type SharedGlyphCache = Arc<RwLock<GlyphCache>>;
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187    use crate::types::{Direction, PositionedGlyph};
188
189    fn shaped() -> ShapingResult {
190        ShapingResult {
191            glyphs: vec![PositionedGlyph {
192                id: 42,
193                x: 1.5,
194                y: 0.0,
195                advance: 10.0,
196                cluster: 0,
197            }],
198            advance_width: 10.0,
199            advance_height: 16.0,
200            direction: Direction::LeftToRight,
201        }
202    }
203
204    fn render_params() -> RenderParams {
205        RenderParams::default()
206    }
207
208    #[test]
209    fn key_changes_with_renderer() {
210        let s = shaped();
211        let p = render_params();
212        let k1 = GlyphCacheKey::new("r1", b"font", &s, &p);
213        let k2 = GlyphCacheKey::new("r2", b"font", &s, &p);
214        assert_ne!(k1, k2);
215    }
216
217    #[test]
218    fn cache_stores_and_retrieves() {
219        let _guard = crate::cache_config::scoped_caching_enabled(true);
220
221        let cache = GlyphCache::new();
222        let key = GlyphCacheKey::new("r1", b"font", &shaped(), &render_params());
223        let output = RenderOutput::Json("x".into());
224
225        cache.insert(key.clone(), output.clone());
226        let hit = match cache.get(&key) {
227            Some(hit) => hit,
228            None => unreachable!("cache should return stored value"),
229        };
230
231        if let RenderOutput::Json(body) = hit {
232            assert_eq!(body, "x");
233        } else {
234            unreachable!("expected json");
235        }
236    }
237}