1use std::collections::{HashMap, BinaryHeap, VecDeque};
7use std::sync::{Arc, Mutex, atomic::{AtomicUsize, Ordering}};
8use std::thread;
9use glam::Vec3;
10
11use crate::terrain::mod_types::{TerrainChunk, TerrainConfig, ChunkCoord, ChunkState};
12use crate::terrain::heightmap::{HeightMap, FractalNoise, DiamondSquare, PerlinTerrain};
13use crate::terrain::biome::{BiomeMap, ClimateSimulator};
14use crate::terrain::vegetation::VegetationSystem;
15
16struct CacheEntry {
20 chunk: TerrainChunk,
21 access_order: u64,
22}
23
24pub struct ChunkCache {
26 entries: HashMap<ChunkCoord, CacheEntry>,
27 max_size: usize,
28 clock: u64,
30 memory_budget: usize,
32 current_memory: usize,
33}
34
35impl ChunkCache {
36 pub fn new(max_size: usize) -> Self {
37 Self {
38 entries: HashMap::new(),
39 max_size,
40 clock: 0,
41 memory_budget: max_size * 1024 * 1024, current_memory: 0,
43 }
44 }
45
46 pub fn with_memory_budget(max_size: usize, budget_bytes: usize) -> Self {
47 let mut c = Self::new(max_size);
48 c.memory_budget = budget_bytes;
49 c
50 }
51
52 pub fn insert(&mut self, coord: ChunkCoord, chunk: TerrainChunk) {
54 let mem = Self::estimate_chunk_memory(&chunk);
55 while (self.entries.len() >= self.max_size || self.current_memory + mem > self.memory_budget)
57 && !self.entries.is_empty()
58 {
59 self.evict_lru();
60 }
61 self.clock += 1;
62 let old_mem = self.entries.get(&coord).map(|e| Self::estimate_chunk_memory(&e.chunk)).unwrap_or(0);
63 self.current_memory = self.current_memory.saturating_sub(old_mem) + mem;
64 self.entries.insert(coord, CacheEntry { chunk, access_order: self.clock });
65 }
66
67 pub fn get(&mut self, coord: ChunkCoord) -> Option<&TerrainChunk> {
69 if let Some(entry) = self.entries.get_mut(&coord) {
70 self.clock += 1;
71 entry.access_order = self.clock;
72 Some(&entry.chunk)
73 } else {
74 None
75 }
76 }
77
78 pub fn contains(&self, coord: ChunkCoord) -> bool {
80 self.entries.contains_key(&coord)
81 }
82
83 pub fn remove(&mut self, coord: ChunkCoord) -> Option<TerrainChunk> {
85 if let Some(entry) = self.entries.remove(&coord) {
86 self.current_memory = self.current_memory
87 .saturating_sub(Self::estimate_chunk_memory(&entry.chunk));
88 Some(entry.chunk)
89 } else {
90 None
91 }
92 }
93
94 fn evict_lru(&mut self) {
96 let lru_key = self.entries.iter()
97 .min_by_key(|(_, e)| e.access_order)
98 .map(|(k, _)| *k);
99 if let Some(key) = lru_key {
100 if let Some(entry) = self.entries.remove(&key) {
101 self.current_memory = self.current_memory
102 .saturating_sub(Self::estimate_chunk_memory(&entry.chunk));
103 }
104 }
105 }
106
107 fn estimate_chunk_memory(chunk: &TerrainChunk) -> usize {
109 let heightmap_bytes = chunk.heightmap.data.len() * 4;
110 let base = std::mem::size_of::<TerrainChunk>();
111 base + heightmap_bytes + 512 }
113
114 pub fn len(&self) -> usize { self.entries.len() }
115 pub fn is_empty(&self) -> bool { self.entries.is_empty() }
116 pub fn current_memory_bytes(&self) -> usize { self.current_memory }
117}
118
119#[derive(Clone, Debug)]
123struct LoadRequest {
124 coord: ChunkCoord,
125 priority: i64, }
127
128impl PartialEq for LoadRequest {
129 fn eq(&self, other: &Self) -> bool { self.priority == other.priority }
130}
131impl Eq for LoadRequest {}
132impl PartialOrd for LoadRequest {
133 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
134 Some(self.cmp(other))
135 }
136}
137impl Ord for LoadRequest {
138 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
139 self.priority.cmp(&other.priority)
140 }
141}
142
143pub struct LoadQueue {
145 heap: BinaryHeap<LoadRequest>,
146 in_queue: std::collections::HashSet<ChunkCoord>,
147}
148
149impl LoadQueue {
150 pub fn new() -> Self {
151 Self { heap: BinaryHeap::new(), in_queue: std::collections::HashSet::new() }
152 }
153
154 pub fn push(&mut self, coord: ChunkCoord, priority: i64) {
156 if self.in_queue.insert(coord) {
157 self.heap.push(LoadRequest { coord, priority });
158 }
159 }
160
161 pub fn pop(&mut self) -> Option<ChunkCoord> {
163 while let Some(req) = self.heap.pop() {
164 if self.in_queue.remove(&req.coord) {
165 return Some(req.coord);
166 }
167 }
168 None
169 }
170
171 pub fn len(&self) -> usize { self.in_queue.len() }
172 pub fn is_empty(&self) -> bool { self.in_queue.is_empty() }
173
174 pub fn reprioritize(&mut self, camera: Vec3, config: &TerrainConfig) {
176 let coords: Vec<ChunkCoord> = self.in_queue.drain().collect();
177 self.heap.clear();
178 for coord in coords {
179 let dist = coord.distance_to_world_pos(camera, config.chunk_size as f32);
180 let priority = -(dist as i64);
181 self.heap.push(LoadRequest { coord, priority });
182 self.in_queue.insert(coord);
183 }
184 }
185}
186
187impl Default for LoadQueue {
188 fn default() -> Self { Self::new() }
189}
190
191pub struct ChunkGenerator {
195 pub config: TerrainConfig,
196}
197
198impl ChunkGenerator {
199 pub fn new(config: TerrainConfig) -> Self { Self { config } }
200
201 pub fn generate(&self, coord: ChunkCoord) -> TerrainChunk {
203 let size = self.config.chunk_size;
204 let seed = self.chunk_seed(coord);
205
206 let mut heightmap = self.generate_heightmap(coord, size, seed);
208 heightmap.island_mask(2.0);
209 heightmap.normalize();
210
211 crate::terrain::heightmap::HydraulicErosion::erode(
213 &mut heightmap, 1000, 1.0, 8.0, 0.05, seed,
214 );
215 crate::terrain::heightmap::ThermalErosion::erode(&mut heightmap, 10, 0.04);
216 heightmap.normalize();
217
218 let sim = ClimateSimulator::default();
220 let climate = sim.simulate(&heightmap);
221 let biome_map = BiomeMap::from_heightmap(&heightmap, &climate);
222
223 let vegetation = VegetationSystem::generate(&heightmap, &biome_map, 0.8, seed);
225
226 TerrainChunk {
227 coord,
228 heightmap,
229 biome_map: Some(biome_map),
230 vegetation: Some(vegetation),
231 lod_level: 0,
232 state: ChunkState::Ready,
233 last_used: std::time::Instant::now(),
234 seed,
235 }
236 }
237
238 pub fn generate_lod(&self, coord: ChunkCoord, lod: u8) -> TerrainChunk {
240 let scale = 1usize << lod as usize;
241 let size = (self.config.chunk_size / scale).max(4);
242 let seed = self.chunk_seed(coord);
243 let mut heightmap = self.generate_heightmap(coord, size, seed);
244 heightmap.normalize();
245
246 TerrainChunk {
247 coord,
248 heightmap,
249 biome_map: None,
250 vegetation: None,
251 lod_level: lod,
252 state: ChunkState::Ready,
253 last_used: std::time::Instant::now(),
254 seed,
255 }
256 }
257
258 fn generate_heightmap(&self, coord: ChunkCoord, size: usize, seed: u64) -> HeightMap {
259 let offset_x = coord.0 as f32 * size as f32;
261 let offset_z = coord.1 as f32 * size as f32;
262 let world_scale = 256.0f32;
263
264 let mut hm = HeightMap::new(size, size);
266 let noise = crate::terrain::heightmap::FractalNoise::generate(
267 size, size, 6, 2.0, 0.5, 4.0, seed,
268 );
269 for y in 0..size {
271 for x in 0..size {
272 let nx = (x as f32 + offset_x) / world_scale;
273 let ny = (y as f32 + offset_z) / world_scale;
274 let idx = y * size + x;
275 hm.data[idx] = noise.data[idx];
276 }
277 }
278 hm
279 }
280
281 fn chunk_seed(&self, coord: ChunkCoord) -> u64 {
282 let base = self.config.seed;
283 let cx = coord.0 as u64;
284 let cz = coord.1 as u64;
285 base.wrapping_add(cx.wrapping_mul(0x9e3779b97f4a7c15))
286 .wrapping_add(cz.wrapping_mul(0x6c62272e07bb0142))
287 }
288}
289
290const CHUNK_MAGIC: u32 = 0x43484E4B; const CHUNK_VERSION: u16 = 1;
295
296pub struct ChunkSerializer;
306
307impl ChunkSerializer {
308 pub fn serialize(chunk: &TerrainChunk) -> Vec<u8> {
310 let mut out = Vec::new();
311 out.extend_from_slice(&CHUNK_MAGIC.to_le_bytes());
313 out.extend_from_slice(&CHUNK_VERSION.to_le_bytes());
314 out.extend_from_slice(&chunk.coord.0.to_le_bytes());
315 out.extend_from_slice(&chunk.coord.1.to_le_bytes());
316 out.extend_from_slice(&0u64.to_le_bytes());
318 out.extend_from_slice(&chunk.seed.to_le_bytes());
319 out.push(chunk.lod_level);
320 let hm_bytes = chunk.heightmap.to_raw_bytes();
322 let hm_len = hm_bytes.len() as u32;
323 out.extend_from_slice(&hm_len.to_le_bytes());
324 out.extend_from_slice(&hm_bytes);
325 let checksum: u32 = out.iter().fold(0u32, |acc, &b| acc.wrapping_add(b as u32));
327 out.extend_from_slice(&checksum.to_le_bytes());
328 out
329 }
330
331 pub fn deserialize(bytes: &[u8]) -> Option<TerrainChunk> {
333 if bytes.len() < 4 + 2 + 4 + 4 + 8 + 8 + 1 + 4 { return None; }
334 let mut pos = 0;
335
336 let magic = u32::from_le_bytes(bytes[pos..pos+4].try_into().ok()?); pos += 4;
337 if magic != CHUNK_MAGIC { return None; }
338
339 let version = u16::from_le_bytes(bytes[pos..pos+2].try_into().ok()?); pos += 2;
340 if version != CHUNK_VERSION { return None; }
341
342 let coord_x = i32::from_le_bytes(bytes[pos..pos+4].try_into().ok()?); pos += 4;
343 let coord_z = i32::from_le_bytes(bytes[pos..pos+4].try_into().ok()?); pos += 4;
344 let _timestamp = u64::from_le_bytes(bytes[pos..pos+8].try_into().ok()?); pos += 8;
345 let seed = u64::from_le_bytes(bytes[pos..pos+8].try_into().ok()?); pos += 8;
346 let lod_level = bytes[pos]; pos += 1;
347
348 let hm_len = u32::from_le_bytes(bytes[pos..pos+4].try_into().ok()?) as usize; pos += 4;
349 if bytes.len() < pos + hm_len + 4 { return None; }
350
351 let hm_bytes = &bytes[pos..pos + hm_len]; pos += hm_len;
352 let heightmap = HeightMap::from_raw_bytes(hm_bytes)?;
353
354 let stored_checksum = u32::from_le_bytes(bytes[pos..pos+4].try_into().ok()?);
356 let computed: u32 = bytes[..pos].iter().fold(0u32, |acc, &b| acc.wrapping_add(b as u32));
357 if stored_checksum != computed { return None; }
358
359 Some(TerrainChunk {
360 coord: ChunkCoord(coord_x, coord_z),
361 heightmap,
362 biome_map: None,
363 vegetation: None,
364 lod_level,
365 state: ChunkState::Ready,
366 last_used: std::time::Instant::now(),
367 seed,
368 })
369 }
370
371 pub fn save_to_file(chunk: &TerrainChunk, path: &str) -> Result<(), String> {
373 let bytes = Self::serialize(chunk);
374 std::fs::write(path, &bytes).map_err(|e| e.to_string())
375 }
376
377 pub fn load_from_file(path: &str) -> Result<TerrainChunk, String> {
379 let bytes = std::fs::read(path).map_err(|e| e.to_string())?;
380 Self::deserialize(&bytes).ok_or_else(|| "Invalid chunk format".to_string())
381 }
382}
383
384#[derive(Debug, Default, Clone)]
388pub struct StreamingStats {
389 pub chunks_loaded: usize,
390 pub chunks_unloaded: usize,
391 pub cache_hits: usize,
392 pub cache_misses: usize,
393 pub pending_count: usize,
394 pub memory_bytes: usize,
395 pub generate_time_ms: f64,
396}
397
398impl StreamingStats {
399 pub fn cache_hit_rate(&self) -> f32 {
400 let total = self.cache_hits + self.cache_misses;
401 if total == 0 { 0.0 } else { self.cache_hits as f32 / total as f32 }
402 }
403}
404
405#[derive(Debug, Default)]
409pub struct VisibilitySet {
410 visible: std::collections::HashSet<ChunkCoord>,
411 previous: std::collections::HashSet<ChunkCoord>,
412}
413
414impl VisibilitySet {
415 pub fn new() -> Self { Self::default() }
416
417 pub fn update(&mut self, camera_pos: Vec3, config: &TerrainConfig) {
419 self.previous = std::mem::take(&mut self.visible);
420 let chunk_world = config.chunk_size as f32;
421 let view_chunks = config.view_distance as i32;
422 let cam_cx = (camera_pos.x / chunk_world).floor() as i32;
423 let cam_cz = (camera_pos.z / chunk_world).floor() as i32;
424 for dz in -view_chunks..=view_chunks {
425 for dx in -view_chunks..=view_chunks {
426 let dist = ((dx * dx + dz * dz) as f32).sqrt();
427 if dist <= config.view_distance as f32 {
428 self.visible.insert(ChunkCoord(cam_cx + dx, cam_cz + dz));
429 }
430 }
431 }
432 }
433
434 pub fn newly_visible(&self) -> impl Iterator<Item = ChunkCoord> + '_ {
436 self.visible.iter().copied().filter(|c| !self.previous.contains(c))
437 }
438
439 pub fn newly_hidden(&self) -> impl Iterator<Item = ChunkCoord> + '_ {
441 self.previous.iter().copied().filter(|c| !self.visible.contains(c))
442 }
443
444 pub fn is_visible(&self, coord: ChunkCoord) -> bool { self.visible.contains(&coord) }
445 pub fn visible_count(&self) -> usize { self.visible.len() }
446 pub fn visible_coords(&self) -> impl Iterator<Item = ChunkCoord> + '_ { self.visible.iter().copied() }
447}
448
449pub struct LodScheduler {
456 pub lod_thresholds: Vec<f32>,
458 pub hysteresis_margin: f32,
460 pending: HashMap<ChunkCoord, u8>,
462}
463
464impl LodScheduler {
465 pub fn new(lod_levels: usize, chunk_size: f32) -> Self {
466 let thresholds: Vec<f32> = (0..lod_levels)
467 .map(|l| chunk_size * 2.0 * (1 << l) as f32)
468 .collect();
469 Self {
470 lod_thresholds: thresholds,
471 hysteresis_margin: chunk_size * 0.5,
472 pending: HashMap::new(),
473 }
474 }
475
476 pub fn desired_lod(&self, dist: f32, current_lod: u8) -> u8 {
478 let mut target = (self.lod_thresholds.len() - 1) as u8;
479 for (l, &threshold) in self.lod_thresholds.iter().enumerate() {
480 if dist < threshold {
481 target = l as u8;
482 break;
483 }
484 }
485 if target > current_lod {
487 let cur_thresh = self.lod_thresholds.get(current_lod as usize).copied().unwrap_or(0.0);
489 if dist < cur_thresh + self.hysteresis_margin {
490 return current_lod; }
492 }
493 target
495 }
496
497 pub fn update(&mut self, camera_pos: Vec3, chunks: &HashMap<ChunkCoord, u8>, config: &TerrainConfig) {
499 self.pending.clear();
500 for (&coord, ¤t_lod) in chunks {
501 let dist = coord.distance_to_world_pos(camera_pos, config.chunk_size as f32);
502 let desired = self.desired_lod(dist, current_lod);
503 if desired != current_lod {
504 self.pending.insert(coord, desired);
505 }
506 }
507 }
508
509 pub fn pending_changes(&self) -> &HashMap<ChunkCoord, u8> { &self.pending }
511
512 pub fn load_priority(
515 coord: ChunkCoord,
516 camera_pos: Vec3,
517 chunk_size: f32,
518 in_frustum: bool,
519 ) -> i64 {
520 let dist = coord.distance_to_world_pos(camera_pos, chunk_size);
521 let dist_score = -(dist as i64);
522 let frustum_bonus: i64 = if in_frustum { 1_000_000 } else { 0 };
523 dist_score + frustum_bonus
524 }
525}
526
527pub struct Prefetcher {
531 history: VecDeque<Vec3>,
533 lookahead_frames: usize,
535 velocity: Vec3,
537}
538
539impl Prefetcher {
540 pub fn new(lookahead_frames: usize) -> Self {
541 Self {
542 history: VecDeque::with_capacity(16),
543 lookahead_frames,
544 velocity: Vec3::ZERO,
545 }
546 }
547
548 pub fn push_position(&mut self, pos: Vec3) {
550 if self.history.len() >= 16 { self.history.pop_front(); }
551 self.history.push_back(pos);
552 self.estimate_velocity();
553 }
554
555 fn estimate_velocity(&mut self) {
556 if self.history.len() < 2 {
557 self.velocity = Vec3::ZERO;
558 return;
559 }
560 let recent = self.history.back().copied().unwrap_or(Vec3::ZERO);
561 let old = self.history.front().copied().unwrap_or(Vec3::ZERO);
562 let n = self.history.len() as f32;
563 self.velocity = (recent - old) / n;
564 }
565
566 pub fn predicted_position(&self) -> Vec3 {
568 self.history.back().copied().unwrap_or(Vec3::ZERO)
569 + self.velocity * self.lookahead_frames as f32
570 }
571
572 pub fn prefetch_coords(&self, config: &TerrainConfig) -> Vec<ChunkCoord> {
574 let pred = self.predicted_position();
575 let chunk_world = config.chunk_size as f32;
576 let pred_cx = (pred.x / chunk_world).floor() as i32;
577 let pred_cz = (pred.z / chunk_world).floor() as i32;
578 let prefetch_radius = 2i32;
579 let mut coords = Vec::new();
580 for dz in -prefetch_radius..=prefetch_radius {
581 for dx in -prefetch_radius..=prefetch_radius {
582 coords.push(ChunkCoord(pred_cx + dx, pred_cz + dz));
583 }
584 }
585 coords
586 }
587}
588
589type GenerateResult = (ChunkCoord, TerrainChunk);
592
593pub struct GeneratorPool {
595 workers: Vec<thread::JoinHandle<()>>,
596 tx: std::sync::mpsc::Sender<ChunkCoord>,
597 rx_results: Arc<Mutex<Vec<GenerateResult>>>,
598 active: Arc<AtomicUsize>,
599 config: TerrainConfig,
600}
601
602impl GeneratorPool {
603 pub fn new(num_workers: usize, config: TerrainConfig) -> Self {
604 let (tx, rx) = std::sync::mpsc::channel::<ChunkCoord>();
605 let rx = Arc::new(Mutex::new(rx));
606 let results = Arc::new(Mutex::new(Vec::new()));
607 let active = Arc::new(AtomicUsize::new(0));
608 let mut workers = Vec::new();
609
610 for _ in 0..num_workers {
611 let rx2 = Arc::clone(&rx);
612 let res2 = Arc::clone(&results);
613 let act2 = Arc::clone(&active);
614 let cfg2 = config.clone();
615 let handle = thread::spawn(move || {
616 let gen = ChunkGenerator::new(cfg2);
617 loop {
618 let coord = {
619 let lock = rx2.lock().unwrap();
620 match lock.recv() {
621 Ok(c) => c,
622 Err(_) => break,
623 }
624 };
625 act2.fetch_add(1, Ordering::Relaxed);
626 let chunk = gen.generate(coord);
627 {
628 let mut lock = res2.lock().unwrap();
629 lock.push((coord, chunk));
630 }
631 act2.fetch_sub(1, Ordering::Relaxed);
632 }
633 });
634 workers.push(handle);
635 }
636
637 Self { workers, tx, rx_results: results, active, config }
638 }
639
640 pub fn submit(&self, coord: ChunkCoord) {
642 let _ = self.tx.send(coord);
643 }
644
645 pub fn drain_results(&self) -> Vec<GenerateResult> {
647 let mut lock = self.rx_results.lock().unwrap();
648 std::mem::take(&mut *lock)
649 }
650
651 pub fn active_count(&self) -> usize { self.active.load(Ordering::Relaxed) }
653}
654
655pub struct StreamingManager {
662 pub config: TerrainConfig,
663 pub cache: ChunkCache,
664 pub load_queue: LoadQueue,
665 pub lod_scheduler: LodScheduler,
666 pub prefetcher: Prefetcher,
667 pub visibility: VisibilitySet,
668 pub stats: StreamingStats,
669 in_flight: std::collections::HashSet<ChunkCoord>,
671 pool: Option<GeneratorPool>,
673 camera_pos: Vec3,
675 chunk_lods: HashMap<ChunkCoord, u8>,
677}
678
679impl StreamingManager {
680 pub fn new(config: TerrainConfig) -> Self {
681 let lod_levels = config.lod_levels;
682 let chunk_size = config.chunk_size as f32;
683 let max_cache = (config.view_distance * 2 + 1).pow(2) as usize * 2;
684 let pool = GeneratorPool::new(4, config.clone());
685
686 Self {
687 lod_scheduler: LodScheduler::new(lod_levels, chunk_size),
688 cache: ChunkCache::new(max_cache),
689 load_queue: LoadQueue::new(),
690 prefetcher: Prefetcher::new(8),
691 visibility: VisibilitySet::new(),
692 stats: StreamingStats::default(),
693 in_flight: std::collections::HashSet::new(),
694 pool: Some(pool),
695 camera_pos: Vec3::ZERO,
696 chunk_lods: HashMap::new(),
697 config,
698 }
699 }
700
701 pub fn new_synchronous(config: TerrainConfig) -> Self {
703 let lod_levels = config.lod_levels;
704 let chunk_size = config.chunk_size as f32;
705 let max_cache = (config.view_distance * 2 + 1).pow(2) as usize * 2;
706 Self {
707 lod_scheduler: LodScheduler::new(lod_levels, chunk_size),
708 cache: ChunkCache::new(max_cache),
709 load_queue: LoadQueue::new(),
710 prefetcher: Prefetcher::new(8),
711 visibility: VisibilitySet::new(),
712 stats: StreamingStats::default(),
713 in_flight: std::collections::HashSet::new(),
714 pool: None,
715 camera_pos: Vec3::ZERO,
716 chunk_lods: HashMap::new(),
717 config,
718 }
719 }
720
721 pub fn update(&mut self, camera_pos: Vec3) {
724 self.camera_pos = camera_pos;
725 self.prefetcher.push_position(camera_pos);
726
727 self.visibility.update(camera_pos, &self.config);
729
730 for coord in self.visibility.newly_visible() {
732 if !self.cache.contains(coord) && !self.in_flight.contains(&coord) {
733 let priority = LodScheduler::load_priority(
734 coord, camera_pos, self.config.chunk_size as f32, true,
735 );
736 self.load_queue.push(coord, priority);
737 }
738 }
739
740 let prefetch = self.prefetcher.prefetch_coords(&self.config);
742 for coord in prefetch {
743 if !self.cache.contains(coord) && !self.in_flight.contains(&coord) {
744 let priority = LodScheduler::load_priority(
745 coord, camera_pos, self.config.chunk_size as f32, false,
746 ) - 500_000; self.load_queue.push(coord, priority);
748 }
749 }
750
751 self.load_queue.reprioritize(camera_pos, &self.config);
753
754 let max_in_flight = 8usize;
756 while self.in_flight.len() < max_in_flight {
757 if let Some(coord) = self.load_queue.pop() {
758 if self.pool.is_some() {
759 self.pool.as_ref().unwrap().submit(coord);
760 self.in_flight.insert(coord);
761 } else {
762 let t0 = std::time::Instant::now();
764 let gen = ChunkGenerator::new(self.config.clone());
765 let chunk = gen.generate(coord);
766 self.stats.generate_time_ms += t0.elapsed().as_secs_f64() * 1000.0;
767 self.chunk_lods.insert(coord, chunk.lod_level);
768 self.cache.insert(coord, chunk);
769 self.stats.chunks_loaded += 1;
770 self.stats.cache_misses += 1;
771 }
772 } else {
773 break;
774 }
775 }
776
777 if let Some(ref pool) = self.pool {
779 for (coord, chunk) in pool.drain_results() {
780 self.in_flight.remove(&coord);
781 self.chunk_lods.insert(coord, chunk.lod_level);
782 self.cache.insert(coord, chunk);
783 self.stats.chunks_loaded += 1;
784 self.stats.cache_misses += 1;
785 }
786 }
787
788 for coord in self.visibility.newly_hidden() {
790 if self.cache.contains(coord) {
791 self.cache.remove(coord);
792 self.chunk_lods.remove(&coord);
793 self.stats.chunks_unloaded += 1;
794 }
795 }
796
797 self.lod_scheduler.update(camera_pos, &self.chunk_lods, &self.config);
799
800 self.stats.pending_count = self.load_queue.len() + self.in_flight.len();
801 self.stats.memory_bytes = self.cache.current_memory_bytes();
802 }
803
804 pub fn get_chunk(&mut self, coord: ChunkCoord) -> Option<&TerrainChunk> {
806 if let Some(chunk) = self.cache.get(coord) {
807 self.stats.cache_hits += 1;
808 Some(chunk)
809 } else {
810 self.stats.cache_misses += 1;
811 None
812 }
813 }
814
815 pub fn force_load(&mut self, coord: ChunkCoord) -> &TerrainChunk {
817 if !self.cache.contains(coord) {
818 let gen = ChunkGenerator::new(self.config.clone());
819 let chunk = gen.generate(coord);
820 self.chunk_lods.insert(coord, chunk.lod_level);
821 self.cache.insert(coord, chunk);
822 self.stats.chunks_loaded += 1;
823 }
824 self.cache.get(coord).unwrap()
825 }
826
827 pub fn sample_height_world(&mut self, world_x: f32, world_z: f32) -> f32 {
829 let chunk_world = self.config.chunk_size as f32;
830 let cx = (world_x / chunk_world).floor() as i32;
831 let cz = (world_z / chunk_world).floor() as i32;
832 let coord = ChunkCoord(cx, cz);
833 let local_x = world_x - cx as f32 * chunk_world;
834 let local_z = world_z - cz as f32 * chunk_world;
835 if let Some(chunk) = self.cache.get(coord) {
836 let hm = &chunk.heightmap;
837 let sx = (local_x / chunk_world * hm.width as f32).clamp(0.0, hm.width as f32 - 1.0);
838 let sz = (local_z / chunk_world * hm.height as f32).clamp(0.0, hm.height as f32 - 1.0);
839 hm.sample_bilinear(sx, sz)
840 } else {
841 0.0
842 }
843 }
844
845 pub fn stats(&self) -> &StreamingStats { &self.stats }
846 pub fn visible_chunk_count(&self) -> usize { self.visibility.visible_count() }
847 pub fn cache_size(&self) -> usize { self.cache.len() }
848 pub fn in_flight_count(&self) -> usize { self.in_flight.len() }
849}
850
851#[cfg(test)]
854mod tests {
855 use super::*;
856
857 fn test_config() -> TerrainConfig {
858 TerrainConfig {
859 chunk_size: 16,
860 view_distance: 2,
861 lod_levels: 3,
862 seed: 42,
863 }
864 }
865
866 #[test]
867 fn test_chunk_cache_insert_get() {
868 let config = test_config();
869 let gen = ChunkGenerator::new(config.clone());
870 let chunk = gen.generate(ChunkCoord(0, 0));
871 let mut cache = ChunkCache::new(10);
872 cache.insert(ChunkCoord(0, 0), chunk);
873 assert!(cache.contains(ChunkCoord(0, 0)));
874 assert!(cache.get(ChunkCoord(0, 0)).is_some());
875 assert!(!cache.contains(ChunkCoord(1, 0)));
876 }
877
878 #[test]
879 fn test_chunk_cache_lru_eviction() {
880 let config = test_config();
881 let gen = ChunkGenerator::new(config.clone());
882 let mut cache = ChunkCache::new(3);
883 for i in 0..4i32 {
884 let chunk = gen.generate(ChunkCoord(i, 0));
885 cache.insert(ChunkCoord(i, 0), chunk);
886 }
887 assert!(cache.len() <= 3);
888 }
889
890 #[test]
891 fn test_load_queue_priority() {
892 let mut q = LoadQueue::new();
893 q.push(ChunkCoord(5, 0), -50);
894 q.push(ChunkCoord(1, 0), -10);
895 q.push(ChunkCoord(3, 0), -30);
896 let first = q.pop().unwrap();
897 assert_eq!(first, ChunkCoord(1, 0)); }
899
900 #[test]
901 fn test_load_queue_no_duplicates() {
902 let mut q = LoadQueue::new();
903 q.push(ChunkCoord(0, 0), 100);
904 q.push(ChunkCoord(0, 0), 200); assert_eq!(q.len(), 1);
906 }
907
908 #[test]
909 fn test_chunk_generator() {
910 let config = test_config();
911 let gen = ChunkGenerator::new(config);
912 let chunk = gen.generate(ChunkCoord(0, 0));
913 assert_eq!(chunk.coord, ChunkCoord(0, 0));
914 assert!(chunk.heightmap.width > 0);
915 assert_eq!(chunk.heightmap.data.len(), chunk.heightmap.width * chunk.heightmap.height);
916 }
917
918 #[test]
919 fn test_chunk_serializer_roundtrip() {
920 let config = test_config();
921 let gen = ChunkGenerator::new(config);
922 let chunk = gen.generate(ChunkCoord(2, -3));
923 let bytes = ChunkSerializer::serialize(&chunk);
924 let restored = ChunkSerializer::deserialize(&bytes).expect("deserialize failed");
925 assert_eq!(restored.coord, chunk.coord);
926 assert_eq!(restored.seed, chunk.seed);
927 assert_eq!(restored.heightmap.width, chunk.heightmap.width);
928 for (a, b) in chunk.heightmap.data.iter().zip(restored.heightmap.data.iter()) {
929 assert!((a - b).abs() < 1e-6);
930 }
931 }
932
933 #[test]
934 fn test_chunk_serializer_invalid_magic() {
935 let mut bytes = vec![0u8; 64];
936 bytes[0] = 0xFF; assert!(ChunkSerializer::deserialize(&bytes).is_none());
938 }
939
940 #[test]
941 fn test_visibility_set_update() {
942 let config = test_config();
943 let mut vis = VisibilitySet::new();
944 vis.update(Vec3::new(0.0, 0.0, 0.0), &config);
945 assert!(vis.visible_count() > 0);
946 assert!(vis.is_visible(ChunkCoord(0, 0)));
947 }
948
949 #[test]
950 fn test_lod_scheduler_desired_lod() {
951 let sched = LodScheduler::new(3, 32.0);
952 assert_eq!(sched.desired_lod(10.0, 0), 0); let far = sched.lod_thresholds[1] + 1.0;
954 let lod = sched.desired_lod(far, 0);
956 assert!(lod >= 1);
957 }
958
959 #[test]
960 fn test_lod_scheduler_hysteresis() {
961 let sched = LodScheduler::new(3, 32.0);
962 let threshold = sched.lod_thresholds[0];
963 let margin = sched.hysteresis_margin;
964 let dist = threshold + margin * 0.5;
966 assert_eq!(sched.desired_lod(dist, 0), 0);
967 let dist2 = threshold + margin + 1.0;
969 assert!(sched.desired_lod(dist2, 0) >= 1);
970 }
971
972 #[test]
973 fn test_prefetcher() {
974 let mut pf = Prefetcher::new(8);
975 pf.push_position(Vec3::new(0.0, 0.0, 0.0));
976 pf.push_position(Vec3::new(10.0, 0.0, 0.0));
977 let pred = pf.predicted_position();
978 assert!(pred.x >= 10.0);
980 }
981
982 #[test]
983 fn test_streaming_manager_synchronous() {
984 let config = test_config();
985 let mut mgr = StreamingManager::new_synchronous(config);
986 mgr.update(Vec3::new(0.0, 0.0, 0.0));
987 assert!(mgr.cache_size() > 0 || mgr.stats.pending_count >= 0);
989 }
990
991 #[test]
992 fn test_streaming_manager_force_load() {
993 let config = test_config();
994 let mut mgr = StreamingManager::new_synchronous(config);
995 let chunk = mgr.force_load(ChunkCoord(0, 0));
996 assert_eq!(chunk.coord, ChunkCoord(0, 0));
997 assert_eq!(mgr.cache_size(), 1);
998 }
999}
1000
1001#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1005pub enum SerializationFormat {
1006 Raw,
1008 RLE,
1010 Delta,
1012}
1013
1014pub struct ExtendedChunkSerializer;
1016
1017impl ExtendedChunkSerializer {
1018 pub fn serialize_with_format(
1020 chunk: &crate::terrain::mod_types::TerrainChunk,
1021 format: SerializationFormat,
1022 ) -> Vec<u8> {
1023 let base = ChunkSerializer::serialize(chunk);
1024 match format {
1025 SerializationFormat::Raw => base,
1026 SerializationFormat::RLE => Self::rle_encode(&base),
1027 SerializationFormat::Delta => Self::delta_encode_bytes(&base),
1028 }
1029 }
1030
1031 pub fn deserialize_with_format(
1033 bytes: &[u8],
1034 format: SerializationFormat,
1035 ) -> Option<crate::terrain::mod_types::TerrainChunk> {
1036 let decoded = match format {
1037 SerializationFormat::Raw => bytes.to_vec(),
1038 SerializationFormat::RLE => Self::rle_decode(bytes)?,
1039 SerializationFormat::Delta => Self::delta_decode_bytes(bytes)?,
1040 };
1041 ChunkSerializer::deserialize(&decoded)
1042 }
1043
1044 pub fn rle_encode(data: &[u8]) -> Vec<u8> {
1046 if data.is_empty() { return Vec::new(); }
1047 let mut out = Vec::new();
1048 let mut i = 0;
1049 while i < data.len() {
1050 let val = data[i];
1051 let mut run = 1usize;
1052 while i + run < data.len() && data[i + run] == val && run < 255 {
1053 run += 1;
1054 }
1055 out.push(run as u8);
1056 out.push(val);
1057 i += run;
1058 }
1059 out
1060 }
1061
1062 pub fn rle_decode(data: &[u8]) -> Option<Vec<u8>> {
1064 if data.len() % 2 != 0 { return None; }
1065 let mut out = Vec::new();
1066 let mut i = 0;
1067 while i + 1 < data.len() {
1068 let count = data[i] as usize;
1069 let val = data[i + 1];
1070 for _ in 0..count { out.push(val); }
1071 i += 2;
1072 }
1073 Some(out)
1074 }
1075
1076 pub fn delta_encode_bytes(data: &[u8]) -> Vec<u8> {
1078 if data.is_empty() { return Vec::new(); }
1079 let mut out = Vec::with_capacity(data.len());
1080 out.push(data[0]);
1081 for i in 1..data.len() {
1082 out.push(data[i].wrapping_sub(data[i - 1]));
1083 }
1084 out
1085 }
1086
1087 pub fn delta_decode_bytes(data: &[u8]) -> Option<Vec<u8>> {
1089 if data.is_empty() { return Some(Vec::new()); }
1090 let mut out = Vec::with_capacity(data.len());
1091 out.push(data[0]);
1092 for i in 1..data.len() {
1093 out.push(data[i].wrapping_add(*out.last().unwrap()));
1094 }
1095 Some(out)
1096 }
1097}
1098
1099#[derive(Clone, Debug)]
1103pub enum ChunkEvent {
1104 Loaded(ChunkCoord),
1105 Unloaded(ChunkCoord),
1106 LodChanged { coord: ChunkCoord, old_lod: u8, new_lod: u8 },
1107 GenerationStarted(ChunkCoord),
1108 GenerationFailed { coord: ChunkCoord, reason: String },
1109}
1110
1111#[derive(Debug, Default)]
1113pub struct ChunkEventQueue {
1114 events: VecDeque<ChunkEvent>,
1115 max_size: usize,
1116}
1117
1118impl ChunkEventQueue {
1119 pub fn new(max_size: usize) -> Self {
1120 Self { events: VecDeque::new(), max_size }
1121 }
1122
1123 pub fn push(&mut self, event: ChunkEvent) {
1124 if self.events.len() >= self.max_size {
1125 self.events.pop_front();
1126 }
1127 self.events.push_back(event);
1128 }
1129
1130 pub fn drain(&mut self) -> Vec<ChunkEvent> {
1131 self.events.drain(..).collect()
1132 }
1133
1134 pub fn len(&self) -> usize { self.events.len() }
1135 pub fn is_empty(&self) -> bool { self.events.is_empty() }
1136}
1137
1138#[derive(Debug, Clone)]
1142pub struct MemoryBudget {
1143 pub max_bytes: usize,
1145 pub current_bytes: usize,
1147 pub headroom_bytes: usize,
1149 pub peak_bytes: usize,
1151}
1152
1153impl MemoryBudget {
1154 pub fn new(max_bytes: usize) -> Self {
1155 Self {
1156 max_bytes,
1157 current_bytes: 0,
1158 headroom_bytes: max_bytes / 10,
1159 peak_bytes: 0,
1160 }
1161 }
1162
1163 pub fn allocate(&mut self, bytes: usize) -> bool {
1164 if self.current_bytes + bytes + self.headroom_bytes > self.max_bytes {
1165 return false;
1166 }
1167 self.current_bytes += bytes;
1168 if self.current_bytes > self.peak_bytes {
1169 self.peak_bytes = self.current_bytes;
1170 }
1171 true
1172 }
1173
1174 pub fn free(&mut self, bytes: usize) {
1175 self.current_bytes = self.current_bytes.saturating_sub(bytes);
1176 }
1177
1178 pub fn utilization(&self) -> f32 {
1179 self.current_bytes as f32 / self.max_bytes as f32
1180 }
1181
1182 pub fn available(&self) -> usize {
1183 self.max_bytes.saturating_sub(self.current_bytes + self.headroom_bytes)
1184 }
1185
1186 pub fn is_over_budget(&self) -> bool {
1187 self.current_bytes + self.headroom_bytes > self.max_bytes
1188 }
1189}
1190
1191#[derive(Debug)]
1195pub struct WorldMap {
1196 pub height_overview: crate::terrain::heightmap::HeightMap,
1198 pub biome_overview: Vec<u8>,
1200 pub resolution: usize,
1202 pub world_size_chunks: usize,
1204}
1205
1206impl WorldMap {
1207 pub fn generate(world_size_chunks: usize, resolution: usize, config: &TerrainConfig) -> Self {
1209 let scale = world_size_chunks as f32 / resolution as f32;
1210 let height_overview = crate::terrain::heightmap::FractalNoise::generate(
1211 resolution, resolution, 6, 2.0, 0.5, 2.0, config.seed,
1212 );
1213 let biome_overview: Vec<u8> = height_overview.data.iter()
1214 .map(|&h| {
1215 let biome = if h < 0.1 { crate::terrain::biome::BiomeType::Ocean }
1216 else if h < 0.2 { crate::terrain::biome::BiomeType::Beach }
1217 else if h < 0.5 { crate::terrain::biome::BiomeType::Grassland }
1218 else if h < 0.7 { crate::terrain::biome::BiomeType::TemperateForest }
1219 else if h < 0.85 { crate::terrain::biome::BiomeType::Mountain }
1220 else { crate::terrain::biome::BiomeType::AlpineGlacier };
1221 biome as u8
1222 })
1223 .collect();
1224 Self { height_overview, biome_overview, resolution, world_size_chunks }
1225 }
1226
1227 pub fn sample_height(&self, nx: f32, ny: f32) -> f32 {
1229 let x = (nx * (self.resolution - 1) as f32).clamp(0.0, (self.resolution - 1) as f32);
1230 let y = (ny * (self.resolution - 1) as f32).clamp(0.0, (self.resolution - 1) as f32);
1231 self.height_overview.sample_bilinear(x, y)
1232 }
1233
1234 pub fn sample_biome(&self, nx: f32, ny: f32) -> crate::terrain::biome::BiomeType {
1236 let x = (nx * (self.resolution - 1) as f32) as usize;
1237 let y = (ny * (self.resolution - 1) as f32) as usize;
1238 let idx = y.min(self.resolution - 1) * self.resolution + x.min(self.resolution - 1);
1239 crate::terrain::biome::biome_from_index(self.biome_overview[idx] as usize)
1240 }
1241
1242 pub fn chunk_to_normalized(&self, coord: ChunkCoord) -> (f32, f32) {
1244 (
1245 (coord.0 as f32 + 0.5) / self.world_size_chunks as f32,
1246 (coord.1 as f32 + 0.5) / self.world_size_chunks as f32,
1247 )
1248 }
1249}
1250
1251#[derive(Clone, Debug)]
1255pub struct ChunkDiff {
1256 pub coord: ChunkCoord,
1257 pub changes: Vec<(usize, usize, f32, f32)>,
1259}
1260
1261impl ChunkDiff {
1262 pub fn new(coord: ChunkCoord) -> Self {
1263 Self { coord, changes: Vec::new() }
1264 }
1265
1266 pub fn record(&mut self, x: usize, y: usize, old_h: f32, new_h: f32) {
1268 if (old_h - new_h).abs() > 1e-6 {
1269 self.changes.push((x, y, old_h, new_h));
1270 }
1271 }
1272
1273 pub fn apply(&self, hm: &mut crate::terrain::heightmap::HeightMap) {
1275 for &(x, y, _, new_h) in &self.changes {
1276 hm.set(x, y, new_h);
1277 }
1278 }
1279
1280 pub fn undo(&self, hm: &mut crate::terrain::heightmap::HeightMap) {
1282 for &(x, y, old_h, _) in &self.changes {
1283 hm.set(x, y, old_h);
1284 }
1285 }
1286
1287 pub fn is_empty(&self) -> bool { self.changes.is_empty() }
1288 pub fn len(&self) -> usize { self.changes.len() }
1289}
1290
1291#[derive(Debug, Default, Clone)]
1295pub struct StreamingProfiler {
1296 pub frame_count: u64,
1297 pub total_generate_ms: f64,
1298 pub total_serialize_ms: f64,
1299 pub total_deserialize_ms: f64,
1300 pub max_generate_ms: f64,
1301 pub min_generate_ms: f64,
1302 pub chunks_per_second: f32,
1303 pub last_frame_ms: f64,
1304}
1305
1306impl StreamingProfiler {
1307 pub fn new() -> Self {
1308 Self { min_generate_ms: f64::INFINITY, ..Default::default() }
1309 }
1310
1311 pub fn record_generate(&mut self, ms: f64) {
1312 self.total_generate_ms += ms;
1313 self.frame_count += 1;
1314 if ms > self.max_generate_ms { self.max_generate_ms = ms; }
1315 if ms < self.min_generate_ms { self.min_generate_ms = ms; }
1316 }
1317
1318 pub fn average_generate_ms(&self) -> f64 {
1319 if self.frame_count == 0 { 0.0 } else { self.total_generate_ms / self.frame_count as f64 }
1320 }
1321
1322 pub fn reset(&mut self) { *self = Self::new(); }
1323}
1324
1325#[derive(Clone, Debug)]
1330pub struct PriorityZone {
1331 pub center: Vec3,
1333 pub radius: f32,
1335 pub priority_bonus: i64,
1337 pub name: String,
1339}
1340
1341impl PriorityZone {
1342 pub fn new(center: Vec3, radius: f32, priority_bonus: i64) -> Self {
1343 Self { center, radius, priority_bonus, name: String::new() }
1344 }
1345
1346 pub fn named(mut self, name: &str) -> Self {
1347 self.name = name.to_string();
1348 self
1349 }
1350
1351 pub fn contains_world_pos(&self, pos: Vec3) -> bool {
1352 let dx = pos.x - self.center.x;
1353 let dz = pos.z - self.center.z;
1354 dx * dx + dz * dz <= self.radius * self.radius
1355 }
1356
1357 pub fn chunk_priority(&self, coord: ChunkCoord, chunk_size: f32) -> i64 {
1358 let world_pos = coord.to_world_pos(chunk_size);
1359 if self.contains_world_pos(world_pos) {
1360 self.priority_bonus
1361 } else {
1362 let dx = world_pos.x - self.center.x;
1363 let dz = world_pos.z - self.center.z;
1364 let dist = (dx * dx + dz * dz).sqrt();
1365 let falloff = (1.0 - (dist / self.radius).min(2.0)) * 0.5;
1366 (self.priority_bonus as f32 * falloff.max(0.0)) as i64
1367 }
1368 }
1369}
1370
1371#[derive(Debug, Default)]
1373pub struct PriorityZoneManager {
1374 pub zones: Vec<PriorityZone>,
1375}
1376
1377impl PriorityZoneManager {
1378 pub fn new() -> Self { Self::default() }
1379
1380 pub fn add_zone(&mut self, zone: PriorityZone) {
1381 self.zones.push(zone);
1382 }
1383
1384 pub fn remove_zone_by_name(&mut self, name: &str) {
1385 self.zones.retain(|z| z.name != name);
1386 }
1387
1388 pub fn total_priority_bonus(&self, coord: ChunkCoord, chunk_size: f32) -> i64 {
1389 self.zones.iter().map(|z| z.chunk_priority(coord, chunk_size)).sum()
1390 }
1391}
1392
1393#[derive(Debug)]
1397pub struct ChunkHitlist {
1398 required: std::collections::HashSet<ChunkCoord>,
1399 loaded: std::collections::HashSet<ChunkCoord>,
1400}
1401
1402impl ChunkHitlist {
1403 pub fn new() -> Self {
1404 Self {
1405 required: std::collections::HashSet::new(),
1406 loaded: std::collections::HashSet::new(),
1407 }
1408 }
1409
1410 pub fn require(&mut self, coord: ChunkCoord) {
1411 self.required.insert(coord);
1412 }
1413
1414 pub fn mark_loaded(&mut self, coord: ChunkCoord) {
1415 if self.required.contains(&coord) {
1416 self.loaded.insert(coord);
1417 }
1418 }
1419
1420 pub fn is_complete(&self) -> bool {
1421 self.required.iter().all(|c| self.loaded.contains(c))
1422 }
1423
1424 pub fn completion_fraction(&self) -> f32 {
1425 if self.required.is_empty() { return 1.0; }
1426 self.loaded.len() as f32 / self.required.len() as f32
1427 }
1428
1429 pub fn pending_coords(&self) -> Vec<ChunkCoord> {
1430 self.required.iter().filter(|c| !self.loaded.contains(*c)).copied().collect()
1431 }
1432}
1433
1434impl Default for ChunkHitlist {
1435 fn default() -> Self { Self::new() }
1436}
1437
1438#[derive(Clone, Debug)]
1442pub struct LodBias {
1443 pub quality_multiplier: f32,
1445 pub center: Vec3,
1447 pub radius: f32,
1449}
1450
1451impl LodBias {
1452 pub fn new(center: Vec3, radius: f32, quality_multiplier: f32) -> Self {
1453 Self { quality_multiplier, center, radius }
1454 }
1455
1456 pub fn lod_multiplier_at(&self, pos: Vec3) -> f32 {
1457 let dx = pos.x - self.center.x;
1458 let dz = pos.z - self.center.z;
1459 let dist = (dx * dx + dz * dz).sqrt();
1460 if dist < self.radius {
1461 let t = 1.0 - dist / self.radius;
1462 1.0 + (self.quality_multiplier - 1.0) * t
1463 } else {
1464 1.0
1465 }
1466 }
1467}
1468
1469#[cfg(test)]
1472mod extended_streaming_tests {
1473 use super::*;
1474
1475 fn test_config() -> TerrainConfig {
1476 TerrainConfig { chunk_size: 16, view_distance: 2, lod_levels: 3, seed: 42 }
1477 }
1478
1479 #[test]
1480 fn test_rle_roundtrip() {
1481 let data: Vec<u8> = vec![1, 1, 1, 2, 3, 3, 4, 4, 4, 4];
1482 let encoded = ExtendedChunkSerializer::rle_encode(&data);
1483 let decoded = ExtendedChunkSerializer::rle_decode(&encoded).unwrap();
1484 assert_eq!(data, decoded);
1485 }
1486
1487 #[test]
1488 fn test_delta_encode_roundtrip() {
1489 let data: Vec<u8> = vec![10, 12, 11, 15, 14, 20, 18];
1490 let enc = ExtendedChunkSerializer::delta_encode_bytes(&data);
1491 let dec = ExtendedChunkSerializer::delta_decode_bytes(&enc).unwrap();
1492 assert_eq!(data, dec);
1493 }
1494
1495 #[test]
1496 fn test_chunk_event_queue() {
1497 let mut q = ChunkEventQueue::new(100);
1498 q.push(ChunkEvent::Loaded(ChunkCoord(0, 0)));
1499 q.push(ChunkEvent::Unloaded(ChunkCoord(1, 0)));
1500 assert_eq!(q.len(), 2);
1501 let events = q.drain();
1502 assert_eq!(events.len(), 2);
1503 assert!(q.is_empty());
1504 }
1505
1506 #[test]
1507 fn test_memory_budget() {
1508 let mut budget = MemoryBudget::new(1024 * 1024);
1509 assert!(budget.allocate(100_000));
1510 budget.free(100_000);
1511 assert_eq!(budget.current_bytes, 0);
1512 assert!(!budget.is_over_budget());
1513 }
1514
1515 #[test]
1516 fn test_memory_budget_over() {
1517 let mut budget = MemoryBudget::new(1000);
1518 assert!(!budget.allocate(1000)); }
1520
1521 #[test]
1522 fn test_world_map_generation() {
1523 let config = test_config();
1524 let wm = WorldMap::generate(64, 32, &config);
1525 assert_eq!(wm.height_overview.data.len(), 32 * 32);
1526 assert_eq!(wm.biome_overview.len(), 32 * 32);
1527 }
1528
1529 #[test]
1530 fn test_world_map_sample() {
1531 let config = test_config();
1532 let wm = WorldMap::generate(64, 32, &config);
1533 let h = wm.sample_height(0.5, 0.5);
1534 assert!(h >= 0.0 && h <= 1.0);
1535 }
1536
1537 #[test]
1538 fn test_chunk_diff_apply_undo() {
1539 let config = test_config();
1540 let gen = ChunkGenerator::new(config);
1541 let mut chunk = gen.generate(ChunkCoord(0, 0));
1542 let old_h = chunk.heightmap.get(5, 5);
1543 let new_h = 0.9f32;
1544 let mut diff = ChunkDiff::new(ChunkCoord(0, 0));
1545 diff.record(5, 5, old_h, new_h);
1546 diff.apply(&mut chunk.heightmap);
1547 assert!((chunk.heightmap.get(5, 5) - new_h).abs() < 1e-6);
1548 diff.undo(&mut chunk.heightmap);
1549 assert!((chunk.heightmap.get(5, 5) - old_h).abs() < 1e-6);
1550 }
1551
1552 #[test]
1553 fn test_priority_zone() {
1554 let zone = PriorityZone::new(Vec3::new(0.0, 0.0, 0.0), 100.0, 500_000);
1555 assert!(zone.contains_world_pos(Vec3::new(50.0, 0.0, 50.0)));
1556 assert!(!zone.contains_world_pos(Vec3::new(200.0, 0.0, 200.0)));
1557 let bonus = zone.chunk_priority(ChunkCoord(0, 0), 16.0);
1558 assert!(bonus > 0);
1559 }
1560
1561 #[test]
1562 fn test_priority_zone_manager() {
1563 let mut mgr = PriorityZoneManager::new();
1564 mgr.add_zone(PriorityZone::new(Vec3::ZERO, 100.0, 100_000).named("start"));
1565 let bonus = mgr.total_priority_bonus(ChunkCoord(0, 0), 16.0);
1566 assert!(bonus > 0);
1567 mgr.remove_zone_by_name("start");
1568 assert!(mgr.zones.is_empty());
1569 }
1570
1571 #[test]
1572 fn test_chunk_hitlist() {
1573 let mut hl = ChunkHitlist::new();
1574 hl.require(ChunkCoord(0, 0));
1575 hl.require(ChunkCoord(1, 0));
1576 assert!(!hl.is_complete());
1577 assert!((hl.completion_fraction() - 0.0).abs() < 1e-5);
1578 hl.mark_loaded(ChunkCoord(0, 0));
1579 assert!((hl.completion_fraction() - 0.5).abs() < 1e-5);
1580 hl.mark_loaded(ChunkCoord(1, 0));
1581 assert!(hl.is_complete());
1582 }
1583
1584 #[test]
1585 fn test_lod_bias() {
1586 let bias = LodBias::new(Vec3::ZERO, 100.0, 2.0);
1587 let at_center = bias.lod_multiplier_at(Vec3::ZERO);
1588 assert!((at_center - 2.0).abs() < 1e-4);
1589 let far_away = bias.lod_multiplier_at(Vec3::new(200.0, 0.0, 0.0));
1590 assert!((far_away - 1.0).abs() < 1e-4);
1591 }
1592
1593 #[test]
1594 fn test_streaming_profiler() {
1595 let mut prof = StreamingProfiler::new();
1596 prof.record_generate(5.0);
1597 prof.record_generate(10.0);
1598 prof.record_generate(3.0);
1599 assert!((prof.average_generate_ms() - 6.0).abs() < 1e-4);
1600 assert!((prof.max_generate_ms - 10.0).abs() < 1e-4);
1601 assert!((prof.min_generate_ms - 3.0).abs() < 1e-4);
1602 }
1603}
1604
1605#[derive(Clone, Debug)]
1609pub struct TerrainPatch {
1610 pub coord: ChunkCoord,
1611 pub offset_x: usize,
1612 pub offset_z: usize,
1613 pub width: usize,
1614 pub height: usize,
1615 pub data: Vec<f32>,
1616 pub dirty: bool,
1617}
1618
1619impl TerrainPatch {
1620 pub fn new(coord: ChunkCoord, offset_x: usize, offset_z: usize, width: usize, height: usize) -> Self {
1621 Self {
1622 coord, offset_x, offset_z, width, height,
1623 data: vec![0.0f32; width * height],
1624 dirty: false,
1625 }
1626 }
1627
1628 pub fn get(&self, x: usize, z: usize) -> f32 {
1629 if x < self.width && z < self.height { self.data[z * self.width + x] } else { 0.0 }
1630 }
1631
1632 pub fn set(&mut self, x: usize, z: usize, v: f32) {
1633 if x < self.width && z < self.height {
1634 self.data[z * self.width + x] = v.clamp(0.0, 1.0);
1635 self.dirty = true;
1636 }
1637 }
1638
1639 pub fn apply_to_chunk(&self, chunk: &mut crate::terrain::mod_types::TerrainChunk) {
1641 for z in 0..self.height {
1642 for x in 0..self.width {
1643 let cx = self.offset_x + x;
1644 let cz = self.offset_z + z;
1645 chunk.heightmap.set(cx, cz, self.get(x, z));
1646 }
1647 }
1648 }
1649
1650 pub fn read_from_chunk(&mut self, chunk: &crate::terrain::mod_types::TerrainChunk) {
1652 for z in 0..self.height {
1653 for x in 0..self.width {
1654 let cx = self.offset_x + x;
1655 let cz = self.offset_z + z;
1656 self.set(x, z, chunk.heightmap.get(cx, cz));
1657 }
1658 }
1659 self.dirty = false;
1660 }
1661}
1662
1663pub struct ChunkStitcher;
1667
1668impl ChunkStitcher {
1669 pub fn stitch_edge(
1673 primary: &mut crate::terrain::mod_types::TerrainChunk,
1674 neighbor: &crate::terrain::mod_types::TerrainChunk,
1675 edge: StitchEdge,
1676 blend_width: usize,
1677 ) {
1678 let pw = primary.heightmap.width;
1679 let ph = primary.heightmap.height;
1680 let nw = neighbor.heightmap.width;
1681 let nh = neighbor.heightmap.height;
1682
1683 match edge {
1684 StitchEdge::East => {
1685 for z in 0..ph {
1686 let nz = (z as f32 / ph as f32 * nh as f32) as usize;
1687 for bx in 0..blend_width {
1688 let px = pw - 1 - bx;
1689 let nx = bx;
1690 let t = bx as f32 / blend_width as f32;
1691 let p_val = primary.heightmap.get(px, z);
1692 let n_val = neighbor.heightmap.get(nx, nz.min(nh - 1));
1693 let blended = p_val + (n_val - p_val) * t;
1694 primary.heightmap.set(px, z, blended);
1695 }
1696 }
1697 }
1698 StitchEdge::West => {
1699 for z in 0..ph {
1700 let nz = (z as f32 / ph as f32 * nh as f32) as usize;
1701 for bx in 0..blend_width {
1702 let px = bx;
1703 let nx = nw - 1 - bx;
1704 let t = bx as f32 / blend_width as f32;
1705 let p_val = primary.heightmap.get(px, z);
1706 let n_val = neighbor.heightmap.get(nx.min(nw - 1), nz.min(nh - 1));
1707 let blended = n_val + (p_val - n_val) * (1.0 - t);
1708 primary.heightmap.set(px, z, blended);
1709 }
1710 }
1711 }
1712 StitchEdge::North => {
1713 for x in 0..pw {
1714 let nx = (x as f32 / pw as f32 * nw as f32) as usize;
1715 for bz in 0..blend_width {
1716 let pz = bz;
1717 let nz = nh - 1 - bz;
1718 let t = bz as f32 / blend_width as f32;
1719 let p_val = primary.heightmap.get(x, pz);
1720 let n_val = neighbor.heightmap.get(nx.min(nw - 1), nz.min(nh - 1));
1721 let blended = n_val + (p_val - n_val) * (1.0 - t);
1722 primary.heightmap.set(x, pz, blended);
1723 }
1724 }
1725 }
1726 StitchEdge::South => {
1727 for x in 0..pw {
1728 let nx = (x as f32 / pw as f32 * nw as f32) as usize;
1729 for bz in 0..blend_width {
1730 let pz = ph - 1 - bz;
1731 let nz = bz;
1732 let t = bz as f32 / blend_width as f32;
1733 let p_val = primary.heightmap.get(x, pz);
1734 let n_val = neighbor.heightmap.get(nx.min(nw - 1), nz.min(nh - 1));
1735 let blended = p_val + (n_val - p_val) * t;
1736 primary.heightmap.set(x, pz, blended);
1737 }
1738 }
1739 }
1740 }
1741 }
1742}
1743
1744#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1746pub enum StitchEdge {
1747 North,
1748 South,
1749 East,
1750 West,
1751}
1752
1753pub struct HeightmapQuantizer;
1757
1758impl HeightmapQuantizer {
1759 pub fn quantize_8bit(heights: &[f32]) -> Vec<u8> {
1761 heights.iter().map(|&h| (h.clamp(0.0, 1.0) * 255.0) as u8).collect()
1762 }
1763
1764 pub fn dequantize_8bit(bytes: &[u8]) -> Vec<f32> {
1766 bytes.iter().map(|&b| b as f32 / 255.0).collect()
1767 }
1768
1769 pub fn quantize_16bit(heights: &[f32]) -> Vec<u16> {
1771 heights.iter().map(|&h| (h.clamp(0.0, 1.0) * 65535.0) as u16).collect()
1772 }
1773
1774 pub fn dequantize_16bit(shorts: &[u16]) -> Vec<f32> {
1776 shorts.iter().map(|&s| s as f32 / 65535.0).collect()
1777 }
1778
1779 pub fn quantization_error(original: &[f32], quantized_8bit: &[u8]) -> f32 {
1781 if original.len() != quantized_8bit.len() { return f32::INFINITY; }
1782 original.iter().zip(quantized_8bit.iter())
1783 .map(|(&orig, &q)| (orig - q as f32 / 255.0).abs())
1784 .sum::<f32>() / original.len() as f32
1785 }
1786}
1787
1788#[derive(Debug, Default, Clone)]
1792pub struct StreamingTelemetry {
1793 pub frame_number: u64,
1794 pub visible_chunk_count: usize,
1795 pub loaded_chunk_count: usize,
1796 pub pending_chunk_count: usize,
1797 pub cache_hit_rate: f32,
1798 pub memory_usage_mb: f32,
1799 pub generation_rate: f32, pub eviction_count: usize,
1801 pub last_update_ms: f32,
1802}
1803
1804impl StreamingTelemetry {
1805 pub fn update_from_stats(&mut self, stats: &StreamingStats) {
1806 self.frame_number += 1;
1807 self.loaded_chunk_count = stats.chunks_loaded;
1808 self.pending_chunk_count = stats.pending_count;
1809 self.cache_hit_rate = stats.cache_hit_rate();
1810 self.memory_usage_mb = stats.memory_bytes as f32 / (1024.0 * 1024.0);
1811 }
1812
1813 pub fn to_display_string(&self) -> String {
1814 format!(
1815 "Frame:{} | Chunks:{} Pending:{} | Cache:{:.0}% | Mem:{:.1}MB",
1816 self.frame_number,
1817 self.loaded_chunk_count,
1818 self.pending_chunk_count,
1819 self.cache_hit_rate * 100.0,
1820 self.memory_usage_mb,
1821 )
1822 }
1823}
1824
1825pub struct ChunkRepair;
1829
1830impl ChunkRepair {
1831 pub fn clamp_heights(chunk: &mut crate::terrain::mod_types::TerrainChunk) {
1833 for v in chunk.heightmap.data.iter_mut() {
1834 *v = v.clamp(0.0, 1.0);
1835 }
1836 }
1837
1838 pub fn fix_nans(chunk: &mut crate::terrain::mod_types::TerrainChunk) {
1840 let w = chunk.heightmap.width;
1841 let h = chunk.heightmap.height;
1842 let data = chunk.heightmap.data.clone();
1843 for y in 0..h {
1844 for x in 0..w {
1845 if chunk.heightmap.get(x, y).is_nan() {
1846 let mut sum = 0.0f32;
1847 let mut count = 0;
1848 for (dx, dy) in &[(-1i32,0),(1,0),(0,-1i32),(0,1)] {
1849 let nx = x as i32 + dx;
1850 let ny = y as i32 + dy;
1851 if nx >= 0 && nx < w as i32 && ny >= 0 && ny < h as i32 {
1852 let v = data[ny as usize * w + nx as usize];
1853 if !v.is_nan() { sum += v; count += 1; }
1854 }
1855 }
1856 let replacement = if count > 0 { sum / count as f32 } else { 0.5 };
1857 chunk.heightmap.set(x, y, replacement);
1858 }
1859 }
1860 }
1861 }
1862
1863 pub fn is_valid(chunk: &crate::terrain::mod_types::TerrainChunk) -> bool {
1865 chunk.heightmap.data.iter().all(|&v| !v.is_nan() && v >= 0.0 && v <= 1.0)
1866 }
1867}
1868
1869#[cfg(test)]
1872mod more_streaming_tests {
1873 use super::*;
1874
1875 fn test_config() -> TerrainConfig {
1876 TerrainConfig { chunk_size: 16, view_distance: 1, lod_levels: 2, seed: 42 }
1877 }
1878
1879 #[test]
1880 fn test_terrain_patch_apply() {
1881 let config = test_config();
1882 let gen = ChunkGenerator::new(config);
1883 let mut chunk = gen.generate(ChunkCoord(0, 0));
1884 let mut patch = TerrainPatch::new(ChunkCoord(0, 0), 0, 0, 4, 4);
1885 for v in patch.data.iter_mut() { *v = 0.99; }
1886 patch.dirty = true;
1887 patch.apply_to_chunk(&mut chunk);
1888 assert!((chunk.heightmap.get(0, 0) - 0.99).abs() < 1e-5);
1889 }
1890
1891 #[test]
1892 fn test_terrain_patch_read_write() {
1893 let config = test_config();
1894 let gen = ChunkGenerator::new(config);
1895 let chunk = gen.generate(ChunkCoord(0, 0));
1896 let mut patch = TerrainPatch::new(ChunkCoord(0, 0), 0, 0, 4, 4);
1897 patch.read_from_chunk(&chunk);
1898 assert!(!patch.dirty);
1899 assert!(patch.data.iter().any(|&v| v > 0.0));
1900 }
1901
1902 #[test]
1903 fn test_heightmap_quantizer_8bit() {
1904 let heights: Vec<f32> = (0..256).map(|i| i as f32 / 255.0).collect();
1905 let quantized = HeightmapQuantizer::quantize_8bit(&heights);
1906 let deq = HeightmapQuantizer::dequantize_8bit(&quantized);
1907 let err = HeightmapQuantizer::quantization_error(&heights, &quantized);
1908 assert!(err < 0.005, "8-bit quantization error should be small");
1909 }
1910
1911 #[test]
1912 fn test_heightmap_quantizer_16bit() {
1913 let heights: Vec<f32> = (0..1024).map(|i| i as f32 / 1023.0).collect();
1914 let quantized = HeightmapQuantizer::quantize_16bit(&heights);
1915 let deq = HeightmapQuantizer::dequantize_16bit(&quantized);
1916 let max_err = heights.iter().zip(deq.iter())
1917 .map(|(&a, &b)| (a - b).abs())
1918 .fold(0.0f32, f32::max);
1919 assert!(max_err < 0.0001, "16-bit quantization should be very accurate");
1920 }
1921
1922 #[test]
1923 fn test_chunk_repair_clamp() {
1924 let config = test_config();
1925 let gen = ChunkGenerator::new(config);
1926 let mut chunk = gen.generate(ChunkCoord(0, 0));
1927 chunk.heightmap.data[0] = 2.0; chunk.heightmap.data[1] = -1.0; ChunkRepair::clamp_heights(&mut chunk);
1930 assert!(ChunkRepair::is_valid(&chunk));
1931 }
1932
1933 #[test]
1934 fn test_chunk_repair_nan() {
1935 let config = test_config();
1936 let gen = ChunkGenerator::new(config);
1937 let mut chunk = gen.generate(ChunkCoord(0, 0));
1938 chunk.heightmap.data[16] = f32::NAN;
1939 ChunkRepair::fix_nans(&mut chunk);
1940 assert!(ChunkRepair::is_valid(&chunk));
1941 }
1942
1943 #[test]
1944 fn test_streaming_telemetry() {
1945 let stats = StreamingStats {
1946 chunks_loaded: 10, chunks_unloaded: 2,
1947 cache_hits: 80, cache_misses: 20,
1948 pending_count: 3, memory_bytes: 2 * 1024 * 1024,
1949 generate_time_ms: 100.0,
1950 };
1951 let mut tel = StreamingTelemetry::default();
1952 tel.update_from_stats(&stats);
1953 assert_eq!(tel.loaded_chunk_count, 10);
1954 assert!((tel.cache_hit_rate - 0.8).abs() < 1e-4);
1955 assert!((tel.memory_usage_mb - 2.0).abs() < 1e-4);
1956 let s = tel.to_display_string();
1957 assert!(s.contains("Chunks:10"));
1958 }
1959
1960 #[test]
1961 fn test_extended_serializer_rle_chunk() {
1962 let config = test_config();
1963 let gen = ChunkGenerator::new(config);
1964 let chunk = gen.generate(ChunkCoord(0, 0));
1965 let bytes = ExtendedChunkSerializer::serialize_with_format(&chunk, SerializationFormat::Raw);
1966 let restored = ExtendedChunkSerializer::deserialize_with_format(
1967 &bytes, SerializationFormat::Raw
1968 );
1969 assert!(restored.is_some());
1970 }
1971}