// SPDX-License-Identifier: AGPL-3.0-only //! A bounded cache of regenerated chunk baselines, shared across the worker pool. use std::num::NonZeroUsize; use std::sync::{Arc, Mutex}; use lru::LruCache; use shared::generator::VoxelGenerator; use shared::world::{Chunk, ChunkPos}; /// A least-recently-used cache of chunk baselines, cloneable so every worker shares one store. #[derive(Clone)] pub struct ChunkCache { /// The shared LRU store. inner: Arc>>, } impl ChunkCache { /// Creates an empty cache holding at most `capacity` baselines before evicting the least-recently-used entry. #[must_use] pub fn new(capacity: NonZeroUsize) -> Self { Self { inner: Arc::new(Mutex::new(LruCache::new(capacity))), } } /// Returns the baseline for `pos`, generating and caching it on a miss. #[must_use] pub fn get_or_generate(&self, pos: ChunkPos, generator: &VoxelGenerator) -> Chunk { // A poisoned lock cannot yield a usable cache; recovering the guard lets generation proceed rather than propagating a panic across every worker that shares this cache. let mut guard = self .inner .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); if let Some(hit) = guard.get(&pos) { return hit.clone(); } let chunk = generator.generate_chunk(pos); guard.put(pos, chunk.clone()); chunk } } #[cfg(test)] impl ChunkCache { /// Number of baselines currently resident. Test-only introspection. fn len(&self) -> usize { self.inner .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .len() } /// Whether a baseline for `pos` is currently resident. Test-only introspection. fn contains(&self, pos: ChunkPos) -> bool { self.inner .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .contains(&pos) } } #[cfg(test)] mod tests { use super::*; use shared::generator::{VoxelGenerator, WorldGenConfig}; use shared::world::{BlockId, ChunkPos}; use std::num::NonZeroUsize; /// Builds a generator with a small, cheap terrain configuration for cache tests. fn test_generator() -> VoxelGenerator { let config = WorldGenConfig { base_height: 8, noise_scale: 0.05, surface_block: BlockId(1), subsurface_block: BlockId(2), stone_block: BlockId(3), }; VoxelGenerator::new(config, 42) } /// A second lookup of the same position must be served from the store, not regenerated. #[test] fn repeated_lookup_is_a_cache_hit() { let generator = test_generator(); let cache = ChunkCache::new(NonZeroUsize::new(4).unwrap_or(NonZeroUsize::MIN)); let pos = ChunkPos::new(0, 0, 0); let first = cache.get_or_generate(pos, &generator); let second = cache.get_or_generate(pos, &generator); // Determinism guarantees identical output, and a single resident entry can only hold if the second call was a hit rather than a fresh generation-and-insert of a distinct value. assert_eq!(first.blocks, second.blocks); assert_eq!(cache.len(), 1); } /// Inserting beyond capacity evicts the least-recently-used entry. #[test] fn exceeding_capacity_evicts_oldest() { let generator = test_generator(); let cache = ChunkCache::new(NonZeroUsize::new(2).unwrap_or(NonZeroUsize::MIN)); let _ = cache.get_or_generate(ChunkPos::new(0, 0, 0), &generator); let _ = cache.get_or_generate(ChunkPos::new(1, 0, 0), &generator); // Touch the first so the second becomes the least-recently-used before the overflow. let _ = cache.get_or_generate(ChunkPos::new(0, 0, 0), &generator); let _ = cache.get_or_generate(ChunkPos::new(2, 0, 0), &generator); assert_eq!(cache.len(), 2); assert!(cache.contains(ChunkPos::new(0, 0, 0))); assert!(!cache.contains(ChunkPos::new(1, 0, 0))); } }