Skip to main content

ass_renderer/cache/
mod.rs

1//! Caching system for expensive operations
2
3use crate::pipeline::shaping::ShapedText;
4use tiny_skia::Path;
5
6#[cfg(not(feature = "nostd"))]
7use std::collections::HashMap;
8#[cfg(not(feature = "nostd"))]
9use std::sync::Arc;
10
11#[cfg(feature = "nostd")]
12use alloc::collections::BTreeMap as HashMap;
13#[cfg(feature = "nostd")]
14use alloc::{string::String, sync::Arc};
15
16/// Cache key for shaped text
17#[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)]
18pub struct TextCacheKey {
19    /// Text content to be shaped
20    pub text: String,
21    /// Font family name
22    pub font_family: String,
23    /// Font size (rounded to avoid float comparison issues)
24    pub font_size: u32,
25    /// Whether text should be bold
26    pub bold: bool,
27    /// Whether text should be italic
28    pub italic: bool,
29}
30
31/// Cache key for drawing paths
32#[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)]
33pub struct DrawingCacheKey {
34    /// Drawing commands as string
35    pub commands: String,
36}
37
38/// Render cache for expensive operations
39pub struct RenderCache {
40    /// Cache for shaped text
41    shaped_text_cache: HashMap<TextCacheKey, Arc<ShapedText>>,
42    max_shaped_entries: usize,
43
44    /// Cache for drawing paths
45    drawing_path_cache: HashMap<DrawingCacheKey, Option<Path>>,
46    max_drawing_entries: usize,
47
48    /// Cache statistics
49    pub stats: CacheStats,
50}
51
52/// Cache statistics for monitoring
53#[derive(Debug, Default, Clone)]
54pub struct CacheStats {
55    /// Number of text cache hits
56    pub text_hits: usize,
57    /// Number of text cache misses
58    pub text_misses: usize,
59    /// Number of drawing cache hits
60    pub drawing_hits: usize,
61    /// Number of drawing cache misses
62    pub drawing_misses: usize,
63    /// Number of cache evictions
64    pub evictions: usize,
65}
66
67impl RenderCache {
68    /// Create a new render cache
69    pub fn new() -> Self {
70        Self {
71            shaped_text_cache: HashMap::new(),
72            max_shaped_entries: 1000,
73            drawing_path_cache: HashMap::new(),
74            max_drawing_entries: 500,
75            stats: CacheStats::default(),
76        }
77    }
78
79    /// Create with custom limits
80    pub fn with_limits(max_shaped: usize, max_drawing: usize) -> Self {
81        Self {
82            shaped_text_cache: HashMap::new(),
83            max_shaped_entries: max_shaped,
84            drawing_path_cache: HashMap::new(),
85            max_drawing_entries: max_drawing,
86            stats: CacheStats::default(),
87        }
88    }
89
90    /// Get shaped text from cache
91    pub fn get_shaped_text(&mut self, key: &TextCacheKey) -> Option<Arc<ShapedText>> {
92        if let Some(shaped) = self.shaped_text_cache.get(key) {
93            self.stats.text_hits += 1;
94            Some(Arc::clone(shaped))
95        } else {
96            self.stats.text_misses += 1;
97            None
98        }
99    }
100
101    /// Store shaped text in cache
102    pub fn store_shaped_text(&mut self, key: TextCacheKey, shaped: ShapedText) -> Arc<ShapedText> {
103        // Evict if at capacity
104        if self.shaped_text_cache.len() >= self.max_shaped_entries {
105            // Simple LRU: remove first item (not ideal but simple)
106            if let Some(first_key) = self.shaped_text_cache.keys().next().cloned() {
107                self.shaped_text_cache.remove(&first_key);
108                self.stats.evictions += 1;
109            }
110        }
111
112        let arc_shaped = Arc::new(shaped);
113        self.shaped_text_cache.insert(key, Arc::clone(&arc_shaped));
114        arc_shaped
115    }
116
117    /// Get drawing path from cache
118    pub fn get_drawing_path(&mut self, key: &DrawingCacheKey) -> Option<Option<Path>> {
119        if let Some(path) = self.drawing_path_cache.get(key) {
120            self.stats.drawing_hits += 1;
121            Some(path.clone())
122        } else {
123            self.stats.drawing_misses += 1;
124            None
125        }
126    }
127
128    /// Store drawing path in cache
129    pub fn store_drawing_path(&mut self, key: DrawingCacheKey, path: Option<Path>) {
130        // Evict if at capacity
131        if self.drawing_path_cache.len() >= self.max_drawing_entries {
132            if let Some(first_key) = self.drawing_path_cache.keys().next().cloned() {
133                self.drawing_path_cache.remove(&first_key);
134                self.stats.evictions += 1;
135            }
136        }
137
138        self.drawing_path_cache.insert(key, path);
139    }
140
141    /// Clear all caches
142    pub fn clear(&mut self) {
143        self.shaped_text_cache.clear();
144        self.drawing_path_cache.clear();
145        self.stats = CacheStats::default();
146    }
147
148    /// Get cache statistics
149    pub fn stats(&self) -> &CacheStats {
150        &self.stats
151    }
152
153    /// Print cache statistics
154    pub fn print_stats(&self) {
155        #[cfg(not(feature = "nostd"))]
156        let text_ratio = if self.stats.text_hits + self.stats.text_misses > 0 {
157            self.stats.text_hits as f64 / (self.stats.text_hits + self.stats.text_misses) as f64
158        } else {
159            0.0
160        };
161
162        #[cfg(not(feature = "nostd"))]
163        let drawing_ratio = if self.stats.drawing_hits + self.stats.drawing_misses > 0 {
164            self.stats.drawing_hits as f64
165                / (self.stats.drawing_hits + self.stats.drawing_misses) as f64
166        } else {
167            0.0
168        };
169
170        #[cfg(not(feature = "nostd"))]
171        eprintln!("=== Cache Statistics ===");
172        #[cfg(not(feature = "nostd"))]
173        eprintln!(
174            "Text Cache: {} entries, {:.1}% hit rate ({}/{} hits)",
175            self.shaped_text_cache.len(),
176            text_ratio * 100.0,
177            self.stats.text_hits,
178            self.stats.text_hits + self.stats.text_misses
179        );
180        #[cfg(not(feature = "nostd"))]
181        eprintln!(
182            "Drawing Cache: {} entries, {:.1}% hit rate ({}/{} hits)",
183            self.drawing_path_cache.len(),
184            drawing_ratio * 100.0,
185            self.stats.drawing_hits,
186            self.stats.drawing_hits + self.stats.drawing_misses
187        );
188        #[cfg(not(feature = "nostd"))]
189        eprintln!("Total Evictions: {}", self.stats.evictions);
190    }
191}
192
193impl Default for RenderCache {
194    fn default() -> Self {
195        Self::new()
196    }
197}