Skip to main content

oxitext_shape/
cache.rs

1//! Bounded LRU shape cache for [`crate::SwashShaper`].
2//!
3//! [`ShapeCache`] wraps an [`lru::LruCache`] in an `RwLock` so it can be
4//! shared across threads.  Cache keys ([`ShapeKey`]) identify a unique
5//! shaping request by font identity, text content, and a hash of any
6//! variation-axis settings in use.
7//!
8//! ## Font identity
9//!
10//! The `font_id` field is the pointer address of the `Arc<[u8]>` holding
11//! the font bytes.  This is the same keying strategy used throughout the
12//! raster pipeline.  Callers **must** hold the same `Arc` alive across
13//! calls for cache hits to occur; a newly constructed `Arc` from the same
14//! bytes will have a different pointer and thus be treated as a cache miss.
15//!
16//! ## Variation axes
17//!
18//! `axis_values_hash` reserves a slot for future variation-axis support.
19//! Set it to `0` for non-variable fonts (the common case in Slice 5a).
20
21use lru::LruCache;
22use oxitext_core::ShapedRun;
23use std::num::NonZeroUsize;
24use std::sync::{Arc, RwLock};
25
26/// Stable identifier for a font resource, derived from `Arc<[u8]>` pointer.
27pub type FontId = u64;
28
29/// Cache key for a single shaping request.
30#[derive(Debug, Clone, PartialEq, Eq, Hash)]
31pub struct ShapeKey {
32    /// Pointer identity of the `Arc<[u8]>` font bytes.
33    pub font_id: FontId,
34    /// The exact UTF-8 text being shaped.
35    pub text: String,
36    /// Hash of any OpenType variation axis settings in use.  Use `0` for
37    /// non-variable fonts.
38    pub axis_values_hash: u64,
39}
40
41impl ShapeKey {
42    /// Construct a key from a font `Arc`, text, and an optional axis hash.
43    ///
44    /// Passes `0` for `axis_values_hash` when called on non-variable fonts.
45    pub fn new(font_data: &Arc<[u8]>, text: &str, axis_values_hash: u64) -> Self {
46        Self {
47            font_id: Arc::as_ptr(font_data) as *const u8 as u64,
48            text: text.to_owned(),
49            axis_values_hash,
50        }
51    }
52}
53
54/// Thread-safe bounded LRU cache for [`ShapedRun`]s.
55///
56/// Wraps [`lru::LruCache`] in an [`RwLock`].  Cache misses trigger a write
57/// lock, cache hits also require a write lock because LRU must update the
58/// recency order on every `get`.
59pub struct ShapeCache {
60    inner: RwLock<LruCache<ShapeKey, Arc<ShapedRun>>>,
61}
62
63impl ShapeCache {
64    /// Creates a new cache with the given capacity.
65    ///
66    /// If `capacity` is 0, falls back to a minimum capacity of 1 so the cache
67    /// is always usable without panicking.
68    pub fn new(capacity: usize) -> Self {
69        let cap = NonZeroUsize::new(capacity).unwrap_or(NonZeroUsize::MIN);
70        ShapeCache {
71            inner: RwLock::new(LruCache::new(cap)),
72        }
73    }
74
75    /// Look up a cached [`ShapedRun`] by key.
76    ///
77    /// Returns `Some(Arc<ShapedRun>)` on a cache hit and updates the LRU order.
78    /// Returns `None` on a miss or if the lock is poisoned.
79    pub fn get(&self, key: &ShapeKey) -> Option<Arc<ShapedRun>> {
80        self.inner.write().ok()?.get(key).cloned()
81    }
82
83    /// Insert a [`ShapedRun`] into the cache.
84    ///
85    /// Evicts the least-recently-used entry if the cache is at capacity.
86    /// Silently no-ops if the lock is poisoned.
87    pub fn insert(&self, key: ShapeKey, run: Arc<ShapedRun>) {
88        if let Ok(mut cache) = self.inner.write() {
89            cache.put(key, run);
90        }
91    }
92
93    /// Returns the number of entries currently in the cache.
94    pub fn len(&self) -> usize {
95        self.inner.read().ok().map_or(0, |g| g.len())
96    }
97
98    /// Returns `true` if the cache contains no entries.
99    pub fn is_empty(&self) -> bool {
100        self.len() == 0
101    }
102}
103
104impl std::fmt::Debug for ShapeCache {
105    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106        let len = self.len();
107        f.debug_struct("ShapeCache").field("len", &len).finish()
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114    use oxitext_core::{ShapedGlyph, ShapedRun};
115
116    fn dummy_run(font_data: Arc<[u8]>) -> Arc<ShapedRun> {
117        Arc::new(ShapedRun {
118            glyphs: smallvec::smallvec![ShapedGlyph {
119                gid: 1,
120                x_advance: 10.0,
121                ..Default::default()
122            }],
123            font_data,
124        })
125    }
126
127    #[test]
128    fn shape_cache_miss_then_hit() {
129        let cache = ShapeCache::new(16);
130        let font: Arc<[u8]> = Arc::from(vec![0u8; 4]);
131        let key = ShapeKey::new(&font, "hello", 0);
132
133        assert!(cache.get(&key).is_none(), "expected miss on empty cache");
134        assert_eq!(cache.len(), 0);
135
136        let run = dummy_run(Arc::clone(&font));
137        cache.insert(key.clone(), Arc::clone(&run));
138
139        let hit = cache.get(&key).expect("expected hit after insert");
140        assert_eq!(hit.glyphs[0].gid, 1);
141        assert_eq!(cache.len(), 1);
142    }
143
144    #[test]
145    fn shape_cache_eviction_at_capacity_one() {
146        let cache = ShapeCache::new(1);
147        let font: Arc<[u8]> = Arc::from(vec![0u8; 4]);
148
149        let key_a = ShapeKey::new(&font, "aaa", 0);
150        let key_b = ShapeKey::new(&font, "bbb", 0);
151
152        let run_a = dummy_run(Arc::clone(&font));
153        let run_b = dummy_run(Arc::clone(&font));
154
155        cache.insert(key_a.clone(), run_a);
156        assert_eq!(cache.len(), 1);
157
158        cache.insert(key_b.clone(), run_b);
159        assert_eq!(
160            cache.len(),
161            1,
162            "capacity 1 — still one entry after second insert"
163        );
164
165        // key_a must have been evicted.
166        assert!(cache.get(&key_a).is_none(), "key_a should be evicted");
167        assert!(cache.get(&key_b).is_some(), "key_b should be present");
168    }
169
170    #[test]
171    fn shape_cache_zero_capacity_fallback() {
172        // capacity 0 falls back to 1; should not panic.
173        let cache = ShapeCache::new(0);
174        let font: Arc<[u8]> = Arc::from(vec![0u8; 4]);
175        let key = ShapeKey::new(&font, "x", 0);
176        let run = dummy_run(Arc::clone(&font));
177        cache.insert(key.clone(), run);
178        assert!(cache.get(&key).is_some());
179    }
180
181    #[test]
182    fn shape_key_identity_uses_arc_pointer() {
183        // Two Arcs with the same bytes but different allocations must differ.
184        let bytes = vec![1u8, 2u8, 3u8];
185        let arc1: Arc<[u8]> = Arc::from(bytes.clone());
186        let arc2: Arc<[u8]> = Arc::from(bytes);
187        let k1 = ShapeKey::new(&arc1, "hi", 0);
188        let k2 = ShapeKey::new(&arc2, "hi", 0);
189        assert_ne!(
190            k1, k2,
191            "different Arc allocations must produce different keys"
192        );
193    }
194}