1#[allow(dead_code, unused_variables, unused_mut, unused_imports)]
2
3use glam::{Vec2, Vec3, Vec4, Quat, Mat4};
4use std::collections::{HashMap, VecDeque, HashSet, BTreeMap};
5
6const MAX_STREAMING_LEVELS: usize = 512;
11const MAX_CELLS_PER_AXIS: usize = 256;
12const DEFAULT_CELL_SIZE: f32 = 512.0;
13const MAX_MEMORY_BUDGET_MB: f32 = 2048.0;
14const STREAMING_HYSTERESIS: f32 = 50.0;
15const MAX_CONCURRENT_LOADS: usize = 4;
16const PREFETCH_LOOKAHEAD_SECONDS: f32 = 2.0;
17const HZB_MAX_MIPS: usize = 8;
18const MAX_SECTOR_PORTALS: usize = 32;
19const LOD_BIAS_DISTANCE_SCALE: f32 = 0.001;
20const MAX_DEPENDENCY_DEPTH: usize = 64;
21const BANDWIDTH_ESTIMATE_WINDOW: usize = 60;
22const LEVEL_TIMELINE_CAPACITY: usize = 1024;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
29pub enum StreamingState {
30 Unloaded,
31 Queued,
32 Loading,
33 Loaded,
34 Unloading,
35 Failed,
36 Evicted,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
40pub enum LoadPriority {
41 Critical = 0,
42 High = 1,
43 Medium = 2,
44 Low = 3,
45 Prefetch = 4,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
49pub enum LodLevel {
50 Lod0 = 0,
51 Lod1 = 1,
52 Lod2 = 2,
53 Lod3 = 3,
54 Lod4 = 4,
55 Culled = 5,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum VolumeShape {
60 Box,
61 Sphere,
62 ConvexHull,
63 Cylinder,
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum SectorTransitionType {
68 Immediate,
69 Fade,
70 Portal,
71 Teleport,
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub enum LevelPersistence {
76 AlwaysLoaded,
77 Dynamic,
78 Transient,
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub enum EvictionPolicy {
83 Lru,
84 Lfu,
85 Distance,
86 Priority,
87}
88
89#[derive(Debug, Clone, Copy, PartialEq)]
90pub enum DependencyEdgeType {
91 HardDependency,
92 SoftDependency,
93 Optional,
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub enum DebugOverlay {
98 None,
99 StreamingState,
100 MemoryUsage,
101 LoadDistance,
102 CellGrid,
103 OcclusionHzb,
104 FrustumCulled,
105 PriorityHeatmap,
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub enum StreamingEventKind {
110 LevelQueued,
111 LevelLoadStarted,
112 LevelLoadCompleted,
113 LevelUnloadStarted,
114 LevelUnloadCompleted,
115 LevelLoadFailed,
116 MemoryPressure,
117 BudgetExceeded,
118 PrefetchHit,
119 PrefetchMiss,
120}
121
122#[derive(Debug, Clone)]
127pub struct Aabb {
128 pub min: Vec3,
129 pub max: Vec3,
130}
131
132impl Aabb {
133 pub fn new(min: Vec3, max: Vec3) -> Self {
134 Self { min, max }
135 }
136
137 pub fn center(&self) -> Vec3 {
138 (self.min + self.max) * 0.5
139 }
140
141 pub fn extents(&self) -> Vec3 {
142 (self.max - self.min) * 0.5
143 }
144
145 pub fn size(&self) -> Vec3 {
146 self.max - self.min
147 }
148
149 pub fn surface_area(&self) -> f32 {
150 let s = self.size();
151 2.0 * (s.x * s.y + s.y * s.z + s.z * s.x)
152 }
153
154 pub fn volume(&self) -> f32 {
155 let s = self.size();
156 s.x * s.y * s.z
157 }
158
159 pub fn contains_point(&self, p: Vec3) -> bool {
160 p.x >= self.min.x && p.x <= self.max.x
161 && p.y >= self.min.y && p.y <= self.max.y
162 && p.z >= self.min.z && p.z <= self.max.z
163 }
164
165 pub fn intersects(&self, other: &Aabb) -> bool {
166 self.min.x <= other.max.x && self.max.x >= other.min.x
167 && self.min.y <= other.max.y && self.max.y >= other.min.y
168 && self.min.z <= other.max.z && self.max.z >= other.min.z
169 }
170
171 pub fn expand_by(&self, amount: f32) -> Aabb {
172 Aabb {
173 min: self.min - Vec3::splat(amount),
174 max: self.max + Vec3::splat(amount),
175 }
176 }
177
178 pub fn distance_sq_to_point(&self, p: Vec3) -> f32 {
179 let dx = (self.min.x - p.x).max(0.0).max(p.x - self.max.x);
180 let dy = (self.min.y - p.y).max(0.0).max(p.y - self.max.y);
181 let dz = (self.min.z - p.z).max(0.0).max(p.z - self.max.z);
182 dx * dx + dy * dy + dz * dz
183 }
184
185 pub fn distance_to_point(&self, p: Vec3) -> f32 {
186 self.distance_sq_to_point(p).sqrt()
187 }
188
189 pub fn merge(&self, other: &Aabb) -> Aabb {
190 Aabb {
191 min: self.min.min(other.min),
192 max: self.max.max(other.max),
193 }
194 }
195
196 pub fn from_center_extents(center: Vec3, extents: Vec3) -> Self {
197 Self {
198 min: center - extents,
199 max: center + extents,
200 }
201 }
202}
203
204#[derive(Debug, Clone)]
205pub struct Sphere {
206 pub center: Vec3,
207 pub radius: f32,
208}
209
210impl Sphere {
211 pub fn new(center: Vec3, radius: f32) -> Self {
212 Self { center, radius }
213 }
214
215 pub fn contains_point(&self, p: Vec3) -> bool {
216 (p - self.center).length_squared() <= self.radius * self.radius
217 }
218
219 pub fn intersects_aabb(&self, aabb: &Aabb) -> bool {
220 let dist_sq = aabb.distance_sq_to_point(self.center);
221 dist_sq <= self.radius * self.radius
222 }
223
224 pub fn intersects_sphere(&self, other: &Sphere) -> bool {
225 let r = self.radius + other.radius;
226 (self.center - other.center).length_squared() <= r * r
227 }
228}
229
230#[derive(Debug, Clone)]
231pub struct FrustumPlane {
232 pub normal: Vec3,
233 pub distance: f32,
234}
235
236impl FrustumPlane {
237 pub fn new(normal: Vec3, distance: f32) -> Self {
238 let len = normal.length();
239 Self {
240 normal: if len > 1e-6 { normal / len } else { normal },
241 distance: if len > 1e-6 { distance / len } else { distance },
242 }
243 }
244
245 pub fn signed_distance_to(&self, p: Vec3) -> f32 {
246 self.normal.dot(p) + self.distance
247 }
248}
249
250#[derive(Debug, Clone)]
251pub struct Frustum {
252 pub planes: [FrustumPlane; 6],
253}
254
255impl Frustum {
256 pub fn from_view_proj(vp: Mat4) -> Self {
258 let cols = vp.to_cols_array_2d();
259 let r0 = Vec4::new(cols[0][0], cols[1][0], cols[2][0], cols[3][0]);
261 let r1 = Vec4::new(cols[0][1], cols[1][1], cols[2][1], cols[3][1]);
262 let r2 = Vec4::new(cols[0][2], cols[1][2], cols[2][2], cols[3][2]);
263 let r3 = Vec4::new(cols[0][3], cols[1][3], cols[2][3], cols[3][3]);
264
265 let left = r3 + r0;
266 let right = r3 - r0;
267 let bottom = r3 + r1;
268 let top = r3 - r1;
269 let near = r3 + r2;
270 let far = r3 - r2;
271
272 let make = |v: Vec4| FrustumPlane::new(Vec3::new(v.x, v.y, v.z), v.w);
273
274 Self {
275 planes: [
276 make(left),
277 make(right),
278 make(bottom),
279 make(top),
280 make(near),
281 make(far),
282 ],
283 }
284 }
285
286 pub fn test_aabb(&self, aabb: &Aabb) -> bool {
287 let c = aabb.center();
288 let e = aabb.extents();
289 for plane in &self.planes {
290 let r = e.x * plane.normal.x.abs()
291 + e.y * plane.normal.y.abs()
292 + e.z * plane.normal.z.abs();
293 let d = plane.signed_distance_to(c);
294 if d + r < 0.0 {
295 return false;
296 }
297 }
298 true
299 }
300
301 pub fn test_sphere(&self, sphere: &Sphere) -> bool {
302 for plane in &self.planes {
303 if plane.signed_distance_to(sphere.center) < -sphere.radius {
304 return false;
305 }
306 }
307 true
308 }
309
310 pub fn test_point(&self, p: Vec3) -> bool {
311 for plane in &self.planes {
312 if plane.signed_distance_to(p) < 0.0 {
313 return false;
314 }
315 }
316 true
317 }
318}
319
320#[derive(Debug, Clone)]
321pub struct ConvexHull {
322 pub planes: Vec<FrustumPlane>,
323 pub vertices: Vec<Vec3>,
324}
325
326impl ConvexHull {
327 pub fn new(vertices: Vec<Vec3>) -> Self {
328 let mut planes = Vec::new();
330 let centroid = if !vertices.is_empty() {
332 vertices.iter().fold(Vec3::ZERO, |acc, &v| acc + v) / vertices.len() as f32
333 } else {
334 Vec3::ZERO
335 };
336
337 let n = vertices.len();
339 for i in 0..n {
340 for j in (i + 1)..n {
341 for k in (j + 1)..n {
342 let a = vertices[i];
343 let b = vertices[j];
344 let c = vertices[k];
345 let normal = (b - a).cross(c - a);
346 if normal.length_squared() < 1e-10 {
347 continue;
348 }
349 let n = normal.normalize();
350 let d = -n.dot(a);
351 if n.dot(centroid) + d > 0.0 {
353 planes.push(FrustumPlane::new(-n, -d));
354 } else {
355 planes.push(FrustumPlane::new(n, d));
356 }
357 break;
358 }
359 break;
360 }
361 break;
362 }
363
364 Self { planes, vertices }
365 }
366
367 pub fn contains_point(&self, p: Vec3) -> bool {
368 for plane in &self.planes {
369 if plane.signed_distance_to(p) < 0.0 {
370 return false;
371 }
372 }
373 true
374 }
375
376 pub fn intersects_aabb(&self, aabb: &Aabb) -> bool {
377 let c = aabb.center();
378 let e = aabb.extents();
379 for plane in &self.planes {
380 let r = e.x * plane.normal.x.abs()
381 + e.y * plane.normal.y.abs()
382 + e.z * plane.normal.z.abs();
383 if plane.signed_distance_to(c) + r < 0.0 {
384 return false;
385 }
386 }
387 true
388 }
389}
390
391#[derive(Debug, Clone)]
396pub struct HzbMipLevel {
397 pub width: usize,
398 pub height: usize,
399 pub data: Vec<f32>, }
401
402impl HzbMipLevel {
403 pub fn new(width: usize, height: usize) -> Self {
404 Self {
405 width,
406 height,
407 data: vec![1.0f32; width * height],
408 }
409 }
410
411 pub fn sample(&self, u: f32, v: f32) -> f32 {
412 let px = ((u * self.width as f32) as usize).min(self.width.saturating_sub(1));
413 let py = ((v * self.height as f32) as usize).min(self.height.saturating_sub(1));
414 self.data[py * self.width + px]
415 }
416
417 pub fn sample_bilinear(&self, u: f32, v: f32) -> f32 {
418 let x = u * (self.width as f32 - 1.0);
419 let y = v * (self.height as f32 - 1.0);
420 let x0 = (x as usize).min(self.width.saturating_sub(1));
421 let y0 = (y as usize).min(self.height.saturating_sub(1));
422 let x1 = (x0 + 1).min(self.width.saturating_sub(1));
423 let y1 = (y0 + 1).min(self.height.saturating_sub(1));
424 let fx = x - x0 as f32;
425 let fy = y - y0 as f32;
426 let v00 = self.data[y0 * self.width + x0];
427 let v10 = self.data[y0 * self.width + x1];
428 let v01 = self.data[y1 * self.width + x0];
429 let v11 = self.data[y1 * self.width + x1];
430 v00 * (1.0 - fx) * (1.0 - fy)
431 + v10 * fx * (1.0 - fy)
432 + v01 * (1.0 - fx) * fy
433 + v11 * fx * fy
434 }
435}
436
437#[derive(Debug, Clone)]
438pub struct HierarchicalZBuffer {
439 pub mips: Vec<HzbMipLevel>,
440 pub base_width: usize,
441 pub base_height: usize,
442}
443
444impl HierarchicalZBuffer {
445 pub fn new(width: usize, height: usize) -> Self {
446 let mut mips = Vec::new();
447 let mut w = width;
448 let mut h = height;
449 for _ in 0..HZB_MAX_MIPS {
450 mips.push(HzbMipLevel::new(w, h));
451 w = (w / 2).max(1);
452 h = (h / 2).max(1);
453 if w == 1 && h == 1 {
454 mips.push(HzbMipLevel::new(1, 1));
455 break;
456 }
457 }
458 Self { mips, base_width: width, base_height: height }
459 }
460
461 pub fn build_from_depth(&mut self, depth: &[f32]) {
463 if self.mips.is_empty() { return; }
464 let w = self.base_width;
465 let h = self.base_height;
466 let mip0 = &mut self.mips[0];
468 let len = (w * h).min(depth.len()).min(mip0.data.len());
469 mip0.data[..len].copy_from_slice(&depth[..len]);
470
471 for i in 1..self.mips.len() {
473 let pw = self.mips[i - 1].width;
474 let ph = self.mips[i - 1].height;
475 let nw = (pw / 2).max(1);
476 let nh = (ph / 2).max(1);
477 let prev_data = self.mips[i - 1].data.clone();
478 let cur = &mut self.mips[i];
479 cur.width = nw;
480 cur.height = nh;
481 cur.data.resize(nw * nh, 1.0);
482 for y in 0..nh {
483 for x in 0..nw {
484 let sx = (x * 2).min(pw.saturating_sub(1));
485 let sy = (y * 2).min(ph.saturating_sub(1));
486 let sx1 = (sx + 1).min(pw.saturating_sub(1));
487 let sy1 = (sy + 1).min(ph.saturating_sub(1));
488 let v00 = prev_data[sy * pw + sx];
489 let v10 = prev_data[sy * pw + sx1];
490 let v01 = prev_data[sy1 * pw + sx];
491 let v11 = prev_data[sy1 * pw + sx1];
492 cur.data[y * nw + x] = v00.min(v10).min(v01).min(v11);
494 }
495 }
496 }
497 }
498
499 pub fn test_aabb_visibility(&self, aabb: &Aabb, view_proj: &Mat4) -> bool {
501 if self.mips.is_empty() { return true; }
502
503 let corners = [
505 Vec3::new(aabb.min.x, aabb.min.y, aabb.min.z),
506 Vec3::new(aabb.max.x, aabb.min.y, aabb.min.z),
507 Vec3::new(aabb.min.x, aabb.max.y, aabb.min.z),
508 Vec3::new(aabb.max.x, aabb.max.y, aabb.min.z),
509 Vec3::new(aabb.min.x, aabb.min.y, aabb.max.z),
510 Vec3::new(aabb.max.x, aabb.min.y, aabb.max.z),
511 Vec3::new(aabb.min.x, aabb.max.y, aabb.max.z),
512 Vec3::new(aabb.max.x, aabb.max.y, aabb.max.z),
513 ];
514
515 let mut min_x = f32::MAX;
516 let mut min_y = f32::MAX;
517 let mut max_x = f32::MIN;
518 let mut max_y = f32::MIN;
519 let mut min_z = f32::MAX;
520 let mut all_behind = true;
521
522 for &c in &corners {
523 let clip = *view_proj * Vec4::new(c.x, c.y, c.z, 1.0);
524 if clip.w <= 0.0 { continue; }
525 all_behind = false;
526 let ndc = Vec3::new(clip.x / clip.w, clip.y / clip.w, clip.z / clip.w);
527 let u = (ndc.x * 0.5 + 0.5).clamp(0.0, 1.0);
528 let v = (1.0 - (ndc.y * 0.5 + 0.5)).clamp(0.0, 1.0);
529 min_x = min_x.min(u);
530 min_y = min_y.min(v);
531 max_x = max_x.max(u);
532 max_y = max_y.max(v);
533 min_z = min_z.min(ndc.z.clamp(0.0, 1.0));
534 }
535
536 if all_behind { return false; }
537
538 let w_uv = max_x - min_x;
540 let h_uv = max_y - min_y;
541 let max_dim = w_uv.max(h_uv);
542 let mip = if max_dim <= 0.0 {
543 self.mips.len() - 1
544 } else {
545 let level = (-max_dim.log2()).max(0.0) as usize;
546 level.min(self.mips.len() - 1)
547 };
548
549 let mip_data = &self.mips[mip];
550 let x0 = ((min_x * mip_data.width as f32) as usize).min(mip_data.width.saturating_sub(1));
552 let x1 = ((max_x * mip_data.width as f32) as usize).min(mip_data.width.saturating_sub(1));
553 let y0 = ((min_y * mip_data.height as f32) as usize).min(mip_data.height.saturating_sub(1));
554 let y1 = ((max_y * mip_data.height as f32) as usize).min(mip_data.height.saturating_sub(1));
555
556 let mut occluder_depth = f32::MIN;
557 for y in y0..=y1 {
558 for x in x0..=x1 {
559 let d = mip_data.data[y * mip_data.width + x];
560 occluder_depth = occluder_depth.max(d);
561 }
562 }
563
564 min_z <= occluder_depth + 1e-4
566 }
567}
568
569#[derive(Debug, Clone)]
574pub struct StreamingLevelAsset {
575 pub id: u64,
576 pub name: String,
577 pub file_path: String,
578 pub size_bytes: u64,
579 pub uncompressed_size_bytes: u64,
580 pub dependencies: Vec<u64>,
581 pub load_time_estimate_ms: f32,
582}
583
584#[derive(Debug, Clone)]
585pub struct StreamingLevel {
586 pub id: u64,
587 pub name: String,
588 pub asset: StreamingLevelAsset,
589 pub bounds: Aabb,
590 pub sphere_bounds: Sphere,
591 pub load_distance: f32,
592 pub unload_distance: f32,
593 pub priority: LoadPriority,
594 pub persistence: LevelPersistence,
595 pub state: StreamingState,
596 pub lod_bias: f32,
597 pub memory_footprint_mb: f32,
598 pub current_lod: LodLevel,
599 pub sector_id: Option<u64>,
600 pub load_timestamp_ms: f64,
601 pub unload_timestamp_ms: f64,
602 pub load_count: u32,
603 pub transform: Mat4,
604 pub is_visible: bool,
605 pub is_frustum_culled: bool,
606 pub is_occlusion_culled: bool,
607 pub distance_to_camera: f32,
608 pub screen_size: f32,
609 pub importance_weight: f32,
610}
611
612impl StreamingLevel {
613 pub fn new(id: u64, name: String, asset: StreamingLevelAsset, bounds: Aabb) -> Self {
614 let center = bounds.center();
615 let radius = bounds.extents().length();
616 Self {
617 id,
618 name,
619 asset,
620 bounds: bounds.clone(),
621 sphere_bounds: Sphere::new(center, radius),
622 load_distance: 1000.0,
623 unload_distance: 1200.0,
624 priority: LoadPriority::Medium,
625 persistence: LevelPersistence::Dynamic,
626 state: StreamingState::Unloaded,
627 lod_bias: 0.0,
628 memory_footprint_mb: 0.0,
629 current_lod: LodLevel::Culled,
630 sector_id: None,
631 load_timestamp_ms: 0.0,
632 unload_timestamp_ms: 0.0,
633 load_count: 0,
634 transform: Mat4::IDENTITY,
635 is_visible: false,
636 is_frustum_culled: false,
637 is_occlusion_culled: false,
638 distance_to_camera: f32::MAX,
639 screen_size: 0.0,
640 importance_weight: 1.0,
641 }
642 }
643
644 pub fn compute_lod(&self, distance: f32, lod_bias: f32) -> LodLevel {
645 let adjusted = distance * (1.0 + lod_bias * LOD_BIAS_DISTANCE_SCALE);
646 if adjusted < 100.0 { LodLevel::Lod0 }
647 else if adjusted < 300.0 { LodLevel::Lod1 }
648 else if adjusted < 600.0 { LodLevel::Lod2 }
649 else if adjusted < 1000.0 { LodLevel::Lod3 }
650 else if adjusted < self.load_distance { LodLevel::Lod4 }
651 else { LodLevel::Culled }
652 }
653
654 pub fn compute_screen_size(&self, camera_pos: Vec3, fov_y_rad: f32, viewport_height: f32) -> f32 {
655 let dist = (self.sphere_bounds.center - camera_pos).length().max(0.01);
656 let angular_size = 2.0 * (self.sphere_bounds.radius / dist).atan();
657 let pixels = (angular_size / fov_y_rad) * viewport_height;
658 pixels / viewport_height
659 }
660
661 pub fn should_load(&self, camera_pos: Vec3) -> bool {
662 match self.persistence {
663 LevelPersistence::AlwaysLoaded => true,
664 _ => self.bounds.distance_to_point(camera_pos) < self.load_distance,
665 }
666 }
667
668 pub fn should_unload(&self, camera_pos: Vec3) -> bool {
669 match self.persistence {
670 LevelPersistence::AlwaysLoaded => false,
671 _ => self.bounds.distance_to_point(camera_pos) > self.unload_distance,
672 }
673 }
674
675 pub fn memory_estimate_mb(&self) -> f32 {
676 let base = self.asset.size_bytes as f32 / (1024.0 * 1024.0);
677 match self.current_lod {
678 LodLevel::Lod0 => base,
679 LodLevel::Lod1 => base * 0.7,
680 LodLevel::Lod2 => base * 0.4,
681 LodLevel::Lod3 => base * 0.2,
682 LodLevel::Lod4 => base * 0.1,
683 LodLevel::Culled => 0.0,
684 }
685 }
686}
687
688#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
693pub struct CellCoord {
694 pub x: i32,
695 pub y: i32,
696 pub z: i32,
697}
698
699impl CellCoord {
700 pub fn new(x: i32, y: i32, z: i32) -> Self {
701 Self { x, y, z }
702 }
703
704 pub fn neighbors_2d(&self) -> [CellCoord; 8] {
705 [
706 CellCoord::new(self.x - 1, self.y - 1, self.z),
707 CellCoord::new(self.x, self.y - 1, self.z),
708 CellCoord::new(self.x + 1, self.y - 1, self.z),
709 CellCoord::new(self.x - 1, self.y, self.z),
710 CellCoord::new(self.x + 1, self.y, self.z),
711 CellCoord::new(self.x - 1, self.y + 1, self.z),
712 CellCoord::new(self.x, self.y + 1, self.z),
713 CellCoord::new(self.x + 1, self.y + 1, self.z),
714 ]
715 }
716
717 pub fn neighbors_3d(&self) -> Vec<CellCoord> {
718 let mut result = Vec::with_capacity(26);
719 for dz in -1i32..=1 {
720 for dy in -1i32..=1 {
721 for dx in -1i32..=1 {
722 if dx == 0 && dy == 0 && dz == 0 { continue; }
723 result.push(CellCoord::new(self.x + dx, self.y + dy, self.z + dz));
724 }
725 }
726 }
727 result
728 }
729
730 pub fn manhattan_distance(&self, other: &CellCoord) -> i32 {
731 (self.x - other.x).abs() + (self.y - other.y).abs() + (self.z - other.z).abs()
732 }
733
734 pub fn chebyshev_distance(&self, other: &CellCoord) -> i32 {
735 let dx = (self.x - other.x).abs();
736 let dy = (self.y - other.y).abs();
737 let dz = (self.z - other.z).abs();
738 dx.max(dy).max(dz)
739 }
740}
741
742#[derive(Debug, Clone)]
743pub struct WorldCell {
744 pub coord: CellCoord,
745 pub bounds: Aabb,
746 pub level_ids: Vec<u64>,
747 pub dynamic_object_ids: Vec<u64>,
748 pub memory_used_mb: f32,
749 pub is_active: bool,
750 pub load_priority_score: f32,
751 pub last_accessed_frame: u64,
752}
753
754impl WorldCell {
755 pub fn new(coord: CellCoord, cell_size: f32) -> Self {
756 let min = Vec3::new(
757 coord.x as f32 * cell_size,
758 coord.z as f32 * cell_size,
759 coord.y as f32 * cell_size,
760 );
761 let max = min + Vec3::splat(cell_size);
762 Self {
763 coord,
764 bounds: Aabb::new(min, max),
765 level_ids: Vec::new(),
766 dynamic_object_ids: Vec::new(),
767 memory_used_mb: 0.0,
768 is_active: false,
769 load_priority_score: 0.0,
770 last_accessed_frame: 0,
771 }
772 }
773
774 pub fn center(&self) -> Vec3 {
775 self.bounds.center()
776 }
777
778 pub fn compute_priority_score(&mut self, camera_pos: Vec3, camera_dir: Vec3) -> f32 {
779 let dist = self.bounds.distance_to_point(camera_pos).max(0.01);
780 let to_cell = (self.center() - camera_pos).normalize_or_zero();
781 let dot = camera_dir.dot(to_cell).clamp(0.0, 1.0);
782 let score = (1.0 / dist) * (0.5 + 0.5 * dot);
784 self.load_priority_score = score;
785 score
786 }
787}
788
789#[derive(Debug)]
790pub struct WorldPartitionGrid {
791 pub cell_size: f32,
792 pub cells: HashMap<CellCoord, WorldCell>,
793 pub spatial_hash: HashMap<u64, CellCoord>, pub origin: Vec3,
795 pub active_radius_cells: i32,
796}
797
798impl WorldPartitionGrid {
799 pub fn new(cell_size: f32, origin: Vec3) -> Self {
800 Self {
801 cell_size,
802 cells: HashMap::new(),
803 spatial_hash: HashMap::new(),
804 origin,
805 active_radius_cells: 4,
806 }
807 }
808
809 pub fn world_to_cell(&self, pos: Vec3) -> CellCoord {
810 let rel = pos - self.origin;
811 CellCoord::new(
812 (rel.x / self.cell_size).floor() as i32,
813 (rel.z / self.cell_size).floor() as i32,
814 (rel.y / self.cell_size).floor() as i32,
815 )
816 }
817
818 pub fn cell_to_world_center(&self, coord: CellCoord) -> Vec3 {
819 Vec3::new(
820 self.origin.x + (coord.x as f32 + 0.5) * self.cell_size,
821 self.origin.y + (coord.z as f32 + 0.5) * self.cell_size,
822 self.origin.z + (coord.y as f32 + 0.5) * self.cell_size,
823 )
824 }
825
826 pub fn get_or_create_cell(&mut self, coord: CellCoord) -> &mut WorldCell {
827 let cell_size = self.cell_size;
828 self.cells.entry(coord).or_insert_with(|| WorldCell::new(coord, cell_size))
829 }
830
831 pub fn get_cells_in_radius(&self, center: Vec3, radius: f32) -> Vec<CellCoord> {
832 let coord = self.world_to_cell(center);
833 let cell_radius = (radius / self.cell_size).ceil() as i32 + 1;
834 let mut result = Vec::new();
835 for dz in -cell_radius..=cell_radius {
836 for dy in -cell_radius..=cell_radius {
837 for dx in -cell_radius..=cell_radius {
838 let c = CellCoord::new(coord.x + dx, coord.y + dy, coord.z + dz);
839 if let Some(cell) = self.cells.get(&c) {
840 if cell.bounds.distance_to_point(center) <= radius {
841 result.push(c);
842 }
843 } else {
844 let cell_center = self.cell_to_world_center(c);
846 if (cell_center - center).length() <= radius + self.cell_size {
847 result.push(c);
848 }
849 }
850 }
851 }
852 }
853 result
854 }
855
856 pub fn register_object(&mut self, object_id: u64, pos: Vec3) {
857 let new_coord = self.world_to_cell(pos);
858 if let Some(&old_coord) = self.spatial_hash.get(&object_id) {
860 if old_coord != new_coord {
861 if let Some(cell) = self.cells.get_mut(&old_coord) {
862 cell.dynamic_object_ids.retain(|&id| id != object_id);
863 }
864 }
865 }
866 self.spatial_hash.insert(object_id, new_coord);
867 let cell = self.get_or_create_cell(new_coord);
868 if !cell.dynamic_object_ids.contains(&object_id) {
869 cell.dynamic_object_ids.push(object_id);
870 }
871 }
872
873 pub fn unregister_object(&mut self, object_id: u64) {
874 if let Some(coord) = self.spatial_hash.remove(&object_id) {
875 if let Some(cell) = self.cells.get_mut(&coord) {
876 cell.dynamic_object_ids.retain(|&id| id != object_id);
877 }
878 }
879 }
880
881 pub fn query_objects_in_radius(&self, pos: Vec3, radius: f32) -> Vec<u64> {
882 let cells = self.get_cells_in_radius(pos, radius);
883 let mut result = Vec::new();
884 for coord in cells {
885 if let Some(cell) = self.cells.get(&coord) {
886 for &oid in &cell.dynamic_object_ids {
887 result.push(oid);
888 }
889 }
890 }
891 result
892 }
893
894 pub fn optimize_cell_size(&self, level_count: usize, world_size: f32) -> f32 {
895 let target_cells = (level_count / 6).max(1);
897 let cells_per_axis = (target_cells as f32).cbrt().ceil() as usize;
898 (world_size / cells_per_axis as f32).max(64.0)
899 }
900
901 pub fn compute_total_memory_mb(&self) -> f32 {
902 self.cells.values().map(|c| c.memory_used_mb).sum()
903 }
904
905 pub fn get_neighbor_cells(&self, coord: CellCoord) -> Vec<&WorldCell> {
906 coord.neighbors_3d()
907 .into_iter()
908 .filter_map(|c| self.cells.get(&c))
909 .collect()
910 }
911}
912
913#[derive(Debug, Clone)]
918pub struct StreamingVolumeBox {
919 pub transform: Mat4,
920 pub half_extents: Vec3,
921}
922
923impl StreamingVolumeBox {
924 pub fn contains_point(&self, world_pos: Vec3) -> bool {
925 let inv = self.transform.inverse();
927 let local = inv.transform_point3(world_pos);
928 local.x.abs() <= self.half_extents.x
929 && local.y.abs() <= self.half_extents.y
930 && local.z.abs() <= self.half_extents.z
931 }
932
933 pub fn distance_to_point(&self, world_pos: Vec3) -> f32 {
934 let inv = self.transform.inverse();
935 let local = inv.transform_point3(world_pos);
936 let dx = (local.x.abs() - self.half_extents.x).max(0.0);
937 let dy = (local.y.abs() - self.half_extents.y).max(0.0);
938 let dz = (local.z.abs() - self.half_extents.z).max(0.0);
939 (dx * dx + dy * dy + dz * dz).sqrt()
940 }
941
942 pub fn world_aabb(&self) -> Aabb {
943 let e = self.half_extents;
945 let corners = [
946 Vec3::new(-e.x, -e.y, -e.z),
947 Vec3::new( e.x, -e.y, -e.z),
948 Vec3::new(-e.x, e.y, -e.z),
949 Vec3::new( e.x, e.y, -e.z),
950 Vec3::new(-e.x, -e.y, e.z),
951 Vec3::new( e.x, -e.y, e.z),
952 Vec3::new(-e.x, e.y, e.z),
953 Vec3::new( e.x, e.y, e.z),
954 ];
955 let mut min = Vec3::splat(f32::MAX);
956 let mut max = Vec3::splat(f32::MIN);
957 for c in corners {
958 let w = self.transform.transform_point3(c);
959 min = min.min(w);
960 max = max.max(w);
961 }
962 Aabb::new(min, max)
963 }
964}
965
966#[derive(Debug, Clone)]
967pub struct StreamingVolume {
968 pub id: u64,
969 pub name: String,
970 pub shape: VolumeShape,
971 pub box_volume: Option<StreamingVolumeBox>,
972 pub sphere_volume: Option<Sphere>,
973 pub convex_hull: Option<ConvexHull>,
974 pub load_radius: f32,
975 pub unload_radius: f32,
976 pub importance_weight: f32,
977 pub target_level_ids: Vec<u64>,
978 pub is_enabled: bool,
979}
980
981impl StreamingVolume {
982 pub fn new_box(id: u64, name: String, transform: Mat4, half_extents: Vec3) -> Self {
983 Self {
984 id,
985 name,
986 shape: VolumeShape::Box,
987 box_volume: Some(StreamingVolumeBox { transform, half_extents }),
988 sphere_volume: None,
989 convex_hull: None,
990 load_radius: 0.0,
991 unload_radius: 0.0,
992 importance_weight: 1.0,
993 target_level_ids: Vec::new(),
994 is_enabled: true,
995 }
996 }
997
998 pub fn new_sphere(id: u64, name: String, center: Vec3, load_radius: f32, unload_radius: f32) -> Self {
999 Self {
1000 id,
1001 name,
1002 shape: VolumeShape::Sphere,
1003 box_volume: None,
1004 sphere_volume: Some(Sphere::new(center, load_radius)),
1005 convex_hull: None,
1006 load_radius,
1007 unload_radius,
1008 importance_weight: 1.0,
1009 target_level_ids: Vec::new(),
1010 is_enabled: true,
1011 }
1012 }
1013
1014 pub fn contains_point(&self, p: Vec3) -> bool {
1015 if !self.is_enabled { return false; }
1016 match self.shape {
1017 VolumeShape::Box => {
1018 self.box_volume.as_ref().map_or(false, |b| b.contains_point(p))
1019 }
1020 VolumeShape::Sphere => {
1021 self.sphere_volume.as_ref().map_or(false, |s| s.contains_point(p))
1022 }
1023 VolumeShape::ConvexHull => {
1024 self.convex_hull.as_ref().map_or(false, |c| c.contains_point(p))
1025 }
1026 VolumeShape::Cylinder => {
1027 self.sphere_volume.as_ref().map_or(false, |s| {
1029 let flat = Vec3::new(p.x - s.center.x, 0.0, p.z - s.center.z);
1030 flat.length_squared() <= s.radius * s.radius
1031 && (p.y - s.center.y).abs() <= s.radius
1032 })
1033 }
1034 }
1035 }
1036
1037 pub fn distance_to_point(&self, p: Vec3) -> f32 {
1038 match self.shape {
1039 VolumeShape::Box => {
1040 self.box_volume.as_ref().map_or(f32::MAX, |b| b.distance_to_point(p))
1041 }
1042 VolumeShape::Sphere | VolumeShape::Cylinder => {
1043 self.sphere_volume.as_ref().map_or(f32::MAX, |s| {
1044 ((s.center - p).length() - s.radius).max(0.0)
1045 })
1046 }
1047 VolumeShape::ConvexHull => {
1048 if let Some(ch) = &self.convex_hull {
1050 if ch.contains_point(p) { 0.0 } else { 1.0 } } else { f32::MAX }
1052 }
1053 }
1054 }
1055
1056 pub fn should_trigger_load(&self, p: Vec3) -> bool {
1057 match self.shape {
1058 VolumeShape::Sphere => {
1059 self.sphere_volume.as_ref().map_or(false, |s| {
1060 (s.center - p).length() < self.load_radius
1061 })
1062 }
1063 _ => self.contains_point(p),
1064 }
1065 }
1066
1067 pub fn should_trigger_unload(&self, p: Vec3) -> bool {
1068 match self.shape {
1069 VolumeShape::Sphere => {
1070 self.sphere_volume.as_ref().map_or(false, |s| {
1071 (s.center - p).length() > self.unload_radius
1072 })
1073 }
1074 _ => !self.contains_point(p),
1075 }
1076 }
1077}
1078
1079#[derive(Debug, Clone)]
1084pub struct LoadRequest {
1085 pub level_id: u64,
1086 pub priority: LoadPriority,
1087 pub distance_weight: f32,
1088 pub enqueue_time_ms: f64,
1089 pub predicted_load_time_ms: f32,
1090 pub is_prefetch: bool,
1091}
1092
1093impl LoadRequest {
1094 pub fn score(&self) -> f32 {
1095 let priority_bonus = match self.priority {
1096 LoadPriority::Critical => 1000.0,
1097 LoadPriority::High => 100.0,
1098 LoadPriority::Medium => 10.0,
1099 LoadPriority::Low => 1.0,
1100 LoadPriority::Prefetch => 0.1,
1101 };
1102 priority_bonus + self.distance_weight * 10.0
1103 }
1104}
1105
1106#[derive(Debug)]
1107pub struct StreamingLoadQueue {
1108 pub pending: Vec<LoadRequest>,
1109 pub in_flight: Vec<LoadRequest>,
1110 pub max_concurrent: usize,
1111 pub total_bytes_loaded: u64,
1112 pub total_loads: u64,
1113 pub bandwidth_samples: VecDeque<f32>, }
1115
1116impl StreamingLoadQueue {
1117 pub fn new(max_concurrent: usize) -> Self {
1118 Self {
1119 pending: Vec::new(),
1120 in_flight: Vec::new(),
1121 max_concurrent,
1122 total_bytes_loaded: 0,
1123 total_loads: 0,
1124 bandwidth_samples: VecDeque::with_capacity(BANDWIDTH_ESTIMATE_WINDOW),
1125 }
1126 }
1127
1128 pub fn enqueue(&mut self, request: LoadRequest) {
1129 self.pending.retain(|r| r.level_id != request.level_id);
1131 self.pending.push(request);
1132 self.pending.sort_by(|a, b| b.score().partial_cmp(&a.score()).unwrap_or(std::cmp::Ordering::Equal));
1134 }
1135
1136 pub fn dequeue_next(&mut self) -> Option<LoadRequest> {
1137 if self.in_flight.len() >= self.max_concurrent { return None; }
1138 if self.pending.is_empty() { return None; }
1139 let req = self.pending.remove(0);
1140 self.in_flight.push(req.clone());
1141 Some(req)
1142 }
1143
1144 pub fn complete_load(&mut self, level_id: u64, bytes_loaded: u64, time_ms: f32) {
1145 self.in_flight.retain(|r| r.level_id != level_id);
1146 self.total_bytes_loaded += bytes_loaded;
1147 self.total_loads += 1;
1148 if time_ms > 0.0 {
1149 let mb_per_s = (bytes_loaded as f32 / (1024.0 * 1024.0)) / (time_ms / 1000.0);
1150 if self.bandwidth_samples.len() >= BANDWIDTH_ESTIMATE_WINDOW {
1151 self.bandwidth_samples.pop_front();
1152 }
1153 self.bandwidth_samples.push_back(mb_per_s);
1154 }
1155 }
1156
1157 pub fn cancel_load(&mut self, level_id: u64) {
1158 self.pending.retain(|r| r.level_id != level_id);
1159 self.in_flight.retain(|r| r.level_id != level_id);
1160 }
1161
1162 pub fn estimated_bandwidth_mb_s(&self) -> f32 {
1163 if self.bandwidth_samples.is_empty() { return 100.0; }
1164 let sum: f32 = self.bandwidth_samples.iter().sum();
1165 sum / self.bandwidth_samples.len() as f32
1166 }
1167
1168 pub fn estimated_remaining_time_ms(&self) -> f32 {
1169 let bandwidth = self.estimated_bandwidth_mb_s();
1170 let total_pending_mb: f32 = self.pending.iter()
1171 .map(|r| r.predicted_load_time_ms)
1172 .sum::<f32>() / 1000.0; if bandwidth > 0.0 { total_pending_mb / bandwidth * 1000.0 } else { f32::MAX }
1174 }
1175
1176 pub fn is_loading(&self, level_id: u64) -> bool {
1177 self.in_flight.iter().any(|r| r.level_id == level_id)
1178 }
1179
1180 pub fn is_pending(&self, level_id: u64) -> bool {
1181 self.pending.iter().any(|r| r.level_id == level_id)
1182 }
1183
1184 pub fn queue_depth(&self) -> usize {
1185 self.pending.len() + self.in_flight.len()
1186 }
1187}
1188
1189#[derive(Debug, Clone)]
1194pub struct CameraVelocityTracker {
1195 pub positions: VecDeque<Vec3>,
1196 pub timestamps: VecDeque<f64>,
1197 pub max_samples: usize,
1198}
1199
1200impl CameraVelocityTracker {
1201 pub fn new(max_samples: usize) -> Self {
1202 Self {
1203 positions: VecDeque::with_capacity(max_samples),
1204 timestamps: VecDeque::with_capacity(max_samples),
1205 max_samples,
1206 }
1207 }
1208
1209 pub fn add_sample(&mut self, pos: Vec3, time_ms: f64) {
1210 if self.positions.len() >= self.max_samples {
1211 self.positions.pop_front();
1212 self.timestamps.pop_front();
1213 }
1214 self.positions.push_back(pos);
1215 self.timestamps.push_back(time_ms);
1216 }
1217
1218 pub fn velocity(&self) -> Vec3 {
1219 let n = self.positions.len();
1220 if n < 2 { return Vec3::ZERO; }
1221 let dt = (self.timestamps[n - 1] - self.timestamps[0]) / 1000.0; if dt < 1e-6 { return Vec3::ZERO; }
1223 let dp = self.positions[n - 1] - self.positions[0];
1224 dp / dt as f32
1225 }
1226
1227 pub fn acceleration(&self) -> Vec3 {
1228 let n = self.positions.len();
1229 if n < 3 { return Vec3::ZERO; }
1230 let mid = n / 2;
1231 let dt1 = ((self.timestamps[mid] - self.timestamps[0]) / 1000.0) as f32;
1232 let dt2 = ((self.timestamps[n - 1] - self.timestamps[mid]) / 1000.0) as f32;
1233 if dt1 < 1e-6 || dt2 < 1e-6 { return Vec3::ZERO; }
1234 let v1 = (self.positions[mid] - self.positions[0]) / dt1;
1235 let v2 = (self.positions[n - 1] - self.positions[mid]) / dt2;
1236 let dt = (dt1 + dt2) * 0.5;
1237 (v2 - v1) / dt
1238 }
1239
1240 pub fn predict_position(&self, lookahead_s: f32) -> Vec3 {
1241 if self.positions.is_empty() { return Vec3::ZERO; }
1242 let current = *self.positions.back().unwrap();
1243 let vel = self.velocity();
1244 let acc = self.acceleration();
1245 current + vel * lookahead_s + acc * 0.5 * lookahead_s * lookahead_s
1247 }
1248}
1249
1250#[derive(Debug)]
1251pub struct PrefetchPredictor {
1252 pub camera_tracker: CameraVelocityTracker,
1253 pub lookahead_seconds: f32,
1254 pub prefetch_budget_ratio: f32, }
1256
1257impl PrefetchPredictor {
1258 pub fn new(lookahead_seconds: f32) -> Self {
1259 Self {
1260 camera_tracker: CameraVelocityTracker::new(30),
1261 lookahead_seconds,
1262 prefetch_budget_ratio: 0.2,
1263 }
1264 }
1265
1266 pub fn update(&mut self, camera_pos: Vec3, time_ms: f64) {
1267 self.camera_tracker.add_sample(camera_pos, time_ms);
1268 }
1269
1270 pub fn predicted_camera_pos(&self) -> Vec3 {
1271 self.camera_tracker.predict_position(self.lookahead_seconds)
1272 }
1273
1274 pub fn get_prefetch_candidates<'a>(
1275 &self,
1276 levels: &'a [StreamingLevel],
1277 current_pos: Vec3,
1278 ) -> Vec<u64> {
1279 let predicted = self.predicted_camera_pos();
1280 let mut candidates = Vec::new();
1281 for level in levels {
1282 if level.state != StreamingState::Unloaded { continue; }
1283 if level.bounds.distance_to_point(predicted) < level.load_distance {
1285 if level.bounds.distance_to_point(current_pos) >= level.load_distance {
1287 candidates.push(level.id);
1288 }
1289 }
1290 }
1291 candidates
1292 }
1293}
1294
1295#[derive(Debug)]
1300pub struct MemoryPressureManager {
1301 pub budget_mb: f32,
1302 pub used_mb: f32,
1303 pub eviction_policy: EvictionPolicy,
1304 pub lru_order: VecDeque<u64>, pub access_counts: HashMap<u64, u64>,
1306 pub pressure_threshold: f32,
1307 pub critical_threshold: f32,
1308}
1309
1310impl MemoryPressureManager {
1311 pub fn new(budget_mb: f32) -> Self {
1312 Self {
1313 budget_mb,
1314 used_mb: 0.0,
1315 eviction_policy: EvictionPolicy::Lru,
1316 lru_order: VecDeque::new(),
1317 access_counts: HashMap::new(),
1318 pressure_threshold: 0.8,
1319 critical_threshold: 0.95,
1320 }
1321 }
1322
1323 pub fn is_under_pressure(&self) -> bool {
1324 self.used_mb / self.budget_mb > self.pressure_threshold
1325 }
1326
1327 pub fn is_critical(&self) -> bool {
1328 self.used_mb / self.budget_mb > self.critical_threshold
1329 }
1330
1331 pub fn available_mb(&self) -> f32 {
1332 (self.budget_mb - self.used_mb).max(0.0)
1333 }
1334
1335 pub fn can_load(&self, size_mb: f32) -> bool {
1336 self.used_mb + size_mb <= self.budget_mb
1337 }
1338
1339 pub fn record_access(&mut self, level_id: u64) {
1340 self.lru_order.retain(|&id| id != level_id);
1341 self.lru_order.push_back(level_id);
1342 *self.access_counts.entry(level_id).or_insert(0) += 1;
1343 }
1344
1345 pub fn record_load(&mut self, level_id: u64, size_mb: f32) {
1346 self.used_mb += size_mb;
1347 self.record_access(level_id);
1348 }
1349
1350 pub fn record_unload(&mut self, level_id: u64, size_mb: f32) {
1351 self.used_mb = (self.used_mb - size_mb).max(0.0);
1352 self.lru_order.retain(|&id| id != level_id);
1353 self.access_counts.remove(&level_id);
1354 }
1355
1356 pub fn select_eviction_candidates(&self, needed_mb: f32, levels: &[StreamingLevel]) -> Vec<u64> {
1357 let mut candidates: Vec<u64> = Vec::new();
1358 let mut freed = 0.0f32;
1359
1360 let sortable_levels: Vec<&StreamingLevel> = levels.iter()
1361 .filter(|l| l.state == StreamingState::Loaded && l.persistence != LevelPersistence::AlwaysLoaded)
1362 .collect();
1363
1364 let mut order: Vec<usize> = (0..sortable_levels.len()).collect();
1365
1366 match self.eviction_policy {
1367 EvictionPolicy::Lru => {
1368 order.sort_by_key(|&i| {
1370 self.lru_order.iter().position(|&id| id == sortable_levels[i].id)
1371 .unwrap_or(0)
1372 });
1373 }
1374 EvictionPolicy::Lfu => {
1375 order.sort_by_key(|&i| {
1376 self.access_counts.get(&sortable_levels[i].id).copied().unwrap_or(0)
1377 });
1378 }
1379 EvictionPolicy::Distance => {
1380 order.sort_by(|&a, &b| {
1381 sortable_levels[b].distance_to_camera
1382 .partial_cmp(&sortable_levels[a].distance_to_camera)
1383 .unwrap_or(std::cmp::Ordering::Equal)
1384 });
1385 }
1386 EvictionPolicy::Priority => {
1387 order.sort_by_key(|&i| sortable_levels[i].priority as u8);
1388 }
1389 }
1390
1391 for i in order {
1392 if freed >= needed_mb { break; }
1393 let lvl = sortable_levels[i];
1394 candidates.push(lvl.id);
1395 freed += lvl.memory_footprint_mb;
1396 }
1397
1398 candidates
1399 }
1400
1401 pub fn pressure_ratio(&self) -> f32 {
1402 if self.budget_mb <= 0.0 { return 1.0; }
1403 (self.used_mb / self.budget_mb).clamp(0.0, 1.0)
1404 }
1405}
1406
1407#[derive(Debug, Clone)]
1412pub struct DependencyNode {
1413 pub level_id: u64,
1414 pub dependencies: Vec<u64>,
1415 pub dependents: Vec<u64>,
1416 pub edge_types: HashMap<u64, DependencyEdgeType>,
1417}
1418
1419impl DependencyNode {
1420 pub fn new(level_id: u64) -> Self {
1421 Self {
1422 level_id,
1423 dependencies: Vec::new(),
1424 dependents: Vec::new(),
1425 edge_types: HashMap::new(),
1426 }
1427 }
1428
1429 pub fn add_dependency(&mut self, dep_id: u64, edge_type: DependencyEdgeType) {
1430 if !self.dependencies.contains(&dep_id) {
1431 self.dependencies.push(dep_id);
1432 self.edge_types.insert(dep_id, edge_type);
1433 }
1434 }
1435}
1436
1437#[derive(Debug)]
1438pub struct DependencyGraph {
1439 pub nodes: HashMap<u64, DependencyNode>,
1440}
1441
1442impl DependencyGraph {
1443 pub fn new() -> Self {
1444 Self { nodes: HashMap::new() }
1445 }
1446
1447 pub fn add_level(&mut self, level_id: u64) {
1448 self.nodes.entry(level_id).or_insert_with(|| DependencyNode::new(level_id));
1449 }
1450
1451 pub fn add_dependency(&mut self, from: u64, to: u64, edge_type: DependencyEdgeType) {
1452 self.add_level(from);
1453 self.add_level(to);
1454 if let Some(node) = self.nodes.get_mut(&from) {
1455 node.add_dependency(to, edge_type);
1456 }
1457 if let Some(node) = self.nodes.get_mut(&to) {
1458 if !node.dependents.contains(&from) {
1459 node.dependents.push(from);
1460 }
1461 }
1462 }
1463
1464 pub fn detect_cycles(&self) -> Option<Vec<u64>> {
1466 let mut visited: HashSet<u64> = HashSet::new();
1467 let mut rec_stack: HashSet<u64> = HashSet::new();
1468 let mut path: Vec<u64> = Vec::new();
1469
1470 for &start_id in self.nodes.keys() {
1471 if !visited.contains(&start_id) {
1472 if let Some(cycle) = self.dfs_cycle_detect(start_id, &mut visited, &mut rec_stack, &mut path) {
1473 return Some(cycle);
1474 }
1475 }
1476 }
1477 None
1478 }
1479
1480 fn dfs_cycle_detect(
1481 &self,
1482 node_id: u64,
1483 visited: &mut HashSet<u64>,
1484 rec_stack: &mut HashSet<u64>,
1485 path: &mut Vec<u64>,
1486 ) -> Option<Vec<u64>> {
1487 visited.insert(node_id);
1488 rec_stack.insert(node_id);
1489 path.push(node_id);
1490
1491 if let Some(node) = self.nodes.get(&node_id) {
1492 for &dep in &node.dependencies {
1493 let edge = node.edge_types.get(&dep).copied().unwrap_or(DependencyEdgeType::HardDependency);
1494 if edge == DependencyEdgeType::Optional { continue; }
1495
1496 if !visited.contains(&dep) {
1497 if let Some(cycle) = self.dfs_cycle_detect(dep, visited, rec_stack, path) {
1498 return Some(cycle);
1499 }
1500 } else if rec_stack.contains(&dep) {
1501 if let Some(start) = path.iter().position(|&id| id == dep) {
1503 return Some(path[start..].to_vec());
1504 }
1505 return Some(vec![dep]);
1506 }
1507 }
1508 }
1509
1510 path.pop();
1511 rec_stack.remove(&node_id);
1512 None
1513 }
1514
1515 pub fn topological_sort(&self) -> Result<Vec<u64>, Vec<u64>> {
1517 let mut in_degree: HashMap<u64, usize> = HashMap::new();
1518 for &id in self.nodes.keys() {
1519 in_degree.insert(id, 0);
1520 }
1521 for node in self.nodes.values() {
1522 for &dep in &node.dependencies {
1523 let edge = node.edge_types.get(&dep).copied().unwrap_or(DependencyEdgeType::HardDependency);
1524 if edge != DependencyEdgeType::Optional {
1525 *in_degree.entry(dep).or_insert(0) += 0; *in_degree.entry(node.level_id).or_insert(0) += 1;
1527 }
1528 }
1529 }
1530
1531 let mut queue: VecDeque<u64> = in_degree.iter()
1532 .filter(|(_, &d)| d == 0)
1533 .map(|(&id, _)| id)
1534 .collect();
1535 let mut order = Vec::new();
1536
1537 while let Some(id) = queue.pop_front() {
1538 order.push(id);
1539 if let Some(node) = self.nodes.get(&id) {
1540 for &dependent in &node.dependents {
1541 if let Some(d) = in_degree.get_mut(&dependent) {
1542 *d = d.saturating_sub(1);
1543 if *d == 0 {
1544 queue.push_back(dependent);
1545 }
1546 }
1547 }
1548 }
1549 }
1550
1551 if order.len() != self.nodes.len() {
1552 let remaining: Vec<u64> = in_degree.iter()
1554 .filter(|(_, &d)| d > 0)
1555 .map(|(&id, _)| id)
1556 .collect();
1557 Err(remaining)
1558 } else {
1559 Ok(order)
1560 }
1561 }
1562
1563 pub fn parallel_load_plan(&self) -> Vec<Vec<u64>> {
1565 let mut batches: Vec<Vec<u64>> = Vec::new();
1566 match self.topological_sort() {
1567 Ok(order) => {
1568 let mut loaded: HashSet<u64> = HashSet::new();
1569 let mut remaining: Vec<u64> = order;
1570 while !remaining.is_empty() {
1571 let mut batch: Vec<u64> = Vec::new();
1572 let mut next_remaining: Vec<u64> = Vec::new();
1573 for id in remaining {
1574 let can_load = if let Some(node) = self.nodes.get(&id) {
1575 node.dependencies.iter().all(|dep| {
1576 let edge = node.edge_types.get(dep).copied()
1577 .unwrap_or(DependencyEdgeType::HardDependency);
1578 edge == DependencyEdgeType::Optional || loaded.contains(dep)
1579 })
1580 } else { true };
1581 if can_load {
1582 batch.push(id);
1583 } else {
1584 next_remaining.push(id);
1585 }
1586 }
1587 if batch.is_empty() { break; } for &id in &batch { loaded.insert(id); }
1589 batches.push(batch);
1590 remaining = next_remaining;
1591 }
1592 }
1593 Err(_) => {
1594 batches.push(self.nodes.keys().copied().collect());
1596 }
1597 }
1598 batches
1599 }
1600
1601 pub fn get_all_dependencies(&self, level_id: u64, include_optional: bool) -> HashSet<u64> {
1602 let mut result = HashSet::new();
1603 let mut stack = vec![level_id];
1604 while let Some(id) = stack.pop() {
1605 if result.contains(&id) { continue; }
1606 result.insert(id);
1607 if let Some(node) = self.nodes.get(&id) {
1608 for &dep in &node.dependencies {
1609 let edge = node.edge_types.get(&dep).copied()
1610 .unwrap_or(DependencyEdgeType::HardDependency);
1611 if include_optional || edge != DependencyEdgeType::Optional {
1612 stack.push(dep);
1613 }
1614 }
1615 }
1616 }
1617 result.remove(&level_id);
1618 result
1619 }
1620}
1621
1622#[derive(Debug, Clone)]
1627pub struct Portal {
1628 pub id: u64,
1629 pub sector_a: u64,
1630 pub sector_b: u64,
1631 pub center: Vec3,
1632 pub normal: Vec3,
1633 pub half_extents: Vec2,
1634 pub is_open: bool,
1635 pub transmission: f32, }
1637
1638impl Portal {
1639 pub fn new(id: u64, sector_a: u64, sector_b: u64, center: Vec3, normal: Vec3, half_extents: Vec2) -> Self {
1640 Self {
1641 id,
1642 sector_a,
1643 sector_b,
1644 center,
1645 normal: normal.normalize_or_zero(),
1646 half_extents,
1647 is_open: true,
1648 transmission: 1.0,
1649 }
1650 }
1651
1652 pub fn is_visible_from(&self, camera_pos: Vec3) -> bool {
1653 if !self.is_open { return false; }
1654 let to_portal = (self.center - camera_pos).normalize_or_zero();
1655 self.normal.dot(to_portal) < 0.0
1657 }
1658
1659 pub fn project_to_clip(&self, view_proj: &Mat4) -> Option<[Vec2; 4]> {
1660 let right = Vec3::new(self.normal.z, 0.0, -self.normal.x).normalize_or_zero();
1661 let up = right.cross(self.normal).normalize_or_zero();
1662 let corners = [
1663 self.center + right * self.half_extents.x + up * self.half_extents.y,
1664 self.center - right * self.half_extents.x + up * self.half_extents.y,
1665 self.center - right * self.half_extents.x - up * self.half_extents.y,
1666 self.center + right * self.half_extents.x - up * self.half_extents.y,
1667 ];
1668 let mut result = [[0.0f32; 2]; 4];
1669 for (i, &c) in corners.iter().enumerate() {
1670 let clip = *view_proj * Vec4::new(c.x, c.y, c.z, 1.0);
1671 if clip.w <= 0.0 { return None; }
1672 result[i] = [clip.x / clip.w, clip.y / clip.w];
1673 }
1674 Some(result.map(|[x, y]| Vec2::new(x, y)))
1675 }
1676}
1677
1678#[derive(Debug, Clone)]
1683pub struct Waypoint {
1684 pub id: u64,
1685 pub name: String,
1686 pub position: Vec3,
1687 pub rotation: Quat,
1688 pub tags: Vec<String>,
1689 pub is_spawn_point: bool,
1690 pub spawn_radius: f32,
1691}
1692
1693impl Waypoint {
1694 pub fn new(id: u64, name: String, position: Vec3) -> Self {
1695 Self {
1696 id,
1697 name,
1698 position,
1699 rotation: Quat::IDENTITY,
1700 tags: Vec::new(),
1701 is_spawn_point: false,
1702 spawn_radius: 1.0,
1703 }
1704 }
1705}
1706
1707#[derive(Debug, Clone)]
1708pub struct Sector {
1709 pub id: u64,
1710 pub name: String,
1711 pub bounds: Aabb,
1712 pub portals: Vec<u64>, pub adjacent_sectors: Vec<u64>, pub waypoints: Vec<Waypoint>,
1715 pub level_ids: Vec<u64>,
1716 pub ai_spawn_budget: u32,
1717 pub is_interior: bool,
1718 pub ambient_sound_id: Option<u64>,
1719 pub reverb_preset: u32,
1720 pub transition_type: SectorTransitionType,
1721}
1722
1723impl Sector {
1724 pub fn new(id: u64, name: String, bounds: Aabb) -> Self {
1725 Self {
1726 id,
1727 name,
1728 bounds,
1729 portals: Vec::new(),
1730 adjacent_sectors: Vec::new(),
1731 waypoints: Vec::new(),
1732 level_ids: Vec::new(),
1733 ai_spawn_budget: 10,
1734 is_interior: false,
1735 ambient_sound_id: None,
1736 reverb_preset: 0,
1737 transition_type: SectorTransitionType::Fade,
1738 }
1739 }
1740
1741 pub fn contains_point(&self, p: Vec3) -> bool {
1742 self.bounds.contains_point(p)
1743 }
1744
1745 pub fn nearest_waypoint(&self, pos: Vec3) -> Option<&Waypoint> {
1746 self.waypoints.iter().min_by(|a, b| {
1747 let da = (a.position - pos).length_squared();
1748 let db = (b.position - pos).length_squared();
1749 da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
1750 })
1751 }
1752
1753 pub fn spawn_waypoints(&self) -> Vec<&Waypoint> {
1754 self.waypoints.iter().filter(|w| w.is_spawn_point).collect()
1755 }
1756}
1757
1758#[derive(Debug)]
1759pub struct SectorGraph {
1760 pub sectors: HashMap<u64, Sector>,
1761 pub portals: HashMap<u64, Portal>,
1762 pub current_sector: Option<u64>,
1763}
1764
1765impl SectorGraph {
1766 pub fn new() -> Self {
1767 Self {
1768 sectors: HashMap::new(),
1769 portals: HashMap::new(),
1770 current_sector: None,
1771 }
1772 }
1773
1774 pub fn add_sector(&mut self, sector: Sector) {
1775 self.sectors.insert(sector.id, sector);
1776 }
1777
1778 pub fn add_portal(&mut self, portal: Portal) {
1779 let id = portal.id;
1780 let sa = portal.sector_a;
1781 let sb = portal.sector_b;
1782 self.portals.insert(id, portal);
1783 if let Some(s) = self.sectors.get_mut(&sa) {
1784 if !s.portals.contains(&id) { s.portals.push(id); }
1785 if !s.adjacent_sectors.contains(&sb) { s.adjacent_sectors.push(sb); }
1786 }
1787 if let Some(s) = self.sectors.get_mut(&sb) {
1788 if !s.portals.contains(&id) { s.portals.push(id); }
1789 if !s.adjacent_sectors.contains(&sa) { s.adjacent_sectors.push(sa); }
1790 }
1791 }
1792
1793 pub fn find_sector_at(&self, pos: Vec3) -> Option<u64> {
1794 for (id, sector) in &self.sectors {
1795 if sector.contains_point(pos) {
1796 return Some(*id);
1797 }
1798 }
1799 None
1800 }
1801
1802 pub fn compute_pvs(&self, camera_pos: Vec3, view_proj: &Mat4, max_depth: usize) -> HashSet<u64> {
1804 let start = match self.find_sector_at(camera_pos) {
1805 Some(id) => id,
1806 None => return HashSet::new(),
1807 };
1808
1809 let mut visible = HashSet::new();
1810 visible.insert(start);
1811 let mut queue: VecDeque<(u64, usize)> = VecDeque::new();
1812 queue.push_back((start, 0));
1813
1814 while let Some((sector_id, depth)) = queue.pop_front() {
1815 if depth >= max_depth { continue; }
1816 let portal_ids = if let Some(s) = self.sectors.get(§or_id) {
1817 s.portals.clone()
1818 } else { continue };
1819
1820 for portal_id in portal_ids {
1821 if let Some(portal) = self.portals.get(&portal_id) {
1822 if !portal.is_open { continue; }
1823 if !portal.is_visible_from(camera_pos) { continue; }
1824 let next = if portal.sector_a == sector_id { portal.sector_b } else { portal.sector_a };
1825 if visible.insert(next) {
1826 queue.push_back((next, depth + 1));
1827 }
1828 }
1829 }
1830 }
1831
1832 visible
1833 }
1834
1835 pub fn get_adjacent_level_ids(&self, sector_id: u64) -> Vec<u64> {
1836 let mut result = Vec::new();
1837 if let Some(sector) = self.sectors.get(§or_id) {
1838 for &level_id in §or.level_ids {
1839 result.push(level_id);
1840 }
1841 for &adj_id in §or.adjacent_sectors {
1842 if let Some(adj) = self.sectors.get(&adj_id) {
1843 for &level_id in &adj.level_ids {
1844 if !result.contains(&level_id) {
1845 result.push(level_id);
1846 }
1847 }
1848 }
1849 }
1850 }
1851 result
1852 }
1853}
1854
1855#[derive(Debug, Clone)]
1860pub struct StreamingEvent {
1861 pub timestamp_ms: f64,
1862 pub kind: StreamingEventKind,
1863 pub level_id: u64,
1864 pub level_name: String,
1865 pub data_mb: f32,
1866 pub duration_ms: f32,
1867 pub camera_pos: Vec3,
1868}
1869
1870#[derive(Debug)]
1871pub struct StreamingTimeline {
1872 pub events: VecDeque<StreamingEvent>,
1873 pub capacity: usize,
1874 pub start_time_ms: f64,
1875 pub bandwidth_history: VecDeque<(f64, f32)>, pub memory_history: VecDeque<(f64, f32)>, }
1878
1879impl StreamingTimeline {
1880 pub fn new(capacity: usize) -> Self {
1881 Self {
1882 events: VecDeque::with_capacity(capacity),
1883 capacity,
1884 start_time_ms: 0.0,
1885 bandwidth_history: VecDeque::with_capacity(capacity),
1886 memory_history: VecDeque::with_capacity(capacity),
1887 }
1888 }
1889
1890 pub fn record(&mut self, event: StreamingEvent) {
1891 if self.events.len() >= self.capacity {
1892 self.events.pop_front();
1893 }
1894 self.events.push_back(event);
1895 }
1896
1897 pub fn record_bandwidth(&mut self, time_ms: f64, mb_per_s: f32) {
1898 if self.bandwidth_history.len() >= self.capacity {
1899 self.bandwidth_history.pop_front();
1900 }
1901 self.bandwidth_history.push_back((time_ms, mb_per_s));
1902 }
1903
1904 pub fn record_memory(&mut self, time_ms: f64, mb_used: f32) {
1905 if self.memory_history.len() >= self.capacity {
1906 self.memory_history.pop_front();
1907 }
1908 self.memory_history.push_back((time_ms, mb_used));
1909 }
1910
1911 pub fn events_in_range(&self, start_ms: f64, end_ms: f64) -> Vec<&StreamingEvent> {
1912 self.events.iter()
1913 .filter(|e| e.timestamp_ms >= start_ms && e.timestamp_ms <= end_ms)
1914 .collect()
1915 }
1916
1917 pub fn load_events(&self) -> Vec<&StreamingEvent> {
1918 self.events.iter()
1919 .filter(|e| e.kind == StreamingEventKind::LevelLoadCompleted)
1920 .collect()
1921 }
1922
1923 pub fn unload_events(&self) -> Vec<&StreamingEvent> {
1924 self.events.iter()
1925 .filter(|e| e.kind == StreamingEventKind::LevelUnloadCompleted)
1926 .collect()
1927 }
1928
1929 pub fn total_data_loaded_mb(&self) -> f32 {
1930 self.load_events().iter().map(|e| e.data_mb).sum()
1931 }
1932
1933 pub fn average_load_time_ms(&self) -> f32 {
1934 let loads = self.load_events();
1935 if loads.is_empty() { return 0.0; }
1936 let total: f32 = loads.iter().map(|e| e.duration_ms).sum();
1937 total / loads.len() as f32
1938 }
1939}
1940
1941#[derive(Debug, Clone)]
1946pub struct LodBudgetEntry {
1947 pub level_id: u64,
1948 pub lod_memory_mb: f32, pub streaming_memory_mb: f32, pub lod_level: LodLevel,
1951 pub lod_bias_contribution: f32,
1952}
1953
1954#[derive(Debug)]
1955pub struct CombinedBudgetManager {
1956 pub total_budget_mb: f32,
1957 pub lod_budget_mb: f32,
1958 pub streaming_budget_mb: f32,
1959 pub entries: HashMap<u64, LodBudgetEntry>,
1960}
1961
1962impl CombinedBudgetManager {
1963 pub fn new(total_budget_mb: f32) -> Self {
1964 Self {
1965 total_budget_mb,
1966 lod_budget_mb: total_budget_mb * 0.6,
1967 streaming_budget_mb: total_budget_mb * 0.4,
1968 entries: HashMap::new(),
1969 }
1970 }
1971
1972 pub fn update_entry(&mut self, level_id: u64, lod: LodLevel, lod_mem: f32, stream_mem: f32) {
1973 let entry = self.entries.entry(level_id).or_insert(LodBudgetEntry {
1974 level_id,
1975 lod_memory_mb: 0.0,
1976 streaming_memory_mb: 0.0,
1977 lod_level: LodLevel::Culled,
1978 lod_bias_contribution: 0.0,
1979 });
1980 entry.lod_level = lod;
1981 entry.lod_memory_mb = lod_mem;
1982 entry.streaming_memory_mb = stream_mem;
1983 }
1984
1985 pub fn total_lod_usage_mb(&self) -> f32 {
1986 self.entries.values().map(|e| e.lod_memory_mb).sum()
1987 }
1988
1989 pub fn total_streaming_usage_mb(&self) -> f32 {
1990 self.entries.values().map(|e| e.streaming_memory_mb).sum()
1991 }
1992
1993 pub fn total_usage_mb(&self) -> f32 {
1994 self.total_lod_usage_mb() + self.total_streaming_usage_mb()
1995 }
1996
1997 pub fn available_mb(&self) -> f32 {
1998 (self.total_budget_mb - self.total_usage_mb()).max(0.0)
1999 }
2000
2001 pub fn compute_global_lod_bias(&self) -> f32 {
2002 let pressure = self.total_usage_mb() / self.total_budget_mb;
2003 if pressure < 0.7 { 0.0 }
2004 else if pressure < 0.9 { (pressure - 0.7) / 0.2 * 2.0 }
2005 else { 2.0 + (pressure - 0.9) / 0.1 * 4.0 }
2006 }
2007
2008 pub fn optimal_lod_for_budget(&self, level_id: u64, distance: f32) -> LodLevel {
2009 let bias = self.compute_global_lod_bias();
2010 let adjusted_dist = distance * (1.0 + bias * 0.5);
2011 if adjusted_dist < 100.0 { LodLevel::Lod0 }
2012 else if adjusted_dist < 300.0 { LodLevel::Lod1 }
2013 else if adjusted_dist < 600.0 { LodLevel::Lod2 }
2014 else if adjusted_dist < 1000.0 { LodLevel::Lod3 }
2015 else { LodLevel::Lod4 }
2016 }
2017}
2018
2019#[derive(Debug, Clone)]
2024pub struct DebugDrawCommand {
2025 pub kind: DebugDrawKind,
2026 pub color: Vec4,
2027 pub duration_ms: f32,
2028}
2029
2030#[derive(Debug, Clone)]
2031pub enum DebugDrawKind {
2032 Box { min: Vec3, max: Vec3 },
2033 Sphere { center: Vec3, radius: f32 },
2034 Line { start: Vec3, end: Vec3 },
2035 Text { pos: Vec3, text: String },
2036 Arrow { start: Vec3, end: Vec3 },
2037}
2038
2039#[derive(Debug)]
2040pub struct StreamingDebugVisualizer {
2041 pub overlay: DebugOverlay,
2042 pub draw_commands: Vec<DebugDrawCommand>,
2043 pub heatmap_data: HashMap<CellCoord, f32>, pub visible_sector_ids: HashSet<u64>,
2045 pub show_load_distances: bool,
2046 pub show_memory_usage: bool,
2047 pub show_cell_grid: bool,
2048 pub max_commands: usize,
2049}
2050
2051impl StreamingDebugVisualizer {
2052 pub fn new() -> Self {
2053 Self {
2054 overlay: DebugOverlay::None,
2055 draw_commands: Vec::new(),
2056 heatmap_data: HashMap::new(),
2057 visible_sector_ids: HashSet::new(),
2058 show_load_distances: false,
2059 show_memory_usage: false,
2060 show_cell_grid: false,
2061 max_commands: 4096,
2062 }
2063 }
2064
2065 pub fn clear(&mut self) {
2066 self.draw_commands.clear();
2067 }
2068
2069 fn add_command(&mut self, cmd: DebugDrawCommand) {
2070 if self.draw_commands.len() < self.max_commands {
2071 self.draw_commands.push(cmd);
2072 }
2073 }
2074
2075 pub fn draw_level_bounds(&mut self, level: &StreamingLevel) {
2076 let color = match level.state {
2077 StreamingState::Loaded => Vec4::new(0.0, 1.0, 0.0, 0.5),
2078 StreamingState::Loading => Vec4::new(1.0, 1.0, 0.0, 0.5),
2079 StreamingState::Unloading => Vec4::new(1.0, 0.5, 0.0, 0.5),
2080 StreamingState::Queued => Vec4::new(0.0, 0.5, 1.0, 0.5),
2081 StreamingState::Unloaded => Vec4::new(0.5, 0.5, 0.5, 0.2),
2082 StreamingState::Failed => Vec4::new(1.0, 0.0, 0.0, 0.7),
2083 StreamingState::Evicted => Vec4::new(0.3, 0.0, 0.3, 0.3),
2084 };
2085 self.add_command(DebugDrawCommand {
2086 kind: DebugDrawKind::Box {
2087 min: level.bounds.min,
2088 max: level.bounds.max,
2089 },
2090 color,
2091 duration_ms: 0.0,
2092 });
2093 }
2094
2095 pub fn draw_load_distance_sphere(&mut self, level: &StreamingLevel) {
2096 if !self.show_load_distances { return; }
2097 self.add_command(DebugDrawCommand {
2098 kind: DebugDrawKind::Sphere {
2099 center: level.bounds.center(),
2100 radius: level.load_distance,
2101 },
2102 color: Vec4::new(0.0, 0.8, 0.0, 0.3),
2103 duration_ms: 0.0,
2104 });
2105 self.add_command(DebugDrawCommand {
2106 kind: DebugDrawKind::Sphere {
2107 center: level.bounds.center(),
2108 radius: level.unload_distance,
2109 },
2110 color: Vec4::new(1.0, 0.3, 0.0, 0.2),
2111 duration_ms: 0.0,
2112 });
2113 }
2114
2115 pub fn draw_cell_grid(&mut self, grid: &WorldPartitionGrid, camera_pos: Vec3) {
2116 if !self.show_cell_grid { return; }
2117 let center_cell = grid.world_to_cell(camera_pos);
2118 let r = 5i32;
2119 for dz in -r..=r {
2120 for dx in -r..=r {
2121 let coord = CellCoord::new(center_cell.x + dx, center_cell.y, center_cell.z + dz);
2122 let min = Vec3::new(
2123 grid.origin.x + coord.x as f32 * grid.cell_size,
2124 camera_pos.y - 1.0,
2125 grid.origin.z + coord.z as f32 * grid.cell_size,
2126 );
2127 let max = min + Vec3::new(grid.cell_size, 2.0, grid.cell_size);
2128 let heat = self.heatmap_data.get(&coord).copied().unwrap_or(0.0);
2129 let color = Vec4::new(heat, 1.0 - heat, 0.0, 0.3);
2130 self.add_command(DebugDrawCommand {
2131 kind: DebugDrawKind::Box { min, max },
2132 color,
2133 duration_ms: 0.0,
2134 });
2135 }
2136 }
2137 }
2138
2139 pub fn draw_memory_label(&mut self, level: &StreamingLevel) {
2140 if !self.show_memory_usage { return; }
2141 let text = format!("{:.1}MB / LOD{:?}", level.memory_footprint_mb, level.current_lod);
2142 self.add_command(DebugDrawCommand {
2143 kind: DebugDrawKind::Text {
2144 pos: level.bounds.center() + Vec3::Y * level.bounds.extents().y,
2145 text,
2146 },
2147 color: Vec4::new(1.0, 1.0, 1.0, 1.0),
2148 duration_ms: 0.0,
2149 });
2150 }
2151
2152 pub fn update_heatmap(&mut self, grid: &WorldPartitionGrid) {
2153 self.heatmap_data.clear();
2154 let max_mem = grid.cells.values()
2155 .map(|c| c.memory_used_mb)
2156 .fold(0.01f32, f32::max);
2157 for (coord, cell) in &grid.cells {
2158 let heat = cell.memory_used_mb / max_mem;
2159 self.heatmap_data.insert(*coord, heat.clamp(0.0, 1.0));
2160 }
2161 }
2162
2163 pub fn draw_sector_portals(&mut self, sector_graph: &SectorGraph) {
2164 for portal in sector_graph.portals.values() {
2165 let color = if portal.is_open {
2166 Vec4::new(0.0, 1.0, 1.0, 0.5)
2167 } else {
2168 Vec4::new(1.0, 0.0, 0.0, 0.5)
2169 };
2170 self.add_command(DebugDrawCommand {
2171 kind: DebugDrawKind::Sphere {
2172 center: portal.center,
2173 radius: 0.5,
2174 },
2175 color,
2176 duration_ms: 0.0,
2177 });
2178 }
2179 }
2180}
2181
2182#[derive(Debug, Clone)]
2187pub struct LevelSaveState {
2188 pub level_id: u64,
2189 pub is_active: bool,
2190 pub transform: Mat4,
2191 pub custom_properties: HashMap<String, f32>,
2192 pub save_timestamp: f64,
2193 pub loaded_sub_objects: Vec<u64>,
2194}
2195
2196impl LevelSaveState {
2197 pub fn new(level_id: u64) -> Self {
2198 Self {
2199 level_id,
2200 is_active: false,
2201 transform: Mat4::IDENTITY,
2202 custom_properties: HashMap::new(),
2203 save_timestamp: 0.0,
2204 loaded_sub_objects: Vec::new(),
2205 }
2206 }
2207}
2208
2209#[derive(Debug)]
2210pub struct PersistentLevelManager {
2211 pub base_level_id: u64,
2212 pub save_states: HashMap<u64, LevelSaveState>,
2213 pub transient_level_ids: HashSet<u64>,
2214 pub dynamic_level_ids: HashSet<u64>,
2215}
2216
2217impl PersistentLevelManager {
2218 pub fn new(base_level_id: u64) -> Self {
2219 Self {
2220 base_level_id,
2221 save_states: HashMap::new(),
2222 transient_level_ids: HashSet::new(),
2223 dynamic_level_ids: HashSet::new(),
2224 }
2225 }
2226
2227 pub fn save_level_state(&mut self, level: &StreamingLevel, timestamp: f64) {
2228 let state = self.save_states.entry(level.id).or_insert_with(|| LevelSaveState::new(level.id));
2229 state.is_active = level.state == StreamingState::Loaded;
2230 state.transform = level.transform;
2231 state.save_timestamp = timestamp;
2232 }
2233
2234 pub fn restore_level_state(&self, level_id: u64) -> Option<&LevelSaveState> {
2235 self.save_states.get(&level_id)
2236 }
2237
2238 pub fn mark_transient(&mut self, level_id: u64) {
2239 self.transient_level_ids.insert(level_id);
2240 self.dynamic_level_ids.remove(&level_id);
2241 }
2242
2243 pub fn mark_dynamic(&mut self, level_id: u64) {
2244 self.dynamic_level_ids.insert(level_id);
2245 self.transient_level_ids.remove(&level_id);
2246 }
2247
2248 pub fn get_levels_to_restore(&self) -> Vec<u64> {
2249 self.save_states.iter()
2250 .filter(|(id, state)| {
2251 state.is_active && !self.transient_level_ids.contains(id)
2252 })
2253 .map(|(&id, _)| id)
2254 .collect()
2255 }
2256}
2257
2258#[derive(Debug, Clone)]
2263pub struct SimulationCamera {
2264 pub position: Vec3,
2265 pub direction: Vec3,
2266 pub speed: f32,
2267 pub path: Vec<Vec3>,
2268 pub path_index: usize,
2269 pub loop_path: bool,
2270 pub time_accumulated_s: f32,
2271}
2272
2273impl SimulationCamera {
2274 pub fn new(position: Vec3) -> Self {
2275 Self {
2276 position,
2277 direction: Vec3::NEG_Z,
2278 speed: 10.0,
2279 path: Vec::new(),
2280 path_index: 0,
2281 loop_path: false,
2282 time_accumulated_s: 0.0,
2283 }
2284 }
2285
2286 pub fn update(&mut self, dt_s: f32) {
2287 self.time_accumulated_s += dt_s;
2288 if self.path.is_empty() { return; }
2289 let target = self.path[self.path_index];
2290 let to_target = target - self.position;
2291 let dist = to_target.length();
2292 let step = self.speed * dt_s;
2293 if dist <= step {
2294 self.position = target;
2295 self.path_index += 1;
2296 if self.path_index >= self.path.len() {
2297 if self.loop_path {
2298 self.path_index = 0;
2299 } else {
2300 self.path_index = self.path.len() - 1;
2301 }
2302 }
2303 } else {
2304 self.direction = to_target / dist;
2305 self.position = self.position + self.direction * step;
2306 }
2307 }
2308
2309 pub fn add_waypoint(&mut self, pos: Vec3) {
2310 self.path.push(pos);
2311 }
2312
2313 pub fn reset(&mut self) {
2314 self.path_index = 0;
2315 self.time_accumulated_s = 0.0;
2316 if !self.path.is_empty() {
2317 self.position = self.path[0];
2318 }
2319 }
2320}
2321
2322#[derive(Debug)]
2323pub struct StreamingSimulator {
2324 pub camera: SimulationCamera,
2325 pub simulated_time_ms: f64,
2326 pub simulated_frames: u64,
2327 pub playback_speed: f32,
2328 pub is_running: bool,
2329 pub load_events_simulated: u32,
2330 pub unload_events_simulated: u32,
2331}
2332
2333impl StreamingSimulator {
2334 pub fn new() -> Self {
2335 Self {
2336 camera: SimulationCamera::new(Vec3::ZERO),
2337 simulated_time_ms: 0.0,
2338 simulated_frames: 0,
2339 playback_speed: 1.0,
2340 is_running: false,
2341 load_events_simulated: 0,
2342 unload_events_simulated: 0,
2343 }
2344 }
2345
2346 pub fn tick(&mut self, dt_s: f32) {
2347 if !self.is_running { return; }
2348 let sim_dt = dt_s * self.playback_speed;
2349 self.camera.update(sim_dt);
2350 self.simulated_time_ms += sim_dt as f64 * 1000.0;
2351 self.simulated_frames += 1;
2352 }
2353
2354 pub fn start(&mut self) { self.is_running = true; }
2355 pub fn stop(&mut self) { self.is_running = false; }
2356 pub fn reset(&mut self) {
2357 self.simulated_time_ms = 0.0;
2358 self.simulated_frames = 0;
2359 self.load_events_simulated = 0;
2360 self.unload_events_simulated = 0;
2361 self.camera.reset();
2362 }
2363}
2364
2365#[derive(Debug, Clone)]
2370pub struct BudgetBreakdown {
2371 pub total_budget_mb: f32,
2372 pub used_mb: f32,
2373 pub available_mb: f32,
2374 pub loaded_level_count: usize,
2375 pub loading_level_count: usize,
2376 pub pending_level_count: usize,
2377 pub largest_level_name: String,
2378 pub largest_level_mb: f32,
2379 pub per_category: HashMap<String, f32>,
2380}
2381
2382#[derive(Debug)]
2383pub struct StreamingBudgetTool {
2384 pub breakdown: BudgetBreakdown,
2385 pub history: VecDeque<(f64, f32)>, pub peak_usage_mb: f32,
2387 pub history_capacity: usize,
2388}
2389
2390impl StreamingBudgetTool {
2391 pub fn new(total_budget_mb: f32) -> Self {
2392 Self {
2393 breakdown: BudgetBreakdown {
2394 total_budget_mb,
2395 used_mb: 0.0,
2396 available_mb: total_budget_mb,
2397 loaded_level_count: 0,
2398 loading_level_count: 0,
2399 pending_level_count: 0,
2400 largest_level_name: String::new(),
2401 largest_level_mb: 0.0,
2402 per_category: HashMap::new(),
2403 },
2404 history: VecDeque::with_capacity(512),
2405 peak_usage_mb: 0.0,
2406 history_capacity: 512,
2407 }
2408 }
2409
2410 pub fn update(&mut self, levels: &[StreamingLevel], time_ms: f64) {
2411 let mut used = 0.0f32;
2412 let mut loaded = 0;
2413 let mut loading = 0;
2414 let mut pending = 0;
2415 let mut largest_mb = 0.0f32;
2416 let mut largest_name = String::new();
2417 let mut per_category: HashMap<String, f32> = HashMap::new();
2418
2419 for level in levels {
2420 match level.state {
2421 StreamingState::Loaded => {
2422 loaded += 1;
2423 used += level.memory_footprint_mb;
2424 let cat = format!("{:?}", level.persistence);
2425 *per_category.entry(cat).or_insert(0.0) += level.memory_footprint_mb;
2426 if level.memory_footprint_mb > largest_mb {
2427 largest_mb = level.memory_footprint_mb;
2428 largest_name = level.name.clone();
2429 }
2430 }
2431 StreamingState::Loading => loading += 1,
2432 StreamingState::Queued => pending += 1,
2433 _ => {}
2434 }
2435 }
2436
2437 self.breakdown.used_mb = used;
2438 self.breakdown.available_mb = self.breakdown.total_budget_mb - used;
2439 self.breakdown.loaded_level_count = loaded;
2440 self.breakdown.loading_level_count = loading;
2441 self.breakdown.pending_level_count = pending;
2442 self.breakdown.largest_level_name = largest_name;
2443 self.breakdown.largest_level_mb = largest_mb;
2444 self.breakdown.per_category = per_category;
2445
2446 self.peak_usage_mb = self.peak_usage_mb.max(used);
2447
2448 if self.history.len() >= self.history_capacity {
2449 self.history.pop_front();
2450 }
2451 self.history.push_back((time_ms, used));
2452 }
2453
2454 pub fn usage_percent(&self) -> f32 {
2455 if self.breakdown.total_budget_mb <= 0.0 { return 0.0; }
2456 (self.breakdown.used_mb / self.breakdown.total_budget_mb * 100.0).clamp(0.0, 100.0)
2457 }
2458}
2459
2460#[derive(Debug, Clone)]
2465pub struct MapThumbnail {
2466 pub sector_id: u64,
2467 pub screen_rect: [f32; 4], pub color: Vec4,
2469 pub label: String,
2470 pub is_loaded: bool,
2471 pub memory_mb: f32,
2472}
2473
2474#[derive(Debug)]
2475pub struct MapOverview {
2476 pub world_bounds: Aabb,
2477 pub thumbnails: Vec<MapThumbnail>,
2478 pub camera_pos_normalized: Vec2,
2479 pub zoom: f32,
2480 pub selected_level_id: Option<u64>,
2481 pub hovered_level_id: Option<u64>,
2482 pub filter_state: Option<StreamingState>,
2483 pub show_labels: bool,
2484 pub show_memory: bool,
2485}
2486
2487impl MapOverview {
2488 pub fn new(world_bounds: Aabb) -> Self {
2489 Self {
2490 world_bounds,
2491 thumbnails: Vec::new(),
2492 camera_pos_normalized: Vec2::ZERO,
2493 zoom: 1.0,
2494 selected_level_id: None,
2495 hovered_level_id: None,
2496 filter_state: None,
2497 show_labels: true,
2498 show_memory: false,
2499 }
2500 }
2501
2502 pub fn world_to_map_uv(&self, world_pos: Vec3) -> Vec2 {
2503 let size = self.world_bounds.size();
2504 let rel = world_pos - self.world_bounds.min;
2505 Vec2::new(
2506 rel.x / size.x.max(1.0),
2507 rel.z / size.z.max(1.0),
2508 )
2509 }
2510
2511 pub fn map_uv_to_world(&self, uv: Vec2) -> Vec3 {
2512 let size = self.world_bounds.size();
2513 self.world_bounds.min + Vec3::new(
2514 uv.x * size.x,
2515 0.0,
2516 uv.y * size.z,
2517 )
2518 }
2519
2520 pub fn update_thumbnails(&mut self, levels: &[StreamingLevel]) {
2521 self.thumbnails.clear();
2522 let world_size = self.world_bounds.size();
2523 for level in levels {
2524 let min_uv = self.world_to_map_uv(level.bounds.min);
2525 let max_uv = self.world_to_map_uv(level.bounds.max);
2526 let color = match level.state {
2527 StreamingState::Loaded => Vec4::new(0.2, 0.8, 0.2, 0.7),
2528 StreamingState::Loading => Vec4::new(1.0, 1.0, 0.0, 0.7),
2529 StreamingState::Unloaded => Vec4::new(0.3, 0.3, 0.3, 0.5),
2530 _ => Vec4::new(0.5, 0.5, 0.5, 0.5),
2531 };
2532 self.thumbnails.push(MapThumbnail {
2533 sector_id: level.id,
2534 screen_rect: [min_uv.x, min_uv.y, max_uv.x - min_uv.x, max_uv.y - min_uv.y],
2535 color,
2536 label: level.name.clone(),
2537 is_loaded: level.state == StreamingState::Loaded,
2538 memory_mb: level.memory_footprint_mb,
2539 });
2540 }
2541 }
2542
2543 pub fn set_camera_position(&mut self, world_pos: Vec3) {
2544 self.camera_pos_normalized = self.world_to_map_uv(world_pos);
2545 }
2546
2547 pub fn levels_at_map_uv(&self, uv: Vec2) -> Vec<u64> {
2548 self.thumbnails.iter()
2549 .filter(|t| {
2550 let r = t.screen_rect;
2551 uv.x >= r[0] && uv.x <= r[0] + r[2] && uv.y >= r[1] && uv.y <= r[1] + r[3]
2552 })
2553 .map(|t| t.sector_id)
2554 .collect()
2555 }
2556}
2557
2558#[derive(Debug)]
2563pub struct LevelStreamingEditorConfig {
2564 pub memory_budget_mb: f32,
2565 pub max_concurrent_loads: usize,
2566 pub default_load_distance: f32,
2567 pub default_unload_distance: f32,
2568 pub default_cell_size: f32,
2569 pub eviction_policy: EvictionPolicy,
2570 pub enable_prefetch: bool,
2571 pub prefetch_lookahead_s: f32,
2572 pub enable_frustum_culling: bool,
2573 pub enable_occlusion_culling: bool,
2574 pub debug_overlay: DebugOverlay,
2575 pub simulation_playback_speed: f32,
2576 pub viewport_width: u32,
2577 pub viewport_height: u32,
2578 pub fov_y_rad: f32,
2579}
2580
2581impl Default for LevelStreamingEditorConfig {
2582 fn default() -> Self {
2583 Self {
2584 memory_budget_mb: 1024.0,
2585 max_concurrent_loads: MAX_CONCURRENT_LOADS,
2586 default_load_distance: 1000.0,
2587 default_unload_distance: 1200.0,
2588 default_cell_size: DEFAULT_CELL_SIZE,
2589 eviction_policy: EvictionPolicy::Lru,
2590 enable_prefetch: true,
2591 prefetch_lookahead_s: PREFETCH_LOOKAHEAD_SECONDS,
2592 enable_frustum_culling: true,
2593 enable_occlusion_culling: true,
2594 debug_overlay: DebugOverlay::None,
2595 simulation_playback_speed: 1.0,
2596 viewport_width: 1920,
2597 viewport_height: 1080,
2598 fov_y_rad: std::f32::consts::FRAC_PI_4,
2599 }
2600 }
2601}
2602
2603#[derive(Debug)]
2604pub struct LevelStreamingEditor {
2605 pub levels: HashMap<u64, StreamingLevel>,
2607 pub next_level_id: u64,
2608
2609 pub world_grid: WorldPartitionGrid,
2611
2612 pub load_queue: StreamingLoadQueue,
2614 pub memory_manager: MemoryPressureManager,
2615
2616 pub volumes: HashMap<u64, StreamingVolume>,
2618 pub next_volume_id: u64,
2619
2620 pub prefetcher: PrefetchPredictor,
2622
2623 pub dependency_graph: DependencyGraph,
2625
2626 pub sector_graph: SectorGraph,
2628
2629 pub persistent_manager: PersistentLevelManager,
2631
2632 pub debug_visualizer: StreamingDebugVisualizer,
2634
2635 pub timeline: StreamingTimeline,
2637
2638 pub budget_tool: StreamingBudgetTool,
2640
2641 pub map_overview: MapOverview,
2643
2644 pub hzb: HierarchicalZBuffer,
2646
2647 pub lod_budget: CombinedBudgetManager,
2649
2650 pub simulator: StreamingSimulator,
2652
2653 pub camera_position: Vec3,
2655 pub camera_direction: Vec3,
2656 pub camera_view_proj: Mat4,
2657
2658 pub config: LevelStreamingEditorConfig,
2660
2661 pub current_frame: u64,
2663 pub current_time_ms: f64,
2664
2665 pub stats: StreamingStats,
2667}
2668
2669#[derive(Debug, Default, Clone)]
2670pub struct StreamingStats {
2671 pub total_levels: usize,
2672 pub loaded_levels: usize,
2673 pub loading_levels: usize,
2674 pub queued_levels: usize,
2675 pub unloaded_levels: usize,
2676 pub frustum_culled_levels: usize,
2677 pub occlusion_culled_levels: usize,
2678 pub total_memory_mb: f32,
2679 pub bandwidth_mb_s: f32,
2680 pub frame_load_count: u32,
2681 pub frame_unload_count: u32,
2682 pub prefetch_hits: u32,
2683 pub prefetch_misses: u32,
2684}
2685
2686impl LevelStreamingEditor {
2687 pub fn new(config: LevelStreamingEditorConfig) -> Self {
2688 let budget_mb = config.memory_budget_mb;
2689 let viewport_w = config.viewport_width as usize;
2690 let viewport_h = config.viewport_height as usize;
2691
2692 Self {
2693 levels: HashMap::new(),
2694 next_level_id: 1,
2695 world_grid: WorldPartitionGrid::new(config.default_cell_size, Vec3::ZERO),
2696 load_queue: StreamingLoadQueue::new(config.max_concurrent_loads),
2697 memory_manager: MemoryPressureManager::new(budget_mb),
2698 volumes: HashMap::new(),
2699 next_volume_id: 1,
2700 prefetcher: PrefetchPredictor::new(config.prefetch_lookahead_s),
2701 dependency_graph: DependencyGraph::new(),
2702 sector_graph: SectorGraph::new(),
2703 persistent_manager: PersistentLevelManager::new(0),
2704 debug_visualizer: StreamingDebugVisualizer::new(),
2705 timeline: StreamingTimeline::new(LEVEL_TIMELINE_CAPACITY),
2706 budget_tool: StreamingBudgetTool::new(budget_mb),
2707 map_overview: MapOverview::new(Aabb::new(-Vec3::splat(5000.0), Vec3::splat(5000.0))),
2708 hzb: HierarchicalZBuffer::new(viewport_w / 4, viewport_h / 4),
2709 lod_budget: CombinedBudgetManager::new(budget_mb),
2710 simulator: StreamingSimulator::new(),
2711 camera_position: Vec3::ZERO,
2712 camera_direction: Vec3::NEG_Z,
2713 camera_view_proj: Mat4::IDENTITY,
2714 config,
2715 current_frame: 0,
2716 current_time_ms: 0.0,
2717 stats: StreamingStats::default(),
2718 }
2719 }
2720
2721 pub fn add_level(&mut self, name: String, asset: StreamingLevelAsset, bounds: Aabb) -> u64 {
2722 let id = self.next_level_id;
2723 self.next_level_id += 1;
2724 let bounds_center = bounds.center();
2725 let mut level = StreamingLevel::new(id, name, asset, bounds);
2726 level.load_distance = self.config.default_load_distance;
2727 level.unload_distance = self.config.default_unload_distance;
2728 self.dependency_graph.add_level(id);
2729
2730 let cell = self.world_grid.world_to_cell(bounds_center);
2732 {
2733 let c = self.world_grid.get_or_create_cell(cell);
2734 c.level_ids.push(id);
2735 }
2736
2737 self.levels.insert(id, level);
2738 id
2739 }
2740
2741 pub fn remove_level(&mut self, id: u64) {
2742 if let Some(level) = self.levels.remove(&id) {
2743 let cell = self.world_grid.world_to_cell(level.bounds.center());
2745 if let Some(c) = self.world_grid.cells.get_mut(&cell) {
2746 c.level_ids.retain(|&lid| lid != id);
2747 }
2748 self.dependency_graph.nodes.remove(&id);
2750 }
2751 }
2752
2753 pub fn add_streaming_volume_sphere(&mut self, name: String, center: Vec3, load_r: f32, unload_r: f32) -> u64 {
2754 let id = self.next_volume_id;
2755 self.next_volume_id += 1;
2756 let vol = StreamingVolume::new_sphere(id, name, center, load_r, unload_r);
2757 self.volumes.insert(id, vol);
2758 id
2759 }
2760
2761 pub fn add_streaming_volume_box(&mut self, name: String, transform: Mat4, half_extents: Vec3) -> u64 {
2762 let id = self.next_volume_id;
2763 self.next_volume_id += 1;
2764 let vol = StreamingVolume::new_box(id, name, transform, half_extents);
2765 self.volumes.insert(id, vol);
2766 id
2767 }
2768
2769 pub fn update_camera(&mut self, position: Vec3, direction: Vec3, view_proj: Mat4) {
2770 self.camera_position = position;
2771 self.camera_direction = direction;
2772 self.camera_view_proj = view_proj;
2773 self.prefetcher.update(position, self.current_time_ms);
2774 self.map_overview.set_camera_position(position);
2775 }
2776
2777 pub fn tick(&mut self, dt_s: f32) {
2779 self.current_frame += 1;
2780 self.current_time_ms += dt_s as f64 * 1000.0;
2781
2782 self.simulator.tick(dt_s);
2784 let cam_pos = if self.simulator.is_running {
2785 self.simulator.camera.position
2786 } else {
2787 self.camera_position
2788 };
2789
2790 self.stats.frame_load_count = 0;
2791 self.stats.frame_unload_count = 0;
2792
2793 self.update_distance_and_culling(cam_pos);
2794 self.process_streaming_volumes(cam_pos);
2795 self.update_load_unload_decisions(cam_pos);
2796 self.process_prefetch(cam_pos);
2797 self.process_load_queue();
2798 self.manage_memory_pressure(cam_pos);
2799 self.update_lod_budget(cam_pos);
2800 self.update_debug_visualization();
2801 self.budget_tool.update(&self.levels.values().cloned().collect::<Vec<_>>(), self.current_time_ms);
2802 self.map_overview.update_thumbnails(&self.levels.values().cloned().collect::<Vec<_>>());
2803
2804 self.collect_stats();
2805
2806 self.timeline.record_memory(self.current_time_ms, self.memory_manager.used_mb);
2807 self.timeline.record_bandwidth(
2808 self.current_time_ms,
2809 self.load_queue.estimated_bandwidth_mb_s(),
2810 );
2811 }
2812
2813 fn update_distance_and_culling(&mut self, cam_pos: Vec3) {
2814 let frustum = Frustum::from_view_proj(self.camera_view_proj);
2815 let vp_h = self.config.viewport_height as f32;
2816 let fov = self.config.fov_y_rad;
2817 let mut frustum_culled = 0;
2818 let mut occlusion_culled = 0;
2819
2820 let level_ids: Vec<u64> = self.levels.keys().copied().collect();
2821 for id in level_ids {
2822 if let Some(level) = self.levels.get_mut(&id) {
2823 let dist = level.bounds.distance_to_point(cam_pos);
2824 level.distance_to_camera = dist;
2825 level.screen_size = level.compute_screen_size(cam_pos, fov, vp_h);
2826 level.current_lod = level.compute_lod(dist, level.lod_bias);
2827
2828 if self.config.enable_frustum_culling {
2830 level.is_frustum_culled = !frustum.test_aabb(&level.bounds);
2831 if level.is_frustum_culled { frustum_culled += 1; }
2832 } else {
2833 level.is_frustum_culled = false;
2834 }
2835
2836 if self.config.enable_occlusion_culling && !level.is_frustum_culled {
2838 level.is_occlusion_culled = !self.hzb.test_aabb_visibility(&level.bounds, &self.camera_view_proj);
2839 if level.is_occlusion_culled { occlusion_culled += 1; }
2840 } else {
2841 level.is_occlusion_culled = false;
2842 }
2843
2844 level.is_visible = !level.is_frustum_culled && !level.is_occlusion_culled;
2845 }
2846 }
2847
2848 self.stats.frustum_culled_levels = frustum_culled;
2849 self.stats.occlusion_culled_levels = occlusion_culled;
2850 }
2851
2852 fn process_streaming_volumes(&mut self, cam_pos: Vec3) {
2853 let volume_ids: Vec<u64> = self.volumes.keys().copied().collect();
2854 for vid in volume_ids {
2855 if let Some(vol) = self.volumes.get(&vid) {
2856 if !vol.is_enabled { continue; }
2857 let should_load = vol.should_trigger_load(cam_pos);
2858 let target_ids = vol.target_level_ids.clone();
2859 let importance = vol.importance_weight;
2860 for lid in target_ids {
2861 if let Some(level) = self.levels.get_mut(&lid) {
2862 if should_load && level.state == StreamingState::Unloaded {
2863 level.importance_weight = level.importance_weight.max(importance);
2864 }
2865 }
2866 }
2867 }
2868 }
2869 }
2870
2871 fn update_load_unload_decisions(&mut self, cam_pos: Vec3) {
2872 let level_ids: Vec<u64> = self.levels.keys().copied().collect();
2873 for id in level_ids {
2874 let (should_load, should_unload, dist, importance, name) = {
2875 if let Some(level) = self.levels.get(&id) {
2876 let sl = level.should_load(cam_pos);
2877 let su = level.should_unload(cam_pos);
2878 (sl, su, level.distance_to_camera, level.importance_weight, level.name.clone())
2879 } else { continue }
2880 };
2881
2882 if let Some(level) = self.levels.get_mut(&id) {
2883 match level.state {
2884 StreamingState::Unloaded | StreamingState::Evicted => {
2885 if should_load {
2886 let mem_est = level.memory_estimate_mb();
2887 if self.memory_manager.can_load(mem_est) || level.persistence == LevelPersistence::AlwaysLoaded {
2888 level.state = StreamingState::Queued;
2889 let dist_weight = 1.0 / (dist + 1.0) * importance;
2890 self.load_queue.enqueue(LoadRequest {
2891 level_id: id,
2892 priority: level.priority,
2893 distance_weight: dist_weight,
2894 enqueue_time_ms: self.current_time_ms,
2895 predicted_load_time_ms: level.asset.load_time_estimate_ms,
2896 is_prefetch: false,
2897 });
2898 }
2899 }
2900 }
2901 StreamingState::Loaded => {
2902 if should_unload {
2903 level.state = StreamingState::Unloading;
2904 self.stats.frame_unload_count += 1;
2905 }
2906 }
2907 StreamingState::Unloading => {
2908 let size = level.memory_footprint_mb;
2910 self.memory_manager.record_unload(id, size);
2911 level.state = StreamingState::Unloaded;
2912 level.unload_timestamp_ms = self.current_time_ms;
2913 let event = StreamingEvent {
2914 timestamp_ms: self.current_time_ms,
2915 kind: StreamingEventKind::LevelUnloadCompleted,
2916 level_id: id,
2917 level_name: name.clone(),
2918 data_mb: size,
2919 duration_ms: 16.0, camera_pos: cam_pos,
2921 };
2922 self.timeline.record(event);
2923 }
2924 _ => {}
2925 }
2926 }
2927 }
2928 }
2929
2930 fn process_prefetch(&mut self, cam_pos: Vec3) {
2931 if !self.config.enable_prefetch { return; }
2932 let levels_vec: Vec<StreamingLevel> = self.levels.values().cloned().collect();
2933 let candidates = self.prefetcher.get_prefetch_candidates(&levels_vec, cam_pos);
2934 for lid in candidates {
2935 if let Some(level) = self.levels.get_mut(&lid) {
2936 if level.state == StreamingState::Unloaded {
2937 level.state = StreamingState::Queued;
2938 self.load_queue.enqueue(LoadRequest {
2939 level_id: lid,
2940 priority: LoadPriority::Prefetch,
2941 distance_weight: 0.1,
2942 enqueue_time_ms: self.current_time_ms,
2943 predicted_load_time_ms: level.asset.load_time_estimate_ms,
2944 is_prefetch: true,
2945 });
2946 }
2947 }
2948 }
2949 }
2950
2951 fn process_load_queue(&mut self) {
2952 while let Some(request) = self.load_queue.dequeue_next() {
2953 if let Some(level) = self.levels.get_mut(&request.level_id) {
2954 if level.state == StreamingState::Queued || level.state == StreamingState::Unloaded {
2955 level.state = StreamingState::Loading;
2956 self.stats.frame_load_count += 1;
2957 }
2958 }
2959 }
2960
2961 let in_flight_ids: Vec<u64> = self.load_queue.in_flight.iter().map(|r| r.level_id).collect();
2963 for lid in in_flight_ids {
2964 let (size_bytes, name, time_ms) = {
2966 if let Some(level) = self.levels.get(&lid) {
2967 (level.asset.size_bytes, level.name.clone(), level.asset.load_time_estimate_ms)
2968 } else { continue }
2969 };
2970
2971 self.load_queue.complete_load(lid, size_bytes, time_ms);
2972 let size_mb = size_bytes as f32 / (1024.0 * 1024.0);
2973 self.memory_manager.record_load(lid, size_mb);
2974
2975 if let Some(level) = self.levels.get_mut(&lid) {
2976 level.state = StreamingState::Loaded;
2977 level.memory_footprint_mb = size_mb;
2978 level.load_timestamp_ms = self.current_time_ms;
2979 level.load_count += 1;
2980 }
2981
2982 let event = StreamingEvent {
2983 timestamp_ms: self.current_time_ms,
2984 kind: StreamingEventKind::LevelLoadCompleted,
2985 level_id: lid,
2986 level_name: name,
2987 data_mb: size_mb,
2988 duration_ms: time_ms,
2989 camera_pos: self.camera_position,
2990 };
2991 self.timeline.record(event);
2992 }
2993 }
2994
2995 fn manage_memory_pressure(&mut self, cam_pos: Vec3) {
2996 if !self.memory_manager.is_under_pressure() { return; }
2997
2998 let needed = self.memory_manager.used_mb - self.memory_manager.budget_mb * self.memory_manager.pressure_threshold;
2999 let levels_vec: Vec<StreamingLevel> = self.levels.values().cloned().collect();
3000 let candidates = self.memory_manager.select_eviction_candidates(needed, &levels_vec);
3001
3002 for lid in candidates {
3003 if let Some(level) = self.levels.get_mut(&lid) {
3004 if level.state == StreamingState::Loaded {
3005 let size = level.memory_footprint_mb;
3006 self.memory_manager.record_unload(lid, size);
3007 level.state = StreamingState::Evicted;
3008 }
3009 }
3010 }
3011 }
3012
3013 fn update_lod_budget(&mut self, cam_pos: Vec3) {
3014 let level_ids: Vec<u64> = self.levels.keys().copied().collect();
3015 for id in level_ids {
3016 if let Some(level) = self.levels.get(&id) {
3017 let lod_mem = level.memory_estimate_mb();
3018 let stream_mem = if level.state == StreamingState::Loaded { 5.0 } else { 0.0 };
3019 self.lod_budget.update_entry(id, level.current_lod, lod_mem, stream_mem);
3020 }
3021 }
3022 }
3023
3024 fn update_debug_visualization(&mut self) {
3025 self.debug_visualizer.clear();
3026 match self.config.debug_overlay {
3027 DebugOverlay::None => {}
3028 DebugOverlay::StreamingState => {
3029 for level in self.levels.values() {
3030 self.debug_visualizer.draw_level_bounds(level);
3031 }
3032 }
3033 DebugOverlay::MemoryUsage => {
3034 for level in self.levels.values() {
3035 self.debug_visualizer.draw_level_bounds(level);
3036 self.debug_visualizer.draw_memory_label(level);
3037 }
3038 }
3039 DebugOverlay::LoadDistance => {
3040 for level in self.levels.values() {
3041 self.debug_visualizer.draw_load_distance_sphere(level);
3042 }
3043 }
3044 DebugOverlay::CellGrid => {
3045 self.debug_visualizer.update_heatmap(&self.world_grid);
3046 self.debug_visualizer.draw_cell_grid(&self.world_grid, self.camera_position);
3047 }
3048 DebugOverlay::PriorityHeatmap => {
3049 self.debug_visualizer.update_heatmap(&self.world_grid);
3050 for level in self.levels.values() {
3051 self.debug_visualizer.draw_level_bounds(level);
3052 }
3053 }
3054 _ => {}
3055 }
3056 }
3057
3058 fn collect_stats(&mut self) {
3059 let mut total = 0;
3060 let mut loaded = 0;
3061 let mut loading = 0;
3062 let mut queued = 0;
3063 let mut unloaded = 0;
3064 let mut memory = 0.0f32;
3065
3066 for level in self.levels.values() {
3067 total += 1;
3068 memory += level.memory_footprint_mb;
3069 match level.state {
3070 StreamingState::Loaded => loaded += 1,
3071 StreamingState::Loading => loading += 1,
3072 StreamingState::Queued => queued += 1,
3073 StreamingState::Unloaded | StreamingState::Evicted => unloaded += 1,
3074 _ => {}
3075 }
3076 }
3077
3078 self.stats.total_levels = total;
3079 self.stats.loaded_levels = loaded;
3080 self.stats.loading_levels = loading;
3081 self.stats.queued_levels = queued;
3082 self.stats.unloaded_levels = unloaded;
3083 self.stats.total_memory_mb = memory;
3084 self.stats.bandwidth_mb_s = self.load_queue.estimated_bandwidth_mb_s();
3085 }
3086
3087 pub fn set_debug_overlay(&mut self, overlay: DebugOverlay) {
3088 self.config.debug_overlay = overlay;
3089 self.debug_visualizer.overlay = overlay;
3090 }
3091
3092 pub fn get_level(&self, id: u64) -> Option<&StreamingLevel> {
3093 self.levels.get(&id)
3094 }
3095
3096 pub fn get_level_mut(&mut self, id: u64) -> Option<&mut StreamingLevel> {
3097 self.levels.get_mut(&id)
3098 }
3099
3100 pub fn force_load_level(&mut self, id: u64) {
3101 if let Some(level) = self.levels.get_mut(&id) {
3102 level.state = StreamingState::Queued;
3103 level.priority = LoadPriority::Critical;
3104 let lid = level.id;
3105 let time_ms = level.asset.load_time_estimate_ms;
3106 self.load_queue.enqueue(LoadRequest {
3107 level_id: lid,
3108 priority: LoadPriority::Critical,
3109 distance_weight: 1000.0,
3110 enqueue_time_ms: self.current_time_ms,
3111 predicted_load_time_ms: time_ms,
3112 is_prefetch: false,
3113 });
3114 }
3115 }
3116
3117 pub fn force_unload_level(&mut self, id: u64) {
3118 if let Some(level) = self.levels.get_mut(&id) {
3119 if level.persistence != LevelPersistence::AlwaysLoaded {
3120 level.state = StreamingState::Unloading;
3121 }
3122 }
3123 }
3124
3125 pub fn set_memory_budget(&mut self, budget_mb: f32) {
3126 self.config.memory_budget_mb = budget_mb;
3127 self.memory_manager.budget_mb = budget_mb;
3128 self.budget_tool.breakdown.total_budget_mb = budget_mb;
3129 self.lod_budget.total_budget_mb = budget_mb;
3130 }
3131
3132 pub fn add_level_dependency(&mut self, from: u64, to: u64, edge: DependencyEdgeType) {
3133 self.dependency_graph.add_dependency(from, to, edge);
3134 }
3135
3136 pub fn check_for_circular_dependencies(&self) -> Option<Vec<u64>> {
3137 self.dependency_graph.detect_cycles()
3138 }
3139
3140 pub fn get_load_plan(&self) -> Vec<Vec<u64>> {
3141 self.dependency_graph.parallel_load_plan()
3142 }
3143
3144 pub fn start_simulation(&mut self) {
3145 self.simulator.start();
3146 }
3147
3148 pub fn stop_simulation(&mut self) {
3149 self.simulator.stop();
3150 }
3151
3152 pub fn reset_simulation(&mut self) {
3153 self.simulator.reset();
3154 for level in self.levels.values_mut() {
3156 if level.persistence != LevelPersistence::AlwaysLoaded {
3157 level.state = StreamingState::Unloaded;
3158 level.memory_footprint_mb = 0.0;
3159 }
3160 }
3161 self.memory_manager.used_mb = 0.0;
3162 self.memory_manager.lru_order.clear();
3163 self.load_queue.pending.clear();
3164 self.load_queue.in_flight.clear();
3165 }
3166
3167 pub fn get_world_bounds(&self) -> Aabb {
3168 let mut result = Aabb::new(Vec3::splat(f32::MAX), Vec3::splat(f32::MIN));
3169 for level in self.levels.values() {
3170 result = result.merge(&level.bounds);
3171 }
3172 if result.min.x > result.max.x {
3173 Aabb::new(Vec3::ZERO, Vec3::ZERO)
3174 } else {
3175 result
3176 }
3177 }
3178
3179 pub fn cells_in_camera_radius(&self, radius: f32) -> Vec<CellCoord> {
3180 self.world_grid.get_cells_in_radius(self.camera_position, radius)
3181 }
3182
3183 pub fn query_levels_near(&self, pos: Vec3, radius: f32) -> Vec<u64> {
3184 self.levels.values()
3185 .filter(|l| l.bounds.distance_to_point(pos) <= radius)
3186 .map(|l| l.id)
3187 .collect()
3188 }
3189
3190 pub fn get_streaming_report(&self) -> StreamingReport {
3191 StreamingReport {
3192 total_levels: self.stats.total_levels,
3193 loaded_count: self.stats.loaded_levels,
3194 loading_count: self.stats.loading_levels,
3195 queued_count: self.stats.queued_levels,
3196 total_memory_mb: self.stats.total_memory_mb,
3197 memory_budget_mb: self.config.memory_budget_mb,
3198 bandwidth_mb_s: self.stats.bandwidth_mb_s,
3199 estimated_queue_time_ms: self.load_queue.estimated_remaining_time_ms(),
3200 has_circular_deps: self.dependency_graph.detect_cycles().is_some(),
3201 memory_pressure_ratio: self.memory_manager.pressure_ratio(),
3202 current_sector: self.sector_graph.current_sector,
3203 global_lod_bias: self.lod_budget.compute_global_lod_bias(),
3204 }
3205 }
3206}
3207
3208#[derive(Debug, Clone)]
3209pub struct StreamingReport {
3210 pub total_levels: usize,
3211 pub loaded_count: usize,
3212 pub loading_count: usize,
3213 pub queued_count: usize,
3214 pub total_memory_mb: f32,
3215 pub memory_budget_mb: f32,
3216 pub bandwidth_mb_s: f32,
3217 pub estimated_queue_time_ms: f32,
3218 pub has_circular_deps: bool,
3219 pub memory_pressure_ratio: f32,
3220 pub current_sector: Option<u64>,
3221 pub global_lod_bias: f32,
3222}
3223
3224#[derive(Debug)]
3230pub struct BandwidthEstimator {
3231 pub ema: f32,
3232 pub alpha: f32, pub peak_mb_s: f32,
3234 pub min_mb_s: f32,
3235 pub sample_count: u64,
3236}
3237
3238impl BandwidthEstimator {
3239 pub fn new(initial_estimate_mb_s: f32) -> Self {
3240 Self {
3241 ema: initial_estimate_mb_s,
3242 alpha: 0.1,
3243 peak_mb_s: initial_estimate_mb_s,
3244 min_mb_s: initial_estimate_mb_s,
3245 sample_count: 0,
3246 }
3247 }
3248
3249 pub fn add_sample(&mut self, mb_per_s: f32) {
3250 self.ema = self.alpha * mb_per_s + (1.0 - self.alpha) * self.ema;
3251 self.peak_mb_s = self.peak_mb_s.max(mb_per_s);
3252 self.min_mb_s = if self.sample_count == 0 { mb_per_s } else { self.min_mb_s.min(mb_per_s) };
3253 self.sample_count += 1;
3254 }
3255
3256 pub fn estimate(&self) -> f32 { self.ema }
3257
3258 pub fn estimated_load_time_ms(&self, size_mb: f32) -> f32 {
3259 if self.ema <= 0.0 { return f32::MAX; }
3260 size_mb / self.ema * 1000.0
3261 }
3262
3263 pub fn variance_adjusted_estimate(&self, size_mb: f32, confidence: f32) -> f32 {
3264 let rate = if confidence > 0.9 {
3266 self.min_mb_s.max(self.ema * 0.5)
3267 } else {
3268 self.ema
3269 };
3270 if rate <= 0.0 { return f32::MAX; }
3271 size_mb / rate * 1000.0
3272 }
3273}
3274
3275#[derive(Debug)]
3277pub struct OcclusionCache {
3278 pub results: HashMap<u64, (bool, u64)>, pub cache_lifetime_frames: u64,
3280}
3281
3282impl OcclusionCache {
3283 pub fn new(lifetime_frames: u64) -> Self {
3284 Self {
3285 results: HashMap::new(),
3286 cache_lifetime_frames: lifetime_frames,
3287 }
3288 }
3289
3290 pub fn get(&self, level_id: u64, current_frame: u64) -> Option<bool> {
3291 if let Some(&(visible, frame)) = self.results.get(&level_id) {
3292 if current_frame - frame <= self.cache_lifetime_frames {
3293 return Some(visible);
3294 }
3295 }
3296 None
3297 }
3298
3299 pub fn set(&mut self, level_id: u64, visible: bool, frame: u64) {
3300 self.results.insert(level_id, (visible, frame));
3301 }
3302
3303 pub fn evict_stale(&mut self, current_frame: u64) {
3304 self.results.retain(|_, (_, frame)| {
3305 current_frame - *frame <= self.cache_lifetime_frames
3306 });
3307 }
3308}
3309
3310#[derive(Debug)]
3312pub struct WorldStreamingBandwidthTracker {
3313 pub estimator: BandwidthEstimator,
3314 pub frame_data: VecDeque<(u64, f32)>, pub total_mb_streamed: f32,
3316 pub peak_frame_mb: f32,
3317}
3318
3319impl WorldStreamingBandwidthTracker {
3320 pub fn new() -> Self {
3321 Self {
3322 estimator: BandwidthEstimator::new(50.0),
3323 frame_data: VecDeque::with_capacity(256),
3324 total_mb_streamed: 0.0,
3325 peak_frame_mb: 0.0,
3326 }
3327 }
3328
3329 pub fn record_frame(&mut self, frame: u64, mb_this_frame: f32, dt_s: f32) {
3330 if self.frame_data.len() >= 256 { self.frame_data.pop_front(); }
3331 self.frame_data.push_back((frame, mb_this_frame));
3332 self.total_mb_streamed += mb_this_frame;
3333 self.peak_frame_mb = self.peak_frame_mb.max(mb_this_frame);
3334 if dt_s > 1e-6 {
3335 self.estimator.add_sample(mb_this_frame / dt_s);
3336 }
3337 }
3338
3339 pub fn average_mb_per_frame(&self) -> f32 {
3340 if self.frame_data.is_empty() { return 0.0; }
3341 let total: f32 = self.frame_data.iter().map(|(_, mb)| mb).sum();
3342 total / self.frame_data.len() as f32
3343 }
3344}
3345
3346#[derive(Debug)]
3348pub struct LevelStreamingProfiler {
3349 pub frame_timings: VecDeque<f32>, pub load_timings: HashMap<u64, f32>, pub queue_depth_history: VecDeque<usize>,
3352 pub memory_history_full: VecDeque<f32>,
3353 pub bandwidth_tracker: WorldStreamingBandwidthTracker,
3354 pub total_frames_profiled: u64,
3355}
3356
3357impl LevelStreamingProfiler {
3358 pub fn new() -> Self {
3359 Self {
3360 frame_timings: VecDeque::with_capacity(256),
3361 load_timings: HashMap::new(),
3362 queue_depth_history: VecDeque::with_capacity(256),
3363 memory_history_full: VecDeque::with_capacity(256),
3364 bandwidth_tracker: WorldStreamingBandwidthTracker::new(),
3365 total_frames_profiled: 0,
3366 }
3367 }
3368
3369 pub fn record_frame(&mut self, timing_ms: f32, queue_depth: usize, memory_mb: f32, streamed_mb: f32, dt_s: f32) {
3370 if self.frame_timings.len() >= 256 { self.frame_timings.pop_front(); }
3371 if self.queue_depth_history.len() >= 256 { self.queue_depth_history.pop_front(); }
3372 if self.memory_history_full.len() >= 256 { self.memory_history_full.pop_front(); }
3373
3374 self.frame_timings.push_back(timing_ms);
3375 self.queue_depth_history.push_back(queue_depth);
3376 self.memory_history_full.push_back(memory_mb);
3377 self.bandwidth_tracker.record_frame(self.total_frames_profiled, streamed_mb, dt_s);
3378 self.total_frames_profiled += 1;
3379 }
3380
3381 pub fn average_frame_time_ms(&self) -> f32 {
3382 if self.frame_timings.is_empty() { return 0.0; }
3383 self.frame_timings.iter().sum::<f32>() / self.frame_timings.len() as f32
3384 }
3385
3386 pub fn p99_frame_time_ms(&self) -> f32 {
3387 let mut sorted: Vec<f32> = self.frame_timings.iter().copied().collect();
3388 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
3389 let idx = (sorted.len() as f32 * 0.99) as usize;
3390 sorted.get(idx).copied().unwrap_or(0.0)
3391 }
3392
3393 pub fn max_queue_depth(&self) -> usize {
3394 self.queue_depth_history.iter().copied().max().unwrap_or(0)
3395 }
3396
3397 pub fn peak_memory_mb(&self) -> f32 {
3398 self.memory_history_full.iter().copied().fold(0.0f32, f32::max)
3399 }
3400}
3401
3402#[derive(Debug, Clone)]
3407pub struct LevelInstance {
3408 pub instance_id: u64,
3409 pub level_id: u64,
3410 pub transform: Mat4,
3411 pub override_lod: Option<LodLevel>,
3412 pub visible: bool,
3413 pub cast_shadows: bool,
3414 pub custom_culling_distance: Option<f32>,
3415 pub tags: HashSet<String>,
3416 pub metadata: HashMap<String, String>,
3417 pub creation_time: f64,
3418 pub last_modified_time: f64,
3419 pub override_load_distance: Option<f32>,
3420 pub override_priority: Option<LoadPriority>,
3421}
3422
3423impl LevelInstance {
3424 pub fn new(instance_id: u64, level_id: u64, transform: Mat4) -> Self {
3425 Self {
3426 instance_id,
3427 level_id,
3428 transform,
3429 override_lod: None,
3430 visible: true,
3431 cast_shadows: true,
3432 custom_culling_distance: None,
3433 tags: HashSet::new(),
3434 metadata: HashMap::new(),
3435 creation_time: 0.0,
3436 last_modified_time: 0.0,
3437 override_load_distance: None,
3438 override_priority: None,
3439 }
3440 }
3441
3442 pub fn world_position(&self) -> Vec3 {
3443 self.transform.transform_point3(Vec3::ZERO)
3444 }
3445
3446 pub fn position(&self) -> Vec3 {
3447 Vec3::new(self.transform.w_axis.x, self.transform.w_axis.y, self.transform.w_axis.z)
3448 }
3449
3450 pub fn rotation_quat(&self) -> Quat {
3451 let m = &self.transform;
3452 let sx = Vec3::new(m.x_axis.x, m.x_axis.y, m.x_axis.z).length();
3453 let sy = Vec3::new(m.y_axis.x, m.y_axis.y, m.y_axis.z).length();
3454 let sz = Vec3::new(m.z_axis.x, m.z_axis.y, m.z_axis.z).length();
3455 let rm = Mat4::from_cols(
3456 m.x_axis / sx,
3457 m.y_axis / sy,
3458 m.z_axis / sz,
3459 Vec4::W,
3460 );
3461 Quat::from_mat4(&rm)
3462 }
3463
3464 pub fn scale(&self) -> Vec3 {
3465 let m = &self.transform;
3466 Vec3::new(
3467 Vec3::new(m.x_axis.x, m.x_axis.y, m.x_axis.z).length(),
3468 Vec3::new(m.y_axis.x, m.y_axis.y, m.y_axis.z).length(),
3469 Vec3::new(m.z_axis.x, m.z_axis.y, m.z_axis.z).length(),
3470 )
3471 }
3472
3473 pub fn add_tag(&mut self, tag: &str) {
3474 self.tags.insert(tag.to_string());
3475 }
3476
3477 pub fn has_tag(&self, tag: &str) -> bool {
3478 self.tags.contains(tag)
3479 }
3480
3481 pub fn set_metadata(&mut self, key: &str, value: &str) {
3482 self.metadata.insert(key.to_string(), value.to_string());
3483 self.last_modified_time += 0.001;
3484 }
3485}
3486
3487#[derive(Debug)]
3489pub struct LevelInstanceManager {
3490 pub instances: HashMap<u64, LevelInstance>,
3491 pub next_instance_id: u64,
3492 pub instance_to_base: HashMap<u64, u64>, pub base_to_instances: HashMap<u64, Vec<u64>>, }
3495
3496impl LevelInstanceManager {
3497 pub fn new() -> Self {
3498 Self {
3499 instances: HashMap::new(),
3500 next_instance_id: 1,
3501 instance_to_base: HashMap::new(),
3502 base_to_instances: HashMap::new(),
3503 }
3504 }
3505
3506 pub fn instantiate(&mut self, level_id: u64, transform: Mat4) -> u64 {
3507 let id = self.next_instance_id;
3508 self.next_instance_id += 1;
3509 self.instances.insert(id, LevelInstance::new(id, level_id, transform));
3510 self.instance_to_base.insert(id, level_id);
3511 self.base_to_instances.entry(level_id).or_default().push(id);
3512 id
3513 }
3514
3515 pub fn remove_instance(&mut self, instance_id: u64) {
3516 if let Some(inst) = self.instances.remove(&instance_id) {
3517 self.instance_to_base.remove(&instance_id);
3518 if let Some(list) = self.base_to_instances.get_mut(&inst.level_id) {
3519 list.retain(|&id| id != instance_id);
3520 }
3521 }
3522 }
3523
3524 pub fn get_instances_for_level(&self, level_id: u64) -> Vec<&LevelInstance> {
3525 self.base_to_instances.get(&level_id)
3526 .map(|ids| ids.iter().filter_map(|id| self.instances.get(id)).collect())
3527 .unwrap_or_default()
3528 }
3529
3530 pub fn instances_near(&self, pos: Vec3, radius: f32) -> Vec<u64> {
3531 self.instances.values()
3532 .filter(|inst| {
3533 let wp = inst.world_position();
3534 (wp - pos).length() <= radius
3535 })
3536 .map(|inst| inst.instance_id)
3537 .collect()
3538 }
3539
3540 pub fn active_instance_count(&self) -> usize {
3541 self.instances.values().filter(|i| i.visible).count()
3542 }
3543}
3544
3545#[derive(Debug, Clone, PartialEq, Eq)]
3550pub enum EditorPanel {
3551 MapOverview,
3552 LevelList,
3553 BudgetTool,
3554 Timeline,
3555 Settings,
3556 DependencyGraph,
3557 SectorEditor,
3558 SimulationControl,
3559 Profiler,
3560}
3561
3562#[derive(Debug)]
3563pub struct LevelStreamingEditorUiState {
3564 pub active_panel: EditorPanel,
3565 pub selected_level_ids: HashSet<u64>,
3566 pub search_filter: String,
3567 pub state_filter: Option<StreamingState>,
3568 pub sort_by_distance: bool,
3569 pub sort_by_memory: bool,
3570 pub show_only_visible: bool,
3571 pub timeline_scroll_x: f32,
3572 pub timeline_zoom: f32,
3573 pub map_scroll: Vec2,
3574 pub map_zoom: f32,
3575 pub is_editing_volume: bool,
3576 pub editing_volume_id: Option<u64>,
3577 pub simulation_panel_open: bool,
3578 pub budget_panel_open: bool,
3579 pub dep_graph_panel_open: bool,
3580}
3581
3582impl LevelStreamingEditorUiState {
3583 pub fn new() -> Self {
3584 Self {
3585 active_panel: EditorPanel::MapOverview,
3586 selected_level_ids: HashSet::new(),
3587 search_filter: String::new(),
3588 state_filter: None,
3589 sort_by_distance: false,
3590 sort_by_memory: false,
3591 show_only_visible: false,
3592 timeline_scroll_x: 0.0,
3593 timeline_zoom: 1.0,
3594 map_scroll: Vec2::ZERO,
3595 map_zoom: 1.0,
3596 is_editing_volume: false,
3597 editing_volume_id: None,
3598 simulation_panel_open: false,
3599 budget_panel_open: false,
3600 dep_graph_panel_open: false,
3601 }
3602 }
3603
3604 pub fn select_level(&mut self, id: u64, multi_select: bool) {
3605 if !multi_select {
3606 self.selected_level_ids.clear();
3607 }
3608 self.selected_level_ids.insert(id);
3609 }
3610
3611 pub fn deselect_level(&mut self, id: u64) {
3612 self.selected_level_ids.remove(&id);
3613 }
3614
3615 pub fn is_level_selected(&self, id: u64) -> bool {
3616 self.selected_level_ids.contains(&id)
3617 }
3618
3619 pub fn filtered_levels<'a>(&'a self, levels: &'a [StreamingLevel]) -> Vec<&'a StreamingLevel> {
3620 let mut result: Vec<&StreamingLevel> = levels.iter()
3621 .filter(|l| {
3622 let name_match = self.search_filter.is_empty()
3623 || l.name.to_lowercase().contains(&self.search_filter.to_lowercase());
3624 let state_match = self.state_filter.map_or(true, |s| l.state == s);
3625 let visibility_match = !self.show_only_visible || l.is_visible;
3626 name_match && state_match && visibility_match
3627 })
3628 .collect();
3629
3630 if self.sort_by_distance {
3631 result.sort_by(|a, b| a.distance_to_camera.partial_cmp(&b.distance_to_camera)
3632 .unwrap_or(std::cmp::Ordering::Equal));
3633 } else if self.sort_by_memory {
3634 result.sort_by(|a, b| b.memory_footprint_mb.partial_cmp(&a.memory_footprint_mb)
3635 .unwrap_or(std::cmp::Ordering::Equal));
3636 }
3637
3638 result
3639 }
3640}
3641
3642#[derive(Debug)]
3647pub struct SpatialHashGrid {
3648 pub bucket_size: f32,
3649 pub buckets: HashMap<(i32, i32, i32), Vec<u64>>,
3650}
3651
3652impl SpatialHashGrid {
3653 pub fn new(bucket_size: f32) -> Self {
3654 Self { bucket_size, buckets: HashMap::new() }
3655 }
3656
3657 fn hash_pos(&self, pos: Vec3) -> (i32, i32, i32) {
3658 (
3659 (pos.x / self.bucket_size).floor() as i32,
3660 (pos.y / self.bucket_size).floor() as i32,
3661 (pos.z / self.bucket_size).floor() as i32,
3662 )
3663 }
3664
3665 pub fn insert(&mut self, id: u64, pos: Vec3) {
3666 let h = self.hash_pos(pos);
3667 self.buckets.entry(h).or_default().push(id);
3668 }
3669
3670 pub fn remove(&mut self, id: u64, pos: Vec3) {
3671 let h = self.hash_pos(pos);
3672 if let Some(v) = self.buckets.get_mut(&h) {
3673 v.retain(|&i| i != id);
3674 }
3675 }
3676
3677 pub fn query_radius(&self, pos: Vec3, radius: f32) -> Vec<u64> {
3678 let r = (radius / self.bucket_size).ceil() as i32 + 1;
3679 let h = self.hash_pos(pos);
3680 let mut result = Vec::new();
3681 for dz in -r..=r {
3682 for dy in -r..=r {
3683 for dx in -r..=r {
3684 let key = (h.0 + dx, h.1 + dy, h.2 + dz);
3685 if let Some(v) = self.buckets.get(&key) {
3686 result.extend_from_slice(v);
3687 }
3688 }
3689 }
3690 }
3691 result
3692 }
3693
3694 pub fn clear(&mut self) {
3695 self.buckets.clear();
3696 }
3697
3698 pub fn total_entries(&self) -> usize {
3699 self.buckets.values().map(|v| v.len()).sum()
3700 }
3701}
3702
3703#[derive(Debug, Clone)]
3708pub struct LevelTransition {
3709 pub from_level_id: u64,
3710 pub to_level_id: u64,
3711 pub transition_type: SectorTransitionType,
3712 pub progress: f32, pub duration_s: f32,
3714 pub is_complete: bool,
3715}
3716
3717impl LevelTransition {
3718 pub fn new(from: u64, to: u64, transition_type: SectorTransitionType, duration_s: f32) -> Self {
3719 Self {
3720 from_level_id: from,
3721 to_level_id: to,
3722 transition_type,
3723 progress: 0.0,
3724 duration_s,
3725 is_complete: false,
3726 }
3727 }
3728
3729 pub fn update(&mut self, dt_s: f32) {
3730 if self.is_complete { return; }
3731 self.progress += dt_s / self.duration_s.max(0.001);
3732 if self.progress >= 1.0 {
3733 self.progress = 1.0;
3734 self.is_complete = true;
3735 }
3736 }
3737
3738 pub fn fade_alpha(&self) -> f32 {
3739 match self.transition_type {
3740 SectorTransitionType::Fade => {
3741 if self.progress < 0.5 {
3742 self.progress * 2.0
3743 } else {
3744 (1.0 - self.progress) * 2.0
3745 }
3746 }
3747 SectorTransitionType::Immediate => 0.0,
3748 _ => 1.0 - self.progress,
3749 }
3750 }
3751
3752 pub fn smoothed_progress(&self) -> f32 {
3753 let t = self.progress;
3755 t * t * (3.0 - 2.0 * t)
3756 }
3757}
3758
3759#[derive(Debug)]
3760pub struct TransitionController {
3761 pub active_transitions: Vec<LevelTransition>,
3762 pub completed_transitions: VecDeque<LevelTransition>,
3763}
3764
3765impl TransitionController {
3766 pub fn new() -> Self {
3767 Self {
3768 active_transitions: Vec::new(),
3769 completed_transitions: VecDeque::new(),
3770 }
3771 }
3772
3773 pub fn begin_transition(&mut self, from: u64, to: u64, t: SectorTransitionType, duration_s: f32) {
3774 self.active_transitions.push(LevelTransition::new(from, to, t, duration_s));
3775 }
3776
3777 pub fn update(&mut self, dt_s: f32) {
3778 let mut to_complete = Vec::new();
3779 for (i, t) in self.active_transitions.iter_mut().enumerate() {
3780 t.update(dt_s);
3781 if t.is_complete { to_complete.push(i); }
3782 }
3783 for i in to_complete.into_iter().rev() {
3784 let t = self.active_transitions.remove(i);
3785 if self.completed_transitions.len() >= 64 { self.completed_transitions.pop_front(); }
3786 self.completed_transitions.push_back(t);
3787 }
3788 }
3789
3790 pub fn is_transitioning(&self) -> bool {
3791 !self.active_transitions.is_empty()
3792 }
3793
3794 pub fn get_fade_alpha(&self, level_id: u64) -> f32 {
3795 let mut alpha = 1.0f32;
3796 for t in &self.active_transitions {
3797 if t.from_level_id == level_id || t.to_level_id == level_id {
3798 alpha = alpha.min(1.0 - t.fade_alpha());
3799 }
3800 }
3801 alpha
3802 }
3803}
3804
3805#[derive(Debug)]
3810pub struct CullingManager {
3811 pub frustum: Frustum,
3812 pub hzb: HierarchicalZBuffer,
3813 pub occlusion_cache: OcclusionCache,
3814 pub total_tested: u64,
3815 pub total_culled_frustum: u64,
3816 pub total_culled_occlusion: u64,
3817 pub total_passed: u64,
3818}
3819
3820impl CullingManager {
3821 pub fn new(vp_width: usize, vp_height: usize) -> Self {
3822 Self {
3823 frustum: Frustum::from_view_proj(Mat4::IDENTITY),
3824 hzb: HierarchicalZBuffer::new(vp_width / 4, vp_height / 4),
3825 occlusion_cache: OcclusionCache::new(4),
3826 total_tested: 0,
3827 total_culled_frustum: 0,
3828 total_culled_occlusion: 0,
3829 total_passed: 0,
3830 }
3831 }
3832
3833 pub fn update_frustum(&mut self, view_proj: Mat4) {
3834 self.frustum = Frustum::from_view_proj(view_proj);
3835 }
3836
3837 pub fn update_hzb(&mut self, depth_buffer: &[f32]) {
3838 self.hzb.build_from_depth(depth_buffer);
3839 }
3840
3841 pub fn test_level(&mut self, level: &StreamingLevel, view_proj: &Mat4, frame: u64) -> bool {
3842 self.total_tested += 1;
3843
3844 if !self.frustum.test_aabb(&level.bounds) {
3846 self.total_culled_frustum += 1;
3847 return false;
3848 }
3849
3850 if let Some(cached_visible) = self.occlusion_cache.get(level.id, frame) {
3852 if !cached_visible {
3853 self.total_culled_occlusion += 1;
3854 return false;
3855 }
3856 self.total_passed += 1;
3857 return true;
3858 }
3859
3860 let hzb_visible = self.hzb.test_aabb_visibility(&level.bounds, view_proj);
3862 self.occlusion_cache.set(level.id, hzb_visible, frame);
3863
3864 if !hzb_visible {
3865 self.total_culled_occlusion += 1;
3866 return false;
3867 }
3868
3869 self.total_passed += 1;
3870 true
3871 }
3872
3873 pub fn cull_efficiency(&self) -> f32 {
3874 if self.total_tested == 0 { return 0.0; }
3875 (self.total_culled_frustum + self.total_culled_occlusion) as f32 / self.total_tested as f32
3876 }
3877
3878 pub fn reset_stats(&mut self) {
3879 self.total_tested = 0;
3880 self.total_culled_frustum = 0;
3881 self.total_culled_occlusion = 0;
3882 self.total_passed = 0;
3883 }
3884}
3885
3886#[derive(Debug)]
3892pub struct FullLevelStreamingEditor {
3893 pub core: LevelStreamingEditor,
3894 pub instance_manager: LevelInstanceManager,
3895 pub ui_state: LevelStreamingEditorUiState,
3896 pub profiler: LevelStreamingProfiler,
3897 pub transition_controller: TransitionController,
3898 pub culling_manager: CullingManager,
3899 pub spatial_hash: SpatialHashGrid,
3900 pub bandwidth_estimator: BandwidthEstimator,
3901 pub occlusion_cache_ext: OcclusionCache,
3902}
3903
3904impl FullLevelStreamingEditor {
3905 pub fn new(config: LevelStreamingEditorConfig) -> Self {
3906 let vp_w = config.viewport_width as usize;
3907 let vp_h = config.viewport_height as usize;
3908 Self {
3909 core: LevelStreamingEditor::new(config),
3910 instance_manager: LevelInstanceManager::new(),
3911 ui_state: LevelStreamingEditorUiState::new(),
3912 profiler: LevelStreamingProfiler::new(),
3913 transition_controller: TransitionController::new(),
3914 culling_manager: CullingManager::new(vp_w, vp_h),
3915 spatial_hash: SpatialHashGrid::new(DEFAULT_CELL_SIZE),
3916 bandwidth_estimator: BandwidthEstimator::new(50.0),
3917 occlusion_cache_ext: OcclusionCache::new(8),
3918 }
3919 }
3920
3921 pub fn tick(&mut self, dt_s: f32) {
3922 let start_frame = self.core.current_frame;
3923 self.core.tick(dt_s);
3924 self.transition_controller.update(dt_s);
3925 self.culling_manager.update_frustum(self.core.camera_view_proj);
3926 self.occlusion_cache_ext.evict_stale(self.core.current_frame);
3927
3928 let queue_depth = self.core.load_queue.queue_depth();
3929 let memory_mb = self.core.memory_manager.used_mb;
3930 self.profiler.record_frame(dt_s * 1000.0, queue_depth, memory_mb, 0.0, dt_s);
3931 }
3932
3933 pub fn add_level_with_instance(
3934 &mut self,
3935 name: String,
3936 asset: StreamingLevelAsset,
3937 bounds: Aabb,
3938 transform: Mat4,
3939 ) -> (u64, u64) {
3940 let level_id = self.core.add_level(name, asset, bounds);
3941 let inst_id = self.instance_manager.instantiate(level_id, transform);
3942 (level_id, inst_id)
3943 }
3944
3945 pub fn select_level_in_ui(&mut self, id: u64, multi: bool) {
3946 self.ui_state.select_level(id, multi);
3947 }
3948
3949 pub fn set_active_panel(&mut self, panel: EditorPanel) {
3950 self.ui_state.active_panel = panel;
3951 }
3952
3953 pub fn get_profiler_summary(&self) -> String {
3954 format!(
3955 "Avg frame: {:.2}ms | P99: {:.2}ms | Peak mem: {:.1}MB | Max queue: {}",
3956 self.profiler.average_frame_time_ms(),
3957 self.profiler.p99_frame_time_ms(),
3958 self.profiler.peak_memory_mb(),
3959 self.profiler.max_queue_depth(),
3960 )
3961 }
3962
3963 pub fn transition_to_sector(&mut self, from_level: u64, to_level: u64, transition: SectorTransitionType) {
3964 let duration = match transition {
3965 SectorTransitionType::Immediate => 0.0,
3966 SectorTransitionType::Fade => 1.0,
3967 SectorTransitionType::Portal => 0.5,
3968 SectorTransitionType::Teleport => 0.2,
3969 };
3970 self.transition_controller.begin_transition(from_level, to_level, transition, duration);
3971 self.core.force_load_level(to_level);
3972 }
3973
3974 pub fn rebuild_spatial_hash(&mut self) {
3975 self.spatial_hash.clear();
3976 for level in self.core.levels.values() {
3977 self.spatial_hash.insert(level.id, level.bounds.center());
3978 }
3979 }
3980
3981 pub fn query_levels_frustum_culled(&self) -> Vec<u64> {
3982 self.core.levels.values()
3983 .filter(|l| l.is_frustum_culled)
3984 .map(|l| l.id)
3985 .collect()
3986 }
3987
3988 pub fn query_levels_occlusion_culled(&self) -> Vec<u64> {
3989 self.core.levels.values()
3990 .filter(|l| l.is_occlusion_culled)
3991 .map(|l| l.id)
3992 .collect()
3993 }
3994
3995 pub fn estimate_load_order_time_ms(&self) -> f32 {
3996 let plan = self.core.dependency_graph.parallel_load_plan();
3997 let bandwidth = self.bandwidth_estimator.estimate();
3998 let mut total_ms = 0.0f32;
3999 for batch in plan {
4000 let batch_time_ms = batch.iter()
4002 .filter_map(|&lid| self.core.levels.get(&lid))
4003 .map(|l| self.bandwidth_estimator.estimated_load_time_ms(
4004 l.asset.size_bytes as f32 / (1024.0 * 1024.0)
4005 ))
4006 .fold(0.0f32, f32::max);
4007 total_ms += batch_time_ms;
4008 }
4009 total_ms
4010 }
4011}
4012
4013#[derive(Debug, Clone)]
4018pub enum StreamingEditorCommand {
4019 SetLoadDistance { level_id: u64, old: f32, new: f32 },
4020 SetUnloadDistance { level_id: u64, old: f32, new: f32 },
4021 SetPriority { level_id: u64, old: LoadPriority, new: LoadPriority },
4022 SetPersistence { level_id: u64, old: LevelPersistence, new: LevelPersistence },
4023 AddDependency { from: u64, to: u64, edge: DependencyEdgeType },
4024 RemoveDependency { from: u64, to: u64 },
4025 MoveLevelBounds { level_id: u64, old_bounds: Aabb, new_bounds: Aabb },
4026 AddVolume { volume_id: u64 },
4027 RemoveVolume { volume_id: u64 },
4028 SetMemoryBudget { old: f32, new: f32 },
4029}
4030
4031#[derive(Debug)]
4032pub struct CommandHistory {
4033 pub undo_stack: Vec<StreamingEditorCommand>,
4034 pub redo_stack: Vec<StreamingEditorCommand>,
4035 pub max_history: usize,
4036}
4037
4038impl CommandHistory {
4039 pub fn new(max_history: usize) -> Self {
4040 Self {
4041 undo_stack: Vec::new(),
4042 redo_stack: Vec::new(),
4043 max_history,
4044 }
4045 }
4046
4047 pub fn push(&mut self, cmd: StreamingEditorCommand) {
4048 self.redo_stack.clear();
4049 if self.undo_stack.len() >= self.max_history {
4050 self.undo_stack.remove(0);
4051 }
4052 self.undo_stack.push(cmd);
4053 }
4054
4055 pub fn undo(&mut self) -> Option<StreamingEditorCommand> {
4056 if let Some(cmd) = self.undo_stack.pop() {
4057 self.redo_stack.push(cmd.clone());
4058 Some(cmd)
4059 } else {
4060 None
4061 }
4062 }
4063
4064 pub fn redo(&mut self) -> Option<StreamingEditorCommand> {
4065 if let Some(cmd) = self.redo_stack.pop() {
4066 self.undo_stack.push(cmd.clone());
4067 Some(cmd)
4068 } else {
4069 None
4070 }
4071 }
4072
4073 pub fn can_undo(&self) -> bool { !self.undo_stack.is_empty() }
4074 pub fn can_redo(&self) -> bool { !self.redo_stack.is_empty() }
4075 pub fn clear(&mut self) { self.undo_stack.clear(); self.redo_stack.clear(); }
4076}
4077
4078pub fn apply_streaming_command(editor: &mut FullLevelStreamingEditor, cmd: &StreamingEditorCommand) {
4079 match cmd {
4080 StreamingEditorCommand::SetLoadDistance { level_id, new, .. } => {
4081 if let Some(l) = editor.core.levels.get_mut(level_id) {
4082 l.load_distance = *new;
4083 }
4084 }
4085 StreamingEditorCommand::SetUnloadDistance { level_id, new, .. } => {
4086 if let Some(l) = editor.core.levels.get_mut(level_id) {
4087 l.unload_distance = *new;
4088 }
4089 }
4090 StreamingEditorCommand::SetPriority { level_id, new, .. } => {
4091 if let Some(l) = editor.core.levels.get_mut(level_id) {
4092 l.priority = *new;
4093 }
4094 }
4095 StreamingEditorCommand::SetPersistence { level_id, new, .. } => {
4096 if let Some(l) = editor.core.levels.get_mut(level_id) {
4097 l.persistence = *new;
4098 }
4099 }
4100 StreamingEditorCommand::AddDependency { from, to, edge } => {
4101 editor.core.dependency_graph.add_dependency(*from, *to, *edge);
4102 }
4103 StreamingEditorCommand::RemoveDependency { from, to } => {
4104 if let Some(node) = editor.core.dependency_graph.nodes.get_mut(from) {
4105 node.dependencies.retain(|&d| d != *to);
4106 node.edge_types.remove(to);
4107 }
4108 }
4109 StreamingEditorCommand::MoveLevelBounds { level_id, new_bounds, .. } => {
4110 if let Some(l) = editor.core.levels.get_mut(level_id) {
4111 l.bounds = new_bounds.clone();
4112 l.sphere_bounds = Sphere::new(new_bounds.center(), new_bounds.extents().length());
4113 }
4114 }
4115 StreamingEditorCommand::AddVolume { .. } => {}
4116 StreamingEditorCommand::RemoveVolume { volume_id } => {
4117 editor.core.volumes.remove(volume_id);
4118 }
4119 StreamingEditorCommand::SetMemoryBudget { new, .. } => {
4120 editor.core.set_memory_budget(*new);
4121 }
4122 }
4123}
4124
4125pub fn undo_streaming_command(editor: &mut FullLevelStreamingEditor, cmd: &StreamingEditorCommand) {
4126 match cmd {
4127 StreamingEditorCommand::SetLoadDistance { level_id, old, .. } => {
4128 if let Some(l) = editor.core.levels.get_mut(level_id) {
4129 l.load_distance = *old;
4130 }
4131 }
4132 StreamingEditorCommand::SetUnloadDistance { level_id, old, .. } => {
4133 if let Some(l) = editor.core.levels.get_mut(level_id) {
4134 l.unload_distance = *old;
4135 }
4136 }
4137 StreamingEditorCommand::SetPriority { level_id, old, .. } => {
4138 if let Some(l) = editor.core.levels.get_mut(level_id) {
4139 l.priority = *old;
4140 }
4141 }
4142 StreamingEditorCommand::SetPersistence { level_id, old, .. } => {
4143 if let Some(l) = editor.core.levels.get_mut(level_id) {
4144 l.persistence = *old;
4145 }
4146 }
4147 StreamingEditorCommand::AddDependency { from, to, .. } => {
4148 if let Some(node) = editor.core.dependency_graph.nodes.get_mut(from) {
4149 node.dependencies.retain(|&d| d != *to);
4150 }
4151 }
4152 StreamingEditorCommand::SetMemoryBudget { old, .. } => {
4153 editor.core.set_memory_budget(*old);
4154 }
4155 _ => {}
4156 }
4157}
4158
4159#[derive(Debug, Clone)]
4164pub struct LevelStreamingSettingsPanel {
4165 pub show_advanced: bool,
4166 pub pending_budget_mb: f32,
4167 pub pending_cell_size: f32,
4168 pub pending_load_dist: f32,
4169 pub pending_unload_dist: f32,
4170 pub pending_max_loads: usize,
4171 pub pending_prefetch: bool,
4172 pub pending_eviction: EvictionPolicy,
4173 pub pending_frustum_cull: bool,
4174 pub pending_occlusion_cull: bool,
4175 pub is_dirty: bool,
4176}
4177
4178impl LevelStreamingSettingsPanel {
4179 pub fn new(config: &LevelStreamingEditorConfig) -> Self {
4180 Self {
4181 show_advanced: false,
4182 pending_budget_mb: config.memory_budget_mb,
4183 pending_cell_size: config.default_cell_size,
4184 pending_load_dist: config.default_load_distance,
4185 pending_unload_dist: config.default_unload_distance,
4186 pending_max_loads: config.max_concurrent_loads,
4187 pending_prefetch: config.enable_prefetch,
4188 pending_eviction: config.eviction_policy,
4189 pending_frustum_cull: config.enable_frustum_culling,
4190 pending_occlusion_cull: config.enable_occlusion_culling,
4191 is_dirty: false,
4192 }
4193 }
4194
4195 pub fn set_budget_mb(&mut self, v: f32) {
4196 self.pending_budget_mb = v.max(64.0);
4197 self.is_dirty = true;
4198 }
4199
4200 pub fn set_load_distance(&mut self, v: f32) {
4201 self.pending_load_dist = v.max(10.0);
4202 if self.pending_unload_dist < self.pending_load_dist {
4203 self.pending_unload_dist = self.pending_load_dist + STREAMING_HYSTERESIS;
4204 }
4205 self.is_dirty = true;
4206 }
4207
4208 pub fn apply_to_config(&mut self, config: &mut LevelStreamingEditorConfig) {
4209 config.memory_budget_mb = self.pending_budget_mb;
4210 config.default_cell_size = self.pending_cell_size;
4211 config.default_load_distance = self.pending_load_dist;
4212 config.default_unload_distance = self.pending_unload_dist;
4213 config.max_concurrent_loads = self.pending_max_loads;
4214 config.enable_prefetch = self.pending_prefetch;
4215 config.eviction_policy = self.pending_eviction;
4216 config.enable_frustum_culling = self.pending_frustum_cull;
4217 config.enable_occlusion_culling = self.pending_occlusion_cull;
4218 self.is_dirty = false;
4219 }
4220
4221 pub fn has_unsaved_changes(&self) -> bool { self.is_dirty }
4222}
4223
4224#[derive(Debug)]
4229pub struct StreamingHeatMap {
4230 pub grid_w: usize,
4231 pub grid_h: usize,
4232 pub cell_size: f32,
4233 pub origin: Vec2,
4234 pub data: Vec<f32>, pub raw_counts: Vec<u32>, }
4237
4238impl StreamingHeatMap {
4239 pub fn new(grid_w: usize, grid_h: usize, cell_size: f32, origin: Vec2) -> Self {
4240 let n = grid_w * grid_h;
4241 Self {
4242 grid_w,
4243 grid_h,
4244 cell_size,
4245 origin,
4246 data: vec![0.0; n],
4247 raw_counts: vec![0; n],
4248 }
4249 }
4250
4251 pub fn world_to_cell(&self, world_x: f32, world_z: f32) -> Option<(usize, usize)> {
4252 let cx = ((world_x - self.origin.x) / self.cell_size) as isize;
4253 let cz = ((world_z - self.origin.y) / self.cell_size) as isize;
4254 if cx >= 0 && cx < self.grid_w as isize && cz >= 0 && cz < self.grid_h as isize {
4255 Some((cx as usize, cz as usize))
4256 } else {
4257 None
4258 }
4259 }
4260
4261 pub fn record_event(&mut self, world_x: f32, world_z: f32) {
4262 if let Some((cx, cz)) = self.world_to_cell(world_x, world_z) {
4263 self.raw_counts[cz * self.grid_w + cx] += 1;
4264 }
4265 }
4266
4267 pub fn normalize(&mut self) {
4268 let max = self.raw_counts.iter().copied().max().unwrap_or(1).max(1) as f32;
4269 for (i, &count) in self.raw_counts.iter().enumerate() {
4270 self.data[i] = count as f32 / max;
4271 }
4272 }
4273
4274 pub fn decay(&mut self, factor: f32) {
4275 for c in &mut self.raw_counts {
4276 *c = (*c as f32 * factor) as u32;
4277 }
4278 self.normalize();
4279 }
4280
4281 pub fn sample(&self, world_x: f32, world_z: f32) -> f32 {
4282 self.world_to_cell(world_x, world_z)
4283 .map(|(cx, cz)| self.data[cz * self.grid_w + cx])
4284 .unwrap_or(0.0)
4285 }
4286
4287 pub fn peak_cell(&self) -> Option<(usize, usize)> {
4288 let (idx, _) = self.data.iter()
4289 .enumerate()
4290 .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))?;
4291 Some((idx % self.grid_w, idx / self.grid_w))
4292 }
4293}
4294
4295#[derive(Debug, Clone)]
4300pub struct AiSpawnRequest {
4301 pub spawn_id: u64,
4302 pub type_id: u32,
4303 pub preferred_sector: Option<u64>,
4304 pub spawn_position: Option<Vec3>,
4305 pub count: u32,
4306 pub priority: u32,
4307}
4308
4309#[derive(Debug)]
4310pub struct SectorAwareAiSpawner {
4311 pub pending_requests: VecDeque<AiSpawnRequest>,
4312 pub active_spawns: HashMap<u64, Vec<u64>>, pub max_per_sector: u32,
4314}
4315
4316impl SectorAwareAiSpawner {
4317 pub fn new(max_per_sector: u32) -> Self {
4318 Self {
4319 pending_requests: VecDeque::new(),
4320 active_spawns: HashMap::new(),
4321 max_per_sector,
4322 }
4323 }
4324
4325 pub fn request_spawn(&mut self, req: AiSpawnRequest) {
4326 self.pending_requests.push_back(req);
4327 }
4328
4329 pub fn process_requests(&mut self, sector_graph: &SectorGraph, camera_pos: Vec3) {
4330 let mut processed = Vec::new();
4331 for (i, req) in self.pending_requests.iter().enumerate() {
4332 let target_sector = req.preferred_sector
4333 .or_else(|| sector_graph.find_sector_at(camera_pos));
4334 let Some(sector_id) = target_sector else { continue };
4335 let current_count = self.active_spawns.get(§or_id).map(|v| v.len()).unwrap_or(0) as u32;
4336 if current_count + req.count > self.max_per_sector { continue; }
4337 if let Some(sector) = sector_graph.sectors.get(§or_id) {
4338 if sector.level_ids.is_empty() { continue; }
4339 let spawn_pos = req.spawn_position.or_else(|| {
4341 sector.nearest_waypoint(camera_pos).map(|w| w.position)
4342 });
4343 if spawn_pos.is_some() {
4344 let entry = self.active_spawns.entry(sector_id).or_default();
4345 for _ in 0..req.count {
4346 entry.push(req.spawn_id);
4347 }
4348 processed.push(i);
4349 }
4350 }
4351 }
4352 for i in processed.into_iter().rev() {
4353 self.pending_requests.remove(i);
4354 }
4355 }
4356
4357 pub fn total_active_spawns(&self) -> usize {
4358 self.active_spawns.values().map(|v| v.len()).sum()
4359 }
4360
4361 pub fn clear_sector(&mut self, sector_id: u64) {
4362 self.active_spawns.remove(§or_id);
4363 }
4364}
4365
4366#[derive(Debug, Clone)]
4371pub struct LodAnalysisResult {
4372 pub level_id: u64,
4373 pub recommended_lod: LodLevel,
4374 pub current_lod: LodLevel,
4375 pub lod_mismatch: bool,
4376 pub potential_memory_save_mb: f32,
4377 pub screen_size_px: f32,
4378 pub distance_m: f32,
4379}
4380
4381pub fn analyze_lod_distribution(
4382 levels: &[StreamingLevel],
4383 budget_manager: &CombinedBudgetManager,
4384) -> Vec<LodAnalysisResult> {
4385 let global_bias = budget_manager.compute_global_lod_bias();
4386 levels.iter().map(|level| {
4387 let recommended = budget_manager.optimal_lod_for_budget(level.id, level.distance_to_camera);
4388 let current_mem = level.memory_estimate_mb();
4389 let rec_mem = {
4390 let mut tmp = level.clone();
4391 tmp.current_lod = recommended;
4392 tmp.memory_estimate_mb()
4393 };
4394 LodAnalysisResult {
4395 level_id: level.id,
4396 recommended_lod: recommended,
4397 current_lod: level.current_lod,
4398 lod_mismatch: recommended != level.current_lod,
4399 potential_memory_save_mb: (current_mem - rec_mem).max(0.0),
4400 screen_size_px: level.screen_size,
4401 distance_m: level.distance_to_camera,
4402 }
4403 }).collect()
4404}
4405
4406pub fn compute_streaming_importance(
4407 level: &StreamingLevel,
4408 camera_pos: Vec3,
4409 camera_dir: Vec3,
4410 time_since_last_load_s: f32,
4411) -> f32 {
4412 let dist = level.bounds.distance_to_point(camera_pos).max(0.01);
4413
4414 let dist_factor = 1.0 / (1.0 + dist * 0.001);
4416
4417 let to_level = (level.bounds.center() - camera_pos).normalize_or_zero();
4419 let dir_factor = (camera_dir.dot(to_level) * 0.5 + 0.5).powf(2.0);
4420
4421 let recency = (1.0 - (-time_since_last_load_s * 0.1).exp()) * 0.2;
4423
4424 let screen_factor = level.screen_size.clamp(0.0, 1.0);
4426
4427 let user_weight = level.importance_weight;
4429
4430 (dist_factor * dir_factor + recency + screen_factor * 0.3) * user_weight
4431}
4432
4433pub fn compute_cell_load_radius(
4434 camera_velocity: Vec3,
4435 base_radius: f32,
4436 lookahead_s: f32,
4437) -> f32 {
4438 let speed = camera_velocity.length();
4439 base_radius + speed * lookahead_s
4441}
4442
4443pub fn distance_based_lod_bias(distance: f32, budget_pressure: f32) -> f32 {
4444 let budget_bias = budget_pressure * 3.0;
4446 let dist_bias = (distance * LOD_BIAS_DISTANCE_SCALE).powf(1.5);
4448 budget_bias + dist_bias
4449}
4450
4451pub fn hysteresis_check_load(dist: f32, load_dist: f32, hysteresis: f32) -> bool {
4452 dist < load_dist - hysteresis
4453}
4454
4455pub fn hysteresis_check_unload(dist: f32, unload_dist: f32, hysteresis: f32) -> bool {
4456 dist > unload_dist + hysteresis
4457}
4458
4459pub fn memory_mb_to_bytes(mb: f32) -> u64 {
4460 (mb * 1024.0 * 1024.0) as u64
4461}
4462
4463pub fn bytes_to_memory_mb(bytes: u64) -> f32 {
4464 bytes as f32 / (1024.0 * 1024.0)
4465}
4466
4467pub fn compute_sector_portal_visibility(
4468 portal: &Portal,
4469 camera_pos: Vec3,
4470 camera_dir: Vec3,
4471) -> f32 {
4472 if !portal.is_open { return 0.0; }
4473 let to_portal = portal.center - camera_pos;
4474 let dist = to_portal.length();
4475 if dist < 1e-4 { return 1.0; }
4476 let norm = to_portal / dist;
4477 let dot_dir = camera_dir.dot(norm).max(0.0);
4478 let dot_normal = (-portal.normal).dot(norm).max(0.0);
4479 dot_dir * dot_normal * portal.transmission * (1.0 / (1.0 + dist * 0.001))
4480}
4481
4482pub fn compute_level_bandwidth_estimate_mb_s(
4483 file_size_bytes: u64,
4484 load_time_ms: f32,
4485) -> f32 {
4486 if load_time_ms <= 0.0 { return 0.0; }
4487 let mb = file_size_bytes as f32 / (1024.0 * 1024.0);
4488 mb / (load_time_ms / 1000.0)
4489}
4490
4491pub fn priority_score_for_distance(
4492 dist: f32,
4493 base_priority: LoadPriority,
4494 importance: f32,
4495) -> f32 {
4496 let p = match base_priority {
4497 LoadPriority::Critical => 10000.0,
4498 LoadPriority::High => 1000.0,
4499 LoadPriority::Medium => 100.0,
4500 LoadPriority::Low => 10.0,
4501 LoadPriority::Prefetch => 1.0,
4502 };
4503 let d = (1000.0 / dist.max(1.0)).min(100.0);
4504 p * importance + d
4505}
4506
4507#[cfg(test)]
4512mod tests {
4513 use super::*;
4514
4515 fn make_asset(id: u64, size_bytes: u64) -> StreamingLevelAsset {
4516 StreamingLevelAsset {
4517 id,
4518 name: format!("Asset_{}", id),
4519 file_path: format!("content/levels/level_{}.pak", id),
4520 size_bytes,
4521 uncompressed_size_bytes: size_bytes * 2,
4522 dependencies: Vec::new(),
4523 load_time_estimate_ms: 200.0,
4524 }
4525 }
4526
4527 #[test]
4528 fn test_aabb_distance() {
4529 let aabb = Aabb::new(Vec3::ZERO, Vec3::splat(10.0));
4530 assert!((aabb.distance_to_point(Vec3::splat(5.0)) - 0.0).abs() < 1e-5);
4531 let dist = aabb.distance_to_point(Vec3::new(20.0, 5.0, 5.0));
4532 assert!((dist - 10.0).abs() < 1e-4);
4533 }
4534
4535 #[test]
4536 fn test_frustum_culling() {
4537 let proj = Mat4::perspective_rh(std::f32::consts::FRAC_PI_4, 1.0, 0.1, 1000.0);
4539 let view = Mat4::look_at_rh(Vec3::new(0.0, 0.0, 10.0), Vec3::ZERO, Vec3::Y);
4540 let vp = proj * view;
4541 let frustum = Frustum::from_view_proj(vp);
4542 let aabb = Aabb::new(Vec3::new(-1.0, -1.0, -1.0), Vec3::new(1.0, 1.0, 1.0));
4543 let _ = frustum.test_aabb(&aabb);
4545 }
4546
4547 #[test]
4548 fn test_dependency_cycle() {
4549 let mut graph = DependencyGraph::new();
4550 graph.add_dependency(1, 2, DependencyEdgeType::HardDependency);
4551 graph.add_dependency(2, 3, DependencyEdgeType::HardDependency);
4552 graph.add_dependency(3, 1, DependencyEdgeType::HardDependency);
4553 assert!(graph.detect_cycles().is_some());
4554 }
4555
4556 #[test]
4557 fn test_dependency_no_cycle() {
4558 let mut graph = DependencyGraph::new();
4559 graph.add_dependency(1, 2, DependencyEdgeType::HardDependency);
4560 graph.add_dependency(2, 3, DependencyEdgeType::HardDependency);
4561 assert!(graph.detect_cycles().is_none());
4562 }
4563
4564 #[test]
4565 fn test_memory_manager_eviction() {
4566 let mut mgr = MemoryPressureManager::new(100.0);
4567 mgr.record_load(1, 40.0);
4568 mgr.record_load(2, 30.0);
4569 mgr.record_load(3, 20.0);
4570 assert!(!mgr.is_critical());
4571 mgr.record_load(4, 8.0);
4572 assert!(mgr.is_critical());
4574 }
4575
4576 #[test]
4577 fn test_hzb_build() {
4578 let mut hzb = HierarchicalZBuffer::new(8, 8);
4579 let depth = vec![0.5f32; 64];
4580 hzb.build_from_depth(&depth);
4581 assert!(hzb.mips[0].data.iter().all(|&d| (d - 0.5).abs() < 1e-5));
4582 }
4583
4584 #[test]
4585 fn test_prefetch_prediction() {
4586 let mut predictor = PrefetchPredictor::new(2.0);
4587 let t = 0.0;
4588 predictor.update(Vec3::ZERO, t);
4589 predictor.update(Vec3::new(10.0, 0.0, 0.0), 1000.0);
4590 let predicted = predictor.predicted_camera_pos();
4591 assert!(predicted.x > 15.0);
4593 }
4594
4595 #[test]
4596 fn test_world_partition() {
4597 let mut grid = WorldPartitionGrid::new(512.0, Vec3::ZERO);
4598 let coord = grid.world_to_cell(Vec3::new(256.0, 0.0, 256.0));
4599 assert_eq!(coord, CellCoord::new(0, 0, 0));
4600 let coord2 = grid.world_to_cell(Vec3::new(600.0, 0.0, 600.0));
4601 assert_eq!(coord2, CellCoord::new(1, 1, 0));
4602 }
4603
4604 #[test]
4605 fn test_load_queue_priority() {
4606 let mut queue = StreamingLoadQueue::new(4);
4607 queue.enqueue(LoadRequest {
4608 level_id: 1, priority: LoadPriority::Low,
4609 distance_weight: 0.1, enqueue_time_ms: 0.0,
4610 predicted_load_time_ms: 100.0, is_prefetch: false,
4611 });
4612 queue.enqueue(LoadRequest {
4613 level_id: 2, priority: LoadPriority::Critical,
4614 distance_weight: 0.1, enqueue_time_ms: 0.0,
4615 predicted_load_time_ms: 100.0, is_prefetch: false,
4616 });
4617 let next = queue.dequeue_next().unwrap();
4618 assert_eq!(next.level_id, 2); }
4620
4621 #[test]
4622 fn test_streaming_level_lod() {
4623 let asset = make_asset(1, 50 * 1024 * 1024);
4624 let bounds = Aabb::new(Vec3::ZERO, Vec3::splat(100.0));
4625 let level = StreamingLevel::new(1, "Test".into(), asset, bounds);
4626 assert_eq!(level.compute_lod(50.0, 0.0), LodLevel::Lod0);
4627 assert_eq!(level.compute_lod(200.0, 0.0), LodLevel::Lod1);
4628 assert_eq!(level.compute_lod(500.0, 0.0), LodLevel::Lod2);
4629 assert_eq!(level.compute_lod(800.0, 0.0), LodLevel::Lod3);
4630 assert_eq!(level.compute_lod(1500.0, 0.0), LodLevel::Culled);
4631 }
4632
4633 #[test]
4634 fn test_editor_add_level() {
4635 let config = LevelStreamingEditorConfig::default();
4636 let mut editor = LevelStreamingEditor::new(config);
4637 let asset = make_asset(1, 10 * 1024 * 1024);
4638 let bounds = Aabb::new(Vec3::ZERO, Vec3::splat(512.0));
4639 let id = editor.add_level("TestLevel".into(), asset, bounds);
4640 assert!(editor.levels.contains_key(&id));
4641 }
4642
4643 #[test]
4644 fn test_sector_pvs() {
4645 let mut sg = SectorGraph::new();
4646 let s1 = Sector::new(1, "Room1".into(), Aabb::new(Vec3::ZERO, Vec3::splat(10.0)));
4647 let s2 = Sector::new(2, "Room2".into(), Aabb::new(Vec3::new(10.0, 0.0, 0.0), Vec3::new(20.0, 10.0, 10.0)));
4648 sg.add_sector(s1);
4649 sg.add_sector(s2);
4650 let portal = Portal::new(1, 1, 2, Vec3::new(10.0, 5.0, 5.0), Vec3::new(-1.0, 0.0, 0.0), Vec2::new(2.0, 2.0));
4651 sg.add_portal(portal);
4652 let pvs = sg.compute_pvs(Vec3::new(5.0, 5.0, 5.0), &Mat4::IDENTITY, 4);
4654 assert!(pvs.contains(&1));
4655 }
4656
4657 #[test]
4658 fn test_topological_sort() {
4659 let mut graph = DependencyGraph::new();
4660 graph.add_dependency(3, 2, DependencyEdgeType::HardDependency);
4661 graph.add_dependency(3, 1, DependencyEdgeType::HardDependency);
4662 graph.add_dependency(2, 1, DependencyEdgeType::HardDependency);
4663 let result = graph.topological_sort().expect("no cycle");
4664 let pos_1 = result.iter().position(|&x| x == 1).unwrap();
4666 let pos_2 = result.iter().position(|&x| x == 2).unwrap();
4667 let pos_3 = result.iter().position(|&x| x == 3).unwrap();
4668 assert!(pos_1 < pos_2);
4669 assert!(pos_1 < pos_3);
4670 }
4671
4672 #[test]
4673 fn test_command_history() {
4674 let mut hist = CommandHistory::new(10);
4675 hist.push(StreamingEditorCommand::SetMemoryBudget { old: 512.0, new: 1024.0 });
4676 assert!(hist.can_undo());
4677 let cmd = hist.undo().unwrap();
4678 assert!(hist.can_redo());
4679 }
4680
4681 #[test]
4682 fn test_bandwidth_estimator() {
4683 let mut est = BandwidthEstimator::new(50.0);
4684 est.add_sample(100.0);
4685 est.add_sample(100.0);
4686 assert!(est.estimate() > 50.0);
4688 }
4689
4690 #[test]
4691 fn test_heatmap() {
4692 let mut hm = StreamingHeatMap::new(10, 10, 100.0, Vec2::ZERO);
4693 hm.record_event(50.0, 50.0);
4694 hm.record_event(50.0, 50.0);
4695 hm.normalize();
4696 assert!((hm.sample(50.0, 50.0) - 1.0).abs() < 1e-5);
4697 }
4698
4699 #[test]
4700 fn test_spatial_hash() {
4701 let mut hash = SpatialHashGrid::new(100.0);
4702 hash.insert(1, Vec3::new(50.0, 0.0, 50.0));
4703 hash.insert(2, Vec3::new(1000.0, 0.0, 1000.0));
4704 let results = hash.query_radius(Vec3::new(50.0, 0.0, 50.0), 10.0);
4705 assert!(results.contains(&1));
4706 assert!(!results.contains(&2));
4707 }
4708
4709 #[test]
4710 fn test_sphere_containment() {
4711 let s = Sphere::new(Vec3::ZERO, 10.0);
4712 assert!(s.contains_point(Vec3::new(5.0, 0.0, 0.0)));
4713 assert!(!s.contains_point(Vec3::new(15.0, 0.0, 0.0)));
4714 }
4715
4716 #[test]
4717 fn test_combined_budget() {
4718 let mut mgr = CombinedBudgetManager::new(1000.0);
4719 mgr.update_entry(1, LodLevel::Lod0, 100.0, 10.0);
4720 mgr.update_entry(2, LodLevel::Lod1, 50.0, 5.0);
4721 assert!((mgr.total_usage_mb() - 165.0).abs() < 1e-4);
4722 }
4723}
4724
4725#[derive(Debug)]
4730pub struct StreamingDistanceCache {
4731 pub distances: HashMap<u64, f32>,
4732 pub last_camera_pos: Vec3,
4733 pub dirty_threshold_sq: f32,
4734 pub frame_updated: u64,
4735}
4736
4737impl StreamingDistanceCache {
4738 pub fn new() -> Self {
4739 Self {
4740 distances: HashMap::new(),
4741 last_camera_pos: Vec3::splat(f32::MAX),
4742 dirty_threshold_sq: 1.0,
4743 frame_updated: 0,
4744 }
4745 }
4746
4747 pub fn update(&mut self, camera_pos: Vec3, levels: &[StreamingLevel], frame: u64) {
4748 let moved_sq = (camera_pos - self.last_camera_pos).length_squared();
4749 if moved_sq < self.dirty_threshold_sq && frame == self.frame_updated {
4750 return;
4751 }
4752 self.last_camera_pos = camera_pos;
4753 self.frame_updated = frame;
4754 for level in levels {
4755 let dist = level.bounds.distance_to_point(camera_pos);
4756 self.distances.insert(level.id, dist);
4757 }
4758 }
4759
4760 pub fn get(&self, level_id: u64) -> Option<f32> {
4761 self.distances.get(&level_id).copied()
4762 }
4763
4764 pub fn invalidate(&mut self) {
4765 self.last_camera_pos = Vec3::splat(f32::MAX);
4766 }
4767
4768 pub fn nearest_level_id(&self) -> Option<u64> {
4769 self.distances.iter()
4770 .min_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
4771 .map(|(&id, _)| id)
4772 }
4773}
4774
4775#[derive(Debug, Clone)]
4780pub struct DynamicStreamingObject {
4781 pub id: u64,
4782 pub name: String,
4783 pub position: Vec3,
4784 pub velocity: Vec3,
4785 pub bounds_radius: f32,
4786 pub current_cell: CellCoord,
4787 pub visible: bool,
4788 pub importance: f32,
4789 pub last_move_time_ms: f64,
4790}
4791
4792impl DynamicStreamingObject {
4793 pub fn new(id: u64, name: String, position: Vec3, bounds_radius: f32) -> Self {
4794 Self {
4795 id,
4796 name,
4797 position,
4798 velocity: Vec3::ZERO,
4799 bounds_radius,
4800 current_cell: CellCoord::new(0, 0, 0),
4801 visible: true,
4802 importance: 1.0,
4803 last_move_time_ms: 0.0,
4804 }
4805 }
4806
4807 pub fn update_position(&mut self, new_pos: Vec3, dt_s: f32, time_ms: f64) {
4808 let delta = new_pos - self.position;
4809 if dt_s > 1e-6 {
4810 self.velocity = delta / dt_s;
4811 }
4812 self.position = new_pos;
4813 if delta.length_squared() > 0.001 {
4814 self.last_move_time_ms = time_ms;
4815 }
4816 }
4817
4818 pub fn predicted_position(&self, lookahead_s: f32) -> Vec3 {
4819 self.position + self.velocity * lookahead_s
4820 }
4821
4822 pub fn bounds_sphere(&self) -> Sphere {
4823 Sphere::new(self.position, self.bounds_radius)
4824 }
4825
4826 pub fn speed(&self) -> f32 { self.velocity.length() }
4827}
4828
4829#[derive(Debug)]
4830pub struct DynamicObjectTracker {
4831 pub objects: HashMap<u64, DynamicStreamingObject>,
4832 pub next_id: u64,
4833 pub grid: WorldPartitionGrid,
4834 pub total_moves: u64,
4835}
4836
4837impl DynamicObjectTracker {
4838 pub fn new(cell_size: f32) -> Self {
4839 Self {
4840 objects: HashMap::new(),
4841 next_id: 1,
4842 grid: WorldPartitionGrid::new(cell_size, Vec3::ZERO),
4843 total_moves: 0,
4844 }
4845 }
4846
4847 pub fn spawn(&mut self, name: String, pos: Vec3, radius: f32) -> u64 {
4848 let id = self.next_id;
4849 self.next_id += 1;
4850 let mut obj = DynamicStreamingObject::new(id, name, pos, radius);
4851 obj.current_cell = self.grid.world_to_cell(pos);
4852 self.grid.register_object(id, pos);
4853 self.objects.insert(id, obj);
4854 id
4855 }
4856
4857 pub fn despawn(&mut self, id: u64) {
4858 if let Some(_obj) = self.objects.remove(&id) {
4859 self.grid.unregister_object(id);
4860 }
4861 }
4862
4863 pub fn update_position(&mut self, id: u64, new_pos: Vec3, dt_s: f32, time_ms: f64) {
4864 if let Some(obj) = self.objects.get_mut(&id) {
4865 let old_cell = obj.current_cell;
4866 obj.update_position(new_pos, dt_s, time_ms);
4867 let new_cell = self.grid.world_to_cell(new_pos);
4868 obj.current_cell = new_cell;
4869 if old_cell != new_cell {
4870 self.grid.register_object(id, new_pos);
4871 self.total_moves += 1;
4872 }
4873 }
4874 }
4875
4876 pub fn query_near(&self, pos: Vec3, radius: f32) -> Vec<u64> {
4877 self.grid.query_objects_in_radius(pos, radius)
4878 }
4879
4880 pub fn objects_in_cell(&self, coord: CellCoord) -> Vec<u64> {
4881 self.grid.cells.get(&coord)
4882 .map(|c| c.dynamic_object_ids.clone())
4883 .unwrap_or_default()
4884 }
4885
4886 pub fn total_objects(&self) -> usize { self.objects.len() }
4887
4888 pub fn objects_by_importance(&self) -> Vec<&DynamicStreamingObject> {
4889 let mut sorted: Vec<&DynamicStreamingObject> = self.objects.values().collect();
4890 sorted.sort_by(|a, b| b.importance.partial_cmp(&a.importance).unwrap_or(std::cmp::Ordering::Equal));
4891 sorted
4892 }
4893
4894 pub fn fast_moving_objects(&self, speed_threshold: f32) -> Vec<u64> {
4895 self.objects.iter()
4896 .filter(|(_, o)| o.speed() > speed_threshold)
4897 .map(|(&id, _)| id)
4898 .collect()
4899 }
4900}
4901
4902#[derive(Debug, Clone)]
4907pub struct StreamingAnalyticsSession {
4908 pub session_id: u64,
4909 pub start_time_ms: f64,
4910 pub end_time_ms: f64,
4911 pub total_loads: u32,
4912 pub total_unloads: u32,
4913 pub total_evictions: u32,
4914 pub peak_memory_mb: f32,
4915 pub total_bandwidth_mb: f32,
4916 pub stall_events: u32,
4917 pub average_load_latency_ms: f32,
4918 pub prefetch_hit_rate: f32,
4919 pub unique_levels_loaded: HashSet<u64>,
4920}
4921
4922impl StreamingAnalyticsSession {
4923 pub fn new(session_id: u64, start_ms: f64) -> Self {
4924 Self {
4925 session_id,
4926 start_time_ms: start_ms,
4927 end_time_ms: start_ms,
4928 total_loads: 0,
4929 total_unloads: 0,
4930 total_evictions: 0,
4931 peak_memory_mb: 0.0,
4932 total_bandwidth_mb: 0.0,
4933 stall_events: 0,
4934 average_load_latency_ms: 0.0,
4935 prefetch_hit_rate: 0.0,
4936 unique_levels_loaded: HashSet::new(),
4937 }
4938 }
4939
4940 pub fn record_load(&mut self, level_id: u64, latency_ms: f32, mb: f32) {
4941 self.total_loads += 1;
4942 self.total_bandwidth_mb += mb;
4943 self.unique_levels_loaded.insert(level_id);
4944 let n = self.total_loads as f32;
4945 self.average_load_latency_ms = self.average_load_latency_ms * (n - 1.0) / n + latency_ms / n;
4946 }
4947
4948 pub fn record_unload(&mut self) { self.total_unloads += 1; }
4949 pub fn record_eviction(&mut self) { self.total_evictions += 1; }
4950 pub fn record_stall(&mut self) { self.stall_events += 1; }
4951
4952 pub fn update_peak_memory(&mut self, used_mb: f32) {
4953 self.peak_memory_mb = self.peak_memory_mb.max(used_mb);
4954 }
4955
4956 pub fn duration_s(&self) -> f32 {
4957 ((self.end_time_ms - self.start_time_ms) / 1000.0) as f32
4958 }
4959
4960 pub fn average_bandwidth_mb_s(&self) -> f32 {
4961 let d = self.duration_s();
4962 if d > 0.0 { self.total_bandwidth_mb / d } else { 0.0 }
4963 }
4964
4965 pub fn finalize(&mut self, end_ms: f64) {
4966 self.end_time_ms = end_ms;
4967 }
4968
4969 pub fn efficiency_score(&self) -> f32 {
4970 if self.total_loads == 0 { return 1.0; }
4972 self.unique_levels_loaded.len() as f32 / self.total_loads as f32
4973 }
4974}
4975
4976#[derive(Debug, Clone)]
4981pub struct RegionOfInterest {
4982 pub id: u64,
4983 pub name: String,
4984 pub bounds: Aabb,
4985 pub boost_priority: LoadPriority,
4986 pub boost_load_distance: f32,
4987 pub is_active: bool,
4988 pub activation_condition: String,
4989 pub activation_time_ms: f64,
4990}
4991
4992impl RegionOfInterest {
4993 pub fn new(id: u64, name: String, bounds: Aabb, priority: LoadPriority) -> Self {
4994 Self {
4995 id,
4996 name,
4997 bounds,
4998 boost_priority: priority,
4999 boost_load_distance: 200.0,
5000 is_active: false,
5001 activation_condition: String::new(),
5002 activation_time_ms: 0.0,
5003 }
5004 }
5005
5006 pub fn activate(&mut self, time_ms: f64) {
5007 self.is_active = true;
5008 self.activation_time_ms = time_ms;
5009 }
5010
5011 pub fn deactivate(&mut self) { self.is_active = false; }
5012
5013 pub fn camera_in_range(&self, camera_pos: Vec3, margin: f32) -> bool {
5014 self.is_active && self.bounds.expand_by(margin).contains_point(camera_pos)
5015 }
5016
5017 pub fn overlap_area_with(&self, other: &Aabb) -> f32 {
5018 let ix = (self.bounds.max.x.min(other.max.x) - self.bounds.min.x.max(other.min.x)).max(0.0);
5019 let iy = (self.bounds.max.y.min(other.max.y) - self.bounds.min.y.max(other.min.y)).max(0.0);
5020 let iz = (self.bounds.max.z.min(other.max.z) - self.bounds.min.z.max(other.min.z)).max(0.0);
5021 ix * iy * iz
5022 }
5023}
5024
5025#[derive(Debug)]
5026pub struct RegionOfInterestManager {
5027 pub regions: HashMap<u64, RegionOfInterest>,
5028 pub active_regions: HashSet<u64>,
5029 pub next_id: u64,
5030}
5031
5032impl RegionOfInterestManager {
5033 pub fn new() -> Self {
5034 Self {
5035 regions: HashMap::new(),
5036 active_regions: HashSet::new(),
5037 next_id: 1,
5038 }
5039 }
5040
5041 pub fn add_region(&mut self, name: String, bounds: Aabb, priority: LoadPriority) -> u64 {
5042 let id = self.next_id;
5043 self.next_id += 1;
5044 self.regions.insert(id, RegionOfInterest::new(id, name, bounds, priority));
5045 id
5046 }
5047
5048 pub fn update(&mut self, camera_pos: Vec3, time_ms: f64) {
5049 self.active_regions.clear();
5050 for (id, region) in &mut self.regions {
5051 let in_range = region.bounds.expand_by(region.boost_load_distance).contains_point(camera_pos);
5052 if in_range && !region.is_active {
5053 region.activate(time_ms);
5054 } else if !in_range && region.is_active {
5055 region.deactivate();
5056 }
5057 if region.is_active {
5058 self.active_regions.insert(*id);
5059 }
5060 }
5061 }
5062
5063 pub fn priority_for_level(&self, level: &StreamingLevel) -> LoadPriority {
5064 for &id in &self.active_regions {
5065 if let Some(region) = self.regions.get(&id) {
5066 if region.bounds.intersects(&level.bounds) {
5067 return region.boost_priority;
5068 }
5069 }
5070 }
5071 level.priority
5072 }
5073
5074 pub fn any_active_near(&self, pos: Vec3, radius: f32) -> bool {
5075 self.active_regions.iter().any(|&id| {
5076 self.regions.get(&id).map_or(false, |r| r.bounds.distance_to_point(pos) <= radius)
5077 })
5078 }
5079}
5080
5081#[derive(Debug, Clone)]
5086pub struct StreamingCheckpoint {
5087 pub id: u64,
5088 pub name: String,
5089 pub camera_position: Vec3,
5090 pub camera_direction: Vec3,
5091 pub loaded_level_ids: Vec<u64>,
5092 pub memory_used_mb: f32,
5093 pub timestamp_ms: f64,
5094 pub save_slot: u32,
5095}
5096
5097impl StreamingCheckpoint {
5098 pub fn capture(
5099 id: u64,
5100 name: String,
5101 camera_pos: Vec3,
5102 camera_dir: Vec3,
5103 levels: &[StreamingLevel],
5104 memory_mb: f32,
5105 time_ms: f64,
5106 ) -> Self {
5107 let loaded: Vec<u64> = levels.iter()
5108 .filter(|l| l.state == StreamingState::Loaded)
5109 .map(|l| l.id)
5110 .collect();
5111 Self {
5112 id,
5113 name,
5114 camera_position: camera_pos,
5115 camera_direction: camera_dir,
5116 loaded_level_ids: loaded,
5117 memory_used_mb: memory_mb,
5118 timestamp_ms: time_ms,
5119 save_slot: 0,
5120 }
5121 }
5122
5123 pub fn warm_up_requests(&self) -> Vec<u64> {
5124 self.loaded_level_ids.clone()
5125 }
5126}
5127
5128#[derive(Debug)]
5129pub struct CheckpointManager {
5130 pub checkpoints: HashMap<u64, StreamingCheckpoint>,
5131 pub next_id: u64,
5132 pub auto_checkpoint_interval_ms: f64,
5133 pub last_auto_checkpoint_ms: f64,
5134 pub max_checkpoints: usize,
5135}
5136
5137impl CheckpointManager {
5138 pub fn new() -> Self {
5139 Self {
5140 checkpoints: HashMap::new(),
5141 next_id: 1,
5142 auto_checkpoint_interval_ms: 60_000.0,
5143 last_auto_checkpoint_ms: 0.0,
5144 max_checkpoints: 16,
5145 }
5146 }
5147
5148 pub fn save(
5149 &mut self,
5150 name: String,
5151 camera_pos: Vec3,
5152 camera_dir: Vec3,
5153 levels: &[StreamingLevel],
5154 memory_mb: f32,
5155 time_ms: f64,
5156 ) -> u64 {
5157 let id = self.next_id;
5158 self.next_id += 1;
5159 let cp = StreamingCheckpoint::capture(id, name, camera_pos, camera_dir, levels, memory_mb, time_ms);
5160 if self.checkpoints.len() >= self.max_checkpoints {
5161 if let Some(&oldest_id) = self.checkpoints.keys().next() {
5162 self.checkpoints.remove(&oldest_id);
5163 }
5164 }
5165 self.checkpoints.insert(id, cp);
5166 id
5167 }
5168
5169 pub fn maybe_auto_checkpoint(
5170 &mut self,
5171 camera_pos: Vec3,
5172 camera_dir: Vec3,
5173 levels: &[StreamingLevel],
5174 memory_mb: f32,
5175 time_ms: f64,
5176 ) -> Option<u64> {
5177 if time_ms - self.last_auto_checkpoint_ms >= self.auto_checkpoint_interval_ms {
5178 self.last_auto_checkpoint_ms = time_ms;
5179 Some(self.save("Auto".into(), camera_pos, camera_dir, levels, memory_mb, time_ms))
5180 } else { None }
5181 }
5182
5183 pub fn get_latest(&self) -> Option<&StreamingCheckpoint> {
5184 self.checkpoints.values()
5185 .max_by(|a, b| a.timestamp_ms.partial_cmp(&b.timestamp_ms).unwrap_or(std::cmp::Ordering::Equal))
5186 }
5187
5188 pub fn delete_checkpoint(&mut self, id: u64) -> bool {
5189 self.checkpoints.remove(&id).is_some()
5190 }
5191
5192 pub fn total_saved_level_ids(&self) -> HashSet<u64> {
5193 let mut set = HashSet::new();
5194 for cp in self.checkpoints.values() {
5195 for &id in &cp.loaded_level_ids { set.insert(id); }
5196 }
5197 set
5198 }
5199}
5200
5201#[derive(Debug, Clone)]
5206pub struct LodTransition {
5207 pub level_id: u64,
5208 pub from_lod: LodLevel,
5209 pub to_lod: LodLevel,
5210 pub progress: f32,
5211 pub duration_s: f32,
5212 pub blend_distance: f32,
5213}
5214
5215impl LodTransition {
5216 pub fn new(level_id: u64, from: LodLevel, to: LodLevel, duration_s: f32) -> Self {
5217 Self {
5218 level_id,
5219 from_lod: from,
5220 to_lod: to,
5221 progress: 0.0,
5222 duration_s,
5223 blend_distance: 50.0,
5224 }
5225 }
5226
5227 pub fn update_progress(&mut self, dt_s: f32) -> bool {
5228 self.progress += dt_s / self.duration_s.max(0.001);
5229 self.progress >= 1.0
5230 }
5231
5232 pub fn blend_alpha(&self) -> f32 {
5233 let t = self.progress.clamp(0.0, 1.0);
5234 t * t * (3.0 - 2.0 * t) }
5236
5237 pub fn is_complete(&self) -> bool { self.progress >= 1.0 }
5238
5239 pub fn reversed(&self) -> Self {
5240 LodTransition::new(self.level_id, self.to_lod, self.from_lod, self.duration_s)
5241 }
5242}
5243
5244#[derive(Debug)]
5245pub struct LodTransitionManager {
5246 pub transitions: HashMap<u64, LodTransition>,
5247 pub completed: VecDeque<(u64, LodLevel)>,
5248}
5249
5250impl LodTransitionManager {
5251 pub fn new() -> Self {
5252 Self {
5253 transitions: HashMap::new(),
5254 completed: VecDeque::with_capacity(64),
5255 }
5256 }
5257
5258 pub fn begin_transition(&mut self, level_id: u64, from: LodLevel, to: LodLevel, duration_s: f32) {
5259 self.transitions.insert(level_id, LodTransition::new(level_id, from, to, duration_s));
5260 }
5261
5262 pub fn update(&mut self, dt_s: f32) {
5263 let mut to_complete: Vec<u64> = Vec::new();
5264 for (id, t) in &mut self.transitions {
5265 if t.update_progress(dt_s) {
5266 to_complete.push(*id);
5267 }
5268 }
5269 for id in to_complete {
5270 if let Some(t) = self.transitions.remove(&id) {
5271 if self.completed.len() >= 64 { self.completed.pop_front(); }
5272 self.completed.push_back((id, t.to_lod));
5273 }
5274 }
5275 }
5276
5277 pub fn get_blend_alpha(&self, level_id: u64) -> f32 {
5278 self.transitions.get(&level_id).map(|t| t.blend_alpha()).unwrap_or(1.0)
5279 }
5280
5281 pub fn is_transitioning(&self, level_id: u64) -> bool {
5282 self.transitions.contains_key(&level_id)
5283 }
5284
5285 pub fn active_count(&self) -> usize { self.transitions.len() }
5286
5287 pub fn cancel_transition(&mut self, level_id: u64) {
5288 self.transitions.remove(&level_id);
5289 }
5290}
5291
5292pub struct StreamingFlowOptimizer;
5297
5298impl StreamingFlowOptimizer {
5299 pub fn reorder_by_geography(requests: &mut Vec<LoadRequest>, levels: &HashMap<u64, StreamingLevel>) {
5300 requests.sort_by(|a, b| {
5301 let pos_a = levels.get(&a.level_id).map(|l| l.bounds.center()).unwrap_or(Vec3::ZERO);
5302 let pos_b = levels.get(&b.level_id).map(|l| l.bounds.center()).unwrap_or(Vec3::ZERO);
5303 pos_a.x.partial_cmp(&pos_b.x).unwrap_or(std::cmp::Ordering::Equal)
5304 .then(pos_a.z.partial_cmp(&pos_b.z).unwrap_or(std::cmp::Ordering::Equal))
5305 });
5306 }
5307
5308 pub fn optimal_batch_size(bandwidth_mb_s: f32, average_level_mb: f32, target_latency_ms: f32) -> usize {
5309 if average_level_mb <= 0.0 || bandwidth_mb_s <= 0.0 { return 1; }
5310 let load_time_ms = average_level_mb / bandwidth_mb_s * 1000.0;
5311 let batch = (target_latency_ms / load_time_ms).ceil() as usize;
5312 batch.clamp(1, MAX_CONCURRENT_LOADS)
5313 }
5314
5315 pub fn estimate_memory_after_loads(
5316 current_mb: f32,
5317 budget_mb: f32,
5318 loads: &[u64],
5319 levels: &HashMap<u64, StreamingLevel>,
5320 ) -> bool {
5321 let additional: f32 = loads.iter()
5322 .filter_map(|id| levels.get(id))
5323 .map(|l| l.memory_estimate_mb())
5324 .sum();
5325 current_mb + additional <= budget_mb
5326 }
5327
5328 pub fn urgency_score(
5329 level: &StreamingLevel,
5330 camera_pos: Vec3,
5331 camera_vel: Vec3,
5332 time_to_load_ms: f32,
5333 ) -> f32 {
5334 let dist = level.bounds.distance_to_point(camera_pos);
5335 let speed = camera_vel.length();
5336 if speed < 0.1 { return 1.0 / dist.max(0.1); }
5337 let dir = camera_vel / speed;
5338 let to_level = (level.bounds.center() - camera_pos).normalize_or_zero();
5339 let dot = dir.dot(to_level).clamp(0.0, 1.0);
5340 let time_to_reach = dist / speed;
5341 let load_time_s = time_to_load_ms / 1000.0;
5342 if time_to_reach < load_time_s { 100.0 }
5343 else { dot * 10.0 / time_to_reach }
5344 }
5345
5346 pub fn speed_adjusted_radius(base_radius: f32, speed_m_s: f32, load_latency_s: f32) -> f32 {
5347 base_radius + speed_m_s * load_latency_s * 1.5
5348 }
5349
5350 pub fn compute_load_stagger_offset(
5351 index: usize,
5352 total: usize,
5353 max_bandwidth_mb_s: f32,
5354 level_size_mb: f32,
5355 ) -> f32 {
5356 if max_bandwidth_mb_s <= 0.0 || total == 0 { return 0.0; }
5357 let load_time = level_size_mb / max_bandwidth_mb_s;
5358 index as f32 * load_time / total as f32
5359 }
5360
5361 pub fn streaming_load_factor(
5362 loading_levels: usize,
5363 max_concurrent: usize,
5364 queue_depth: usize,
5365 ) -> f32 {
5366 let in_flight_factor = loading_levels as f32 / max_concurrent.max(1) as f32;
5367 let queue_factor = (queue_depth as f32 / 16.0).min(1.0);
5368 (in_flight_factor * 0.7 + queue_factor * 0.3).clamp(0.0, 1.0)
5369 }
5370}
5371
5372#[derive(Debug)]
5377pub struct SectorWaypointPathfinder {
5378 pub adjacency: HashMap<u64, Vec<u64>>,
5379}
5380
5381impl SectorWaypointPathfinder {
5382 pub fn new(sector_graph: &SectorGraph) -> Self {
5383 let mut adjacency = HashMap::new();
5384 for (id, sector) in §or_graph.sectors {
5385 adjacency.insert(*id, sector.adjacent_sectors.clone());
5386 }
5387 Self { adjacency }
5388 }
5389
5390 pub fn find_sector_path(&self, start: u64, end: u64) -> Option<Vec<u64>> {
5391 if start == end { return Some(vec![start]); }
5392 let mut visited: HashSet<u64> = HashSet::new();
5393 let mut queue: VecDeque<(u64, Vec<u64>)> = VecDeque::new();
5394 queue.push_back((start, vec![start]));
5395 visited.insert(start);
5396 while let Some((current, path)) = queue.pop_front() {
5397 if let Some(neighbors) = self.adjacency.get(¤t) {
5398 for &next in neighbors {
5399 if next == end {
5400 let mut full_path = path.clone();
5401 full_path.push(next);
5402 return Some(full_path);
5403 }
5404 if !visited.contains(&next) {
5405 visited.insert(next);
5406 let mut new_path = path.clone();
5407 new_path.push(next);
5408 queue.push_back((next, new_path));
5409 }
5410 }
5411 }
5412 }
5413 None
5414 }
5415
5416 pub fn preload_levels_for_path(&self, path: &[u64], sector_graph: &SectorGraph) -> Vec<u64> {
5417 let mut levels = Vec::new();
5418 for §or_id in path {
5419 if let Some(sector) = sector_graph.sectors.get(§or_id) {
5420 for &lid in §or.level_ids {
5421 if !levels.contains(&lid) { levels.push(lid); }
5422 }
5423 }
5424 }
5425 levels
5426 }
5427
5428 pub fn estimate_travel_time_s(
5429 &self,
5430 path: &[u64],
5431 sector_graph: &SectorGraph,
5432 speed_m_s: f32,
5433 ) -> f32 {
5434 if path.len() < 2 { return 0.0; }
5435 let mut total_dist = 0.0f32;
5436 for i in 0..(path.len() - 1) {
5437 let a = sector_graph.sectors.get(&path[i]).map(|s| s.bounds.center());
5438 let b = sector_graph.sectors.get(&path[i + 1]).map(|s| s.bounds.center());
5439 if let (Some(a), Some(b)) = (a, b) {
5440 total_dist += (b - a).length();
5441 }
5442 }
5443 if speed_m_s > 0.0 { total_dist / speed_m_s } else { f32::MAX }
5444 }
5445
5446 pub fn reachable_sectors_within_distance(&self, start: u64, max_hops: usize) -> HashSet<u64> {
5447 let mut visited: HashSet<u64> = HashSet::new();
5448 let mut queue: VecDeque<(u64, usize)> = VecDeque::new();
5449 queue.push_back((start, 0));
5450 visited.insert(start);
5451 while let Some((id, depth)) = queue.pop_front() {
5452 if depth >= max_hops { continue; }
5453 if let Some(neighbors) = self.adjacency.get(&id) {
5454 for &next in neighbors {
5455 if visited.insert(next) {
5456 queue.push_back((next, depth + 1));
5457 }
5458 }
5459 }
5460 }
5461 visited
5462 }
5463}
5464
5465#[derive(Debug, Clone)]
5470pub struct LevelAssetEntry {
5471 pub asset: StreamingLevelAsset,
5472 pub tags: Vec<String>,
5473 pub last_used_frame: u64,
5474 pub load_count: u32,
5475 pub is_pinned: bool,
5476}
5477
5478impl LevelAssetEntry {
5479 pub fn new(asset: StreamingLevelAsset) -> Self {
5480 Self {
5481 asset,
5482 tags: Vec::new(),
5483 last_used_frame: 0,
5484 load_count: 0,
5485 is_pinned: false,
5486 }
5487 }
5488
5489 pub fn mark_used(&mut self, frame: u64) {
5490 self.last_used_frame = frame;
5491 self.load_count += 1;
5492 }
5493
5494 pub fn size_mb(&self) -> f32 {
5495 self.asset.size_bytes as f32 / (1024.0 * 1024.0)
5496 }
5497}
5498
5499#[derive(Debug)]
5500pub struct LevelAssetCatalogue {
5501 pub entries: HashMap<u64, LevelAssetEntry>,
5502 pub total_size_bytes: u64,
5503 pub tags_index: HashMap<String, Vec<u64>>,
5504}
5505
5506impl LevelAssetCatalogue {
5507 pub fn new() -> Self {
5508 Self {
5509 entries: HashMap::new(),
5510 total_size_bytes: 0,
5511 tags_index: HashMap::new(),
5512 }
5513 }
5514
5515 pub fn register(&mut self, asset: StreamingLevelAsset) {
5516 let id = asset.id;
5517 let size = asset.size_bytes;
5518 self.total_size_bytes += size;
5519 self.entries.insert(id, LevelAssetEntry::new(asset));
5520 }
5521
5522 pub fn tag_asset(&mut self, asset_id: u64, tag: &str) {
5523 if let Some(entry) = self.entries.get_mut(&asset_id) {
5524 if !entry.tags.contains(&tag.to_string()) {
5525 entry.tags.push(tag.to_string());
5526 }
5527 }
5528 self.tags_index.entry(tag.to_string()).or_default().push(asset_id);
5529 }
5530
5531 pub fn assets_by_tag(&self, tag: &str) -> Vec<&LevelAssetEntry> {
5532 self.tags_index.get(tag)
5533 .map(|ids| ids.iter().filter_map(|id| self.entries.get(id)).collect())
5534 .unwrap_or_default()
5535 }
5536
5537 pub fn largest_assets(&self, n: usize) -> Vec<&LevelAssetEntry> {
5538 let mut sorted: Vec<&LevelAssetEntry> = self.entries.values().collect();
5539 sorted.sort_by(|a, b| b.asset.size_bytes.cmp(&a.asset.size_bytes));
5540 sorted.into_iter().take(n).collect()
5541 }
5542
5543 pub fn pin_asset(&mut self, id: u64) {
5544 if let Some(e) = self.entries.get_mut(&id) { e.is_pinned = true; }
5545 }
5546
5547 pub fn unpin_asset(&mut self, id: u64) {
5548 if let Some(e) = self.entries.get_mut(&id) { e.is_pinned = false; }
5549 }
5550
5551 pub fn total_size_mb(&self) -> f32 {
5552 self.total_size_bytes as f32 / (1024.0 * 1024.0)
5553 }
5554
5555 pub fn unpinned_assets_sorted_by_lru(&self, current_frame: u64) -> Vec<u64> {
5556 let mut sorted: Vec<&LevelAssetEntry> = self.entries.values()
5557 .filter(|e| !e.is_pinned)
5558 .collect();
5559 sorted.sort_by_key(|e| e.last_used_frame);
5560 sorted.iter().map(|e| e.asset.id).collect()
5561 }
5562}
5563
5564#[derive(Debug)]
5569pub struct StreamingWorldComposer {
5570 pub world_name: String,
5571 pub base_level_ids: Vec<u64>,
5572 pub layer_groups: HashMap<String, Vec<u64>>,
5573 pub streaming_sets: HashMap<String, Vec<u64>>,
5574 pub world_bounds: Aabb,
5575 pub camera_start: Vec3,
5576 pub camera_start_dir: Vec3,
5577 pub description: String,
5578}
5579
5580impl StreamingWorldComposer {
5581 pub fn new(world_name: String) -> Self {
5582 Self {
5583 world_name,
5584 base_level_ids: Vec::new(),
5585 layer_groups: HashMap::new(),
5586 streaming_sets: HashMap::new(),
5587 world_bounds: Aabb::new(-Vec3::splat(10000.0), Vec3::splat(10000.0)),
5588 camera_start: Vec3::ZERO,
5589 camera_start_dir: Vec3::NEG_Z,
5590 description: String::new(),
5591 }
5592 }
5593
5594 pub fn add_to_layer(&mut self, layer: &str, level_id: u64) {
5595 self.layer_groups.entry(layer.to_string()).or_default().push(level_id);
5596 }
5597
5598 pub fn create_streaming_set(&mut self, set_name: &str, level_ids: Vec<u64>) {
5599 self.streaming_sets.insert(set_name.to_string(), level_ids);
5600 }
5601
5602 pub fn get_streaming_set(&self, set_name: &str) -> Vec<u64> {
5603 self.streaming_sets.get(set_name).cloned().unwrap_or_default()
5604 }
5605
5606 pub fn levels_in_layer(&self, layer: &str) -> Vec<u64> {
5607 self.layer_groups.get(layer).cloned().unwrap_or_default()
5608 }
5609
5610 pub fn all_managed_level_ids(&self) -> Vec<u64> {
5611 let mut result = self.base_level_ids.clone();
5612 for ids in self.layer_groups.values() {
5613 for &id in ids {
5614 if !result.contains(&id) { result.push(id); }
5615 }
5616 }
5617 result
5618 }
5619
5620 pub fn layer_names(&self) -> Vec<&str> {
5621 self.layer_groups.keys().map(|s| s.as_str()).collect()
5622 }
5623
5624 pub fn set_world_bounds_from_levels(&mut self, levels: &[StreamingLevel]) {
5625 let managed = self.all_managed_level_ids();
5626 let mut merged = Aabb::new(Vec3::splat(f32::MAX), Vec3::splat(f32::MIN));
5627 for l in levels {
5628 if managed.contains(&l.id) {
5629 merged = merged.merge(&l.bounds);
5630 }
5631 }
5632 if merged.min.x <= merged.max.x {
5633 self.world_bounds = merged;
5634 }
5635 }
5636
5637 pub fn count_layers(&self) -> usize { self.layer_groups.len() }
5638}
5639
5640pub fn compute_cell_priority_scores(
5645 cells: &mut HashMap<CellCoord, WorldCell>,
5646 camera_pos: Vec3,
5647 camera_dir: Vec3,
5648 budget_pressure: f32,
5649) {
5650 for cell in cells.values_mut() {
5651 let _ = cell.compute_priority_score(camera_pos, camera_dir);
5652 if budget_pressure > 0.8 {
5653 cell.load_priority_score *= 1.0 - (budget_pressure - 0.8) * 2.0;
5654 }
5655 }
5656}
5657
5658pub fn cells_by_priority(cells: &HashMap<CellCoord, WorldCell>, top_n: usize) -> Vec<CellCoord> {
5659 let mut sorted: Vec<(&CellCoord, f32)> = cells.iter()
5660 .map(|(k, v)| (k, v.load_priority_score))
5661 .collect();
5662 sorted.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
5663 sorted.into_iter().take(top_n).map(|(k, _)| *k).collect()
5664}
5665
5666pub fn estimate_level_load_time_ms(
5667 size_bytes: u64,
5668 bandwidth_mb_s: f32,
5669 decompression_factor: f32,
5670) -> f32 {
5671 if bandwidth_mb_s <= 0.0 { return f32::MAX; }
5672 let mb = size_bytes as f32 / (1024.0 * 1024.0);
5673 let io_time_ms = mb / bandwidth_mb_s * 1000.0;
5674 let decomp_time_ms = mb * decompression_factor;
5675 io_time_ms + decomp_time_ms
5676}
5677
5678pub fn lru_eviction_order(
5679 levels: &[StreamingLevel],
5680 lru_order: &VecDeque<u64>,
5681) -> Vec<u64> {
5682 let mut result = Vec::new();
5683 for &id in lru_order.iter() {
5684 if let Some(l) = levels.iter().find(|l| l.id == id) {
5685 if l.state == StreamingState::Loaded && l.persistence != LevelPersistence::AlwaysLoaded {
5686 result.push(id);
5687 }
5688 }
5689 }
5690 result
5691}
5692
5693pub fn should_stream_via_portal(
5694 portal: &Portal,
5695 camera_pos: Vec3,
5696 max_portal_stream_dist: f32,
5697) -> bool {
5698 let dist = (portal.center - camera_pos).length();
5699 portal.is_open && dist < max_portal_stream_dist
5700}
5701
5702pub fn occlusion_cull_sectors(
5703 sectors: &[u64],
5704 visible_pvs: &HashSet<u64>,
5705) -> (Vec<u64>, Vec<u64>) {
5706 let mut visible = Vec::new();
5707 let mut culled = Vec::new();
5708 for &id in sectors {
5709 if visible_pvs.contains(&id) { visible.push(id); } else { culled.push(id); }
5710 }
5711 (visible, culled)
5712}
5713
5714pub fn compute_portal_screen_coverage(portal: &Portal, camera_pos: Vec3, fov_y: f32) -> f32 {
5715 let dist = (portal.center - camera_pos).length().max(0.01);
5716 let angular_h = 2.0 * (portal.half_extents.x / dist).atan();
5717 let angular_v = 2.0 * (portal.half_extents.y / dist).atan();
5718 (angular_h / fov_y).min(1.0) * (angular_v / fov_y).min(1.0)
5719}
5720
5721pub fn compute_streaming_jitter(load_times: &[f32]) -> f32 {
5722 if load_times.len() < 2 { return 0.0; }
5723 let mean = load_times.iter().sum::<f32>() / load_times.len() as f32;
5724 let var = load_times.iter().map(|&t| (t - mean).powi(2)).sum::<f32>() / load_times.len() as f32;
5725 var.sqrt()
5726}
5727
5728pub fn priority_weighted_sort(requests: &mut Vec<LoadRequest>) {
5729 requests.sort_by(|a, b| b.score().partial_cmp(&a.score()).unwrap_or(std::cmp::Ordering::Equal));
5730}
5731
5732pub fn build_adjacency_matrix(sectors: &HashMap<u64, Sector>) -> HashMap<(u64, u64), f32> {
5733 let mut matrix = HashMap::new();
5734 for (id, sector) in sectors {
5735 for &adj_id in §or.adjacent_sectors {
5736 let a = sector.bounds.center();
5737 let b = sectors.get(&adj_id).map(|s| s.bounds.center()).unwrap_or(Vec3::ZERO);
5738 matrix.insert((*id, adj_id), (b - a).length());
5739 }
5740 }
5741 matrix
5742}
5743
5744pub fn level_memory_breakdown(levels: &[StreamingLevel]) -> HashMap<LodLevel, f32> {
5745 let mut breakdown: HashMap<LodLevel, f32> = HashMap::new();
5746 for level in levels {
5747 if level.state == StreamingState::Loaded {
5748 *breakdown.entry(level.current_lod).or_insert(0.0) += level.memory_footprint_mb;
5749 }
5750 }
5751 breakdown
5752}
5753
5754pub fn sector_coverage_area(sector: &Sector) -> f32 {
5755 let s = sector.bounds.size();
5756 s.x * s.z
5757}
5758
5759pub fn streaming_priority_from_coverage(coverage_ratio: f32, base_priority: LoadPriority) -> LoadPriority {
5760 if coverage_ratio > 0.25 { LoadPriority::Critical }
5761 else if coverage_ratio > 0.1 { LoadPriority::High }
5762 else if coverage_ratio > 0.01 { LoadPriority::Medium }
5763 else { base_priority }
5764}
5765
5766pub fn sector_transition_fade_curve(progress: f32, transition: SectorTransitionType) -> f32 {
5767 match transition {
5768 SectorTransitionType::Immediate => 1.0,
5769 SectorTransitionType::Fade => {
5770 if progress < 0.5 { progress * 2.0 } else { (1.0 - progress) * 2.0 }
5771 }
5772 SectorTransitionType::Portal => { let t = progress; t * t * (3.0 - 2.0 * t) }
5773 SectorTransitionType::Teleport => { if progress < 0.1 || progress > 0.9 { 0.0 } else { 1.0 } }
5774 }
5775}
5776
5777impl PartialOrd for CellCoord {
5779 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { Some(self.cmp(other)) }
5780}
5781impl Ord for CellCoord {
5782 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
5783 self.x.cmp(&other.x).then(self.y.cmp(&other.y)).then(self.z.cmp(&other.z))
5784 }
5785}
5786
5787pub fn cells_to_activate(
5788 camera_pos: Vec3,
5789 camera_vel: Vec3,
5790 cell_size: f32,
5791 base_radius: f32,
5792 lookahead_s: f32,
5793 grid_origin: Vec3,
5794) -> Vec<CellCoord> {
5795 let predicted = camera_pos + camera_vel * lookahead_s;
5796 let world_to_coord = |p: Vec3| {
5797 let rel = p - grid_origin;
5798 CellCoord::new((rel.x / cell_size).floor() as i32, (rel.z / cell_size).floor() as i32, (rel.y / cell_size).floor() as i32)
5799 };
5800 let center = world_to_coord(camera_pos);
5801 let pred = world_to_coord(predicted);
5802 let cell_r = (base_radius / cell_size).ceil() as i32 + 1;
5803 let mut coords = Vec::new();
5804 for &base in &[center, pred] {
5805 for dz in -cell_r..=cell_r {
5806 for dx in -cell_r..=cell_r {
5807 coords.push(CellCoord::new(base.x + dx, base.y, base.z + dz));
5808 }
5809 }
5810 }
5811 coords.sort();
5812 coords.dedup();
5813 coords
5814}
5815
5816#[derive(Debug)]
5821pub struct StreamingWorldManager {
5822 pub editor: FullLevelStreamingEditor,
5823 pub dynamic_tracker: DynamicObjectTracker,
5824 pub composer: StreamingWorldComposer,
5825 pub roi_manager: RegionOfInterestManager,
5826 pub checkpoint_mgr: CheckpointManager,
5827 pub lod_transitions: LodTransitionManager,
5828 pub analytics: StreamingAnalyticsSession,
5829 pub asset_catalogue: LevelAssetCatalogue,
5830 pub command_history: CommandHistory,
5831 pub distance_cache: StreamingDistanceCache,
5832 pub settings_panel: LevelStreamingSettingsPanel,
5833 pub pathfinder: Option<SectorWaypointPathfinder>,
5834 pub flow_state: WorldFlowState,
5835}
5836
5837#[derive(Debug, Default, Clone)]
5838pub struct WorldFlowState {
5839 pub is_loading_world: bool,
5840 pub load_progress: f32,
5841 pub current_phase: String,
5842 pub errors: Vec<String>,
5843 pub warnings: Vec<String>,
5844 pub is_simulation_mode: bool,
5845 pub last_checkpoint_id: Option<u64>,
5846}
5847
5848impl StreamingWorldManager {
5849 pub fn new(world_name: String) -> Self {
5850 let config = LevelStreamingEditorConfig::default();
5851 let settings = LevelStreamingSettingsPanel::new(&config);
5852 let editor = FullLevelStreamingEditor::new(config);
5853 Self {
5854 editor,
5855 dynamic_tracker: DynamicObjectTracker::new(DEFAULT_CELL_SIZE),
5856 composer: StreamingWorldComposer::new(world_name),
5857 roi_manager: RegionOfInterestManager::new(),
5858 checkpoint_mgr: CheckpointManager::new(),
5859 lod_transitions: LodTransitionManager::new(),
5860 analytics: StreamingAnalyticsSession::new(1, 0.0),
5861 asset_catalogue: LevelAssetCatalogue::new(),
5862 command_history: CommandHistory::new(128),
5863 distance_cache: StreamingDistanceCache::new(),
5864 settings_panel: settings,
5865 pathfinder: None,
5866 flow_state: WorldFlowState::default(),
5867 }
5868 }
5869
5870 pub fn tick(&mut self, dt_s: f32) {
5871 let cam_pos = self.editor.core.camera_position;
5872 let cam_dir = self.editor.core.camera_direction;
5873 let time_ms = self.editor.core.current_time_ms;
5874
5875 self.editor.tick(dt_s);
5876 self.roi_manager.update(cam_pos, time_ms);
5877 self.lod_transitions.update(dt_s);
5878
5879 let levels_vec: Vec<StreamingLevel> = self.editor.core.levels.values().cloned().collect();
5880 let memory_mb = self.editor.core.memory_manager.used_mb;
5881 self.analytics.update_peak_memory(memory_mb);
5882
5883 let _ = self.checkpoint_mgr.maybe_auto_checkpoint(cam_pos, cam_dir, &levels_vec, memory_mb, time_ms);
5884 self.distance_cache.update(cam_pos, &levels_vec, self.editor.core.current_frame);
5885
5886 if self.settings_panel.has_unsaved_changes() {
5887 self.settings_panel.apply_to_config(&mut self.editor.core.config);
5888 self.editor.core.set_memory_budget(self.editor.core.config.memory_budget_mb);
5889 }
5890
5891 let completed: Vec<(u64, LodLevel)> = self.lod_transitions.completed.drain(..).collect();
5892 for (level_id, new_lod) in completed {
5893 if let Some(level) = self.editor.core.levels.get_mut(&level_id) {
5894 level.current_lod = new_lod;
5895 }
5896 }
5897
5898 if self.pathfinder.is_none() && !self.editor.core.sector_graph.sectors.is_empty() {
5899 self.pathfinder = Some(SectorWaypointPathfinder::new(&self.editor.core.sector_graph));
5900 }
5901 }
5902
5903 pub fn do_command(&mut self, cmd: StreamingEditorCommand) {
5904 apply_streaming_command(&mut self.editor, &cmd);
5905 self.command_history.push(cmd);
5906 }
5907
5908 pub fn undo(&mut self) {
5909 if let Some(cmd) = self.command_history.undo() {
5910 undo_streaming_command(&mut self.editor, &cmd);
5911 }
5912 }
5913
5914 pub fn redo(&mut self) {
5915 if let Some(cmd) = self.command_history.redo() {
5916 apply_streaming_command(&mut self.editor, &cmd);
5917 }
5918 }
5919
5920 pub fn save_checkpoint(&mut self) -> Option<u64> {
5921 let cam_pos = self.editor.core.camera_position;
5922 let cam_dir = self.editor.core.camera_direction;
5923 let levels_vec: Vec<StreamingLevel> = self.editor.core.levels.values().cloned().collect();
5924 let memory_mb = self.editor.core.memory_manager.used_mb;
5925 let time_ms = self.editor.core.current_time_ms;
5926 let id = self.checkpoint_mgr.save("Manual".into(), cam_pos, cam_dir, &levels_vec, memory_mb, time_ms);
5927 self.flow_state.last_checkpoint_id = Some(id);
5928 Some(id)
5929 }
5930
5931 pub fn find_path_to_sector(&self, from_sector: u64, to_sector: u64) -> Option<Vec<u64>> {
5932 self.pathfinder.as_ref()?.find_sector_path(from_sector, to_sector)
5933 }
5934
5935 pub fn world_report(&self) -> WorldStreamingReport {
5936 let core_report = self.editor.core.get_streaming_report();
5937 WorldStreamingReport {
5938 core: core_report,
5939 dynamic_objects: self.dynamic_tracker.total_objects(),
5940 active_roi_count: self.roi_manager.active_regions.len(),
5941 lod_transitions_active: self.lod_transitions.active_count(),
5942 analytics_total_loads: self.analytics.total_loads,
5943 analytics_bandwidth_mb_s: self.analytics.average_bandwidth_mb_s(),
5944 asset_catalogue_mb: self.asset_catalogue.total_size_mb(),
5945 has_unsaved_settings: self.settings_panel.has_unsaved_changes(),
5946 can_undo: self.command_history.can_undo(),
5947 can_redo: self.command_history.can_redo(),
5948 }
5949 }
5950
5951 pub fn spawn_dynamic_object(&mut self, name: String, pos: Vec3, radius: f32) -> u64 {
5952 self.dynamic_tracker.spawn(name, pos, radius)
5953 }
5954
5955 pub fn update_dynamic_object(&mut self, id: u64, new_pos: Vec3, dt_s: f32) {
5956 let time_ms = self.editor.core.current_time_ms;
5957 self.dynamic_tracker.update_position(id, new_pos, dt_s, time_ms);
5958 }
5959
5960 pub fn get_levels_to_stream_for_object(&self, object_id: u64, extra_radius: f32) -> Vec<u64> {
5961 if let Some(obj) = self.dynamic_tracker.objects.get(&object_id) {
5962 return self.editor.core.query_levels_near(obj.position, obj.bounds_radius + extra_radius);
5963 }
5964 Vec::new()
5965 }
5966
5967 pub fn set_simulation_speed(&mut self, speed: f32) {
5968 self.editor.core.simulator.playback_speed = speed.max(0.0);
5969 self.flow_state.is_simulation_mode = speed > 0.0 && self.editor.core.simulator.is_running;
5970 }
5971
5972 pub fn full_reset(&mut self) {
5973 self.editor.core.reset_simulation();
5974 self.analytics = StreamingAnalyticsSession::new(self.analytics.session_id + 1, self.editor.core.current_time_ms);
5975 self.flow_state = WorldFlowState::default();
5976 }
5977}
5978
5979#[derive(Debug, Clone)]
5980pub struct WorldStreamingReport {
5981 pub core: StreamingReport,
5982 pub dynamic_objects: usize,
5983 pub active_roi_count: usize,
5984 pub lod_transitions_active: usize,
5985 pub analytics_total_loads: u32,
5986 pub analytics_bandwidth_mb_s: f32,
5987 pub asset_catalogue_mb: f32,
5988 pub has_unsaved_settings: bool,
5989 pub can_undo: bool,
5990 pub can_redo: bool,
5991}
5992
5993#[cfg(test)]
5998mod extended_tests {
5999 use super::*;
6000
6001 #[test]
6002 fn test_dynamic_object_tracker() {
6003 let mut tracker = DynamicObjectTracker::new(512.0);
6004 let id = tracker.spawn("Npc1".into(), Vec3::ZERO, 1.0);
6005 assert!(tracker.objects.contains_key(&id));
6006 tracker.update_position(id, Vec3::new(600.0, 0.0, 0.0), 1.0, 1000.0);
6007 let obj = &tracker.objects[&id];
6008 assert_ne!(obj.current_cell, CellCoord::new(0, 0, 0));
6009 }
6010
6011 #[test]
6012 fn test_streaming_world_manager_tick() {
6013 let mut mgr = StreamingWorldManager::new("TestWorld".into());
6014 mgr.editor.core.update_camera(Vec3::new(100.0, 0.0, 100.0), Vec3::NEG_Z, Mat4::IDENTITY);
6015 mgr.tick(0.016);
6016 let report = mgr.world_report();
6017 assert_eq!(report.core.total_levels, 0);
6018 }
6019
6020 #[test]
6021 fn test_distance_cache() {
6022 let mut cache = StreamingDistanceCache::new();
6023 let asset = StreamingLevelAsset { id:1, name:"a".into(), file_path:"".into(),
6024 size_bytes:0, uncompressed_size_bytes:0, dependencies:vec![], load_time_estimate_ms:0.0 };
6025 let level = StreamingLevel::new(1,"a".into(),asset,Aabb::new(Vec3::new(100.,0.,0.),Vec3::new(200.,10.,10.)));
6026 cache.update(Vec3::ZERO, &[level], 1);
6027 assert!(cache.get(1).is_some());
6028 let dist = cache.get(1).unwrap();
6029 assert!((dist - 100.0).abs() < 1.0);
6030 }
6031
6032 #[test]
6033 fn test_lod_transition_smooth_step() {
6034 let mut t = LodTransition::new(1, LodLevel::Lod0, LodLevel::Lod1, 1.0);
6035 t.progress = 0.5;
6036 let alpha = t.blend_alpha();
6037 assert!((alpha - 0.5).abs() < 0.01);
6039 t.progress = 0.0;
6040 assert!((t.blend_alpha() - 0.0).abs() < 0.01);
6041 t.progress = 1.0;
6042 assert!((t.blend_alpha() - 1.0).abs() < 0.01);
6043 }
6044
6045 #[test]
6046 fn test_analytics_session() {
6047 let mut session = StreamingAnalyticsSession::new(1, 0.0);
6048 session.record_load(1, 250.0, 50.0);
6049 session.record_load(2, 150.0, 30.0);
6050 assert_eq!(session.total_loads, 2);
6051 assert!((session.average_load_latency_ms - 200.0).abs() < 1.0);
6052 assert_eq!(session.unique_levels_loaded.len(), 2);
6053 }
6054
6055 #[test]
6056 fn test_region_of_interest() {
6057 let mut mgr = RegionOfInterestManager::new();
6058 let bounds = Aabb::new(Vec3::ZERO, Vec3::splat(100.0));
6059 let id = mgr.add_region("Combat".into(), bounds, LoadPriority::High);
6060 mgr.update(Vec3::new(50.0, 0.0, 50.0), 0.0);
6061 assert!(mgr.active_regions.contains(&id));
6062 mgr.update(Vec3::new(500.0, 0.0, 500.0), 100.0);
6063 assert!(!mgr.active_regions.contains(&id));
6064 }
6065
6066 #[test]
6067 fn test_checkpoint_save_restore() {
6068 let mut mgr = CheckpointManager::new();
6069 let id = mgr.save("Test".into(), Vec3::ZERO, Vec3::NEG_Z, &[], 128.0, 1000.0);
6070 let cp = mgr.checkpoints.get(&id).unwrap();
6071 assert_eq!(cp.memory_used_mb, 128.0);
6072 assert_eq!(cp.loaded_level_ids.len(), 0);
6073 }
6074
6075 #[test]
6076 fn test_sector_pathfinding() {
6077 let mut sg = SectorGraph::new();
6078 sg.add_sector(Sector::new(1,"A".into(),Aabb::new(Vec3::ZERO,Vec3::splat(10.))));
6079 sg.add_sector(Sector::new(2,"B".into(),Aabb::new(Vec3::splat(10.),Vec3::splat(20.))));
6080 sg.add_sector(Sector::new(3,"C".into(),Aabb::new(Vec3::splat(20.),Vec3::splat(30.))));
6081 sg.add_portal(Portal::new(1,1,2,Vec3::new(10.,5.,5.),Vec3::X,Vec2::splat(2.)));
6082 sg.add_portal(Portal::new(2,2,3,Vec3::new(20.,5.,5.),Vec3::X,Vec2::splat(2.)));
6083 let pf = SectorWaypointPathfinder::new(&sg);
6084 let path = pf.find_sector_path(1,3).unwrap();
6085 assert_eq!(path, vec![1,2,3]);
6086 }
6087
6088 #[test]
6089 fn test_asset_catalogue() {
6090 let mut cat = LevelAssetCatalogue::new();
6091 let asset = StreamingLevelAsset { id:1, name:"Forest".into(), file_path:"".into(),
6092 size_bytes:1024*1024, uncompressed_size_bytes:0, dependencies:vec![], load_time_estimate_ms:200.0 };
6093 cat.register(asset);
6094 cat.tag_asset(1,"outdoor");
6095 assert_eq!(cat.assets_by_tag("outdoor").len(), 1);
6096 assert!((cat.total_size_mb() - 1.0).abs() < 0.01);
6097 }
6098
6099 #[test]
6100 fn test_cells_to_activate_velocity() {
6101 let coords = cells_to_activate(
6102 Vec3::ZERO, Vec3::new(20.0,0.0,0.0), 512.0, 256.0, 2.0, Vec3::ZERO
6103 );
6104 assert!(!coords.is_empty());
6105 let mut sorted = coords.clone();
6107 sorted.sort();
6108 sorted.dedup();
6109 assert_eq!(sorted.len(), coords.len());
6110 }
6111
6112 #[test]
6113 fn test_streaming_flow_optimizer_batch_size() {
6114 let batch = StreamingFlowOptimizer::optimal_batch_size(100.0, 25.0, 500.0);
6115 assert!(batch >= 1 && batch <= MAX_CONCURRENT_LOADS);
6116 }
6117
6118 #[test]
6119 fn test_world_composer_layers() {
6120 let mut composer = StreamingWorldComposer::new("World".into());
6121 composer.add_to_layer("terrain", 1);
6122 composer.add_to_layer("terrain", 2);
6123 composer.add_to_layer("buildings", 3);
6124 assert_eq!(composer.levels_in_layer("terrain").len(), 2);
6125 assert_eq!(composer.all_managed_level_ids().len(), 3);
6126 assert_eq!(composer.count_layers(), 2);
6127 }
6128
6129 #[test]
6130 fn test_lod_analysis() {
6131 let asset = StreamingLevelAsset { id:1, name:"T".into(), file_path:"".into(),
6132 size_bytes:10*1024*1024, uncompressed_size_bytes:0, dependencies:vec![], load_time_estimate_ms:0.0 };
6133 let mut level = StreamingLevel::new(1,"T".into(),asset,Aabb::new(Vec3::ZERO,Vec3::splat(100.)));
6134 level.distance_to_camera = 50.0;
6135 level.current_lod = LodLevel::Lod0;
6136 let mgr = CombinedBudgetManager::new(1000.0);
6137 let analysis = analyze_lod_distribution(&[level.clone()], &mgr);
6138 assert_eq!(analysis.len(), 1);
6139 assert_eq!(analysis[0].current_lod, LodLevel::Lod0);
6140 }
6141
6142 #[test]
6143 fn test_streaming_importance() {
6144 let asset = StreamingLevelAsset { id:1, name:"T".into(), file_path:"".into(),
6145 size_bytes:0, uncompressed_size_bytes:0, dependencies:vec![], load_time_estimate_ms:0.0 };
6146 let level = StreamingLevel::new(1,"T".into(),asset,Aabb::new(Vec3::new(50.,0.,0.),Vec3::new(150.,50.,50.)));
6147 let importance = compute_streaming_importance(
6148 &level, Vec3::ZERO, Vec3::X, 5.0
6149 );
6150 assert!(importance.is_finite() && importance > 0.0);
6151 }
6152
6153 #[test]
6154 fn test_hysteresis() {
6155 assert!(hysteresis_check_load(80.0, 100.0, 10.0));
6156 assert!(!hysteresis_check_load(95.0, 100.0, 10.0));
6157 assert!(hysteresis_check_unload(125.0, 110.0, 10.0));
6158 assert!(!hysteresis_check_unload(115.0, 110.0, 10.0));
6159 }
6160}
6161
6162#[derive(Clone, Debug)]
6167pub struct StreamingTile {
6168 pub tile_x: i32,
6169 pub tile_y: i32,
6170 pub tile_size_world: f32,
6171 pub level_ids: Vec<u64>,
6172 pub terrain_height_min: f32,
6173 pub terrain_height_max: f32,
6174 pub is_water: bool,
6175 pub biome_id: u32,
6176 pub detail_density: f32,
6177 pub last_visited_time: f64,
6178}
6179
6180impl StreamingTile {
6181 pub fn new(tile_x: i32, tile_y: i32, tile_size: f32) -> Self {
6182 Self {
6183 tile_x,
6184 tile_y,
6185 tile_size_world: tile_size,
6186 level_ids: Vec::new(),
6187 terrain_height_min: 0.0,
6188 terrain_height_max: 100.0,
6189 is_water: false,
6190 biome_id: 0,
6191 detail_density: 1.0,
6192 last_visited_time: 0.0,
6193 }
6194 }
6195
6196 pub fn world_center(&self) -> Vec3 {
6197 Vec3::new(
6198 (self.tile_x as f32 + 0.5) * self.tile_size_world,
6199 (self.terrain_height_min + self.terrain_height_max) * 0.5,
6200 (self.tile_y as f32 + 0.5) * self.tile_size_world,
6201 )
6202 }
6203
6204 pub fn world_bounds(&self) -> Aabb {
6205 let min = Vec3::new(
6206 self.tile_x as f32 * self.tile_size_world,
6207 self.terrain_height_min,
6208 self.tile_y as f32 * self.tile_size_world,
6209 );
6210 let max = Vec3::new(
6211 (self.tile_x + 1) as f32 * self.tile_size_world,
6212 self.terrain_height_max,
6213 (self.tile_y + 1) as f32 * self.tile_size_world,
6214 );
6215 Aabb::new(min, max)
6216 }
6217
6218 pub fn distance_to_point(&self, point: Vec3) -> f32 {
6219 let center = self.world_center();
6220 let half = self.tile_size_world * 0.5;
6221 let dx = (center.x - point.x).abs() - half;
6222 let dz = (center.z - point.z).abs() - half;
6223 (dx.max(0.0) * dx.max(0.0) + dz.max(0.0) * dz.max(0.0)).sqrt()
6224 }
6225
6226 pub fn contains_point_2d(&self, x: f32, z: f32) -> bool {
6227 let min_x = self.tile_x as f32 * self.tile_size_world;
6228 let min_z = self.tile_y as f32 * self.tile_size_world;
6229 let max_x = min_x + self.tile_size_world;
6230 let max_z = min_z + self.tile_size_world;
6231 x >= min_x && x < max_x && z >= min_z && z < max_z
6232 }
6233}
6234
6235#[derive(Clone, Debug)]
6236pub struct StreamingTileMap {
6237 pub tile_size_world: f32,
6238 pub tiles: HashMap<(i32, i32), StreamingTile>,
6239 pub world_origin: Vec3,
6240 pub max_tiles: usize,
6241}
6242
6243impl StreamingTileMap {
6244 pub fn new(tile_size_world: f32) -> Self {
6245 Self {
6246 tile_size_world,
6247 tiles: HashMap::new(),
6248 world_origin: Vec3::ZERO,
6249 max_tiles: 1024,
6250 }
6251 }
6252
6253 pub fn world_to_tile(&self, world_pos: Vec3) -> (i32, i32) {
6254 let tx = ((world_pos.x - self.world_origin.x) / self.tile_size_world).floor() as i32;
6255 let tz = ((world_pos.z - self.world_origin.z) / self.tile_size_world).floor() as i32;
6256 (tx, tz)
6257 }
6258
6259 pub fn get_or_create(&mut self, tile_x: i32, tile_y: i32) -> &mut StreamingTile {
6260 self.tiles.entry((tile_x, tile_y)).or_insert_with(|| {
6261 StreamingTile::new(tile_x, tile_y, self.tile_size_world)
6262 })
6263 }
6264
6265 pub fn get(&self, tile_x: i32, tile_y: i32) -> Option<&StreamingTile> {
6266 self.tiles.get(&(tile_x, tile_y))
6267 }
6268
6269 pub fn tiles_in_radius(&self, center: Vec3, radius: f32) -> Vec<(i32, i32)> {
6270 let tile_radius = (radius / self.tile_size_world).ceil() as i32 + 1;
6271 let (cx, cz) = self.world_to_tile(center);
6272 let mut result = Vec::new();
6273 for tx in (cx - tile_radius)..=(cx + tile_radius) {
6274 for tz in (cz - tile_radius)..=(cz + tile_radius) {
6275 if let Some(tile) = self.tiles.get(&(tx, tz)) {
6276 if tile.distance_to_point(center) <= radius {
6277 result.push((tx, tz));
6278 }
6279 } else {
6280 let half = self.tile_size_world * 0.5;
6282 let tc_x = (tx as f32 + 0.5) * self.tile_size_world;
6283 let tc_z = (tz as f32 + 0.5) * self.tile_size_world;
6284 let dx = (center.x - tc_x).abs() - half;
6285 let dz = (center.z - tc_z).abs() - half;
6286 let dist = (dx.max(0.0).powi(2) + dz.max(0.0).powi(2)).sqrt();
6287 if dist <= radius {
6288 result.push((tx, tz));
6289 }
6290 }
6291 }
6292 }
6293 result
6294 }
6295
6296 pub fn tiles_in_frustum(&self, frustum: &Frustum) -> Vec<(i32, i32)> {
6297 self.tiles.iter()
6298 .filter(|(_, tile)| frustum.test_aabb(&tile.world_bounds()))
6299 .map(|(&k, _)| k)
6300 .collect()
6301 }
6302
6303 pub fn add_level_to_tile(&mut self, tile_x: i32, tile_y: i32, level_id: u64) {
6304 let tile = self.get_or_create(tile_x, tile_y);
6305 if !tile.level_ids.contains(&level_id) {
6306 tile.level_ids.push(level_id);
6307 }
6308 }
6309
6310 pub fn remove_level_from_all(&mut self, level_id: u64) {
6311 for tile in self.tiles.values_mut() {
6312 tile.level_ids.retain(|&id| id != level_id);
6313 }
6314 }
6315
6316 pub fn tile_count(&self) -> usize {
6317 self.tiles.len()
6318 }
6319
6320 pub fn stale_tiles(&self, current_time: f64, max_age_seconds: f64) -> Vec<(i32, i32)> {
6321 self.tiles.iter()
6322 .filter(|(_, t)| current_time - t.last_visited_time > max_age_seconds)
6323 .map(|(&k, _)| k)
6324 .collect()
6325 }
6326
6327 pub fn evict_stale(&mut self, current_time: f64, max_age_seconds: f64) -> usize {
6328 let stale = self.stale_tiles(current_time, max_age_seconds);
6329 let count = stale.len();
6330 for key in stale {
6331 self.tiles.remove(&key);
6332 }
6333 count
6334 }
6335}
6336
6337#[derive(Clone, Debug)]
6342pub struct LevelInstancer {
6343 pub instances: HashMap<u64, LevelInstance>,
6344 pub next_instance_id: u64,
6345 pub spatial_index: HashMap<(i32, i32), Vec<u64>>, pub tile_size: f32,
6347}
6348
6349impl LevelInstancer {
6350 pub fn new(tile_size: f32) -> Self {
6351 Self {
6352 instances: HashMap::new(),
6353 next_instance_id: 1,
6354 spatial_index: HashMap::new(),
6355 tile_size,
6356 }
6357 }
6358
6359 pub fn place_instance(&mut self, level_id: u64, transform: Mat4) -> u64 {
6360 let id = self.next_instance_id;
6361 self.next_instance_id += 1;
6362 let instance = LevelInstance::new(id, level_id, transform);
6363 let tile_key = self.world_to_tile(instance.position());
6364 self.spatial_index.entry(tile_key).or_insert_with(Vec::new).push(id);
6365 self.instances.insert(id, instance);
6366 id
6367 }
6368
6369 fn world_to_tile(&self, pos: Vec3) -> (i32, i32) {
6370 ((pos.x / self.tile_size).floor() as i32,
6371 (pos.z / self.tile_size).floor() as i32)
6372 }
6373
6374 pub fn remove_instance(&mut self, id: u64) -> Option<LevelInstance> {
6375 if let Some(inst) = self.instances.remove(&id) {
6376 let tile_key = self.world_to_tile(inst.position());
6377 if let Some(list) = self.spatial_index.get_mut(&tile_key) {
6378 list.retain(|&i| i != id);
6379 }
6380 Some(inst)
6381 } else {
6382 None
6383 }
6384 }
6385
6386 pub fn instances_in_radius(&self, center: Vec3, radius: f32) -> Vec<u64> {
6387 let tile_r = (radius / self.tile_size).ceil() as i32 + 1;
6388 let (cx, cz) = self.world_to_tile(center);
6389 let mut result = Vec::new();
6390 for tx in (cx - tile_r)..=(cx + tile_r) {
6391 for tz in (cz - tile_r)..=(cz + tile_r) {
6392 if let Some(ids) = self.spatial_index.get(&(tx, tz)) {
6393 for &id in ids {
6394 if let Some(inst) = self.instances.get(&id) {
6395 let dist = (inst.position() - center).length();
6396 if dist <= radius {
6397 result.push(id);
6398 }
6399 }
6400 }
6401 }
6402 }
6403 }
6404 result
6405 }
6406
6407 pub fn instances_with_tag(&self, tag: &str) -> Vec<u64> {
6408 self.instances.values()
6409 .filter(|i| i.has_tag(tag))
6410 .map(|i| i.instance_id)
6411 .collect()
6412 }
6413
6414 pub fn instances_for_level(&self, level_id: u64) -> Vec<u64> {
6415 self.instances.values()
6416 .filter(|i| i.level_id == level_id)
6417 .map(|i| i.instance_id)
6418 .collect()
6419 }
6420
6421 pub fn update_transform(&mut self, id: u64, new_transform: Mat4) {
6422 let old_pos = self.instances.get(&id).map(|i| i.position());
6423 if let Some(inst) = self.instances.get_mut(&id) {
6424 inst.transform = new_transform;
6425 inst.last_modified_time += 0.001;
6426 }
6427 if let Some(old_pos) = old_pos {
6428 let old_tile = self.world_to_tile(old_pos);
6429 let new_pos = self.instances.get(&id).map(|i| i.position()).unwrap_or(old_pos);
6430 let new_tile = self.world_to_tile(new_pos);
6431 if old_tile != new_tile {
6432 if let Some(list) = self.spatial_index.get_mut(&old_tile) {
6433 list.retain(|&i| i != id);
6434 }
6435 self.spatial_index.entry(new_tile).or_insert_with(Vec::new).push(id);
6436 }
6437 }
6438 }
6439
6440 pub fn count_by_level(&self) -> HashMap<u64, usize> {
6441 let mut counts: HashMap<u64, usize> = HashMap::new();
6442 for inst in self.instances.values() {
6443 *counts.entry(inst.level_id).or_insert(0) += 1;
6444 }
6445 counts
6446 }
6447
6448 pub fn visible_instances_in_frustum(&self, frustum: &Frustum, level_bounds: &HashMap<u64, Aabb>) -> Vec<u64> {
6449 self.instances.values()
6450 .filter(|inst| {
6451 if !inst.visible { return false; }
6452 if let Some(bounds) = level_bounds.get(&inst.level_id) {
6453 let pos = inst.position();
6455 let translated = Aabb::new(bounds.min + pos, bounds.max + pos);
6456 frustum.test_aabb(&translated)
6457 } else {
6458 true
6459 }
6460 })
6461 .map(|i| i.instance_id)
6462 .collect()
6463 }
6464}
6465
6466#[derive(Clone, Debug)]
6471pub struct TerrainPatch {
6472 pub patch_id: u32,
6473 pub grid_x: i32,
6474 pub grid_z: i32,
6475 pub patch_size: f32,
6476 pub current_lod: u32,
6477 pub max_lod: u32,
6478 pub height_data: Vec<f32>, pub height_grid_res: u32, pub vertex_count: u32,
6481 pub is_stitched: bool,
6482 pub neighbor_lods: [u32; 4], pub morph_fraction: f32, }
6485
6486impl TerrainPatch {
6487 pub fn new(patch_id: u32, grid_x: i32, grid_z: i32, patch_size: f32, max_lod: u32) -> Self {
6488 let base_res = 64u32;
6489 let res = base_res;
6490 let height_data = vec![0.0f32; (res * res) as usize];
6491 Self {
6492 patch_id,
6493 grid_x,
6494 grid_z,
6495 patch_size,
6496 current_lod: 0,
6497 max_lod,
6498 height_data,
6499 height_grid_res: res,
6500 vertex_count: res * res,
6501 is_stitched: false,
6502 neighbor_lods: [0; 4],
6503 morph_fraction: 0.0,
6504 }
6505 }
6506
6507 pub fn world_position(&self) -> Vec3 {
6508 Vec3::new(
6509 self.grid_x as f32 * self.patch_size,
6510 0.0,
6511 self.grid_z as f32 * self.patch_size,
6512 )
6513 }
6514
6515 pub fn world_bounds(&self) -> Aabb {
6516 let min_x = self.grid_x as f32 * self.patch_size;
6517 let min_z = self.grid_z as f32 * self.patch_size;
6518 let h_min = self.height_data.iter().copied().fold(f32::INFINITY, f32::min);
6519 let h_max = self.height_data.iter().copied().fold(f32::NEG_INFINITY, f32::max);
6520 Aabb::new(
6521 Vec3::new(min_x, h_min, min_z),
6522 Vec3::new(min_x + self.patch_size, h_max, min_z + self.patch_size),
6523 )
6524 }
6525
6526 pub fn sample_height_bilinear(&self, local_x: f32, local_z: f32) -> f32 {
6527 let res = self.height_grid_res as f32;
6529 let u = (local_x / self.patch_size) * (res - 1.0);
6530 let v = (local_z / self.patch_size) * (res - 1.0);
6531 let x0 = u.floor() as usize;
6532 let z0 = v.floor() as usize;
6533 let x1 = (x0 + 1).min(self.height_grid_res as usize - 1);
6534 let z1 = (z0 + 1).min(self.height_grid_res as usize - 1);
6535 let fx = u - u.floor();
6536 let fz = v - v.floor();
6537 let res_u = self.height_grid_res as usize;
6538 let h00 = self.height_data[z0 * res_u + x0];
6539 let h10 = self.height_data[z0 * res_u + x1];
6540 let h01 = self.height_data[z1 * res_u + x0];
6541 let h11 = self.height_data[z1 * res_u + x1];
6542 h00 * (1.0 - fx) * (1.0 - fz)
6543 + h10 * fx * (1.0 - fz)
6544 + h01 * (1.0 - fx) * fz
6545 + h11 * fx * fz
6546 }
6547
6548 pub fn compute_normal_at(&self, local_x: f32, local_z: f32) -> Vec3 {
6549 let step = self.patch_size / self.height_grid_res as f32;
6550 let hx_plus = self.sample_height_bilinear((local_x + step).min(self.patch_size), local_z);
6551 let hx_minus = self.sample_height_bilinear((local_x - step).max(0.0), local_z);
6552 let hz_plus = self.sample_height_bilinear(local_x, (local_z + step).min(self.patch_size));
6553 let hz_minus = self.sample_height_bilinear(local_x, (local_z - step).max(0.0));
6554 let grad_x = (hx_plus - hx_minus) / (2.0 * step);
6555 let grad_z = (hz_plus - hz_minus) / (2.0 * step);
6556 Vec3::new(-grad_x, 1.0, -grad_z).normalize()
6557 }
6558
6559 pub fn desired_lod_for_distance(&self, distance: f32) -> u32 {
6560 let thresholds = [50.0, 150.0, 400.0, 900.0, 2000.0];
6561 for (lod, &threshold) in thresholds.iter().enumerate() {
6562 if distance < threshold {
6563 return lod as u32;
6564 }
6565 }
6566 self.max_lod
6567 }
6568
6569 pub fn update_lod(&mut self, camera_pos: Vec3) {
6570 let center = self.world_position() + Vec3::splat(self.patch_size * 0.5);
6571 let dist = (camera_pos - center).length();
6572 let desired = self.desired_lod_for_distance(dist);
6573 if desired != self.current_lod {
6574 self.morph_fraction = 0.0;
6575 } else {
6576 self.morph_fraction = (self.morph_fraction + 0.05).min(1.0);
6577 }
6578 self.current_lod = desired.min(self.max_lod);
6579 let step = 1u32 << self.current_lod;
6581 let reduced_res = (self.height_grid_res / step).max(2);
6582 self.vertex_count = reduced_res * reduced_res;
6583 }
6584
6585 pub fn needs_stitching(&self) -> bool {
6586 self.neighbor_lods.iter().any(|&n| n != self.current_lod)
6587 }
6588
6589 pub fn stitch_skirt_vertices(&self) -> Vec<Vec3> {
6590 let mut skirt = Vec::new();
6592 let step = 1u32 << self.current_lod;
6593 let res = self.height_grid_res / step;
6594 let cell_size = self.patch_size / res as f32;
6595 let base = self.world_position();
6596 for i in 0..=res {
6598 let x = base.x + i as f32 * cell_size;
6599 let h = self.sample_height_bilinear(i as f32 * cell_size, 0.0);
6600 skirt.push(Vec3::new(x, h, base.z));
6601 skirt.push(Vec3::new(x, h - 1.0, base.z)); }
6603 skirt
6604 }
6605}
6606
6607#[derive(Clone, Debug)]
6612pub struct TerrainManager {
6613 pub patches: HashMap<(i32, i32), TerrainPatch>,
6614 pub patch_size: f32,
6615 pub max_lod: u32,
6616 pub streaming_radius: f32,
6617 pub total_vertices_rendered: u32,
6618 pub total_patches_visible: u32,
6619}
6620
6621impl TerrainManager {
6622 pub fn new(patch_size: f32, max_lod: u32, streaming_radius: f32) -> Self {
6623 Self {
6624 patches: HashMap::new(),
6625 patch_size,
6626 max_lod,
6627 streaming_radius,
6628 total_vertices_rendered: 0,
6629 total_patches_visible: 0,
6630 }
6631 }
6632
6633 pub fn get_or_create_patch(&mut self, grid_x: i32, grid_z: i32) -> &mut TerrainPatch {
6634 let sz = self.patch_size;
6635 let ml = self.max_lod;
6636 let next_id = self.patches.len() as u32 + 1;
6637 self.patches.entry((grid_x, grid_z)).or_insert_with(|| {
6638 TerrainPatch::new(next_id, grid_x, grid_z, sz, ml)
6639 })
6640 }
6641
6642 pub fn update(&mut self, camera_pos: Vec3) {
6643 let tile_r = (self.streaming_radius / self.patch_size).ceil() as i32 + 1;
6644 let cx = (camera_pos.x / self.patch_size).floor() as i32;
6645 let cz = (camera_pos.z / self.patch_size).floor() as i32;
6646 let mut total_verts = 0u32;
6647 let mut visible = 0u32;
6648 for tx in (cx - tile_r)..=(cx + tile_r) {
6649 for tz in (cz - tile_r)..=(cz + tile_r) {
6650 let center = Vec3::new(
6651 (tx as f32 + 0.5) * self.patch_size,
6652 camera_pos.y,
6653 (tz as f32 + 0.5) * self.patch_size,
6654 );
6655 let dist = (camera_pos - center).length();
6656 if dist <= self.streaming_radius {
6657 let patch = self.get_or_create_patch(tx, tz);
6658 patch.update_lod(camera_pos);
6659 total_verts += patch.vertex_count;
6660 visible += 1;
6661 }
6662 }
6663 }
6664 let keys: Vec<(i32, i32)> = self.patches.keys().copied().collect();
6666 for &(tx, tz) in &keys {
6667 let neighbors = [
6668 ((tx, tz - 1), 0usize),
6669 ((tx, tz + 1), 1),
6670 ((tx + 1, tz), 2),
6671 ((tx - 1, tz), 3),
6672 ];
6673 let my_lod = self.patches[&(tx, tz)].current_lod;
6674 let _ = my_lod;
6675 let mut nlods = [0u32; 4];
6676 for (nkey, dir) in &neighbors {
6677 nlods[*dir] = self.patches.get(nkey).map(|p| p.current_lod).unwrap_or(0);
6678 }
6679 if let Some(patch) = self.patches.get_mut(&(tx, tz)) {
6680 patch.neighbor_lods = nlods;
6681 }
6682 }
6683 self.total_vertices_rendered = total_verts;
6684 self.total_patches_visible = visible;
6685 }
6686
6687 pub fn sample_height_world(&self, world_x: f32, world_z: f32) -> f32 {
6688 let gx = (world_x / self.patch_size).floor() as i32;
6689 let gz = (world_z / self.patch_size).floor() as i32;
6690 if let Some(patch) = self.patches.get(&(gx, gz)) {
6691 let local_x = world_x - gx as f32 * self.patch_size;
6692 let local_z = world_z - gz as f32 * self.patch_size;
6693 patch.sample_height_bilinear(local_x.max(0.0), local_z.max(0.0))
6694 } else {
6695 0.0
6696 }
6697 }
6698
6699 pub fn visible_patch_count(&self) -> usize {
6700 self.patches.len()
6701 }
6702
6703 pub fn patches_needing_stitch(&self) -> Vec<(i32, i32)> {
6704 self.patches.iter()
6705 .filter(|(_, p)| p.needs_stitching())
6706 .map(|(&k, _)| k)
6707 .collect()
6708 }
6709}
6710
6711#[derive(Clone, Debug)]
6716pub struct DeadlineRequest {
6717 pub id: u64,
6718 pub priority: f32,
6719 pub deadline_seconds: f64,
6720 pub estimated_load_ms: f32,
6721 pub size_bytes: u64,
6722 pub level_id: u64,
6723 pub request_time: f64,
6724 pub cancelled: bool,
6725}
6726
6727impl DeadlineRequest {
6728 pub fn urgency_at(&self, current_time: f64) -> f32 {
6729 let remaining = (self.deadline_seconds - current_time).max(0.001) as f32;
6730 let normalized_load = self.estimated_load_ms / 1000.0;
6731 self.priority * (normalized_load / remaining).min(100.0)
6732 }
6733
6734 pub fn is_overdue(&self, current_time: f64) -> bool {
6735 current_time > self.deadline_seconds
6736 }
6737
6738 pub fn slack_ms(&self, current_time: f64) -> f32 {
6739 ((self.deadline_seconds - current_time) * 1000.0 - self.estimated_load_ms as f64).max(0.0) as f32
6740 }
6741}
6742
6743#[derive(Clone, Debug)]
6744pub struct DeadlineScheduler {
6745 pub requests: Vec<DeadlineRequest>,
6746 pub next_request_id: u64,
6747 pub total_scheduled: u64,
6748 pub total_completed: u64,
6749 pub total_missed: u64,
6750 pub bandwidth_bytes_per_sec: f64,
6751 pub inflight_bytes: u64,
6752 pub max_inflight_bytes: u64,
6753}
6754
6755impl DeadlineScheduler {
6756 pub fn new(bandwidth_bytes_per_sec: f64) -> Self {
6757 Self {
6758 requests: Vec::new(),
6759 next_request_id: 1,
6760 total_scheduled: 0,
6761 total_completed: 0,
6762 total_missed: 0,
6763 bandwidth_bytes_per_sec,
6764 inflight_bytes: 0,
6765 max_inflight_bytes: 64 * 1024 * 1024, }
6767 }
6768
6769 pub fn submit(&mut self, level_id: u64, priority: f32, deadline: f64, size_bytes: u64,
6770 estimated_load_ms: f32, current_time: f64) -> u64 {
6771 let id = self.next_request_id;
6772 self.next_request_id += 1;
6773 self.requests.push(DeadlineRequest {
6774 id,
6775 priority,
6776 deadline_seconds: deadline,
6777 estimated_load_ms,
6778 size_bytes,
6779 level_id,
6780 request_time: current_time,
6781 cancelled: false,
6782 });
6783 self.total_scheduled += 1;
6784 id
6785 }
6786
6787 pub fn cancel(&mut self, request_id: u64) {
6788 if let Some(r) = self.requests.iter_mut().find(|r| r.id == request_id) {
6789 r.cancelled = true;
6790 }
6791 }
6792
6793 pub fn update(&mut self, current_time: f64, delta_seconds: f64) {
6794 self.requests.retain(|r| !r.cancelled);
6796 let missed: Vec<u64> = self.requests.iter()
6798 .filter(|r| r.is_overdue(current_time))
6799 .map(|r| r.id)
6800 .collect();
6801 self.total_missed += missed.len() as u64;
6802 self.requests.retain(|r| !r.is_overdue(current_time));
6803 let available_bytes = (self.bandwidth_bytes_per_sec * delta_seconds) as u64;
6805 self.inflight_bytes = self.inflight_bytes.saturating_sub(available_bytes);
6806 }
6807
6808 pub fn next_batch(&mut self, current_time: f64, max_count: usize) -> Vec<u64> {
6809 let mut sortable: Vec<(usize, f32)> = self.requests.iter().enumerate()
6811 .map(|(i, r)| (i, r.urgency_at(current_time)))
6812 .collect();
6813 sortable.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
6814 let mut result = Vec::new();
6815 let mut inflight = self.inflight_bytes;
6816 for (idx, _urgency) in sortable.iter().take(max_count) {
6817 let r = &self.requests[*idx];
6818 if inflight + r.size_bytes <= self.max_inflight_bytes {
6819 inflight += r.size_bytes;
6820 result.push(r.id);
6821 }
6822 }
6823 self.inflight_bytes = inflight;
6824 result
6825 }
6826
6827 pub fn complete_request(&mut self, request_id: u64) {
6828 if let Some(pos) = self.requests.iter().position(|r| r.id == request_id) {
6829 let r = self.requests.remove(pos);
6830 self.inflight_bytes = self.inflight_bytes.saturating_sub(r.size_bytes);
6831 self.total_completed += 1;
6832 }
6833 }
6834
6835 pub fn utilization(&self) -> f32 {
6836 self.inflight_bytes as f32 / self.max_inflight_bytes as f32
6837 }
6838
6839 pub fn deadline_miss_rate(&self) -> f32 {
6840 if self.total_scheduled == 0 { return 0.0; }
6841 self.total_missed as f32 / self.total_scheduled as f32
6842 }
6843}
6844
6845#[derive(Clone, Debug)]
6850pub struct SpatialHash3D {
6851 pub cell_size: f32,
6852 cells: HashMap<(i32, i32, i32), Vec<u64>>,
6853 pub object_cells: HashMap<u64, (i32, i32, i32)>,
6854}
6855
6856impl SpatialHash3D {
6857 pub fn new(cell_size: f32) -> Self {
6858 Self {
6859 cell_size,
6860 cells: HashMap::new(),
6861 object_cells: HashMap::new(),
6862 }
6863 }
6864
6865 fn hash_pos(&self, pos: Vec3) -> (i32, i32, i32) {
6866 (
6867 (pos.x / self.cell_size).floor() as i32,
6868 (pos.y / self.cell_size).floor() as i32,
6869 (pos.z / self.cell_size).floor() as i32,
6870 )
6871 }
6872
6873 pub fn insert(&mut self, object_id: u64, pos: Vec3) {
6874 let key = self.hash_pos(pos);
6875 self.cells.entry(key).or_insert_with(Vec::new).push(object_id);
6876 self.object_cells.insert(object_id, key);
6877 }
6878
6879 pub fn remove(&mut self, object_id: u64) {
6880 if let Some(&key) = self.object_cells.get(&object_id) {
6881 if let Some(list) = self.cells.get_mut(&key) {
6882 list.retain(|&id| id != object_id);
6883 }
6884 self.object_cells.remove(&object_id);
6885 }
6886 }
6887
6888 pub fn update(&mut self, object_id: u64, new_pos: Vec3) {
6889 self.remove(object_id);
6890 self.insert(object_id, new_pos);
6891 }
6892
6893 pub fn query_radius(&self, center: Vec3, radius: f32) -> Vec<u64> {
6894 let r_cells = (radius / self.cell_size).ceil() as i32 + 1;
6895 let cc = self.hash_pos(center);
6896 let mut result = Vec::new();
6897 for x in (cc.0 - r_cells)..=(cc.0 + r_cells) {
6898 for y in (cc.1 - r_cells)..=(cc.1 + r_cells) {
6899 for z in (cc.2 - r_cells)..=(cc.2 + r_cells) {
6900 if let Some(list) = self.cells.get(&(x, y, z)) {
6901 result.extend_from_slice(list);
6902 }
6903 }
6904 }
6905 }
6906 result
6907 }
6908
6909 pub fn query_aabb(&self, aabb: &Aabb) -> Vec<u64> {
6910 let min_key = self.hash_pos(aabb.min);
6911 let max_key = self.hash_pos(aabb.max);
6912 let mut result = Vec::new();
6913 for x in min_key.0..=max_key.0 {
6914 for y in min_key.1..=max_key.1 {
6915 for z in min_key.2..=max_key.2 {
6916 if let Some(list) = self.cells.get(&(x, y, z)) {
6917 result.extend_from_slice(list);
6918 }
6919 }
6920 }
6921 }
6922 result
6923 }
6924
6925 pub fn object_count(&self) -> usize {
6926 self.object_cells.len()
6927 }
6928
6929 pub fn cell_count(&self) -> usize {
6930 self.cells.len()
6931 }
6932
6933 pub fn average_occupancy(&self) -> f32 {
6934 if self.cells.is_empty() { return 0.0; }
6935 let total: usize = self.cells.values().map(|v| v.len()).sum();
6936 total as f32 / self.cells.len() as f32
6937 }
6938}
6939
6940#[derive(Clone, Debug)]
6945pub struct FogZone {
6946 pub zone_id: u32,
6947 pub bounds: Aabb,
6948 pub fog_density: f32,
6949 pub fog_color: Vec3,
6950 pub scatter_coefficient: f32,
6951 pub absorption_coefficient: f32,
6952 pub height_falloff: f32, pub height_offset: f32,
6954 pub animation_speed: f32,
6955 pub turbulence: f32,
6956 pub enabled: bool,
6957 pub blend_distance: f32, }
6959
6960impl FogZone {
6961 pub fn new(zone_id: u32, bounds: Aabb) -> Self {
6962 Self {
6963 zone_id,
6964 bounds,
6965 fog_density: 0.01,
6966 fog_color: Vec3::new(0.7, 0.75, 0.8),
6967 scatter_coefficient: 0.005,
6968 absorption_coefficient: 0.003,
6969 height_falloff: 0.01,
6970 height_offset: 0.0,
6971 animation_speed: 0.1,
6972 turbulence: 0.5,
6973 enabled: true,
6974 blend_distance: 50.0,
6975 }
6976 }
6977
6978 pub fn density_at(&self, world_pos: Vec3, time: f64) -> f32 {
6979 if !self.enabled { return 0.0; }
6980 if !self.bounds.contains_point(world_pos) { return 0.0; }
6981 let h = (world_pos.y - self.height_offset).max(0.0);
6983 let height_factor = (-self.height_falloff * h).exp();
6984 let t = time as f32;
6986 let noise = (world_pos.x * 0.1 + t * self.animation_speed).sin()
6987 * (world_pos.z * 0.1 + t * self.animation_speed * 0.7).cos()
6988 * self.turbulence * 0.5 + 0.5;
6989 let blend = self.blend_factor(world_pos);
6990 self.fog_density * height_factor * (1.0 + noise) * blend
6991 }
6992
6993 fn blend_factor(&self, pos: Vec3) -> f32 {
6994 let min_dist = [
6996 pos.x - self.bounds.min.x,
6997 self.bounds.max.x - pos.x,
6998 pos.y - self.bounds.min.y,
6999 self.bounds.max.y - pos.y,
7000 pos.z - self.bounds.min.z,
7001 self.bounds.max.z - pos.z,
7002 ].iter().copied().fold(f32::INFINITY, f32::min);
7003 let t = (min_dist / self.blend_distance).clamp(0.0, 1.0);
7004 t * t * (3.0 - 2.0 * t)
7005 }
7006
7007 pub fn transmittance_along_ray(&self, start: Vec3, end: Vec3, samples: u32, time: f64) -> f32 {
7008 let mut optical_depth = 0.0f32;
7010 let dir = end - start;
7011 let total_len = dir.length();
7012 if total_len < 1e-6 { return 1.0; }
7013 let step = total_len / samples as f32;
7014 let d = dir / total_len;
7015 for i in 0..samples {
7016 let t = (i as f32 + 0.5) * step;
7017 let pos = start + d * t;
7018 let density = self.density_at(pos, time);
7019 optical_depth += density * step * (self.scatter_coefficient + self.absorption_coefficient);
7020 }
7021 (-optical_depth).exp()
7022 }
7023
7024 pub fn phase_function_henyey_greenstein(cos_theta: f32, g: f32) -> f32 {
7025 let g2 = g * g;
7027 let denom = (1.0 + g2 - 2.0 * g * cos_theta).powf(1.5);
7028 (1.0 - g2) / (4.0 * std::f32::consts::PI * denom.max(1e-10))
7029 }
7030}
7031
7032#[derive(Clone, Debug)]
7037pub struct SceneNode {
7038 pub node_id: u64,
7039 pub name: String,
7040 pub local_transform: Mat4,
7041 pub world_transform: Mat4,
7042 pub parent_id: Option<u64>,
7043 pub children: Vec<u64>,
7044 pub level_id: Option<u64>,
7045 pub is_static: bool,
7046 pub dirty: bool,
7047 pub visibility_distance: f32,
7048 pub lod_bias: f32,
7049}
7050
7051impl SceneNode {
7052 pub fn new(node_id: u64, name: &str) -> Self {
7053 Self {
7054 node_id,
7055 name: name.to_string(),
7056 local_transform: Mat4::IDENTITY,
7057 world_transform: Mat4::IDENTITY,
7058 parent_id: None,
7059 children: Vec::new(),
7060 level_id: None,
7061 is_static: false,
7062 dirty: true,
7063 visibility_distance: 1000.0,
7064 lod_bias: 0.0,
7065 }
7066 }
7067
7068 pub fn set_local_transform(&mut self, transform: Mat4) {
7069 self.local_transform = transform;
7070 self.dirty = true;
7071 }
7072
7073 pub fn world_position(&self) -> Vec3 {
7074 Vec3::new(
7075 self.world_transform.w_axis.x,
7076 self.world_transform.w_axis.y,
7077 self.world_transform.w_axis.z,
7078 )
7079 }
7080
7081 pub fn is_visible_from(&self, camera_pos: Vec3) -> bool {
7082 let dist = (self.world_position() - camera_pos).length();
7083 dist <= self.visibility_distance
7084 }
7085}
7086
7087#[derive(Clone, Debug)]
7088pub struct SceneGraph {
7089 pub nodes: HashMap<u64, SceneNode>,
7090 pub root_nodes: Vec<u64>,
7091 pub next_node_id: u64,
7092}
7093
7094impl SceneGraph {
7095 pub fn new() -> Self {
7096 Self {
7097 nodes: HashMap::new(),
7098 root_nodes: Vec::new(),
7099 next_node_id: 1,
7100 }
7101 }
7102
7103 pub fn create_node(&mut self, name: &str) -> u64 {
7104 let id = self.next_node_id;
7105 self.next_node_id += 1;
7106 self.nodes.insert(id, SceneNode::new(id, name));
7107 self.root_nodes.push(id);
7108 id
7109 }
7110
7111 pub fn attach_child(&mut self, parent_id: u64, child_id: u64) {
7112 if let Some(parent) = self.nodes.get_mut(&parent_id) {
7113 if !parent.children.contains(&child_id) {
7114 parent.children.push(child_id);
7115 }
7116 }
7117 if let Some(child) = self.nodes.get_mut(&child_id) {
7118 child.parent_id = Some(parent_id);
7119 child.dirty = true;
7120 }
7121 self.root_nodes.retain(|&id| id != child_id);
7122 }
7123
7124 pub fn detach_child(&mut self, child_id: u64) {
7125 let parent_id = self.nodes.get(&child_id).and_then(|n| n.parent_id);
7126 if let Some(pid) = parent_id {
7127 if let Some(parent) = self.nodes.get_mut(&pid) {
7128 parent.children.retain(|&id| id != child_id);
7129 }
7130 }
7131 if let Some(node) = self.nodes.get_mut(&child_id) {
7132 node.parent_id = None;
7133 node.dirty = true;
7134 }
7135 if !self.root_nodes.contains(&child_id) {
7136 self.root_nodes.push(child_id);
7137 }
7138 }
7139
7140 pub fn update_world_transforms(&mut self) {
7141 let roots = self.root_nodes.clone();
7143 let mut queue = std::collections::VecDeque::new();
7144 for root in roots {
7145 if let Some(node) = self.nodes.get_mut(&root) {
7146 if node.dirty {
7147 node.world_transform = node.local_transform;
7148 node.dirty = false;
7149 }
7150 }
7151 queue.push_back(root);
7152 }
7153 while let Some(nid) = queue.pop_front() {
7154 let (parent_world, children) = if let Some(node) = self.nodes.get(&nid) {
7155 (node.world_transform, node.children.clone())
7156 } else {
7157 continue;
7158 };
7159 for child_id in children {
7160 if let Some(child) = self.nodes.get_mut(&child_id) {
7161 if child.dirty {
7162 child.world_transform = parent_world * child.local_transform;
7163 child.dirty = false;
7164 }
7165 }
7166 queue.push_back(child_id);
7167 }
7168 }
7169 }
7170
7171 pub fn find_by_name(&self, name: &str) -> Option<u64> {
7172 self.nodes.values()
7173 .find(|n| n.name == name)
7174 .map(|n| n.node_id)
7175 }
7176
7177 pub fn collect_subtree(&self, root_id: u64) -> Vec<u64> {
7178 let mut result = Vec::new();
7179 let mut queue = std::collections::VecDeque::new();
7180 queue.push_back(root_id);
7181 while let Some(nid) = queue.pop_front() {
7182 result.push(nid);
7183 if let Some(node) = self.nodes.get(&nid) {
7184 for &child in &node.children {
7185 queue.push_back(child);
7186 }
7187 }
7188 }
7189 result
7190 }
7191
7192 pub fn depth_of(&self, node_id: u64) -> u32 {
7193 let mut depth = 0u32;
7194 let mut current = node_id;
7195 loop {
7196 if let Some(node) = self.nodes.get(¤t) {
7197 if let Some(pid) = node.parent_id {
7198 depth += 1;
7199 current = pid;
7200 } else {
7201 break;
7202 }
7203 } else {
7204 break;
7205 }
7206 }
7207 depth
7208 }
7209
7210 pub fn node_count(&self) -> usize {
7211 self.nodes.len()
7212 }
7213
7214 pub fn root_count(&self) -> usize {
7215 self.root_nodes.len()
7216 }
7217
7218 pub fn remove_subtree(&mut self, root_id: u64) -> usize {
7219 let subtree = self.collect_subtree(root_id);
7220 let count = subtree.len();
7221 self.detach_child(root_id);
7222 for id in &subtree {
7223 self.nodes.remove(id);
7224 }
7225 self.root_nodes.retain(|id| !subtree.contains(id));
7226 count
7227 }
7228}
7229
7230#[derive(Clone, Debug, Default)]
7235pub struct StreamingMetricsDashboard {
7236 pub frame_number: u64,
7237 pub current_time: f64,
7238 pub loads_this_frame: u32,
7240 pub unloads_this_frame: u32,
7241 pub total_loads: u64,
7242 pub total_unloads: u64,
7243 pub peak_memory_mb: f32,
7245 pub current_memory_mb: f32,
7246 pub memory_budget_mb: f32,
7247 pub avg_load_time_ms: f32,
7249 pub max_load_time_ms: f32,
7250 pub streaming_stalls: u32,
7251 pub frames_with_load: u32,
7252 pub active_levels: u32,
7254 pub loading_levels: u32,
7255 pub visible_levels: u32,
7256 pub culled_levels: u32,
7257 pub bytes_loaded_this_sec: u64,
7259 pub bytes_unloaded_this_sec: u64,
7260 pub bandwidth_utilization: f32,
7261 pub memory_history: VecDeque<f32>,
7263 pub fps_history: VecDeque<f32>,
7264 history_capacity: usize,
7265}
7266
7267impl StreamingMetricsDashboard {
7268 pub fn new(history_capacity: usize) -> Self {
7269 Self {
7270 memory_history: VecDeque::with_capacity(history_capacity),
7271 fps_history: VecDeque::with_capacity(history_capacity),
7272 history_capacity,
7273 memory_budget_mb: 512.0,
7274 ..Default::default()
7275 }
7276 }
7277
7278 pub fn begin_frame(&mut self, time: f64, fps: f32) {
7279 self.frame_number += 1;
7280 self.current_time = time;
7281 self.loads_this_frame = 0;
7282 self.unloads_this_frame = 0;
7283 if self.memory_history.len() >= self.history_capacity {
7285 self.memory_history.pop_front();
7286 }
7287 self.memory_history.push_back(self.current_memory_mb);
7288 if self.fps_history.len() >= self.history_capacity {
7289 self.fps_history.pop_front();
7290 }
7291 self.fps_history.push_back(fps);
7292 }
7293
7294 pub fn record_load(&mut self, load_time_ms: f32, bytes: u64) {
7295 self.loads_this_frame += 1;
7296 self.total_loads += 1;
7297 self.bytes_loaded_this_sec += bytes;
7298 if load_time_ms > self.max_load_time_ms {
7299 self.max_load_time_ms = load_time_ms;
7300 }
7301 let n = self.total_loads as f32;
7303 self.avg_load_time_ms = (self.avg_load_time_ms * (n - 1.0) + load_time_ms) / n;
7304 }
7305
7306 pub fn record_unload(&mut self, bytes: u64) {
7307 self.unloads_this_frame += 1;
7308 self.total_unloads += 1;
7309 self.bytes_unloaded_this_sec += bytes;
7310 }
7311
7312 pub fn record_stall(&mut self) {
7313 self.streaming_stalls += 1;
7314 }
7315
7316 pub fn update_memory(&mut self, used_mb: f32) {
7317 self.current_memory_mb = used_mb;
7318 if used_mb > self.peak_memory_mb {
7319 self.peak_memory_mb = used_mb;
7320 }
7321 }
7322
7323 pub fn memory_utilization(&self) -> f32 {
7324 if self.memory_budget_mb < 1.0 { return 0.0; }
7325 self.current_memory_mb / self.memory_budget_mb
7326 }
7327
7328 pub fn average_fps(&self) -> f32 {
7329 if self.fps_history.is_empty() { return 0.0; }
7330 self.fps_history.iter().sum::<f32>() / self.fps_history.len() as f32
7331 }
7332
7333 pub fn min_fps(&self) -> f32 {
7334 self.fps_history.iter().copied().fold(f32::INFINITY, f32::min)
7335 }
7336
7337 pub fn memory_trend(&self) -> f32 {
7338 let n = self.memory_history.len();
7340 if n < 2 { return 0.0; }
7341 let x_mean = (n as f32 - 1.0) * 0.5;
7342 let y_mean: f32 = self.memory_history.iter().sum::<f32>() / n as f32;
7343 let mut numer = 0.0f32;
7344 let mut denom = 0.0f32;
7345 for (i, &y) in self.memory_history.iter().enumerate() {
7346 let x = i as f32 - x_mean;
7347 numer += x * (y - y_mean);
7348 denom += x * x;
7349 }
7350 if denom.abs() < 1e-10 { return 0.0; }
7351 numer / denom
7352 }
7353
7354 pub fn is_memory_critical(&self) -> bool {
7355 self.memory_utilization() > 0.9
7356 }
7357
7358 pub fn is_bandwidth_saturated(&self) -> bool {
7359 self.bandwidth_utilization > 0.95
7360 }
7361
7362 pub fn report_summary(&self) -> HashMap<String, f32> {
7363 let mut map = HashMap::new();
7364 map.insert("memory_mb".to_string(), self.current_memory_mb);
7365 map.insert("memory_utilization".to_string(), self.memory_utilization());
7366 map.insert("avg_load_ms".to_string(), self.avg_load_time_ms);
7367 map.insert("max_load_ms".to_string(), self.max_load_time_ms);
7368 map.insert("stalls".to_string(), self.streaming_stalls as f32);
7369 map.insert("total_loads".to_string(), self.total_loads as f32);
7370 map.insert("avg_fps".to_string(), self.average_fps());
7371 map.insert("memory_trend_mb_per_frame".to_string(), self.memory_trend());
7372 map
7373 }
7374}
7375
7376#[derive(Clone, Debug)]
7381pub struct IntegratedStreamingWorld {
7382 pub scene_graph: SceneGraph,
7383 pub terrain: TerrainManager,
7384 pub tile_map: StreamingTileMap,
7385 pub instancer: LevelInstancer,
7386 pub fog_zones: Vec<FogZone>,
7387 pub deadline_scheduler: DeadlineScheduler,
7388 pub spatial_hash: SpatialHash3D,
7389 pub metrics: StreamingMetricsDashboard,
7390 pub camera_pos: Vec3,
7391 pub camera_dir: Vec3,
7392 pub camera_velocity: Vec3,
7393 pub current_time: f64,
7394 pub delta_time: f32,
7395 pub stream_radius_main: f32,
7396 pub stream_radius_secondary: f32,
7397 pub frame_count: u64,
7398}
7399
7400impl IntegratedStreamingWorld {
7401 pub fn new() -> Self {
7402 Self {
7403 scene_graph: SceneGraph::new(),
7404 terrain: TerrainManager::new(256.0, 5, 2000.0),
7405 tile_map: StreamingTileMap::new(512.0),
7406 instancer: LevelInstancer::new(256.0),
7407 fog_zones: Vec::new(),
7408 deadline_scheduler: DeadlineScheduler::new(100.0 * 1024.0 * 1024.0), spatial_hash: SpatialHash3D::new(128.0),
7410 metrics: StreamingMetricsDashboard::new(120),
7411 camera_pos: Vec3::ZERO,
7412 camera_dir: Vec3::NEG_Z,
7413 camera_velocity: Vec3::ZERO,
7414 current_time: 0.0,
7415 delta_time: 0.016,
7416 stream_radius_main: 800.0,
7417 stream_radius_secondary: 1500.0,
7418 frame_count: 0,
7419 }
7420 }
7421
7422 pub fn update(&mut self, camera_pos: Vec3, camera_dir: Vec3, delta_time: f32) {
7423 let prev_pos = self.camera_pos;
7424 self.camera_pos = camera_pos;
7425 self.camera_dir = camera_dir.normalize_or_zero();
7426 self.camera_velocity = (camera_pos - prev_pos) / delta_time.max(1e-6);
7427 self.delta_time = delta_time;
7428 self.current_time += delta_time as f64;
7429 self.frame_count += 1;
7430 let fps = if delta_time > 1e-6 { 1.0 / delta_time } else { 60.0 };
7432 self.metrics.begin_frame(self.current_time, fps);
7433 self.terrain.update(camera_pos);
7434 self.deadline_scheduler.update(self.current_time, delta_time as f64);
7435 self.scene_graph.update_world_transforms();
7436 let mem_mb = self.estimate_memory_mb();
7437 self.metrics.update_memory(mem_mb);
7438 }
7439
7440 fn estimate_memory_mb(&self) -> f32 {
7441 let terrain_verts = self.terrain.total_vertices_rendered as f32 * 32.0; let scene_nodes = self.scene_graph.node_count() as f32 * 256.0;
7443 let instances = self.instancer.instances.len() as f32 * 512.0;
7444 let tiles = self.tile_map.tile_count() as f32 * 128.0;
7445 (terrain_verts + scene_nodes + instances + tiles) / (1024.0 * 1024.0)
7446 }
7447
7448 pub fn add_fog_zone(&mut self, bounds: Aabb) -> u32 {
7449 let id = self.fog_zones.len() as u32 + 1;
7450 self.fog_zones.push(FogZone::new(id, bounds));
7451 id
7452 }
7453
7454 pub fn fog_density_at(&self, pos: Vec3) -> f32 {
7455 self.fog_zones.iter()
7456 .filter(|z| z.enabled)
7457 .map(|z| z.density_at(pos, self.current_time))
7458 .sum()
7459 }
7460
7461 pub fn schedule_level_load(&mut self, level_id: u64, priority: f32, size_bytes: u64) -> u64 {
7462 let deadline = self.current_time + 2.0; let est_ms = size_bytes as f32 / (100.0 * 1024.0); self.deadline_scheduler.submit(level_id, priority, deadline, size_bytes, est_ms, self.current_time)
7465 }
7466
7467 pub fn place_level_instance(&mut self, level_id: u64, position: Vec3) -> u64 {
7468 let transform = Mat4::from_translation(position);
7469 let instance_id = self.instancer.place_instance(level_id, transform);
7470 self.spatial_hash.insert(instance_id, position);
7471 let node_id = self.scene_graph.create_node(&format!("Level_{}", instance_id));
7473 if let Some(node) = self.scene_graph.nodes.get_mut(&node_id) {
7474 node.local_transform = transform;
7475 node.world_transform = transform;
7476 node.level_id = Some(level_id);
7477 node.dirty = false;
7478 }
7479 instance_id
7480 }
7481
7482 pub fn instances_near(&self, center: Vec3, radius: f32) -> Vec<u64> {
7483 self.spatial_hash.query_radius(center, radius)
7484 }
7485
7486 pub fn terrain_height_at(&self, x: f32, z: f32) -> f32 {
7487 self.terrain.sample_height_world(x, z)
7488 }
7489
7490 pub fn predicted_camera_position(&self, look_ahead_seconds: f32) -> Vec3 {
7491 self.camera_pos + self.camera_velocity * look_ahead_seconds
7492 + Vec3::Y * 0.5 * (-9.8) * look_ahead_seconds * look_ahead_seconds
7493 }
7494
7495 pub fn tiles_to_preload(&self, look_ahead_seconds: f32) -> Vec<(i32, i32)> {
7496 let future_pos = self.predicted_camera_position(look_ahead_seconds);
7497 let mut tiles = self.tile_map.tiles_in_radius(future_pos, self.stream_radius_main);
7498 let current_tiles = self.tile_map.tiles_in_radius(self.camera_pos, self.stream_radius_main);
7499 tiles.retain(|t| !current_tiles.contains(t));
7500 tiles
7501 }
7502
7503 pub fn memory_mb(&self) -> f32 { self.metrics.current_memory_mb }
7504 pub fn is_critical(&self) -> bool { self.metrics.is_memory_critical() }
7505 pub fn frame_count(&self) -> u64 { self.frame_count }
7506 pub fn avg_fps(&self) -> f32 { self.metrics.average_fps() }
7507}
7508
7509#[cfg(test)]
7514mod additional_tests {
7515 use super::*;
7516
7517 #[test]
7518 fn test_streaming_tile_bounds() {
7519 let tile = StreamingTile::new(2, 3, 100.0);
7520 let bounds = tile.world_bounds();
7521 assert!((bounds.min.x - 200.0).abs() < 0.01);
7522 assert!((bounds.max.x - 300.0).abs() < 0.01);
7523 }
7524
7525 #[test]
7526 fn test_streaming_tile_contains() {
7527 let tile = StreamingTile::new(0, 0, 100.0);
7528 assert!(tile.contains_point_2d(50.0, 50.0));
7529 assert!(!tile.contains_point_2d(150.0, 50.0));
7530 }
7531
7532 #[test]
7533 fn test_tile_map_world_to_tile() {
7534 let map = StreamingTileMap::new(128.0);
7535 let (tx, tz) = map.world_to_tile(Vec3::new(300.0, 0.0, 300.0));
7536 assert_eq!(tx, 2);
7537 assert_eq!(tz, 2);
7538 }
7539
7540 #[test]
7541 fn test_tile_map_tiles_in_radius() {
7542 let mut map = StreamingTileMap::new(100.0);
7543 map.get_or_create(0, 0).last_visited_time = 0.0;
7544 map.get_or_create(1, 0).last_visited_time = 0.0;
7545 map.get_or_create(0, 1).last_visited_time = 0.0;
7546 let tiles = map.tiles_in_radius(Vec3::new(50.0, 0.0, 50.0), 200.0);
7547 assert!(!tiles.is_empty());
7548 }
7549
7550 #[test]
7551 fn test_tile_map_evict_stale() {
7552 let mut map = StreamingTileMap::new(100.0);
7553 map.get_or_create(0, 0).last_visited_time = 0.0;
7554 map.get_or_create(1, 1).last_visited_time = 0.0;
7555 let evicted = map.evict_stale(100.0, 50.0);
7556 assert_eq!(evicted, 2);
7557 assert_eq!(map.tile_count(), 0);
7558 }
7559
7560 #[test]
7561 fn test_level_instance_position() {
7562 let t = Mat4::from_translation(Vec3::new(10.0, 5.0, -3.0));
7563 let inst = LevelInstance::new(1, 42, t);
7564 let pos = inst.position();
7565 assert!((pos.x - 10.0).abs() < 0.01);
7566 assert!((pos.y - 5.0).abs() < 0.01);
7567 assert!((pos.z + 3.0).abs() < 0.01);
7568 }
7569
7570 #[test]
7571 fn test_level_instancer_place_and_query() {
7572 let mut instancer = LevelInstancer::new(100.0);
7573 let t = Mat4::from_translation(Vec3::new(50.0, 0.0, 50.0));
7574 let id = instancer.place_instance(1, t);
7575 let found = instancer.instances_in_radius(Vec3::new(50.0, 0.0, 50.0), 10.0);
7576 assert!(found.contains(&id));
7577 }
7578
7579 #[test]
7580 fn test_level_instancer_remove() {
7581 let mut instancer = LevelInstancer::new(100.0);
7582 let t = Mat4::IDENTITY;
7583 let id = instancer.place_instance(5, t);
7584 assert!(instancer.instances.contains_key(&id));
7585 instancer.remove_instance(id);
7586 assert!(!instancer.instances.contains_key(&id));
7587 }
7588
7589 #[test]
7590 fn test_terrain_patch_height_sample() {
7591 let mut patch = TerrainPatch::new(1, 0, 0, 100.0, 5);
7592 for h in &mut patch.height_data { *h = 42.0; }
7594 let h = patch.sample_height_bilinear(50.0, 50.0);
7595 assert!((h - 42.0).abs() < 0.01);
7596 }
7597
7598 #[test]
7599 fn test_terrain_patch_lod_selection() {
7600 let mut patch = TerrainPatch::new(1, 0, 0, 256.0, 5);
7601 patch.update_lod(Vec3::new(128.0, 0.0, 128.0)); assert_eq!(patch.current_lod, 0); patch.update_lod(Vec3::new(10000.0, 0.0, 10000.0)); assert!(patch.current_lod > 0);
7605 }
7606
7607 #[test]
7608 fn test_terrain_manager_update() {
7609 let mut mgr = TerrainManager::new(256.0, 4, 1000.0);
7610 mgr.update(Vec3::new(0.0, 0.0, 0.0));
7611 assert!(mgr.visible_patch_count() > 0);
7612 }
7613
7614 #[test]
7615 fn test_deadline_scheduler_submit_complete() {
7616 let mut sched = DeadlineScheduler::new(100.0 * 1024.0 * 1024.0);
7617 let id = sched.submit(1, 1.0, 2.0, 1024, 1.0, 0.0);
7618 sched.complete_request(id);
7619 assert_eq!(sched.total_completed, 1);
7620 assert!(sched.requests.is_empty());
7621 }
7622
7623 #[test]
7624 fn test_deadline_scheduler_miss_rate() {
7625 let mut sched = DeadlineScheduler::new(1.0);
7626 sched.submit(1, 1.0, 0.001, 1024, 1.0, 0.0);
7627 sched.update(1.0, 1.0); assert_eq!(sched.total_missed, 1);
7629 }
7630
7631 #[test]
7632 fn test_spatial_hash_insert_query() {
7633 let mut sh = SpatialHash3D::new(10.0);
7634 sh.insert(1, Vec3::new(5.0, 0.0, 5.0));
7635 sh.insert(2, Vec3::new(100.0, 0.0, 100.0));
7636 let result = sh.query_radius(Vec3::new(5.0, 0.0, 5.0), 5.0);
7637 assert!(result.contains(&1));
7638 assert!(!result.contains(&2));
7639 }
7640
7641 #[test]
7642 fn test_spatial_hash_update() {
7643 let mut sh = SpatialHash3D::new(10.0);
7644 sh.insert(1, Vec3::new(5.0, 0.0, 5.0));
7645 sh.update(1, Vec3::new(200.0, 0.0, 200.0));
7646 let near = sh.query_radius(Vec3::new(5.0, 0.0, 5.0), 5.0);
7647 assert!(!near.contains(&1));
7648 let far = sh.query_radius(Vec3::new(200.0, 0.0, 200.0), 5.0);
7649 assert!(far.contains(&1));
7650 }
7651
7652 #[test]
7653 fn test_fog_zone_density() {
7654 let bounds = Aabb::new(Vec3::new(-100.0, -50.0, -100.0), Vec3::new(100.0, 50.0, 100.0));
7655 let zone = FogZone::new(1, bounds);
7656 let d = zone.density_at(Vec3::new(0.0, 0.0, 0.0), 0.0);
7657 assert!(d >= 0.0 && d.is_finite());
7658 let d_outside = zone.density_at(Vec3::new(1000.0, 0.0, 0.0), 0.0);
7659 assert_eq!(d_outside, 0.0);
7660 }
7661
7662 #[test]
7663 fn test_fog_transmittance() {
7664 let bounds = Aabb::new(Vec3::new(-200.0, -100.0, -200.0), Vec3::new(200.0, 100.0, 200.0));
7665 let zone = FogZone::new(1, bounds);
7666 let t = zone.transmittance_along_ray(
7667 Vec3::new(-100.0, 0.0, 0.0),
7668 Vec3::new(100.0, 0.0, 0.0),
7669 16,
7670 0.0,
7671 );
7672 assert!(t > 0.0 && t <= 1.0);
7673 }
7674
7675 #[test]
7676 fn test_henyey_greenstein_forward() {
7677 let g = 0.8;
7679 let forward = FogZone::phase_function_henyey_greenstein(1.0, g);
7680 let backward = FogZone::phase_function_henyey_greenstein(-1.0, g);
7681 assert!(forward > backward);
7682 }
7683
7684 #[test]
7685 fn test_scene_graph_attach() {
7686 let mut sg = SceneGraph::new();
7687 let parent = sg.create_node("parent");
7688 let child = sg.create_node("child");
7689 sg.attach_child(parent, child);
7690 assert!(sg.nodes[&parent].children.contains(&child));
7691 assert_eq!(sg.nodes[&child].parent_id, Some(parent));
7692 assert!(!sg.root_nodes.contains(&child));
7693 }
7694
7695 #[test]
7696 fn test_scene_graph_world_transform() {
7697 let mut sg = SceneGraph::new();
7698 let parent = sg.create_node("parent");
7699 let child = sg.create_node("child");
7700 sg.attach_child(parent, child);
7701 let t_parent = Mat4::from_translation(Vec3::new(10.0, 0.0, 0.0));
7702 let t_child = Mat4::from_translation(Vec3::new(5.0, 0.0, 0.0));
7703 sg.nodes.get_mut(&parent).unwrap().local_transform = t_parent;
7704 sg.nodes.get_mut(&parent).unwrap().world_transform = t_parent;
7705 sg.nodes.get_mut(&child).unwrap().local_transform = t_child;
7706 sg.nodes.get_mut(&child).unwrap().dirty = true;
7707 sg.update_world_transforms();
7708 let child_pos = sg.nodes[&child].world_transform.w_axis.x;
7709 assert!((child_pos - 15.0).abs() < 0.01);
7710 }
7711
7712 #[test]
7713 fn test_scene_graph_depth() {
7714 let mut sg = SceneGraph::new();
7715 let a = sg.create_node("a");
7716 let b = sg.create_node("b");
7717 let c = sg.create_node("c");
7718 sg.attach_child(a, b);
7719 sg.attach_child(b, c);
7720 assert_eq!(sg.depth_of(c), 2);
7721 assert_eq!(sg.depth_of(a), 0);
7722 }
7723
7724 #[test]
7725 fn test_scene_graph_subtree_removal() {
7726 let mut sg = SceneGraph::new();
7727 let a = sg.create_node("a");
7728 let b = sg.create_node("b");
7729 let c = sg.create_node("c");
7730 sg.attach_child(a, b);
7731 sg.attach_child(b, c);
7732 let removed = sg.remove_subtree(b);
7733 assert_eq!(removed, 2); assert!(sg.nodes.contains_key(&a));
7735 assert!(!sg.nodes.contains_key(&b));
7736 assert!(!sg.nodes.contains_key(&c));
7737 }
7738
7739 #[test]
7740 fn test_metrics_dashboard_memory_trend() {
7741 let mut dash = StreamingMetricsDashboard::new(30);
7742 for i in 0..20 {
7743 dash.begin_frame(i as f64 * 0.016, 60.0);
7744 dash.update_memory(100.0 + i as f32 * 2.0); }
7746 let trend = dash.memory_trend();
7747 assert!(trend > 0.0, "Memory trend should be positive (increasing): {}", trend);
7748 }
7749
7750 #[test]
7751 fn test_metrics_dashboard_critical() {
7752 let mut dash = StreamingMetricsDashboard::new(10);
7753 dash.memory_budget_mb = 100.0;
7754 dash.update_memory(95.0);
7755 assert!(dash.is_memory_critical());
7756 dash.update_memory(80.0);
7757 assert!(!dash.is_memory_critical());
7758 }
7759
7760 #[test]
7761 fn test_integrated_world_update() {
7762 let mut world = IntegratedStreamingWorld::new();
7763 world.update(Vec3::new(100.0, 10.0, 100.0), Vec3::NEG_Z, 0.016);
7764 assert!(world.frame_count == 1);
7765 assert!(world.current_time > 0.0);
7766 }
7767
7768 #[test]
7769 fn test_integrated_world_fog() {
7770 let mut world = IntegratedStreamingWorld::new();
7771 let bounds = Aabb::new(Vec3::new(-500.0, -200.0, -500.0), Vec3::new(500.0, 200.0, 500.0));
7772 world.add_fog_zone(bounds);
7773 let density = world.fog_density_at(Vec3::ZERO);
7774 assert!(density >= 0.0 && density.is_finite());
7775 }
7776
7777 #[test]
7778 fn test_integrated_world_instance_placement() {
7779 let mut world = IntegratedStreamingWorld::new();
7780 let id = world.place_level_instance(42, Vec3::new(100.0, 0.0, 100.0));
7781 let near = world.instances_near(Vec3::new(100.0, 0.0, 100.0), 50.0);
7782 assert!(near.contains(&id));
7783 }
7784
7785 #[test]
7786 fn test_integrated_world_prediction() {
7787 let mut world = IntegratedStreamingWorld::new();
7788 world.camera_velocity = Vec3::new(10.0, 0.0, 0.0);
7789 let future = world.predicted_camera_position(1.0);
7790 assert!((future.x - world.camera_pos.x - 10.0).abs() < 0.1);
7792 }
7793
7794 #[test]
7795 fn test_terrain_patch_normal() {
7796 let mut patch = TerrainPatch::new(1, 0, 0, 100.0, 5);
7797 for h in &mut patch.height_data { *h = 0.0; }
7799 let normal = patch.compute_normal_at(50.0, 50.0);
7800 assert!((normal.y - 1.0).abs() < 0.01);
7801 }
7802
7803 #[test]
7804 fn test_terrain_patch_sloped_normal() {
7805 let mut patch = TerrainPatch::new(1, 0, 0, 100.0, 5);
7806 let res = patch.height_grid_res as usize;
7807 for z in 0..res {
7809 for x in 0..res {
7810 patch.height_data[z * res + x] = x as f32 * 1.0;
7811 }
7812 }
7813 let normal = patch.compute_normal_at(50.0, 50.0);
7814 assert!(normal.x < 0.0);
7816 assert!(normal.is_finite());
7817 }
7818
7819 #[test]
7820 fn test_scene_graph_find_by_name() {
7821 let mut sg = SceneGraph::new();
7822 let _a = sg.create_node("alpha");
7823 let _b = sg.create_node("beta");
7824 let found = sg.find_by_name("beta");
7825 assert!(found.is_some());
7826 let not_found = sg.find_by_name("gamma");
7827 assert!(not_found.is_none());
7828 }
7829
7830 #[test]
7831 fn test_deadline_scheduler_batch() {
7832 let mut sched = DeadlineScheduler::new(100.0 * 1024.0 * 1024.0);
7833 for i in 0..10 {
7834 sched.submit(i, 1.0, 2.0, 1024 * 1024, 10.0, 0.0);
7835 }
7836 let batch = sched.next_batch(0.0, 5);
7837 assert!(!batch.is_empty());
7838 assert!(batch.len() <= 5);
7839 }
7840
7841 #[test]
7842 fn test_spatial_hash_aabb_query() {
7843 let mut sh = SpatialHash3D::new(10.0);
7844 sh.insert(1, Vec3::new(5.0, 0.0, 5.0));
7845 sh.insert(2, Vec3::new(50.0, 0.0, 50.0));
7846 let aabb = Aabb::new(Vec3::new(0.0, -5.0, 0.0), Vec3::new(10.0, 5.0, 10.0));
7847 let result = sh.query_aabb(&aabb);
7848 assert!(result.contains(&1));
7849 }
7850
7851 #[test]
7852 fn test_level_instancer_count_by_level() {
7853 let mut instancer = LevelInstancer::new(100.0);
7854 instancer.place_instance(1, Mat4::IDENTITY);
7855 instancer.place_instance(1, Mat4::from_translation(Vec3::X));
7856 instancer.place_instance(2, Mat4::from_translation(Vec3::Y));
7857 let counts = instancer.count_by_level();
7858 assert_eq!(counts[&1], 2);
7859 assert_eq!(counts[&2], 1);
7860 }
7861
7862 #[test]
7863 fn test_fog_zone_disabled() {
7864 let bounds = Aabb::new(Vec3::splat(-100.0), Vec3::splat(100.0));
7865 let mut zone = FogZone::new(1, bounds);
7866 zone.enabled = false;
7867 let d = zone.density_at(Vec3::ZERO, 0.0);
7868 assert_eq!(d, 0.0);
7869 }
7870
7871 #[test]
7872 fn test_terrain_patch_stitch_skirt_nonempty() {
7873 let patch = TerrainPatch::new(1, 0, 0, 100.0, 3);
7874 let skirt = patch.stitch_skirt_vertices();
7875 assert!(!skirt.is_empty());
7876 }
7877
7878 #[test]
7879 fn test_tile_map_add_remove_level() {
7880 let mut map = StreamingTileMap::new(100.0);
7881 map.add_level_to_tile(0, 0, 99);
7882 assert!(map.tiles.get(&(0, 0)).map(|t| t.level_ids.contains(&99)).unwrap_or(false));
7883 map.remove_level_from_all(99);
7884 assert!(map.tiles.get(&(0, 0)).map(|t| t.level_ids.is_empty()).unwrap_or(true));
7885 }
7886
7887 #[test]
7888 fn test_deadline_scheduler_utilization() {
7889 let sched = DeadlineScheduler::new(1024.0);
7890 assert!((sched.utilization() - 0.0).abs() < 0.01);
7891 }
7892
7893 #[test]
7894 fn test_spatial_hash_average_occupancy() {
7895 let mut sh = SpatialHash3D::new(10.0);
7896 sh.insert(1, Vec3::ZERO);
7897 sh.insert(2, Vec3::new(1.0, 0.0, 0.0));
7898 let occ = sh.average_occupancy();
7899 assert!(occ >= 1.0 && occ.is_finite());
7900 }
7901
7902 #[test]
7903 fn test_integrated_world_terrain_height() {
7904 let mut world = IntegratedStreamingWorld::new();
7905 world.update(Vec3::ZERO, Vec3::NEG_Z, 0.016);
7906 let h = world.terrain_height_at(50.0, 50.0);
7907 assert!(h.is_finite());
7908 }
7909
7910 #[test]
7911 fn test_metrics_dashboard_report() {
7912 let mut dash = StreamingMetricsDashboard::new(10);
7913 dash.record_load(10.0, 1024 * 1024);
7914 dash.record_unload(512 * 1024);
7915 let report = dash.report_summary();
7916 assert!(report.contains_key("avg_load_ms"));
7917 assert!((report["avg_load_ms"] - 10.0).abs() < 0.01);
7918 }
7919
7920 #[test]
7921 fn test_scene_graph_collect_subtree() {
7922 let mut sg = SceneGraph::new();
7923 let a = sg.create_node("a");
7924 let b = sg.create_node("b");
7925 let c = sg.create_node("c");
7926 sg.attach_child(a, b);
7927 sg.attach_child(a, c);
7928 let sub = sg.collect_subtree(a);
7929 assert_eq!(sub.len(), 3);
7930 assert!(sub.contains(&a) && sub.contains(&b) && sub.contains(&c));
7931 }
7932
7933 #[test]
7934 fn test_level_instance_tags() {
7935 let mut inst = LevelInstance::new(1, 1, Mat4::IDENTITY);
7936 inst.add_tag("outdoor");
7937 inst.add_tag("night");
7938 assert!(inst.has_tag("outdoor"));
7939 assert!(!inst.has_tag("indoor"));
7940 }
7941
7942 #[test]
7943 fn test_level_instance_scale() {
7944 let t = Mat4::from_scale(Vec3::new(2.0, 3.0, 4.0));
7945 let inst = LevelInstance::new(1, 1, t);
7946 let scale = inst.scale();
7947 assert!((scale.x - 2.0).abs() < 0.01);
7948 assert!((scale.y - 3.0).abs() < 0.01);
7949 assert!((scale.z - 4.0).abs() < 0.01);
7950 }
7951
7952 #[test]
7953 fn test_terrain_patch_vertex_count_at_lod() {
7954 let mut patch = TerrainPatch::new(1, 0, 0, 256.0, 4);
7955 patch.update_lod(Vec3::new(128.0, 0.0, 128.0));
7957 let verts_lod0 = patch.vertex_count;
7958 patch.update_lod(Vec3::new(10000.0, 0.0, 10000.0));
7960 let verts_lod_high = patch.vertex_count;
7961 assert!(verts_lod0 >= verts_lod_high, "Lower LOD should have fewer vertices");
7962 }
7963
7964 #[test]
7965 fn test_integrated_world_schedule_load() {
7966 let mut world = IntegratedStreamingWorld::new();
7967 let req_id = world.schedule_level_load(7, 1.0, 4 * 1024 * 1024);
7968 assert!(req_id > 0);
7969 assert!(!world.deadline_scheduler.requests.is_empty());
7970 }
7971
7972 #[test]
7973 fn test_fog_zone_height_falloff() {
7974 let bounds = Aabb::new(Vec3::new(-200.0, -100.0, -200.0), Vec3::new(200.0, 1000.0, 200.0));
7975 let mut zone = FogZone::new(1, bounds);
7976 zone.height_falloff = 0.1;
7977 zone.turbulence = 0.0;
7978 let d_low = zone.density_at(Vec3::new(0.0, 0.0, 0.0), 0.0);
7979 let d_high = zone.density_at(Vec3::new(0.0, 100.0, 0.0), 0.0);
7980 assert!(d_low >= d_high, "Density should decrease with height");
7981 }
7982
7983 #[test]
7984 fn test_spatial_hash_remove() {
7985 let mut sh = SpatialHash3D::new(10.0);
7986 sh.insert(10, Vec3::ZERO);
7987 sh.remove(10);
7988 assert_eq!(sh.object_count(), 0);
7989 assert!(sh.query_radius(Vec3::ZERO, 5.0).is_empty());
7990 }
7991}