1use crate::io::abm::{load_abm, save_abm};
16use crate::io::IoError;
17use crate::mesh::{Mesh, Vertex};
18use glam::Vec3;
19use std::collections::HashMap;
20use std::path::PathBuf;
21use std::sync::{Arc, RwLock};
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
29pub struct ChunkCoord {
30 pub x: i32,
32 pub y: i32,
34 pub z: i32,
36}
37
38impl ChunkCoord {
39 #[inline(always)]
41 pub const fn new(x: i32, y: i32, z: i32) -> Self {
42 Self { x, y, z }
43 }
44}
45
46impl std::fmt::Display for ChunkCoord {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 write!(f, "({}, {}, {})", self.x, self.y, self.z)
49 }
50}
51
52#[derive(Debug, Clone)]
58pub struct ChunkedCacheConfig {
59 pub chunk_size: f32,
61 pub chunk_resolution: usize,
63 pub cache_dir: Option<PathBuf>,
65 pub max_cached_chunks: usize,
67}
68
69impl Default for ChunkedCacheConfig {
70 fn default() -> Self {
71 Self {
72 chunk_size: 1.0,
73 chunk_resolution: 32,
74 cache_dir: None,
75 max_cached_chunks: 512,
76 }
77 }
78}
79
80struct ChunkEntry {
86 mesh: Arc<Mesh>,
88 sdf_hash: u64,
90 dirty: bool,
92}
93
94struct ChunkedStore {
96 entries: HashMap<ChunkCoord, ChunkEntry>,
97 order: Vec<ChunkCoord>,
99}
100
101pub struct ChunkedMeshCache {
111 config: ChunkedCacheConfig,
112 chunks: RwLock<ChunkedStore>,
113 current_sdf_hash: RwLock<u64>,
115}
116
117impl ChunkedMeshCache {
118 pub fn new(config: ChunkedCacheConfig) -> Self {
120 Self {
121 config,
122 chunks: RwLock::new(ChunkedStore {
123 entries: HashMap::new(),
124 order: Vec::new(),
125 }),
126 current_sdf_hash: RwLock::new(0),
127 }
128 }
129
130 pub fn get_chunk(&self, coord: &ChunkCoord) -> Option<Arc<Mesh>> {
134 let chunks = self
135 .chunks
136 .read()
137 .expect("ChunkedMeshCache: RwLock poisoned on chunks.read() in get_chunk()");
138 chunks.entries.get(coord).map(|e| Arc::clone(&e.mesh))
139 }
140
141 pub fn set_chunk(&self, coord: ChunkCoord, mesh: Mesh, sdf_hash: u64) {
146 let mut chunks = self
147 .chunks
148 .write()
149 .expect("ChunkedMeshCache: RwLock poisoned on chunks.write() in set_chunk()");
150
151 if chunks.entries.len() >= self.config.max_cached_chunks
153 && !chunks.entries.contains_key(&coord)
154 {
155 if let Some(evict_key) = chunks.order.first().copied() {
156 chunks.entries.remove(&evict_key);
157 chunks.order.remove(0);
158 }
159 }
160
161 if chunks.entries.contains_key(&coord) {
163 if let Some(pos) = chunks.order.iter().position(|c| *c == coord) {
164 chunks.order.remove(pos);
165 }
166 }
167
168 chunks.entries.insert(
169 coord,
170 ChunkEntry {
171 mesh: Arc::new(mesh),
172 sdf_hash,
173 dirty: true,
174 },
175 );
176 chunks.order.push(coord);
177 }
178
179 pub fn invalidate_all(&self) {
183 let mut chunks = self
184 .chunks
185 .write()
186 .expect("ChunkedMeshCache: RwLock poisoned on chunks.write() in invalidate_all()");
187 for entry in chunks.entries.values_mut() {
188 entry.dirty = true;
189 }
190 }
191
192 pub fn invalidate_region(&self, min: Vec3, max: Vec3) {
194 let coords = self.chunks_in_bounds(min, max);
195 let mut chunks = self
196 .chunks
197 .write()
198 .expect("ChunkedMeshCache: RwLock poisoned on chunks.write() in invalidate_region()");
199 for c in &coords {
200 if let Some(entry) = chunks.entries.get_mut(c) {
201 entry.dirty = true;
202 }
203 }
204 }
205
206 pub fn dirty_chunks(&self) -> Vec<ChunkCoord> {
208 let chunks = self
209 .chunks
210 .read()
211 .expect("ChunkedMeshCache: RwLock poisoned on chunks.read() in dirty_chunks()");
212 chunks
213 .entries
214 .iter()
215 .filter(|(_, e)| e.dirty)
216 .map(|(&c, _)| c)
217 .collect()
218 }
219
220 #[allow(clippy::significant_drop_tightening)]
223 pub fn update_sdf_hash(&self, new_hash: u64) -> Vec<ChunkCoord> {
224 {
225 let mut h = self.current_sdf_hash.write()
226 .expect("ChunkedMeshCache: RwLock poisoned on current_sdf_hash.write() in update_sdf_hash()");
227 *h = new_hash;
228 }
229
230 let mut invalidated = Vec::new();
231 let mut chunks = self
232 .chunks
233 .write()
234 .expect("ChunkedMeshCache: RwLock poisoned on chunks.write() in update_sdf_hash()");
235 for (&coord, entry) in chunks.entries.iter_mut() {
236 if entry.sdf_hash != new_hash {
237 entry.dirty = true;
238 invalidated.push(coord);
239 }
240 }
241 invalidated
242 }
243
244 pub fn cached_chunks(&self) -> Vec<ChunkCoord> {
248 let chunks = self
249 .chunks
250 .read()
251 .expect("ChunkedMeshCache: RwLock poisoned on chunks.read() in cached_chunks()");
252 chunks.entries.keys().copied().collect()
253 }
254
255 #[inline]
257 pub fn chunk_count(&self) -> usize {
258 let chunks = self
259 .chunks
260 .read()
261 .expect("ChunkedMeshCache: RwLock poisoned on chunks.read() in chunk_count()");
262 chunks.entries.len()
263 }
264
265 pub fn memory_usage(&self) -> usize {
267 let chunks = self
268 .chunks
269 .read()
270 .expect("ChunkedMeshCache: RwLock poisoned on chunks.read() in memory_usage()");
271 chunks
272 .entries
273 .values()
274 .map(|e| estimate_mesh_bytes(&e.mesh))
275 .sum()
276 }
277
278 #[inline]
282 pub fn world_to_chunk(&self, pos: Vec3) -> ChunkCoord {
283 let s = self.config.chunk_size;
284 ChunkCoord {
285 x: pos.x.div_euclid(s).floor() as i32,
286 y: pos.y.div_euclid(s).floor() as i32,
287 z: pos.z.div_euclid(s).floor() as i32,
288 }
289 }
290
291 #[inline]
293 pub fn chunk_bounds(&self, coord: &ChunkCoord) -> (Vec3, Vec3) {
294 let s = self.config.chunk_size;
295 let min = Vec3::new(coord.x as f32 * s, coord.y as f32 * s, coord.z as f32 * s);
296 let max = min + Vec3::splat(s);
297 (min, max)
298 }
299
300 pub fn chunks_in_bounds(&self, min: Vec3, max: Vec3) -> Vec<ChunkCoord> {
302 let lo = self.world_to_chunk(min);
303 let hi = self.world_to_chunk(max);
304 let mut coords = Vec::new();
305 for x in lo.x..=hi.x {
306 for y in lo.y..=hi.y {
307 for z in lo.z..=hi.z {
308 coords.push(ChunkCoord::new(x, y, z));
309 }
310 }
311 }
312 coords
313 }
314
315 #[allow(clippy::significant_drop_tightening)]
322 pub fn persist_dirty(&self) -> Result<usize, IoError> {
323 let dir = self.config.cache_dir.as_ref().ok_or_else(|| {
324 IoError::Io(std::io::Error::new(
325 std::io::ErrorKind::NotFound,
326 "cache_dir not configured",
327 ))
328 })?;
329 std::fs::create_dir_all(dir)?;
330
331 let mut chunks = self
332 .chunks
333 .write()
334 .expect("ChunkedMeshCache: RwLock poisoned on chunks.write() in persist_dirty()");
335 let mut written = 0usize;
336 for (&coord, entry) in chunks.entries.iter_mut() {
337 if entry.dirty {
338 let path = dir.join(format!("{}_{}_{}_.abm", coord.x, coord.y, coord.z));
339 save_abm(&entry.mesh, &path)?;
340 entry.dirty = false;
341 written += 1;
342 }
343 }
344 Ok(written)
345 }
346
347 #[allow(clippy::significant_drop_tightening)]
351 pub fn load_chunk(&self, coord: &ChunkCoord) -> Result<Option<Arc<Mesh>>, IoError> {
352 let dir = self.config.cache_dir.as_ref().ok_or_else(|| {
353 IoError::Io(std::io::Error::new(
354 std::io::ErrorKind::NotFound,
355 "cache_dir not configured",
356 ))
357 })?;
358 let path = dir.join(format!("{}_{}_{}_.abm", coord.x, coord.y, coord.z));
359 if !path.exists() {
360 return Ok(None);
361 }
362 let mesh = load_abm(&path)?;
363 let sdf_hash = *self
364 .current_sdf_hash
365 .read()
366 .expect("ChunkedMeshCache: RwLock poisoned on current_sdf_hash.read() in load_chunk()");
367 let arc = Arc::new(mesh);
368
369 let mut chunks = self
370 .chunks
371 .write()
372 .expect("ChunkedMeshCache: RwLock poisoned on chunks.write() in load_chunk()");
373 let is_new = !chunks.entries.contains_key(coord);
374 if is_new && chunks.entries.len() >= self.config.max_cached_chunks {
377 if let Some(evict_key) = chunks.order.first().copied() {
378 chunks.entries.remove(&evict_key);
379 chunks.order.remove(0);
380 }
381 }
382 chunks.entries.insert(
383 *coord,
384 ChunkEntry {
385 mesh: Arc::clone(&arc),
386 sdf_hash,
387 dirty: false,
388 },
389 );
390 if is_new {
391 chunks.order.push(*coord);
392 }
393 Ok(Some(arc))
394 }
395
396 pub fn clear(&self) {
400 let mut chunks = self
401 .chunks
402 .write()
403 .expect("ChunkedMeshCache: RwLock poisoned on chunks.write() in clear()");
404 chunks.entries.clear();
405 chunks.order.clear();
406 }
407
408 #[allow(clippy::significant_drop_tightening)]
419 pub fn merge_all(&self) -> Mesh {
420 let chunks = self
421 .chunks
422 .read()
423 .expect("ChunkedMeshCache: RwLock poisoned on chunks.read() in merge_all()");
424 let mut ordered: Vec<(&ChunkCoord, &ChunkEntry)> = chunks.entries.iter().collect();
425 ordered.sort_unstable_by_key(|(c, _)| (c.x, c.y, c.z));
426 let mut vertices: Vec<Vertex> = Vec::new();
427 let mut indices: Vec<u32> = Vec::new();
428 for (_, entry) in ordered {
429 let base = vertices.len() as u32;
430 vertices.extend_from_slice(&entry.mesh.vertices);
431 indices.extend(entry.mesh.indices.iter().map(|&i| i + base));
432 }
433 Mesh { vertices, indices }
434 }
435}
436
437#[inline(always)]
443fn estimate_mesh_bytes(mesh: &Mesh) -> usize {
444 let vertex_size = std::mem::size_of::<Vertex>();
445 mesh.vertices.len() * vertex_size + mesh.indices.len() * std::mem::size_of::<u32>()
446}
447
448#[cfg(test)]
453mod tests {
454 use super::*;
455 use crate::mesh::Vertex;
456 use glam::Vec3;
457
458 fn make_mesh(num_verts: usize, num_tris: usize) -> Mesh {
460 let vertices: Vec<Vertex> = (0..num_verts)
461 .map(|i| {
462 let f = i as f32;
463 Vertex::new(Vec3::new(f, f, f), Vec3::Y)
464 })
465 .collect();
466 let indices: Vec<u32> = (0..num_tris * 3).map(|i| (i % num_verts) as u32).collect();
467 Mesh { vertices, indices }
468 }
469
470 #[test]
471 fn test_world_to_chunk_positive() {
472 let cache = ChunkedMeshCache::new(ChunkedCacheConfig {
473 chunk_size: 2.0,
474 ..Default::default()
475 });
476 let c = cache.world_to_chunk(Vec3::new(3.5, 1.0, 5.9));
477 assert_eq!(c, ChunkCoord::new(1, 0, 2));
478 }
479
480 #[test]
481 fn test_world_to_chunk_negative() {
482 let cache = ChunkedMeshCache::new(ChunkedCacheConfig {
483 chunk_size: 1.0,
484 ..Default::default()
485 });
486 let c = cache.world_to_chunk(Vec3::new(-0.5, -1.5, -0.01));
487 assert_eq!(c, ChunkCoord::new(-1, -2, -1));
488 }
489
490 #[test]
491 fn test_world_to_chunk_on_boundary() {
492 let cache = ChunkedMeshCache::new(ChunkedCacheConfig {
493 chunk_size: 1.0,
494 ..Default::default()
495 });
496 let c = cache.world_to_chunk(Vec3::new(1.0, 0.0, -1.0));
498 assert_eq!(c, ChunkCoord::new(1, 0, -1));
499 }
500
501 #[test]
502 fn test_chunk_bounds() {
503 let cache = ChunkedMeshCache::new(ChunkedCacheConfig {
504 chunk_size: 2.0,
505 ..Default::default()
506 });
507 let (min, max) = cache.chunk_bounds(&ChunkCoord::new(1, -1, 0));
508 assert!((min.x - 2.0).abs() < 1e-6);
509 assert!((min.y - (-2.0)).abs() < 1e-6);
510 assert!((min.z - 0.0).abs() < 1e-6);
511 assert!((max.x - 4.0).abs() < 1e-6);
512 assert!((max.y - 0.0).abs() < 1e-6);
513 assert!((max.z - 2.0).abs() < 1e-6);
514 }
515
516 #[test]
517 fn test_chunks_in_bounds() {
518 let cache = ChunkedMeshCache::new(ChunkedCacheConfig {
519 chunk_size: 1.0,
520 ..Default::default()
521 });
522 let coords = cache.chunks_in_bounds(Vec3::new(0.0, 0.0, 0.0), Vec3::new(1.5, 0.5, 0.5));
523 assert!(coords.contains(&ChunkCoord::new(0, 0, 0)));
525 assert!(coords.contains(&ChunkCoord::new(1, 0, 0)));
526 }
527
528 #[test]
529 fn test_insert_and_get() {
530 let cache = ChunkedMeshCache::new(ChunkedCacheConfig::default());
531 let coord = ChunkCoord::new(0, 0, 0);
532 let mesh = make_mesh(4, 2);
533
534 cache.set_chunk(coord, mesh, 42);
535 let retrieved = cache.get_chunk(&coord);
536 assert!(retrieved.is_some());
537 assert_eq!(retrieved.unwrap().vertex_count(), 4);
538 }
539
540 #[test]
541 fn test_get_missing_returns_none() {
542 let cache = ChunkedMeshCache::new(ChunkedCacheConfig::default());
543 assert!(cache.get_chunk(&ChunkCoord::new(99, 99, 99)).is_none());
544 }
545
546 #[test]
547 fn test_invalidate_region() {
548 let cache = ChunkedMeshCache::new(ChunkedCacheConfig {
549 chunk_size: 1.0,
550 ..Default::default()
551 });
552
553 for x in 0..3 {
555 for z in 0..3 {
556 cache.set_chunk(ChunkCoord::new(x, 0, z), make_mesh(4, 2), 1);
557 }
558 }
559 {
561 let mut chunks = cache.chunks.write().unwrap();
562 for e in chunks.entries.values_mut() {
563 e.dirty = false;
564 }
565 }
566
567 cache.invalidate_region(Vec3::new(1.1, 0.0, 1.1), Vec3::new(1.9, 0.9, 1.9));
569
570 let dirty = cache.dirty_chunks();
571 assert_eq!(dirty.len(), 1);
572 assert_eq!(dirty[0], ChunkCoord::new(1, 0, 1));
573 }
574
575 #[test]
576 fn test_invalidate_all() {
577 let cache = ChunkedMeshCache::new(ChunkedCacheConfig::default());
578 cache.set_chunk(ChunkCoord::new(0, 0, 0), make_mesh(4, 2), 1);
579 cache.set_chunk(ChunkCoord::new(1, 0, 0), make_mesh(4, 2), 1);
580 {
582 let mut chunks = cache.chunks.write().unwrap();
583 for e in chunks.entries.values_mut() {
584 e.dirty = false;
585 }
586 }
587 assert_eq!(cache.dirty_chunks().len(), 0);
588
589 cache.invalidate_all();
590 assert_eq!(cache.dirty_chunks().len(), 2);
591 }
592
593 #[test]
594 fn test_merge_all() {
595 let cache = ChunkedMeshCache::new(ChunkedCacheConfig::default());
596 let mut mesh_a = make_mesh(4, 0);
598 mesh_a.indices = vec![0, 1, 2, 1, 2, 3];
599 cache.set_chunk(ChunkCoord::new(0, 0, 0), mesh_a, 1);
600
601 let mut mesh_b = make_mesh(3, 0);
603 mesh_b.indices = vec![0, 1, 2];
604 cache.set_chunk(ChunkCoord::new(1, 0, 0), mesh_b, 1);
605
606 let merged = cache.merge_all();
607 assert_eq!(merged.vertices.len(), 7); assert_eq!(merged.indices.len(), 9); for &idx in &merged.indices {
614 assert!((idx as usize) < merged.vertices.len());
615 }
616 }
617
618 #[test]
619 fn test_memory_usage() {
620 let cache = ChunkedMeshCache::new(ChunkedCacheConfig::default());
621 assert_eq!(cache.memory_usage(), 0);
622
623 let mesh = make_mesh(10, 4);
624 let expected = 10 * std::mem::size_of::<Vertex>() + 12 * std::mem::size_of::<u32>();
625 cache.set_chunk(ChunkCoord::new(0, 0, 0), mesh, 1);
626 assert_eq!(cache.memory_usage(), expected);
627 }
628
629 #[test]
630 fn test_update_sdf_hash_invalidation() {
631 let cache = ChunkedMeshCache::new(ChunkedCacheConfig::default());
632 cache.set_chunk(ChunkCoord::new(0, 0, 0), make_mesh(4, 2), 10);
633 cache.set_chunk(ChunkCoord::new(1, 0, 0), make_mesh(4, 2), 10);
634 cache.set_chunk(ChunkCoord::new(2, 0, 0), make_mesh(4, 2), 20);
635 {
637 let mut chunks = cache.chunks.write().unwrap();
638 for e in chunks.entries.values_mut() {
639 e.dirty = false;
640 }
641 }
642
643 let invalidated = cache.update_sdf_hash(20);
645 assert_eq!(invalidated.len(), 2);
646 assert!(!invalidated.contains(&ChunkCoord::new(2, 0, 0)));
648 }
649
650 #[test]
651 fn test_clear() {
652 let cache = ChunkedMeshCache::new(ChunkedCacheConfig::default());
653 cache.set_chunk(ChunkCoord::new(0, 0, 0), make_mesh(4, 2), 1);
654 cache.set_chunk(ChunkCoord::new(1, 0, 0), make_mesh(4, 2), 1);
655 assert_eq!(cache.chunk_count(), 2);
656
657 cache.clear();
658 assert_eq!(cache.chunk_count(), 0);
659 assert!(cache.get_chunk(&ChunkCoord::new(0, 0, 0)).is_none());
660 }
661
662 #[test]
663 fn test_cached_chunks() {
664 let cache = ChunkedMeshCache::new(ChunkedCacheConfig::default());
665 cache.set_chunk(ChunkCoord::new(0, 0, 0), make_mesh(4, 2), 1);
666 cache.set_chunk(ChunkCoord::new(1, 2, 3), make_mesh(4, 2), 1);
667
668 let cached = cache.cached_chunks();
669 assert_eq!(cached.len(), 2);
670 assert!(cached.contains(&ChunkCoord::new(0, 0, 0)));
671 assert!(cached.contains(&ChunkCoord::new(1, 2, 3)));
672 }
673
674 #[test]
675 fn test_persist_dirty_and_load_chunk() {
676 let dir = std::env::temp_dir().join("alice_sdf_chunked_test");
677 let _ = std::fs::remove_dir_all(&dir);
678
679 let config = ChunkedCacheConfig {
680 cache_dir: Some(dir.clone()),
681 ..Default::default()
682 };
683 let cache = ChunkedMeshCache::new(config);
684
685 let coord = ChunkCoord::new(1, -2, 3);
687 cache.set_chunk(coord, make_mesh(6, 3), 42);
688 let written = cache.persist_dirty().unwrap();
689 assert_eq!(written, 1);
690
691 assert_eq!(cache.dirty_chunks().len(), 0);
693
694 let config2 = ChunkedCacheConfig {
696 cache_dir: Some(dir.clone()),
697 ..Default::default()
698 };
699 let cache2 = ChunkedMeshCache::new(config2);
700 let loaded = cache2.load_chunk(&coord).unwrap();
701 assert!(loaded.is_some());
702 assert_eq!(loaded.unwrap().vertex_count(), 6);
703
704 let _ = std::fs::remove_dir_all(&dir);
706 }
707
708 #[test]
709 fn test_persist_without_cache_dir_errors() {
710 let cache = ChunkedMeshCache::new(ChunkedCacheConfig {
711 cache_dir: None,
712 ..Default::default()
713 });
714 cache.set_chunk(ChunkCoord::new(0, 0, 0), make_mesh(4, 2), 1);
715 assert!(cache.persist_dirty().is_err());
716 }
717
718 #[test]
719 fn test_max_cached_chunks_eviction() {
720 let cache = ChunkedMeshCache::new(ChunkedCacheConfig {
721 max_cached_chunks: 3,
722 ..Default::default()
723 });
724 for i in 0..5 {
725 cache.set_chunk(ChunkCoord::new(i, 0, 0), make_mesh(4, 2), 1);
726 }
727 assert!(cache.chunk_count() <= 3);
729 }
730}