use std::collections::HashMap;
use std::time::{Duration, Instant};
use crate::image::TextureData;
#[derive(Debug)]
struct CacheEntry {
data: TextureData,
last_accessed: Instant,
size_bytes: usize,
ref_count: u32,
}
#[derive(Debug, Clone)]
pub struct TextureCacheConfig {
pub max_size_bytes: usize,
pub max_age: Duration,
pub max_entries: usize,
}
impl Default for TextureCacheConfig {
fn default() -> Self {
Self {
max_size_bytes: 256 * 1024 * 1024, max_age: Duration::from_secs(300), max_entries: 1000,
}
}
}
pub struct TextureCache {
entries: HashMap<String, CacheEntry>,
config: TextureCacheConfig,
current_size: usize,
stats: CacheStats,
}
#[derive(Debug, Clone, Default)]
pub struct CacheStats {
pub hits: u64,
pub misses: u64,
pub evictions: u64,
pub bytes_loaded: u64,
}
impl TextureCache {
#[must_use]
pub fn new() -> Self {
Self::with_config(TextureCacheConfig::default())
}
#[must_use]
pub fn with_config(config: TextureCacheConfig) -> Self {
Self {
entries: HashMap::new(),
config,
current_size: 0,
stats: CacheStats::default(),
}
}
pub fn get(&mut self, key: &str) -> Option<&TextureData> {
if let Some(entry) = self.entries.get_mut(key) {
entry.last_accessed = Instant::now();
entry.ref_count += 1;
self.stats.hits += 1;
Some(&entry.data)
} else {
self.stats.misses += 1;
None
}
}
pub fn insert(&mut self, key: String, data: TextureData) {
let size_bytes = data.data.len();
if let Some(old) = self.entries.remove(&key) {
self.current_size -= old.size_bytes;
}
self.evict_if_needed(size_bytes);
self.current_size += size_bytes;
self.stats.bytes_loaded += size_bytes as u64;
self.entries.insert(
key,
CacheEntry {
data,
last_accessed: Instant::now(),
size_bytes,
ref_count: 1,
},
);
}
pub fn get_or_insert_with<F>(&mut self, key: &str, loader: F) -> &TextureData
where
F: FnOnce() -> TextureData,
{
if !self.entries.contains_key(key) {
let data = loader();
self.insert(key.to_string(), data);
}
if let Some(entry) = self.entries.get_mut(key) {
entry.last_accessed = Instant::now();
entry.ref_count += 1;
self.stats.hits += 1;
&entry.data
} else {
static FALLBACK: std::sync::OnceLock<TextureData> = std::sync::OnceLock::new();
FALLBACK.get_or_init(|| TextureData {
width: 1,
height: 1,
data: vec![0, 0, 0, 0],
format: crate::image::ImageFormat::Unknown,
})
}
}
pub fn remove(&mut self, key: &str) -> Option<TextureData> {
if let Some(entry) = self.entries.remove(key) {
self.current_size -= entry.size_bytes;
Some(entry.data)
} else {
None
}
}
#[must_use]
pub fn contains(&self, key: &str) -> bool {
self.entries.contains_key(key)
}
pub fn clear(&mut self) {
self.entries.clear();
self.current_size = 0;
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
#[must_use]
pub fn size_bytes(&self) -> usize {
self.current_size
}
#[must_use]
pub fn stats(&self) -> &CacheStats {
&self.stats
}
fn evict_if_needed(&mut self, needed_bytes: usize) {
while self.current_size + needed_bytes > self.config.max_size_bytes
&& !self.entries.is_empty()
{
self.evict_lru();
}
while self.entries.len() >= self.config.max_entries && !self.entries.is_empty() {
self.evict_lru();
}
self.evict_expired();
}
fn evict_lru(&mut self) {
let oldest_key = self
.entries
.iter()
.min_by_key(|(_, entry)| entry.last_accessed)
.map(|(key, _)| key.clone());
if let Some(key) = oldest_key {
if let Some(entry) = self.entries.remove(&key) {
self.current_size -= entry.size_bytes;
self.stats.evictions += 1;
}
}
}
fn evict_expired(&mut self) {
let now = Instant::now();
let max_age = self.config.max_age;
let expired_keys: Vec<String> = self
.entries
.iter()
.filter(|(_, entry)| now.duration_since(entry.last_accessed) > max_age)
.map(|(key, _)| key.clone())
.collect();
for key in expired_keys {
if let Some(entry) = self.entries.remove(&key) {
self.current_size -= entry.size_bytes;
self.stats.evictions += 1;
}
}
}
pub fn maintenance(&mut self) {
self.evict_expired();
}
}
impl Default for TextureCache {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "gpu")]
pub mod sync {
use std::sync::{Arc, RwLock};
use super::{CacheStats, TextureCache, TextureCacheConfig};
use crate::image::TextureData;
#[derive(Clone)]
pub struct SyncTextureCache {
inner: Arc<RwLock<TextureCache>>,
}
impl SyncTextureCache {
#[must_use]
pub fn new() -> Self {
Self {
inner: Arc::new(RwLock::new(TextureCache::new())),
}
}
#[must_use]
pub fn with_config(config: TextureCacheConfig) -> Self {
Self {
inner: Arc::new(RwLock::new(TextureCache::with_config(config))),
}
}
#[must_use]
pub fn get(&self, key: &str) -> Option<TextureData> {
let mut cache = self.inner.write().ok()?;
cache.get(key).cloned()
}
pub fn insert(&self, key: String, data: TextureData) {
if let Ok(mut cache) = self.inner.write() {
cache.insert(key, data);
}
}
#[must_use]
pub fn contains(&self, key: &str) -> bool {
self.inner
.read()
.map(|cache| cache.contains(key))
.unwrap_or(false)
}
#[must_use]
pub fn stats(&self) -> Option<CacheStats> {
self.inner.read().ok().map(|cache| cache.stats().clone())
}
}
impl Default for SyncTextureCache {
fn default() -> Self {
Self::new()
}
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::{TextureCache, TextureCacheConfig};
use crate::image::create_solid_color;
#[test]
fn test_cache_insert_and_get() {
let mut cache = TextureCache::new();
let texture = create_solid_color(10, 10, 255, 0, 0, 255);
cache.insert("test".to_string(), texture.clone());
assert!(cache.contains("test"));
assert_eq!(cache.len(), 1);
let retrieved = cache.get("test");
assert!(retrieved.is_some());
assert_eq!(retrieved.unwrap().width, 10);
}
#[test]
fn test_cache_miss() {
let mut cache = TextureCache::new();
assert!(cache.get("nonexistent").is_none());
assert_eq!(cache.stats().misses, 1);
}
#[test]
fn test_cache_eviction_by_size() {
let config = TextureCacheConfig {
max_size_bytes: 1000, max_age: Duration::from_secs(3600),
max_entries: 100,
};
let mut cache = TextureCache::with_config(config);
let texture = create_solid_color(20, 20, 255, 0, 0, 255); cache.insert("big".to_string(), texture);
assert!(cache.contains("big"));
}
#[test]
fn test_cache_eviction_by_count() {
let config = TextureCacheConfig {
max_size_bytes: 1024 * 1024,
max_age: Duration::from_secs(3600),
max_entries: 2,
};
let mut cache = TextureCache::with_config(config);
cache.insert("a".to_string(), create_solid_color(2, 2, 255, 0, 0, 255));
cache.insert("b".to_string(), create_solid_color(2, 2, 0, 255, 0, 255));
cache.insert("c".to_string(), create_solid_color(2, 2, 0, 0, 255, 255));
assert!(cache.len() <= 2);
}
#[test]
fn test_cache_remove() {
let mut cache = TextureCache::new();
let texture = create_solid_color(10, 10, 255, 0, 0, 255);
cache.insert("test".to_string(), texture);
assert!(cache.contains("test"));
let removed = cache.remove("test");
assert!(removed.is_some());
assert!(!cache.contains("test"));
}
#[test]
fn test_cache_clear() {
let mut cache = TextureCache::new();
cache.insert("a".to_string(), create_solid_color(2, 2, 255, 0, 0, 255));
cache.insert("b".to_string(), create_solid_color(2, 2, 0, 255, 0, 255));
assert_eq!(cache.len(), 2);
cache.clear();
assert_eq!(cache.len(), 0);
assert_eq!(cache.size_bytes(), 0);
}
#[test]
fn test_get_or_insert_with() {
let mut cache = TextureCache::new();
let data =
cache.get_or_insert_with("lazy", || create_solid_color(5, 5, 128, 128, 128, 255));
assert_eq!(data.width, 5);
assert!(cache.contains("lazy"));
let data2 = cache.get_or_insert_with("lazy", || create_solid_color(10, 10, 0, 0, 0, 255));
assert_eq!(data2.width, 5); }
#[test]
fn test_cache_stats() {
let mut cache = TextureCache::new();
cache.insert("a".to_string(), create_solid_color(2, 2, 255, 0, 0, 255));
let _ = cache.get("a"); let _ = cache.get("b"); let _ = cache.get("a");
let stats = cache.stats();
assert_eq!(stats.hits, 2);
assert_eq!(stats.misses, 1);
}
}