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);
990 }
991
992 #[test]
993 fn test_streaming_manager_force_load() {
994 let config = test_config();
995 let mut mgr = StreamingManager::new_synchronous(config);
996 let chunk = mgr.force_load(ChunkCoord(0, 0));
997 assert_eq!(chunk.coord, ChunkCoord(0, 0));
998 assert_eq!(mgr.cache_size(), 1);
999 }
1000}
1001
1002#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1006pub enum SerializationFormat {
1007 Raw,
1009 RLE,
1011 Delta,
1013}
1014
1015pub struct ExtendedChunkSerializer;
1017
1018impl ExtendedChunkSerializer {
1019 pub fn serialize_with_format(
1021 chunk: &crate::terrain::mod_types::TerrainChunk,
1022 format: SerializationFormat,
1023 ) -> Vec<u8> {
1024 let base = ChunkSerializer::serialize(chunk);
1025 match format {
1026 SerializationFormat::Raw => base,
1027 SerializationFormat::RLE => Self::rle_encode(&base),
1028 SerializationFormat::Delta => Self::delta_encode_bytes(&base),
1029 }
1030 }
1031
1032 pub fn deserialize_with_format(
1034 bytes: &[u8],
1035 format: SerializationFormat,
1036 ) -> Option<crate::terrain::mod_types::TerrainChunk> {
1037 let decoded = match format {
1038 SerializationFormat::Raw => bytes.to_vec(),
1039 SerializationFormat::RLE => Self::rle_decode(bytes)?,
1040 SerializationFormat::Delta => Self::delta_decode_bytes(bytes)?,
1041 };
1042 ChunkSerializer::deserialize(&decoded)
1043 }
1044
1045 pub fn rle_encode(data: &[u8]) -> Vec<u8> {
1047 if data.is_empty() { return Vec::new(); }
1048 let mut out = Vec::new();
1049 let mut i = 0;
1050 while i < data.len() {
1051 let val = data[i];
1052 let mut run = 1usize;
1053 while i + run < data.len() && data[i + run] == val && run < 255 {
1054 run += 1;
1055 }
1056 out.push(run as u8);
1057 out.push(val);
1058 i += run;
1059 }
1060 out
1061 }
1062
1063 pub fn rle_decode(data: &[u8]) -> Option<Vec<u8>> {
1065 if data.len() % 2 != 0 { return None; }
1066 let mut out = Vec::new();
1067 let mut i = 0;
1068 while i + 1 < data.len() {
1069 let count = data[i] as usize;
1070 let val = data[i + 1];
1071 for _ in 0..count { out.push(val); }
1072 i += 2;
1073 }
1074 Some(out)
1075 }
1076
1077 pub fn delta_encode_bytes(data: &[u8]) -> Vec<u8> {
1079 if data.is_empty() { return Vec::new(); }
1080 let mut out = Vec::with_capacity(data.len());
1081 out.push(data[0]);
1082 for i in 1..data.len() {
1083 out.push(data[i].wrapping_sub(data[i - 1]));
1084 }
1085 out
1086 }
1087
1088 pub fn delta_decode_bytes(data: &[u8]) -> Option<Vec<u8>> {
1090 if data.is_empty() { return Some(Vec::new()); }
1091 let mut out = Vec::with_capacity(data.len());
1092 out.push(data[0]);
1093 for i in 1..data.len() {
1094 out.push(data[i].wrapping_add(*out.last().unwrap()));
1095 }
1096 Some(out)
1097 }
1098}
1099
1100#[derive(Clone, Debug)]
1104pub enum ChunkEvent {
1105 Loaded(ChunkCoord),
1106 Unloaded(ChunkCoord),
1107 LodChanged { coord: ChunkCoord, old_lod: u8, new_lod: u8 },
1108 GenerationStarted(ChunkCoord),
1109 GenerationFailed { coord: ChunkCoord, reason: String },
1110}
1111
1112#[derive(Debug, Default)]
1114pub struct ChunkEventQueue {
1115 events: VecDeque<ChunkEvent>,
1116 max_size: usize,
1117}
1118
1119impl ChunkEventQueue {
1120 pub fn new(max_size: usize) -> Self {
1121 Self { events: VecDeque::new(), max_size }
1122 }
1123
1124 pub fn push(&mut self, event: ChunkEvent) {
1125 if self.events.len() >= self.max_size {
1126 self.events.pop_front();
1127 }
1128 self.events.push_back(event);
1129 }
1130
1131 pub fn drain(&mut self) -> Vec<ChunkEvent> {
1132 self.events.drain(..).collect()
1133 }
1134
1135 pub fn len(&self) -> usize { self.events.len() }
1136 pub fn is_empty(&self) -> bool { self.events.is_empty() }
1137}
1138
1139#[derive(Debug, Clone)]
1143pub struct MemoryBudget {
1144 pub max_bytes: usize,
1146 pub current_bytes: usize,
1148 pub headroom_bytes: usize,
1150 pub peak_bytes: usize,
1152}
1153
1154impl MemoryBudget {
1155 pub fn new(max_bytes: usize) -> Self {
1156 Self {
1157 max_bytes,
1158 current_bytes: 0,
1159 headroom_bytes: max_bytes / 10,
1160 peak_bytes: 0,
1161 }
1162 }
1163
1164 pub fn allocate(&mut self, bytes: usize) -> bool {
1165 if self.current_bytes + bytes + self.headroom_bytes > self.max_bytes {
1166 return false;
1167 }
1168 self.current_bytes += bytes;
1169 if self.current_bytes > self.peak_bytes {
1170 self.peak_bytes = self.current_bytes;
1171 }
1172 true
1173 }
1174
1175 pub fn free(&mut self, bytes: usize) {
1176 self.current_bytes = self.current_bytes.saturating_sub(bytes);
1177 }
1178
1179 pub fn utilization(&self) -> f32 {
1180 self.current_bytes as f32 / self.max_bytes as f32
1181 }
1182
1183 pub fn available(&self) -> usize {
1184 self.max_bytes.saturating_sub(self.current_bytes + self.headroom_bytes)
1185 }
1186
1187 pub fn is_over_budget(&self) -> bool {
1188 self.current_bytes + self.headroom_bytes > self.max_bytes
1189 }
1190}
1191
1192#[derive(Debug)]
1196pub struct WorldMap {
1197 pub height_overview: crate::terrain::heightmap::HeightMap,
1199 pub biome_overview: Vec<u8>,
1201 pub resolution: usize,
1203 pub world_size_chunks: usize,
1205}
1206
1207impl WorldMap {
1208 pub fn generate(world_size_chunks: usize, resolution: usize, config: &TerrainConfig) -> Self {
1210 let scale = world_size_chunks as f32 / resolution as f32;
1211 let height_overview = crate::terrain::heightmap::FractalNoise::generate(
1212 resolution, resolution, 6, 2.0, 0.5, 2.0, config.seed,
1213 );
1214 let biome_overview: Vec<u8> = height_overview.data.iter()
1215 .map(|&h| {
1216 let biome = if h < 0.1 { crate::terrain::biome::BiomeType::Ocean }
1217 else if h < 0.2 { crate::terrain::biome::BiomeType::Beach }
1218 else if h < 0.5 { crate::terrain::biome::BiomeType::Grassland }
1219 else if h < 0.7 { crate::terrain::biome::BiomeType::TemperateForest }
1220 else if h < 0.85 { crate::terrain::biome::BiomeType::Mountain }
1221 else { crate::terrain::biome::BiomeType::AlpineGlacier };
1222 biome as u8
1223 })
1224 .collect();
1225 Self { height_overview, biome_overview, resolution, world_size_chunks }
1226 }
1227
1228 pub fn sample_height(&self, nx: f32, ny: f32) -> f32 {
1230 let x = (nx * (self.resolution - 1) as f32).clamp(0.0, (self.resolution - 1) as f32);
1231 let y = (ny * (self.resolution - 1) as f32).clamp(0.0, (self.resolution - 1) as f32);
1232 self.height_overview.sample_bilinear(x, y)
1233 }
1234
1235 pub fn sample_biome(&self, nx: f32, ny: f32) -> crate::terrain::biome::BiomeType {
1237 let x = (nx * (self.resolution - 1) as f32) as usize;
1238 let y = (ny * (self.resolution - 1) as f32) as usize;
1239 let idx = y.min(self.resolution - 1) * self.resolution + x.min(self.resolution - 1);
1240 crate::terrain::biome::biome_from_index(self.biome_overview[idx] as usize)
1241 }
1242
1243 pub fn chunk_to_normalized(&self, coord: ChunkCoord) -> (f32, f32) {
1245 (
1246 (coord.0 as f32 + 0.5) / self.world_size_chunks as f32,
1247 (coord.1 as f32 + 0.5) / self.world_size_chunks as f32,
1248 )
1249 }
1250}
1251
1252#[derive(Clone, Debug)]
1256pub struct ChunkDiff {
1257 pub coord: ChunkCoord,
1258 pub changes: Vec<(usize, usize, f32, f32)>,
1260}
1261
1262impl ChunkDiff {
1263 pub fn new(coord: ChunkCoord) -> Self {
1264 Self { coord, changes: Vec::new() }
1265 }
1266
1267 pub fn record(&mut self, x: usize, y: usize, old_h: f32, new_h: f32) {
1269 if (old_h - new_h).abs() > 1e-6 {
1270 self.changes.push((x, y, old_h, new_h));
1271 }
1272 }
1273
1274 pub fn apply(&self, hm: &mut crate::terrain::heightmap::HeightMap) {
1276 for &(x, y, _, new_h) in &self.changes {
1277 hm.set(x, y, new_h);
1278 }
1279 }
1280
1281 pub fn undo(&self, hm: &mut crate::terrain::heightmap::HeightMap) {
1283 for &(x, y, old_h, _) in &self.changes {
1284 hm.set(x, y, old_h);
1285 }
1286 }
1287
1288 pub fn is_empty(&self) -> bool { self.changes.is_empty() }
1289 pub fn len(&self) -> usize { self.changes.len() }
1290}
1291
1292#[derive(Debug, Default, Clone)]
1296pub struct StreamingProfiler {
1297 pub frame_count: u64,
1298 pub total_generate_ms: f64,
1299 pub total_serialize_ms: f64,
1300 pub total_deserialize_ms: f64,
1301 pub max_generate_ms: f64,
1302 pub min_generate_ms: f64,
1303 pub chunks_per_second: f32,
1304 pub last_frame_ms: f64,
1305}
1306
1307impl StreamingProfiler {
1308 pub fn new() -> Self {
1309 Self { min_generate_ms: f64::INFINITY, ..Default::default() }
1310 }
1311
1312 pub fn record_generate(&mut self, ms: f64) {
1313 self.total_generate_ms += ms;
1314 self.frame_count += 1;
1315 if ms > self.max_generate_ms { self.max_generate_ms = ms; }
1316 if ms < self.min_generate_ms { self.min_generate_ms = ms; }
1317 }
1318
1319 pub fn average_generate_ms(&self) -> f64 {
1320 if self.frame_count == 0 { 0.0 } else { self.total_generate_ms / self.frame_count as f64 }
1321 }
1322
1323 pub fn reset(&mut self) { *self = Self::new(); }
1324}
1325
1326#[derive(Clone, Debug)]
1331pub struct PriorityZone {
1332 pub center: Vec3,
1334 pub radius: f32,
1336 pub priority_bonus: i64,
1338 pub name: String,
1340}
1341
1342impl PriorityZone {
1343 pub fn new(center: Vec3, radius: f32, priority_bonus: i64) -> Self {
1344 Self { center, radius, priority_bonus, name: String::new() }
1345 }
1346
1347 pub fn named(mut self, name: &str) -> Self {
1348 self.name = name.to_string();
1349 self
1350 }
1351
1352 pub fn contains_world_pos(&self, pos: Vec3) -> bool {
1353 let dx = pos.x - self.center.x;
1354 let dz = pos.z - self.center.z;
1355 dx * dx + dz * dz <= self.radius * self.radius
1356 }
1357
1358 pub fn chunk_priority(&self, coord: ChunkCoord, chunk_size: f32) -> i64 {
1359 let world_pos = coord.to_world_pos(chunk_size);
1360 if self.contains_world_pos(world_pos) {
1361 self.priority_bonus
1362 } else {
1363 let dx = world_pos.x - self.center.x;
1364 let dz = world_pos.z - self.center.z;
1365 let dist = (dx * dx + dz * dz).sqrt();
1366 let falloff = (1.0 - (dist / self.radius).min(2.0)) * 0.5;
1367 (self.priority_bonus as f32 * falloff.max(0.0)) as i64
1368 }
1369 }
1370}
1371
1372#[derive(Debug, Default)]
1374pub struct PriorityZoneManager {
1375 pub zones: Vec<PriorityZone>,
1376}
1377
1378impl PriorityZoneManager {
1379 pub fn new() -> Self { Self::default() }
1380
1381 pub fn add_zone(&mut self, zone: PriorityZone) {
1382 self.zones.push(zone);
1383 }
1384
1385 pub fn remove_zone_by_name(&mut self, name: &str) {
1386 self.zones.retain(|z| z.name != name);
1387 }
1388
1389 pub fn total_priority_bonus(&self, coord: ChunkCoord, chunk_size: f32) -> i64 {
1390 self.zones.iter().map(|z| z.chunk_priority(coord, chunk_size)).sum()
1391 }
1392}
1393
1394#[derive(Debug)]
1398pub struct ChunkHitlist {
1399 required: std::collections::HashSet<ChunkCoord>,
1400 loaded: std::collections::HashSet<ChunkCoord>,
1401}
1402
1403impl ChunkHitlist {
1404 pub fn new() -> Self {
1405 Self {
1406 required: std::collections::HashSet::new(),
1407 loaded: std::collections::HashSet::new(),
1408 }
1409 }
1410
1411 pub fn require(&mut self, coord: ChunkCoord) {
1412 self.required.insert(coord);
1413 }
1414
1415 pub fn mark_loaded(&mut self, coord: ChunkCoord) {
1416 if self.required.contains(&coord) {
1417 self.loaded.insert(coord);
1418 }
1419 }
1420
1421 pub fn is_complete(&self) -> bool {
1422 self.required.iter().all(|c| self.loaded.contains(c))
1423 }
1424
1425 pub fn completion_fraction(&self) -> f32 {
1426 if self.required.is_empty() { return 1.0; }
1427 self.loaded.len() as f32 / self.required.len() as f32
1428 }
1429
1430 pub fn pending_coords(&self) -> Vec<ChunkCoord> {
1431 self.required.iter().filter(|c| !self.loaded.contains(*c)).copied().collect()
1432 }
1433}
1434
1435impl Default for ChunkHitlist {
1436 fn default() -> Self { Self::new() }
1437}
1438
1439#[derive(Clone, Debug)]
1443pub struct LodBias {
1444 pub quality_multiplier: f32,
1446 pub center: Vec3,
1448 pub radius: f32,
1450}
1451
1452impl LodBias {
1453 pub fn new(center: Vec3, radius: f32, quality_multiplier: f32) -> Self {
1454 Self { quality_multiplier, center, radius }
1455 }
1456
1457 pub fn lod_multiplier_at(&self, pos: Vec3) -> f32 {
1458 let dx = pos.x - self.center.x;
1459 let dz = pos.z - self.center.z;
1460 let dist = (dx * dx + dz * dz).sqrt();
1461 if dist < self.radius {
1462 let t = 1.0 - dist / self.radius;
1463 1.0 + (self.quality_multiplier - 1.0) * t
1464 } else {
1465 1.0
1466 }
1467 }
1468}
1469
1470#[cfg(test)]
1473mod extended_streaming_tests {
1474 use super::*;
1475
1476 fn test_config() -> TerrainConfig {
1477 TerrainConfig { chunk_size: 16, view_distance: 2, lod_levels: 3, seed: 42 }
1478 }
1479
1480 #[test]
1481 fn test_rle_roundtrip() {
1482 let data: Vec<u8> = vec![1, 1, 1, 2, 3, 3, 4, 4, 4, 4];
1483 let encoded = ExtendedChunkSerializer::rle_encode(&data);
1484 let decoded = ExtendedChunkSerializer::rle_decode(&encoded).unwrap();
1485 assert_eq!(data, decoded);
1486 }
1487
1488 #[test]
1489 fn test_delta_encode_roundtrip() {
1490 let data: Vec<u8> = vec![10, 12, 11, 15, 14, 20, 18];
1491 let enc = ExtendedChunkSerializer::delta_encode_bytes(&data);
1492 let dec = ExtendedChunkSerializer::delta_decode_bytes(&enc).unwrap();
1493 assert_eq!(data, dec);
1494 }
1495
1496 #[test]
1497 fn test_chunk_event_queue() {
1498 let mut q = ChunkEventQueue::new(100);
1499 q.push(ChunkEvent::Loaded(ChunkCoord(0, 0)));
1500 q.push(ChunkEvent::Unloaded(ChunkCoord(1, 0)));
1501 assert_eq!(q.len(), 2);
1502 let events = q.drain();
1503 assert_eq!(events.len(), 2);
1504 assert!(q.is_empty());
1505 }
1506
1507 #[test]
1508 fn test_memory_budget() {
1509 let mut budget = MemoryBudget::new(1024 * 1024);
1510 assert!(budget.allocate(100_000));
1511 budget.free(100_000);
1512 assert_eq!(budget.current_bytes, 0);
1513 assert!(!budget.is_over_budget());
1514 }
1515
1516 #[test]
1517 fn test_memory_budget_over() {
1518 let mut budget = MemoryBudget::new(1000);
1519 assert!(!budget.allocate(1000)); }
1521
1522 #[test]
1523 fn test_world_map_generation() {
1524 let config = test_config();
1525 let wm = WorldMap::generate(64, 32, &config);
1526 assert_eq!(wm.height_overview.data.len(), 32 * 32);
1527 assert_eq!(wm.biome_overview.len(), 32 * 32);
1528 }
1529
1530 #[test]
1531 fn test_world_map_sample() {
1532 let config = test_config();
1533 let wm = WorldMap::generate(64, 32, &config);
1534 let h = wm.sample_height(0.5, 0.5);
1535 assert!(h >= 0.0 && h <= 1.0);
1536 }
1537
1538 #[test]
1539 fn test_chunk_diff_apply_undo() {
1540 let config = test_config();
1541 let gen = ChunkGenerator::new(config);
1542 let mut chunk = gen.generate(ChunkCoord(0, 0));
1543 let old_h = chunk.heightmap.get(5, 5);
1544 let new_h = 0.9f32;
1545 let mut diff = ChunkDiff::new(ChunkCoord(0, 0));
1546 diff.record(5, 5, old_h, new_h);
1547 diff.apply(&mut chunk.heightmap);
1548 assert!((chunk.heightmap.get(5, 5) - new_h).abs() < 1e-6);
1549 diff.undo(&mut chunk.heightmap);
1550 assert!((chunk.heightmap.get(5, 5) - old_h).abs() < 1e-6);
1551 }
1552
1553 #[test]
1554 fn test_priority_zone() {
1555 let zone = PriorityZone::new(Vec3::new(0.0, 0.0, 0.0), 100.0, 500_000);
1556 assert!(zone.contains_world_pos(Vec3::new(50.0, 0.0, 50.0)));
1557 assert!(!zone.contains_world_pos(Vec3::new(200.0, 0.0, 200.0)));
1558 let bonus = zone.chunk_priority(ChunkCoord(0, 0), 16.0);
1559 assert!(bonus > 0);
1560 }
1561
1562 #[test]
1563 fn test_priority_zone_manager() {
1564 let mut mgr = PriorityZoneManager::new();
1565 mgr.add_zone(PriorityZone::new(Vec3::ZERO, 100.0, 100_000).named("start"));
1566 let bonus = mgr.total_priority_bonus(ChunkCoord(0, 0), 16.0);
1567 assert!(bonus > 0);
1568 mgr.remove_zone_by_name("start");
1569 assert!(mgr.zones.is_empty());
1570 }
1571
1572 #[test]
1573 fn test_chunk_hitlist() {
1574 let mut hl = ChunkHitlist::new();
1575 hl.require(ChunkCoord(0, 0));
1576 hl.require(ChunkCoord(1, 0));
1577 assert!(!hl.is_complete());
1578 assert!((hl.completion_fraction() - 0.0).abs() < 1e-5);
1579 hl.mark_loaded(ChunkCoord(0, 0));
1580 assert!((hl.completion_fraction() - 0.5).abs() < 1e-5);
1581 hl.mark_loaded(ChunkCoord(1, 0));
1582 assert!(hl.is_complete());
1583 }
1584
1585 #[test]
1586 fn test_lod_bias() {
1587 let bias = LodBias::new(Vec3::ZERO, 100.0, 2.0);
1588 let at_center = bias.lod_multiplier_at(Vec3::ZERO);
1589 assert!((at_center - 2.0).abs() < 1e-4);
1590 let far_away = bias.lod_multiplier_at(Vec3::new(200.0, 0.0, 0.0));
1591 assert!((far_away - 1.0).abs() < 1e-4);
1592 }
1593
1594 #[test]
1595 fn test_streaming_profiler() {
1596 let mut prof = StreamingProfiler::new();
1597 prof.record_generate(5.0);
1598 prof.record_generate(10.0);
1599 prof.record_generate(3.0);
1600 assert!((prof.average_generate_ms() - 6.0).abs() < 1e-4);
1601 assert!((prof.max_generate_ms - 10.0).abs() < 1e-4);
1602 assert!((prof.min_generate_ms - 3.0).abs() < 1e-4);
1603 }
1604}
1605
1606#[derive(Clone, Debug)]
1610pub struct TerrainPatch {
1611 pub coord: ChunkCoord,
1612 pub offset_x: usize,
1613 pub offset_z: usize,
1614 pub width: usize,
1615 pub height: usize,
1616 pub data: Vec<f32>,
1617 pub dirty: bool,
1618}
1619
1620impl TerrainPatch {
1621 pub fn new(coord: ChunkCoord, offset_x: usize, offset_z: usize, width: usize, height: usize) -> Self {
1622 Self {
1623 coord, offset_x, offset_z, width, height,
1624 data: vec![0.0f32; width * height],
1625 dirty: false,
1626 }
1627 }
1628
1629 pub fn get(&self, x: usize, z: usize) -> f32 {
1630 if x < self.width && z < self.height { self.data[z * self.width + x] } else { 0.0 }
1631 }
1632
1633 pub fn set(&mut self, x: usize, z: usize, v: f32) {
1634 if x < self.width && z < self.height {
1635 self.data[z * self.width + x] = v.clamp(0.0, 1.0);
1636 self.dirty = true;
1637 }
1638 }
1639
1640 pub fn apply_to_chunk(&self, chunk: &mut crate::terrain::mod_types::TerrainChunk) {
1642 for z in 0..self.height {
1643 for x in 0..self.width {
1644 let cx = self.offset_x + x;
1645 let cz = self.offset_z + z;
1646 chunk.heightmap.set(cx, cz, self.get(x, z));
1647 }
1648 }
1649 }
1650
1651 pub fn read_from_chunk(&mut self, chunk: &crate::terrain::mod_types::TerrainChunk) {
1653 for z in 0..self.height {
1654 for x in 0..self.width {
1655 let cx = self.offset_x + x;
1656 let cz = self.offset_z + z;
1657 self.set(x, z, chunk.heightmap.get(cx, cz));
1658 }
1659 }
1660 self.dirty = false;
1661 }
1662}
1663
1664pub struct ChunkStitcher;
1668
1669impl ChunkStitcher {
1670 pub fn stitch_edge(
1674 primary: &mut crate::terrain::mod_types::TerrainChunk,
1675 neighbor: &crate::terrain::mod_types::TerrainChunk,
1676 edge: StitchEdge,
1677 blend_width: usize,
1678 ) {
1679 let pw = primary.heightmap.width;
1680 let ph = primary.heightmap.height;
1681 let nw = neighbor.heightmap.width;
1682 let nh = neighbor.heightmap.height;
1683
1684 match edge {
1685 StitchEdge::East => {
1686 for z in 0..ph {
1687 let nz = (z as f32 / ph as f32 * nh as f32) as usize;
1688 for bx in 0..blend_width {
1689 let px = pw - 1 - bx;
1690 let nx = bx;
1691 let t = bx as f32 / blend_width as f32;
1692 let p_val = primary.heightmap.get(px, z);
1693 let n_val = neighbor.heightmap.get(nx, nz.min(nh - 1));
1694 let blended = p_val + (n_val - p_val) * t;
1695 primary.heightmap.set(px, z, blended);
1696 }
1697 }
1698 }
1699 StitchEdge::West => {
1700 for z in 0..ph {
1701 let nz = (z as f32 / ph as f32 * nh as f32) as usize;
1702 for bx in 0..blend_width {
1703 let px = bx;
1704 let nx = nw - 1 - bx;
1705 let t = bx as f32 / blend_width as f32;
1706 let p_val = primary.heightmap.get(px, z);
1707 let n_val = neighbor.heightmap.get(nx.min(nw - 1), nz.min(nh - 1));
1708 let blended = n_val + (p_val - n_val) * (1.0 - t);
1709 primary.heightmap.set(px, z, blended);
1710 }
1711 }
1712 }
1713 StitchEdge::North => {
1714 for x in 0..pw {
1715 let nx = (x as f32 / pw as f32 * nw as f32) as usize;
1716 for bz in 0..blend_width {
1717 let pz = bz;
1718 let nz = nh - 1 - bz;
1719 let t = bz as f32 / blend_width as f32;
1720 let p_val = primary.heightmap.get(x, pz);
1721 let n_val = neighbor.heightmap.get(nx.min(nw - 1), nz.min(nh - 1));
1722 let blended = n_val + (p_val - n_val) * (1.0 - t);
1723 primary.heightmap.set(x, pz, blended);
1724 }
1725 }
1726 }
1727 StitchEdge::South => {
1728 for x in 0..pw {
1729 let nx = (x as f32 / pw as f32 * nw as f32) as usize;
1730 for bz in 0..blend_width {
1731 let pz = ph - 1 - bz;
1732 let nz = bz;
1733 let t = bz as f32 / blend_width as f32;
1734 let p_val = primary.heightmap.get(x, pz);
1735 let n_val = neighbor.heightmap.get(nx.min(nw - 1), nz.min(nh - 1));
1736 let blended = p_val + (n_val - p_val) * t;
1737 primary.heightmap.set(x, pz, blended);
1738 }
1739 }
1740 }
1741 }
1742 }
1743}
1744
1745#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1747pub enum StitchEdge {
1748 North,
1749 South,
1750 East,
1751 West,
1752}
1753
1754pub struct HeightmapQuantizer;
1758
1759impl HeightmapQuantizer {
1760 pub fn quantize_8bit(heights: &[f32]) -> Vec<u8> {
1762 heights.iter().map(|&h| (h.clamp(0.0, 1.0) * 255.0) as u8).collect()
1763 }
1764
1765 pub fn dequantize_8bit(bytes: &[u8]) -> Vec<f32> {
1767 bytes.iter().map(|&b| b as f32 / 255.0).collect()
1768 }
1769
1770 pub fn quantize_16bit(heights: &[f32]) -> Vec<u16> {
1772 heights.iter().map(|&h| (h.clamp(0.0, 1.0) * 65535.0) as u16).collect()
1773 }
1774
1775 pub fn dequantize_16bit(shorts: &[u16]) -> Vec<f32> {
1777 shorts.iter().map(|&s| s as f32 / 65535.0).collect()
1778 }
1779
1780 pub fn quantization_error(original: &[f32], quantized_8bit: &[u8]) -> f32 {
1782 if original.len() != quantized_8bit.len() { return f32::INFINITY; }
1783 original.iter().zip(quantized_8bit.iter())
1784 .map(|(&orig, &q)| (orig - q as f32 / 255.0).abs())
1785 .sum::<f32>() / original.len() as f32
1786 }
1787}
1788
1789#[derive(Debug, Default, Clone)]
1793pub struct StreamingTelemetry {
1794 pub frame_number: u64,
1795 pub visible_chunk_count: usize,
1796 pub loaded_chunk_count: usize,
1797 pub pending_chunk_count: usize,
1798 pub cache_hit_rate: f32,
1799 pub memory_usage_mb: f32,
1800 pub generation_rate: f32, pub eviction_count: usize,
1802 pub last_update_ms: f32,
1803}
1804
1805impl StreamingTelemetry {
1806 pub fn update_from_stats(&mut self, stats: &StreamingStats) {
1807 self.frame_number += 1;
1808 self.loaded_chunk_count = stats.chunks_loaded;
1809 self.pending_chunk_count = stats.pending_count;
1810 self.cache_hit_rate = stats.cache_hit_rate();
1811 self.memory_usage_mb = stats.memory_bytes as f32 / (1024.0 * 1024.0);
1812 }
1813
1814 pub fn to_display_string(&self) -> String {
1815 format!(
1816 "Frame:{} | Chunks:{} Pending:{} | Cache:{:.0}% | Mem:{:.1}MB",
1817 self.frame_number,
1818 self.loaded_chunk_count,
1819 self.pending_chunk_count,
1820 self.cache_hit_rate * 100.0,
1821 self.memory_usage_mb,
1822 )
1823 }
1824}
1825
1826pub struct ChunkRepair;
1830
1831impl ChunkRepair {
1832 pub fn clamp_heights(chunk: &mut crate::terrain::mod_types::TerrainChunk) {
1834 for v in chunk.heightmap.data.iter_mut() {
1835 *v = v.clamp(0.0, 1.0);
1836 }
1837 }
1838
1839 pub fn fix_nans(chunk: &mut crate::terrain::mod_types::TerrainChunk) {
1841 let w = chunk.heightmap.width;
1842 let h = chunk.heightmap.height;
1843 let data = chunk.heightmap.data.clone();
1844 for y in 0..h {
1845 for x in 0..w {
1846 if chunk.heightmap.get(x, y).is_nan() {
1847 let mut sum = 0.0f32;
1848 let mut count = 0;
1849 for (dx, dy) in &[(-1i32,0),(1,0),(0,-1i32),(0,1)] {
1850 let nx = x as i32 + dx;
1851 let ny = y as i32 + dy;
1852 if nx >= 0 && nx < w as i32 && ny >= 0 && ny < h as i32 {
1853 let v = data[ny as usize * w + nx as usize];
1854 if !v.is_nan() { sum += v; count += 1; }
1855 }
1856 }
1857 let replacement = if count > 0 { sum / count as f32 } else { 0.5 };
1858 chunk.heightmap.set(x, y, replacement);
1859 }
1860 }
1861 }
1862 }
1863
1864 pub fn is_valid(chunk: &crate::terrain::mod_types::TerrainChunk) -> bool {
1866 chunk.heightmap.data.iter().all(|&v| !v.is_nan() && v >= 0.0 && v <= 1.0)
1867 }
1868}
1869
1870#[cfg(test)]
1873mod more_streaming_tests {
1874 use super::*;
1875
1876 fn test_config() -> TerrainConfig {
1877 TerrainConfig { chunk_size: 16, view_distance: 1, lod_levels: 2, seed: 42 }
1878 }
1879
1880 #[test]
1881 fn test_terrain_patch_apply() {
1882 let config = test_config();
1883 let gen = ChunkGenerator::new(config);
1884 let mut chunk = gen.generate(ChunkCoord(0, 0));
1885 let mut patch = TerrainPatch::new(ChunkCoord(0, 0), 0, 0, 4, 4);
1886 for v in patch.data.iter_mut() { *v = 0.99; }
1887 patch.dirty = true;
1888 patch.apply_to_chunk(&mut chunk);
1889 assert!((chunk.heightmap.get(0, 0) - 0.99).abs() < 1e-5);
1890 }
1891
1892 #[test]
1893 fn test_terrain_patch_read_write() {
1894 let config = test_config();
1895 let gen = ChunkGenerator::new(config);
1896 let chunk = gen.generate(ChunkCoord(0, 0));
1897 let mut patch = TerrainPatch::new(ChunkCoord(0, 0), 0, 0, 4, 4);
1898 patch.read_from_chunk(&chunk);
1899 assert!(!patch.dirty);
1900 assert!(patch.data.iter().any(|&v| v > 0.0));
1901 }
1902
1903 #[test]
1904 fn test_heightmap_quantizer_8bit() {
1905 let heights: Vec<f32> = (0..256).map(|i| i as f32 / 255.0).collect();
1906 let quantized = HeightmapQuantizer::quantize_8bit(&heights);
1907 let deq = HeightmapQuantizer::dequantize_8bit(&quantized);
1908 let err = HeightmapQuantizer::quantization_error(&heights, &quantized);
1909 assert!(err < 0.005, "8-bit quantization error should be small");
1910 }
1911
1912 #[test]
1913 fn test_heightmap_quantizer_16bit() {
1914 let heights: Vec<f32> = (0..1024).map(|i| i as f32 / 1023.0).collect();
1915 let quantized = HeightmapQuantizer::quantize_16bit(&heights);
1916 let deq = HeightmapQuantizer::dequantize_16bit(&quantized);
1917 let max_err = heights.iter().zip(deq.iter())
1918 .map(|(&a, &b)| (a - b).abs())
1919 .fold(0.0f32, f32::max);
1920 assert!(max_err < 0.0001, "16-bit quantization should be very accurate");
1921 }
1922
1923 #[test]
1924 fn test_chunk_repair_clamp() {
1925 let config = test_config();
1926 let gen = ChunkGenerator::new(config);
1927 let mut chunk = gen.generate(ChunkCoord(0, 0));
1928 chunk.heightmap.data[0] = 2.0; chunk.heightmap.data[1] = -1.0; ChunkRepair::clamp_heights(&mut chunk);
1931 assert!(ChunkRepair::is_valid(&chunk));
1932 }
1933
1934 #[test]
1935 fn test_chunk_repair_nan() {
1936 let config = test_config();
1937 let gen = ChunkGenerator::new(config);
1938 let mut chunk = gen.generate(ChunkCoord(0, 0));
1939 chunk.heightmap.data[16] = f32::NAN;
1940 ChunkRepair::fix_nans(&mut chunk);
1941 assert!(ChunkRepair::is_valid(&chunk));
1942 }
1943
1944 #[test]
1945 fn test_streaming_telemetry() {
1946 let stats = StreamingStats {
1947 chunks_loaded: 10, chunks_unloaded: 2,
1948 cache_hits: 80, cache_misses: 20,
1949 pending_count: 3, memory_bytes: 2 * 1024 * 1024,
1950 generate_time_ms: 100.0,
1951 };
1952 let mut tel = StreamingTelemetry::default();
1953 tel.update_from_stats(&stats);
1954 assert_eq!(tel.loaded_chunk_count, 10);
1955 assert!((tel.cache_hit_rate - 0.8).abs() < 1e-4);
1956 assert!((tel.memory_usage_mb - 2.0).abs() < 1e-4);
1957 let s = tel.to_display_string();
1958 assert!(s.contains("Chunks:10"));
1959 }
1960
1961 #[test]
1962 fn test_extended_serializer_rle_chunk() {
1963 let config = test_config();
1964 let gen = ChunkGenerator::new(config);
1965 let chunk = gen.generate(ChunkCoord(0, 0));
1966 let bytes = ExtendedChunkSerializer::serialize_with_format(&chunk, SerializationFormat::Raw);
1967 let restored = ExtendedChunkSerializer::deserialize_with_format(
1968 &bytes, SerializationFormat::Raw
1969 );
1970 assert!(restored.is_some());
1971 }
1972}