1use std::collections::{BTreeMap, VecDeque};
16use thiserror::Error;
17
18#[derive(Debug, Clone, PartialEq, Error)]
22pub enum CacheError {
23 #[error("Texel coordinate ({x}, {y}) out of bounds ({width}x{height})")]
25 OutOfBounds {
26 x: u32,
27 y: u32,
28 width: u32,
29 height: u32,
30 },
31 #[error("Cache capacity must be at least 1")]
33 ZeroCapacity,
34 #[error("Tile size must be at least 1")]
36 ZeroTileSize,
37 #[error("Texture dimensions must be at least 1×1")]
39 ZeroDimensions,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
46pub struct TileKey {
47 pub tile_x: u32,
49 pub tile_y: u32,
51}
52
53impl TileKey {
54 #[must_use]
56 pub fn from_texel(x: u32, y: u32, tile_size: u32) -> Self {
57 Self {
58 tile_x: x / tile_size,
59 tile_y: y / tile_size,
60 }
61 }
62}
63
64#[derive(Debug, Clone, Default)]
68pub struct TexCacheStats {
69 pub hits: u64,
71 pub misses: u64,
73 pub prefetches: u64,
75 pub evictions: u64,
77}
78
79impl TexCacheStats {
80 #[must_use]
82 pub fn hit_rate(&self) -> f64 {
83 let total = self.hits + self.misses;
84 if total == 0 {
85 0.0
86 } else {
87 self.hits as f64 / total as f64
88 }
89 }
90
91 #[must_use]
93 pub fn total_accesses(&self) -> u64 {
94 self.hits + self.misses
95 }
96}
97
98pub struct TextureCache {
112 pub width: u32,
114 pub height: u32,
116 pub tile_size: u32,
118 capacity: usize,
120 lines: BTreeMap<TileKey, Vec<u8>>,
122 lru: VecDeque<TileKey>,
124 stats: TexCacheStats,
126 texture_data: Vec<u8>,
128}
129
130impl TextureCache {
131 pub fn new(
140 width: u32,
141 height: u32,
142 tile_size: u32,
143 capacity: usize,
144 texture_data: Vec<u8>,
145 ) -> Result<Self, CacheError> {
146 if width == 0 || height == 0 {
147 return Err(CacheError::ZeroDimensions);
148 }
149 if tile_size == 0 {
150 return Err(CacheError::ZeroTileSize);
151 }
152 if capacity == 0 {
153 return Err(CacheError::ZeroCapacity);
154 }
155 let total = (width as usize) * (height as usize);
156 let mut padded = texture_data;
157 padded.resize(total, 0);
158
159 Ok(Self {
160 width,
161 height,
162 tile_size,
163 capacity,
164 lines: BTreeMap::new(),
165 lru: VecDeque::new(),
166 stats: TexCacheStats::default(),
167 texture_data: padded,
168 })
169 }
170
171 pub fn fetch(&mut self, x: u32, y: u32) -> Result<u8, CacheError> {
180 self.check_bounds(x, y)?;
181 let key = TileKey::from_texel(x, y, self.tile_size);
182 if self.lines.contains_key(&key) {
183 self.stats.hits += 1;
184 self.promote_lru(key);
185 } else {
186 self.stats.misses += 1;
187 self.load_tile(key);
188 }
189 let tile = &self.lines[&key];
190 let local_x = (x % self.tile_size) as usize;
191 let local_y = (y % self.tile_size) as usize;
192 Ok(tile[local_y * self.tile_size as usize + local_x])
193 }
194
195 pub fn prefetch(&mut self, x: u32, y: u32) -> Result<(), CacheError> {
205 self.check_bounds(x, y)?;
206 let key = TileKey::from_texel(x, y, self.tile_size);
207 if !self.lines.contains_key(&key) {
208 self.load_tile(key);
209 self.stats.prefetches += 1;
210 }
211 Ok(())
212 }
213
214 pub fn flush(&mut self) {
218 self.lines.clear();
219 self.lru.clear();
220 }
221
222 pub fn invalidate_tile(&mut self, key: TileKey) -> bool {
226 if self.lines.remove(&key).is_some() {
227 self.lru.retain(|k| k != &key);
228 true
229 } else {
230 false
231 }
232 }
233
234 #[must_use]
236 pub fn resident_count(&self) -> usize {
237 self.lines.len()
238 }
239
240 #[must_use]
242 pub fn is_resident(&self, key: TileKey) -> bool {
243 self.lines.contains_key(&key)
244 }
245
246 #[must_use]
248 pub fn stats(&self) -> &TexCacheStats {
249 &self.stats
250 }
251
252 pub fn reset_stats(&mut self) {
254 self.stats = TexCacheStats::default();
255 }
256
257 #[must_use]
259 pub fn capacity(&self) -> usize {
260 self.capacity
261 }
262
263 #[must_use]
265 pub fn grid_size(&self) -> (u32, u32) {
266 let tiles_x = (self.width + self.tile_size - 1) / self.tile_size;
267 let tiles_y = (self.height + self.tile_size - 1) / self.tile_size;
268 (tiles_x, tiles_y)
269 }
270
271 fn check_bounds(&self, x: u32, y: u32) -> Result<(), CacheError> {
274 if x >= self.width || y >= self.height {
275 Err(CacheError::OutOfBounds {
276 x,
277 y,
278 width: self.width,
279 height: self.height,
280 })
281 } else {
282 Ok(())
283 }
284 }
285
286 fn load_tile(&mut self, key: TileKey) {
288 if self.lines.len() >= self.capacity {
290 if let Some(victim) = self.lru.pop_back() {
291 self.lines.remove(&victim);
292 self.stats.evictions += 1;
293 }
294 }
295 let ts = self.tile_size as usize;
297 let w = self.width as usize;
298 let tile_x0 = key.tile_x as usize * ts;
299 let tile_y0 = key.tile_y as usize * ts;
300 let mut tile = vec![0u8; ts * ts];
301 for local_y in 0..ts {
302 let global_y = tile_y0 + local_y;
303 if global_y >= self.height as usize {
304 break;
305 }
306 for local_x in 0..ts {
307 let global_x = tile_x0 + local_x;
308 if global_x >= self.width as usize {
309 break;
310 }
311 tile[local_y * ts + local_x] = self.texture_data[global_y * w + global_x];
312 }
313 }
314 self.lines.insert(key, tile);
315 self.lru.push_front(key);
316 }
317
318 fn promote_lru(&mut self, key: TileKey) {
320 self.lru.retain(|k| k != &key);
321 self.lru.push_front(key);
322 }
323}
324
325#[cfg(test)]
328mod tests {
329 use super::*;
330
331 fn make_texture(width: u32, height: u32) -> Vec<u8> {
333 (0..(width * height)).map(|i| (i % 256) as u8).collect()
334 }
335
336 fn make_cache(capacity: usize, tile_size: u32) -> Result<TextureCache, CacheError> {
337 let data = make_texture(16, 16);
338 TextureCache::new(16, 16, tile_size, capacity, data)
339 }
340
341 #[test]
344 fn test_tile_key_from_texel() {
345 let key = TileKey::from_texel(9, 5, 4);
346 assert_eq!(key.tile_x, 2); assert_eq!(key.tile_y, 1); }
349
350 #[test]
351 fn test_tile_key_origin() {
352 let key = TileKey::from_texel(0, 0, 8);
353 assert_eq!(key.tile_x, 0);
354 assert_eq!(key.tile_y, 0);
355 }
356
357 #[test]
360 fn test_stats_hit_rate_zero_accesses() {
361 let s = TexCacheStats::default();
362 assert_eq!(s.hit_rate(), 0.0);
363 }
364
365 #[test]
366 fn test_stats_hit_rate_all_hits() {
367 let s = TexCacheStats {
368 hits: 10,
369 misses: 0,
370 ..Default::default()
371 };
372 assert!((s.hit_rate() - 1.0).abs() < 1e-9);
373 }
374
375 #[test]
376 fn test_stats_total_accesses() {
377 let s = TexCacheStats {
378 hits: 3,
379 misses: 7,
380 ..Default::default()
381 };
382 assert_eq!(s.total_accesses(), 10);
383 }
384
385 #[test]
388 fn test_new_zero_capacity_error() {
389 let err = TextureCache::new(8, 8, 4, 0, vec![]);
390 assert!(matches!(err, Err(CacheError::ZeroCapacity)));
391 }
392
393 #[test]
394 fn test_new_zero_tile_size_error() {
395 let err = TextureCache::new(8, 8, 0, 4, vec![]);
396 assert!(matches!(err, Err(CacheError::ZeroTileSize)));
397 }
398
399 #[test]
400 fn test_new_zero_dimensions_error() {
401 let err = TextureCache::new(0, 8, 4, 4, vec![]);
402 assert!(matches!(err, Err(CacheError::ZeroDimensions)));
403 }
404
405 #[test]
408 fn test_fetch_first_access_is_miss() -> Result<(), CacheError> {
409 let mut cache = make_cache(8, 4)?;
410 let _ = cache.fetch(0, 0)?;
411 assert_eq!(cache.stats().misses, 1);
412 assert_eq!(cache.stats().hits, 0);
413 Ok(())
414 }
415
416 #[test]
417 fn test_fetch_second_access_same_tile_is_hit() -> Result<(), CacheError> {
418 let mut cache = make_cache(8, 4)?;
419 let _ = cache.fetch(0, 0)?;
420 let _ = cache.fetch(1, 1)?; assert_eq!(cache.stats().misses, 1);
422 assert_eq!(cache.stats().hits, 1);
423 Ok(())
424 }
425
426 #[test]
427 fn test_fetch_correct_texel_value() -> Result<(), CacheError> {
428 let data = make_texture(16, 16);
429 let expected_at_3_5 = data[5 * 16 + 3];
430 let mut cache = TextureCache::new(16, 16, 4, 8, data)?;
431 let val = cache.fetch(3, 5)?;
432 assert_eq!(val, expected_at_3_5);
433 Ok(())
434 }
435
436 #[test]
437 fn test_fetch_out_of_bounds() -> Result<(), CacheError> {
438 let mut cache = make_cache(4, 4)?;
439 let err = cache.fetch(16, 0);
440 assert!(matches!(err, Err(CacheError::OutOfBounds { .. })));
441 Ok(())
442 }
443
444 #[test]
447 fn test_lru_eviction_when_full() -> Result<(), CacheError> {
448 let mut cache = make_cache(2, 4)?; cache.fetch(0, 0)?; cache.fetch(4, 0)?; cache.fetch(8, 0)?; assert_eq!(cache.stats().evictions, 1);
455 assert_eq!(cache.resident_count(), 2);
456 Ok(())
457 }
458
459 #[test]
460 fn test_lru_promotion_prevents_eviction() -> Result<(), CacheError> {
461 let mut cache = make_cache(2, 4)?;
462 cache.fetch(0, 0)?; cache.fetch(4, 0)?; cache.fetch(0, 0)?; cache.fetch(8, 0)?;
467 let key_0_0 = TileKey {
468 tile_x: 0,
469 tile_y: 0,
470 };
471 assert!(
472 cache.is_resident(key_0_0),
473 "promoted tile should survive eviction"
474 );
475 Ok(())
476 }
477
478 #[test]
481 fn test_prefetch_loads_tile_without_hit_count() -> Result<(), CacheError> {
482 let mut cache = make_cache(8, 4)?;
483 cache.prefetch(0, 0)?;
484 assert_eq!(cache.stats().prefetches, 1);
485 assert_eq!(cache.stats().hits, 0);
486 assert_eq!(cache.stats().misses, 0);
487 cache.fetch(0, 0)?;
489 assert_eq!(cache.stats().hits, 1);
490 Ok(())
491 }
492
493 #[test]
494 fn test_prefetch_already_resident_is_noop() -> Result<(), CacheError> {
495 let mut cache = make_cache(8, 4)?;
496 cache.fetch(0, 0)?; cache.prefetch(0, 0)?; assert_eq!(cache.stats().prefetches, 0);
499 Ok(())
500 }
501
502 #[test]
505 fn test_flush_clears_cache() -> Result<(), CacheError> {
506 let mut cache = make_cache(8, 4)?;
507 cache.fetch(0, 0)?;
508 cache.fetch(4, 0)?;
509 assert_eq!(cache.resident_count(), 2);
510 cache.flush();
511 assert_eq!(cache.resident_count(), 0);
512 assert_eq!(cache.stats().misses, 2);
514 Ok(())
515 }
516
517 #[test]
518 fn test_invalidate_tile() -> Result<(), CacheError> {
519 let mut cache = make_cache(8, 4)?;
520 cache.fetch(0, 0)?;
521 let key = TileKey {
522 tile_x: 0,
523 tile_y: 0,
524 };
525 assert!(cache.invalidate_tile(key));
526 assert!(!cache.is_resident(key));
527 assert!(!cache.invalidate_tile(key));
529 Ok(())
530 }
531
532 #[test]
535 fn test_grid_size_exact_division() -> Result<(), CacheError> {
536 let data = vec![0u8; 64];
537 let cache = TextureCache::new(8, 8, 4, 4, data)?;
538 assert_eq!(cache.grid_size(), (2, 2));
539 Ok(())
540 }
541
542 #[test]
543 fn test_grid_size_non_exact() -> Result<(), CacheError> {
544 let data = vec![0u8; 100];
545 let cache = TextureCache::new(10, 10, 4, 4, data)?;
546 assert_eq!(cache.grid_size(), (3, 3));
548 Ok(())
549 }
550}