pub mod chunked;
pub use chunked::{ChunkCoord, ChunkedCacheConfig, ChunkedMeshCache};
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::path::PathBuf;
use std::sync::{Arc, RwLock};
use crate::mesh::Mesh;
use crate::types::SdfNode;
#[derive(Debug, Clone)]
pub struct CacheConfig {
pub max_entries: usize,
#[deprecated(
since = "5.0.0",
note = "not implemented: MeshCache never touches the disk; persist with crate::io::save_abm or ChunkedMeshCache"
)]
pub disk_cache_dir: Option<PathBuf>,
#[deprecated(
since = "5.0.0",
note = "not implemented: evicted meshes are dropped; persist with crate::io::save_abm or ChunkedMeshCache"
)]
pub persist_on_evict: bool,
}
#[allow(deprecated)] impl Default for CacheConfig {
fn default() -> Self {
Self {
max_entries: 64,
disk_cache_dir: None,
persist_on_evict: false,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct MeshCacheKey {
pub sdf_hash: u64,
pub resolution: u32,
pub bounds_hash: u64,
}
struct CacheEntry {
mesh: Arc<Mesh>,
size_bytes: usize,
}
struct LruMap {
map: HashMap<MeshCacheKey, CacheEntry>,
order: Vec<MeshCacheKey>,
}
impl LruMap {
fn new() -> Self {
Self {
map: HashMap::new(),
order: Vec::new(),
}
}
#[inline(always)]
fn touch(&mut self, key: &MeshCacheKey) {
if let Some(pos) = self.order.iter().position(|k| k == key) {
self.order.remove(pos);
self.order.push(key.clone());
}
}
fn insert(
&mut self,
key: MeshCacheKey,
entry: CacheEntry,
max_entries: usize,
) -> Option<MeshCacheKey> {
if self.map.contains_key(&key) {
self.map.insert(key.clone(), entry);
self.touch(&key);
return None;
}
let evicted = if self.map.len() >= max_entries && max_entries > 0 {
if let Some(oldest_key) = self.order.first().cloned() {
self.map.remove(&oldest_key);
self.order.remove(0);
Some(oldest_key)
} else {
None
}
} else {
None
};
self.map.insert(key.clone(), entry);
self.order.push(key);
evicted
}
fn remove(&mut self, key: &MeshCacheKey) -> Option<Arc<Mesh>> {
if let Some(entry) = self.map.remove(key) {
if let Some(pos) = self.order.iter().position(|k| k == key) {
self.order.remove(pos);
}
Some(entry.mesh)
} else {
None
}
}
fn get(&mut self, key: &MeshCacheKey) -> Option<Arc<Mesh>> {
if self.map.contains_key(key) {
self.touch(key);
Some(Arc::clone(&self.map[key].mesh))
} else {
None
}
}
fn clear(&mut self) {
self.map.clear();
self.order.clear();
}
#[inline(always)]
fn len(&self) -> usize {
self.map.len()
}
fn memory_usage(&self) -> usize {
self.map.values().map(|e| e.size_bytes).sum()
}
}
pub struct MeshCache {
config: CacheConfig,
entries: RwLock<LruMap>,
}
impl MeshCache {
pub fn new(config: CacheConfig) -> Self {
Self {
config,
entries: RwLock::new(LruMap::new()),
}
}
pub fn get(&self, key: &MeshCacheKey) -> Option<Arc<Mesh>> {
let mut entries = self
.entries
.write()
.expect("MeshCache: RwLock poisoned on entries.write() in get()");
entries.get(key)
}
pub fn insert(&self, key: MeshCacheKey, mesh: Mesh) -> Arc<Mesh> {
let size_bytes = estimate_mesh_size(&mesh);
let arc_mesh = Arc::new(mesh);
let entry = CacheEntry {
mesh: Arc::clone(&arc_mesh),
size_bytes,
};
let mut entries = self
.entries
.write()
.expect("MeshCache: RwLock poisoned on entries.write() in insert()");
let _evicted = entries.insert(key, entry, self.config.max_entries);
arc_mesh
}
pub fn remove(&self, key: &MeshCacheKey) -> Option<Arc<Mesh>> {
let mut entries = self
.entries
.write()
.expect("MeshCache: RwLock poisoned on entries.write() in remove()");
entries.remove(key)
}
pub fn clear(&self) {
let mut entries = self
.entries
.write()
.expect("MeshCache: RwLock poisoned on entries.write() in clear()");
entries.clear();
}
#[inline(always)]
pub fn len(&self) -> usize {
let entries = self
.entries
.read()
.expect("MeshCache: RwLock poisoned on entries.read() in len()");
entries.len()
}
#[inline(always)]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn memory_usage(&self) -> usize {
let entries = self
.entries
.read()
.expect("MeshCache: RwLock poisoned on entries.read() in memory_usage()");
entries.memory_usage()
}
pub fn get_or_generate<F>(&self, key: MeshCacheKey, generator: F) -> Arc<Mesh>
where
F: FnOnce() -> Mesh,
{
{
let mut entries = self
.entries
.write()
.expect("MeshCache: RwLock poisoned on entries.write() in get_or_generate()");
if let Some(mesh) = entries.get(&key) {
return mesh;
}
}
let mesh = generator();
let size_bytes = estimate_mesh_size(&mesh);
let arc_mesh = Arc::new(mesh);
let entry = CacheEntry {
mesh: Arc::clone(&arc_mesh),
size_bytes,
};
let mut entries = self
.entries
.write()
.expect("MeshCache: RwLock poisoned on entries.write() in get_or_generate()");
if let Some(existing) = entries.get(&key) {
return existing;
}
entries.insert(key, entry, self.config.max_entries);
arc_mesh
}
}
pub fn hash_sdf_node(node: &SdfNode) -> u64 {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
serde_json::to_string(node)
.unwrap_or_default()
.hash(&mut hasher);
hasher.finish()
}
pub fn compute_cache_key(
node: &SdfNode,
min_bounds: glam::Vec3,
max_bounds: glam::Vec3,
resolution: usize,
) -> MeshCacheKey {
let sdf_hash = hash_sdf_node(node);
let bounds_hash = hash_bounds(min_bounds, max_bounds);
MeshCacheKey {
sdf_hash,
resolution: resolution as u32,
bounds_hash,
}
}
#[inline(always)]
fn hash_bounds(min_bounds: glam::Vec3, max_bounds: glam::Vec3) -> u64 {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
quantize_f32(min_bounds.x).hash(&mut hasher);
quantize_f32(min_bounds.y).hash(&mut hasher);
quantize_f32(min_bounds.z).hash(&mut hasher);
quantize_f32(max_bounds.x).hash(&mut hasher);
quantize_f32(max_bounds.y).hash(&mut hasher);
quantize_f32(max_bounds.z).hash(&mut hasher);
hasher.finish()
}
#[inline(always)]
fn quantize_f32(value: f32) -> i32 {
(value * 1000.0) as i32
}
#[inline(always)]
fn estimate_mesh_size(mesh: &Mesh) -> usize {
let vertex_size = std::mem::size_of::<crate::mesh::Vertex>();
mesh.vertices.len() * vertex_size + mesh.indices.len() * std::mem::size_of::<u32>()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mesh::Vertex;
use glam::Vec3;
fn make_test_mesh(num_verts: usize, num_tris: usize) -> Mesh {
let vertices: Vec<Vertex> = (0..num_verts)
.map(|i| {
let f = i as f32;
Vertex::new(Vec3::new(f, f, f), Vec3::Y)
})
.collect();
let indices: Vec<u32> = (0..num_tris * 3).map(|i| (i % num_verts) as u32).collect();
Mesh { vertices, indices }
}
#[test]
fn test_insert_and_retrieve() {
let cache = MeshCache::new(CacheConfig {
max_entries: 4,
..Default::default()
});
let key = MeshCacheKey {
sdf_hash: 42,
resolution: 32,
bounds_hash: 100,
};
let mesh = make_test_mesh(8, 4);
let expected_verts = mesh.vertices.len();
cache.insert(key.clone(), mesh);
let retrieved = cache.get(&key);
assert!(retrieved.is_some());
assert_eq!(retrieved.unwrap().vertex_count(), expected_verts);
}
#[test]
fn test_lru_eviction() {
let cache = MeshCache::new(CacheConfig {
max_entries: 3,
..Default::default()
});
for i in 0..3u64 {
let key = MeshCacheKey {
sdf_hash: i,
resolution: 16,
bounds_hash: 0,
};
cache.insert(key, make_test_mesh(4, 2));
}
assert_eq!(cache.len(), 3);
let key4 = MeshCacheKey {
sdf_hash: 99,
resolution: 16,
bounds_hash: 0,
};
cache.insert(key4, make_test_mesh(4, 2));
assert_eq!(cache.len(), 3);
let evicted_key = MeshCacheKey {
sdf_hash: 0,
resolution: 16,
bounds_hash: 0,
};
assert!(cache.get(&evicted_key).is_none());
for i in 1..3u64 {
let key = MeshCacheKey {
sdf_hash: i,
resolution: 16,
bounds_hash: 0,
};
assert!(cache.get(&key).is_some());
}
let key99 = MeshCacheKey {
sdf_hash: 99,
resolution: 16,
bounds_hash: 0,
};
assert!(cache.get(&key99).is_some());
}
#[test]
fn test_lru_access_updates_order() {
let cache = MeshCache::new(CacheConfig {
max_entries: 3,
..Default::default()
});
for i in 0..3u64 {
let key = MeshCacheKey {
sdf_hash: i,
resolution: 16,
bounds_hash: 0,
};
cache.insert(key, make_test_mesh(4, 2));
}
let key0 = MeshCacheKey {
sdf_hash: 0,
resolution: 16,
bounds_hash: 0,
};
assert!(cache.get(&key0).is_some());
let key3 = MeshCacheKey {
sdf_hash: 3,
resolution: 16,
bounds_hash: 0,
};
cache.insert(key3, make_test_mesh(4, 2));
let evicted_key = MeshCacheKey {
sdf_hash: 1,
resolution: 16,
bounds_hash: 0,
};
assert!(cache.get(&evicted_key).is_none());
assert!(cache.get(&key0).is_some());
}
#[test]
fn test_get_or_generate_caches() {
let cache = MeshCache::new(CacheConfig::default());
let key = MeshCacheKey {
sdf_hash: 1,
resolution: 16,
bounds_hash: 0,
};
let call_count = std::sync::atomic::AtomicU32::new(0);
let mesh1 = cache.get_or_generate(key.clone(), || {
call_count.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
make_test_mesh(10, 5)
});
assert_eq!(call_count.load(std::sync::atomic::Ordering::SeqCst), 1);
assert_eq!(mesh1.vertex_count(), 10);
let mesh2 = cache.get_or_generate(key, || {
call_count.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
make_test_mesh(20, 10)
});
assert_eq!(call_count.load(std::sync::atomic::Ordering::SeqCst), 1);
assert_eq!(mesh2.vertex_count(), 10); }
#[test]
fn test_cache_key_determinism() {
let node = SdfNode::sphere(1.0);
let hash1 = hash_sdf_node(&node);
let hash2 = hash_sdf_node(&node);
assert_eq!(hash1, hash2, "Same SDF tree must produce the same hash");
let node2 = SdfNode::sphere(2.0);
let hash3 = hash_sdf_node(&node2);
assert_ne!(
hash1, hash3,
"Different SDF trees should produce different hashes"
);
}
#[test]
fn test_compute_cache_key_determinism() {
let node = SdfNode::sphere(1.0).subtract(SdfNode::box3d(0.5, 0.5, 0.5));
let min = Vec3::splat(-2.0);
let max = Vec3::splat(2.0);
let key1 = compute_cache_key(&node, min, max, 32);
let key2 = compute_cache_key(&node, min, max, 32);
assert_eq!(key1, key2);
let key3 = compute_cache_key(&node, min, max, 64);
assert_ne!(key1, key3);
let key4 = compute_cache_key(&node, Vec3::splat(-3.0), max, 32);
assert_ne!(key1, key4);
}
#[test]
fn test_thread_safety_concurrent_reads() {
use std::sync::Arc as StdArc;
let cache = StdArc::new(MeshCache::new(CacheConfig {
max_entries: 16,
..Default::default()
}));
for i in 0..8u64 {
let key = MeshCacheKey {
sdf_hash: i,
resolution: 16,
bounds_hash: 0,
};
cache.insert(key, make_test_mesh(4, 2));
}
let mut handles = Vec::new();
for i in 0..8u64 {
let cache_clone = StdArc::clone(&cache);
handles.push(std::thread::spawn(move || {
let key = MeshCacheKey {
sdf_hash: i,
resolution: 16,
bounds_hash: 0,
};
let result = cache_clone.get(&key);
assert!(result.is_some(), "Key {} should be in cache", i);
result.unwrap().vertex_count()
}));
}
for handle in handles {
let count = handle.join().unwrap();
assert_eq!(count, 4);
}
}
#[test]
fn test_clear() {
let cache = MeshCache::new(CacheConfig::default());
let key = MeshCacheKey {
sdf_hash: 1,
resolution: 16,
bounds_hash: 0,
};
cache.insert(key.clone(), make_test_mesh(4, 2));
assert_eq!(cache.len(), 1);
cache.clear();
assert_eq!(cache.len(), 0);
assert!(cache.is_empty());
assert!(cache.get(&key).is_none());
}
#[test]
fn test_remove() {
let cache = MeshCache::new(CacheConfig::default());
let key1 = MeshCacheKey {
sdf_hash: 1,
resolution: 16,
bounds_hash: 0,
};
let key2 = MeshCacheKey {
sdf_hash: 2,
resolution: 16,
bounds_hash: 0,
};
cache.insert(key1.clone(), make_test_mesh(4, 2));
cache.insert(key2.clone(), make_test_mesh(8, 4));
assert_eq!(cache.len(), 2);
let removed = cache.remove(&key1);
assert!(removed.is_some());
assert_eq!(removed.unwrap().vertex_count(), 4);
assert_eq!(cache.len(), 1);
assert!(cache.get(&key1).is_none());
assert!(cache.get(&key2).is_some());
let removed_none = cache.remove(&key1);
assert!(removed_none.is_none());
}
#[test]
fn test_memory_usage() {
let cache = MeshCache::new(CacheConfig::default());
let key = MeshCacheKey {
sdf_hash: 1,
resolution: 16,
bounds_hash: 0,
};
let mesh = make_test_mesh(100, 50);
let expected_size = mesh.vertices.len() * std::mem::size_of::<Vertex>()
+ mesh.indices.len() * std::mem::size_of::<u32>();
cache.insert(key, mesh);
assert_eq!(cache.memory_usage(), expected_size);
}
#[test]
fn test_empty_cache() {
let cache = MeshCache::new(CacheConfig::default());
assert!(cache.is_empty());
assert_eq!(cache.len(), 0);
assert_eq!(cache.memory_usage(), 0);
}
#[test]
fn test_insert_duplicate_key_updates() {
let cache = MeshCache::new(CacheConfig {
max_entries: 4,
..Default::default()
});
let key = MeshCacheKey {
sdf_hash: 42,
resolution: 32,
bounds_hash: 100,
};
cache.insert(key.clone(), make_test_mesh(4, 2));
assert_eq!(cache.len(), 1);
cache.insert(key.clone(), make_test_mesh(16, 8));
assert_eq!(cache.len(), 1);
let retrieved = cache.get(&key).unwrap();
assert_eq!(retrieved.vertex_count(), 16); }
}