1use std::collections::HashMap;
6use std::time::{Duration, Instant};
7
8use crate::image::TextureData;
9
10#[derive(Debug)]
12struct CacheEntry {
13 data: TextureData,
15 last_accessed: Instant,
17 size_bytes: usize,
19 ref_count: u32,
21}
22
23#[derive(Debug, Clone)]
25pub struct TextureCacheConfig {
26 pub max_size_bytes: usize,
28 pub max_age: Duration,
30 pub max_entries: usize,
32}
33
34impl Default for TextureCacheConfig {
35 fn default() -> Self {
36 Self {
37 max_size_bytes: 256 * 1024 * 1024, max_age: Duration::from_secs(300), max_entries: 1000,
40 }
41 }
42}
43
44pub struct TextureCache {
48 entries: HashMap<String, CacheEntry>,
50 config: TextureCacheConfig,
52 current_size: usize,
54 stats: CacheStats,
56}
57
58#[derive(Debug, Clone, Default)]
60pub struct CacheStats {
61 pub hits: u64,
63 pub misses: u64,
65 pub evictions: u64,
67 pub bytes_loaded: u64,
69}
70
71impl TextureCache {
72 #[must_use]
74 pub fn new() -> Self {
75 Self::with_config(TextureCacheConfig::default())
76 }
77
78 #[must_use]
80 pub fn with_config(config: TextureCacheConfig) -> Self {
81 Self {
82 entries: HashMap::new(),
83 config,
84 current_size: 0,
85 stats: CacheStats::default(),
86 }
87 }
88
89 pub fn get(&mut self, key: &str) -> Option<&TextureData> {
93 if let Some(entry) = self.entries.get_mut(key) {
94 entry.last_accessed = Instant::now();
95 entry.ref_count += 1;
96 self.stats.hits += 1;
97 Some(&entry.data)
98 } else {
99 self.stats.misses += 1;
100 None
101 }
102 }
103
104 pub fn insert(&mut self, key: String, data: TextureData) {
108 let size_bytes = data.data.len();
109
110 if let Some(old) = self.entries.remove(&key) {
112 self.current_size -= old.size_bytes;
113 }
114
115 self.evict_if_needed(size_bytes);
117
118 self.current_size += size_bytes;
120 self.stats.bytes_loaded += size_bytes as u64;
121
122 self.entries.insert(
123 key,
124 CacheEntry {
125 data,
126 last_accessed: Instant::now(),
127 size_bytes,
128 ref_count: 1,
129 },
130 );
131 }
132
133 pub fn get_or_insert_with<F>(&mut self, key: &str, loader: F) -> &TextureData
137 where
138 F: FnOnce() -> TextureData,
139 {
140 if !self.entries.contains_key(key) {
141 let data = loader();
142 self.insert(key.to_string(), data);
143 }
144
145 if let Some(entry) = self.entries.get_mut(key) {
149 entry.last_accessed = Instant::now();
150 entry.ref_count += 1;
151 self.stats.hits += 1;
152 &entry.data
153 } else {
154 static FALLBACK: std::sync::OnceLock<TextureData> = std::sync::OnceLock::new();
156 FALLBACK.get_or_init(|| TextureData {
157 width: 1,
158 height: 1,
159 data: vec![0, 0, 0, 0],
160 format: crate::image::ImageFormat::Unknown,
161 })
162 }
163 }
164
165 pub fn remove(&mut self, key: &str) -> Option<TextureData> {
167 if let Some(entry) = self.entries.remove(key) {
168 self.current_size -= entry.size_bytes;
169 Some(entry.data)
170 } else {
171 None
172 }
173 }
174
175 #[must_use]
177 pub fn contains(&self, key: &str) -> bool {
178 self.entries.contains_key(key)
179 }
180
181 pub fn clear(&mut self) {
183 self.entries.clear();
184 self.current_size = 0;
185 }
186
187 #[must_use]
189 pub fn len(&self) -> usize {
190 self.entries.len()
191 }
192
193 #[must_use]
195 pub fn is_empty(&self) -> bool {
196 self.entries.is_empty()
197 }
198
199 #[must_use]
201 pub fn size_bytes(&self) -> usize {
202 self.current_size
203 }
204
205 #[must_use]
207 pub fn stats(&self) -> &CacheStats {
208 &self.stats
209 }
210
211 fn evict_if_needed(&mut self, needed_bytes: usize) {
213 while self.current_size + needed_bytes > self.config.max_size_bytes
215 && !self.entries.is_empty()
216 {
217 self.evict_lru();
218 }
219
220 while self.entries.len() >= self.config.max_entries && !self.entries.is_empty() {
222 self.evict_lru();
223 }
224
225 self.evict_expired();
227 }
228
229 fn evict_lru(&mut self) {
231 let oldest_key = self
232 .entries
233 .iter()
234 .min_by_key(|(_, entry)| entry.last_accessed)
235 .map(|(key, _)| key.clone());
236
237 if let Some(key) = oldest_key {
238 if let Some(entry) = self.entries.remove(&key) {
239 self.current_size -= entry.size_bytes;
240 self.stats.evictions += 1;
241 }
242 }
243 }
244
245 fn evict_expired(&mut self) {
247 let now = Instant::now();
248 let max_age = self.config.max_age;
249
250 let expired_keys: Vec<String> = self
251 .entries
252 .iter()
253 .filter(|(_, entry)| now.duration_since(entry.last_accessed) > max_age)
254 .map(|(key, _)| key.clone())
255 .collect();
256
257 for key in expired_keys {
258 if let Some(entry) = self.entries.remove(&key) {
259 self.current_size -= entry.size_bytes;
260 self.stats.evictions += 1;
261 }
262 }
263 }
264
265 pub fn maintenance(&mut self) {
267 self.evict_expired();
268 }
269}
270
271impl Default for TextureCache {
272 fn default() -> Self {
273 Self::new()
274 }
275}
276
277#[cfg(feature = "gpu")]
279pub mod sync {
280 use std::sync::{Arc, RwLock};
281
282 use super::{CacheStats, TextureCache, TextureCacheConfig};
283 use crate::image::TextureData;
284
285 #[derive(Clone)]
287 pub struct SyncTextureCache {
288 inner: Arc<RwLock<TextureCache>>,
289 }
290
291 impl SyncTextureCache {
292 #[must_use]
294 pub fn new() -> Self {
295 Self {
296 inner: Arc::new(RwLock::new(TextureCache::new())),
297 }
298 }
299
300 #[must_use]
302 pub fn with_config(config: TextureCacheConfig) -> Self {
303 Self {
304 inner: Arc::new(RwLock::new(TextureCache::with_config(config))),
305 }
306 }
307
308 #[must_use]
310 pub fn get(&self, key: &str) -> Option<TextureData> {
311 let mut cache = self.inner.write().ok()?;
312 cache.get(key).cloned()
313 }
314
315 pub fn insert(&self, key: String, data: TextureData) {
317 if let Ok(mut cache) = self.inner.write() {
318 cache.insert(key, data);
319 }
320 }
321
322 #[must_use]
324 pub fn contains(&self, key: &str) -> bool {
325 self.inner
326 .read()
327 .map(|cache| cache.contains(key))
328 .unwrap_or(false)
329 }
330
331 #[must_use]
333 pub fn stats(&self) -> Option<CacheStats> {
334 self.inner.read().ok().map(|cache| cache.stats().clone())
335 }
336 }
337
338 impl Default for SyncTextureCache {
339 fn default() -> Self {
340 Self::new()
341 }
342 }
343}
344
345#[cfg(test)]
346mod tests {
347 use std::time::Duration;
348
349 use super::{TextureCache, TextureCacheConfig};
350 use crate::image::create_solid_color;
351
352 #[test]
353 fn test_cache_insert_and_get() {
354 let mut cache = TextureCache::new();
355 let texture = create_solid_color(10, 10, 255, 0, 0, 255);
356
357 cache.insert("test".to_string(), texture.clone());
358
359 assert!(cache.contains("test"));
360 assert_eq!(cache.len(), 1);
361
362 let retrieved = cache.get("test");
363 assert!(retrieved.is_some());
364 assert_eq!(retrieved.unwrap().width, 10);
365 }
366
367 #[test]
368 fn test_cache_miss() {
369 let mut cache = TextureCache::new();
370 assert!(cache.get("nonexistent").is_none());
371 assert_eq!(cache.stats().misses, 1);
372 }
373
374 #[test]
375 fn test_cache_eviction_by_size() {
376 let config = TextureCacheConfig {
377 max_size_bytes: 1000, max_age: Duration::from_secs(3600),
379 max_entries: 100,
380 };
381
382 let mut cache = TextureCache::with_config(config);
383
384 let texture = create_solid_color(20, 20, 255, 0, 0, 255); cache.insert("big".to_string(), texture);
387
388 assert!(cache.contains("big"));
390 }
391
392 #[test]
393 fn test_cache_eviction_by_count() {
394 let config = TextureCacheConfig {
395 max_size_bytes: 1024 * 1024,
396 max_age: Duration::from_secs(3600),
397 max_entries: 2,
398 };
399
400 let mut cache = TextureCache::with_config(config);
401
402 cache.insert("a".to_string(), create_solid_color(2, 2, 255, 0, 0, 255));
403 cache.insert("b".to_string(), create_solid_color(2, 2, 0, 255, 0, 255));
404 cache.insert("c".to_string(), create_solid_color(2, 2, 0, 0, 255, 255));
405
406 assert!(cache.len() <= 2);
408 }
409
410 #[test]
411 fn test_cache_remove() {
412 let mut cache = TextureCache::new();
413 let texture = create_solid_color(10, 10, 255, 0, 0, 255);
414
415 cache.insert("test".to_string(), texture);
416 assert!(cache.contains("test"));
417
418 let removed = cache.remove("test");
419 assert!(removed.is_some());
420 assert!(!cache.contains("test"));
421 }
422
423 #[test]
424 fn test_cache_clear() {
425 let mut cache = TextureCache::new();
426
427 cache.insert("a".to_string(), create_solid_color(2, 2, 255, 0, 0, 255));
428 cache.insert("b".to_string(), create_solid_color(2, 2, 0, 255, 0, 255));
429
430 assert_eq!(cache.len(), 2);
431
432 cache.clear();
433
434 assert_eq!(cache.len(), 0);
435 assert_eq!(cache.size_bytes(), 0);
436 }
437
438 #[test]
439 fn test_get_or_insert_with() {
440 let mut cache = TextureCache::new();
441
442 let data =
444 cache.get_or_insert_with("lazy", || create_solid_color(5, 5, 128, 128, 128, 255));
445
446 assert_eq!(data.width, 5);
447 assert!(cache.contains("lazy"));
448
449 let data2 = cache.get_or_insert_with("lazy", || create_solid_color(10, 10, 0, 0, 0, 255));
451
452 assert_eq!(data2.width, 5); }
454
455 #[test]
456 fn test_cache_stats() {
457 let mut cache = TextureCache::new();
458
459 cache.insert("a".to_string(), create_solid_color(2, 2, 255, 0, 0, 255));
460
461 let _ = cache.get("a"); let _ = cache.get("b"); let _ = cache.get("a"); let stats = cache.stats();
466 assert_eq!(stats.hits, 2);
467 assert_eq!(stats.misses, 1);
468 }
469}