use std::collections::HashMap;
use std::collections::VecDeque;
use std::fs;
use std::io::{Read, Write};
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use bevy::asset::Assets;
use bevy::ecs::resource::Resource;
use bevy::image::Image;
use crate::generator::{GeneratedHandles, TextureMap, map_to_images, map_to_images_card};
pub const DEFAULT_MEMORY_CACHE_ENTRIES: usize = 256;
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct TextureCacheKey {
pub kind: &'static str,
pub fingerprint: u64,
pub width: u32,
pub height: u32,
}
pub trait TextureCacheStore: Send + Sync {
fn get(
&mut self,
key: &TextureCacheKey,
images: &mut Assets<Image>,
) -> Option<Arc<GeneratedHandles>>;
fn put(
&mut self,
key: TextureCacheKey,
handles: Arc<GeneratedHandles>,
is_card: bool,
map: Option<&TextureMap>,
);
fn peek_memory_only(&self, _key: &TextureCacheKey) -> Option<Arc<GeneratedHandles>> {
None
}
}
#[derive(Resource)]
pub struct TextureCache {
pub manifest_version: u32,
inner: Mutex<Box<dyn TextureCacheStore>>,
}
impl TextureCache {
pub fn new(store: Box<dyn TextureCacheStore>, manifest_version: u32) -> Self {
Self {
manifest_version,
inner: Mutex::new(store),
}
}
pub fn memory(max_entries: usize) -> Self {
Self::new(Box::new(MemoryStore::new(max_entries)), 0)
}
pub fn file(dir: impl Into<PathBuf>, manifest_version: u32) -> std::io::Result<Self> {
Ok(Self::new(
Box::new(FileStore::new(dir.into())?),
manifest_version,
))
}
pub fn get_handles(&self, key: &TextureCacheKey) -> Option<Arc<GeneratedHandles>> {
self.inner.lock().ok()?.peek_memory_only(key)
}
pub fn insert(&mut self, key: TextureCacheKey, handles: Arc<GeneratedHandles>) {
if let Ok(mut store) = self.inner.lock() {
store.put(key, handles, false, None);
}
}
}
pub struct MemoryStore {
max_entries: usize,
entries: HashMap<TextureCacheKey, Arc<GeneratedHandles>>,
insertion_order: VecDeque<TextureCacheKey>,
}
impl MemoryStore {
pub fn new(max_entries: usize) -> Self {
let cap = max_entries.max(1);
Self {
max_entries: cap,
entries: HashMap::with_capacity(cap),
insertion_order: VecDeque::with_capacity(cap),
}
}
}
impl TextureCacheStore for MemoryStore {
fn get(
&mut self,
key: &TextureCacheKey,
_images: &mut Assets<Image>,
) -> Option<Arc<GeneratedHandles>> {
self.entries.get(key).cloned()
}
fn put(
&mut self,
key: TextureCacheKey,
handles: Arc<GeneratedHandles>,
_is_card: bool,
_map: Option<&TextureMap>,
) {
if let std::collections::hash_map::Entry::Occupied(mut e) = self.entries.entry(key.clone())
{
e.insert(handles);
return;
}
if self.entries.len() >= self.max_entries
&& let Some(oldest) = self.insertion_order.pop_front()
{
self.entries.remove(&oldest);
}
self.insertion_order.push_back(key.clone());
self.entries.insert(key, handles);
}
fn peek_memory_only(&self, key: &TextureCacheKey) -> Option<Arc<GeneratedHandles>> {
self.entries.get(key).cloned()
}
}
const FILE_MAGIC: &[u8; 4] = b"BSTX";
const FILE_FORMAT_VERSION: u32 = 1;
pub struct FileStore {
root: PathBuf,
}
impl FileStore {
pub fn new(root: PathBuf) -> std::io::Result<Self> {
fs::create_dir_all(&root)?;
Ok(Self { root })
}
fn path_for(&self, key: &TextureCacheKey) -> PathBuf {
use std::hash::{DefaultHasher, Hash, Hasher};
let mut h = DefaultHasher::new();
key.hash(&mut h);
self.root.join(format!("{:016x}.bin", h.finish()))
}
}
impl TextureCacheStore for FileStore {
fn get(
&mut self,
key: &TextureCacheKey,
images: &mut Assets<Image>,
) -> Option<Arc<GeneratedHandles>> {
let path = self.path_for(key);
let mut file = fs::File::open(&path).ok()?;
let mut header = [0u8; 4 + 4 + 1 + 4 + 4 + 4 + 4 + 4];
file.read_exact(&mut header).ok()?;
if &header[0..4] != FILE_MAGIC {
return None;
}
let version = u32::from_le_bytes(header[4..8].try_into().unwrap());
if version != FILE_FORMAT_VERSION {
return None;
}
let is_card = header[8] != 0;
let width = u32::from_le_bytes(header[9..13].try_into().unwrap());
let height = u32::from_le_bytes(header[13..17].try_into().unwrap());
let albedo_len = u32::from_le_bytes(header[17..21].try_into().unwrap()) as usize;
let normal_len = u32::from_le_bytes(header[21..25].try_into().unwrap()) as usize;
let roughness_len = u32::from_le_bytes(header[25..29].try_into().unwrap()) as usize;
let mut albedo = vec![0u8; albedo_len];
let mut normal = vec![0u8; normal_len];
let mut roughness = vec![0u8; roughness_len];
file.read_exact(&mut albedo).ok()?;
file.read_exact(&mut normal).ok()?;
file.read_exact(&mut roughness).ok()?;
let map = TextureMap {
albedo,
normal,
roughness,
width,
height,
};
let handles = if is_card {
map_to_images_card(map, images)
} else {
map_to_images(map, images)
};
Some(Arc::new(handles))
}
fn put(
&mut self,
key: TextureCacheKey,
_handles: Arc<GeneratedHandles>,
is_card: bool,
map: Option<&TextureMap>,
) {
let Some(map) = map else {
return;
};
let path = self.path_for(&key);
if let Err(e) = (|| -> std::io::Result<()> {
let mut file = fs::File::create(&path)?;
file.write_all(FILE_MAGIC)?;
file.write_all(&FILE_FORMAT_VERSION.to_le_bytes())?;
file.write_all(&[is_card as u8])?;
file.write_all(&map.width.to_le_bytes())?;
file.write_all(&map.height.to_le_bytes())?;
file.write_all(&(map.albedo.len() as u32).to_le_bytes())?;
file.write_all(&(map.normal.len() as u32).to_le_bytes())?;
file.write_all(&(map.roughness.len() as u32).to_le_bytes())?;
file.write_all(&map.albedo)?;
file.write_all(&map.normal)?;
file.write_all(&map.roughness)?;
Ok(())
})() {
bevy::log::warn!("FileStore::put failed for {}: {e}", path.display());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn dummy_handles() -> Arc<GeneratedHandles> {
Arc::new(GeneratedHandles {
albedo: Default::default(),
normal: Default::default(),
roughness: Default::default(),
})
}
fn key(kind: &'static str, fp: u64) -> TextureCacheKey {
TextureCacheKey {
kind,
fingerprint: fp,
width: 64,
height: 64,
}
}
#[test]
fn memory_store_round_trips_handles() {
let mut store = MemoryStore::new(8);
let k = key("Bark", 42);
assert!(store.peek_memory_only(&k).is_none());
store.put(k.clone(), dummy_handles(), false, None);
assert!(store.peek_memory_only(&k).is_some());
}
#[test]
fn memory_store_evicts_oldest_at_capacity() {
let mut store = MemoryStore::new(2);
store.put(key("Bark", 1), dummy_handles(), false, None);
store.put(key("Bark", 2), dummy_handles(), false, None);
store.put(key("Bark", 3), dummy_handles(), false, None);
assert!(store.peek_memory_only(&key("Bark", 1)).is_none());
assert!(store.peek_memory_only(&key("Bark", 2)).is_some());
assert!(store.peek_memory_only(&key("Bark", 3)).is_some());
}
#[test]
fn memory_store_treats_replace_as_no_evict() {
let mut store = MemoryStore::new(2);
store.put(key("Bark", 1), dummy_handles(), false, None);
store.put(key("Bark", 2), dummy_handles(), false, None);
store.put(key("Bark", 1), dummy_handles(), false, None);
assert!(store.peek_memory_only(&key("Bark", 1)).is_some());
assert!(store.peek_memory_only(&key("Bark", 2)).is_some());
}
}