Skip to main content

proof_engine/editor/
world_streaming_editor.rs

1#![allow(dead_code, unused_variables, unused_mut, unused_imports)]
2use glam::{Vec2, Vec3, Vec4, Quat, Mat4};
3use std::collections::{HashMap, HashSet, BTreeMap, VecDeque};
4
5// ============================================================
6// CONSTANTS
7// ============================================================
8
9const MAX_OCTREE_DEPTH: u32 = 8;
10const MAX_OBJECTS_PER_OCTREE_NODE: usize = 16;
11const BVH_MAX_LEAF_OBJECTS: usize = 4;
12const DEFAULT_CHUNK_SIZE: f32 = 256.0;
13const DEFAULT_STREAMING_RADIUS: f32 = 2048.0;
14const DEFAULT_MAX_LOADED_CHUNKS: usize = 256;
15const DEFAULT_MAX_MEMORY_MB: u64 = 4096;
16const LOD_DISTANCES: [f32; 5] = [64.0, 128.0, 256.0, 512.0, 1024.0];
17const FRUSTUM_NEAR_PLANE: usize = 0;
18const FRUSTUM_FAR_PLANE: usize = 1;
19const FRUSTUM_LEFT_PLANE: usize = 2;
20const FRUSTUM_RIGHT_PLANE: usize = 3;
21const FRUSTUM_TOP_PLANE: usize = 4;
22const FRUSTUM_BOTTOM_PLANE: usize = 5;
23const VIRTUAL_TEXTURE_TILE_SIZE: u32 = 128;
24const VIRTUAL_TEXTURE_ATLAS_SIZE: u32 = 4096;
25const IMPOSTOR_ATLAS_COLS: u32 = 8;
26const IMPOSTOR_ATLAS_ROWS: u32 = 8;
27const TERRAIN_PATCH_SIZE: usize = 65;
28const SCREEN_SPACE_ERROR_THRESHOLD: f32 = 2.0;
29const SAH_TRAVERSAL_COST: f32 = 1.0;
30const SAH_INTERSECTION_COST: f32 = 2.0;
31const LRU_MAX_AGE_FRAMES: u64 = 300;
32const STREAMING_PRIORITY_LEVELS: usize = 8;
33const MAX_ASYNC_LOAD_QUEUE: usize = 1024;
34const HLOD_CLUSTER_RADIUS: f32 = 512.0;
35const DATA_LAYER_MAX: usize = 32;
36const WORLD_PARTITION_CELL_SIZE: f32 = 512.0;
37const PROFILER_HISTORY_FRAMES: usize = 128;
38const NORMAL_SMOOTH_EPSILON: f32 = 1e-6;
39const BILINEAR_CLAMP_EPSILON: f32 = 1e-5;
40const CHUNK_DEPENDENCY_MAX_DEPTH: usize = 16;
41const FEEDBACK_BUFFER_MIPS: usize = 8;
42
43// ============================================================
44// ENUMS
45// ============================================================
46
47#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
48pub enum LodLevel {
49    Unloaded,
50    Impostor,
51    Low,
52    Medium,
53    High,
54    Ultra,
55}
56
57impl LodLevel {
58    pub fn index(&self) -> usize {
59        match self {
60            LodLevel::Unloaded => 0,
61            LodLevel::Impostor => 1,
62            LodLevel::Low => 2,
63            LodLevel::Medium => 3,
64            LodLevel::High => 4,
65            LodLevel::Ultra => 5,
66        }
67    }
68
69    pub fn from_index(idx: usize) -> Self {
70        match idx {
71            0 => LodLevel::Unloaded,
72            1 => LodLevel::Impostor,
73            2 => LodLevel::Low,
74            3 => LodLevel::Medium,
75            4 => LodLevel::High,
76            5 => LodLevel::Ultra,
77            _ => LodLevel::Unloaded,
78        }
79    }
80
81    pub fn memory_multiplier(&self) -> f32 {
82        match self {
83            LodLevel::Unloaded => 0.0,
84            LodLevel::Impostor => 0.02,
85            LodLevel::Low => 0.1,
86            LodLevel::Medium => 0.3,
87            LodLevel::High => 0.7,
88            LodLevel::Ultra => 1.0,
89        }
90    }
91
92    pub fn vertex_reduction_ratio(&self) -> f32 {
93        match self {
94            LodLevel::Unloaded => 0.0,
95            LodLevel::Impostor => 0.001,
96            LodLevel::Low => 0.05,
97            LodLevel::Medium => 0.2,
98            LodLevel::High => 0.6,
99            LodLevel::Ultra => 1.0,
100        }
101    }
102
103    pub fn next_higher(&self) -> LodLevel {
104        match self {
105            LodLevel::Unloaded => LodLevel::Impostor,
106            LodLevel::Impostor => LodLevel::Low,
107            LodLevel::Low => LodLevel::Medium,
108            LodLevel::Medium => LodLevel::High,
109            LodLevel::High => LodLevel::Ultra,
110            LodLevel::Ultra => LodLevel::Ultra,
111        }
112    }
113
114    pub fn next_lower(&self) -> LodLevel {
115        match self {
116            LodLevel::Unloaded => LodLevel::Unloaded,
117            LodLevel::Impostor => LodLevel::Unloaded,
118            LodLevel::Low => LodLevel::Impostor,
119            LodLevel::Medium => LodLevel::Low,
120            LodLevel::High => LodLevel::Medium,
121            LodLevel::Ultra => LodLevel::High,
122        }
123    }
124
125    pub fn is_loaded(&self) -> bool {
126        !matches!(self, LodLevel::Unloaded)
127    }
128
129    pub fn screen_space_error_threshold(&self) -> f32 {
130        match self {
131            LodLevel::Unloaded => f32::MAX,
132            LodLevel::Impostor => 64.0,
133            LodLevel::Low => 16.0,
134            LodLevel::Medium => 4.0,
135            LodLevel::High => 1.0,
136            LodLevel::Ultra => 0.0,
137        }
138    }
139}
140
141#[derive(Debug, Clone, PartialEq, Eq, Hash)]
142pub enum ChunkLoadState {
143    Unloaded,
144    Queued,
145    Loading,
146    Loaded,
147    Evicting,
148}
149
150impl ChunkLoadState {
151    pub fn can_evict(&self) -> bool {
152        matches!(self, ChunkLoadState::Loaded)
153    }
154
155    pub fn is_pending(&self) -> bool {
156        matches!(self, ChunkLoadState::Queued | ChunkLoadState::Loading)
157    }
158
159    pub fn is_active(&self) -> bool {
160        matches!(self, ChunkLoadState::Loaded | ChunkLoadState::Loading)
161    }
162
163    pub fn transition_to_loaded(&self) -> Option<ChunkLoadState> {
164        match self {
165            ChunkLoadState::Loading => Some(ChunkLoadState::Loaded),
166            _ => None,
167        }
168    }
169
170    pub fn transition_to_unloaded(&self) -> Option<ChunkLoadState> {
171        match self {
172            ChunkLoadState::Evicting => Some(ChunkLoadState::Unloaded),
173            _ => None,
174        }
175    }
176}
177
178#[derive(Debug, Clone, PartialEq, Eq, Hash)]
179pub enum BvhNodeKind {
180    Internal,
181    Leaf,
182}
183
184#[derive(Debug, Clone, PartialEq, Eq, Hash)]
185pub enum EvictionPolicy {
186    Lru,
187    Lfu,
188    DistanceBased,
189    PriorityBased,
190}
191
192#[derive(Debug, Clone, PartialEq, Eq, Hash)]
193pub enum DataLayerMode {
194    Included,
195    Excluded,
196    Inherited,
197}
198
199#[derive(Debug, Clone, PartialEq, Eq, Hash)]
200pub enum StreamingLoadType {
201    Synchronous,
202    Asynchronous,
203    Prefetch,
204}
205
206#[derive(Debug, Clone, PartialEq)]
207pub enum ProfilerEventType {
208    ChunkLoad,
209    ChunkUnload,
210    LodSwitch,
211    FrustumCull,
212    OctreeQuery,
213    BvhQuery,
214    TerrainStitch,
215    ImpostorUpdate,
216    VirtualTextureUpdate,
217    HlodBuild,
218}
219
220// ============================================================
221// CHUNK COORDINATE
222// ============================================================
223
224#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Copy)]
225pub struct ChunkCoord {
226    pub x: i32,
227    pub y: i32,
228    pub z: i32,
229}
230
231impl ChunkCoord {
232    pub fn new(x: i32, y: i32, z: i32) -> Self {
233        Self { x, y, z }
234    }
235
236    pub fn from_world_pos(pos: Vec3, chunk_size: f32) -> Self {
237        let x = (pos.x / chunk_size).floor() as i32;
238        let y = (pos.y / chunk_size).floor() as i32;
239        let z = (pos.z / chunk_size).floor() as i32;
240        Self { x, y, z }
241    }
242
243    pub fn to_world_min(&self, chunk_size: f32) -> Vec3 {
244        Vec3::new(
245            self.x as f32 * chunk_size,
246            self.y as f32 * chunk_size,
247            self.z as f32 * chunk_size,
248        )
249    }
250
251    pub fn to_world_center(&self, chunk_size: f32) -> Vec3 {
252        let min = self.to_world_min(chunk_size);
253        min + Vec3::splat(chunk_size * 0.5)
254    }
255
256    pub fn manhattan_distance(&self, other: &ChunkCoord) -> i32 {
257        (self.x - other.x).abs() + (self.y - other.y).abs() + (self.z - other.z).abs()
258    }
259
260    pub fn chebyshev_distance(&self, other: &ChunkCoord) -> i32 {
261        let dx = (self.x - other.x).abs();
262        let dy = (self.y - other.y).abs();
263        let dz = (self.z - other.z).abs();
264        dx.max(dy).max(dz)
265    }
266
267    pub fn euclidean_distance_sq(&self, other: &ChunkCoord) -> i64 {
268        let dx = (self.x - other.x) as i64;
269        let dy = (self.y - other.y) as i64;
270        let dz = (self.z - other.z) as i64;
271        dx * dx + dy * dy + dz * dz
272    }
273
274    pub fn neighbors_6(&self) -> [ChunkCoord; 6] {
275        [
276            ChunkCoord::new(self.x + 1, self.y, self.z),
277            ChunkCoord::new(self.x - 1, self.y, self.z),
278            ChunkCoord::new(self.x, self.y + 1, self.z),
279            ChunkCoord::new(self.x, self.y - 1, self.z),
280            ChunkCoord::new(self.x, self.y, self.z + 1),
281            ChunkCoord::new(self.x, self.y, self.z - 1),
282        ]
283    }
284
285    pub fn neighbors_26(&self) -> Vec<ChunkCoord> {
286        let mut result = Vec::with_capacity(26);
287        for dx in -1i32..=1 {
288            for dy in -1i32..=1 {
289                for dz in -1i32..=1 {
290                    if dx == 0 && dy == 0 && dz == 0 {
291                        continue;
292                    }
293                    result.push(ChunkCoord::new(self.x + dx, self.y + dy, self.z + dz));
294                }
295            }
296        }
297        result
298    }
299
300    pub fn chunks_in_radius(center: &ChunkCoord, radius: i32) -> Vec<ChunkCoord> {
301        let mut result = Vec::new();
302        for dx in -radius..=radius {
303            for dy in -radius..=radius {
304                for dz in -radius..=radius {
305                    let coord = ChunkCoord::new(center.x + dx, center.y + dy, center.z + dz);
306                    if coord.chebyshev_distance(center) <= radius {
307                        result.push(coord);
308                    }
309                }
310            }
311        }
312        result
313    }
314
315    pub fn is_adjacent(&self, other: &ChunkCoord) -> bool {
316        self.chebyshev_distance(other) == 1
317    }
318
319    pub fn offset(&self, dx: i32, dy: i32, dz: i32) -> ChunkCoord {
320        ChunkCoord::new(self.x + dx, self.y + dy, self.z + dz)
321    }
322
323    pub fn pack_u64(&self) -> u64 {
324        let xi = (self.x as i64 + 0x0000_8000i64) as u64;
325        let yi = (self.y as i64 + 0x0000_8000i64) as u64;
326        let zi = (self.z as i64 + 0x0000_8000i64) as u64;
327        (xi & 0xFFFF) | ((yi & 0xFFFF) << 16) | ((zi & 0xFFFF) << 32)
328    }
329
330    pub fn unpack_u64(packed: u64) -> ChunkCoord {
331        let xi = ((packed & 0xFFFF) as i64 - 0x0000_8000i64) as i32;
332        let yi = (((packed >> 16) & 0xFFFF) as i64 - 0x0000_8000i64) as i32;
333        let zi = (((packed >> 32) & 0xFFFF) as i64 - 0x0000_8000i64) as i32;
334        ChunkCoord::new(xi, yi, zi)
335    }
336}
337
338impl std::fmt::Display for ChunkCoord {
339    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
340        write!(f, "({},{},{})", self.x, self.y, self.z)
341    }
342}
343
344// ============================================================
345// CHUNK BOUNDS (AABB)
346// ============================================================
347
348#[derive(Debug, Clone, PartialEq)]
349pub struct ChunkBounds {
350    pub min: Vec3,
351    pub max: Vec3,
352}
353
354impl ChunkBounds {
355    pub fn new(min: Vec3, max: Vec3) -> Self {
356        Self { min, max }
357    }
358
359    pub fn from_center_size(center: Vec3, half_size: Vec3) -> Self {
360        Self {
361            min: center - half_size,
362            max: center + half_size,
363        }
364    }
365
366    pub fn from_chunk_coord(coord: &ChunkCoord, chunk_size: f32) -> Self {
367        let min = coord.to_world_min(chunk_size);
368        let max = min + Vec3::splat(chunk_size);
369        Self { min, max }
370    }
371
372    pub fn contains(&self, point: Vec3) -> bool {
373        point.x >= self.min.x && point.x <= self.max.x
374            && point.y >= self.min.y && point.y <= self.max.y
375            && point.z >= self.min.z && point.z <= self.max.z
376    }
377
378    pub fn intersects(&self, other: &ChunkBounds) -> bool {
379        self.min.x <= other.max.x && self.max.x >= other.min.x
380            && self.min.y <= other.max.y && self.max.y >= other.min.y
381            && self.min.z <= other.max.z && self.max.z >= other.min.z
382    }
383
384    pub fn expand(&self, amount: f32) -> ChunkBounds {
385        let delta = Vec3::splat(amount);
386        ChunkBounds {
387            min: self.min - delta,
388            max: self.max + delta,
389        }
390    }
391
392    pub fn volume(&self) -> f32 {
393        let size = self.size();
394        size.x * size.y * size.z
395    }
396
397    pub fn center(&self) -> Vec3 {
398        (self.min + self.max) * 0.5
399    }
400
401    pub fn size(&self) -> Vec3 {
402        self.max - self.min
403    }
404
405    pub fn surface_area(&self) -> f32 {
406        let s = self.size();
407        2.0 * (s.x * s.y + s.y * s.z + s.z * s.x)
408    }
409
410    pub fn half_size(&self) -> Vec3 {
411        self.size() * 0.5
412    }
413
414    pub fn merge(&self, other: &ChunkBounds) -> ChunkBounds {
415        ChunkBounds {
416            min: self.min.min(other.min),
417            max: self.max.max(other.max),
418        }
419    }
420
421    pub fn intersection(&self, other: &ChunkBounds) -> Option<ChunkBounds> {
422        let min = self.min.max(other.min);
423        let max = self.max.min(other.max);
424        if min.x <= max.x && min.y <= max.y && min.z <= max.z {
425            Some(ChunkBounds { min, max })
426        } else {
427            None
428        }
429    }
430
431    pub fn distance_sq_to_point(&self, point: Vec3) -> f32 {
432        let clamped = point.clamp(self.min, self.max);
433        (point - clamped).length_squared()
434    }
435
436    pub fn distance_to_point(&self, point: Vec3) -> f32 {
437        self.distance_sq_to_point(point).sqrt()
438    }
439
440    pub fn closest_point(&self, point: Vec3) -> Vec3 {
441        point.clamp(self.min, self.max)
442    }
443
444    pub fn farthest_point(&self, point: Vec3) -> Vec3 {
445        let center = self.center();
446        let half = self.half_size();
447        let dir = point - center;
448        let sign = Vec3::new(
449            if dir.x >= 0.0 { 1.0 } else { -1.0 },
450            if dir.y >= 0.0 { 1.0 } else { -1.0 },
451            if dir.z >= 0.0 { 1.0 } else { -1.0 },
452        );
453        center + half * sign
454    }
455
456    pub fn octant_bounds(&self, octant: usize) -> ChunkBounds {
457        let center = self.center();
458        let (min_x, max_x) = if octant & 1 == 0 { (self.min.x, center.x) } else { (center.x, self.max.x) };
459        let (min_y, max_y) = if octant & 2 == 0 { (self.min.y, center.y) } else { (center.y, self.max.y) };
460        let (min_z, max_z) = if octant & 4 == 0 { (self.min.z, center.z) } else { (center.z, self.max.z) };
461        ChunkBounds {
462            min: Vec3::new(min_x, min_y, min_z),
463            max: Vec3::new(max_x, max_y, max_z),
464        }
465    }
466
467    pub fn octant_for_point(&self, point: Vec3) -> usize {
468        let center = self.center();
469        let mut octant = 0usize;
470        if point.x > center.x { octant |= 1; }
471        if point.y > center.y { octant |= 2; }
472        if point.z > center.z { octant |= 4; }
473        octant
474    }
475
476    pub fn transformed_by(&self, transform: Mat4) -> ChunkBounds {
477        let corners = self.corners();
478        let mut new_min = Vec3::splat(f32::MAX);
479        let mut new_max = Vec3::splat(f32::MIN);
480        for corner in &corners {
481            let transformed = transform.transform_point3(*corner);
482            new_min = new_min.min(transformed);
483            new_max = new_max.max(transformed);
484        }
485        ChunkBounds { min: new_min, max: new_max }
486    }
487
488    pub fn corners(&self) -> [Vec3; 8] {
489        [
490            Vec3::new(self.min.x, self.min.y, self.min.z),
491            Vec3::new(self.max.x, self.min.y, self.min.z),
492            Vec3::new(self.min.x, self.max.y, self.min.z),
493            Vec3::new(self.max.x, self.max.y, self.min.z),
494            Vec3::new(self.min.x, self.min.y, self.max.z),
495            Vec3::new(self.max.x, self.min.y, self.max.z),
496            Vec3::new(self.min.x, self.max.y, self.max.z),
497            Vec3::new(self.max.x, self.max.y, self.max.z),
498        ]
499    }
500
501    pub fn is_degenerate(&self) -> bool {
502        let size = self.size();
503        size.x <= 0.0 || size.y <= 0.0 || size.z <= 0.0
504    }
505
506    pub fn scale(&self, factor: f32) -> ChunkBounds {
507        let center = self.center();
508        let half = self.half_size() * factor;
509        ChunkBounds {
510            min: center - half,
511            max: center + half,
512        }
513    }
514}
515
516// ============================================================
517// STREAMING CONFIG
518// ============================================================
519
520#[derive(Debug, Clone)]
521pub struct StreamingConfig {
522    pub max_loaded_chunks: usize,
523    pub lod_distances: [f32; 5],
524    pub max_memory_mb: u64,
525    pub chunk_size: f32,
526    pub streaming_radius: f32,
527    pub vertical_streaming_radius: f32,
528    pub enable_frustum_culling: bool,
529    pub enable_occlusion_culling: bool,
530    pub enable_hlod: bool,
531    pub enable_virtual_textures: bool,
532    pub enable_impostor_billboards: bool,
533    pub max_concurrent_loads: usize,
534    pub max_concurrent_unloads: usize,
535    pub lod_bias: f32,
536    pub screen_height_pixels: u32,
537    pub fov_vertical_rad: f32,
538    pub eviction_policy: EvictionPolicy,
539    pub prefetch_distance: f32,
540    pub min_lod_retain_frames: u64,
541}
542
543impl Default for StreamingConfig {
544    fn default() -> Self {
545        Self {
546            max_loaded_chunks: DEFAULT_MAX_LOADED_CHUNKS,
547            lod_distances: LOD_DISTANCES,
548            max_memory_mb: DEFAULT_MAX_MEMORY_MB,
549            chunk_size: DEFAULT_CHUNK_SIZE,
550            streaming_radius: DEFAULT_STREAMING_RADIUS,
551            vertical_streaming_radius: DEFAULT_STREAMING_RADIUS * 0.5,
552            enable_frustum_culling: true,
553            enable_occlusion_culling: false,
554            enable_hlod: true,
555            enable_virtual_textures: true,
556            enable_impostor_billboards: true,
557            max_concurrent_loads: 4,
558            max_concurrent_unloads: 2,
559            lod_bias: 0.0,
560            screen_height_pixels: 1080,
561            fov_vertical_rad: std::f32::consts::FRAC_PI_4,
562            eviction_policy: EvictionPolicy::Lru,
563            prefetch_distance: 1.5,
564            min_lod_retain_frames: 10,
565        }
566    }
567}
568
569impl StreamingConfig {
570    pub fn new() -> Self {
571        Self::default()
572    }
573
574    pub fn with_quality_preset(preset: QualityPreset) -> Self {
575        let mut cfg = Self::default();
576        match preset {
577            QualityPreset::Low => {
578                cfg.max_loaded_chunks = 64;
579                cfg.lod_distances = [32.0, 64.0, 128.0, 256.0, 512.0];
580                cfg.max_memory_mb = 1024;
581                cfg.max_concurrent_loads = 2;
582                cfg.enable_hlod = false;
583                cfg.enable_virtual_textures = false;
584            }
585            QualityPreset::Medium => {
586                cfg.max_loaded_chunks = 128;
587                cfg.lod_distances = [48.0, 96.0, 192.0, 384.0, 768.0];
588                cfg.max_memory_mb = 2048;
589                cfg.max_concurrent_loads = 3;
590            }
591            QualityPreset::High => {
592                cfg.max_loaded_chunks = 256;
593                cfg.max_concurrent_loads = 6;
594                cfg.enable_occlusion_culling = true;
595            }
596            QualityPreset::Ultra => {
597                cfg.max_loaded_chunks = 512;
598                cfg.lod_distances = [96.0, 192.0, 384.0, 768.0, 1536.0];
599                cfg.max_memory_mb = 8192;
600                cfg.max_concurrent_loads = 8;
601                cfg.enable_occlusion_culling = true;
602            }
603        }
604        cfg
605    }
606
607    pub fn lod_for_distance(&self, dist: f32) -> LodLevel {
608        let biased = dist * (1.0 + self.lod_bias);
609        if biased < self.lod_distances[0] { LodLevel::Ultra }
610        else if biased < self.lod_distances[1] { LodLevel::High }
611        else if biased < self.lod_distances[2] { LodLevel::Medium }
612        else if biased < self.lod_distances[3] { LodLevel::Low }
613        else if biased < self.lod_distances[4] { LodLevel::Impostor }
614        else { LodLevel::Unloaded }
615    }
616
617    pub fn streaming_radius_chunks(&self) -> i32 {
618        (self.streaming_radius / self.chunk_size).ceil() as i32
619    }
620
621    pub fn memory_budget_per_lod(&self, lod: &LodLevel) -> u64 {
622        let ratio = lod.memory_multiplier();
623        (self.max_memory_mb as f32 * ratio * 0.3) as u64
624    }
625}
626
627#[derive(Debug, Clone)]
628pub enum QualityPreset {
629    Low,
630    Medium,
631    High,
632    Ultra,
633}
634
635// ============================================================
636// STREAMING CAMERA
637// ============================================================
638
639#[derive(Debug, Clone)]
640pub struct StreamingCamera {
641    pub position: Vec3,
642    pub forward: Vec3,
643    pub up: Vec3,
644    pub right: Vec3,
645    pub fov_deg: f32,
646    pub near: f32,
647    pub far: f32,
648    pub aspect_ratio: f32,
649    pub frustum_planes: [Vec4; 6],
650    pub view_matrix: Mat4,
651    pub proj_matrix: Mat4,
652    pub view_proj_matrix: Mat4,
653}
654
655impl StreamingCamera {
656    pub fn new(position: Vec3, target: Vec3, up: Vec3, fov_deg: f32, aspect: f32, near: f32, far: f32) -> Self {
657        let forward = (target - position).normalize();
658        let right = forward.cross(up).normalize();
659        let up_corrected = right.cross(forward).normalize();
660        let view = Mat4::look_at_rh(position, target, up_corrected);
661        let proj = Mat4::perspective_rh(fov_deg.to_radians(), aspect, near, far);
662        let view_proj = proj * view;
663        let mut cam = Self {
664            position,
665            forward,
666            up: up_corrected,
667            right,
668            fov_deg,
669            near,
670            far,
671            aspect_ratio: aspect,
672            frustum_planes: [Vec4::ZERO; 6],
673            view_matrix: view,
674            proj_matrix: proj,
675            view_proj_matrix: view_proj,
676        };
677        cam.extract_frustum_planes();
678        cam
679    }
680
681    pub fn extract_frustum_planes(&mut self) {
682        let m = self.view_proj_matrix;
683        let rows = [
684            Vec4::new(m.col(0).x, m.col(1).x, m.col(2).x, m.col(3).x),
685            Vec4::new(m.col(0).y, m.col(1).y, m.col(2).y, m.col(3).y),
686            Vec4::new(m.col(0).z, m.col(1).z, m.col(2).z, m.col(3).z),
687            Vec4::new(m.col(0).w, m.col(1).w, m.col(2).w, m.col(3).w),
688        ];
689        // Near: row3 + row2
690        self.frustum_planes[FRUSTUM_NEAR_PLANE] = rows[3] + rows[2];
691        // Far: row3 - row2
692        self.frustum_planes[FRUSTUM_FAR_PLANE]  = rows[3] - rows[2];
693        // Left: row3 + row0
694        self.frustum_planes[FRUSTUM_LEFT_PLANE]  = rows[3] + rows[0];
695        // Right: row3 - row0
696        self.frustum_planes[FRUSTUM_RIGHT_PLANE] = rows[3] - rows[0];
697        // Top: row3 - row1
698        self.frustum_planes[FRUSTUM_TOP_PLANE]   = rows[3] - rows[1];
699        // Bottom: row3 + row1
700        self.frustum_planes[FRUSTUM_BOTTOM_PLANE]= rows[3] + rows[1];
701        // Normalize each plane
702        for plane in &mut self.frustum_planes {
703            let len = Vec3::new(plane.x, plane.y, plane.z).length();
704            if len > 1e-8 {
705                *plane /= len;
706            }
707        }
708    }
709
710    pub fn update_position(&mut self, new_pos: Vec3, new_target: Vec3) {
711        self.position = new_pos;
712        self.forward = (new_target - new_pos).normalize();
713        let world_up = Vec3::Y;
714        self.right = self.forward.cross(world_up).normalize();
715        self.up = self.right.cross(self.forward).normalize();
716        self.view_matrix = Mat4::look_at_rh(new_pos, new_target, self.up);
717        self.view_proj_matrix = self.proj_matrix * self.view_matrix;
718        self.extract_frustum_planes();
719    }
720
721    pub fn project_sphere_to_screen(&self, center: Vec3, radius: f32, screen_height: f32) -> f32 {
722        let dist = (center - self.position).length();
723        if dist <= radius { return screen_height; }
724        let fov_rad = self.fov_deg.to_radians();
725        let proj_radius = (radius / dist) / (fov_rad * 0.5).tan();
726        proj_radius * screen_height
727    }
728
729    pub fn compute_lod_screen_size(&self, bounds: &ChunkBounds, screen_height: f32) -> f32 {
730        let center = bounds.center();
731        let radius = bounds.half_size().length();
732        self.project_sphere_to_screen(center, radius, screen_height)
733    }
734
735    pub fn distance_to_bounds(&self, bounds: &ChunkBounds) -> f32 {
736        bounds.distance_to_point(self.position)
737    }
738}
739
740// ============================================================
741// FRUSTUM CULLING
742// ============================================================
743
744#[derive(Debug, Clone)]
745pub struct FrustumCulling {
746    pub planes: [Vec4; 6],
747}
748
749impl FrustumCulling {
750    pub fn new(planes: [Vec4; 6]) -> Self {
751        Self { planes }
752    }
753
754    pub fn from_view_proj(view_proj: Mat4) -> Self {
755        let mut planes = [Vec4::ZERO; 6];
756        let m = view_proj;
757        let rows = [
758            Vec4::new(m.col(0).x, m.col(1).x, m.col(2).x, m.col(3).x),
759            Vec4::new(m.col(0).y, m.col(1).y, m.col(2).y, m.col(3).y),
760            Vec4::new(m.col(0).z, m.col(1).z, m.col(2).z, m.col(3).z),
761            Vec4::new(m.col(0).w, m.col(1).w, m.col(2).w, m.col(3).w),
762        ];
763        planes[FRUSTUM_NEAR_PLANE]   = rows[3] + rows[2];
764        planes[FRUSTUM_FAR_PLANE]    = rows[3] - rows[2];
765        planes[FRUSTUM_LEFT_PLANE]   = rows[3] + rows[0];
766        planes[FRUSTUM_RIGHT_PLANE]  = rows[3] - rows[0];
767        planes[FRUSTUM_TOP_PLANE]    = rows[3] - rows[1];
768        planes[FRUSTUM_BOTTOM_PLANE] = rows[3] + rows[1];
769        for plane in &mut planes {
770            let len = Vec3::new(plane.x, plane.y, plane.z).length();
771            if len > 1e-8 { *plane /= len; }
772        }
773        Self { planes }
774    }
775
776    pub fn test_aabb(&self, bounds: &ChunkBounds) -> FrustumResult {
777        let mut result = FrustumResult::Inside;
778        for plane in &self.planes {
779            let normal = Vec3::new(plane.x, plane.y, plane.z);
780            let d = plane.w;
781            // positive vertex: the corner that is most in direction of normal
782            let px = if normal.x >= 0.0 { bounds.max.x } else { bounds.min.x };
783            let py = if normal.y >= 0.0 { bounds.max.y } else { bounds.min.y };
784            let pz = if normal.z >= 0.0 { bounds.max.z } else { bounds.min.z };
785            let p_vert = Vec3::new(px, py, pz);
786            // negative vertex
787            let nx_v = if normal.x >= 0.0 { bounds.min.x } else { bounds.max.x };
788            let ny_v = if normal.y >= 0.0 { bounds.min.y } else { bounds.max.y };
789            let nz_v = if normal.z >= 0.0 { bounds.min.z } else { bounds.max.z };
790            let n_vert = Vec3::new(nx_v, ny_v, nz_v);
791
792            if normal.dot(p_vert) + d < 0.0 {
793                return FrustumResult::Outside;
794            }
795            if normal.dot(n_vert) + d < 0.0 {
796                result = FrustumResult::Intersects;
797            }
798        }
799        result
800    }
801
802    pub fn test_sphere(&self, center: Vec3, radius: f32) -> FrustumResult {
803        let mut result = FrustumResult::Inside;
804        for plane in &self.planes {
805            let normal = Vec3::new(plane.x, plane.y, plane.z);
806            let dist = normal.dot(center) + plane.w;
807            if dist < -radius { return FrustumResult::Outside; }
808            if dist < radius  { result = FrustumResult::Intersects; }
809        }
810        result
811    }
812
813    pub fn test_point(&self, point: Vec3) -> bool {
814        for plane in &self.planes {
815            let normal = Vec3::new(plane.x, plane.y, plane.z);
816            if normal.dot(point) + plane.w < 0.0 { return false; }
817        }
818        true
819    }
820
821    pub fn test_aabb_fast(&self, bounds: &ChunkBounds) -> bool {
822        for plane in &self.planes {
823            let normal = Vec3::new(plane.x, plane.y, plane.z);
824            let d = plane.w;
825            let px = if normal.x >= 0.0 { bounds.max.x } else { bounds.min.x };
826            let py = if normal.y >= 0.0 { bounds.max.y } else { bounds.min.y };
827            let pz = if normal.z >= 0.0 { bounds.max.z } else { bounds.min.z };
828            if normal.dot(Vec3::new(px, py, pz)) + d < 0.0 {
829                return false;
830            }
831        }
832        true
833    }
834
835    pub fn compute_visibility_mask(&self, bounds_list: &[ChunkBounds]) -> Vec<bool> {
836        bounds_list.iter().map(|b| self.test_aabb_fast(b)).collect()
837    }
838}
839
840#[derive(Debug, Clone, PartialEq)]
841pub enum FrustumResult {
842    Inside,
843    Outside,
844    Intersects,
845}
846
847// ============================================================
848// OCTREE
849// ============================================================
850
851#[derive(Debug, Clone)]
852pub struct OctreeNode {
853    pub bounds: ChunkBounds,
854    pub children: Option<Box<[OctreeNode; 8]>>,
855    pub objects: Vec<u32>,
856    pub depth: u32,
857}
858
859impl OctreeNode {
860    pub fn new(bounds: ChunkBounds, depth: u32) -> Self {
861        Self {
862            bounds,
863            children: None,
864            objects: Vec::new(),
865            depth,
866        }
867    }
868
869    pub fn is_leaf(&self) -> bool {
870        self.children.is_none()
871    }
872
873    pub fn object_count(&self) -> usize {
874        self.objects.len()
875    }
876
877    pub fn total_object_count(&self) -> usize {
878        let mut count = self.objects.len();
879        if let Some(children) = &self.children {
880            for child in children.iter() {
881                count += child.total_object_count();
882            }
883        }
884        count
885    }
886
887    pub fn depth(&self) -> u32 {
888        self.depth
889    }
890
891    pub fn max_depth(&self) -> u32 {
892        if let Some(children) = &self.children {
893            children.iter().map(|c| c.max_depth()).max().unwrap_or(self.depth)
894        } else {
895            self.depth
896        }
897    }
898
899    pub fn query_sphere(&self, center: Vec3, radius: f32, result: &mut Vec<u32>) {
900        let dist_sq = self.bounds.distance_sq_to_point(center);
901        if dist_sq > radius * radius { return; }
902        result.extend_from_slice(&self.objects);
903        if let Some(children) = &self.children {
904            for child in children.iter() {
905                child.query_sphere(center, radius, result);
906            }
907        }
908    }
909
910    pub fn query_aabb(&self, query: &ChunkBounds, result: &mut Vec<u32>) {
911        if !self.bounds.intersects(query) { return; }
912        result.extend_from_slice(&self.objects);
913        if let Some(children) = &self.children {
914            for child in children.iter() {
915                child.query_aabb(query, result);
916            }
917        }
918    }
919
920    pub fn query_frustum(&self, frustum: &FrustumCulling, result: &mut Vec<u32>) {
921        let fr = frustum.test_aabb(&self.bounds);
922        match fr {
923            FrustumResult::Outside => return,
924            FrustumResult::Inside => {
925                self.collect_all(result);
926                return;
927            }
928            FrustumResult::Intersects => {
929                result.extend_from_slice(&self.objects);
930                if let Some(children) = &self.children {
931                    for child in children.iter() {
932                        child.query_frustum(frustum, result);
933                    }
934                }
935            }
936        }
937    }
938
939    pub fn collect_all(&self, result: &mut Vec<u32>) {
940        result.extend_from_slice(&self.objects);
941        if let Some(children) = &self.children {
942            for child in children.iter() {
943                child.collect_all(result);
944            }
945        }
946    }
947
948    pub fn node_count(&self) -> usize {
949        let mut count = 1;
950        if let Some(children) = &self.children {
951            for child in children.iter() {
952                count += child.node_count();
953            }
954        }
955        count
956    }
957}
958
959// ============================================================
960// OCTREE BUILDER
961// ============================================================
962
963#[derive(Debug, Clone)]
964pub struct OctreeBuilder {
965    pub max_depth: u32,
966    pub max_per_node: usize,
967}
968
969impl OctreeBuilder {
970    pub fn new(max_depth: u32, max_per_node: usize) -> Self {
971        Self { max_depth, max_per_node }
972    }
973
974    pub fn build(&self, points: &[(u32, Vec3)]) -> OctreeNode {
975        if points.is_empty() {
976            return OctreeNode::new(
977                ChunkBounds::new(Vec3::ZERO, Vec3::ZERO),
978                0,
979            );
980        }
981        let bounds = self.compute_bounds(points);
982        let mut root = OctreeNode::new(bounds, 0);
983        for &(id, pos) in points {
984            self.insert(&mut root, id, pos);
985        }
986        root
987    }
988
989    fn compute_bounds(&self, points: &[(u32, Vec3)]) -> ChunkBounds {
990        let mut min = Vec3::splat(f32::MAX);
991        let mut max = Vec3::splat(f32::MIN);
992        for &(_, pos) in points {
993            min = min.min(pos);
994            max = max.max(pos);
995        }
996        let padding = Vec3::splat(0.001);
997        ChunkBounds { min: min - padding, max: max + padding }
998    }
999
1000    pub fn insert(&self, node: &mut OctreeNode, id: u32, pos: Vec3) {
1001        if !node.bounds.contains(pos) { return; }
1002
1003        if node.is_leaf() {
1004            if node.objects.len() < self.max_per_node || node.depth >= self.max_depth {
1005                node.objects.push(id);
1006            } else {
1007                self.subdivide(node);
1008                self.insert_into_children(node, id, pos);
1009            }
1010        } else {
1011            self.insert_into_children(node, id, pos);
1012        }
1013    }
1014
1015    fn insert_into_children(&self, node: &mut OctreeNode, id: u32, pos: Vec3) {
1016        let octant = node.bounds.octant_for_point(pos);
1017        if let Some(children) = &mut node.children {
1018            self.insert(&mut children[octant], id, pos);
1019        }
1020    }
1021
1022    fn subdivide(&self, node: &mut OctreeNode) {
1023        let child_depth = node.depth + 1;
1024        let children: [OctreeNode; 8] = std::array::from_fn(|i| {
1025            let bounds = node.bounds.octant_bounds(i);
1026            OctreeNode::new(bounds, child_depth)
1027        });
1028        node.children = Some(Box::new(children));
1029
1030        let existing = std::mem::take(&mut node.objects);
1031        for id in existing {
1032            // We need the position to re-insert; for now push to first matching child
1033            // In a real impl, we'd store positions too — so we check bounds inclusion
1034            // by placing them in root's objects as fallback for objects at exact center
1035            node.objects.push(id);
1036        }
1037    }
1038
1039    pub fn build_with_positions(&self, points: &[(u32, Vec3)]) -> (OctreeNode, HashMap<u32, Vec3>) {
1040        let mut pos_map = HashMap::new();
1041        for &(id, pos) in points {
1042            pos_map.insert(id, pos);
1043        }
1044        let bounds = if points.is_empty() {
1045            ChunkBounds::new(Vec3::ZERO, Vec3::ONE)
1046        } else {
1047            self.compute_bounds(points)
1048        };
1049        let mut root = OctreeNode::new(bounds, 0);
1050        for &(id, pos) in points {
1051            self.insert_with_pos(&mut root, id, pos, &pos_map);
1052        }
1053        (root, pos_map)
1054    }
1055
1056    fn insert_with_pos(&self, node: &mut OctreeNode, id: u32, pos: Vec3, pos_map: &HashMap<u32, Vec3>) {
1057        if !node.bounds.contains(pos) { return; }
1058        if node.is_leaf() {
1059            if node.objects.len() < self.max_per_node || node.depth >= self.max_depth {
1060                node.objects.push(id);
1061            } else {
1062                self.subdivide_with_pos(node, pos_map);
1063                let octant = node.bounds.octant_for_point(pos);
1064                if let Some(children) = &mut node.children {
1065                    self.insert_with_pos(&mut children[octant], id, pos, pos_map);
1066                }
1067            }
1068        } else {
1069            let octant = node.bounds.octant_for_point(pos);
1070            if let Some(children) = &mut node.children {
1071                self.insert_with_pos(&mut children[octant], id, pos, pos_map);
1072            }
1073        }
1074    }
1075
1076    fn subdivide_with_pos(&self, node: &mut OctreeNode, pos_map: &HashMap<u32, Vec3>) {
1077        let child_depth = node.depth + 1;
1078        let children: [OctreeNode; 8] = std::array::from_fn(|i| {
1079            OctreeNode::new(node.bounds.octant_bounds(i), child_depth)
1080        });
1081        node.children = Some(Box::new(children));
1082        let existing = std::mem::take(&mut node.objects);
1083        for id in existing {
1084            if let Some(&pos) = pos_map.get(&id) {
1085                let octant = node.bounds.octant_for_point(pos);
1086                if let Some(children) = &mut node.children {
1087                    children[octant].objects.push(id);
1088                }
1089            } else {
1090                node.objects.push(id);
1091            }
1092        }
1093    }
1094}
1095
1096// ============================================================
1097// BVH NODE
1098// ============================================================
1099
1100#[derive(Debug, Clone)]
1101pub struct BvhNode {
1102    pub bounds: ChunkBounds,
1103    pub kind: BvhNodeKind,
1104    pub left: Option<Box<BvhNode>>,
1105    pub right: Option<Box<BvhNode>>,
1106    pub objects: Vec<u32>,
1107    pub parent_index: Option<usize>,
1108}
1109
1110impl BvhNode {
1111    pub fn new_leaf(bounds: ChunkBounds, objects: Vec<u32>) -> Self {
1112        Self {
1113            bounds,
1114            kind: BvhNodeKind::Leaf,
1115            left: None,
1116            right: None,
1117            objects,
1118            parent_index: None,
1119        }
1120    }
1121
1122    pub fn new_internal(bounds: ChunkBounds, left: BvhNode, right: BvhNode) -> Self {
1123        Self {
1124            bounds,
1125            kind: BvhNodeKind::Internal,
1126            left: Some(Box::new(left)),
1127            right: Some(Box::new(right)),
1128            objects: Vec::new(),
1129            parent_index: None,
1130        }
1131    }
1132
1133    pub fn is_leaf(&self) -> bool {
1134        matches!(self.kind, BvhNodeKind::Leaf)
1135    }
1136
1137    pub fn depth(&self) -> usize {
1138        match (&self.left, &self.right) {
1139            (Some(l), Some(r)) => 1 + l.depth().max(r.depth()),
1140            (Some(l), None) => 1 + l.depth(),
1141            (None, Some(r)) => 1 + r.depth(),
1142            (None, None) => 0,
1143        }
1144    }
1145
1146    pub fn node_count(&self) -> usize {
1147        let mut count = 1;
1148        if let Some(l) = &self.left  { count += l.node_count(); }
1149        if let Some(r) = &self.right { count += r.node_count(); }
1150        count
1151    }
1152
1153    pub fn query_ray(&self, origin: Vec3, dir: Vec3, t_min: f32, t_max: f32, result: &mut Vec<u32>) {
1154        if !ray_aabb_intersect(origin, dir, &self.bounds, t_min, t_max) { return; }
1155        if self.is_leaf() {
1156            result.extend_from_slice(&self.objects);
1157            return;
1158        }
1159        if let Some(l) = &self.left  { l.query_ray(origin, dir, t_min, t_max, result); }
1160        if let Some(r) = &self.right { r.query_ray(origin, dir, t_min, t_max, result); }
1161    }
1162
1163    pub fn query_aabb(&self, query: &ChunkBounds, result: &mut Vec<u32>) {
1164        if !self.bounds.intersects(query) { return; }
1165        if self.is_leaf() {
1166            result.extend_from_slice(&self.objects);
1167            return;
1168        }
1169        if let Some(l) = &self.left  { l.query_aabb(query, result); }
1170        if let Some(r) = &self.right { r.query_aabb(query, result); }
1171    }
1172
1173    pub fn query_frustum(&self, frustum: &FrustumCulling, result: &mut Vec<u32>) {
1174        match frustum.test_aabb(&self.bounds) {
1175            FrustumResult::Outside => {}
1176            FrustumResult::Inside => { self.collect_all(result); }
1177            FrustumResult::Intersects => {
1178                if self.is_leaf() {
1179                    result.extend_from_slice(&self.objects);
1180                } else {
1181                    if let Some(l) = &self.left  { l.query_frustum(frustum, result); }
1182                    if let Some(r) = &self.right { r.query_frustum(frustum, result); }
1183                }
1184            }
1185        }
1186    }
1187
1188    fn collect_all(&self, result: &mut Vec<u32>) {
1189        result.extend_from_slice(&self.objects);
1190        if let Some(l) = &self.left  { l.collect_all(result); }
1191        if let Some(r) = &self.right { r.collect_all(result); }
1192    }
1193}
1194
1195// ============================================================
1196// BVH BUILDER (SAH)
1197// ============================================================
1198
1199#[derive(Debug, Clone)]
1200pub struct BvhBuilder {
1201    pub max_leaf_objects: usize,
1202    pub num_bins: usize,
1203}
1204
1205impl BvhBuilder {
1206    pub fn new(max_leaf_objects: usize, num_bins: usize) -> Self {
1207        Self { max_leaf_objects, num_bins }
1208    }
1209
1210    pub fn build(&self, objects: &[(u32, ChunkBounds)]) -> Option<BvhNode> {
1211        if objects.is_empty() { return None; }
1212        let indices: Vec<usize> = (0..objects.len()).collect();
1213        Some(self.build_recursive(objects, &indices))
1214    }
1215
1216    fn build_recursive(&self, objects: &[(u32, ChunkBounds)], indices: &[usize]) -> BvhNode {
1217        if indices.len() <= self.max_leaf_objects {
1218            return self.make_leaf(objects, indices);
1219        }
1220
1221        let node_bounds = self.compute_union_bounds(objects, indices);
1222        let (split_axis, split_pos, split_cost) = self.find_best_split(objects, indices, &node_bounds);
1223
1224        // Check if splitting is worth it vs making a leaf
1225        let leaf_cost = SAH_INTERSECTION_COST * indices.len() as f32;
1226        if split_cost >= leaf_cost {
1227            return self.make_leaf(objects, indices);
1228        }
1229
1230        let (left_indices, right_indices) = self.partition(objects, indices, split_axis, split_pos);
1231
1232        if left_indices.is_empty() || right_indices.is_empty() {
1233            return self.make_leaf(objects, indices);
1234        }
1235
1236        let left  = self.build_recursive(objects, &left_indices);
1237        let right = self.build_recursive(objects, &right_indices);
1238        BvhNode::new_internal(node_bounds, left, right)
1239    }
1240
1241    fn find_best_split(
1242        &self,
1243        objects: &[(u32, ChunkBounds)],
1244        indices: &[usize],
1245        node_bounds: &ChunkBounds,
1246    ) -> (usize, f32, f32) {
1247        let node_sa = node_bounds.surface_area();
1248        let mut best_cost = f32::MAX;
1249        let mut best_axis = 0;
1250        let mut best_split = 0.0f32;
1251
1252        let size = node_bounds.size();
1253
1254        for axis in 0..3 {
1255            let axis_len = match axis { 0 => size.x, 1 => size.y, _ => size.z };
1256            if axis_len < 1e-8 { continue; }
1257
1258            let axis_min = match axis { 0 => node_bounds.min.x, 1 => node_bounds.min.y, _ => node_bounds.min.z };
1259
1260            for bin in 1..self.num_bins {
1261                let t = bin as f32 / self.num_bins as f32;
1262                let split_pos = axis_min + t * axis_len;
1263
1264                let mut left_bounds: Option<ChunkBounds> = None;
1265                let mut right_bounds: Option<ChunkBounds> = None;
1266                let mut left_count = 0usize;
1267                let mut right_count = 0usize;
1268
1269                for &idx in indices {
1270                    let center = objects[idx].1.center();
1271                    let coord = match axis { 0 => center.x, 1 => center.y, _ => center.z };
1272                    if coord < split_pos {
1273                        left_count += 1;
1274                        left_bounds = Some(match left_bounds {
1275                            Some(b) => b.merge(&objects[idx].1),
1276                            None => objects[idx].1.clone(),
1277                        });
1278                    } else {
1279                        right_count += 1;
1280                        right_bounds = Some(match right_bounds {
1281                            Some(b) => b.merge(&objects[idx].1),
1282                            None => objects[idx].1.clone(),
1283                        });
1284                    }
1285                }
1286
1287                if left_count == 0 || right_count == 0 { continue; }
1288
1289                let left_sa  = left_bounds.map(|b| b.surface_area()).unwrap_or(0.0);
1290                let right_sa = right_bounds.map(|b| b.surface_area()).unwrap_or(0.0);
1291
1292                let cost = SAH_TRAVERSAL_COST
1293                    + SAH_INTERSECTION_COST * (
1294                        left_sa  / node_sa * left_count  as f32
1295                      + right_sa / node_sa * right_count as f32
1296                    );
1297
1298                if cost < best_cost {
1299                    best_cost = cost;
1300                    best_axis = axis;
1301                    best_split = split_pos;
1302                }
1303            }
1304        }
1305        (best_axis, best_split, best_cost)
1306    }
1307
1308    fn partition(
1309        &self,
1310        objects: &[(u32, ChunkBounds)],
1311        indices: &[usize],
1312        axis: usize,
1313        split: f32,
1314    ) -> (Vec<usize>, Vec<usize>) {
1315        let mut left  = Vec::new();
1316        let mut right = Vec::new();
1317        for &idx in indices {
1318            let center = objects[idx].1.center();
1319            let coord = match axis { 0 => center.x, 1 => center.y, _ => center.z };
1320            if coord < split { left.push(idx); } else { right.push(idx); }
1321        }
1322        (left, right)
1323    }
1324
1325    fn make_leaf(&self, objects: &[(u32, ChunkBounds)], indices: &[usize]) -> BvhNode {
1326        let bounds = self.compute_union_bounds(objects, indices);
1327        let ids: Vec<u32> = indices.iter().map(|&i| objects[i].0).collect();
1328        BvhNode::new_leaf(bounds, ids)
1329    }
1330
1331    fn compute_union_bounds(&self, objects: &[(u32, ChunkBounds)], indices: &[usize]) -> ChunkBounds {
1332        let mut min = Vec3::splat(f32::MAX);
1333        let mut max = Vec3::splat(f32::MIN);
1334        for &idx in indices {
1335            min = min.min(objects[idx].1.min);
1336            max = max.max(objects[idx].1.max);
1337        }
1338        ChunkBounds { min, max }
1339    }
1340}
1341
1342// ============================================================
1343// RAY-AABB INTERSECTION HELPER
1344// ============================================================
1345
1346pub fn ray_aabb_intersect(origin: Vec3, dir: Vec3, bounds: &ChunkBounds, t_min: f32, t_max: f32) -> bool {
1347    let inv_dir = Vec3::new(
1348        if dir.x.abs() > 1e-12 { 1.0 / dir.x } else { f32::MAX },
1349        if dir.y.abs() > 1e-12 { 1.0 / dir.y } else { f32::MAX },
1350        if dir.z.abs() > 1e-12 { 1.0 / dir.z } else { f32::MAX },
1351    );
1352    let t1 = (bounds.min - origin) * inv_dir;
1353    let t2 = (bounds.max - origin) * inv_dir;
1354    let tmin = t1.min(t2);
1355    let tmax = t1.max(t2);
1356    let enter = tmin.x.max(tmin.y).max(tmin.z).max(t_min);
1357    let exit  = tmax.x.min(tmax.y).min(tmax.z).min(t_max);
1358    enter <= exit
1359}
1360
1361// ============================================================
1362// STREAMING CHUNK
1363// ============================================================
1364
1365#[derive(Debug, Clone)]
1366pub struct StreamingChunk {
1367    pub coord: ChunkCoord,
1368    pub bounds: ChunkBounds,
1369    pub lod_level: LodLevel,
1370    pub load_state: ChunkLoadState,
1371    pub resident_objects: Vec<u32>,
1372    pub memory_bytes: u64,
1373    pub last_visible_frame: u64,
1374    pub last_loaded_frame: u64,
1375    pub load_priority: f32,
1376    pub distance_to_viewer: f32,
1377    pub screen_space_size: f32,
1378    pub is_visible: bool,
1379    pub dependencies: Vec<ChunkCoord>,
1380    pub hlod_cluster_id: Option<u32>,
1381    pub data_layer_mask: u32,
1382    pub version: u32,
1383    pub flags: ChunkFlags,
1384}
1385
1386#[derive(Debug, Clone, Default)]
1387pub struct ChunkFlags {
1388    pub dirty: bool,
1389    pub needs_lod_update: bool,
1390    pub needs_terrain_stitch: bool,
1391    pub impostor_valid: bool,
1392    pub heightmap_loaded: bool,
1393    pub collision_loaded: bool,
1394    pub nav_mesh_loaded: bool,
1395}
1396
1397impl StreamingChunk {
1398    pub fn new(coord: ChunkCoord, chunk_size: f32) -> Self {
1399        let bounds = ChunkBounds::from_chunk_coord(&coord, chunk_size);
1400        Self {
1401            coord,
1402            bounds,
1403            lod_level: LodLevel::Unloaded,
1404            load_state: ChunkLoadState::Unloaded,
1405            resident_objects: Vec::new(),
1406            memory_bytes: 0,
1407            last_visible_frame: 0,
1408            last_loaded_frame: 0,
1409            load_priority: 0.0,
1410            distance_to_viewer: f32::MAX,
1411            screen_space_size: 0.0,
1412            is_visible: false,
1413            dependencies: Vec::new(),
1414            hlod_cluster_id: None,
1415            data_layer_mask: 0xFFFF_FFFF,
1416            version: 0,
1417            flags: ChunkFlags::default(),
1418        }
1419    }
1420
1421    pub fn update_distance(&mut self, viewer_pos: Vec3) {
1422        self.distance_to_viewer = self.bounds.distance_to_point(viewer_pos);
1423    }
1424
1425    pub fn compute_load_priority(&mut self, viewer_pos: Vec3, viewer_forward: Vec3) -> f32 {
1426        let center = self.bounds.center();
1427        let to_chunk = (center - viewer_pos).normalize_or_zero();
1428        let dot = viewer_forward.dot(to_chunk).max(0.0);
1429        let dist_factor = 1.0 / (1.0 + self.distance_to_viewer * 0.01);
1430        let facing_factor = 0.5 + 0.5 * dot;
1431        let priority = dist_factor * facing_factor * (if self.is_visible { 2.0 } else { 1.0 });
1432        self.load_priority = priority;
1433        priority
1434    }
1435
1436    pub fn estimate_memory_for_lod(&self, lod: &LodLevel) -> u64 {
1437        let base_mb = 8u64;
1438        let object_mb = self.resident_objects.len() as u64 * 2;
1439        let total_mb = (base_mb + object_mb) as f32 * lod.memory_multiplier();
1440        (total_mb * 1024.0 * 1024.0) as u64
1441    }
1442
1443    pub fn can_load(&self) -> bool {
1444        matches!(self.load_state, ChunkLoadState::Unloaded | ChunkLoadState::Queued)
1445    }
1446
1447    pub fn can_evict(&self) -> bool {
1448        self.load_state.can_evict() && self.lod_level.is_loaded()
1449    }
1450
1451    pub fn age_frames(&self, current_frame: u64) -> u64 {
1452        current_frame.saturating_sub(self.last_visible_frame)
1453    }
1454
1455    pub fn needs_lod_upgrade(&self, desired_lod: &LodLevel) -> bool {
1456        self.lod_level < *desired_lod
1457    }
1458
1459    pub fn needs_lod_downgrade(&self, desired_lod: &LodLevel) -> bool {
1460        self.lod_level > *desired_lod && self.lod_level != LodLevel::Unloaded
1461    }
1462
1463    pub fn mark_visible(&mut self, frame: u64) {
1464        self.is_visible = true;
1465        self.last_visible_frame = frame;
1466    }
1467
1468    pub fn mark_not_visible(&mut self) {
1469        self.is_visible = false;
1470    }
1471
1472    pub fn add_object(&mut self, object_id: u32) {
1473        if !self.resident_objects.contains(&object_id) {
1474            self.resident_objects.push(object_id);
1475        }
1476    }
1477
1478    pub fn remove_object(&mut self, object_id: u32) {
1479        self.resident_objects.retain(|&id| id != object_id);
1480    }
1481
1482    pub fn is_dependency_satisfied(&self, loaded_chunks: &HashSet<ChunkCoord>) -> bool {
1483        self.dependencies.iter().all(|dep| loaded_chunks.contains(dep))
1484    }
1485}
1486
1487// ============================================================
1488// STREAMING PRIORITY QUEUE
1489// ============================================================
1490
1491#[derive(Debug, Clone)]
1492pub struct StreamingPriority {
1493    pub queue: BTreeMap<OrderedFloat, ChunkCoord>,
1494    pub coord_to_priority: HashMap<ChunkCoord, f32>,
1495    pub max_size: usize,
1496}
1497
1498#[derive(Debug, Clone, PartialEq)]
1499struct OrderedFloat(f32);
1500
1501impl Eq for OrderedFloat {}
1502
1503impl PartialOrd for OrderedFloat {
1504    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1505        Some(self.cmp(other))
1506    }
1507}
1508
1509impl Ord for OrderedFloat {
1510    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1511        self.0.partial_cmp(&other.0).unwrap_or(std::cmp::Ordering::Equal)
1512    }
1513}
1514
1515impl StreamingPriority {
1516    pub fn new(max_size: usize) -> Self {
1517        Self {
1518            queue: BTreeMap::new(),
1519            coord_to_priority: HashMap::new(),
1520            max_size,
1521        }
1522    }
1523
1524    pub fn push(&mut self, coord: ChunkCoord, priority: f32) {
1525        if let Some(&old_priority) = self.coord_to_priority.get(&coord) {
1526            self.queue.remove(&OrderedFloat(old_priority));
1527        }
1528        // Use negative priority so highest-priority items come first (BTreeMap is ascending)
1529        self.queue.insert(OrderedFloat(-priority), coord.clone());
1530        self.coord_to_priority.insert(coord, priority);
1531    }
1532
1533    pub fn pop_highest(&mut self) -> Option<(ChunkCoord, f32)> {
1534        if let Some((key, coord)) = self.queue.pop_first() {
1535            let priority = -key.0;
1536            self.coord_to_priority.remove(&coord);
1537            Some((coord, priority))
1538        } else {
1539            None
1540        }
1541    }
1542
1543    pub fn peek_highest(&self) -> Option<(&ChunkCoord, f32)> {
1544        self.queue.iter().next().map(|(k, v)| (v, -k.0))
1545    }
1546
1547    pub fn contains(&self, coord: &ChunkCoord) -> bool {
1548        self.coord_to_priority.contains_key(coord)
1549    }
1550
1551    pub fn remove(&mut self, coord: &ChunkCoord) {
1552        if let Some(priority) = self.coord_to_priority.remove(coord) {
1553            self.queue.remove(&OrderedFloat(-priority));
1554        }
1555    }
1556
1557    pub fn len(&self) -> usize {
1558        self.queue.len()
1559    }
1560
1561    pub fn is_empty(&self) -> bool {
1562        self.queue.is_empty()
1563    }
1564
1565    pub fn update_priorities(&mut self, viewer_pos: Vec3, chunks: &HashMap<ChunkCoord, StreamingChunk>) {
1566        let coords: Vec<ChunkCoord> = self.coord_to_priority.keys().cloned().collect();
1567        for coord in coords {
1568            if let Some(chunk) = chunks.get(&coord) {
1569                let priority = 1.0 / (1.0 + chunk.distance_to_viewer);
1570                self.push(coord, priority);
1571            }
1572        }
1573    }
1574
1575    pub fn drain_up_to(&mut self, n: usize) -> Vec<(ChunkCoord, f32)> {
1576        let mut result = Vec::with_capacity(n);
1577        for _ in 0..n {
1578            if let Some(item) = self.pop_highest() {
1579                result.push(item);
1580            } else {
1581                break;
1582            }
1583        }
1584        result
1585    }
1586
1587    pub fn recompute_all(&mut self, viewer_pos: Vec3, chunk_size: f32) {
1588        let coords: Vec<ChunkCoord> = self.coord_to_priority.keys().cloned().collect();
1589        let old_queue = std::mem::take(&mut self.queue);
1590        self.coord_to_priority.clear();
1591        for coord in coords {
1592            let center = coord.to_world_center(chunk_size);
1593            let dist = (center - viewer_pos).length();
1594            let priority = 1.0 / (1.0 + dist * 0.01);
1595            self.push(coord, priority);
1596        }
1597    }
1598}
1599
1600// ============================================================
1601// MEMORY BUDGET
1602// ============================================================
1603
1604#[derive(Debug, Clone)]
1605pub struct MemoryBudget {
1606    pub max_bytes: u64,
1607    pub used_bytes: u64,
1608    pub lod_usage: HashMap<LodLevel, u64>,
1609    pub chunk_memory: HashMap<ChunkCoord, u64>,
1610    pub lru_order: VecDeque<ChunkCoord>,
1611    pub access_count: HashMap<ChunkCoord, u64>,
1612    pub last_access_frame: HashMap<ChunkCoord, u64>,
1613    pub policy: EvictionPolicy,
1614}
1615
1616impl MemoryBudget {
1617    pub fn new(max_mb: u64, policy: EvictionPolicy) -> Self {
1618        Self {
1619            max_bytes: max_mb * 1024 * 1024,
1620            used_bytes: 0,
1621            lod_usage: HashMap::new(),
1622            chunk_memory: HashMap::new(),
1623            lru_order: VecDeque::new(),
1624            access_count: HashMap::new(),
1625            last_access_frame: HashMap::new(),
1626            policy,
1627        }
1628    }
1629
1630    pub fn available_bytes(&self) -> u64 {
1631        self.max_bytes.saturating_sub(self.used_bytes)
1632    }
1633
1634    pub fn usage_ratio(&self) -> f32 {
1635        self.used_bytes as f32 / self.max_bytes as f32
1636    }
1637
1638    pub fn can_allocate(&self, bytes: u64) -> bool {
1639        self.used_bytes + bytes <= self.max_bytes
1640    }
1641
1642    pub fn allocate(&mut self, coord: ChunkCoord, lod: LodLevel, bytes: u64, frame: u64) -> bool {
1643        if !self.can_allocate(bytes) { return false; }
1644        self.used_bytes += bytes;
1645        *self.lod_usage.entry(lod).or_insert(0) += bytes;
1646        self.chunk_memory.insert(coord.clone(), bytes);
1647        self.touch(coord, frame);
1648        true
1649    }
1650
1651    pub fn free(&mut self, coord: &ChunkCoord, lod: &LodLevel) {
1652        if let Some(bytes) = self.chunk_memory.remove(coord) {
1653            self.used_bytes = self.used_bytes.saturating_sub(bytes);
1654            if let Some(usage) = self.lod_usage.get_mut(lod) {
1655                *usage = usage.saturating_sub(bytes);
1656            }
1657        }
1658        self.lru_order.retain(|c| c != coord);
1659        self.access_count.remove(coord);
1660        self.last_access_frame.remove(coord);
1661    }
1662
1663    pub fn touch(&mut self, coord: ChunkCoord, frame: u64) {
1664        self.lru_order.retain(|c| c != &coord);
1665        self.lru_order.push_back(coord.clone());
1666        *self.access_count.entry(coord.clone()).or_insert(0) += 1;
1667        self.last_access_frame.insert(coord, frame);
1668    }
1669
1670    pub fn eviction_candidates(&self, num: usize, current_frame: u64) -> Vec<ChunkCoord> {
1671        match self.policy {
1672            EvictionPolicy::Lru => {
1673                self.lru_order.iter().take(num).cloned().collect()
1674            }
1675            EvictionPolicy::Lfu => {
1676                let mut by_count: Vec<_> = self.access_count.iter().collect();
1677                by_count.sort_by_key(|(_, &c)| c);
1678                by_count.iter().take(num).map(|(c, _)| (*c).clone()).collect()
1679            }
1680            EvictionPolicy::DistanceBased => {
1681                // Return oldest accessed candidates
1682                let mut by_frame: Vec<_> = self.last_access_frame.iter().collect();
1683                by_frame.sort_by_key(|(_, &f)| f);
1684                by_frame.iter().take(num).map(|(c, _)| (*c).clone()).collect()
1685            }
1686            EvictionPolicy::PriorityBased => {
1687                let mut aged: Vec<_> = self.last_access_frame
1688                    .iter()
1689                    .filter(|(_, &f)| current_frame.saturating_sub(f) > LRU_MAX_AGE_FRAMES)
1690                    .collect();
1691                aged.sort_by_key(|(_, &f)| f);
1692                aged.iter().take(num).map(|(c, _)| (*c).clone()).collect()
1693            }
1694        }
1695    }
1696
1697    pub fn needs_eviction(&self) -> bool {
1698        self.usage_ratio() > 0.95
1699    }
1700
1701    pub fn memory_for_coord(&self, coord: &ChunkCoord) -> u64 {
1702        *self.chunk_memory.get(coord).unwrap_or(&0)
1703    }
1704
1705    pub fn lod_usage_mb(&self, lod: &LodLevel) -> f32 {
1706        *self.lod_usage.get(lod).unwrap_or(&0) as f32 / (1024.0 * 1024.0)
1707    }
1708
1709    pub fn total_used_mb(&self) -> f32 {
1710        self.used_bytes as f32 / (1024.0 * 1024.0)
1711    }
1712
1713    pub fn chunk_count(&self) -> usize {
1714        self.chunk_memory.len()
1715    }
1716}
1717
1718// ============================================================
1719// CHUNK MESH LOD
1720// ============================================================
1721
1722#[derive(Debug, Clone)]
1723pub struct ChunkMeshLod {
1724    pub base_vertex_count: u32,
1725    pub lod_vertex_counts: [u32; 6],
1726    pub simplification_ratios: [f32; 6],
1727    pub screen_space_error_thresholds: [f32; 6],
1728    pub index_buffer_sizes: [u32; 6],
1729    pub memory_sizes_bytes: [u64; 6],
1730    pub transition_distances: [f32; 5],
1731}
1732
1733impl ChunkMeshLod {
1734    pub fn new(base_vertex_count: u32, chunk_size: f32) -> Self {
1735        let ratios: [f32; 6] = [0.0, 0.001, 0.05, 0.2, 0.6, 1.0];
1736        let sse_thresholds: [f32; 6] = [f32::MAX, 64.0, 16.0, 4.0, 1.0, 0.0];
1737        let mut lod_vertex_counts = [0u32; 6];
1738        let mut index_buffer_sizes = [0u32; 6];
1739        let mut memory_sizes = [0u64; 6];
1740        for i in 0..6 {
1741            lod_vertex_counts[i] = (base_vertex_count as f32 * ratios[i]) as u32;
1742            index_buffer_sizes[i] = lod_vertex_counts[i] * 3; // approximate triangle list
1743            // 12 bytes per vertex (position), 4 bytes per index
1744            memory_sizes[i] = lod_vertex_counts[i] as u64 * 12 + index_buffer_sizes[i] as u64 * 4;
1745        }
1746        Self {
1747            base_vertex_count,
1748            lod_vertex_counts,
1749            simplification_ratios: ratios,
1750            screen_space_error_thresholds: sse_thresholds,
1751            index_buffer_sizes,
1752            memory_sizes_bytes: memory_sizes,
1753            transition_distances: LOD_DISTANCES,
1754        }
1755    }
1756
1757    pub fn vertex_count_for_lod(&self, lod: &LodLevel) -> u32 {
1758        self.lod_vertex_counts[lod.index()]
1759    }
1760
1761    pub fn memory_for_lod(&self, lod: &LodLevel) -> u64 {
1762        self.memory_sizes_bytes[lod.index()]
1763    }
1764
1765    pub fn select_lod_for_screen_size(&self, screen_size_pixels: f32) -> LodLevel {
1766        for i in (0..6).rev() {
1767            if screen_size_pixels >= self.screen_space_error_thresholds[i] {
1768                return LodLevel::from_index(i);
1769            }
1770        }
1771        LodLevel::Unloaded
1772    }
1773
1774    pub fn select_lod_for_distance(&self, dist: f32) -> LodLevel {
1775        if dist < self.transition_distances[0]      { LodLevel::Ultra }
1776        else if dist < self.transition_distances[1] { LodLevel::High }
1777        else if dist < self.transition_distances[2] { LodLevel::Medium }
1778        else if dist < self.transition_distances[3] { LodLevel::Low }
1779        else if dist < self.transition_distances[4] { LodLevel::Impostor }
1780        else { LodLevel::Unloaded }
1781    }
1782
1783    pub fn blend_factor(&self, lod: &LodLevel, dist: f32) -> f32 {
1784        let idx = lod.index();
1785        if idx == 0 || idx >= 5 { return 1.0; }
1786        let near = self.transition_distances[idx - 1];
1787        let far  = self.transition_distances[idx];
1788        if far <= near { return 1.0; }
1789        ((dist - near) / (far - near)).clamp(0.0, 1.0)
1790    }
1791
1792    pub fn total_triangle_count(&self, lod: &LodLevel) -> u32 {
1793        self.index_buffer_sizes[lod.index()] / 3
1794    }
1795
1796    pub fn reduction_percentage(&self, lod: &LodLevel) -> f32 {
1797        (1.0 - self.simplification_ratios[lod.index()]) * 100.0
1798    }
1799}
1800
1801// ============================================================
1802// IMPOSTOR BILLBOARD
1803// ============================================================
1804
1805#[derive(Debug, Clone)]
1806pub struct ImpostorBillboard {
1807    pub atlas_uv_min: Vec2,
1808    pub atlas_uv_max: Vec2,
1809    pub world_position: Vec3,
1810    pub scale: Vec2,
1811    pub pivot_offset: Vec3,
1812    pub facing_angle_rad: f32,
1813    pub num_views: u32,
1814    pub current_view_index: u32,
1815    pub last_update_frame: u64,
1816    pub is_dirty: bool,
1817    pub depth_prepass_enabled: bool,
1818}
1819
1820impl ImpostorBillboard {
1821    pub fn new(world_position: Vec3, scale: Vec2, num_views: u32) -> Self {
1822        let tile_w = 1.0 / IMPOSTOR_ATLAS_COLS as f32;
1823        let tile_h = 1.0 / IMPOSTOR_ATLAS_ROWS as f32;
1824        Self {
1825            atlas_uv_min: Vec2::ZERO,
1826            atlas_uv_max: Vec2::new(tile_w, tile_h),
1827            world_position,
1828            scale,
1829            pivot_offset: Vec3::ZERO,
1830            facing_angle_rad: 0.0,
1831            num_views,
1832            current_view_index: 0,
1833            last_update_frame: 0,
1834            is_dirty: true,
1835            depth_prepass_enabled: false,
1836        }
1837    }
1838
1839    pub fn update_view_index(&mut self, camera_pos: Vec3) {
1840        let to_cam = (camera_pos - self.world_position).normalize_or_zero();
1841        let angle = to_cam.x.atan2(to_cam.z);
1842        let normalized = (angle + std::f32::consts::PI) / (2.0 * std::f32::consts::PI);
1843        self.current_view_index = (normalized * self.num_views as f32) as u32 % self.num_views;
1844        self.facing_angle_rad = angle;
1845        self.update_atlas_uvs();
1846    }
1847
1848    fn update_atlas_uvs(&mut self) {
1849        let cols = IMPOSTOR_ATLAS_COLS;
1850        let rows = IMPOSTOR_ATLAS_ROWS;
1851        let tile_w = 1.0 / cols as f32;
1852        let tile_h = 1.0 / rows as f32;
1853        let col = (self.current_view_index % cols) as f32;
1854        let row = (self.current_view_index / cols) as f32;
1855        self.atlas_uv_min = Vec2::new(col * tile_w, row * tile_h);
1856        self.atlas_uv_max = Vec2::new((col + 1.0) * tile_w, (row + 1.0) * tile_h);
1857    }
1858
1859    pub fn compute_billboard_matrix(&self, camera_pos: Vec3, camera_up: Vec3) -> Mat4 {
1860        let to_cam = (camera_pos - self.world_position).normalize_or_zero();
1861        let right = to_cam.cross(camera_up).normalize_or_zero();
1862        let up    = right.cross(to_cam).normalize_or_zero();
1863        let scaled_right = right * self.scale.x;
1864        let scaled_up    = up    * self.scale.y;
1865        let pos = self.world_position + self.pivot_offset;
1866        Mat4::from_cols(
1867            scaled_right.extend(0.0),
1868            scaled_up.extend(0.0),
1869            to_cam.extend(0.0),
1870            pos.extend(1.0),
1871        )
1872    }
1873
1874    pub fn screen_space_bounds(&self, camera: &StreamingCamera) -> (Vec2, Vec2) {
1875        let proj_pos = camera.proj_matrix * camera.view_matrix * self.world_position.extend(1.0);
1876        if proj_pos.w.abs() < 1e-8 {
1877            return (Vec2::ZERO, Vec2::ZERO);
1878        }
1879        let ndc = proj_pos.truncate() / proj_pos.w;
1880        let half_scale = self.scale * 0.5 / proj_pos.w;
1881        let center_2d = Vec2::new(ndc.x, ndc.y);
1882        (center_2d - half_scale, center_2d + half_scale)
1883    }
1884
1885    pub fn mark_dirty(&mut self) {
1886        self.is_dirty = true;
1887    }
1888
1889    pub fn clear_dirty(&mut self, frame: u64) {
1890        self.is_dirty = false;
1891        self.last_update_frame = frame;
1892    }
1893
1894    pub fn should_update(&self, camera_pos: Vec3, angle_threshold_deg: f32) -> bool {
1895        if self.is_dirty { return true; }
1896        let to_cam = (camera_pos - self.world_position).normalize_or_zero();
1897        let current_angle = to_cam.x.atan2(to_cam.z);
1898        let diff = (current_angle - self.facing_angle_rad).abs();
1899        let wrap = if diff > std::f32::consts::PI { 2.0 * std::f32::consts::PI - diff } else { diff };
1900        let step = 2.0 * std::f32::consts::PI / self.num_views as f32;
1901        wrap > step * 0.5 + angle_threshold_deg.to_radians()
1902    }
1903}
1904
1905// ============================================================
1906// TERRAIN HEIGHTMAP
1907// ============================================================
1908
1909#[derive(Debug, Clone)]
1910pub struct TerrainHeightmap {
1911    pub width: usize,
1912    pub height: usize,
1913    pub heights: Vec<f32>,
1914    pub cell_size: f32,
1915    pub origin: Vec2,
1916    pub min_height: f32,
1917    pub max_height: f32,
1918    pub scale_y: f32,
1919}
1920
1921impl TerrainHeightmap {
1922    pub fn new(width: usize, height: usize, cell_size: f32, origin: Vec2, scale_y: f32) -> Self {
1923        let size = width * height;
1924        Self {
1925            width,
1926            height,
1927            heights: vec![0.0; size],
1928            cell_size,
1929            origin,
1930            min_height: 0.0,
1931            max_height: 0.0,
1932            scale_y,
1933        }
1934    }
1935
1936    pub fn set_height(&mut self, x: usize, z: usize, h: f32) {
1937        if x < self.width && z < self.height {
1938            self.heights[z * self.width + x] = h;
1939            self.min_height = self.min_height.min(h);
1940            self.max_height = self.max_height.max(h);
1941        }
1942    }
1943
1944    pub fn get_height(&self, x: usize, z: usize) -> f32 {
1945        if x < self.width && z < self.height {
1946            self.heights[z * self.width + x]
1947        } else {
1948            0.0
1949        }
1950    }
1951
1952    pub fn sample_bilinear(&self, world_x: f32, world_z: f32) -> f32 {
1953        let local_x = (world_x - self.origin.x) / self.cell_size;
1954        let local_z = (world_z - self.origin.y) / self.cell_size;
1955
1956        let ix = local_x.floor() as isize;
1957        let iz = local_z.floor() as isize;
1958        let fx = local_x - ix as f32;
1959        let fz = local_z - iz as f32;
1960
1961        let h00 = self.get_clamped(ix,     iz    );
1962        let h10 = self.get_clamped(ix + 1, iz    );
1963        let h01 = self.get_clamped(ix,     iz + 1);
1964        let h11 = self.get_clamped(ix + 1, iz + 1);
1965
1966        let h0 = h00 * (1.0 - fx) + h10 * fx;
1967        let h1 = h01 * (1.0 - fx) + h11 * fx;
1968        (h0 * (1.0 - fz) + h1 * fz) * self.scale_y
1969    }
1970
1971    fn get_clamped(&self, x: isize, z: isize) -> f32 {
1972        let cx = x.clamp(0, self.width as isize - 1) as usize;
1973        let cz = z.clamp(0, self.height as isize - 1) as usize;
1974        self.heights[cz * self.width + cx]
1975    }
1976
1977    pub fn compute_normal(&self, x: usize, z: usize) -> Vec3 {
1978        let left  = self.get_height(x.saturating_sub(1), z);
1979        let right = if x + 1 < self.width { self.get_height(x + 1, z) } else { self.get_height(x, z) };
1980        let down  = self.get_height(x, z.saturating_sub(1));
1981        let up    = if z + 1 < self.height { self.get_height(x, z + 1) } else { self.get_height(x, z) };
1982
1983        let dx = (right - left) * self.scale_y / (2.0 * self.cell_size);
1984        let dz = (up   - down ) * self.scale_y / (2.0 * self.cell_size);
1985        Vec3::new(-dx, 1.0, -dz).normalize()
1986    }
1987
1988    pub fn compute_normal_bilinear(&self, world_x: f32, world_z: f32) -> Vec3 {
1989        let epsilon = self.cell_size * 0.5;
1990        let h_px = self.sample_bilinear(world_x + epsilon, world_z);
1991        let h_nx = self.sample_bilinear(world_x - epsilon, world_z);
1992        let h_pz = self.sample_bilinear(world_x, world_z + epsilon);
1993        let h_nz = self.sample_bilinear(world_x, world_z - epsilon);
1994
1995        let dx = (h_px - h_nx) / (2.0 * epsilon);
1996        let dz = (h_pz - h_nz) / (2.0 * epsilon);
1997        Vec3::new(-dx, 1.0, -dz).normalize()
1998    }
1999
2000    pub fn slope_at(&self, world_x: f32, world_z: f32) -> f32 {
2001        let normal = self.compute_normal_bilinear(world_x, world_z);
2002        normal.dot(Vec3::Y).acos().to_degrees()
2003    }
2004
2005    pub fn curvature_at(&self, x: usize, z: usize) -> f32 {
2006        if x == 0 || x >= self.width - 1 || z == 0 || z >= self.height - 1 {
2007            return 0.0;
2008        }
2009        let h   = self.get_height(x, z);
2010        let h_l = self.get_height(x - 1, z);
2011        let h_r = self.get_height(x + 1, z);
2012        let h_u = self.get_height(x, z + 1);
2013        let h_d = self.get_height(x, z - 1);
2014
2015        let d2x = (h_l - 2.0 * h + h_r) / (self.cell_size * self.cell_size);
2016        let d2z = (h_d - 2.0 * h + h_u) / (self.cell_size * self.cell_size);
2017        d2x + d2z
2018    }
2019
2020    pub fn bounds(&self) -> ChunkBounds {
2021        let world_width  = (self.width  - 1) as f32 * self.cell_size;
2022        let world_height = (self.height - 1) as f32 * self.cell_size;
2023        ChunkBounds {
2024            min: Vec3::new(self.origin.x, self.min_height * self.scale_y, self.origin.y),
2025            max: Vec3::new(self.origin.x + world_width, self.max_height * self.scale_y, self.origin.y + world_height),
2026        }
2027    }
2028
2029    pub fn generate_flat(width: usize, height: usize, cell_size: f32, origin: Vec2) -> Self {
2030        TerrainHeightmap::new(width, height, cell_size, origin, 1.0)
2031    }
2032
2033    pub fn generate_sinusoidal(width: usize, height: usize, cell_size: f32, origin: Vec2, amplitude: f32, frequency: f32) -> Self {
2034        let mut hm = TerrainHeightmap::new(width, height, cell_size, origin, 1.0);
2035        for z in 0..height {
2036            for x in 0..width {
2037                let wx = origin.x + x as f32 * cell_size;
2038                let wz = origin.y + z as f32 * cell_size;
2039                let h = amplitude * (wx * frequency).sin() * (wz * frequency).cos();
2040                hm.set_height(x, z, h);
2041            }
2042        }
2043        hm
2044    }
2045}
2046
2047// ============================================================
2048// TERRAIN PATCH
2049// ============================================================
2050
2051#[derive(Debug, Clone)]
2052pub struct TerrainPatch {
2053    pub coord: ChunkCoord,
2054    pub heightmap: TerrainHeightmap,
2055    pub lod_level: LodLevel,
2056    pub neighbor_lods: [Option<LodLevel>; 4], // +x, -x, +z, -z
2057    pub seam_data: [Vec<f32>; 4],
2058    pub needs_stitch: bool,
2059    pub error_metric: f32,
2060}
2061
2062impl TerrainPatch {
2063    pub fn new(coord: ChunkCoord, size: usize, cell_size: f32) -> Self {
2064        let origin = Vec2::new(
2065            coord.x as f32 * (size as f32 - 1.0) * cell_size,
2066            coord.z as f32 * (size as f32 - 1.0) * cell_size,
2067        );
2068        let heightmap = TerrainHeightmap::new(size, size, cell_size, origin, 1.0);
2069        let seam_data = [Vec::new(), Vec::new(), Vec::new(), Vec::new()];
2070        Self {
2071            coord,
2072            heightmap,
2073            lod_level: LodLevel::Unloaded,
2074            neighbor_lods: [None, None, None, None],
2075            seam_data,
2076            needs_stitch: false,
2077            error_metric: 0.0,
2078        }
2079    }
2080
2081    pub fn compute_seam_data(&mut self, side: usize) {
2082        let size = self.heightmap.width;
2083        let mut seam = Vec::with_capacity(size);
2084        match side {
2085            0 => { // +x edge
2086                for z in 0..self.heightmap.height {
2087                    seam.push(self.heightmap.get_height(size - 1, z));
2088                }
2089            }
2090            1 => { // -x edge
2091                for z in 0..self.heightmap.height {
2092                    seam.push(self.heightmap.get_height(0, z));
2093                }
2094            }
2095            2 => { // +z edge
2096                for x in 0..self.heightmap.width {
2097                    seam.push(self.heightmap.get_height(x, size - 1));
2098                }
2099            }
2100            3 => { // -z edge
2101                for x in 0..self.heightmap.width {
2102                    seam.push(self.heightmap.get_height(x, 0));
2103                }
2104            }
2105            _ => {}
2106        }
2107        self.seam_data[side] = seam;
2108    }
2109
2110    pub fn stitch_edge(&mut self, side: usize, neighbor_seam: &[f32], neighbor_lod: &LodLevel) {
2111        let my_lod_idx = self.lod_level.index();
2112        let neighbor_lod_idx = neighbor_lod.index();
2113
2114        if my_lod_idx <= neighbor_lod_idx {
2115            return; // no stitching needed if we're same or finer
2116        }
2117
2118        let ratio = (1 << (my_lod_idx - neighbor_lod_idx)) as usize;
2119        let my_size = self.heightmap.width;
2120
2121        match side {
2122            0 => { // +x edge
2123                for z in 0..my_size {
2124                    if z % ratio != 0 {
2125                        let z0 = (z / ratio) * ratio;
2126                        let z1 = (z0 + ratio).min(my_size - 1);
2127                        let t  = (z - z0) as f32 / ratio as f32;
2128                        let h0 = if z0 < neighbor_seam.len() { neighbor_seam[z0 / ratio] } else { 0.0 };
2129                        let h1 = if z1 / ratio < neighbor_seam.len() { neighbor_seam[z1 / ratio] } else { h0 };
2130                        let blended = h0 * (1.0 - t) + h1 * t;
2131                        self.heightmap.set_height(my_size - 1, z, blended);
2132                    }
2133                }
2134            }
2135            1 => { // -x edge
2136                for z in 0..my_size {
2137                    if z % ratio != 0 {
2138                        let z0 = (z / ratio) * ratio;
2139                        let z1 = (z0 + ratio).min(my_size - 1);
2140                        let t  = (z - z0) as f32 / ratio as f32;
2141                        let h0 = if z0 < neighbor_seam.len() { neighbor_seam[z0 / ratio] } else { 0.0 };
2142                        let h1 = if z1 / ratio < neighbor_seam.len() { neighbor_seam[z1 / ratio] } else { h0 };
2143                        let blended = h0 * (1.0 - t) + h1 * t;
2144                        self.heightmap.set_height(0, z, blended);
2145                    }
2146                }
2147            }
2148            2 => { // +z edge
2149                for x in 0..my_size {
2150                    if x % ratio != 0 {
2151                        let x0 = (x / ratio) * ratio;
2152                        let x1 = (x0 + ratio).min(my_size - 1);
2153                        let t  = (x - x0) as f32 / ratio as f32;
2154                        let h0 = if x0 < neighbor_seam.len() { neighbor_seam[x0 / ratio] } else { 0.0 };
2155                        let h1 = if x1 / ratio < neighbor_seam.len() { neighbor_seam[x1 / ratio] } else { h0 };
2156                        let blended = h0 * (1.0 - t) + h1 * t;
2157                        self.heightmap.set_height(x, my_size - 1, blended);
2158                    }
2159                }
2160            }
2161            3 => { // -z edge
2162                for x in 0..my_size {
2163                    if x % ratio != 0 {
2164                        let x0 = (x / ratio) * ratio;
2165                        let x1 = (x0 + ratio).min(my_size - 1);
2166                        let t  = (x - x0) as f32 / ratio as f32;
2167                        let h0 = if x0 < neighbor_seam.len() { neighbor_seam[x0 / ratio] } else { 0.0 };
2168                        let h1 = if x1 / ratio < neighbor_seam.len() { neighbor_seam[x1 / ratio] } else { h0 };
2169                        let blended = h0 * (1.0 - t) + h1 * t;
2170                        self.heightmap.set_height(x, 0, blended);
2171                    }
2172                }
2173            }
2174            _ => {}
2175        }
2176        self.needs_stitch = false;
2177    }
2178
2179    pub fn compute_error_metric(&mut self) -> f32 {
2180        let size = self.heightmap.width;
2181        if size < 3 { self.error_metric = 0.0; return 0.0; }
2182        let mut max_error = 0.0f32;
2183        for z in 1..size - 1 {
2184            for x in 1..size - 1 {
2185                let h = self.heightmap.get_height(x, z);
2186                let avg = (
2187                    self.heightmap.get_height(x - 1, z) +
2188                    self.heightmap.get_height(x + 1, z) +
2189                    self.heightmap.get_height(x, z - 1) +
2190                    self.heightmap.get_height(x, z + 1)
2191                ) * 0.25;
2192                max_error = max_error.max((h - avg).abs());
2193            }
2194        }
2195        self.error_metric = max_error;
2196        max_error
2197    }
2198
2199    pub fn lod_for_screen_size(&self, screen_pixels: f32) -> LodLevel {
2200        if screen_pixels > 512.0       { LodLevel::Ultra   }
2201        else if screen_pixels > 256.0  { LodLevel::High    }
2202        else if screen_pixels > 128.0  { LodLevel::Medium  }
2203        else if screen_pixels > 64.0   { LodLevel::Low     }
2204        else if screen_pixels > 16.0   { LodLevel::Impostor}
2205        else                           { LodLevel::Unloaded}
2206    }
2207
2208    pub fn update_neighbor_lods(&mut self, neighbors: [Option<LodLevel>; 4]) {
2209        let changed = self.neighbor_lods != neighbors;
2210        self.neighbor_lods = neighbors;
2211        if changed { self.needs_stitch = true; }
2212    }
2213}
2214
2215// ============================================================
2216// VIRTUAL TEXTURE
2217// ============================================================
2218
2219#[derive(Debug, Clone)]
2220pub struct VirtualTextureTile {
2221    pub mip: u32,
2222    pub tile_x: u32,
2223    pub tile_y: u32,
2224    pub atlas_slot: u32,
2225    pub last_requested_frame: u64,
2226    pub is_resident: bool,
2227    pub priority: f32,
2228}
2229
2230impl VirtualTextureTile {
2231    pub fn new(mip: u32, tile_x: u32, tile_y: u32) -> Self {
2232        Self {
2233            mip,
2234            tile_x,
2235            tile_y,
2236            atlas_slot: u32::MAX,
2237            last_requested_frame: 0,
2238            is_resident: false,
2239            priority: 0.0,
2240        }
2241    }
2242
2243    pub fn tile_id(&self) -> u64 {
2244        (self.mip as u64) | ((self.tile_x as u64) << 8) | ((self.tile_y as u64) << 24)
2245    }
2246}
2247
2248#[derive(Debug, Clone)]
2249pub struct VirtualTexture {
2250    pub page_table: Vec<Vec<u32>>,       // [mip][tile_index] -> atlas slot
2251    pub tile_cache: HashMap<u64, VirtualTextureTile>,
2252    pub atlas_size: u32,
2253    pub tile_size: u32,
2254    pub num_mips: u32,
2255    pub max_resident_tiles: usize,
2256    pub resident_count: usize,
2257    pub free_slots: VecDeque<u32>,
2258    pub feedback_buffer: Vec<u32>,
2259    pub current_frame: u64,
2260}
2261
2262impl VirtualTexture {
2263    pub fn new(atlas_size: u32, tile_size: u32, num_mips: u32) -> Self {
2264        let tiles_per_row = atlas_size / tile_size;
2265        let max_tiles = (tiles_per_row * tiles_per_row) as usize;
2266        let mut free_slots = VecDeque::with_capacity(max_tiles);
2267        for i in 0..max_tiles as u32 {
2268            free_slots.push_back(i);
2269        }
2270        let page_table: Vec<Vec<u32>> = (0..num_mips)
2271            .map(|mip| {
2272                let tiles_at_mip = (tiles_per_row >> mip).max(1);
2273                vec![u32::MAX; (tiles_at_mip * tiles_at_mip) as usize]
2274            })
2275            .collect();
2276
2277        Self {
2278            page_table,
2279            tile_cache: HashMap::new(),
2280            atlas_size,
2281            tile_size,
2282            num_mips,
2283            max_resident_tiles: max_tiles,
2284            resident_count: 0,
2285            free_slots,
2286            feedback_buffer: vec![0u32; FEEDBACK_BUFFER_MIPS],
2287            current_frame: 0,
2288        }
2289    }
2290
2291    pub fn request_tile(&mut self, mip: u32, tile_x: u32, tile_y: u32, frame: u64) {
2292        let tile_id = (mip as u64) | ((tile_x as u64) << 8) | ((tile_y as u64) << 24);
2293        if let Some(tile) = self.tile_cache.get_mut(&tile_id) {
2294            tile.last_requested_frame = frame;
2295            return;
2296        }
2297        let mut tile = VirtualTextureTile::new(mip, tile_x, tile_y);
2298        tile.last_requested_frame = frame;
2299        self.tile_cache.insert(tile_id, tile);
2300    }
2301
2302    pub fn load_tile(&mut self, mip: u32, tile_x: u32, tile_y: u32) -> Option<u32> {
2303        let tile_id = (mip as u64) | ((tile_x as u64) << 8) | ((tile_y as u64) << 24);
2304        let slot = self.free_slots.pop_front()?;
2305
2306        if let Some(tile) = self.tile_cache.get_mut(&tile_id) {
2307            tile.atlas_slot = slot;
2308            tile.is_resident = true;
2309        }
2310
2311        let tiles_at_mip = ((self.atlas_size / self.tile_size) >> mip).max(1) as usize;
2312        let index = tile_y as usize * tiles_at_mip + tile_x as usize;
2313        if (mip as usize) < self.page_table.len() && index < self.page_table[mip as usize].len() {
2314            self.page_table[mip as usize][index] = slot;
2315        }
2316
2317        self.resident_count += 1;
2318        Some(slot)
2319    }
2320
2321    pub fn evict_tile(&mut self, mip: u32, tile_x: u32, tile_y: u32) {
2322        let tile_id = (mip as u64) | ((tile_x as u64) << 8) | ((tile_y as u64) << 24);
2323        if let Some(tile) = self.tile_cache.get_mut(&tile_id) {
2324            if tile.is_resident {
2325                let slot = tile.atlas_slot;
2326                tile.is_resident = false;
2327                tile.atlas_slot = u32::MAX;
2328                self.free_slots.push_back(slot);
2329                self.resident_count -= 1;
2330
2331                let tiles_at_mip = ((self.atlas_size / self.tile_size) >> mip).max(1) as usize;
2332                let index = tile_y as usize * tiles_at_mip + tile_x as usize;
2333                if (mip as usize) < self.page_table.len() && index < self.page_table[mip as usize].len() {
2334                    self.page_table[mip as usize][index] = u32::MAX;
2335                }
2336            }
2337        }
2338    }
2339
2340    pub fn analyze_feedback_buffer(&self) -> Vec<(u32, u32, u32, f32)> {
2341        let mut requests = Vec::new();
2342        for (mip_idx, &count) in self.feedback_buffer.iter().enumerate() {
2343            if count > 0 {
2344                let mip = mip_idx as u32;
2345                let tiles = ((self.atlas_size / self.tile_size) >> mip).max(1);
2346                let priority = count as f32 / (tiles * tiles) as f32;
2347                // Generate tile requests for all tiles in this mip
2348                for y in 0..tiles {
2349                    for x in 0..tiles {
2350                        requests.push((mip, x, y, priority));
2351                    }
2352                }
2353            }
2354        }
2355        requests.sort_by(|a, b| b.3.partial_cmp(&a.3).unwrap_or(std::cmp::Ordering::Equal));
2356        requests
2357    }
2358
2359    pub fn record_feedback(&mut self, mip: u32) {
2360        let idx = (mip as usize).min(self.feedback_buffer.len() - 1);
2361        self.feedback_buffer[idx] += 1;
2362    }
2363
2364    pub fn clear_feedback(&mut self) {
2365        for v in &mut self.feedback_buffer { *v = 0; }
2366    }
2367
2368    pub fn tile_atlas_uv(&self, slot: u32) -> (Vec2, Vec2) {
2369        let tiles_per_row = self.atlas_size / self.tile_size;
2370        let col = slot % tiles_per_row;
2371        let row = slot / tiles_per_row;
2372        let uv_tile_size = self.tile_size as f32 / self.atlas_size as f32;
2373        let uv_min = Vec2::new(col as f32 * uv_tile_size, row as f32 * uv_tile_size);
2374        let uv_max = uv_min + Vec2::splat(uv_tile_size);
2375        (uv_min, uv_max)
2376    }
2377
2378    pub fn evict_lru_tiles(&mut self, target_free: usize) {
2379        if self.free_slots.len() >= target_free { return; }
2380        let mut by_age: Vec<_> = self.tile_cache.values()
2381            .filter(|t| t.is_resident)
2382            .map(|t| (t.tile_id(), t.last_requested_frame, t.mip, t.tile_x, t.tile_y))
2383            .collect();
2384        by_age.sort_by_key(|&(_, frame, _, _, _)| frame);
2385
2386        let to_evict = (target_free - self.free_slots.len()).min(by_age.len());
2387        for i in 0..to_evict {
2388            let (_, _, mip, tx, ty) = by_age[i];
2389            self.evict_tile(mip, tx, ty);
2390        }
2391    }
2392}
2393
2394// ============================================================
2395// STREAMING STATS
2396// ============================================================
2397
2398#[derive(Debug, Clone, Default)]
2399pub struct StreamingStats {
2400    pub frame_number: u64,
2401    pub chunks_loaded_this_frame: u32,
2402    pub chunks_unloaded_this_frame: u32,
2403    pub chunks_lod_switched_this_frame: u32,
2404    pub total_chunks_loaded: u32,
2405    pub total_chunks_unloaded: u32,
2406    pub total_draw_calls_saved: u64,
2407    pub memory_used_bytes: u64,
2408    pub memory_budget_bytes: u64,
2409    pub chunks_in_frustum: u32,
2410    pub chunks_frustum_culled: u32,
2411    pub chunks_distance_culled: u32,
2412    pub active_chunks: u32,
2413    pub queued_loads: u32,
2414    pub active_loads: u32,
2415    pub impostor_draw_calls: u32,
2416    pub lod_low_draw_calls: u32,
2417    pub lod_medium_draw_calls: u32,
2418    pub lod_high_draw_calls: u32,
2419    pub lod_ultra_draw_calls: u32,
2420    pub terrain_stitch_ops: u32,
2421    pub virtual_texture_uploads: u32,
2422    pub hlod_merges: u32,
2423    pub frame_load_time_us: u64,
2424    pub frame_cull_time_us: u64,
2425    pub frame_lod_time_us: u64,
2426    pub peak_memory_bytes: u64,
2427    pub avg_chunk_load_time_us: f64,
2428    pub total_visible_objects: u32,
2429    pub total_culled_objects: u32,
2430}
2431
2432impl StreamingStats {
2433    pub fn new() -> Self {
2434        Self::default()
2435    }
2436
2437    pub fn reset_frame_counters(&mut self) {
2438        self.chunks_loaded_this_frame = 0;
2439        self.chunks_unloaded_this_frame = 0;
2440        self.chunks_lod_switched_this_frame = 0;
2441        self.impostor_draw_calls = 0;
2442        self.lod_low_draw_calls = 0;
2443        self.lod_medium_draw_calls = 0;
2444        self.lod_high_draw_calls = 0;
2445        self.lod_ultra_draw_calls = 0;
2446        self.terrain_stitch_ops = 0;
2447        self.virtual_texture_uploads = 0;
2448        self.frame_load_time_us = 0;
2449        self.frame_cull_time_us = 0;
2450        self.frame_lod_time_us = 0;
2451        self.chunks_in_frustum = 0;
2452        self.chunks_frustum_culled = 0;
2453        self.chunks_distance_culled = 0;
2454    }
2455
2456    pub fn memory_usage_ratio(&self) -> f32 {
2457        if self.memory_budget_bytes == 0 { return 0.0; }
2458        self.memory_used_bytes as f32 / self.memory_budget_bytes as f32
2459    }
2460
2461    pub fn draw_calls_saved_ratio(&self) -> f32 {
2462        let total = self.impostor_draw_calls + self.lod_low_draw_calls
2463            + self.lod_medium_draw_calls + self.lod_high_draw_calls + self.lod_ultra_draw_calls;
2464        if total == 0 { return 0.0; }
2465        self.total_draw_calls_saved as f32 / total as f32
2466    }
2467
2468    pub fn advance_frame(&mut self) {
2469        self.frame_number += 1;
2470        self.total_chunks_loaded += self.chunks_loaded_this_frame;
2471        self.total_chunks_unloaded += self.chunks_unloaded_this_frame;
2472        if self.memory_used_bytes > self.peak_memory_bytes {
2473            self.peak_memory_bytes = self.memory_used_bytes;
2474        }
2475        if self.chunks_loaded_this_frame > 0 {
2476            let load_time = self.frame_load_time_us as f64;
2477            let count = self.chunks_loaded_this_frame as f64;
2478            let alpha = 0.1;
2479            self.avg_chunk_load_time_us = self.avg_chunk_load_time_us * (1.0 - alpha)
2480                + (load_time / count) * alpha;
2481        }
2482        self.reset_frame_counters();
2483    }
2484
2485    pub fn record_lod_draw_call(&mut self, lod: &LodLevel) {
2486        match lod {
2487            LodLevel::Impostor => self.impostor_draw_calls += 1,
2488            LodLevel::Low      => self.lod_low_draw_calls += 1,
2489            LodLevel::Medium   => self.lod_medium_draw_calls += 1,
2490            LodLevel::High     => self.lod_high_draw_calls += 1,
2491            LodLevel::Ultra    => self.lod_ultra_draw_calls += 1,
2492            LodLevel::Unloaded => {}
2493        }
2494    }
2495}
2496
2497// ============================================================
2498// WORLD PARTITION
2499// ============================================================
2500
2501#[derive(Debug, Clone)]
2502pub struct WorldPartitionCell {
2503    pub coord: ChunkCoord,
2504    pub bounds: ChunkBounds,
2505    pub actors: Vec<u32>,
2506    pub is_loaded: bool,
2507    pub streaming_source_count: u32,
2508}
2509
2510impl WorldPartitionCell {
2511    pub fn new(coord: ChunkCoord, cell_size: f32) -> Self {
2512        let bounds = ChunkBounds::from_chunk_coord(&coord, cell_size);
2513        Self {
2514            coord,
2515            bounds,
2516            actors: Vec::new(),
2517            is_loaded: false,
2518            streaming_source_count: 0,
2519        }
2520    }
2521
2522    pub fn add_actor(&mut self, actor_id: u32) {
2523        if !self.actors.contains(&actor_id) {
2524            self.actors.push(actor_id);
2525        }
2526    }
2527
2528    pub fn remove_actor(&mut self, actor_id: u32) {
2529        self.actors.retain(|&id| id != actor_id);
2530    }
2531}
2532
2533#[derive(Debug, Clone)]
2534pub struct WorldPartition {
2535    pub cells: HashMap<ChunkCoord, WorldPartitionCell>,
2536    pub cell_size: f32,
2537    pub actor_to_cell: HashMap<u32, ChunkCoord>,
2538    pub loaded_cells: HashSet<ChunkCoord>,
2539    pub streaming_sources: Vec<Vec3>,
2540    pub bounds: ChunkBounds,
2541}
2542
2543impl WorldPartition {
2544    pub fn new(cell_size: f32) -> Self {
2545        Self {
2546            cells: HashMap::new(),
2547            cell_size,
2548            actor_to_cell: HashMap::new(),
2549            loaded_cells: HashSet::new(),
2550            streaming_sources: Vec::new(),
2551            bounds: ChunkBounds::new(Vec3::ZERO, Vec3::ZERO),
2552        }
2553    }
2554
2555    pub fn register_actor(&mut self, actor_id: u32, world_pos: Vec3) {
2556        let coord = ChunkCoord::from_world_pos(world_pos, self.cell_size);
2557        let cell = self.cells.entry(coord.clone()).or_insert_with(|| {
2558            WorldPartitionCell::new(coord.clone(), self.cell_size)
2559        });
2560        cell.add_actor(actor_id);
2561        self.actor_to_cell.insert(actor_id, coord);
2562        self.recompute_bounds();
2563    }
2564
2565    pub fn unregister_actor(&mut self, actor_id: u32) {
2566        if let Some(coord) = self.actor_to_cell.remove(&actor_id) {
2567            if let Some(cell) = self.cells.get_mut(&coord) {
2568                cell.remove_actor(actor_id);
2569            }
2570        }
2571    }
2572
2573    pub fn move_actor(&mut self, actor_id: u32, new_pos: Vec3) {
2574        let new_coord = ChunkCoord::from_world_pos(new_pos, self.cell_size);
2575        if let Some(old_coord) = self.actor_to_cell.get(&actor_id).cloned() {
2576            if old_coord == new_coord { return; }
2577            if let Some(old_cell) = self.cells.get_mut(&old_coord) {
2578                old_cell.remove_actor(actor_id);
2579            }
2580        }
2581        let cell = self.cells.entry(new_coord.clone()).or_insert_with(|| {
2582            WorldPartitionCell::new(new_coord.clone(), self.cell_size)
2583        });
2584        cell.add_actor(actor_id);
2585        self.actor_to_cell.insert(actor_id, new_coord);
2586    }
2587
2588    pub fn add_streaming_source(&mut self, pos: Vec3) {
2589        self.streaming_sources.push(pos);
2590    }
2591
2592    pub fn clear_streaming_sources(&mut self) {
2593        self.streaming_sources.clear();
2594    }
2595
2596    pub fn compute_cells_to_load(&self, load_radius: f32) -> HashSet<ChunkCoord> {
2597        let mut to_load = HashSet::new();
2598        let radius_cells = (load_radius / self.cell_size).ceil() as i32;
2599        for &source in &self.streaming_sources {
2600            let center = ChunkCoord::from_world_pos(source, self.cell_size);
2601            let candidates = ChunkCoord::chunks_in_radius(&center, radius_cells);
2602            for coord in candidates {
2603                if let Some(cell) = self.cells.get(&coord) {
2604                    let dist = cell.bounds.distance_to_point(source);
2605                    if dist <= load_radius {
2606                        to_load.insert(coord);
2607                    }
2608                }
2609            }
2610        }
2611        to_load
2612    }
2613
2614    pub fn get_actors_in_bounds(&self, bounds: &ChunkBounds) -> Vec<u32> {
2615        let mut result = Vec::new();
2616        for (_, cell) in &self.cells {
2617            if cell.bounds.intersects(bounds) {
2618                result.extend_from_slice(&cell.actors);
2619            }
2620        }
2621        result
2622    }
2623
2624    pub fn get_actors_in_radius(&self, center: Vec3, radius: f32) -> Vec<u32> {
2625        let query_bounds = ChunkBounds::from_center_size(center, Vec3::splat(radius));
2626        let mut result = Vec::new();
2627        for (_, cell) in &self.cells {
2628            if cell.bounds.intersects(&query_bounds) {
2629                for &actor in &cell.actors {
2630                    result.push(actor);
2631                }
2632            }
2633        }
2634        result
2635    }
2636
2637    fn recompute_bounds(&mut self) {
2638        let mut min = Vec3::splat(f32::MAX);
2639        let mut max = Vec3::splat(f32::MIN);
2640        for (_, cell) in &self.cells {
2641            if !cell.actors.is_empty() {
2642                min = min.min(cell.bounds.min);
2643                max = max.max(cell.bounds.max);
2644            }
2645        }
2646        if min.x <= max.x {
2647            self.bounds = ChunkBounds { min, max };
2648        }
2649    }
2650
2651    pub fn cell_count(&self) -> usize {
2652        self.cells.len()
2653    }
2654
2655    pub fn actor_count(&self) -> usize {
2656        self.actor_to_cell.len()
2657    }
2658}
2659
2660// ============================================================
2661// ACTOR STREAMING PROXY
2662// ============================================================
2663
2664#[derive(Debug, Clone)]
2665pub struct ActorStreamingProxy {
2666    pub actor_id: u32,
2667    pub world_position: Vec3,
2668    pub world_rotation: Quat,
2669    pub world_scale: Vec3,
2670    pub bounds: ChunkBounds,
2671    pub streaming_distance: f32,
2672    pub lod_level: LodLevel,
2673    pub is_loaded: bool,
2674    pub data_layer_mask: u32,
2675    pub hlod_cluster_id: Option<u32>,
2676    pub last_frame_visible: u64,
2677    pub importance: f32,
2678}
2679
2680impl ActorStreamingProxy {
2681    pub fn new(actor_id: u32, position: Vec3, bounds: ChunkBounds) -> Self {
2682        Self {
2683            actor_id,
2684            world_position: position,
2685            world_rotation: Quat::IDENTITY,
2686            world_scale: Vec3::ONE,
2687            bounds,
2688            streaming_distance: 1000.0,
2689            lod_level: LodLevel::Unloaded,
2690            is_loaded: false,
2691            data_layer_mask: 0xFFFF_FFFF,
2692            hlod_cluster_id: None,
2693            last_frame_visible: 0,
2694            importance: 1.0,
2695        }
2696    }
2697
2698    pub fn world_transform(&self) -> Mat4 {
2699        Mat4::from_scale_rotation_translation(
2700            self.world_scale,
2701            self.world_rotation,
2702            self.world_position,
2703        )
2704    }
2705
2706    pub fn distance_to_viewer(&self, viewer_pos: Vec3) -> f32 {
2707        (self.world_position - viewer_pos).length()
2708    }
2709
2710    pub fn should_load(&self, viewer_pos: Vec3) -> bool {
2711        let dist = self.distance_to_viewer(viewer_pos);
2712        dist <= self.streaming_distance
2713    }
2714
2715    pub fn desired_lod(&self, viewer_pos: Vec3, config: &StreamingConfig) -> LodLevel {
2716        let dist = self.distance_to_viewer(viewer_pos);
2717        config.lod_for_distance(dist * self.importance.recip())
2718    }
2719
2720    pub fn screen_size_at_distance(&self, viewer_pos: Vec3, camera: &StreamingCamera, screen_height: f32) -> f32 {
2721        let radius = self.bounds.half_size().length();
2722        camera.project_sphere_to_screen(self.world_position, radius, screen_height)
2723    }
2724
2725    pub fn is_in_data_layer(&self, layer_mask: u32) -> bool {
2726        (self.data_layer_mask & layer_mask) != 0
2727    }
2728
2729    pub fn update_transform(&mut self, pos: Vec3, rot: Quat, scale: Vec3) {
2730        self.world_position = pos;
2731        self.world_rotation = rot;
2732        self.world_scale = scale;
2733        let extent = self.bounds.size() * scale * 0.5;
2734        self.bounds = ChunkBounds::from_center_size(pos, extent);
2735    }
2736}
2737
2738// ============================================================
2739// DATA LAYER SYSTEM
2740// ============================================================
2741
2742#[derive(Debug, Clone)]
2743pub struct DataLayer {
2744    pub id: u32,
2745    pub name: String,
2746    pub mode: DataLayerMode,
2747    pub parent_id: Option<u32>,
2748    pub child_ids: Vec<u32>,
2749    pub is_visible: bool,
2750    pub is_loaded: bool,
2751    pub actor_ids: Vec<u32>,
2752    pub spatial_bounds: Option<ChunkBounds>,
2753    pub load_state: ChunkLoadState,
2754    pub debug_color: Vec3,
2755}
2756
2757impl DataLayer {
2758    pub fn new(id: u32, name: String) -> Self {
2759        Self {
2760            id,
2761            name,
2762            mode: DataLayerMode::Inherited,
2763            parent_id: None,
2764            child_ids: Vec::new(),
2765            is_visible: true,
2766            is_loaded: false,
2767            actor_ids: Vec::new(),
2768            spatial_bounds: None,
2769            load_state: ChunkLoadState::Unloaded,
2770            debug_color: Vec3::ONE,
2771        }
2772    }
2773
2774    pub fn add_actor(&mut self, actor_id: u32) {
2775        if !self.actor_ids.contains(&actor_id) {
2776            self.actor_ids.push(actor_id);
2777        }
2778    }
2779
2780    pub fn remove_actor(&mut self, actor_id: u32) {
2781        self.actor_ids.retain(|&id| id != actor_id);
2782    }
2783
2784    pub fn is_active(&self) -> bool {
2785        self.is_visible && self.is_loaded
2786    }
2787
2788    pub fn effective_mode(&self) -> DataLayerMode {
2789        match &self.mode {
2790            DataLayerMode::Inherited => DataLayerMode::Included,
2791            other => other.clone(),
2792        }
2793    }
2794}
2795
2796#[derive(Debug, Clone)]
2797pub struct DataLayerSystem {
2798    pub layers: HashMap<u32, DataLayer>,
2799    pub next_id: u32,
2800    pub active_layers: HashSet<u32>,
2801    pub actor_layer_map: HashMap<u32, Vec<u32>>,
2802}
2803
2804impl DataLayerSystem {
2805    pub fn new() -> Self {
2806        Self {
2807            layers: HashMap::new(),
2808            next_id: 1,
2809            active_layers: HashSet::new(),
2810            actor_layer_map: HashMap::new(),
2811        }
2812    }
2813
2814    pub fn create_layer(&mut self, name: String) -> u32 {
2815        let id = self.next_id;
2816        self.next_id += 1;
2817        self.layers.insert(id, DataLayer::new(id, name));
2818        id
2819    }
2820
2821    pub fn delete_layer(&mut self, id: u32) {
2822        if let Some(layer) = self.layers.remove(&id) {
2823            for actor_id in &layer.actor_ids {
2824                if let Some(layers) = self.actor_layer_map.get_mut(actor_id) {
2825                    layers.retain(|&lid| lid != id);
2826                }
2827            }
2828        }
2829        self.active_layers.remove(&id);
2830    }
2831
2832    pub fn add_actor_to_layer(&mut self, layer_id: u32, actor_id: u32) {
2833        if let Some(layer) = self.layers.get_mut(&layer_id) {
2834            layer.add_actor(actor_id);
2835        }
2836        self.actor_layer_map.entry(actor_id).or_insert_with(Vec::new).push(layer_id);
2837    }
2838
2839    pub fn activate_layer(&mut self, id: u32) {
2840        self.active_layers.insert(id);
2841        if let Some(layer) = self.layers.get_mut(&id) {
2842            layer.is_loaded = true;
2843            layer.is_visible = true;
2844        }
2845    }
2846
2847    pub fn deactivate_layer(&mut self, id: u32) {
2848        self.active_layers.remove(&id);
2849        if let Some(layer) = self.layers.get_mut(&id) {
2850            layer.is_loaded = false;
2851            layer.is_visible = false;
2852        }
2853    }
2854
2855    pub fn is_actor_visible(&self, actor_id: u32) -> bool {
2856        match self.actor_layer_map.get(&actor_id) {
2857            None => true,
2858            Some(layer_ids) => {
2859                layer_ids.iter().all(|lid| {
2860                    self.layers.get(lid).map_or(true, |l| {
2861                        match l.effective_mode() {
2862                            DataLayerMode::Included  => self.active_layers.contains(lid),
2863                            DataLayerMode::Excluded  => !self.active_layers.contains(lid),
2864                            DataLayerMode::Inherited => true,
2865                        }
2866                    })
2867                })
2868            }
2869        }
2870    }
2871
2872    pub fn actors_in_active_layers(&self) -> Vec<u32> {
2873        let mut actors = Vec::new();
2874        for lid in &self.active_layers {
2875            if let Some(layer) = self.layers.get(lid) {
2876                actors.extend_from_slice(&layer.actor_ids);
2877            }
2878        }
2879        actors.sort_unstable();
2880        actors.dedup();
2881        actors
2882    }
2883
2884    pub fn compute_layer_bounds(&mut self, layer_id: u32, actor_positions: &HashMap<u32, Vec3>) {
2885        if let Some(layer) = self.layers.get_mut(&layer_id) {
2886            let mut min = Vec3::splat(f32::MAX);
2887            let mut max = Vec3::splat(f32::MIN);
2888            for &actor_id in &layer.actor_ids {
2889                if let Some(&pos) = actor_positions.get(&actor_id) {
2890                    min = min.min(pos);
2891                    max = max.max(pos);
2892                }
2893            }
2894            if min.x <= max.x {
2895                layer.spatial_bounds = Some(ChunkBounds {
2896                    min: min - Vec3::splat(1.0),
2897                    max: max + Vec3::splat(1.0),
2898                });
2899            }
2900        }
2901    }
2902
2903    pub fn layer_count(&self) -> usize {
2904        self.layers.len()
2905    }
2906}
2907
2908// ============================================================
2909// HLOD CLUSTER
2910// ============================================================
2911
2912#[derive(Debug, Clone)]
2913pub struct HlodCluster {
2914    pub cluster_id: u32,
2915    pub actor_ids: Vec<u32>,
2916    pub merged_bounds: ChunkBounds,
2917    pub center: Vec3,
2918    pub radius: f32,
2919    pub lod_level: LodLevel,
2920    pub simplified_vertex_count: u32,
2921    pub simplified_triangle_count: u32,
2922    pub memory_bytes: u64,
2923    pub is_built: bool,
2924    pub sub_clusters: Vec<u32>,
2925    pub parent_cluster_id: Option<u32>,
2926    pub depth: u32,
2927    pub importance: f32,
2928}
2929
2930impl HlodCluster {
2931    pub fn new(cluster_id: u32, depth: u32) -> Self {
2932        Self {
2933            cluster_id,
2934            actor_ids: Vec::new(),
2935            merged_bounds: ChunkBounds::new(Vec3::ZERO, Vec3::ZERO),
2936            center: Vec3::ZERO,
2937            radius: 0.0,
2938            lod_level: LodLevel::Low,
2939            simplified_vertex_count: 0,
2940            simplified_triangle_count: 0,
2941            memory_bytes: 0,
2942            is_built: false,
2943            sub_clusters: Vec::new(),
2944            parent_cluster_id: None,
2945            depth,
2946            importance: 1.0,
2947        }
2948    }
2949
2950    pub fn add_actor(&mut self, actor_id: u32, bounds: &ChunkBounds) {
2951        self.actor_ids.push(actor_id);
2952        if self.actor_ids.len() == 1 {
2953            self.merged_bounds = bounds.clone();
2954        } else {
2955            self.merged_bounds = self.merged_bounds.merge(bounds);
2956        }
2957        self.center = self.merged_bounds.center();
2958        let half = self.merged_bounds.half_size();
2959        self.radius = half.length();
2960    }
2961
2962    pub fn compute_simplified_geometry(&mut self) {
2963        // Estimate: each actor contributes N triangles, simplified to 10%
2964        let tris_per_actor = 1000u32;
2965        let total_tris = tris_per_actor * self.actor_ids.len() as u32;
2966        let ratio = match self.depth {
2967            0 => 0.1,
2968            1 => 0.2,
2969            2 => 0.4,
2970            _ => 0.5,
2971        };
2972        self.simplified_triangle_count = (total_tris as f32 * ratio) as u32;
2973        self.simplified_vertex_count   = (self.simplified_triangle_count as f32 * 0.6) as u32;
2974        self.memory_bytes = self.simplified_vertex_count as u64 * 32 // 32 bytes per vertex
2975                          + self.simplified_triangle_count as u64 * 12; // 12 bytes per triangle
2976    }
2977
2978    pub fn compute_screen_size(&self, camera_pos: Vec3, screen_height: f32, fov_rad: f32) -> f32 {
2979        let dist = (self.center - camera_pos).length();
2980        if dist < 1e-6 { return screen_height; }
2981        let angular = self.radius / dist;
2982        let half_fov_tan = (fov_rad * 0.5).tan();
2983        (angular / half_fov_tan) * screen_height
2984    }
2985
2986    pub fn should_use_hlod(&self, camera_pos: Vec3, screen_height: f32, fov_rad: f32, threshold: f32) -> bool {
2987        self.compute_screen_size(camera_pos, screen_height, fov_rad) < threshold
2988    }
2989
2990    pub fn overlap_with(&self, other: &HlodCluster) -> bool {
2991        self.merged_bounds.intersects(&other.merged_bounds)
2992    }
2993
2994    pub fn merge_cluster(&mut self, other: &HlodCluster) {
2995        self.actor_ids.extend_from_slice(&other.actor_ids);
2996        self.merged_bounds = self.merged_bounds.merge(&other.merged_bounds);
2997        self.center = self.merged_bounds.center();
2998        let half = self.merged_bounds.half_size();
2999        self.radius = half.length();
3000        self.simplified_vertex_count   += other.simplified_vertex_count;
3001        self.simplified_triangle_count += other.simplified_triangle_count;
3002        self.memory_bytes += other.memory_bytes;
3003    }
3004}
3005
3006// ============================================================
3007// HLOD BUILDER
3008// ============================================================
3009
3010#[derive(Debug, Clone)]
3011pub struct HlodBuilder {
3012    pub cluster_radius: f32,
3013    pub max_actors_per_cluster: usize,
3014    pub max_depth: u32,
3015    pub simplification_ratio: f32,
3016    pub next_cluster_id: u32,
3017}
3018
3019impl HlodBuilder {
3020    pub fn new(cluster_radius: f32, max_actors: usize, max_depth: u32) -> Self {
3021        Self {
3022            cluster_radius,
3023            max_actors_per_cluster: max_actors,
3024            max_depth,
3025            simplification_ratio: 0.1,
3026            next_cluster_id: 1,
3027        }
3028    }
3029
3030    pub fn build_clusters(&mut self, actors: &[(u32, Vec3, ChunkBounds)]) -> Vec<HlodCluster> {
3031        if actors.is_empty() { return Vec::new(); }
3032        self.build_level(actors, 0)
3033    }
3034
3035    fn build_level(&mut self, actors: &[(u32, Vec3, ChunkBounds)], depth: u32) -> Vec<HlodCluster> {
3036        let mut clusters: Vec<HlodCluster> = Vec::new();
3037        let mut assigned: Vec<bool> = vec![false; actors.len()];
3038
3039        for i in 0..actors.len() {
3040            if assigned[i] { continue; }
3041            let mut cluster = HlodCluster::new(self.next_cluster_id, depth);
3042            self.next_cluster_id += 1;
3043            let seed_pos = actors[i].1;
3044            cluster.add_actor(actors[i].0, &actors[i].2);
3045            assigned[i] = true;
3046
3047            for j in (i + 1)..actors.len() {
3048                if assigned[j] { continue; }
3049                if cluster.actor_ids.len() >= self.max_actors_per_cluster { break; }
3050                let dist = (actors[j].1 - seed_pos).length();
3051                if dist <= self.cluster_radius {
3052                    cluster.add_actor(actors[j].0, &actors[j].2);
3053                    assigned[j] = true;
3054                }
3055            }
3056
3057            cluster.compute_simplified_geometry();
3058            clusters.push(cluster);
3059        }
3060
3061        clusters
3062    }
3063
3064    pub fn build_hierarchical(&mut self, actors: &[(u32, Vec3, ChunkBounds)]) -> Vec<Vec<HlodCluster>> {
3065        let mut hierarchy: Vec<Vec<HlodCluster>> = Vec::new();
3066        let leaf_clusters = self.build_clusters(actors);
3067        hierarchy.push(leaf_clusters);
3068
3069        let mut depth = 1u32;
3070        while depth <= self.max_depth {
3071            let prev_clusters = hierarchy.last().unwrap();
3072            if prev_clusters.len() <= 1 { break; }
3073
3074            let cluster_actors: Vec<(u32, Vec3, ChunkBounds)> = prev_clusters.iter()
3075                .map(|c| (c.cluster_id, c.center, c.merged_bounds.clone()))
3076                .collect();
3077
3078            let new_radius = self.cluster_radius * (1 << depth) as f32;
3079            let mut builder = HlodBuilder::new(new_radius, self.max_actors_per_cluster * 4, self.max_depth);
3080            builder.next_cluster_id = self.next_cluster_id;
3081            let next_level = builder.build_clusters(&cluster_actors);
3082            self.next_cluster_id = builder.next_cluster_id;
3083            hierarchy.push(next_level);
3084            depth += 1;
3085        }
3086        hierarchy
3087    }
3088
3089    pub fn spatially_sort_actors(&self, actors: &mut [(u32, Vec3, ChunkBounds)]) {
3090        // Z-order curve sort for spatial locality
3091        actors.sort_by(|a, b| {
3092            let za = morton_encode_2d(a.1.x as u32, a.1.z as u32);
3093            let zb = morton_encode_2d(b.1.x as u32, b.1.z as u32);
3094            za.cmp(&zb)
3095        });
3096    }
3097
3098    pub fn compute_cluster_bounds(cluster: &HlodCluster) -> ChunkBounds {
3099        cluster.merged_bounds.clone()
3100    }
3101
3102    pub fn merge_small_clusters(&self, clusters: &mut Vec<HlodCluster>, min_actors: usize) {
3103        let small_ids: Vec<usize> = clusters.iter().enumerate()
3104            .filter(|(_, c)| c.actor_ids.len() < min_actors)
3105            .map(|(i, _)| i)
3106            .collect();
3107
3108        for i in small_ids.iter().rev() {
3109            if *i >= clusters.len() { continue; }
3110            let small = clusters.remove(*i);
3111            // Find nearest cluster to merge into
3112            let mut best = 0;
3113            let mut best_dist = f32::MAX;
3114            for (j, c) in clusters.iter().enumerate() {
3115                let dist = (c.center - small.center).length();
3116                if dist < best_dist { best_dist = dist; best = j; }
3117            }
3118            if !clusters.is_empty() {
3119                clusters[best].merge_cluster(&small);
3120            } else {
3121                clusters.push(small);
3122            }
3123        }
3124    }
3125}
3126
3127// Morton encoding for Z-order curve
3128fn morton_encode_2d(x: u32, y: u32) -> u64 {
3129    let mut result = 0u64;
3130    let x = x as u64;
3131    let y = y as u64;
3132    for i in 0..32u64 {
3133        result |= ((x >> i) & 1) << (2 * i);
3134        result |= ((y >> i) & 1) << (2 * i + 1);
3135    }
3136    result
3137}
3138
3139// ============================================================
3140// STREAMING DISTANCE CALCULATOR
3141// ============================================================
3142
3143#[derive(Debug, Clone)]
3144pub struct StreamingDistanceCalculator {
3145    pub screen_height: f32,
3146    pub fov_vertical_rad: f32,
3147    pub min_screen_size_fraction: f32,
3148    pub max_streaming_distance: f32,
3149    pub lod_bias: f32,
3150}
3151
3152impl StreamingDistanceCalculator {
3153    pub fn new(screen_height: f32, fov_rad: f32) -> Self {
3154        Self {
3155            screen_height,
3156            fov_vertical_rad: fov_rad,
3157            min_screen_size_fraction: 0.01,
3158            max_streaming_distance: 10000.0,
3159            lod_bias: 0.0,
3160        }
3161    }
3162
3163    pub fn compute_streaming_distance(&self, bounding_radius: f32, min_screen_fraction: f32) -> f32 {
3164        let target_screen = self.screen_height * min_screen_fraction;
3165        let half_fov_tan = (self.fov_vertical_rad * 0.5).tan();
3166        if target_screen < 1e-8 || half_fov_tan < 1e-8 {
3167            return self.max_streaming_distance;
3168        }
3169        let dist = bounding_radius * self.screen_height / (target_screen * half_fov_tan);
3170        (dist * (1.0 + self.lod_bias)).min(self.max_streaming_distance)
3171    }
3172
3173    pub fn compute_lod_transition_distances(&self, bounding_radius: f32) -> [f32; 5] {
3174        let fractions = [0.8, 0.4, 0.2, 0.1, 0.05];
3175        let mut dists = [0.0f32; 5];
3176        for (i, &frac) in fractions.iter().enumerate() {
3177            dists[i] = self.compute_streaming_distance(bounding_radius, frac);
3178        }
3179        dists
3180    }
3181
3182    pub fn screen_size_at_distance(&self, bounding_radius: f32, distance: f32) -> f32 {
3183        if distance < 1e-6 { return self.screen_height; }
3184        let half_fov_tan = (self.fov_vertical_rad * 0.5).tan();
3185        let angular_size = bounding_radius / distance;
3186        (angular_size / half_fov_tan) * self.screen_height
3187    }
3188
3189    pub fn desired_lod_at_distance(&self, distance: f32, bounding_radius: f32) -> LodLevel {
3190        let screen_size = self.screen_size_at_distance(bounding_radius, distance);
3191        let fraction = screen_size / self.screen_height;
3192        if fraction > 0.5        { LodLevel::Ultra   }
3193        else if fraction > 0.2   { LodLevel::High    }
3194        else if fraction > 0.08  { LodLevel::Medium  }
3195        else if fraction > 0.03  { LodLevel::Low     }
3196        else if fraction > 0.01  { LodLevel::Impostor}
3197        else                     { LodLevel::Unloaded}
3198    }
3199
3200    pub fn compute_cull_distance(&self, bounding_radius: f32) -> f32 {
3201        self.compute_streaming_distance(bounding_radius, self.min_screen_size_fraction)
3202    }
3203
3204    pub fn importance_adjusted_distance(&self, distance: f32, importance: f32) -> f32 {
3205        distance / importance.max(0.01)
3206    }
3207
3208    pub fn compute_lod_blend_alpha(&self, distance: f32, near_dist: f32, far_dist: f32) -> f32 {
3209        if distance <= near_dist { return 0.0; }
3210        if distance >= far_dist  { return 1.0; }
3211        let range = far_dist - near_dist;
3212        if range < 1e-6 { return 1.0; }
3213        (distance - near_dist) / range
3214    }
3215}
3216
3217// ============================================================
3218// CHUNK DEPENDENCY
3219// ============================================================
3220
3221#[derive(Debug, Clone)]
3222pub struct ChunkDependencyEdge {
3223    pub from: ChunkCoord,
3224    pub to: ChunkCoord,
3225    pub is_hard: bool,
3226    pub weight: f32,
3227}
3228
3229#[derive(Debug, Clone)]
3230pub struct ChunkDependency {
3231    pub dependency_graph: HashMap<ChunkCoord, Vec<ChunkCoord>>,
3232    pub reverse_graph: HashMap<ChunkCoord, Vec<ChunkCoord>>,
3233    pub edges: Vec<ChunkDependencyEdge>,
3234    pub topological_order: Vec<ChunkCoord>,
3235    pub is_dirty: bool,
3236}
3237
3238impl ChunkDependency {
3239    pub fn new() -> Self {
3240        Self {
3241            dependency_graph: HashMap::new(),
3242            reverse_graph: HashMap::new(),
3243            edges: Vec::new(),
3244            topological_order: Vec::new(),
3245            is_dirty: true,
3246        }
3247    }
3248
3249    pub fn add_dependency(&mut self, from: ChunkCoord, to: ChunkCoord, is_hard: bool) {
3250        self.dependency_graph.entry(from.clone()).or_insert_with(Vec::new).push(to.clone());
3251        self.reverse_graph.entry(to.clone()).or_insert_with(Vec::new).push(from.clone());
3252        self.edges.push(ChunkDependencyEdge {
3253            from,
3254            to,
3255            is_hard,
3256            weight: if is_hard { 1.0 } else { 0.5 },
3257        });
3258        self.is_dirty = true;
3259    }
3260
3261    pub fn remove_dependency(&mut self, from: &ChunkCoord, to: &ChunkCoord) {
3262        if let Some(deps) = self.dependency_graph.get_mut(from) {
3263            deps.retain(|c| c != to);
3264        }
3265        if let Some(revs) = self.reverse_graph.get_mut(to) {
3266            revs.retain(|c| c != from);
3267        }
3268        self.edges.retain(|e| !(&e.from == from && &e.to == to));
3269        self.is_dirty = true;
3270    }
3271
3272    pub fn dependencies_of(&self, coord: &ChunkCoord) -> Vec<&ChunkCoord> {
3273        self.dependency_graph.get(coord).map(|v| v.iter().collect()).unwrap_or_default()
3274    }
3275
3276    pub fn dependents_of(&self, coord: &ChunkCoord) -> Vec<&ChunkCoord> {
3277        self.reverse_graph.get(coord).map(|v| v.iter().collect()).unwrap_or_default()
3278    }
3279
3280    pub fn topological_sort(&mut self) -> bool {
3281        if !self.is_dirty { return true; }
3282        // Kahn's algorithm
3283        let mut in_degree: HashMap<ChunkCoord, usize> = HashMap::new();
3284        let all_nodes: HashSet<ChunkCoord> = self.dependency_graph.keys()
3285            .chain(self.reverse_graph.keys())
3286            .cloned()
3287            .collect();
3288
3289        for node in &all_nodes {
3290            in_degree.entry(node.clone()).or_insert(0);
3291        }
3292        for edge in &self.edges {
3293            *in_degree.entry(edge.from.clone()).or_insert(0) += 1;
3294        }
3295
3296        let mut queue: VecDeque<ChunkCoord> = in_degree.iter()
3297            .filter(|(_, &deg)| deg == 0)
3298            .map(|(c, _)| c.clone())
3299            .collect();
3300
3301        let mut order = Vec::new();
3302        while let Some(node) = queue.pop_front() {
3303            order.push(node.clone());
3304            if let Some(dependents) = self.reverse_graph.get(&node) {
3305                for dep in dependents.clone() {
3306                    let deg = in_degree.entry(dep.clone()).or_insert(0);
3307                    if *deg > 0 { *deg -= 1; }
3308                    if *deg == 0 { queue.push_back(dep); }
3309                }
3310            }
3311        }
3312
3313        if order.len() == all_nodes.len() {
3314            self.topological_order = order;
3315            self.is_dirty = false;
3316            true
3317        } else {
3318            false // cycle detected
3319        }
3320    }
3321
3322    pub fn transitive_dependencies(&self, coord: &ChunkCoord, max_depth: usize) -> HashSet<ChunkCoord> {
3323        let mut visited = HashSet::new();
3324        let mut stack = vec![(coord.clone(), 0)];
3325        while let Some((current, depth)) = stack.pop() {
3326            if depth >= max_depth || !visited.insert(current.clone()) { continue; }
3327            if let Some(deps) = self.dependency_graph.get(&current) {
3328                for dep in deps {
3329                    stack.push((dep.clone(), depth + 1));
3330                }
3331            }
3332        }
3333        visited.remove(coord);
3334        visited
3335    }
3336
3337    pub fn has_cycle(&self) -> bool {
3338        let all_nodes: HashSet<ChunkCoord> = self.dependency_graph.keys()
3339            .chain(self.reverse_graph.keys())
3340            .cloned()
3341            .collect();
3342        let mut color: HashMap<ChunkCoord, u8> = HashMap::new(); // 0=white, 1=gray, 2=black
3343        for node in &all_nodes {
3344            if *color.get(node).unwrap_or(&0) == 0 {
3345                if self.dfs_has_cycle(node, &mut color) { return true; }
3346            }
3347        }
3348        false
3349    }
3350
3351    fn dfs_has_cycle(&self, node: &ChunkCoord, color: &mut HashMap<ChunkCoord, u8>) -> bool {
3352        color.insert(node.clone(), 1);
3353        if let Some(neighbors) = self.dependency_graph.get(node) {
3354            for neighbor in neighbors {
3355                let c = *color.get(neighbor).unwrap_or(&0);
3356                if c == 1 { return true; }
3357                if c == 0 && self.dfs_has_cycle(neighbor, color) { return true; }
3358            }
3359        }
3360        color.insert(node.clone(), 2);
3361        false
3362    }
3363
3364    pub fn can_load(&self, coord: &ChunkCoord, loaded: &HashSet<ChunkCoord>) -> bool {
3365        if let Some(deps) = self.dependency_graph.get(coord) {
3366            deps.iter().all(|dep| loaded.contains(dep))
3367        } else {
3368            true
3369        }
3370    }
3371}
3372
3373// ============================================================
3374// ASYNC LOAD QUEUE
3375// ============================================================
3376
3377#[derive(Debug, Clone)]
3378pub struct AsyncLoadRequest {
3379    pub request_id: u64,
3380    pub coord: ChunkCoord,
3381    pub target_lod: LodLevel,
3382    pub priority: f32,
3383    pub frame_queued: u64,
3384    pub load_type: StreamingLoadType,
3385    pub is_cancelled: bool,
3386    pub retry_count: u32,
3387}
3388
3389impl AsyncLoadRequest {
3390    pub fn new(id: u64, coord: ChunkCoord, lod: LodLevel, priority: f32, frame: u64) -> Self {
3391        Self {
3392            request_id: id,
3393            coord,
3394            target_lod: lod,
3395            priority,
3396            frame_queued: frame,
3397            load_type: StreamingLoadType::Asynchronous,
3398            is_cancelled: false,
3399            retry_count: 0,
3400        }
3401    }
3402}
3403
3404#[derive(Debug, Clone)]
3405pub struct AsyncLoadQueue {
3406    pub pending: BTreeMap<u64, AsyncLoadRequest>,  // sorted by -priority*1e6 as key
3407    pub in_flight: HashMap<u64, AsyncLoadRequest>,
3408    pub completed: VecDeque<(u64, bool)>, // (request_id, success)
3409    pub cancelled: HashSet<u64>,
3410    pub next_id: u64,
3411    pub max_in_flight: usize,
3412    pub max_pending: usize,
3413}
3414
3415impl AsyncLoadQueue {
3416    pub fn new(max_in_flight: usize, max_pending: usize) -> Self {
3417        Self {
3418            pending: BTreeMap::new(),
3419            in_flight: HashMap::new(),
3420            completed: VecDeque::new(),
3421            cancelled: HashSet::new(),
3422            next_id: 1,
3423            max_in_flight,
3424            max_pending,
3425        }
3426    }
3427
3428    pub fn enqueue(&mut self, coord: ChunkCoord, lod: LodLevel, priority: f32, frame: u64) -> u64 {
3429        let id = self.next_id;
3430        self.next_id += 1;
3431        let req = AsyncLoadRequest::new(id, coord, lod, priority, frame);
3432        // Use negated priority * large factor as sort key for descending order
3433        let key = ((-priority * 1_000_000.0) as i64 as u64).wrapping_add(id);
3434        self.pending.insert(key, req);
3435        if self.pending.len() > self.max_pending {
3436            self.pending.pop_last();
3437        }
3438        id
3439    }
3440
3441    pub fn cancel(&mut self, request_id: u64) {
3442        self.cancelled.insert(request_id);
3443        if let Some(req) = self.in_flight.get_mut(&request_id) {
3444            req.is_cancelled = true;
3445        }
3446        self.pending.retain(|_, req| req.request_id != request_id);
3447    }
3448
3449    pub fn cancel_coord(&mut self, coord: &ChunkCoord) {
3450        let ids_to_cancel: Vec<u64> = self.in_flight.values()
3451            .filter(|r| &r.coord == coord)
3452            .map(|r| r.request_id)
3453            .chain(
3454                self.pending.values()
3455                    .filter(|r| &r.coord == coord)
3456                    .map(|r| r.request_id)
3457            )
3458            .collect();
3459        for id in ids_to_cancel { self.cancel(id); }
3460    }
3461
3462    pub fn dispatch_available(&mut self, frame: u64) -> Vec<AsyncLoadRequest> {
3463        let to_dispatch = self.max_in_flight - self.in_flight.len().min(self.max_in_flight);
3464        let mut dispatched = Vec::new();
3465        let mut keys_to_remove = Vec::new();
3466
3467        for (&key, req) in &self.pending {
3468            if dispatched.len() >= to_dispatch { break; }
3469            if self.cancelled.contains(&req.request_id) {
3470                keys_to_remove.push(key);
3471                continue;
3472            }
3473            keys_to_remove.push(key);
3474            dispatched.push(req.clone());
3475        }
3476
3477        for key in keys_to_remove {
3478            if let Some(req) = self.pending.remove(&key) {
3479                if !self.cancelled.contains(&req.request_id) {
3480                    self.in_flight.insert(req.request_id, req);
3481                }
3482            }
3483        }
3484
3485        dispatched
3486    }
3487
3488    pub fn complete_request(&mut self, request_id: u64, success: bool) {
3489        self.in_flight.remove(&request_id);
3490        self.cancelled.remove(&request_id);
3491        self.completed.push_back((request_id, success));
3492        while self.completed.len() > 256 {
3493            self.completed.pop_front();
3494        }
3495    }
3496
3497    pub fn drain_completed(&mut self) -> Vec<(u64, bool)> {
3498        self.completed.drain(..).collect()
3499    }
3500
3501    pub fn is_pending(&self, coord: &ChunkCoord) -> bool {
3502        self.pending.values().any(|r| &r.coord == coord && !self.cancelled.contains(&r.request_id))
3503    }
3504
3505    pub fn is_in_flight(&self, coord: &ChunkCoord) -> bool {
3506        self.in_flight.values().any(|r| &r.coord == coord && !r.is_cancelled)
3507    }
3508
3509    pub fn pending_count(&self) -> usize {
3510        self.pending.len()
3511    }
3512
3513    pub fn in_flight_count(&self) -> usize {
3514        self.in_flight.len()
3515    }
3516
3517    pub fn update_priority(&mut self, coord: &ChunkCoord, new_priority: f32) {
3518        let to_update: Vec<(u64, u64)> = self.pending.iter()
3519            .filter(|(_, r)| &r.coord == coord)
3520            .map(|(&k, r)| (k, r.request_id))
3521            .collect();
3522        for (old_key, req_id) in to_update {
3523            if let Some(mut req) = self.pending.remove(&old_key) {
3524                req.priority = new_priority;
3525                let new_key = ((-new_priority * 1_000_000.0) as i64 as u64).wrapping_add(req_id);
3526                self.pending.insert(new_key, req);
3527            }
3528        }
3529    }
3530
3531    pub fn stale_requests(&self, current_frame: u64, max_age_frames: u64) -> Vec<u64> {
3532        self.pending.values()
3533            .filter(|r| current_frame.saturating_sub(r.frame_queued) > max_age_frames)
3534            .map(|r| r.request_id)
3535            .collect()
3536    }
3537}
3538
3539// ============================================================
3540// STREAMING PROFILER
3541// ============================================================
3542
3543#[derive(Debug, Clone)]
3544pub struct ProfilerEvent {
3545    pub event_type: ProfilerEventType,
3546    pub start_time_us: u64,
3547    pub duration_us: u64,
3548    pub frame: u64,
3549    pub metadata: u32,
3550}
3551
3552#[derive(Debug, Clone)]
3553pub struct FrameProfileData {
3554    pub frame: u64,
3555    pub events: Vec<ProfilerEvent>,
3556    pub total_load_us: u64,
3557    pub total_unload_us: u64,
3558    pub total_cull_us: u64,
3559    pub total_lod_us: u64,
3560    pub chunks_loaded: u32,
3561    pub chunks_unloaded: u32,
3562}
3563
3564impl FrameProfileData {
3565    pub fn new(frame: u64) -> Self {
3566        Self {
3567            frame,
3568            events: Vec::new(),
3569            total_load_us: 0,
3570            total_unload_us: 0,
3571            total_cull_us: 0,
3572            total_lod_us: 0,
3573            chunks_loaded: 0,
3574            chunks_unloaded: 0,
3575        }
3576    }
3577}
3578
3579#[derive(Debug, Clone)]
3580pub struct StreamingProfiler {
3581    pub history: VecDeque<FrameProfileData>,
3582    pub current_frame_data: FrameProfileData,
3583    pub current_frame: u64,
3584    pub active_timers: HashMap<String, u64>,
3585    pub max_history: usize,
3586    pub total_events_recorded: u64,
3587    pub peak_load_time_us: u64,
3588    pub peak_frame_time_us: u64,
3589}
3590
3591impl StreamingProfiler {
3592    pub fn new(max_history: usize) -> Self {
3593        Self {
3594            history: VecDeque::with_capacity(max_history),
3595            current_frame_data: FrameProfileData::new(0),
3596            current_frame: 0,
3597            active_timers: HashMap::new(),
3598            max_history,
3599            total_events_recorded: 0,
3600            peak_load_time_us: 0,
3601            peak_frame_time_us: 0,
3602        }
3603    }
3604
3605    pub fn begin_frame(&mut self, frame: u64) {
3606        self.current_frame = frame;
3607        self.current_frame_data = FrameProfileData::new(frame);
3608    }
3609
3610    pub fn end_frame(&mut self) {
3611        let data = self.current_frame_data.clone();
3612        let frame_total = data.total_load_us + data.total_unload_us
3613            + data.total_cull_us + data.total_lod_us;
3614        if frame_total > self.peak_frame_time_us {
3615            self.peak_frame_time_us = frame_total;
3616        }
3617        if data.total_load_us > self.peak_load_time_us {
3618            self.peak_load_time_us = data.total_load_us;
3619        }
3620        self.history.push_back(data);
3621        if self.history.len() > self.max_history {
3622            self.history.pop_front();
3623        }
3624    }
3625
3626    pub fn record_event(&mut self, event_type: ProfilerEventType, start_us: u64, duration_us: u64, meta: u32) {
3627        let event = ProfilerEvent {
3628            event_type: event_type.clone(),
3629            start_time_us: start_us,
3630            duration_us,
3631            frame: self.current_frame,
3632            metadata: meta,
3633        };
3634        match event_type {
3635            ProfilerEventType::ChunkLoad => {
3636                self.current_frame_data.total_load_us += duration_us;
3637                self.current_frame_data.chunks_loaded += 1;
3638            }
3639            ProfilerEventType::ChunkUnload => {
3640                self.current_frame_data.total_unload_us += duration_us;
3641                self.current_frame_data.chunks_unloaded += 1;
3642            }
3643            ProfilerEventType::FrustumCull => {
3644                self.current_frame_data.total_cull_us += duration_us;
3645            }
3646            ProfilerEventType::LodSwitch => {
3647                self.current_frame_data.total_lod_us += duration_us;
3648            }
3649            _ => {}
3650        }
3651        self.current_frame_data.events.push(event);
3652        self.total_events_recorded += 1;
3653    }
3654
3655    pub fn average_load_time_us(&self) -> f64 {
3656        if self.history.is_empty() { return 0.0; }
3657        let total: u64 = self.history.iter().map(|f| f.total_load_us).sum();
3658        total as f64 / self.history.len() as f64
3659    }
3660
3661    pub fn average_chunks_per_frame(&self) -> f64 {
3662        if self.history.is_empty() { return 0.0; }
3663        let total: u32 = self.history.iter().map(|f| f.chunks_loaded).sum();
3664        total as f64 / self.history.len() as f64
3665    }
3666
3667    pub fn compute_percentile_load_time(&self, pct: f64) -> u64 {
3668        let mut times: Vec<u64> = self.history.iter().map(|f| f.total_load_us).collect();
3669        times.sort_unstable();
3670        if times.is_empty() { return 0; }
3671        let idx = ((pct / 100.0) * (times.len() - 1) as f64) as usize;
3672        times[idx.min(times.len() - 1)]
3673    }
3674
3675    pub fn frame_time_ms(&self, frame_idx: usize) -> f64 {
3676        if let Some(data) = self.history.get(frame_idx) {
3677            (data.total_load_us + data.total_unload_us + data.total_cull_us + data.total_lod_us) as f64 / 1000.0
3678        } else {
3679            0.0
3680        }
3681    }
3682
3683    pub fn event_count_by_type(&self, event_type: &ProfilerEventType) -> usize {
3684        self.history.iter()
3685            .flat_map(|f| f.events.iter())
3686            .filter(|e| std::mem::discriminant(&e.event_type) == std::mem::discriminant(event_type))
3687            .count()
3688    }
3689
3690    pub fn history_len(&self) -> usize {
3691        self.history.len()
3692    }
3693
3694    pub fn report_summary(&self) -> ProfilerSummary {
3695        ProfilerSummary {
3696            frames_recorded: self.history.len(),
3697            avg_load_time_us: self.average_load_time_us(),
3698            avg_chunks_per_frame: self.average_chunks_per_frame(),
3699            peak_load_time_us: self.peak_load_time_us,
3700            peak_frame_time_us: self.peak_frame_time_us,
3701            p99_load_time_us: self.compute_percentile_load_time(99.0),
3702            total_events: self.total_events_recorded,
3703        }
3704    }
3705}
3706
3707#[derive(Debug, Clone)]
3708pub struct ProfilerSummary {
3709    pub frames_recorded: usize,
3710    pub avg_load_time_us: f64,
3711    pub avg_chunks_per_frame: f64,
3712    pub peak_load_time_us: u64,
3713    pub peak_frame_time_us: u64,
3714    pub p99_load_time_us: u64,
3715    pub total_events: u64,
3716}
3717
3718// ============================================================
3719// WORLD STREAMING EDITOR
3720// ============================================================
3721
3722#[derive(Debug, Clone)]
3723pub struct WorldStreamingEditor {
3724    pub config: StreamingConfig,
3725    pub chunks: HashMap<ChunkCoord, StreamingChunk>,
3726    pub viewer_position: Vec3,
3727    pub viewer_forward: Vec3,
3728    pub camera: Option<StreamingCamera>,
3729    pub frustum: Option<FrustumCulling>,
3730    pub load_queue: AsyncLoadQueue,
3731    pub unload_queue: AsyncLoadQueue,
3732    pub memory_budget: MemoryBudget,
3733    pub streaming_stats: StreamingStats,
3734    pub profiler: StreamingProfiler,
3735    pub octree: Option<OctreeNode>,
3736    pub bvh: Option<BvhNode>,
3737    pub world_partition: WorldPartition,
3738    pub data_layers: DataLayerSystem,
3739    pub hlod_hierarchy: Vec<Vec<HlodCluster>>,
3740    pub virtual_texture: Option<VirtualTexture>,
3741    pub terrain_patches: HashMap<ChunkCoord, TerrainPatch>,
3742    pub actor_proxies: HashMap<u32, ActorStreamingProxy>,
3743    pub impostors: HashMap<u32, ImpostorBillboard>,
3744    pub chunk_dependencies: ChunkDependency,
3745    pub loaded_chunk_set: HashSet<ChunkCoord>,
3746    pub frame_number: u64,
3747    pub current_time_us: u64,
3748    pub pending_lod_transitions: Vec<(ChunkCoord, LodLevel, LodLevel)>,
3749    pub mesh_lods: HashMap<u32, ChunkMeshLod>,
3750    pub streaming_distance_calculator: StreamingDistanceCalculator,
3751    pub hlod_builder: HlodBuilder,
3752}
3753
3754impl WorldStreamingEditor {
3755    pub fn new(config: StreamingConfig) -> Self {
3756        let memory_budget = MemoryBudget::new(config.max_memory_mb, config.eviction_policy.clone());
3757        let max_in_flight = config.max_concurrent_loads;
3758        let max_pending = MAX_ASYNC_LOAD_QUEUE;
3759        let screen_h = config.screen_height_pixels as f32;
3760        let fov_rad = config.fov_vertical_rad;
3761        let cell_size = WORLD_PARTITION_CELL_SIZE;
3762        let vt = if config.enable_virtual_textures {
3763            Some(VirtualTexture::new(VIRTUAL_TEXTURE_ATLAS_SIZE, VIRTUAL_TEXTURE_TILE_SIZE, FEEDBACK_BUFFER_MIPS as u32))
3764        } else {
3765            None
3766        };
3767        Self {
3768            config: config.clone(),
3769            chunks: HashMap::new(),
3770            viewer_position: Vec3::ZERO,
3771            viewer_forward: Vec3::NEG_Z,
3772            camera: None,
3773            frustum: None,
3774            load_queue: AsyncLoadQueue::new(max_in_flight, max_pending),
3775            unload_queue: AsyncLoadQueue::new(config.max_concurrent_unloads, max_pending / 4),
3776            memory_budget,
3777            streaming_stats: StreamingStats::new(),
3778            profiler: StreamingProfiler::new(PROFILER_HISTORY_FRAMES),
3779            octree: None,
3780            bvh: None,
3781            world_partition: WorldPartition::new(cell_size),
3782            data_layers: DataLayerSystem::new(),
3783            hlod_hierarchy: Vec::new(),
3784            virtual_texture: vt,
3785            terrain_patches: HashMap::new(),
3786            actor_proxies: HashMap::new(),
3787            impostors: HashMap::new(),
3788            chunk_dependencies: ChunkDependency::new(),
3789            loaded_chunk_set: HashSet::new(),
3790            frame_number: 0,
3791            current_time_us: 0,
3792            pending_lod_transitions: Vec::new(),
3793            mesh_lods: HashMap::new(),
3794            streaming_distance_calculator: StreamingDistanceCalculator::new(screen_h, fov_rad),
3795            hlod_builder: HlodBuilder::new(HLOD_CLUSTER_RADIUS, 32, 3),
3796        }
3797    }
3798
3799    pub fn set_viewer(&mut self, position: Vec3, forward: Vec3) {
3800        self.viewer_position = position;
3801        self.viewer_forward = forward.normalize_or_zero();
3802        self.world_partition.clear_streaming_sources();
3803        self.world_partition.add_streaming_source(position);
3804    }
3805
3806    pub fn set_camera(&mut self, camera: StreamingCamera) {
3807        let planes = camera.frustum_planes;
3808        self.camera = Some(camera);
3809        self.frustum = Some(FrustumCulling::new(planes));
3810    }
3811
3812    pub fn tick(&mut self, delta_time_s: f32, time_us: u64) {
3813        self.current_time_us = time_us;
3814        self.profiler.begin_frame(self.frame_number);
3815        self.streaming_stats.advance_frame();
3816
3817        self.update_chunk_visibility();
3818        self.update_chunk_lods();
3819        self.process_load_queue();
3820        self.process_unload_queue();
3821        self.evict_memory_if_needed();
3822        self.update_terrain_stitching();
3823        self.update_impostors();
3824        self.update_virtual_textures();
3825
3826        self.profiler.end_frame();
3827        self.frame_number += 1;
3828    }
3829
3830    fn update_chunk_visibility(&mut self) {
3831        let viewer = self.viewer_position;
3832        let frame = self.frame_number;
3833        let streaming_radius = self.config.streaming_radius;
3834        let vertical_radius = self.config.vertical_streaming_radius;
3835
3836        let viewer_coord = ChunkCoord::from_world_pos(viewer, self.config.chunk_size);
3837        let radius_chunks = self.config.streaming_radius_chunks();
3838
3839        let candidates = ChunkCoord::chunks_in_radius(&viewer_coord, radius_chunks);
3840
3841        // Ensure chunks exist for all candidates in radius
3842        for coord in &candidates {
3843            self.chunks.entry(coord.clone()).or_insert_with(|| {
3844                StreamingChunk::new(coord.clone(), self.config.chunk_size)
3845            });
3846        }
3847
3848        let frustum = self.frustum.clone();
3849        let cull_enabled = self.config.enable_frustum_culling;
3850
3851        for (coord, chunk) in self.chunks.iter_mut() {
3852            chunk.update_distance(viewer);
3853
3854            let vert_dist = (chunk.bounds.center().y - viewer.y).abs();
3855            if vert_dist > vertical_radius {
3856                chunk.mark_not_visible();
3857                continue;
3858            }
3859            if chunk.distance_to_viewer > streaming_radius {
3860                chunk.mark_not_visible();
3861                continue;
3862            }
3863            if cull_enabled {
3864                if let Some(ref frust) = frustum {
3865                    if !frust.test_aabb_fast(&chunk.bounds) {
3866                        chunk.mark_not_visible();
3867                        continue;
3868                    }
3869                }
3870            }
3871            chunk.mark_visible(frame);
3872            chunk.compute_load_priority(viewer, self.viewer_forward);
3873        }
3874    }
3875
3876    fn update_chunk_lods(&mut self) {
3877        let viewer = self.viewer_position;
3878        let screen_h = self.config.screen_height_pixels as f32;
3879
3880        let camera_opt = self.camera.clone();
3881        let config = self.config.clone();
3882        let frame = self.frame_number;
3883
3884        let mut transitions = Vec::new();
3885
3886        for (coord, chunk) in self.chunks.iter_mut() {
3887            if !chunk.is_visible || chunk.load_state == ChunkLoadState::Unloaded {
3888                continue;
3889            }
3890            let desired_lod = if let Some(ref cam) = camera_opt {
3891                let screen_size = cam.compute_lod_screen_size(&chunk.bounds, screen_h);
3892                ChunkMeshLod::new(10000, config.chunk_size).select_lod_for_screen_size(screen_size)
3893            } else {
3894                config.lod_for_distance(chunk.distance_to_viewer)
3895            };
3896
3897            if desired_lod != chunk.lod_level && chunk.last_loaded_frame + config.min_lod_retain_frames <= frame {
3898                transitions.push((coord.clone(), chunk.lod_level.clone(), desired_lod));
3899            }
3900        }
3901        self.pending_lod_transitions = transitions;
3902        for (coord, old_lod, new_lod) in &self.pending_lod_transitions {
3903            if let Some(chunk) = self.chunks.get_mut(coord) {
3904                chunk.lod_level = new_lod.clone();
3905                chunk.flags.needs_lod_update = true;
3906            }
3907            self.streaming_stats.chunks_lod_switched_this_frame += 1;
3908        }
3909    }
3910
3911    fn process_load_queue(&mut self) {
3912        let frame = self.frame_number;
3913        let viewer = self.viewer_position;
3914
3915        // Re-prioritize pending requests
3916        let chunk_size = self.config.chunk_size;
3917        self.load_queue.update_priority(&ChunkCoord::new(0,0,0), 0.0); // trigger no-op
3918
3919        // Enqueue visible unloaded chunks
3920        let mut to_enqueue: Vec<(ChunkCoord, LodLevel, f32)> = Vec::new();
3921        for (coord, chunk) in &self.chunks {
3922            if !chunk.is_visible { continue; }
3923            if chunk.load_state != ChunkLoadState::Unloaded { continue; }
3924            if self.load_queue.is_pending(coord) || self.load_queue.is_in_flight(coord) { continue; }
3925            let desired_lod = self.config.lod_for_distance(chunk.distance_to_viewer);
3926            if desired_lod == LodLevel::Unloaded { continue; }
3927            if !self.chunk_dependencies.can_load(coord, &self.loaded_chunk_set) { continue; }
3928            to_enqueue.push((coord.clone(), desired_lod, chunk.load_priority));
3929        }
3930
3931        for (coord, lod, priority) in to_enqueue {
3932            let id = self.load_queue.enqueue(coord.clone(), lod, priority, frame);
3933            if let Some(chunk) = self.chunks.get_mut(&coord) {
3934                chunk.load_state = ChunkLoadState::Queued;
3935            }
3936        }
3937
3938        // Dispatch available slots
3939        let dispatched = self.load_queue.dispatch_available(frame);
3940        for req in dispatched {
3941            if let Some(chunk) = self.chunks.get_mut(&req.coord) {
3942                chunk.load_state = ChunkLoadState::Loading;
3943            }
3944            // Simulate instant load completion for editor purposes
3945            let est_bytes = self.estimate_chunk_memory(&req.coord, &req.target_lod);
3946            let can_alloc = self.memory_budget.can_allocate(est_bytes);
3947            let success = can_alloc;
3948            if success {
3949                self.memory_budget.allocate(req.coord.clone(), req.target_lod.clone(), est_bytes, frame);
3950                if let Some(chunk) = self.chunks.get_mut(&req.coord) {
3951                    chunk.load_state = ChunkLoadState::Loaded;
3952                    chunk.lod_level = req.target_lod.clone();
3953                    chunk.memory_bytes = est_bytes;
3954                    chunk.last_loaded_frame = frame;
3955                }
3956                self.loaded_chunk_set.insert(req.coord.clone());
3957                self.streaming_stats.chunks_loaded_this_frame += 1;
3958            }
3959            self.load_queue.complete_request(req.request_id, success);
3960        }
3961    }
3962
3963    fn process_unload_queue(&mut self) {
3964        let viewer = self.viewer_position;
3965        let streaming_radius = self.config.streaming_radius;
3966        let frame = self.frame_number;
3967
3968        // Mark distant loaded chunks for eviction
3969        let mut to_evict: Vec<ChunkCoord> = Vec::new();
3970        for (coord, chunk) in &self.chunks {
3971            if !chunk.can_evict() { continue; }
3972            if chunk.distance_to_viewer > streaming_radius * 1.1 {
3973                to_evict.push(coord.clone());
3974            }
3975        }
3976
3977        for coord in &to_evict {
3978            if !self.unload_queue.is_pending(coord) && !self.unload_queue.is_in_flight(coord) {
3979                let priority = if let Some(chunk) = self.chunks.get(coord) { chunk.distance_to_viewer } else { 0.0 };
3980                self.unload_queue.enqueue(coord.clone(), LodLevel::Unloaded, priority, frame);
3981                if let Some(chunk) = self.chunks.get_mut(coord) {
3982                    chunk.load_state = ChunkLoadState::Evicting;
3983                }
3984            }
3985        }
3986
3987        let dispatched = self.unload_queue.dispatch_available(frame);
3988        for req in dispatched {
3989            let old_lod = self.chunks.get(&req.coord).map(|c| c.lod_level.clone()).unwrap_or(LodLevel::Unloaded);
3990            self.memory_budget.free(&req.coord, &old_lod);
3991            if let Some(chunk) = self.chunks.get_mut(&req.coord) {
3992                chunk.load_state = ChunkLoadState::Unloaded;
3993                chunk.lod_level = LodLevel::Unloaded;
3994                chunk.memory_bytes = 0;
3995            }
3996            self.loaded_chunk_set.remove(&req.coord);
3997            self.streaming_stats.chunks_unloaded_this_frame += 1;
3998            self.unload_queue.complete_request(req.request_id, true);
3999        }
4000    }
4001
4002    fn evict_memory_if_needed(&mut self) {
4003        if !self.memory_budget.needs_eviction() { return; }
4004        let frame = self.frame_number;
4005        let candidates = self.memory_budget.eviction_candidates(8, frame);
4006        for coord in candidates {
4007            if let Some(chunk) = self.chunks.get(&coord) {
4008                if chunk.can_evict() && !chunk.is_visible {
4009                    let lod = chunk.lod_level.clone();
4010                    self.memory_budget.free(&coord, &lod);
4011                    if let Some(c) = self.chunks.get_mut(&coord) {
4012                        c.load_state = ChunkLoadState::Unloaded;
4013                        c.lod_level = LodLevel::Unloaded;
4014                        c.memory_bytes = 0;
4015                    }
4016                    self.loaded_chunk_set.remove(&coord);
4017                }
4018            }
4019        }
4020    }
4021
4022    fn update_terrain_stitching(&mut self) {
4023        let needs_stitch: Vec<ChunkCoord> = self.terrain_patches
4024            .iter()
4025            .filter(|(_, p)| p.needs_stitch)
4026            .map(|(c, _)| c.clone())
4027            .collect();
4028
4029        for coord in needs_stitch {
4030            let neighbors_6 = coord.neighbors_6();
4031            let neighbor_seams_and_lods: Vec<_> = [0usize, 1, 2, 3].iter().map(|&side| {
4032                let ncoord = neighbors_6[side * 2];
4033                let seam = self.terrain_patches.get(&ncoord).map(|p| p.seam_data[1 - side % 2].clone()).unwrap_or_default();
4034                let lod = self.terrain_patches.get(&ncoord).map(|p| p.lod_level.clone());
4035                (seam, lod)
4036            }).collect();
4037
4038            if let Some(patch) = self.terrain_patches.get_mut(&coord) {
4039                for side in 0..4 {
4040                    if let (seam, Some(neighbor_lod)) = &neighbor_seams_and_lods[side] {
4041                        if !seam.is_empty() {
4042                            patch.stitch_edge(side, seam, neighbor_lod);
4043                        }
4044                    }
4045                }
4046                self.streaming_stats.terrain_stitch_ops += 1;
4047            }
4048        }
4049    }
4050
4051    fn update_impostors(&mut self) {
4052        if !self.config.enable_impostor_billboards { return; }
4053        let viewer = self.viewer_position;
4054        let frame = self.frame_number;
4055        for (_, impostor) in self.impostors.iter_mut() {
4056            if impostor.should_update(viewer, 5.0) {
4057                impostor.update_view_index(viewer);
4058                impostor.clear_dirty(frame);
4059            }
4060        }
4061    }
4062
4063    fn update_virtual_textures(&mut self) {
4064        if !self.config.enable_virtual_textures { return; }
4065        if let Some(ref mut vt) = self.virtual_texture {
4066            let requests = vt.analyze_feedback_buffer();
4067            let to_load: Vec<_> = requests.iter().take(8).filter(|r| r.3 > 0.01).cloned().collect();
4068            for (mip, tx, ty, _) in to_load {
4069                vt.load_tile(mip, tx, ty);
4070                self.streaming_stats.virtual_texture_uploads += 1;
4071            }
4072            vt.evict_lru_tiles(vt.max_resident_tiles / 4);
4073            vt.clear_feedback();
4074        }
4075    }
4076
4077    pub fn estimate_chunk_memory(&self, coord: &ChunkCoord, lod: &LodLevel) -> u64 {
4078        let base = 4 * 1024 * 1024u64; // 4 MB base
4079        (base as f32 * lod.memory_multiplier()) as u64
4080    }
4081
4082    pub fn register_actor(&mut self, actor_id: u32, position: Vec3, bounds: ChunkBounds, streaming_dist: f32) {
4083        let mut proxy = ActorStreamingProxy::new(actor_id, position, bounds);
4084        proxy.streaming_distance = streaming_dist;
4085        self.actor_proxies.insert(actor_id, proxy);
4086        self.world_partition.register_actor(actor_id, position);
4087    }
4088
4089    pub fn unregister_actor(&mut self, actor_id: u32) {
4090        self.actor_proxies.remove(&actor_id);
4091        self.world_partition.unregister_actor(actor_id);
4092    }
4093
4094    pub fn add_terrain_patch(&mut self, coord: ChunkCoord, size: usize, cell_size: f32) {
4095        let patch = TerrainPatch::new(coord.clone(), size, cell_size);
4096        self.terrain_patches.insert(coord, patch);
4097    }
4098
4099    pub fn build_octree(&mut self) {
4100        let points: Vec<(u32, Vec3)> = self.actor_proxies.iter()
4101            .map(|(&id, proxy)| (id, proxy.world_position))
4102            .collect();
4103        let builder = OctreeBuilder::new(MAX_OCTREE_DEPTH, MAX_OBJECTS_PER_OCTREE_NODE);
4104        self.octree = Some(builder.build(&points));
4105    }
4106
4107    pub fn build_bvh(&mut self) {
4108        let objects: Vec<(u32, ChunkBounds)> = self.actor_proxies.iter()
4109            .map(|(&id, proxy)| (id, proxy.bounds.clone()))
4110            .collect();
4111        let builder = BvhBuilder::new(BVH_MAX_LEAF_OBJECTS, 16);
4112        self.bvh = builder.build(&objects);
4113    }
4114
4115    pub fn build_hlod(&mut self) {
4116        let actors: Vec<(u32, Vec3, ChunkBounds)> = self.actor_proxies.iter()
4117            .map(|(&id, proxy)| (id, proxy.world_position, proxy.bounds.clone()))
4118            .collect();
4119        self.hlod_hierarchy = self.hlod_builder.build_hierarchical(&actors);
4120        self.streaming_stats.hlod_merges += 1;
4121    }
4122
4123    pub fn query_visible_objects(&self) -> Vec<u32> {
4124        let mut result = Vec::new();
4125        if let Some(ref frustum) = self.frustum {
4126            if let Some(ref bvh) = self.bvh {
4127                bvh.query_frustum(frustum, &mut result);
4128            }
4129        }
4130        result
4131    }
4132
4133    pub fn query_objects_in_radius(&self, center: Vec3, radius: f32) -> Vec<u32> {
4134        let mut result = Vec::new();
4135        if let Some(ref octree) = self.octree {
4136            octree.query_sphere(center, radius, &mut result);
4137        }
4138        result
4139    }
4140
4141    pub fn query_objects_in_bounds(&self, bounds: &ChunkBounds) -> Vec<u32> {
4142        let mut result = Vec::new();
4143        if let Some(ref bvh) = self.bvh {
4144            bvh.query_aabb(bounds, &mut result);
4145        }
4146        result
4147    }
4148
4149    pub fn get_chunk_lod(&self, coord: &ChunkCoord) -> LodLevel {
4150        self.chunks.get(coord).map(|c| c.lod_level.clone()).unwrap_or(LodLevel::Unloaded)
4151    }
4152
4153    pub fn get_chunk_state(&self, coord: &ChunkCoord) -> ChunkLoadState {
4154        self.chunks.get(coord).map(|c| c.load_state.clone()).unwrap_or(ChunkLoadState::Unloaded)
4155    }
4156
4157    pub fn chunk_count(&self) -> usize {
4158        self.chunks.len()
4159    }
4160
4161    pub fn loaded_chunk_count(&self) -> usize {
4162        self.loaded_chunk_set.len()
4163    }
4164
4165    pub fn memory_usage_mb(&self) -> f32 {
4166        self.memory_budget.total_used_mb()
4167    }
4168
4169    pub fn stats(&self) -> &StreamingStats {
4170        &self.streaming_stats
4171    }
4172
4173    pub fn profiler_summary(&self) -> ProfilerSummary {
4174        self.profiler.report_summary()
4175    }
4176
4177    pub fn set_lod_bias(&mut self, bias: f32) {
4178        self.config.lod_bias = bias;
4179        self.streaming_distance_calculator.lod_bias = bias;
4180    }
4181
4182    pub fn force_lod(&mut self, coord: &ChunkCoord, lod: LodLevel) {
4183        if let Some(chunk) = self.chunks.get_mut(coord) {
4184            let old = chunk.lod_level.clone();
4185            chunk.lod_level = lod.clone();
4186            chunk.flags.needs_lod_update = true;
4187            self.pending_lod_transitions.push((coord.clone(), old, lod));
4188        }
4189    }
4190
4191    pub fn get_terrain_height(&self, world_x: f32, world_z: f32) -> f32 {
4192        let coord = ChunkCoord::from_world_pos(
4193            Vec3::new(world_x, 0.0, world_z),
4194            self.config.chunk_size,
4195        );
4196        if let Some(patch) = self.terrain_patches.get(&coord) {
4197            patch.heightmap.sample_bilinear(world_x, world_z)
4198        } else {
4199            0.0
4200        }
4201    }
4202
4203    pub fn get_terrain_normal(&self, world_x: f32, world_z: f32) -> Vec3 {
4204        let coord = ChunkCoord::from_world_pos(
4205            Vec3::new(world_x, 0.0, world_z),
4206            self.config.chunk_size,
4207        );
4208        if let Some(patch) = self.terrain_patches.get(&coord) {
4209            patch.heightmap.compute_normal_bilinear(world_x, world_z)
4210        } else {
4211            Vec3::Y
4212        }
4213    }
4214
4215    pub fn compute_streaming_bounds(&self) -> ChunkBounds {
4216        let half_r = Vec3::new(
4217            self.config.streaming_radius,
4218            self.config.vertical_streaming_radius,
4219            self.config.streaming_radius,
4220        );
4221        ChunkBounds::from_center_size(self.viewer_position, half_r)
4222    }
4223
4224    pub fn debug_draw_chunks(&self) -> Vec<(ChunkBounds, Vec3, LodLevel)> {
4225        self.chunks.values()
4226            .filter(|c| c.load_state != ChunkLoadState::Unloaded)
4227            .map(|c| {
4228                let color = match c.lod_level {
4229                    LodLevel::Ultra    => Vec3::new(0.0, 1.0, 0.0),
4230                    LodLevel::High     => Vec3::new(0.5, 1.0, 0.0),
4231                    LodLevel::Medium   => Vec3::new(1.0, 1.0, 0.0),
4232                    LodLevel::Low      => Vec3::new(1.0, 0.5, 0.0),
4233                    LodLevel::Impostor => Vec3::new(1.0, 0.0, 0.0),
4234                    LodLevel::Unloaded => Vec3::new(0.3, 0.3, 0.3),
4235                };
4236                (c.bounds.clone(), color, c.lod_level.clone())
4237            })
4238            .collect()
4239    }
4240}
4241
4242// ============================================================
4243// ADDITIONAL MATH UTILITIES
4244// ============================================================
4245
4246pub fn compute_view_matrix(eye: Vec3, target: Vec3, up: Vec3) -> Mat4 {
4247    Mat4::look_at_rh(eye, target, up)
4248}
4249
4250pub fn compute_projection_matrix(fov_y_deg: f32, aspect: f32, near: f32, far: f32) -> Mat4 {
4251    Mat4::perspective_rh(fov_y_deg.to_radians(), aspect, near, far)
4252}
4253
4254pub fn compute_ortho_matrix(left: f32, right: f32, bottom: f32, top: f32, near: f32, far: f32) -> Mat4 {
4255    Mat4::orthographic_rh(left, right, bottom, top, near, far)
4256}
4257
4258pub fn screen_to_world_ray(
4259    screen_x: f32,
4260    screen_y: f32,
4261    screen_w: f32,
4262    screen_h: f32,
4263    inv_view_proj: Mat4,
4264) -> (Vec3, Vec3) {
4265    let ndc_x = (screen_x / screen_w) * 2.0 - 1.0;
4266    let ndc_y = 1.0 - (screen_y / screen_h) * 2.0;
4267    let near_point = inv_view_proj.project_point3(Vec3::new(ndc_x, ndc_y, -1.0));
4268    let far_point  = inv_view_proj.project_point3(Vec3::new(ndc_x, ndc_y,  1.0));
4269    let dir = (far_point - near_point).normalize();
4270    (near_point, dir)
4271}
4272
4273pub fn compute_sphere_screen_radius(center: Vec3, radius: f32, proj_matrix: Mat4, screen_height: f32) -> f32 {
4274    let proj_center = proj_matrix.project_point3(center);
4275    let proj_edge   = proj_matrix.project_point3(center + Vec3::new(radius, 0.0, 0.0));
4276    let screen_radius = (proj_center - proj_edge).length() * screen_height * 0.5;
4277    screen_radius.abs()
4278}
4279
4280pub fn lerp_vec3(a: Vec3, b: Vec3, t: f32) -> Vec3 {
4281    a + (b - a) * t
4282}
4283
4284pub fn smooth_step(edge0: f32, edge1: f32, x: f32) -> f32 {
4285    let t = ((x - edge0) / (edge1 - edge0)).clamp(0.0, 1.0);
4286    t * t * (3.0 - 2.0 * t)
4287}
4288
4289pub fn smoother_step(edge0: f32, edge1: f32, x: f32) -> f32 {
4290    let t = ((x - edge0) / (edge1 - edge0)).clamp(0.0, 1.0);
4291    t * t * t * (t * (t * 6.0 - 15.0) + 10.0)
4292}
4293
4294pub fn project_point_to_plane(point: Vec3, plane_normal: Vec3, plane_d: f32) -> Vec3 {
4295    let dist = plane_normal.dot(point) + plane_d;
4296    point - plane_normal * dist
4297}
4298
4299pub fn distance_point_to_line(point: Vec3, line_origin: Vec3, line_dir: Vec3) -> f32 {
4300    let v = point - line_origin;
4301    let d = v - line_dir * v.dot(line_dir);
4302    d.length()
4303}
4304
4305pub fn closest_point_on_segment(point: Vec3, a: Vec3, b: Vec3) -> Vec3 {
4306    let ab = b - a;
4307    let len_sq = ab.length_squared();
4308    if len_sq < 1e-12 { return a; }
4309    let t = ((point - a).dot(ab) / len_sq).clamp(0.0, 1.0);
4310    a + ab * t
4311}
4312
4313pub fn triangle_area(a: Vec3, b: Vec3, c: Vec3) -> f32 {
4314    (b - a).cross(c - a).length() * 0.5
4315}
4316
4317pub fn triangle_normal(a: Vec3, b: Vec3, c: Vec3) -> Vec3 {
4318    (b - a).cross(c - a).normalize()
4319}
4320
4321pub fn barycentric_coords(point: Vec3, a: Vec3, b: Vec3, c: Vec3) -> Vec3 {
4322    let v0 = b - a;
4323    let v1 = c - a;
4324    let v2 = point - a;
4325    let d00 = v0.dot(v0);
4326    let d01 = v0.dot(v1);
4327    let d11 = v1.dot(v1);
4328    let d20 = v2.dot(v0);
4329    let d21 = v2.dot(v1);
4330    let denom = d00 * d11 - d01 * d01;
4331    if denom.abs() < 1e-12 { return Vec3::new(1.0, 0.0, 0.0); }
4332    let v = (d11 * d20 - d01 * d21) / denom;
4333    let w = (d00 * d21 - d01 * d20) / denom;
4334    Vec3::new(1.0 - v - w, v, w)
4335}
4336
4337pub fn point_in_triangle(point: Vec3, a: Vec3, b: Vec3, c: Vec3) -> bool {
4338    let bary = barycentric_coords(point, a, b, c);
4339    bary.x >= 0.0 && bary.y >= 0.0 && bary.z >= 0.0
4340}
4341
4342pub fn compute_tangent_space(pos0: Vec3, pos1: Vec3, pos2: Vec3, uv0: Vec2, uv1: Vec2, uv2: Vec2) -> (Vec3, Vec3) {
4343    let edge1 = pos1 - pos0;
4344    let edge2 = pos2 - pos0;
4345    let duv1  = uv1 - uv0;
4346    let duv2  = uv2 - uv0;
4347    let denom = duv1.x * duv2.y - duv2.x * duv1.y;
4348    if denom.abs() < 1e-12 {
4349        return (Vec3::X, Vec3::Y);
4350    }
4351    let inv = 1.0 / denom;
4352    let tangent   = (edge1 * duv2.y - edge2 * duv1.y) * inv;
4353    let bitangent = (edge2 * duv1.x - edge1 * duv2.x) * inv;
4354    (tangent.normalize(), bitangent.normalize())
4355}
4356
4357pub fn compute_lod_bias_from_mip(mip: u32, base_mip: u32) -> f32 {
4358    if mip <= base_mip { 0.0 } else { (mip - base_mip) as f32 }
4359}
4360
4361// ============================================================
4362// NOISE UTILITIES (for terrain generation)
4363// ============================================================
4364
4365pub fn hash_2d(x: i32, y: i32) -> u32 {
4366    let mut h = x.wrapping_mul(1234567891i32).wrapping_add(y.wrapping_mul(987654321i32)) as u32;
4367    h ^= h >> 16;
4368    h = h.wrapping_mul(0x45d9f3b);
4369    h ^= h >> 16;
4370    h
4371}
4372
4373pub fn hash_3d(x: i32, y: i32, z: i32) -> u32 {
4374    let mut h = x.wrapping_mul(1234567891i32)
4375        .wrapping_add(y.wrapping_mul(987654321i32))
4376        .wrapping_add(z.wrapping_mul(741852963i32)) as u32;
4377    h ^= h >> 16;
4378    h = h.wrapping_mul(0x45d9f3b);
4379    h ^= h >> 16;
4380    h
4381}
4382
4383pub fn value_noise_2d(x: f32, y: f32) -> f32 {
4384    let ix = x.floor() as i32;
4385    let iy = y.floor() as i32;
4386    let fx = x - ix as f32;
4387    let fy = y - iy as f32;
4388    let ux = smooth_step(0.0, 1.0, fx);
4389    let uy = smooth_step(0.0, 1.0, fy);
4390    let v00 = (hash_2d(ix,     iy    ) as f32) / u32::MAX as f32;
4391    let v10 = (hash_2d(ix + 1, iy    ) as f32) / u32::MAX as f32;
4392    let v01 = (hash_2d(ix,     iy + 1) as f32) / u32::MAX as f32;
4393    let v11 = (hash_2d(ix + 1, iy + 1) as f32) / u32::MAX as f32;
4394    let h0 = v00 * (1.0 - ux) + v10 * ux;
4395    let h1 = v01 * (1.0 - ux) + v11 * ux;
4396    h0 * (1.0 - uy) + h1 * uy
4397}
4398
4399pub fn fbm_noise_2d(x: f32, y: f32, octaves: u32, lacunarity: f32, gain: f32) -> f32 {
4400    let mut sum = 0.0f32;
4401    let mut amplitude = 1.0f32;
4402    let mut frequency = 1.0f32;
4403    let mut max_amp = 0.0f32;
4404    for _ in 0..octaves {
4405        sum += value_noise_2d(x * frequency, y * frequency) * amplitude;
4406        max_amp += amplitude;
4407        amplitude *= gain;
4408        frequency *= lacunarity;
4409    }
4410    if max_amp > 0.0 { sum / max_amp } else { 0.0 }
4411}
4412
4413pub fn generate_heightmap_fbm(
4414    width: usize,
4415    height: usize,
4416    cell_size: f32,
4417    origin: Vec2,
4418    octaves: u32,
4419    scale: f32,
4420    amplitude: f32,
4421) -> TerrainHeightmap {
4422    let mut hm = TerrainHeightmap::new(width, height, cell_size, origin, 1.0);
4423    for z in 0..height {
4424        for x in 0..width {
4425            let wx = (origin.x + x as f32 * cell_size) * scale;
4426            let wz = (origin.y + z as f32 * cell_size) * scale;
4427            let h = fbm_noise_2d(wx, wz, octaves, 2.0, 0.5) * amplitude;
4428            hm.set_height(x, z, h);
4429        }
4430    }
4431    hm
4432}
4433
4434// ============================================================
4435// LOD TRANSITION MANAGER
4436// ============================================================
4437
4438#[derive(Debug, Clone)]
4439pub struct LodTransitionState {
4440    pub coord: ChunkCoord,
4441    pub from_lod: LodLevel,
4442    pub to_lod: LodLevel,
4443    pub blend_alpha: f32,
4444    pub transition_duration_frames: u32,
4445    pub frames_elapsed: u32,
4446}
4447
4448impl LodTransitionState {
4449    pub fn new(coord: ChunkCoord, from: LodLevel, to: LodLevel, duration_frames: u32) -> Self {
4450        Self {
4451            coord,
4452            from_lod: from,
4453            to_lod: to,
4454            blend_alpha: 0.0,
4455            transition_duration_frames: duration_frames,
4456            frames_elapsed: 0,
4457        }
4458    }
4459
4460    pub fn advance(&mut self) -> bool {
4461        self.frames_elapsed += 1;
4462        self.blend_alpha = if self.transition_duration_frames > 0 {
4463            (self.frames_elapsed as f32 / self.transition_duration_frames as f32).clamp(0.0, 1.0)
4464        } else {
4465            1.0
4466        };
4467        self.blend_alpha >= 1.0
4468    }
4469
4470    pub fn is_complete(&self) -> bool {
4471        self.blend_alpha >= 1.0
4472    }
4473
4474    pub fn smooth_alpha(&self) -> f32 {
4475        smooth_step(0.0, 1.0, self.blend_alpha)
4476    }
4477}
4478
4479#[derive(Debug, Clone)]
4480pub struct LodTransitionManager {
4481    pub active_transitions: HashMap<ChunkCoord, LodTransitionState>,
4482    pub transition_duration_frames: u32,
4483    pub enable_smooth_transitions: bool,
4484}
4485
4486impl LodTransitionManager {
4487    pub fn new(duration_frames: u32, smooth: bool) -> Self {
4488        Self {
4489            active_transitions: HashMap::new(),
4490            transition_duration_frames: duration_frames,
4491            enable_smooth_transitions: smooth,
4492        }
4493    }
4494
4495    pub fn begin_transition(&mut self, coord: ChunkCoord, from: LodLevel, to: LodLevel) {
4496        let duration = if self.enable_smooth_transitions { self.transition_duration_frames } else { 0 };
4497        let state = LodTransitionState::new(coord.clone(), from, to, duration);
4498        self.active_transitions.insert(coord, state);
4499    }
4500
4501    pub fn tick(&mut self) -> Vec<ChunkCoord> {
4502        let mut completed = Vec::new();
4503        for (coord, state) in self.active_transitions.iter_mut() {
4504            if state.advance() {
4505                completed.push(coord.clone());
4506            }
4507        }
4508        for c in &completed {
4509            self.active_transitions.remove(c);
4510        }
4511        completed
4512    }
4513
4514    pub fn get_blend_alpha(&self, coord: &ChunkCoord) -> f32 {
4515        self.active_transitions.get(coord).map(|s| s.smooth_alpha()).unwrap_or(1.0)
4516    }
4517
4518    pub fn is_transitioning(&self, coord: &ChunkCoord) -> bool {
4519        self.active_transitions.contains_key(coord)
4520    }
4521
4522    pub fn active_count(&self) -> usize {
4523        self.active_transitions.len()
4524    }
4525
4526    pub fn cancel_transition(&mut self, coord: &ChunkCoord) {
4527        self.active_transitions.remove(coord);
4528    }
4529}
4530
4531// ============================================================
4532// VISIBILITY GRID (broad-phase spatial hash)
4533// ============================================================
4534
4535#[derive(Debug, Clone)]
4536pub struct VisibilityGrid {
4537    pub cell_size: f32,
4538    pub cells: HashMap<(i32, i32, i32), Vec<u32>>,
4539    pub object_cells: HashMap<u32, (i32, i32, i32)>,
4540}
4541
4542impl VisibilityGrid {
4543    pub fn new(cell_size: f32) -> Self {
4544        Self {
4545            cell_size,
4546            cells: HashMap::new(),
4547            object_cells: HashMap::new(),
4548        }
4549    }
4550
4551    pub fn insert(&mut self, id: u32, pos: Vec3) {
4552        let cell = self.pos_to_cell(pos);
4553        self.cells.entry(cell).or_insert_with(Vec::new).push(id);
4554        self.object_cells.insert(id, cell);
4555    }
4556
4557    pub fn remove(&mut self, id: u32) {
4558        if let Some(cell) = self.object_cells.remove(&id) {
4559            if let Some(ids) = self.cells.get_mut(&cell) {
4560                ids.retain(|&i| i != id);
4561            }
4562        }
4563    }
4564
4565    pub fn update(&mut self, id: u32, new_pos: Vec3) {
4566        let new_cell = self.pos_to_cell(new_pos);
4567        if let Some(old_cell) = self.object_cells.get(&id).cloned() {
4568            if old_cell == new_cell { return; }
4569            if let Some(ids) = self.cells.get_mut(&old_cell) {
4570                ids.retain(|&i| i != id);
4571            }
4572        }
4573        self.cells.entry(new_cell).or_insert_with(Vec::new).push(id);
4574        self.object_cells.insert(id, new_cell);
4575    }
4576
4577    pub fn query_radius(&self, center: Vec3, radius: f32) -> Vec<u32> {
4578        let cells = self.cells_in_radius(center, radius);
4579        let mut result = Vec::new();
4580        for cell in cells {
4581            if let Some(ids) = self.cells.get(&cell) {
4582                result.extend_from_slice(ids);
4583            }
4584        }
4585        result
4586    }
4587
4588    pub fn query_aabb(&self, bounds: &ChunkBounds) -> Vec<u32> {
4589        let cells = self.cells_in_aabb(bounds);
4590        let mut result = Vec::new();
4591        for cell in cells {
4592            if let Some(ids) = self.cells.get(&cell) {
4593                result.extend_from_slice(ids);
4594            }
4595        }
4596        result
4597    }
4598
4599    fn pos_to_cell(&self, pos: Vec3) -> (i32, i32, i32) {
4600        (
4601            (pos.x / self.cell_size).floor() as i32,
4602            (pos.y / self.cell_size).floor() as i32,
4603            (pos.z / self.cell_size).floor() as i32,
4604        )
4605    }
4606
4607    fn cells_in_radius(&self, center: Vec3, radius: f32) -> Vec<(i32, i32, i32)> {
4608        let cr = (radius / self.cell_size).ceil() as i32;
4609        let cc = self.pos_to_cell(center);
4610        let mut cells = Vec::new();
4611        for dx in -cr..=cr {
4612            for dy in -cr..=cr {
4613                for dz in -cr..=cr {
4614                    cells.push((cc.0 + dx, cc.1 + dy, cc.2 + dz));
4615                }
4616            }
4617        }
4618        cells
4619    }
4620
4621    fn cells_in_aabb(&self, bounds: &ChunkBounds) -> Vec<(i32, i32, i32)> {
4622        let min_c = self.pos_to_cell(bounds.min);
4623        let max_c = self.pos_to_cell(bounds.max);
4624        let mut cells = Vec::new();
4625        for cx in min_c.0..=max_c.0 {
4626            for cy in min_c.1..=max_c.1 {
4627                for cz in min_c.2..=max_c.2 {
4628                    cells.push((cx, cy, cz));
4629                }
4630            }
4631        }
4632        cells
4633    }
4634
4635    pub fn cell_count(&self) -> usize {
4636        self.cells.len()
4637    }
4638
4639    pub fn object_count(&self) -> usize {
4640        self.object_cells.len()
4641    }
4642}
4643
4644// ============================================================
4645// SCREEN SPACE ERROR METRIC
4646// ============================================================
4647
4648#[derive(Debug, Clone)]
4649pub struct ScreenSpaceErrorMetric {
4650    pub screen_height_pixels: f32,
4651    pub fov_vertical_rad: f32,
4652    pub error_threshold: f32,
4653}
4654
4655impl ScreenSpaceErrorMetric {
4656    pub fn new(screen_height: f32, fov_rad: f32, threshold: f32) -> Self {
4657        Self {
4658            screen_height_pixels: screen_height,
4659            fov_vertical_rad: fov_rad,
4660            error_threshold: threshold,
4661        }
4662    }
4663
4664    pub fn project_error(&self, world_error: f32, distance: f32) -> f32 {
4665        if distance < 1e-6 { return f32::MAX; }
4666        let half_fov_tan = (self.fov_vertical_rad * 0.5).tan();
4667        (world_error / distance) / half_fov_tan * self.screen_height_pixels
4668    }
4669
4670    pub fn needs_refinement(&self, world_error: f32, distance: f32) -> bool {
4671        self.project_error(world_error, distance) > self.error_threshold
4672    }
4673
4674    pub fn select_lod_for_bounds(&self, bounds: &ChunkBounds, camera_pos: Vec3) -> LodLevel {
4675        let dist = bounds.distance_to_point(camera_pos);
4676        let radius = bounds.half_size().length();
4677        let screen_size = self.project_error(radius, dist.max(0.001));
4678        if screen_size > 512.0       { LodLevel::Ultra   }
4679        else if screen_size > 256.0  { LodLevel::High    }
4680        else if screen_size > 64.0   { LodLevel::Medium  }
4681        else if screen_size > 16.0   { LodLevel::Low     }
4682        else if screen_size > 2.0    { LodLevel::Impostor}
4683        else                         { LodLevel::Unloaded}
4684    }
4685
4686    pub fn blend_factor(&self, world_error: f32, distance: f32) -> f32 {
4687        let sse = self.project_error(world_error, distance);
4688        let near = self.error_threshold * 0.5;
4689        let far  = self.error_threshold * 2.0;
4690        smooth_step(near, far, sse)
4691    }
4692
4693    pub fn max_error_for_distance(&self, distance: f32) -> f32 {
4694        let half_fov_tan = (self.fov_vertical_rad * 0.5).tan();
4695        self.error_threshold * distance * half_fov_tan / self.screen_height_pixels
4696    }
4697}
4698
4699// ============================================================
4700// CHUNK POOL
4701// ============================================================
4702
4703#[derive(Debug, Clone)]
4704pub struct ChunkPool {
4705    pub free_chunks: VecDeque<StreamingChunk>,
4706    pub allocated_count: usize,
4707    pub chunk_size: f32,
4708    pub pool_capacity: usize,
4709}
4710
4711impl ChunkPool {
4712    pub fn new(capacity: usize, chunk_size: f32) -> Self {
4713        Self {
4714            free_chunks: VecDeque::with_capacity(capacity),
4715            allocated_count: 0,
4716            chunk_size,
4717            pool_capacity: capacity,
4718        }
4719    }
4720
4721    pub fn pre_allocate(&mut self, count: usize) {
4722        for i in 0..count.min(self.pool_capacity) {
4723            let coord = ChunkCoord::new(i as i32, 0, 0);
4724            self.free_chunks.push_back(StreamingChunk::new(coord, self.chunk_size));
4725        }
4726    }
4727
4728    pub fn acquire(&mut self, coord: ChunkCoord) -> StreamingChunk {
4729        if let Some(mut chunk) = self.free_chunks.pop_front() {
4730            chunk.coord = coord.clone();
4731            chunk.bounds = ChunkBounds::from_chunk_coord(&coord, self.chunk_size);
4732            chunk.lod_level = LodLevel::Unloaded;
4733            chunk.load_state = ChunkLoadState::Unloaded;
4734            chunk.resident_objects.clear();
4735            chunk.memory_bytes = 0;
4736            chunk.is_visible = false;
4737            chunk.flags = ChunkFlags::default();
4738            self.allocated_count += 1;
4739            chunk
4740        } else {
4741            self.allocated_count += 1;
4742            StreamingChunk::new(coord, self.chunk_size)
4743        }
4744    }
4745
4746    pub fn release(&mut self, chunk: StreamingChunk) {
4747        if self.free_chunks.len() < self.pool_capacity {
4748            self.free_chunks.push_back(chunk);
4749        }
4750        if self.allocated_count > 0 { self.allocated_count -= 1; }
4751    }
4752
4753    pub fn free_count(&self) -> usize {
4754        self.free_chunks.len()
4755    }
4756
4757    pub fn allocated_count(&self) -> usize {
4758        self.allocated_count
4759    }
4760}
4761
4762// ============================================================
4763// STREAMING MESH SIMPLIFIER
4764// ============================================================
4765
4766#[derive(Debug, Clone)]
4767pub struct MeshVertex {
4768    pub position: Vec3,
4769    pub normal: Vec3,
4770    pub uv: Vec2,
4771}
4772
4773#[derive(Debug, Clone)]
4774pub struct SimplifiedMesh {
4775    pub vertices: Vec<MeshVertex>,
4776    pub indices: Vec<u32>,
4777    pub lod_level: LodLevel,
4778    pub error_metric: f32,
4779}
4780
4781impl SimplifiedMesh {
4782    pub fn new(lod: LodLevel) -> Self {
4783        Self {
4784            vertices: Vec::new(),
4785            indices: Vec::new(),
4786            lod_level: lod,
4787            error_metric: 0.0,
4788        }
4789    }
4790
4791    pub fn vertex_count(&self) -> usize {
4792        self.vertices.len()
4793    }
4794
4795    pub fn triangle_count(&self) -> usize {
4796        self.indices.len() / 3
4797    }
4798
4799    pub fn memory_bytes(&self) -> usize {
4800        // Each vertex: 3+3+2 = 8 floats = 32 bytes; each index: 4 bytes
4801        self.vertices.len() * 32 + self.indices.len() * 4
4802    }
4803
4804    pub fn compute_bounds(&self) -> ChunkBounds {
4805        if self.vertices.is_empty() {
4806            return ChunkBounds::new(Vec3::ZERO, Vec3::ZERO);
4807        }
4808        let mut min = Vec3::splat(f32::MAX);
4809        let mut max = Vec3::splat(f32::MIN);
4810        for v in &self.vertices {
4811            min = min.min(v.position);
4812            max = max.max(v.position);
4813        }
4814        ChunkBounds { min, max }
4815    }
4816
4817    pub fn recompute_normals(&mut self) {
4818        let n = self.vertices.len();
4819        let mut normals = vec![Vec3::ZERO; n];
4820        for tri in self.indices.chunks_exact(3) {
4821            let (i0, i1, i2) = (tri[0] as usize, tri[1] as usize, tri[2] as usize);
4822            if i0 >= n || i1 >= n || i2 >= n { continue; }
4823            let p0 = self.vertices[i0].position;
4824            let p1 = self.vertices[i1].position;
4825            let p2 = self.vertices[i2].position;
4826            let normal = triangle_normal(p0, p1, p2);
4827            normals[i0] += normal;
4828            normals[i1] += normal;
4829            normals[i2] += normal;
4830        }
4831        for (i, v) in self.vertices.iter_mut().enumerate() {
4832            let len = normals[i].length();
4833            if len > NORMAL_SMOOTH_EPSILON {
4834                v.normal = normals[i] / len;
4835            }
4836        }
4837    }
4838}
4839
4840// ============================================================
4841// STREAMING EVENT SYSTEM
4842// ============================================================
4843
4844#[derive(Debug, Clone)]
4845pub enum StreamingEvent {
4846    ChunkLoaded { coord: ChunkCoord, lod: LodLevel },
4847    ChunkUnloaded { coord: ChunkCoord },
4848    LodChanged { coord: ChunkCoord, from: LodLevel, to: LodLevel },
4849    MemoryWarning { used_mb: f32, budget_mb: f32 },
4850    LoadQueueFull { size: usize },
4851    ActorLoaded { actor_id: u32, lod: LodLevel },
4852    ActorUnloaded { actor_id: u32 },
4853    HlodActivated { cluster_id: u32 },
4854    HlodDeactivated { cluster_id: u32 },
4855    VirtualTextureEviction { tile_count: u32 },
4856}
4857
4858#[derive(Debug, Clone)]
4859pub struct StreamingEventQueue {
4860    pub events: VecDeque<StreamingEvent>,
4861    pub max_events: usize,
4862    pub total_events_enqueued: u64,
4863    pub total_events_dequeued: u64,
4864}
4865
4866impl StreamingEventQueue {
4867    pub fn new(max_events: usize) -> Self {
4868        Self {
4869            events: VecDeque::with_capacity(max_events),
4870            max_events,
4871            total_events_enqueued: 0,
4872            total_events_dequeued: 0,
4873        }
4874    }
4875
4876    pub fn push(&mut self, event: StreamingEvent) {
4877        if self.events.len() >= self.max_events {
4878            self.events.pop_front();
4879        }
4880        self.events.push_back(event);
4881        self.total_events_enqueued += 1;
4882    }
4883
4884    pub fn drain(&mut self) -> Vec<StreamingEvent> {
4885        let count = self.events.len();
4886        self.total_events_dequeued += count as u64;
4887        self.events.drain(..).collect()
4888    }
4889
4890    pub fn peek(&self) -> Option<&StreamingEvent> {
4891        self.events.front()
4892    }
4893
4894    pub fn len(&self) -> usize {
4895        self.events.len()
4896    }
4897
4898    pub fn is_empty(&self) -> bool {
4899        self.events.is_empty()
4900    }
4901
4902    pub fn count_by_type(&self, discriminant: &str) -> usize {
4903        self.events.iter().filter(|e| {
4904            match (e, discriminant) {
4905                (StreamingEvent::ChunkLoaded { .. }, "ChunkLoaded") => true,
4906                (StreamingEvent::ChunkUnloaded { .. }, "ChunkUnloaded") => true,
4907                (StreamingEvent::LodChanged { .. }, "LodChanged") => true,
4908                _ => false,
4909            }
4910        }).count()
4911    }
4912}
4913
4914// ============================================================
4915// SCENE GRAPH NODE
4916// ============================================================
4917
4918#[derive(Debug, Clone)]
4919pub struct SceneNode {
4920    pub id: u32,
4921    pub local_transform: Mat4,
4922    pub world_transform: Mat4,
4923    pub bounds: ChunkBounds,
4924    pub children: Vec<u32>,
4925    pub parent: Option<u32>,
4926    pub lod_level: LodLevel,
4927    pub is_visible: bool,
4928    pub is_static: bool,
4929    pub chunk_coord: Option<ChunkCoord>,
4930}
4931
4932impl SceneNode {
4933    pub fn new(id: u32) -> Self {
4934        Self {
4935            id,
4936            local_transform: Mat4::IDENTITY,
4937            world_transform: Mat4::IDENTITY,
4938            bounds: ChunkBounds::new(Vec3::ZERO, Vec3::ONE),
4939            children: Vec::new(),
4940            parent: None,
4941            lod_level: LodLevel::Unloaded,
4942            is_visible: true,
4943            is_static: true,
4944            chunk_coord: None,
4945        }
4946    }
4947
4948    pub fn set_translation(&mut self, pos: Vec3) {
4949        let (scale, rot, _) = self.local_transform.to_scale_rotation_translation();
4950        self.local_transform = Mat4::from_scale_rotation_translation(scale, rot, pos);
4951    }
4952
4953    pub fn set_rotation(&mut self, rot: Quat) {
4954        let (scale, _, pos) = self.local_transform.to_scale_rotation_translation();
4955        self.local_transform = Mat4::from_scale_rotation_translation(scale, rot, pos);
4956    }
4957
4958    pub fn set_scale(&mut self, scale: Vec3) {
4959        let (_, rot, pos) = self.local_transform.to_scale_rotation_translation();
4960        self.local_transform = Mat4::from_scale_rotation_translation(scale, rot, pos);
4961    }
4962
4963    pub fn update_world_transform(&mut self, parent_world: Mat4) {
4964        self.world_transform = parent_world * self.local_transform;
4965        let world_bounds = self.bounds.transformed_by(self.world_transform);
4966        self.bounds = world_bounds;
4967    }
4968
4969    pub fn world_position(&self) -> Vec3 {
4970        let (_, _, pos) = self.world_transform.to_scale_rotation_translation();
4971        pos
4972    }
4973
4974    pub fn world_forward(&self) -> Vec3 {
4975        let rot = Quat::from_mat4(&self.world_transform);
4976        rot * Vec3::NEG_Z
4977    }
4978}
4979
4980#[derive(Debug, Clone)]
4981pub struct SceneGraph {
4982    pub nodes: HashMap<u32, SceneNode>,
4983    pub root_nodes: Vec<u32>,
4984    pub next_id: u32,
4985    pub dirty_nodes: HashSet<u32>,
4986}
4987
4988impl SceneGraph {
4989    pub fn new() -> Self {
4990        Self {
4991            nodes: HashMap::new(),
4992            root_nodes: Vec::new(),
4993            next_id: 1,
4994            dirty_nodes: HashSet::new(),
4995        }
4996    }
4997
4998    pub fn create_node(&mut self) -> u32 {
4999        let id = self.next_id;
5000        self.next_id += 1;
5001        self.nodes.insert(id, SceneNode::new(id));
5002        self.root_nodes.push(id);
5003        id
5004    }
5005
5006    pub fn attach_child(&mut self, parent: u32, child: u32) {
5007        if let Some(p) = self.nodes.get_mut(&parent) {
5008            if !p.children.contains(&child) {
5009                p.children.push(child);
5010            }
5011        }
5012        if let Some(c) = self.nodes.get_mut(&child) {
5013            c.parent = Some(parent);
5014        }
5015        self.root_nodes.retain(|&id| id != child);
5016        self.dirty_nodes.insert(child);
5017    }
5018
5019    pub fn detach(&mut self, node_id: u32) {
5020        if let Some(parent_id) = self.nodes.get(&node_id).and_then(|n| n.parent) {
5021            if let Some(p) = self.nodes.get_mut(&parent_id) {
5022                p.children.retain(|&id| id != node_id);
5023            }
5024        }
5025        if let Some(n) = self.nodes.get_mut(&node_id) {
5026            n.parent = None;
5027        }
5028        self.root_nodes.push(node_id);
5029        self.dirty_nodes.insert(node_id);
5030    }
5031
5032    pub fn update_transforms(&mut self) {
5033        let roots: Vec<u32> = self.root_nodes.clone();
5034        for root in roots {
5035            self.update_subtree(root, Mat4::IDENTITY);
5036        }
5037        self.dirty_nodes.clear();
5038    }
5039
5040    fn update_subtree(&mut self, node_id: u32, parent_world: Mat4) {
5041        let world = {
5042            if let Some(node) = self.nodes.get_mut(&node_id) {
5043                node.world_transform = parent_world * node.local_transform;
5044                node.world_transform
5045            } else {
5046                return;
5047            }
5048        };
5049        let children: Vec<u32> = self.nodes.get(&node_id).map(|n| n.children.clone()).unwrap_or_default();
5050        for child in children {
5051            self.update_subtree(child, world);
5052        }
5053    }
5054
5055    pub fn mark_dirty(&mut self, node_id: u32) {
5056        self.dirty_nodes.insert(node_id);
5057    }
5058
5059    pub fn node_count(&self) -> usize {
5060        self.nodes.len()
5061    }
5062}
5063
5064// ============================================================
5065// STREAMING MESH LOD MANAGER
5066// ============================================================
5067
5068#[derive(Debug, Clone)]
5069pub struct MeshLodManager {
5070    pub mesh_lods: HashMap<u32, Vec<SimplifiedMesh>>,
5071    pub screen_space_metric: ScreenSpaceErrorMetric,
5072    pub transition_manager: LodTransitionManager,
5073    pub current_lods: HashMap<u32, usize>,
5074}
5075
5076impl MeshLodManager {
5077    pub fn new(screen_height: f32, fov_rad: f32) -> Self {
5078        Self {
5079            mesh_lods: HashMap::new(),
5080            screen_space_metric: ScreenSpaceErrorMetric::new(screen_height, fov_rad, SCREEN_SPACE_ERROR_THRESHOLD),
5081            transition_manager: LodTransitionManager::new(4, true),
5082            current_lods: HashMap::new(),
5083        }
5084    }
5085
5086    pub fn register_mesh(&mut self, mesh_id: u32, lods: Vec<SimplifiedMesh>) {
5087        self.current_lods.insert(mesh_id, lods.len().saturating_sub(1));
5088        self.mesh_lods.insert(mesh_id, lods);
5089    }
5090
5091    pub fn update_lod(&mut self, mesh_id: u32, camera_pos: Vec3) {
5092        let lods = match self.mesh_lods.get(&mesh_id) {
5093            Some(l) => l.clone(),
5094            None => return,
5095        };
5096        if lods.is_empty() { return; }
5097        let bounds = lods[0].compute_bounds();
5098        let desired_lod = self.screen_space_metric.select_lod_for_bounds(&bounds, camera_pos);
5099        let desired_idx = desired_lod.index().min(lods.len() - 1);
5100        let current_idx = *self.current_lods.get(&mesh_id).unwrap_or(&0);
5101        if desired_idx != current_idx {
5102            self.transition_manager.begin_transition(
5103                ChunkCoord::new(mesh_id as i32, 0, 0),
5104                LodLevel::from_index(current_idx),
5105                LodLevel::from_index(desired_idx),
5106            );
5107            self.current_lods.insert(mesh_id, desired_idx);
5108        }
5109    }
5110
5111    pub fn get_current_lod_mesh(&self, mesh_id: u32) -> Option<&SimplifiedMesh> {
5112        let lods = self.mesh_lods.get(&mesh_id)?;
5113        let idx = *self.current_lods.get(&mesh_id)?;
5114        lods.get(idx)
5115    }
5116
5117    pub fn tick(&mut self) {
5118        self.transition_manager.tick();
5119    }
5120
5121    pub fn total_triangle_count(&self) -> usize {
5122        self.mesh_lods.values()
5123            .filter_map(|lods| {
5124                let idx = 0; // default
5125                lods.get(idx).map(|m| m.triangle_count())
5126            })
5127            .sum()
5128    }
5129
5130    pub fn memory_usage_bytes(&self) -> usize {
5131        self.mesh_lods.values()
5132            .flat_map(|lods| lods.iter())
5133            .map(|m| m.memory_bytes())
5134            .sum()
5135    }
5136}
5137
5138// ============================================================
5139// CLUSTER GRID
5140// ============================================================
5141
5142#[derive(Debug, Clone)]
5143pub struct ClusterGrid {
5144    pub cell_size: f32,
5145    pub clusters: HashMap<(i32, i32), Vec<u32>>,
5146    pub actor_cluster_map: HashMap<u32, u32>,
5147    pub cluster_bounds: HashMap<u32, ChunkBounds>,
5148    pub next_cluster_id: u32,
5149}
5150
5151impl ClusterGrid {
5152    pub fn new(cell_size: f32) -> Self {
5153        Self {
5154            cell_size,
5155            clusters: HashMap::new(),
5156            actor_cluster_map: HashMap::new(),
5157            cluster_bounds: HashMap::new(),
5158            next_cluster_id: 1,
5159        }
5160    }
5161
5162    pub fn cell_for_pos(&self, pos: Vec3) -> (i32, i32) {
5163        (
5164            (pos.x / self.cell_size).floor() as i32,
5165            (pos.z / self.cell_size).floor() as i32,
5166        )
5167    }
5168
5169    pub fn insert_actor(&mut self, actor_id: u32, pos: Vec3, bounds: ChunkBounds) {
5170        let cell = self.cell_for_pos(pos);
5171        let cluster_id = if let Some(&existing) = self.clusters.get(&cell).and_then(|v| v.first()) {
5172            existing
5173        } else {
5174            let id = self.next_cluster_id;
5175            self.next_cluster_id += 1;
5176            self.clusters.entry(cell).or_insert_with(Vec::new).push(id);
5177            id
5178        };
5179        self.actor_cluster_map.insert(actor_id, cluster_id);
5180        let cb = self.cluster_bounds.entry(cluster_id).or_insert_with(|| bounds.clone());
5181        *cb = cb.merge(&bounds);
5182    }
5183
5184    pub fn get_cluster_for_actor(&self, actor_id: u32) -> Option<u32> {
5185        self.actor_cluster_map.get(&actor_id).cloned()
5186    }
5187
5188    pub fn actors_in_cell(&self, cell: (i32, i32)) -> Vec<u32> {
5189        self.clusters.get(&cell)
5190            .map(|cluster_ids| {
5191                cluster_ids.iter()
5192                    .flat_map(|cid| {
5193                        self.actor_cluster_map.iter()
5194                            .filter(|(_, &c)| c == *cid)
5195                            .map(|(&a, _)| a)
5196                    })
5197                    .collect()
5198            })
5199            .unwrap_or_default()
5200    }
5201
5202    pub fn cells_in_radius(&self, center: Vec3, radius: f32) -> Vec<(i32, i32)> {
5203        let cr = (radius / self.cell_size).ceil() as i32;
5204        let cc = self.cell_for_pos(center);
5205        let mut cells = Vec::new();
5206        for dx in -cr..=cr {
5207            for dz in -cr..=cr {
5208                let cx = cc.0 + dx;
5209                let cz = cc.1 + dz;
5210                let world_x = cx as f32 * self.cell_size + self.cell_size * 0.5;
5211                let world_z = cz as f32 * self.cell_size + self.cell_size * 0.5;
5212                let cell_center = Vec3::new(world_x, center.y, world_z);
5213                if (cell_center - center).length() <= radius + self.cell_size * std::f32::consts::SQRT_2 * 0.5 {
5214                    cells.push((cx, cz));
5215                }
5216            }
5217        }
5218        cells
5219    }
5220
5221    pub fn cluster_count(&self) -> usize {
5222        self.cluster_bounds.len()
5223    }
5224}
5225
5226// ============================================================
5227// SPATIAL QUERY SYSTEM
5228// ============================================================
5229
5230#[derive(Debug, Clone)]
5231pub struct SpatialQueryResult {
5232    pub object_id: u32,
5233    pub distance: f32,
5234    pub intersection_point: Vec3,
5235    pub normal: Vec3,
5236}
5237
5238#[derive(Debug, Clone)]
5239pub struct SpatialQuerySystem {
5240    pub bvh: Option<BvhNode>,
5241    pub octree: Option<OctreeNode>,
5242    pub visibility_grid: VisibilityGrid,
5243    pub object_bounds: HashMap<u32, ChunkBounds>,
5244}
5245
5246impl SpatialQuerySystem {
5247    pub fn new(grid_cell_size: f32) -> Self {
5248        Self {
5249            bvh: None,
5250            octree: None,
5251            visibility_grid: VisibilityGrid::new(grid_cell_size),
5252            object_bounds: HashMap::new(),
5253        }
5254    }
5255
5256    pub fn register_object(&mut self, id: u32, pos: Vec3, bounds: ChunkBounds) {
5257        self.visibility_grid.insert(id, pos);
5258        self.object_bounds.insert(id, bounds);
5259    }
5260
5261    pub fn unregister_object(&mut self, id: u32) {
5262        self.visibility_grid.remove(id);
5263        self.object_bounds.remove(&id);
5264    }
5265
5266    pub fn raycast(&self, origin: Vec3, direction: Vec3, max_dist: f32) -> Vec<SpatialQueryResult> {
5267        let mut candidates = Vec::new();
5268        if let Some(ref bvh) = self.bvh {
5269            bvh.query_ray(origin, direction, 0.0, max_dist, &mut candidates);
5270        }
5271        let mut results = Vec::new();
5272        for id in candidates {
5273            if let Some(bounds) = self.object_bounds.get(&id) {
5274                if ray_aabb_intersect(origin, direction, bounds, 0.0, max_dist) {
5275                    let closest = bounds.closest_point(origin);
5276                    let dist = (closest - origin).length();
5277                    let normal = (origin - closest).normalize_or_zero();
5278                    results.push(SpatialQueryResult {
5279                        object_id: id,
5280                        distance: dist,
5281                        intersection_point: closest,
5282                        normal,
5283                    });
5284                }
5285            }
5286        }
5287        results.sort_by(|a, b| a.distance.partial_cmp(&b.distance).unwrap_or(std::cmp::Ordering::Equal));
5288        results
5289    }
5290
5291    pub fn overlap_sphere(&self, center: Vec3, radius: f32) -> Vec<u32> {
5292        let candidates = self.visibility_grid.query_radius(center, radius);
5293        candidates.into_iter()
5294            .filter(|id| {
5295                if let Some(bounds) = self.object_bounds.get(id) {
5296                    bounds.distance_to_point(center) <= radius
5297                } else {
5298                    false
5299                }
5300            })
5301            .collect()
5302    }
5303
5304    pub fn overlap_aabb(&self, bounds: &ChunkBounds) -> Vec<u32> {
5305        let candidates = self.visibility_grid.query_aabb(bounds);
5306        candidates.into_iter()
5307            .filter(|id| {
5308                if let Some(ob) = self.object_bounds.get(id) {
5309                    ob.intersects(bounds)
5310                } else {
5311                    false
5312                }
5313            })
5314            .collect()
5315    }
5316
5317    pub fn frustum_query(&self, frustum: &FrustumCulling) -> Vec<u32> {
5318        let mut result = Vec::new();
5319        if let Some(ref bvh) = self.bvh {
5320            bvh.query_frustum(frustum, &mut result);
5321        } else {
5322            for (&id, bounds) in &self.object_bounds {
5323                if frustum.test_aabb_fast(bounds) {
5324                    result.push(id);
5325                }
5326            }
5327        }
5328        result
5329    }
5330
5331    pub fn rebuild_bvh(&mut self) {
5332        let objects: Vec<(u32, ChunkBounds)> = self.object_bounds.iter()
5333            .map(|(&id, b)| (id, b.clone()))
5334            .collect();
5335        let builder = BvhBuilder::new(BVH_MAX_LEAF_OBJECTS, 16);
5336        self.bvh = builder.build(&objects);
5337    }
5338
5339    pub fn rebuild_octree(&mut self) {
5340        let points: Vec<(u32, Vec3)> = self.object_bounds.iter()
5341            .map(|(&id, b)| (id, b.center()))
5342            .collect();
5343        let builder = OctreeBuilder::new(MAX_OCTREE_DEPTH, MAX_OBJECTS_PER_OCTREE_NODE);
5344        self.octree = Some(builder.build(&points));
5345    }
5346}
5347
5348// ============================================================
5349// ADDITIONAL SPATIAL MATH
5350// ============================================================
5351
5352pub fn sphere_vs_sphere(c0: Vec3, r0: f32, c1: Vec3, r1: f32) -> bool {
5353    (c0 - c1).length_squared() <= (r0 + r1) * (r0 + r1)
5354}
5355
5356pub fn capsule_vs_sphere(cap_a: Vec3, cap_b: Vec3, cap_r: f32, sphere_c: Vec3, sphere_r: f32) -> bool {
5357    let closest = closest_point_on_segment(sphere_c, cap_a, cap_b);
5358    (sphere_c - closest).length_squared() <= (cap_r + sphere_r) * (cap_r + sphere_r)
5359}
5360
5361pub fn aabb_vs_sphere(bounds: &ChunkBounds, center: Vec3, radius: f32) -> bool {
5362    bounds.distance_sq_to_point(center) <= radius * radius
5363}
5364
5365pub fn obb_vs_point(center: Vec3, half_extents: Vec3, orientation: Quat, point: Vec3) -> bool {
5366    let local = Quat::conjugate(orientation).mul_vec3(point - center);
5367    local.x.abs() <= half_extents.x
5368        && local.y.abs() <= half_extents.y
5369        && local.z.abs() <= half_extents.z
5370}
5371
5372pub fn compute_aabb_from_obb(center: Vec3, half_extents: Vec3, orientation: Quat) -> ChunkBounds {
5373    let mat = Mat4::from_quat(orientation);
5374    let wx = mat.col(0).truncate() * half_extents.x;
5375    let wy = mat.col(1).truncate() * half_extents.y;
5376    let wz = mat.col(2).truncate() * half_extents.z;
5377    let abs_wx = Vec3::new(wx.x.abs(), wx.y.abs(), wx.z.abs());
5378    let abs_wy = Vec3::new(wy.x.abs(), wy.y.abs(), wy.z.abs());
5379    let abs_wz = Vec3::new(wz.x.abs(), wz.y.abs(), wz.z.abs());
5380    let new_half = abs_wx + abs_wy + abs_wz;
5381    ChunkBounds {
5382        min: center - new_half,
5383        max: center + new_half,
5384    }
5385}
5386
5387pub fn slerp_quat(a: Quat, b: Quat, t: f32) -> Quat {
5388    a.slerp(b, t)
5389}
5390
5391pub fn compute_look_at_quat(forward: Vec3, up: Vec3) -> Quat {
5392    let f = forward.normalize();
5393    let r = up.cross(f).normalize();
5394    let u = f.cross(r).normalize();
5395    Quat::from_mat3(&glam::Mat3::from_cols(r, u, f))
5396}
5397
5398// ============================================================
5399// TERRAIN MATERIAL BLENDING
5400// ============================================================
5401
5402#[derive(Debug, Clone)]
5403pub struct TerrainMaterialLayer {
5404    pub material_id: u32,
5405    pub blend_weight: f32,
5406    pub uv_scale: Vec2,
5407    pub normal_intensity: f32,
5408    pub roughness: f32,
5409    pub metalness: f32,
5410}
5411
5412impl TerrainMaterialLayer {
5413    pub fn new(material_id: u32) -> Self {
5414        Self {
5415            material_id,
5416            blend_weight: 1.0,
5417            uv_scale: Vec2::ONE,
5418            normal_intensity: 1.0,
5419            roughness: 0.8,
5420            metalness: 0.0,
5421        }
5422    }
5423}
5424
5425#[derive(Debug, Clone)]
5426pub struct TerrainMaterialBlender {
5427    pub layers: Vec<TerrainMaterialLayer>,
5428    pub splat_map: Vec<Vec4>,  // RGBA splat weights for up to 4 materials
5429    pub width: usize,
5430    pub height: usize,
5431}
5432
5433impl TerrainMaterialBlender {
5434    pub fn new(width: usize, height: usize) -> Self {
5435        Self {
5436            layers: Vec::new(),
5437            splat_map: vec![Vec4::new(1.0, 0.0, 0.0, 0.0); width * height],
5438            width,
5439            height,
5440        }
5441    }
5442
5443    pub fn add_layer(&mut self, layer: TerrainMaterialLayer) {
5444        self.layers.push(layer);
5445    }
5446
5447    pub fn set_splat(&mut self, x: usize, z: usize, weights: Vec4) {
5448        if x < self.width && z < self.height {
5449            let normalized = {
5450                let sum = weights.x + weights.y + weights.z + weights.w;
5451                if sum > 1e-8 { weights / sum } else { Vec4::new(1.0, 0.0, 0.0, 0.0) }
5452            };
5453            self.splat_map[z * self.width + x] = normalized;
5454        }
5455    }
5456
5457    pub fn get_splat(&self, x: usize, z: usize) -> Vec4 {
5458        if x < self.width && z < self.height {
5459            self.splat_map[z * self.width + x]
5460        } else {
5461            Vec4::new(1.0, 0.0, 0.0, 0.0)
5462        }
5463    }
5464
5465    pub fn sample_splat_bilinear(&self, fx: f32, fz: f32) -> Vec4 {
5466        let ix = fx.floor() as isize;
5467        let iz = fz.floor() as isize;
5468        let tx = fx - ix as f32;
5469        let tz = fz - iz as f32;
5470        let get = |x: isize, z: isize| -> Vec4 {
5471            let cx = x.clamp(0, self.width as isize - 1) as usize;
5472            let cz = z.clamp(0, self.height as isize - 1) as usize;
5473            self.splat_map[cz * self.width + cx]
5474        };
5475        let v00 = get(ix,     iz    );
5476        let v10 = get(ix + 1, iz    );
5477        let v01 = get(ix,     iz + 1);
5478        let v11 = get(ix + 1, iz + 1);
5479        let h0 = v00 * (1.0 - tx) + v10 * tx;
5480        let h1 = v01 * (1.0 - tx) + v11 * tx;
5481        h0 * (1.0 - tz) + h1 * tz
5482    }
5483
5484    pub fn blend_roughness(&self, splat: Vec4) -> f32 {
5485        let mut result = 0.0f32;
5486        for (i, layer) in self.layers.iter().enumerate().take(4) {
5487            let w = match i { 0 => splat.x, 1 => splat.y, 2 => splat.z, _ => splat.w };
5488            result += layer.roughness * w;
5489        }
5490        result
5491    }
5492
5493    pub fn blend_normal_intensity(&self, splat: Vec4) -> f32 {
5494        let mut result = 0.0f32;
5495        for (i, layer) in self.layers.iter().enumerate().take(4) {
5496            let w = match i { 0 => splat.x, 1 => splat.y, 2 => splat.z, _ => splat.w };
5497            result += layer.normal_intensity * w;
5498        }
5499        result
5500    }
5501
5502    pub fn dominant_material_at(&self, x: usize, z: usize) -> u32 {
5503        let splat = self.get_splat(x, z);
5504        let weights = [splat.x, splat.y, splat.z, splat.w];
5505        let max_idx = weights.iter().enumerate().max_by(|a, b| a.1.partial_cmp(b.1).unwrap()).map(|(i, _)| i).unwrap_or(0);
5506        self.layers.get(max_idx).map(|l| l.material_id).unwrap_or(0)
5507    }
5508}
5509
5510// ============================================================
5511// CHUNK NEIGHBOR MANAGER
5512// ============================================================
5513
5514#[derive(Debug, Clone)]
5515pub struct ChunkNeighborManager {
5516    pub neighbor_cache: HashMap<ChunkCoord, [Option<ChunkCoord>; 6]>,
5517    pub loaded_set: HashSet<ChunkCoord>,
5518}
5519
5520impl ChunkNeighborManager {
5521    pub fn new() -> Self {
5522        Self {
5523            neighbor_cache: HashMap::new(),
5524            loaded_set: HashSet::new(),
5525        }
5526    }
5527
5528    pub fn register_loaded(&mut self, coord: ChunkCoord) {
5529        self.loaded_set.insert(coord);
5530    }
5531
5532    pub fn unregister(&mut self, coord: &ChunkCoord) {
5533        self.loaded_set.remove(coord);
5534        self.neighbor_cache.remove(coord);
5535    }
5536
5537    pub fn get_neighbors(&mut self, coord: &ChunkCoord) -> [Option<ChunkCoord>; 6] {
5538        if let Some(cached) = self.neighbor_cache.get(coord) {
5539            return *cached;
5540        }
5541        let n6 = coord.neighbors_6();
5542        let result: [Option<ChunkCoord>; 6] = std::array::from_fn(|i| {
5543            if self.loaded_set.contains(&n6[i]) { Some(n6[i].clone()) } else { None }
5544        });
5545        self.neighbor_cache.insert(coord.clone(), result);
5546        result
5547    }
5548
5549    pub fn all_neighbors_loaded(&self, coord: &ChunkCoord) -> bool {
5550        coord.neighbors_6().iter().all(|n| self.loaded_set.contains(n))
5551    }
5552
5553    pub fn loaded_neighbor_count(&self, coord: &ChunkCoord) -> usize {
5554        coord.neighbors_6().iter().filter(|n| self.loaded_set.contains(n)).count()
5555    }
5556
5557    pub fn needs_seam_update(&self, coord: &ChunkCoord, other: &ChunkCoord) -> bool {
5558        coord.is_adjacent(other) && self.loaded_set.contains(coord) && self.loaded_set.contains(other)
5559    }
5560
5561    pub fn invalidate_cache_for(&mut self, coord: &ChunkCoord) {
5562        self.neighbor_cache.remove(coord);
5563        for neighbor in coord.neighbors_26() {
5564            self.neighbor_cache.remove(&neighbor);
5565        }
5566    }
5567}
5568
5569// ============================================================
5570// PREFETCH SYSTEM
5571// ============================================================
5572
5573#[derive(Debug, Clone)]
5574pub struct PrefetchSystem {
5575    pub velocity_buffer: VecDeque<Vec3>,
5576    pub velocity_history: usize,
5577    pub prefetch_distance: f32,
5578    pub prefetch_angle_deg: f32,
5579    pub predicted_position: Vec3,
5580    pub predicted_forward: Vec3,
5581    pub confidence: f32,
5582}
5583
5584impl PrefetchSystem {
5585    pub fn new(history: usize, prefetch_dist: f32) -> Self {
5586        Self {
5587            velocity_buffer: VecDeque::with_capacity(history),
5588            velocity_history: history,
5589            prefetch_distance: prefetch_dist,
5590            prefetch_angle_deg: 90.0,
5591            predicted_position: Vec3::ZERO,
5592            predicted_forward: Vec3::NEG_Z,
5593            confidence: 0.0,
5594        }
5595    }
5596
5597    pub fn update(&mut self, current_pos: Vec3, current_forward: Vec3, dt: f32) {
5598        if dt < 1e-6 { return; }
5599        if let Some(&prev_pos) = self.velocity_buffer.back() {
5600            let vel = (current_pos - prev_pos) / dt;
5601            self.velocity_buffer.push_back(vel);
5602        } else {
5603            self.velocity_buffer.push_back(Vec3::ZERO);
5604        }
5605        // Actually push current position as sample
5606        self.velocity_buffer.push_back(current_pos);
5607        while self.velocity_buffer.len() > self.velocity_history * 2 {
5608            self.velocity_buffer.pop_front();
5609        }
5610        self.compute_prediction(current_pos, current_forward, dt);
5611    }
5612
5613    fn compute_prediction(&mut self, pos: Vec3, forward: Vec3, dt: f32) {
5614        // Exponential moving average of velocity
5615        let n = self.velocity_buffer.len();
5616        if n < 2 {
5617            self.predicted_position = pos + forward * self.prefetch_distance;
5618            self.predicted_forward = forward;
5619            self.confidence = 0.1;
5620            return;
5621        }
5622        let mut avg_vel = Vec3::ZERO;
5623        let mut weight_sum = 0.0f32;
5624        let samples: Vec<_> = self.velocity_buffer.iter().cloned().collect();
5625        for i in 0..samples.len().saturating_sub(1) {
5626            let w = (i + 1) as f32;
5627            avg_vel += samples[i] * w;
5628            weight_sum += w;
5629        }
5630        if weight_sum > 0.0 { avg_vel /= weight_sum; }
5631        let speed = avg_vel.length();
5632        self.confidence = (speed * 0.1).clamp(0.0, 1.0);
5633        let lookahead_time = self.prefetch_distance / speed.max(1.0);
5634        self.predicted_position = pos + avg_vel * lookahead_time;
5635        self.predicted_forward = if speed > 0.01 { avg_vel.normalize() } else { forward };
5636    }
5637
5638    pub fn chunks_to_prefetch(&self, chunk_size: f32, extra_radius_chunks: i32) -> Vec<ChunkCoord> {
5639        let center = ChunkCoord::from_world_pos(self.predicted_position, chunk_size);
5640        ChunkCoord::chunks_in_radius(&center, extra_radius_chunks)
5641    }
5642
5643    pub fn should_prefetch(&self, coord: &ChunkCoord, chunk_size: f32) -> bool {
5644        if self.confidence < 0.3 { return false; }
5645        let center = coord.to_world_center(chunk_size);
5646        let to_chunk = (center - self.predicted_position).normalize_or_zero();
5647        let angle_cos = self.predicted_forward.dot(to_chunk);
5648        angle_cos >= (self.prefetch_angle_deg.to_radians() * 0.5).cos()
5649    }
5650}
5651
5652// ============================================================
5653// LEVEL STREAMING VOLUME
5654// ============================================================
5655
5656#[derive(Debug, Clone)]
5657pub struct LevelStreamingVolume {
5658    pub id: u32,
5659    pub bounds: ChunkBounds,
5660    pub trigger_on_enter: bool,
5661    pub trigger_on_exit: bool,
5662    pub associated_chunks: Vec<ChunkCoord>,
5663    pub load_distance_override: Option<f32>,
5664    pub priority: f32,
5665    pub is_active: bool,
5666    pub last_state: bool,
5667}
5668
5669impl LevelStreamingVolume {
5670    pub fn new(id: u32, bounds: ChunkBounds) -> Self {
5671        Self {
5672            id,
5673            bounds,
5674            trigger_on_enter: true,
5675            trigger_on_exit: false,
5676            associated_chunks: Vec::new(),
5677            load_distance_override: None,
5678            priority: 1.0,
5679            is_active: false,
5680            last_state: false,
5681        }
5682    }
5683
5684    pub fn check_viewer(&mut self, viewer_pos: Vec3) -> Option<bool> {
5685        let inside = self.bounds.contains(viewer_pos);
5686        if inside != self.last_state {
5687            self.last_state = inside;
5688            Some(inside)
5689        } else {
5690            None
5691        }
5692    }
5693
5694    pub fn effective_load_distance(&self, base_distance: f32) -> f32 {
5695        self.load_distance_override.unwrap_or(base_distance) * self.priority
5696    }
5697
5698    pub fn contains_viewer(&self, viewer_pos: Vec3) -> bool {
5699        self.bounds.contains(viewer_pos)
5700    }
5701
5702    pub fn distance_to_viewer(&self, viewer_pos: Vec3) -> f32 {
5703        self.bounds.distance_to_point(viewer_pos)
5704    }
5705}
5706
5707#[derive(Debug, Clone)]
5708pub struct LevelStreamingVolumeManager {
5709    pub volumes: HashMap<u32, LevelStreamingVolume>,
5710    pub next_id: u32,
5711    pub active_volume_ids: HashSet<u32>,
5712}
5713
5714impl LevelStreamingVolumeManager {
5715    pub fn new() -> Self {
5716        Self {
5717            volumes: HashMap::new(),
5718            next_id: 1,
5719            active_volume_ids: HashSet::new(),
5720        }
5721    }
5722
5723    pub fn add_volume(&mut self, bounds: ChunkBounds) -> u32 {
5724        let id = self.next_id;
5725        self.next_id += 1;
5726        self.volumes.insert(id, LevelStreamingVolume::new(id, bounds));
5727        id
5728    }
5729
5730    pub fn remove_volume(&mut self, id: u32) {
5731        self.volumes.remove(&id);
5732        self.active_volume_ids.remove(&id);
5733    }
5734
5735    pub fn update_viewer(&mut self, viewer_pos: Vec3) -> Vec<(u32, bool)> {
5736        let mut events = Vec::new();
5737        for (id, volume) in self.volumes.iter_mut() {
5738            if let Some(entered) = volume.check_viewer(viewer_pos) {
5739                if entered {
5740                    self.active_volume_ids.insert(*id);
5741                } else {
5742                    self.active_volume_ids.remove(id);
5743                }
5744                events.push((*id, entered));
5745            }
5746        }
5747        events
5748    }
5749
5750    pub fn chunks_to_force_load(&self) -> Vec<(ChunkCoord, f32)> {
5751        let mut result = Vec::new();
5752        for id in &self.active_volume_ids {
5753            if let Some(vol) = self.volumes.get(id) {
5754                for coord in &vol.associated_chunks {
5755                    result.push((coord.clone(), vol.priority));
5756                }
5757            }
5758        }
5759        result
5760    }
5761
5762    pub fn active_volume_count(&self) -> usize {
5763        self.active_volume_ids.len()
5764    }
5765}
5766
5767// ============================================================
5768// RUNTIME STATISTICS
5769// ============================================================
5770
5771#[derive(Debug, Clone)]
5772pub struct RuntimeHistogram {
5773    pub buckets: Vec<u64>,
5774    pub min_val: f32,
5775    pub max_val: f32,
5776    pub total_samples: u64,
5777    pub sum: f64,
5778}
5779
5780impl RuntimeHistogram {
5781    pub fn new(num_buckets: usize, min_val: f32, max_val: f32) -> Self {
5782        Self {
5783            buckets: vec![0u64; num_buckets],
5784            min_val,
5785            max_val,
5786            total_samples: 0,
5787            sum: 0.0,
5788        }
5789    }
5790
5791    pub fn record(&mut self, value: f32) {
5792        let n = self.buckets.len();
5793        let range = self.max_val - self.min_val;
5794        if range <= 0.0 { return; }
5795        let idx = (((value - self.min_val) / range) * n as f32) as usize;
5796        let clamped = idx.min(n - 1);
5797        self.buckets[clamped] += 1;
5798        self.total_samples += 1;
5799        self.sum += value as f64;
5800    }
5801
5802    pub fn mean(&self) -> f64 {
5803        if self.total_samples == 0 { return 0.0; }
5804        self.sum / self.total_samples as f64
5805    }
5806
5807    pub fn percentile(&self, pct: f32) -> f32 {
5808        if self.total_samples == 0 { return self.min_val; }
5809        let target = (pct / 100.0 * self.total_samples as f32) as u64;
5810        let mut cumulative = 0u64;
5811        let n = self.buckets.len();
5812        let range = self.max_val - self.min_val;
5813        for (i, &count) in self.buckets.iter().enumerate() {
5814            cumulative += count;
5815            if cumulative >= target {
5816                return self.min_val + (i as f32 / n as f32) * range;
5817            }
5818        }
5819        self.max_val
5820    }
5821
5822    pub fn reset(&mut self) {
5823        for b in &mut self.buckets { *b = 0; }
5824        self.total_samples = 0;
5825        self.sum = 0.0;
5826    }
5827
5828    pub fn mode_bucket(&self) -> usize {
5829        self.buckets.iter().enumerate().max_by_key(|(_, &c)| c).map(|(i, _)| i).unwrap_or(0)
5830    }
5831
5832    pub fn mode_value(&self) -> f32 {
5833        let n = self.buckets.len();
5834        let range = self.max_val - self.min_val;
5835        self.min_val + (self.mode_bucket() as f32 / n as f32) * range
5836    }
5837}
5838
5839// ============================================================
5840// CHUNK SERIALIZATION HELPERS
5841// ============================================================
5842
5843#[derive(Debug, Clone)]
5844pub struct ChunkHeader {
5845    pub magic: u32,
5846    pub version: u32,
5847    pub coord: ChunkCoord,
5848    pub data_size_bytes: u64,
5849    pub lod_count: u32,
5850    pub has_terrain: bool,
5851    pub has_collision: bool,
5852    pub has_nav: bool,
5853    pub object_count: u32,
5854    pub checksum: u32,
5855}
5856
5857impl ChunkHeader {
5858    pub const MAGIC: u32 = 0x43484E4B; // 'CHNK'
5859    pub const VERSION: u32 = 1;
5860
5861    pub fn new(coord: ChunkCoord) -> Self {
5862        Self {
5863            magic: Self::MAGIC,
5864            version: Self::VERSION,
5865            coord,
5866            data_size_bytes: 0,
5867            lod_count: 0,
5868            has_terrain: false,
5869            has_collision: false,
5870            has_nav: false,
5871            object_count: 0,
5872            checksum: 0,
5873        }
5874    }
5875
5876    pub fn is_valid(&self) -> bool {
5877        self.magic == Self::MAGIC && self.version <= Self::VERSION
5878    }
5879
5880    pub fn compute_checksum(&self) -> u32 {
5881        let packed = self.coord.pack_u64();
5882        let mut h = packed as u32;
5883        h ^= self.data_size_bytes as u32;
5884        h = h.wrapping_mul(0x9e3779b9);
5885        h ^= self.object_count;
5886        h = h.rotate_left(13);
5887        h
5888    }
5889
5890    pub fn validate_checksum(&self) -> bool {
5891        self.checksum == self.compute_checksum()
5892    }
5893
5894    pub fn finalize(&mut self) {
5895        self.checksum = self.compute_checksum();
5896    }
5897}
5898
5899// ============================================================
5900// RENDERING STATE
5901// ============================================================
5902
5903#[derive(Debug, Clone)]
5904pub struct RenderVisibilityState {
5905    pub visible_chunks: Vec<ChunkCoord>,
5906    pub impostor_chunks: Vec<ChunkCoord>,
5907    pub hlod_clusters: Vec<u32>,
5908    pub total_triangles: u64,
5909    pub total_draw_calls: u32,
5910    pub culled_by_frustum: u32,
5911    pub culled_by_distance: u32,
5912    pub culled_by_occlusion: u32,
5913}
5914
5915impl RenderVisibilityState {
5916    pub fn new() -> Self {
5917        Self {
5918            visible_chunks: Vec::new(),
5919            impostor_chunks: Vec::new(),
5920            hlod_clusters: Vec::new(),
5921            total_triangles: 0,
5922            total_draw_calls: 0,
5923            culled_by_frustum: 0,
5924            culled_by_distance: 0,
5925            culled_by_occlusion: 0,
5926        }
5927    }
5928
5929    pub fn reset(&mut self) {
5930        self.visible_chunks.clear();
5931        self.impostor_chunks.clear();
5932        self.hlod_clusters.clear();
5933        self.total_triangles = 0;
5934        self.total_draw_calls = 0;
5935        self.culled_by_frustum = 0;
5936        self.culled_by_distance = 0;
5937        self.culled_by_occlusion = 0;
5938    }
5939
5940    pub fn total_culled(&self) -> u32 {
5941        self.culled_by_frustum + self.culled_by_distance + self.culled_by_occlusion
5942    }
5943
5944    pub fn visibility_ratio(&self, total_chunks: u32) -> f32 {
5945        if total_chunks == 0 { return 0.0; }
5946        self.visible_chunks.len() as f32 / total_chunks as f32
5947    }
5948}
5949
5950// ============================================================
5951// FINAL INTEGRATION / TESTS
5952// ============================================================
5953
5954pub fn create_default_editor() -> WorldStreamingEditor {
5955    WorldStreamingEditor::new(StreamingConfig::default())
5956}
5957
5958pub fn create_editor_with_preset(preset: QualityPreset) -> WorldStreamingEditor {
5959    WorldStreamingEditor::new(StreamingConfig::with_quality_preset(preset))
5960}
5961
5962pub fn build_test_world(editor: &mut WorldStreamingEditor, actor_count: u32, world_size: f32) {
5963    for i in 0..actor_count {
5964        let angle = (i as f32 / actor_count as f32) * std::f32::consts::TAU;
5965        let radius = (i as f32 / actor_count as f32) * world_size * 0.5;
5966        let x = angle.cos() * radius;
5967        let z = angle.sin() * radius;
5968        let y = value_noise_2d(x * 0.01, z * 0.01) * 50.0;
5969        let pos = Vec3::new(x, y, z);
5970        let half = Vec3::splat(5.0 + (i % 10) as f32);
5971        let bounds = ChunkBounds::from_center_size(pos, half);
5972        let dist = 200.0 + (i % 5) as f32 * 100.0;
5973        editor.register_actor(i, pos, bounds, dist);
5974    }
5975    editor.build_octree();
5976    editor.build_bvh();
5977}
5978
5979pub fn run_streaming_simulation(editor: &mut WorldStreamingEditor, frames: u32, path: &[(Vec3, Vec3)]) {
5980    let step = if path.is_empty() { 0 } else { (frames as usize / path.len()).max(1) };
5981    for frame in 0..frames {
5982        let path_idx = (frame as usize / step).min(path.len().saturating_sub(1));
5983        let (pos, fwd) = if path.is_empty() { (Vec3::ZERO, Vec3::NEG_Z) } else { path[path_idx] };
5984        editor.set_viewer(pos, fwd);
5985        editor.tick(1.0 / 60.0, frame as u64 * 16667);
5986    }
5987}
5988
5989pub fn compute_streaming_coverage(editor: &WorldStreamingEditor) -> f32 {
5990    let loaded = editor.loaded_chunk_count();
5991    let total = editor.chunk_count();
5992    if total == 0 { return 0.0; }
5993    loaded as f32 / total as f32
5994}
5995
5996pub fn debug_print_lod_distribution(editor: &WorldStreamingEditor) -> HashMap<LodLevel, usize> {
5997    let mut dist: HashMap<LodLevel, usize> = HashMap::new();
5998    for chunk in editor.chunks.values() {
5999        *dist.entry(chunk.lod_level.clone()).or_insert(0) += 1;
6000    }
6001    dist
6002}
6003
6004pub fn compute_world_bounds_from_chunks(editor: &WorldStreamingEditor) -> Option<ChunkBounds> {
6005    let mut all_bounds: Option<ChunkBounds> = None;
6006    for chunk in editor.chunks.values() {
6007        all_bounds = Some(match all_bounds {
6008            None => chunk.bounds.clone(),
6009            Some(b) => b.merge(&chunk.bounds),
6010        });
6011    }
6012    all_bounds
6013}
6014
6015pub fn find_nearest_loaded_chunk(editor: &WorldStreamingEditor, pos: Vec3) -> Option<ChunkCoord> {
6016    editor.chunks.iter()
6017        .filter(|(_, c)| c.load_state == ChunkLoadState::Loaded)
6018        .min_by(|(_, a), (_, b)| {
6019            let da = a.bounds.distance_sq_to_point(pos);
6020            let db = b.bounds.distance_sq_to_point(pos);
6021            da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
6022        })
6023        .map(|(coord, _)| coord.clone())
6024}
6025
6026pub fn recompute_all_chunk_priorities(editor: &mut WorldStreamingEditor) {
6027    let viewer = editor.viewer_position;
6028    let forward = editor.viewer_forward;
6029    let coords: Vec<ChunkCoord> = editor.chunks.keys().cloned().collect();
6030    for coord in coords {
6031        if let Some(chunk) = editor.chunks.get_mut(&coord) {
6032            chunk.compute_load_priority(viewer, forward);
6033        }
6034    }
6035}
6036
6037pub fn estimate_total_world_memory(editor: &WorldStreamingEditor) -> u64 {
6038    editor.chunks.values()
6039        .filter(|c| c.load_state == ChunkLoadState::Loaded)
6040        .map(|c| c.memory_bytes)
6041        .sum()
6042}
6043
6044// ============================================================
6045// UNIT-STYLE VALIDATION FUNCTIONS
6046// ============================================================
6047
6048pub fn validate_chunk_bounds(bounds: &ChunkBounds) -> bool {
6049    !bounds.is_degenerate()
6050        && bounds.volume() > 0.0
6051        && bounds.surface_area() > 0.0
6052        && bounds.center() == (bounds.min + bounds.max) * 0.5
6053}
6054
6055pub fn validate_frustum_planes(planes: &[Vec4; 6]) -> bool {
6056    for plane in planes {
6057        let n = Vec3::new(plane.x, plane.y, plane.z);
6058        let len = n.length();
6059        if (len - 1.0).abs() > 0.01 { return false; }
6060    }
6061    true
6062}
6063
6064pub fn validate_bvh(node: &BvhNode) -> bool {
6065    if node.is_leaf() { return true; }
6066    let ok_left  = node.left.as_ref().map(|l| l.bounds.intersects(&node.bounds) && validate_bvh(l)).unwrap_or(true);
6067    let ok_right = node.right.as_ref().map(|r| r.bounds.intersects(&node.bounds) && validate_bvh(r)).unwrap_or(true);
6068    ok_left && ok_right
6069}
6070
6071pub fn validate_lod_transitions(from: &LodLevel, to: &LodLevel) -> bool {
6072    // Transitions of more than 1 step are allowed but should be flagged
6073    let diff = (from.index() as isize - to.index() as isize).abs();
6074    diff <= 2
6075}
6076
6077pub fn stress_test_octree(count: u32, world_size: f32) -> (OctreeNode, usize) {
6078    let mut points = Vec::with_capacity(count as usize);
6079    for i in 0..count {
6080        let angle = (i as f32 / count as f32) * std::f32::consts::TAU;
6081        let r = (i as f32 / count as f32) * world_size * 0.5;
6082        let h = value_noise_2d(angle, r * 0.01) * 50.0;
6083        let pos = Vec3::new(angle.cos() * r, h, angle.sin() * r);
6084        points.push((i, pos));
6085    }
6086    let builder = OctreeBuilder::new(MAX_OCTREE_DEPTH, MAX_OBJECTS_PER_OCTREE_NODE);
6087    let tree = builder.build(&points);
6088    let node_count = tree.node_count();
6089    (tree, node_count)
6090}
6091
6092pub fn stress_test_bvh(count: u32, world_size: f32) -> (Option<BvhNode>, usize) {
6093    let mut objects = Vec::with_capacity(count as usize);
6094    for i in 0..count {
6095        let angle = (i as f32 / count as f32) * std::f32::consts::TAU;
6096        let r = (i as f32 / count as f32) * world_size * 0.5;
6097        let center = Vec3::new(angle.cos() * r, 0.0, angle.sin() * r);
6098        let half = Vec3::splat(5.0);
6099        let bounds = ChunkBounds::from_center_size(center, half);
6100        objects.push((i, bounds));
6101    }
6102    let builder = BvhBuilder::new(BVH_MAX_LEAF_OBJECTS, 16);
6103    let bvh = builder.build(&objects);
6104    let node_count = bvh.as_ref().map(|b| b.node_count()).unwrap_or(0);
6105    (bvh, node_count)
6106}
6107
6108pub fn benchmark_frustum_cull(
6109    frustum: &FrustumCulling,
6110    bounds_list: &[ChunkBounds],
6111) -> (usize, usize) {
6112    let mut visible = 0;
6113    let mut culled = 0;
6114    for bounds in bounds_list {
6115        if frustum.test_aabb_fast(bounds) {
6116            visible += 1;
6117        } else {
6118            culled += 1;
6119        }
6120    }
6121    (visible, culled)
6122}
6123
6124pub fn generate_grid_bounds(cols: usize, rows: usize, cell_size: f32) -> Vec<(ChunkCoord, ChunkBounds)> {
6125    let mut result = Vec::with_capacity(cols * rows);
6126    for z in 0..rows as i32 {
6127        for x in 0..cols as i32 {
6128            let coord = ChunkCoord::new(x, 0, z);
6129            let bounds = ChunkBounds::from_chunk_coord(&coord, cell_size);
6130            result.push((coord, bounds));
6131        }
6132    }
6133    result
6134}
6135
6136pub fn compute_lod_histogram(editor: &WorldStreamingEditor) -> [u32; 6] {
6137    let mut hist = [0u32; 6];
6138    for chunk in editor.chunks.values() {
6139        hist[chunk.lod_level.index()] += 1;
6140    }
6141    hist
6142}
6143
6144pub fn build_minimal_test_scene() -> WorldStreamingEditor {
6145    let mut editor = create_default_editor();
6146    let camera = StreamingCamera::new(
6147        Vec3::new(0.0, 100.0, 0.0),
6148        Vec3::ZERO,
6149        Vec3::Y,
6150        60.0,
6151        16.0 / 9.0,
6152        0.1,
6153        10000.0,
6154    );
6155    editor.set_camera(camera);
6156    editor.set_viewer(Vec3::new(0.0, 100.0, 0.0), Vec3::NEG_Z);
6157
6158    // Add some test terrain patches
6159    for z in -2i32..=2 {
6160        for x in -2i32..=2 {
6161            let coord = ChunkCoord::new(x, 0, z);
6162            editor.add_terrain_patch(coord, TERRAIN_PATCH_SIZE, 4.0);
6163        }
6164    }
6165
6166    // Build spatial structures
6167    build_test_world(&mut editor, 200, 2048.0);
6168    editor
6169}
6170
6171// ============================================================
6172// IMPORTS USED IN IMPLEMENTATION (re-exports for clarity)
6173// ============================================================
6174
6175// (re-exports removed — imports already at top)
6176
6177// ============================================================
6178// ACTOR IMPORTANCE SYSTEM
6179// ============================================================
6180
6181#[derive(Debug, Clone)]
6182pub struct ActorImportance {
6183    pub actor_id: u32,
6184    pub base_importance: f32,
6185    pub distance_falloff_exponent: f32,
6186    pub is_gameplay_relevant: bool,
6187    pub last_interaction_frame: u64,
6188    pub interaction_boost: f32,
6189    pub tag_boosts: HashMap<String, f32>,
6190}
6191
6192impl ActorImportance {
6193    pub fn new(actor_id: u32, base_importance: f32) -> Self {
6194        Self {
6195            actor_id,
6196            base_importance,
6197            distance_falloff_exponent: 2.0,
6198            is_gameplay_relevant: false,
6199            last_interaction_frame: 0,
6200            interaction_boost: 0.0,
6201            tag_boosts: HashMap::new(),
6202        }
6203    }
6204
6205    pub fn compute_importance(&self, distance: f32, current_frame: u64) -> f32 {
6206        let dist_factor = 1.0 / (1.0 + distance.powf(self.distance_falloff_exponent) * 0.0001);
6207        let gameplay_factor = if self.is_gameplay_relevant { 3.0 } else { 1.0 };
6208        let interaction_decay = {
6209            let age = current_frame.saturating_sub(self.last_interaction_frame) as f32;
6210            self.interaction_boost * (-age * 0.01).exp()
6211        };
6212        let tag_sum: f32 = self.tag_boosts.values().sum();
6213        (self.base_importance + interaction_boost_clamped(interaction_decay) + tag_sum)
6214            * dist_factor
6215            * gameplay_factor
6216    }
6217
6218    pub fn boost_interaction(&mut self, frame: u64, boost: f32) {
6219        self.last_interaction_frame = frame;
6220        self.interaction_boost = (self.interaction_boost + boost).min(10.0);
6221    }
6222
6223    pub fn add_tag_boost(&mut self, tag: String, value: f32) {
6224        *self.tag_boosts.entry(tag).or_insert(0.0) += value;
6225    }
6226
6227    pub fn remove_tag_boost(&mut self, tag: &str) {
6228        self.tag_boosts.remove(tag);
6229    }
6230
6231    pub fn effective_streaming_distance(&self, base_dist: f32, distance: f32, frame: u64) -> f32 {
6232        let importance = self.compute_importance(distance, frame);
6233        base_dist * importance.sqrt().clamp(0.5, 4.0)
6234    }
6235}
6236
6237fn interaction_boost_clamped(v: f32) -> f32 { v.clamp(0.0, 10.0) }
6238
6239// ============================================================
6240// DYNAMIC LOADING BUDGET CONTROLLER
6241// ============================================================
6242
6243#[derive(Debug, Clone)]
6244pub struct LoadingBudgetController {
6245    pub max_loads_per_frame: usize,
6246    pub max_unloads_per_frame: usize,
6247    pub target_frame_time_ms: f32,
6248    pub last_frame_time_ms: f32,
6249    pub smoothed_frame_time_ms: f32,
6250    pub smoothing_alpha: f32,
6251    pub overbudget_scale: f32,
6252    pub underbudget_scale: f32,
6253    pub min_loads: usize,
6254    pub max_loads_cap: usize,
6255}
6256
6257impl LoadingBudgetController {
6258    pub fn new(target_ms: f32, max_loads: usize) -> Self {
6259        Self {
6260            max_loads_per_frame: max_loads,
6261            max_unloads_per_frame: max_loads / 2,
6262            target_frame_time_ms: target_ms,
6263            last_frame_time_ms: target_ms,
6264            smoothed_frame_time_ms: target_ms,
6265            smoothing_alpha: 0.1,
6266            overbudget_scale: 0.7,
6267            underbudget_scale: 1.3,
6268            min_loads: 1,
6269            max_loads_cap: max_loads * 4,
6270        }
6271    }
6272
6273    pub fn update(&mut self, measured_frame_time_ms: f32) {
6274        self.last_frame_time_ms = measured_frame_time_ms;
6275        self.smoothed_frame_time_ms = self.smoothed_frame_time_ms * (1.0 - self.smoothing_alpha)
6276            + measured_frame_time_ms * self.smoothing_alpha;
6277        self.adjust_budget();
6278    }
6279
6280    fn adjust_budget(&mut self) {
6281        let ratio = self.smoothed_frame_time_ms / self.target_frame_time_ms;
6282        if ratio > 1.1 {
6283            // Over budget: reduce loads
6284            let new_max = (self.max_loads_per_frame as f32 * self.overbudget_scale) as usize;
6285            self.max_loads_per_frame = new_max.max(self.min_loads);
6286        } else if ratio < 0.9 {
6287            // Under budget: increase loads
6288            let new_max = (self.max_loads_per_frame as f32 * self.underbudget_scale) as usize;
6289            self.max_loads_per_frame = new_max.min(self.max_loads_cap);
6290        }
6291        self.max_unloads_per_frame = self.max_loads_per_frame / 2;
6292    }
6293
6294    pub fn loads_allowed(&self) -> usize {
6295        self.max_loads_per_frame
6296    }
6297
6298    pub fn unloads_allowed(&self) -> usize {
6299        self.max_unloads_per_frame
6300    }
6301
6302    pub fn is_overbudget(&self) -> bool {
6303        self.smoothed_frame_time_ms > self.target_frame_time_ms * 1.1
6304    }
6305
6306    pub fn headroom_ms(&self) -> f32 {
6307        (self.target_frame_time_ms - self.smoothed_frame_time_ms).max(0.0)
6308    }
6309}
6310
6311// ============================================================
6312// DISTANCE FIELD APPROXIMATION
6313// ============================================================
6314
6315#[derive(Debug, Clone)]
6316pub struct DistanceField2D {
6317    pub width: usize,
6318    pub height: usize,
6319    pub data: Vec<f32>,
6320    pub cell_size: f32,
6321    pub origin: Vec2,
6322}
6323
6324impl DistanceField2D {
6325    pub fn new(width: usize, height: usize, cell_size: f32, origin: Vec2) -> Self {
6326        Self {
6327            width,
6328            height,
6329            data: vec![f32::MAX; width * height],
6330            cell_size,
6331            origin,
6332        }
6333    }
6334
6335    pub fn set(&mut self, x: usize, y: usize, value: f32) {
6336        if x < self.width && y < self.height {
6337            self.data[y * self.width + x] = value;
6338        }
6339    }
6340
6341    pub fn get(&self, x: usize, y: usize) -> f32 {
6342        if x < self.width && y < self.height {
6343            self.data[y * self.width + x]
6344        } else {
6345            f32::MAX
6346        }
6347    }
6348
6349    pub fn sample(&self, world_x: f32, world_y: f32) -> f32 {
6350        let lx = (world_x - self.origin.x) / self.cell_size;
6351        let ly = (world_y - self.origin.y) / self.cell_size;
6352        let ix = lx.floor() as isize;
6353        let iy = ly.floor() as isize;
6354        let fx = lx - ix as f32;
6355        let fy = ly - iy as f32;
6356        let get_c = |x: isize, y: isize| -> f32 {
6357            let cx = x.clamp(0, self.width as isize - 1) as usize;
6358            let cy = y.clamp(0, self.height as isize - 1) as usize;
6359            self.data[cy * self.width + cx]
6360        };
6361        let v00 = get_c(ix,     iy    );
6362        let v10 = get_c(ix + 1, iy    );
6363        let v01 = get_c(ix,     iy + 1);
6364        let v11 = get_c(ix + 1, iy + 1);
6365        let h0 = v00 * (1.0 - fx) + v10 * fx;
6366        let h1 = v01 * (1.0 - fx) + v11 * fx;
6367        h0 * (1.0 - fy) + h1 * fy
6368    }
6369
6370    pub fn compute_from_obstacles(
6371        &mut self,
6372        obstacles: &[(f32, f32)], // world positions
6373    ) {
6374        for z in 0..self.height {
6375            for x in 0..self.width {
6376                let wx = self.origin.x + x as f32 * self.cell_size;
6377                let wy = self.origin.y + z as f32 * self.cell_size;
6378                let min_dist = obstacles.iter()
6379                    .map(|(ox, oy)| ((wx - ox) * (wx - ox) + (wy - oy) * (wy - oy)).sqrt())
6380                    .fold(f32::MAX, f32::min);
6381                self.data[z * self.width + x] = min_dist;
6382            }
6383        }
6384    }
6385
6386    pub fn gradient_at(&self, world_x: f32, world_y: f32) -> Vec2 {
6387        let eps = self.cell_size;
6388        let dx = (self.sample(world_x + eps, world_y) - self.sample(world_x - eps, world_y)) / (2.0 * eps);
6389        let dy = (self.sample(world_x, world_y + eps) - self.sample(world_x, world_y - eps)) / (2.0 * eps);
6390        Vec2::new(dx, dy)
6391    }
6392
6393    pub fn is_inside_obstacle(&self, world_x: f32, world_y: f32, threshold: f32) -> bool {
6394        self.sample(world_x, world_y) < threshold
6395    }
6396
6397    pub fn sweep_pass_horizontal(&mut self) {
6398        let w = self.width;
6399        let h = self.height;
6400        for y in 0..h {
6401            // Forward pass
6402            for x in 1..w {
6403                let prev = self.data[y * w + (x - 1)];
6404                if prev + self.cell_size < self.data[y * w + x] {
6405                    self.data[y * w + x] = prev + self.cell_size;
6406                }
6407            }
6408            // Backward pass
6409            for x in (0..w - 1).rev() {
6410                let next = self.data[y * w + (x + 1)];
6411                if next + self.cell_size < self.data[y * w + x] {
6412                    self.data[y * w + x] = next + self.cell_size;
6413                }
6414            }
6415        }
6416    }
6417
6418    pub fn sweep_pass_vertical(&mut self) {
6419        let w = self.width;
6420        let h = self.height;
6421        for x in 0..w {
6422            for y in 1..h {
6423                let prev = self.data[(y - 1) * w + x];
6424                if prev + self.cell_size < self.data[y * w + x] {
6425                    self.data[y * w + x] = prev + self.cell_size;
6426                }
6427            }
6428            for y in (0..h - 1).rev() {
6429                let next = self.data[(y + 1) * w + x];
6430                if next + self.cell_size < self.data[y * w + x] {
6431                    self.data[y * w + x] = next + self.cell_size;
6432                }
6433            }
6434        }
6435    }
6436
6437    pub fn fast_sweep(&mut self) {
6438        self.sweep_pass_horizontal();
6439        self.sweep_pass_vertical();
6440        self.sweep_pass_horizontal();
6441    }
6442}
6443
6444// ============================================================
6445// RENDER BATCH BUILDER
6446// ============================================================
6447
6448#[derive(Debug, Clone)]
6449pub struct RenderBatch {
6450    pub mesh_id: u32,
6451    pub lod_level: LodLevel,
6452    pub instance_data: Vec<Mat4>,
6453    pub bounds_union: ChunkBounds,
6454    pub material_id: u32,
6455    pub is_impostor: bool,
6456}
6457
6458impl RenderBatch {
6459    pub fn new(mesh_id: u32, lod: LodLevel, material_id: u32) -> Self {
6460        Self {
6461            mesh_id,
6462            lod_level: lod,
6463            instance_data: Vec::new(),
6464            bounds_union: ChunkBounds::new(Vec3::splat(f32::MAX), Vec3::splat(f32::MIN)),
6465            material_id,
6466            is_impostor: false,
6467        }
6468    }
6469
6470    pub fn add_instance(&mut self, transform: Mat4, bounds: &ChunkBounds) {
6471        self.instance_data.push(transform);
6472        self.bounds_union = self.bounds_union.merge(bounds);
6473    }
6474
6475    pub fn instance_count(&self) -> usize {
6476        self.instance_data.len()
6477    }
6478
6479    pub fn is_valid(&self) -> bool {
6480        !self.instance_data.is_empty()
6481    }
6482
6483    pub fn sort_back_to_front(&mut self, camera_pos: Vec3) {
6484        self.instance_data.sort_by(|a, b| {
6485            let pa = a.col(3).truncate();
6486            let pb = b.col(3).truncate();
6487            let da = (pa - camera_pos).length_squared();
6488            let db = (pb - camera_pos).length_squared();
6489            db.partial_cmp(&da).unwrap_or(std::cmp::Ordering::Equal)
6490        });
6491    }
6492
6493    pub fn sort_front_to_back(&mut self, camera_pos: Vec3) {
6494        self.instance_data.sort_by(|a, b| {
6495            let pa = a.col(3).truncate();
6496            let pb = b.col(3).truncate();
6497            let da = (pa - camera_pos).length_squared();
6498            let db = (pb - camera_pos).length_squared();
6499            da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
6500        });
6501    }
6502}
6503
6504#[derive(Debug, Clone)]
6505pub struct RenderBatchBuilder {
6506    pub batches: HashMap<u64, RenderBatch>,
6507    pub max_instances_per_batch: usize,
6508}
6509
6510impl RenderBatchBuilder {
6511    pub fn new(max_instances: usize) -> Self {
6512        Self {
6513            batches: HashMap::new(),
6514            max_instances_per_batch: max_instances,
6515        }
6516    }
6517
6518    pub fn add(&mut self, mesh_id: u32, lod: LodLevel, material_id: u32, transform: Mat4, bounds: &ChunkBounds) {
6519        let key = (mesh_id as u64) | ((material_id as u64) << 32) | ((lod.index() as u64) << 48);
6520        let batch = self.batches.entry(key).or_insert_with(|| RenderBatch::new(mesh_id, lod.clone(), material_id));
6521        if batch.instance_count() < self.max_instances_per_batch {
6522            batch.add_instance(transform, bounds);
6523        } else {
6524            // Overflow: create new batch with modified key
6525            let overflow_key = key ^ ((batch.instance_count() as u64) << 56);
6526            let ob = self.batches.entry(overflow_key).or_insert_with(|| RenderBatch::new(mesh_id, lod.clone(), material_id));
6527            ob.add_instance(transform, bounds);
6528        }
6529    }
6530
6531    pub fn build(&self) -> Vec<&RenderBatch> {
6532        let mut batches: Vec<_> = self.batches.values().filter(|b| b.is_valid()).collect();
6533        // Sort by material to minimize state changes
6534        batches.sort_by_key(|b| b.material_id);
6535        batches
6536    }
6537
6538    pub fn clear(&mut self) {
6539        self.batches.clear();
6540    }
6541
6542    pub fn total_instances(&self) -> usize {
6543        self.batches.values().map(|b| b.instance_count()).sum()
6544    }
6545
6546    pub fn batch_count(&self) -> usize {
6547        self.batches.len()
6548    }
6549}
6550
6551// ============================================================
6552// CHUNK PATCH STITCHER
6553// ============================================================
6554
6555#[derive(Debug, Clone)]
6556pub struct PatchStitcher {
6557    pub blend_region_cells: usize,
6558    pub use_geomorphing: bool,
6559    pub geomorph_distance_range: (f32, f32),
6560}
6561
6562impl PatchStitcher {
6563    pub fn new(blend_cells: usize, geomorph: bool, near: f32, far: f32) -> Self {
6564        Self {
6565            blend_region_cells: blend_cells,
6566            use_geomorphing: geomorph,
6567            geomorph_distance_range: (near, far),
6568        }
6569    }
6570
6571    pub fn compute_geomorph_alpha(&self, distance: f32) -> f32 {
6572        let (near, far) = self.geomorph_distance_range;
6573        smooth_step(near, far, distance)
6574    }
6575
6576    pub fn blend_heights(
6577        &self,
6578        h_fine: f32,
6579        h_coarse: f32,
6580        blend_alpha: f32,
6581    ) -> f32 {
6582        h_fine * (1.0 - blend_alpha) + h_coarse * blend_alpha
6583    }
6584
6585    pub fn compute_skirt_heights(
6586        &self,
6587        edge_heights: &[f32],
6588        skirt_depth: f32,
6589    ) -> Vec<f32> {
6590        edge_heights.iter().map(|&h| h - skirt_depth).collect()
6591    }
6592
6593    pub fn stitch_border(
6594        &self,
6595        patch: &mut TerrainHeightmap,
6596        side: usize,
6597        neighbor: &TerrainHeightmap,
6598        blend_alpha: f32,
6599    ) {
6600        let size = patch.width;
6601        match side {
6602            0 => { // +x
6603                for z in 0..patch.height {
6604                    let my_h    = patch.get_height(size - 1, z);
6605                    let nb_h    = neighbor.get_height(0, z);
6606                    let blended = my_h * (1.0 - blend_alpha) + nb_h * blend_alpha;
6607                    patch.set_height(size - 1, z, blended);
6608                }
6609            }
6610            1 => { // -x
6611                for z in 0..patch.height {
6612                    let my_h    = patch.get_height(0, z);
6613                    let nb_h    = neighbor.get_height(size - 1, z);
6614                    let blended = my_h * (1.0 - blend_alpha) + nb_h * blend_alpha;
6615                    patch.set_height(0, z, blended);
6616                }
6617            }
6618            2 => { // +z
6619                for x in 0..patch.width {
6620                    let my_h    = patch.get_height(x, size - 1);
6621                    let nb_h    = neighbor.get_height(x, 0);
6622                    let blended = my_h * (1.0 - blend_alpha) + nb_h * blend_alpha;
6623                    patch.set_height(x, size - 1, blended);
6624                }
6625            }
6626            3 => { // -z
6627                for x in 0..patch.width {
6628                    let my_h    = patch.get_height(x, 0);
6629                    let nb_h    = neighbor.get_height(x, size - 1);
6630                    let blended = my_h * (1.0 - blend_alpha) + nb_h * blend_alpha;
6631                    patch.set_height(x, 0, blended);
6632                }
6633            }
6634            _ => {}
6635        }
6636    }
6637
6638    pub fn compute_blend_weights_for_row(
6639        &self,
6640        row_len: usize,
6641        is_start: bool,
6642    ) -> Vec<f32> {
6643        let blend_count = self.blend_region_cells.min(row_len);
6644        let mut weights = vec![1.0f32; row_len];
6645        for i in 0..blend_count {
6646            let t = i as f32 / blend_count as f32;
6647            let w = if is_start { t } else { 1.0 - t };
6648            let idx = if is_start { i } else { row_len - 1 - i };
6649            weights[idx] = smooth_step(0.0, 1.0, w);
6650        }
6651        weights
6652    }
6653}
6654
6655// ============================================================
6656// INSTANCE CULLING PIPELINE
6657// ============================================================
6658
6659#[derive(Debug, Clone)]
6660pub struct InstanceCullingPipeline {
6661    pub frustum: FrustumCulling,
6662    pub lod_calculator: StreamingDistanceCalculator,
6663    pub screen_error_metric: ScreenSpaceErrorMetric,
6664    pub max_instances: usize,
6665    pub culled_count: u32,
6666    pub passed_count: u32,
6667}
6668
6669impl InstanceCullingPipeline {
6670    pub fn new(
6671        frustum: FrustumCulling,
6672        screen_h: f32,
6673        fov_rad: f32,
6674        max_instances: usize,
6675    ) -> Self {
6676        Self {
6677            frustum,
6678            lod_calculator: StreamingDistanceCalculator::new(screen_h, fov_rad),
6679            screen_error_metric: ScreenSpaceErrorMetric::new(screen_h, fov_rad, SCREEN_SPACE_ERROR_THRESHOLD),
6680            max_instances,
6681            culled_count: 0,
6682            passed_count: 0,
6683        }
6684    }
6685
6686    pub fn cull_instances(
6687        &mut self,
6688        instances: &[(u32, Mat4, ChunkBounds)],
6689        camera_pos: Vec3,
6690    ) -> Vec<(u32, Mat4, LodLevel)> {
6691        self.culled_count = 0;
6692        self.passed_count = 0;
6693        let mut result = Vec::with_capacity(instances.len());
6694
6695        for (id, transform, bounds) in instances {
6696            if !self.frustum.test_aabb_fast(bounds) {
6697                self.culled_count += 1;
6698                continue;
6699            }
6700            let dist = bounds.distance_to_point(camera_pos);
6701            let lod = self.screen_error_metric.select_lod_for_bounds(bounds, camera_pos);
6702            if lod == LodLevel::Unloaded {
6703                self.culled_count += 1;
6704                continue;
6705            }
6706            result.push((*id, *transform, lod));
6707            self.passed_count += 1;
6708            if result.len() >= self.max_instances { break; }
6709        }
6710        result
6711    }
6712
6713    pub fn cull_ratio(&self) -> f32 {
6714        let total = self.culled_count + self.passed_count;
6715        if total == 0 { return 0.0; }
6716        self.culled_count as f32 / total as f32
6717    }
6718
6719    pub fn update_frustum(&mut self, view_proj: Mat4) {
6720        self.frustum = FrustumCulling::from_view_proj(view_proj);
6721    }
6722
6723    pub fn sort_by_distance_asc(
6724        instances: &mut Vec<(u32, Mat4, LodLevel)>,
6725        camera_pos: Vec3,
6726    ) {
6727        instances.sort_by(|(_, ta, _), (_, tb, _)| {
6728            let da = (ta.col(3).truncate() - camera_pos).length_squared();
6729            let db = (tb.col(3).truncate() - camera_pos).length_squared();
6730            da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
6731        });
6732    }
6733
6734    pub fn group_by_lod(
6735        instances: Vec<(u32, Mat4, LodLevel)>,
6736    ) -> HashMap<usize, Vec<(u32, Mat4)>> {
6737        let mut map: HashMap<usize, Vec<(u32, Mat4)>> = HashMap::new();
6738        for (id, t, lod) in instances {
6739            map.entry(lod.index()).or_insert_with(Vec::new).push((id, t));
6740        }
6741        map
6742    }
6743}
6744
6745// ============================================================
6746// HEIGHTFIELD COLLISION
6747// ============================================================
6748
6749#[derive(Debug, Clone)]
6750pub struct HeightfieldCollision {
6751    pub heightmap: TerrainHeightmap,
6752    pub friction: f32,
6753    pub restitution: f32,
6754    pub layer_mask: u32,
6755}
6756
6757impl HeightfieldCollision {
6758    pub fn new(heightmap: TerrainHeightmap) -> Self {
6759        Self {
6760            heightmap,
6761            friction: 0.7,
6762            restitution: 0.1,
6763            layer_mask: 0xFFFF_FFFF,
6764        }
6765    }
6766
6767    pub fn height_at_world(&self, x: f32, z: f32) -> f32 {
6768        self.heightmap.sample_bilinear(x, z)
6769    }
6770
6771    pub fn normal_at_world(&self, x: f32, z: f32) -> Vec3 {
6772        self.heightmap.compute_normal_bilinear(x, z)
6773    }
6774
6775    pub fn penetration_depth(&self, point: Vec3) -> f32 {
6776        let surface_h = self.height_at_world(point.x, point.z);
6777        (surface_h - point.y).max(0.0)
6778    }
6779
6780    pub fn resolve_sphere(
6781        &self,
6782        center: Vec3,
6783        radius: f32,
6784    ) -> Option<(Vec3, Vec3)> {
6785        let surf_h = self.height_at_world(center.x, center.z);
6786        let pen = surf_h + radius - center.y;
6787        if pen <= 0.0 { return None; }
6788        let normal = self.normal_at_world(center.x, center.z);
6789        let resolved = center + normal * pen;
6790        Some((resolved, normal))
6791    }
6792
6793    pub fn raycast(&self, origin: Vec3, dir: Vec3, max_dist: f32, steps: usize) -> Option<(Vec3, Vec3, f32)> {
6794        let dir_n = dir.normalize();
6795        let step_size = max_dist / steps as f32;
6796        for i in 0..=steps {
6797            let t = i as f32 * step_size;
6798            let p = origin + dir_n * t;
6799            let h = self.height_at_world(p.x, p.z);
6800            if p.y <= h {
6801                let normal = self.normal_at_world(p.x, p.z);
6802                return Some((p, normal, t));
6803            }
6804        }
6805        None
6806    }
6807
6808    pub fn slope_degrees_at(&self, x: f32, z: f32) -> f32 {
6809        self.heightmap.slope_at(x, z)
6810    }
6811
6812    pub fn is_walkable(&self, x: f32, z: f32, max_slope_degrees: f32) -> bool {
6813        self.slope_degrees_at(x, z) <= max_slope_degrees
6814    }
6815
6816    pub fn compute_contact_manifold(
6817        &self,
6818        sphere_center: Vec3,
6819        sphere_radius: f32,
6820        sample_radius: f32,
6821        samples: u32,
6822    ) -> Vec<(Vec3, Vec3, f32)> {
6823        let mut contacts = Vec::new();
6824        for i in 0..samples {
6825            let angle = (i as f32 / samples as f32) * std::f32::consts::TAU;
6826            let sx = sphere_center.x + angle.cos() * sample_radius;
6827            let sz = sphere_center.z + angle.sin() * sample_radius;
6828            let h = self.height_at_world(sx, sz);
6829            let contact_pt = Vec3::new(sx, h, sz);
6830            let pen = (sphere_center.y - sphere_radius) - h;
6831            if pen < 0.0 {
6832                let normal = self.normal_at_world(sx, sz);
6833                contacts.push((contact_pt, normal, pen.abs()));
6834            }
6835        }
6836        contacts
6837    }
6838}
6839
6840// ============================================================
6841// ADAPTIVE LOD CONTROLLER
6842// ============================================================
6843
6844#[derive(Debug, Clone)]
6845pub struct AdaptiveLodController {
6846    pub target_fps: f32,
6847    pub current_fps: f32,
6848    pub global_lod_bias: f32,
6849    pub min_bias: f32,
6850    pub max_bias: f32,
6851    pub adjustment_speed: f32,
6852    pub history: VecDeque<f32>,
6853    pub history_size: usize,
6854    pub hysteresis: f32,
6855}
6856
6857impl AdaptiveLodController {
6858    pub fn new(target_fps: f32) -> Self {
6859        Self {
6860            target_fps,
6861            current_fps: target_fps,
6862            global_lod_bias: 0.0,
6863            min_bias: -1.0,
6864            max_bias: 2.0,
6865            adjustment_speed: 0.05,
6866            history: VecDeque::new(),
6867            history_size: 30,
6868            hysteresis: 5.0,
6869        }
6870    }
6871
6872    pub fn update(&mut self, measured_fps: f32) {
6873        self.current_fps = measured_fps;
6874        self.history.push_back(measured_fps);
6875        if self.history.len() > self.history_size {
6876            self.history.pop_front();
6877        }
6878        let avg_fps = self.history.iter().sum::<f32>() / self.history.len() as f32;
6879        let deficit = self.target_fps - avg_fps;
6880        if deficit > self.hysteresis {
6881            // Need to reduce quality
6882            self.global_lod_bias = (self.global_lod_bias + self.adjustment_speed).min(self.max_bias);
6883        } else if deficit < -self.hysteresis {
6884            // Have headroom, increase quality
6885            self.global_lod_bias = (self.global_lod_bias - self.adjustment_speed).max(self.min_bias);
6886        }
6887    }
6888
6889    pub fn adjusted_lod_distance(&self, base_dist: f32) -> f32 {
6890        base_dist * (1.0 - self.global_lod_bias * 0.2)
6891    }
6892
6893    pub fn adjusted_streaming_radius(&self, base_radius: f32) -> f32 {
6894        base_radius * (1.0 - self.global_lod_bias * 0.15).clamp(0.5, 1.5)
6895    }
6896
6897    pub fn is_struggling(&self) -> bool {
6898        self.current_fps < self.target_fps * 0.8
6899    }
6900
6901    pub fn quality_factor(&self) -> f32 {
6902        (1.0 - self.global_lod_bias / self.max_bias).clamp(0.0, 1.0)
6903    }
6904}
6905
6906// ============================================================
6907// REGION BITMASK
6908// ============================================================
6909
6910#[derive(Debug, Clone, Default)]
6911pub struct RegionBitmask {
6912    pub bits: Vec<u64>,
6913    pub width: usize,
6914    pub height: usize,
6915}
6916
6917impl RegionBitmask {
6918    pub fn new(width: usize, height: usize) -> Self {
6919        let words = (width * height + 63) / 64;
6920        Self { bits: vec![0u64; words], width, height }
6921    }
6922
6923    pub fn set(&mut self, x: usize, y: usize) {
6924        if x < self.width && y < self.height {
6925            let idx = y * self.width + x;
6926            self.bits[idx / 64] |= 1u64 << (idx % 64);
6927        }
6928    }
6929
6930    pub fn clear(&mut self, x: usize, y: usize) {
6931        if x < self.width && y < self.height {
6932            let idx = y * self.width + x;
6933            self.bits[idx / 64] &= !(1u64 << (idx % 64));
6934        }
6935    }
6936
6937    pub fn get(&self, x: usize, y: usize) -> bool {
6938        if x < self.width && y < self.height {
6939            let idx = y * self.width + x;
6940            (self.bits[idx / 64] >> (idx % 64)) & 1 == 1
6941        } else {
6942            false
6943        }
6944    }
6945
6946    pub fn count_set(&self) -> usize {
6947        self.bits.iter().map(|w| w.count_ones() as usize).sum()
6948    }
6949
6950    pub fn or_with(&mut self, other: &RegionBitmask) {
6951        let len = self.bits.len().min(other.bits.len());
6952        for i in 0..len {
6953            self.bits[i] |= other.bits[i];
6954        }
6955    }
6956
6957    pub fn and_with(&mut self, other: &RegionBitmask) {
6958        let len = self.bits.len().min(other.bits.len());
6959        for i in 0..len {
6960            self.bits[i] &= other.bits[i];
6961        }
6962    }
6963
6964    pub fn invert(&mut self) {
6965        for word in &mut self.bits { *word = !*word; }
6966        // Clear extra bits in the last word
6967        let total = self.width * self.height;
6968        let last_bits = total % 64;
6969        if last_bits != 0 {
6970            if let Some(last) = self.bits.last_mut() {
6971                let mask = (1u64 << last_bits) - 1;
6972                *last &= mask;
6973            }
6974        }
6975    }
6976
6977    pub fn flood_fill(&mut self, start_x: usize, start_y: usize) {
6978        let mut stack = vec![(start_x, start_y)];
6979        while let Some((x, y)) = stack.pop() {
6980            if self.get(x, y) { continue; }
6981            self.set(x, y);
6982            if x > 0            { stack.push((x - 1, y)); }
6983            if x + 1 < self.width { stack.push((x + 1, y)); }
6984            if y > 0            { stack.push((x, y - 1)); }
6985            if y + 1 < self.height { stack.push((x, y + 1)); }
6986        }
6987    }
6988}
6989
6990// ============================================================
6991// CHUNK UPDATE SCHEDULER
6992// ============================================================
6993
6994#[derive(Debug, Clone)]
6995pub struct ChunkUpdateScheduler {
6996    pub update_queue: VecDeque<(ChunkCoord, u64)>,  // (coord, scheduled_frame)
6997    pub in_progress: HashSet<ChunkCoord>,
6998    pub completed_this_frame: Vec<ChunkCoord>,
6999    pub max_updates_per_frame: usize,
7000    pub update_interval_frames: u64,
7001    pub next_scheduled_frame: HashMap<ChunkCoord, u64>,
7002}
7003
7004impl ChunkUpdateScheduler {
7005    pub fn new(max_per_frame: usize, interval: u64) -> Self {
7006        Self {
7007            update_queue: VecDeque::new(),
7008            in_progress: HashSet::new(),
7009            completed_this_frame: Vec::new(),
7010            max_updates_per_frame: max_per_frame,
7011            update_interval_frames: interval,
7012            next_scheduled_frame: HashMap::new(),
7013        }
7014    }
7015
7016    pub fn schedule(&mut self, coord: ChunkCoord, current_frame: u64) {
7017        let next = *self.next_scheduled_frame.get(&coord).unwrap_or(&0);
7018        if current_frame >= next && !self.in_progress.contains(&coord) {
7019            self.update_queue.push_back((coord.clone(), current_frame));
7020            self.next_scheduled_frame.insert(coord, current_frame + self.update_interval_frames);
7021        }
7022    }
7023
7024    pub fn dispatch(&mut self, current_frame: u64) -> Vec<ChunkCoord> {
7025        let mut dispatched = Vec::new();
7026        let max = self.max_updates_per_frame;
7027        while dispatched.len() < max {
7028            if let Some((coord, frame)) = self.update_queue.pop_front() {
7029                if current_frame < frame + self.update_interval_frames * 2 {
7030                    self.in_progress.insert(coord.clone());
7031                    dispatched.push(coord);
7032                }
7033            } else {
7034                break;
7035            }
7036        }
7037        dispatched
7038    }
7039
7040    pub fn complete(&mut self, coord: ChunkCoord) {
7041        self.in_progress.remove(&coord);
7042        self.completed_this_frame.push(coord);
7043    }
7044
7045    pub fn end_frame(&mut self) {
7046        self.completed_this_frame.clear();
7047    }
7048
7049    pub fn pending_count(&self) -> usize {
7050        self.update_queue.len()
7051    }
7052
7053    pub fn in_progress_count(&self) -> usize {
7054        self.in_progress.len()
7055    }
7056
7057    pub fn reschedule_all_loaded(&mut self, loaded: &HashSet<ChunkCoord>, frame: u64) {
7058        for coord in loaded {
7059            self.schedule(coord.clone(), frame);
7060        }
7061    }
7062}
7063
7064// ============================================================
7065// IMPOSTOR CAPTURE SYSTEM
7066// ============================================================
7067
7068#[derive(Debug, Clone)]
7069pub struct ImpostorCaptureJob {
7070    pub actor_id: u32,
7071    pub world_bounds: ChunkBounds,
7072    pub num_views: u32,
7073    pub atlas_slot: u32,
7074    pub is_complete: bool,
7075    pub capture_frame: u64,
7076    pub view_directions: Vec<Vec3>,
7077}
7078
7079impl ImpostorCaptureJob {
7080    pub fn new(actor_id: u32, bounds: ChunkBounds, num_views: u32, slot: u32) -> Self {
7081        let view_directions = Self::compute_view_directions(num_views);
7082        Self {
7083            actor_id,
7084            world_bounds: bounds,
7085            num_views,
7086            atlas_slot: slot,
7087            is_complete: false,
7088            capture_frame: 0,
7089            view_directions,
7090        }
7091    }
7092
7093    fn compute_view_directions(num_views: u32) -> Vec<Vec3> {
7094        (0..num_views)
7095            .map(|i| {
7096                let angle = (i as f32 / num_views as f32) * std::f32::consts::TAU;
7097                Vec3::new(angle.cos(), 0.0, angle.sin())
7098            })
7099            .collect()
7100    }
7101
7102    pub fn view_matrix_for_view(&self, view_idx: u32) -> Mat4 {
7103        if view_idx as usize >= self.view_directions.len() {
7104            return Mat4::IDENTITY;
7105        }
7106        let dir = self.view_directions[view_idx as usize];
7107        let center = self.world_bounds.center();
7108        let radius = self.world_bounds.half_size().length() * 2.0;
7109        let eye = center - dir * radius;
7110        Mat4::look_at_rh(eye, center, Vec3::Y)
7111    }
7112
7113    pub fn atlas_uv_for_view(&self, view_idx: u32) -> (Vec2, Vec2) {
7114        let cols = IMPOSTOR_ATLAS_COLS;
7115        let rows = IMPOSTOR_ATLAS_ROWS;
7116        let slot_col = (self.atlas_slot % cols) as f32;
7117        let slot_row = (self.atlas_slot / cols) as f32;
7118        let view_col = (view_idx % cols) as f32;
7119        let view_row = (view_idx / cols) as f32;
7120        let cell_w = 1.0 / cols as f32;
7121        let cell_h = 1.0 / rows as f32;
7122        let _ = (slot_col, slot_row); // suppress unused warning
7123        let uv_min = Vec2::new(view_col * cell_w, view_row * cell_h);
7124        let uv_max = uv_min + Vec2::new(cell_w, cell_h);
7125        (uv_min, uv_max)
7126    }
7127
7128    pub fn mark_complete(&mut self, frame: u64) {
7129        self.is_complete = true;
7130        self.capture_frame = frame;
7131    }
7132}
7133
7134#[derive(Debug, Clone)]
7135pub struct ImpostorCaptureQueue {
7136    pub pending: VecDeque<ImpostorCaptureJob>,
7137    pub in_progress: Option<ImpostorCaptureJob>,
7138    pub next_atlas_slot: u32,
7139    pub max_atlas_slots: u32,
7140    pub completed_jobs: Vec<ImpostorCaptureJob>,
7141}
7142
7143impl ImpostorCaptureQueue {
7144    pub fn new(max_slots: u32) -> Self {
7145        Self {
7146            pending: VecDeque::new(),
7147            in_progress: None,
7148            next_atlas_slot: 0,
7149            max_atlas_slots: max_slots,
7150            completed_jobs: Vec::new(),
7151        }
7152    }
7153
7154    pub fn request_capture(&mut self, actor_id: u32, bounds: ChunkBounds, num_views: u32) -> Option<u32> {
7155        if self.next_atlas_slot >= self.max_atlas_slots { return None; }
7156        let slot = self.next_atlas_slot;
7157        self.next_atlas_slot += 1;
7158        let job = ImpostorCaptureJob::new(actor_id, bounds, num_views, slot);
7159        self.pending.push_back(job);
7160        Some(slot)
7161    }
7162
7163    pub fn tick(&mut self, frame: u64) -> Option<&ImpostorCaptureJob> {
7164        if self.in_progress.is_none() {
7165            self.in_progress = self.pending.pop_front();
7166        }
7167        if let Some(ref mut job) = self.in_progress {
7168            if !job.is_complete {
7169                job.mark_complete(frame);
7170                let completed = self.in_progress.take().unwrap();
7171                self.completed_jobs.push(completed);
7172            }
7173        }
7174        self.completed_jobs.last()
7175    }
7176
7177    pub fn drain_completed(&mut self) -> Vec<ImpostorCaptureJob> {
7178        std::mem::take(&mut self.completed_jobs)
7179    }
7180
7181    pub fn pending_count(&self) -> usize {
7182        self.pending.len()
7183    }
7184}
7185
7186// ============================================================
7187// SECTOR STREAMING MAP
7188// ============================================================
7189
7190#[derive(Debug, Clone)]
7191pub struct SectorStreamingMap {
7192    pub sectors: HashMap<ChunkCoord, SectorInfo>,
7193    pub sector_size_chunks: u32,
7194    pub chunk_size: f32,
7195}
7196
7197#[derive(Debug, Clone)]
7198pub struct SectorInfo {
7199    pub coord: ChunkCoord,
7200    pub chunks: Vec<ChunkCoord>,
7201    pub is_loaded: bool,
7202    pub priority: f32,
7203    pub load_order: u32,
7204    pub memory_estimate_mb: f32,
7205    pub last_resident_frame: u64,
7206}
7207
7208impl SectorInfo {
7209    pub fn new(coord: ChunkCoord, sector_size: u32) -> Self {
7210        let mut chunks = Vec::new();
7211        let size = sector_size as i32;
7212        for dz in 0..size {
7213            for dx in 0..size {
7214                chunks.push(ChunkCoord::new(
7215                    coord.x * size + dx,
7216                    coord.y,
7217                    coord.z * size + dz,
7218                ));
7219            }
7220        }
7221        Self {
7222            coord,
7223            chunks,
7224            is_loaded: false,
7225            priority: 0.0,
7226            load_order: 0,
7227            memory_estimate_mb: sector_size as f32 * sector_size as f32 * 8.0,
7228            last_resident_frame: 0,
7229        }
7230    }
7231
7232    pub fn chunk_count(&self) -> usize {
7233        self.chunks.len()
7234    }
7235}
7236
7237impl SectorStreamingMap {
7238    pub fn new(sector_size_chunks: u32, chunk_size: f32) -> Self {
7239        Self {
7240            sectors: HashMap::new(),
7241            sector_size_chunks,
7242            chunk_size,
7243        }
7244    }
7245
7246    pub fn world_pos_to_sector(&self, pos: Vec3) -> ChunkCoord {
7247        let chunk = ChunkCoord::from_world_pos(pos, self.chunk_size);
7248        let size = self.sector_size_chunks as i32;
7249        ChunkCoord::new(
7250            chunk.x.div_euclid(size),
7251            chunk.y.div_euclid(size),
7252            chunk.z.div_euclid(size),
7253        )
7254    }
7255
7256    pub fn get_or_create_sector(&mut self, coord: ChunkCoord) -> &mut SectorInfo {
7257        let size = self.sector_size_chunks;
7258        self.sectors.entry(coord.clone()).or_insert_with(|| SectorInfo::new(coord, size))
7259    }
7260
7261    pub fn sectors_in_radius(&self, center_pos: Vec3, radius_m: f32) -> Vec<ChunkCoord> {
7262        let sector_size_m = self.sector_size_chunks as f32 * self.chunk_size;
7263        let center_sector = self.world_pos_to_sector(center_pos);
7264        let radius_sectors = (radius_m / sector_size_m).ceil() as i32 + 1;
7265        let mut result = Vec::new();
7266        for dz in -radius_sectors..=radius_sectors {
7267            for dx in -radius_sectors..=radius_sectors {
7268                let coord = center_sector.offset(dx, 0, dz);
7269                let world_center = Vec3::new(
7270                    (coord.x as f32 + 0.5) * sector_size_m,
7271                    center_pos.y,
7272                    (coord.z as f32 + 0.5) * sector_size_m,
7273                );
7274                if (world_center - center_pos).length() <= radius_m + sector_size_m {
7275                    result.push(coord);
7276                }
7277            }
7278        }
7279        result
7280    }
7281
7282    pub fn compute_sector_priorities(&mut self, viewer_pos: Vec3) {
7283        let chunk_size = self.chunk_size;
7284        let sector_size = self.sector_size_chunks;
7285        for (coord, info) in self.sectors.iter_mut() {
7286            let sector_world = Vec3::new(
7287                (coord.x as f32 + 0.5) * sector_size as f32 * chunk_size,
7288                viewer_pos.y,
7289                (coord.z as f32 + 0.5) * sector_size as f32 * chunk_size,
7290            );
7291            let dist = (sector_world - viewer_pos).length();
7292            info.priority = 1.0 / (1.0 + dist * 0.001);
7293        }
7294    }
7295
7296    pub fn loaded_sector_count(&self) -> usize {
7297        self.sectors.values().filter(|s| s.is_loaded).count()
7298    }
7299
7300    pub fn total_sector_count(&self) -> usize {
7301        self.sectors.len()
7302    }
7303
7304    pub fn total_memory_estimate_mb(&self) -> f32 {
7305        self.sectors.values().filter(|s| s.is_loaded).map(|s| s.memory_estimate_mb).sum()
7306    }
7307}
7308
7309// ============================================================
7310// WORLD BOUNDS TRACKER
7311// ============================================================
7312
7313#[derive(Debug, Clone)]
7314pub struct WorldBoundsTracker {
7315    pub world_bounds: ChunkBounds,
7316    pub occupied_cells: HashSet<ChunkCoord>,
7317    pub cell_size: f32,
7318    pub total_actors: u32,
7319    pub dirty: bool,
7320}
7321
7322impl WorldBoundsTracker {
7323    pub fn new(cell_size: f32) -> Self {
7324        Self {
7325            world_bounds: ChunkBounds::new(Vec3::ZERO, Vec3::ZERO),
7326            occupied_cells: HashSet::new(),
7327            cell_size,
7328            total_actors: 0,
7329            dirty: false,
7330        }
7331    }
7332
7333    pub fn register(&mut self, pos: Vec3) {
7334        let coord = ChunkCoord::from_world_pos(pos, self.cell_size);
7335        self.occupied_cells.insert(coord);
7336        self.total_actors += 1;
7337        self.dirty = true;
7338    }
7339
7340    pub fn recompute_bounds(&mut self) {
7341        if !self.dirty { return; }
7342        let mut min = Vec3::splat(f32::MAX);
7343        let mut max = Vec3::splat(f32::MIN);
7344        for coord in &self.occupied_cells {
7345            let cell_min = coord.to_world_min(self.cell_size);
7346            let cell_max = cell_min + Vec3::splat(self.cell_size);
7347            min = min.min(cell_min);
7348            max = max.max(cell_max);
7349        }
7350        if min.x <= max.x {
7351            self.world_bounds = ChunkBounds { min, max };
7352        }
7353        self.dirty = false;
7354    }
7355
7356    pub fn center(&mut self) -> Vec3 {
7357        self.recompute_bounds();
7358        self.world_bounds.center()
7359    }
7360
7361    pub fn extents(&mut self) -> Vec3 {
7362        self.recompute_bounds();
7363        self.world_bounds.size()
7364    }
7365
7366    pub fn is_point_in_world(&self, pos: Vec3) -> bool {
7367        self.world_bounds.contains(pos)
7368    }
7369
7370    pub fn cell_count(&self) -> usize {
7371        self.occupied_cells.len()
7372    }
7373}
7374
7375// ============================================================
7376// STREAMING LEVEL MANAGER
7377// ============================================================
7378
7379#[derive(Debug, Clone)]
7380pub struct StreamingLevel {
7381    pub id: u32,
7382    pub name: String,
7383    pub bounds: ChunkBounds,
7384    pub chunks: Vec<ChunkCoord>,
7385    pub load_state: ChunkLoadState,
7386    pub is_persistent: bool,
7387    pub min_streaming_distance: f32,
7388    pub max_streaming_distance: f32,
7389    pub priority: f32,
7390}
7391
7392impl StreamingLevel {
7393    pub fn new(id: u32, name: String, bounds: ChunkBounds) -> Self {
7394        Self {
7395            id,
7396            name,
7397            bounds,
7398            chunks: Vec::new(),
7399            load_state: ChunkLoadState::Unloaded,
7400            is_persistent: false,
7401            min_streaming_distance: 0.0,
7402            max_streaming_distance: 2048.0,
7403            priority: 1.0,
7404        }
7405    }
7406
7407    pub fn should_load(&self, viewer_pos: Vec3) -> bool {
7408        let dist = self.bounds.distance_to_point(viewer_pos);
7409        dist >= self.min_streaming_distance && dist <= self.max_streaming_distance
7410    }
7411
7412    pub fn should_unload(&self, viewer_pos: Vec3) -> bool {
7413        if self.is_persistent { return false; }
7414        let dist = self.bounds.distance_to_point(viewer_pos);
7415        dist > self.max_streaming_distance * 1.2
7416    }
7417
7418    pub fn chunk_count(&self) -> usize {
7419        self.chunks.len()
7420    }
7421}
7422
7423#[derive(Debug, Clone)]
7424pub struct StreamingLevelManager {
7425    pub levels: HashMap<u32, StreamingLevel>,
7426    pub next_id: u32,
7427    pub loaded_levels: HashSet<u32>,
7428    pub pending_load: HashSet<u32>,
7429    pub pending_unload: HashSet<u32>,
7430}
7431
7432impl StreamingLevelManager {
7433    pub fn new() -> Self {
7434        Self {
7435            levels: HashMap::new(),
7436            next_id: 1,
7437            loaded_levels: HashSet::new(),
7438            pending_load: HashSet::new(),
7439            pending_unload: HashSet::new(),
7440        }
7441    }
7442
7443    pub fn register_level(&mut self, name: String, bounds: ChunkBounds) -> u32 {
7444        let id = self.next_id;
7445        self.next_id += 1;
7446        self.levels.insert(id, StreamingLevel::new(id, name, bounds));
7447        id
7448    }
7449
7450    pub fn update_streaming(&mut self, viewer_pos: Vec3) {
7451        self.pending_load.clear();
7452        self.pending_unload.clear();
7453
7454        for (id, level) in &self.levels {
7455            if self.loaded_levels.contains(id) {
7456                if level.should_unload(viewer_pos) {
7457                    self.pending_unload.insert(*id);
7458                }
7459            } else {
7460                if level.should_load(viewer_pos) {
7461                    self.pending_load.insert(*id);
7462                }
7463            }
7464        }
7465    }
7466
7467    pub fn commit_loads(&mut self) {
7468        for id in self.pending_load.drain().collect::<Vec<_>>() {
7469            self.loaded_levels.insert(id);
7470            if let Some(level) = self.levels.get_mut(&id) {
7471                level.load_state = ChunkLoadState::Loaded;
7472            }
7473        }
7474    }
7475
7476    pub fn commit_unloads(&mut self) {
7477        for id in self.pending_unload.drain().collect::<Vec<_>>() {
7478            self.loaded_levels.remove(&id);
7479            if let Some(level) = self.levels.get_mut(&id) {
7480                level.load_state = ChunkLoadState::Unloaded;
7481            }
7482        }
7483    }
7484
7485    pub fn chunks_for_loaded_levels(&self) -> Vec<ChunkCoord> {
7486        self.loaded_levels.iter()
7487            .filter_map(|id| self.levels.get(id))
7488            .flat_map(|l| l.chunks.iter().cloned())
7489            .collect()
7490    }
7491
7492    pub fn loaded_level_count(&self) -> usize {
7493        self.loaded_levels.len()
7494    }
7495
7496    pub fn total_level_count(&self) -> usize {
7497        self.levels.len()
7498    }
7499}
7500
7501// ============================================================
7502// VISIBILITY PROPAGATION (PORTAL SYSTEM STUB)
7503// ============================================================
7504
7505#[derive(Debug, Clone)]
7506pub struct Portal {
7507    pub id: u32,
7508    pub from_chunk: ChunkCoord,
7509    pub to_chunk: ChunkCoord,
7510    pub center: Vec3,
7511    pub normal: Vec3,
7512    pub half_extents: Vec2,
7513    pub is_open: bool,
7514}
7515
7516impl Portal {
7517    pub fn new(id: u32, from: ChunkCoord, to: ChunkCoord, center: Vec3, normal: Vec3, half_extents: Vec2) -> Self {
7518        Self {
7519            id,
7520            from_chunk: from,
7521            to_chunk: to,
7522            center,
7523            normal,
7524            half_extents,
7525            is_open: true,
7526        }
7527    }
7528
7529    pub fn is_visible_from(&self, viewer_pos: Vec3, viewer_forward: Vec3) -> bool {
7530        if !self.is_open { return false; }
7531        let to_portal = (self.center - viewer_pos).normalize_or_zero();
7532        let facing = viewer_forward.dot(to_portal);
7533        let normal_facing = self.normal.dot(to_portal);
7534        facing > -0.5 && normal_facing < 0.1
7535    }
7536
7537    pub fn bounds_2d(&self) -> [Vec3; 4] {
7538        let right = Vec3::new(-self.normal.z, 0.0, self.normal.x).normalize_or_zero();
7539        let up    = Vec3::Y;
7540        let c     = self.center;
7541        let hw    = right * self.half_extents.x;
7542        let hh    = up * self.half_extents.y;
7543        [c - hw - hh, c + hw - hh, c + hw + hh, c - hw + hh]
7544    }
7545
7546    pub fn project_to_screen_rect(
7547        &self,
7548        view_proj: Mat4,
7549    ) -> Option<(Vec2, Vec2)> {
7550        let corners = self.bounds_2d();
7551        let mut min_ndc = Vec2::splat(f32::MAX);
7552        let mut max_ndc = Vec2::splat(f32::MIN);
7553        for corner in &corners {
7554            let proj = view_proj.project_point3(*corner);
7555            let ndc = Vec2::new(proj.x, proj.y);
7556            min_ndc = min_ndc.min(ndc);
7557            max_ndc = max_ndc.max(ndc);
7558        }
7559        if min_ndc.x > 1.0 || max_ndc.x < -1.0 || min_ndc.y > 1.0 || max_ndc.y < -1.0 {
7560            None
7561        } else {
7562            Some((min_ndc, max_ndc))
7563        }
7564    }
7565}
7566
7567#[derive(Debug, Clone)]
7568pub struct PortalVisibilitySystem {
7569    pub portals: HashMap<u32, Portal>,
7570    pub chunk_portals: HashMap<ChunkCoord, Vec<u32>>,
7571    pub next_id: u32,
7572    pub visible_chunks: HashSet<ChunkCoord>,
7573}
7574
7575impl PortalVisibilitySystem {
7576    pub fn new() -> Self {
7577        Self {
7578            portals: HashMap::new(),
7579            chunk_portals: HashMap::new(),
7580            next_id: 1,
7581            visible_chunks: HashSet::new(),
7582        }
7583    }
7584
7585    pub fn add_portal(&mut self, from: ChunkCoord, to: ChunkCoord, center: Vec3, normal: Vec3, half: Vec2) -> u32 {
7586        let id = self.next_id;
7587        self.next_id += 1;
7588        let portal = Portal::new(id, from.clone(), to.clone(), center, normal, half);
7589        self.portals.insert(id, portal);
7590        self.chunk_portals.entry(from).or_insert_with(Vec::new).push(id);
7591        id
7592    }
7593
7594    pub fn compute_visibility(
7595        &mut self,
7596        viewer_chunk: &ChunkCoord,
7597        viewer_pos: Vec3,
7598        viewer_forward: Vec3,
7599        max_depth: usize,
7600    ) {
7601        self.visible_chunks.clear();
7602        self.visible_chunks.insert(viewer_chunk.clone());
7603        let mut to_visit = vec![(viewer_chunk.clone(), 0usize)];
7604        while let Some((current, depth)) = to_visit.pop() {
7605            if depth >= max_depth { continue; }
7606            if let Some(portal_ids) = self.chunk_portals.get(&current).cloned() {
7607                for pid in portal_ids {
7608                    if let Some(portal) = self.portals.get(&pid) {
7609                        if portal.is_visible_from(viewer_pos, viewer_forward) {
7610                            let dest = portal.to_chunk.clone();
7611                            if self.visible_chunks.insert(dest.clone()) {
7612                                to_visit.push((dest, depth + 1));
7613                            }
7614                        }
7615                    }
7616                }
7617            }
7618        }
7619    }
7620
7621    pub fn is_chunk_visible(&self, coord: &ChunkCoord) -> bool {
7622        self.visible_chunks.contains(coord)
7623    }
7624
7625    pub fn visible_chunk_count(&self) -> usize {
7626        self.visible_chunks.len()
7627    }
7628}
7629
7630// ============================================================
7631// FINAL EXTENDED UTILITIES
7632// ============================================================
7633
7634pub fn compute_chunk_lod_blend_weights(
7635    chunk: &StreamingChunk,
7636    camera: &StreamingCamera,
7637    config: &StreamingConfig,
7638    screen_height: f32,
7639) -> (LodLevel, LodLevel, f32) {
7640    let dist = chunk.distance_to_viewer;
7641    let desired = config.lod_for_distance(dist);
7642    let blend_near = if desired.index() > 0 {
7643        config.lod_distances[desired.index().saturating_sub(1)]
7644    } else {
7645        0.0
7646    };
7647    let blend_far = if desired.index() < 5 {
7648        config.lod_distances[desired.index().min(4)]
7649    } else {
7650        config.streaming_radius
7651    };
7652    let alpha = smooth_step(blend_near, blend_far, dist);
7653    (desired.next_lower(), desired.clone(), alpha)
7654}
7655
7656pub fn compute_dynamic_streaming_radius(
7657    base_radius: f32,
7658    memory_pressure: f32,
7659    fps_scale: f32,
7660) -> f32 {
7661    let mem_scale = (1.0 - memory_pressure * 0.5).clamp(0.4, 1.0);
7662    let fps_factor = fps_scale.clamp(0.5, 1.5);
7663    base_radius * mem_scale * fps_factor
7664}
7665
7666pub fn clamp_viewer_to_world_bounds(viewer: Vec3, world: &ChunkBounds) -> Vec3 {
7667    viewer.clamp(world.min, world.max)
7668}
7669
7670pub fn world_to_chunk_grid(pos: Vec3, chunk_size: f32) -> (i32, i32, i32) {
7671    (
7672        (pos.x / chunk_size).floor() as i32,
7673        (pos.y / chunk_size).floor() as i32,
7674        (pos.z / chunk_size).floor() as i32,
7675    )
7676}
7677
7678pub fn chunk_grid_to_world_center(cx: i32, cy: i32, cz: i32, chunk_size: f32) -> Vec3 {
7679    Vec3::new(
7680        (cx as f32 + 0.5) * chunk_size,
7681        (cy as f32 + 0.5) * chunk_size,
7682        (cz as f32 + 0.5) * chunk_size,
7683    )
7684}
7685
7686pub fn compute_level_streaming_priority(
7687    level_bounds: &ChunkBounds,
7688    viewer_pos: Vec3,
7689    viewer_velocity: Vec3,
7690    lookahead_t: f32,
7691) -> f32 {
7692    let predicted = viewer_pos + viewer_velocity * lookahead_t;
7693    let dist_current   = level_bounds.distance_to_point(viewer_pos);
7694    let dist_predicted = level_bounds.distance_to_point(predicted);
7695    let approach_rate = (dist_current - dist_predicted) / lookahead_t.max(0.001);
7696    let base = 1.0 / (1.0 + dist_current * 0.001);
7697    let approach_bonus = approach_rate.max(0.0) * 0.01;
7698    (base + approach_bonus).clamp(0.0, 1.0)
7699}
7700
7701pub fn estimate_lod_memory_total(
7702    chunks: &HashMap<ChunkCoord, StreamingChunk>,
7703    base_chunk_memory_mb: f32,
7704) -> f32 {
7705    chunks.values()
7706        .filter(|c| c.lod_level.is_loaded())
7707        .map(|c| base_chunk_memory_mb * c.lod_level.memory_multiplier())
7708        .sum()
7709}
7710
7711pub fn compute_lod_switch_hysteresis(
7712    current_lod: &LodLevel,
7713    desired_lod: &LodLevel,
7714    base_dist: f32,
7715    hysteresis_fraction: f32,
7716) -> f32 {
7717    if current_lod == desired_lod { return base_dist; }
7718    if current_lod < desired_lod {
7719        base_dist * (1.0 + hysteresis_fraction)
7720    } else {
7721        base_dist * (1.0 - hysteresis_fraction)
7722    }
7723}
7724
7725pub fn build_lod_distance_array(base_dist: f32, scale_factor: f32) -> [f32; 5] {
7726    [
7727        base_dist,
7728        base_dist * scale_factor,
7729        base_dist * scale_factor * scale_factor,
7730        base_dist * scale_factor * scale_factor * scale_factor,
7731        base_dist * scale_factor * scale_factor * scale_factor * scale_factor,
7732    ]
7733}
7734
7735pub fn compute_per_frame_memory_delta(
7736    prev_loaded: &HashSet<ChunkCoord>,
7737    curr_loaded: &HashSet<ChunkCoord>,
7738    memory_per_chunk_mb: f32,
7739) -> f32 {
7740    let newly_loaded   = curr_loaded.difference(prev_loaded).count() as f32;
7741    let newly_unloaded = prev_loaded.difference(curr_loaded).count() as f32;
7742    (newly_loaded - newly_unloaded) * memory_per_chunk_mb
7743}
7744
7745pub fn aabb_corner_distances(bounds: &ChunkBounds, point: Vec3) -> [f32; 8] {
7746    let corners = bounds.corners();
7747    std::array::from_fn(|i| (corners[i] - point).length())
7748}
7749
7750pub fn bvh_sah_cost(left_sa: f32, right_sa: f32, parent_sa: f32, left_count: usize, right_count: usize) -> f32 {
7751    SAH_TRAVERSAL_COST + SAH_INTERSECTION_COST * (
7752        left_sa  / parent_sa * left_count  as f32
7753      + right_sa / parent_sa * right_count as f32
7754    )
7755}
7756
7757pub fn compute_cluster_merge_cost(a: &HlodCluster, b: &HlodCluster) -> f32 {
7758    let merged = a.merged_bounds.merge(&b.merged_bounds);
7759    let sa_merged = merged.surface_area();
7760    let sa_a = a.merged_bounds.surface_area();
7761    let sa_b = b.merged_bounds.surface_area();
7762    sa_merged - sa_a - sa_b
7763}
7764
7765pub fn evaluate_lod_quality_metrics(
7766    editor: &WorldStreamingEditor,
7767    reference_positions: &[Vec3],
7768) -> Vec<(Vec3, LodLevel, f32)> {
7769    reference_positions.iter().map(|&pos| {
7770        let dist = pos.length();
7771        let lod = editor.config.lod_for_distance(dist);
7772        let quality = 1.0 - lod.memory_multiplier();
7773        (pos, lod, quality)
7774    }).collect()
7775}
7776
7777pub fn build_streaming_config_from_hardware_caps(
7778    available_memory_mb: u64,
7779    available_cores: u32,
7780    screen_width: u32,
7781    screen_height: u32,
7782    target_fps: f32,
7783) -> StreamingConfig {
7784    let mut cfg = StreamingConfig::default();
7785    cfg.max_memory_mb = available_memory_mb;
7786    cfg.max_concurrent_loads = (available_cores / 2).max(1) as usize;
7787    cfg.screen_height_pixels = screen_height;
7788    let aspect = screen_width as f32 / screen_height as f32;
7789    let budget_tier = available_memory_mb / 1024;
7790    if budget_tier >= 8 {
7791        cfg.max_loaded_chunks = 512;
7792        cfg.enable_occlusion_culling = true;
7793        cfg.enable_hlod = true;
7794        cfg.enable_virtual_textures = true;
7795    } else if budget_tier >= 4 {
7796        cfg.max_loaded_chunks = 256;
7797        cfg.enable_hlod = true;
7798    } else {
7799        cfg.max_loaded_chunks = 128;
7800        cfg.enable_impostor_billboards = false;
7801        cfg.enable_virtual_textures = false;
7802    }
7803    if target_fps >= 120.0 {
7804        cfg.lod_distances = build_lod_distance_array(32.0, 2.0);
7805    } else if target_fps >= 60.0 {
7806        cfg.lod_distances = LOD_DISTANCES;
7807    } else {
7808        cfg.lod_distances = build_lod_distance_array(24.0, 2.2);
7809    }
7810    cfg
7811}
7812
7813
7814// ============================================================
7815// DENSE MATH SUPPLEMENT
7816// ============================================================
7817
7818pub fn catmull_rom(p0: Vec3, p1: Vec3, p2: Vec3, p3: Vec3, t: f32) -> Vec3 {
7819    let t2 = t * t;
7820    let t3 = t2 * t;
7821    let m0 = (p2 - p0) * 0.5;
7822    let m1 = (p3 - p1) * 0.5;
7823    let b0 = 2.0 * t3 - 3.0 * t2 + 1.0;
7824    let b1 = t3 - 2.0 * t2 + t;
7825    let b2 = -2.0 * t3 + 3.0 * t2;
7826    let b3 = t3 - t2;
7827    p1 * b0 + m0 * b1 + p2 * b2 + m1 * b3
7828}
7829
7830pub fn bezier_cubic(p0: Vec3, p1: Vec3, p2: Vec3, p3: Vec3, t: f32) -> Vec3 {
7831    let u = 1.0 - t;
7832    p0 * (u * u * u) + p1 * (3.0 * u * u * t) + p2 * (3.0 * u * t * t) + p3 * (t * t * t)
7833}
7834
7835pub fn bezier_cubic_tangent(p0: Vec3, p1: Vec3, p2: Vec3, p3: Vec3, t: f32) -> Vec3 {
7836    let u = 1.0 - t;
7837    (p1 - p0) * (3.0 * u * u) + (p2 - p1) * (6.0 * u * t) + (p3 - p2) * (3.0 * t * t)
7838}
7839
7840pub fn bezier_arc_length_table(p0: Vec3, p1: Vec3, p2: Vec3, p3: Vec3, samples: usize) -> Vec<f32> {
7841    let mut table = vec![0.0f32; samples + 1];
7842    let mut prev = p0;
7843    for i in 1..=samples {
7844        let t = i as f32 / samples as f32;
7845        let cur = bezier_cubic(p0, p1, p2, p3, t);
7846        table[i] = table[i - 1] + (cur - prev).length();
7847        prev = cur;
7848    }
7849    table
7850}
7851
7852pub fn bezier_arc_length_t(table: &[f32], target_len: f32) -> f32 {
7853    let total = *table.last().unwrap_or(&0.0);
7854    if total < 1e-8 { return 0.0; }
7855    let target = (target_len / total).clamp(0.0, 1.0) * total;
7856    let n = table.len() - 1;
7857    for i in 1..table.len() {
7858        if table[i] >= target {
7859            let t0 = (i - 1) as f32 / n as f32;
7860            let t1 = i as f32 / n as f32;
7861            let frac = if table[i] - table[i-1] > 1e-8 {
7862                (target - table[i-1]) / (table[i] - table[i-1])
7863            } else { 0.0 };
7864            return t0 + (t1 - t0) * frac;
7865        }
7866    }
7867    1.0
7868}
7869
7870pub fn thermal_erosion_pass(hm: &mut TerrainHeightmap, talus_angle: f32, carry_fraction: f32) {
7871    let w = hm.width;
7872    let h = hm.height;
7873    let talus = talus_angle.tan() * hm.cell_size;
7874    let mut delta = vec![0.0f32; w * h];
7875    for z in 1..h-1 {
7876        for x in 1..w-1 {
7877            let c = hm.get_height(x, z);
7878            let neighbors = [(x-1, z), (x+1, z), (x, z-1), (x, z+1)];
7879            let mut max_diff = 0.0f32;
7880            let mut max_n = (x, z);
7881            for &(nx, nz) in &neighbors {
7882                let diff = c - hm.get_height(nx, nz);
7883                if diff > max_diff { max_diff = diff; max_n = (nx, nz); }
7884            }
7885            if max_diff > talus {
7886                let transport = (max_diff - talus) * carry_fraction;
7887                delta[z * w + x] -= transport;
7888                delta[max_n.1 * w + max_n.0] += transport;
7889            }
7890        }
7891    }
7892    for z in 0..h {
7893        for x in 0..w {
7894            hm.set_height(x, z, hm.get_height(x, z) + delta[z * w + x]);
7895        }
7896    }
7897}
7898
7899pub fn hydraulic_erosion_step(
7900    hm: &mut TerrainHeightmap, drop_x: f32, drop_z: f32,
7901    volume: f32, erosion_rate: f32, deposition_rate: f32, max_steps: usize,
7902) {
7903    let (mut x, mut z, mut vol, mut sediment) = (drop_x, drop_z, volume, 0.0f32);
7904    for _ in 0..max_steps {
7905        let grad = hm.compute_normal_bilinear(x, z);
7906        let speed = Vec2::new(-grad.x, -grad.z).length();
7907        if speed < 1e-6 { break; }
7908        let capacity = speed * vol;
7909        let h_here = hm.sample_bilinear(x, z);
7910        let ix = ((x - hm.origin.x) / hm.cell_size) as usize;
7911        let iz = ((z - hm.origin.y) / hm.cell_size) as usize;
7912        if sediment > capacity {
7913            let d = (sediment - capacity) * deposition_rate;
7914            sediment -= d;
7915            hm.set_height(ix, iz, h_here + d);
7916        } else {
7917            let e = (capacity - sediment).min(h_here * erosion_rate);
7918            sediment += e;
7919            hm.set_height(ix, iz, (h_here - e).max(0.0));
7920        }
7921        x -= grad.x * hm.cell_size;
7922        z -= grad.z * hm.cell_size;
7923        vol *= 0.99;
7924        if vol < 0.01 { break; }
7925    }
7926}
7927
7928pub fn compute_heightmap_ao(hm: &TerrainHeightmap, num_rays: usize, max_dist: f32) -> Vec<f32> {
7929    let (w, h) = (hm.width, hm.height);
7930    let mut ao = vec![1.0f32; w * h];
7931    for z in 0..h {
7932        for x in 0..w {
7933            let height = hm.get_height(x, z);
7934            let wx = hm.origin.x + x as f32 * hm.cell_size;
7935            let wz = hm.origin.y + z as f32 * hm.cell_size;
7936            let mut occ = 0.0f32;
7937            let steps = (max_dist / hm.cell_size) as usize;
7938            for ray in 0..num_rays {
7939                let angle = (ray as f32 / num_rays as f32) * std::f32::consts::TAU;
7940                let (dx, dz) = (angle.cos(), angle.sin());
7941                let mut max_h = 0.0f32;
7942                for step in 1..=steps {
7943                    let t = step as f32 * hm.cell_size;
7944                    let sh = hm.sample_bilinear(wx + dx * t, wz + dz * t);
7945                    let horizon = (sh - height) / t;
7946                    if horizon > max_h { max_h = horizon; }
7947                }
7948                occ += (max_h.atan() / std::f32::consts::FRAC_PI_2).clamp(0.0, 1.0);
7949            }
7950            ao[z * w + x] = 1.0 - (occ / num_rays as f32).clamp(0.0, 1.0);
7951        }
7952    }
7953    ao
7954}
7955
7956pub fn build_lod_transition_curve(min_dist: f32, max_dist: f32, steps: usize) -> Vec<(f32, f32)> {
7957    (0..=steps).map(|i| {
7958        let t = i as f32 / steps as f32;
7959        let d = min_dist + t * (max_dist - min_dist);
7960        (d, (1.0 - (d - min_dist) / (max_dist - min_dist + 1e-8)).clamp(0.0, 1.0))
7961    }).collect()
7962}
7963
7964pub fn sh_l1_eval(coeffs: &[Vec3; 4], normal: Vec3) -> Vec3 {
7965    let n = normal.normalize_or_zero();
7966    coeffs[0] * 0.282095 + coeffs[1] * 0.488603 * n.y
7967        + coeffs[2] * 0.488603 * n.z + coeffs[3] * 0.488603 * n.x
7968}
7969
7970pub fn sh_l1_project(dir: Vec3, color: Vec3) -> [Vec3; 4] {
7971    let n = dir.normalize_or_zero();
7972    let w = std::f32::consts::FRAC_1_PI * 0.25;
7973    [color*w*0.282095, color*w*0.488603*n.y, color*w*0.488603*n.z, color*w*0.488603*n.x]
7974}
7975
7976pub fn pack_rgba8(r: f32, g: f32, b: f32, a: f32) -> u32 {
7977    (r.clamp(0.0,1.0)*255.0) as u32
7978    | (((g.clamp(0.0,1.0)*255.0) as u32) << 8)
7979    | (((b.clamp(0.0,1.0)*255.0) as u32) << 16)
7980    | (((a.clamp(0.0,1.0)*255.0) as u32) << 24)
7981}
7982
7983pub fn unpack_rgba8(packed: u32) -> (f32, f32, f32, f32) {
7984    ((packed & 0xFF) as f32 / 255.0, ((packed>>8)&0xFF) as f32/255.0,
7985     ((packed>>16)&0xFF) as f32/255.0, ((packed>>24)&0xFF) as f32/255.0)
7986}
7987
7988pub fn halton(index: u32, base: u32) -> f32 {
7989    let (mut result, mut f, mut i) = (0.0f32, 1.0f32, index);
7990    while i > 0 { f /= base as f32; result += f * (i % base) as f32; i /= base; }
7991    result
7992}
7993
7994pub fn halton_2d(index: u32) -> Vec2 { Vec2::new(halton(index,2), halton(index,3)) }
7995pub fn halton_3d(index: u32) -> Vec3 { Vec3::new(halton(index,2), halton(index,3), halton(index,5)) }
7996
7997pub fn exp_smooth(current: f32, target: f32, lambda: f32, dt: f32) -> f32 {
7998    current + (target - current) * (1.0 - (-lambda * dt).exp())
7999}
8000
8001pub fn exp_smooth_vec3(current: Vec3, target: Vec3, lambda: f32, dt: f32) -> Vec3 {
8002    current + (target - current) * (1.0 - (-lambda * dt).exp())
8003}
8004
8005pub fn aabb_solid_angle_approx(bounds: &ChunkBounds, viewpoint: Vec3) -> f32 {
8006    let dist = (bounds.center() - viewpoint).length();
8007    if dist < 1e-6 { return std::f32::consts::TAU * 2.0; }
8008    let angle = (bounds.half_size().length() / dist).min(1.0).asin();
8009    std::f32::consts::PI * angle * angle
8010}
8011
8012pub fn chunk_visual_importance(bounds: &ChunkBounds, viewpoint: Vec3, object_count: usize) -> f32 {
8013    aabb_solid_angle_approx(bounds, viewpoint)
8014        * (object_count as f32 / bounds.volume().max(1.0)).sqrt()
8015}
8016
8017pub fn isqrt(n: u64) -> u64 {
8018    if n == 0 { return 0; }
8019    let (mut x, mut y) = (n, (n + 1) / 2);
8020    while y < x { x = y; y = (x + n / x) / 2; }
8021    x
8022}
8023
8024pub fn required_lod_levels(world_size: f32, min_feature: f32) -> u32 {
8025    if min_feature <= 0.0 { return 1; }
8026    ((world_size / min_feature).log2().ceil() as u32).max(1)
8027}
8028
8029pub fn estimate_bvh_memory_bytes(n: usize) -> usize { 2 * n * 128 }
8030
8031pub fn estimate_octree_memory_bytes(n: usize, depth: u32) -> usize {
8032    ((8usize.pow(depth + 1) - 1) / 7).min(n * 4) * 64
8033}
8034
8035pub fn bvh_sah_cost_fn(lsa: f32, rsa: f32, psa: f32, lc: usize, rc: usize) -> f32 {
8036    SAH_TRAVERSAL_COST + SAH_INTERSECTION_COST * (lsa/psa*lc as f32 + rsa/psa*rc as f32)
8037}
8038
8039pub fn cluster_merge_cost(a: &HlodCluster, b: &HlodCluster) -> f32 {
8040    a.merged_bounds.merge(&b.merged_bounds).surface_area()
8041        - a.merged_bounds.surface_area() - b.merged_bounds.surface_area()
8042}
8043
8044pub fn evaluate_lod_quality(editor: &WorldStreamingEditor, positions: &[Vec3]) -> Vec<(Vec3, LodLevel, f32)> {
8045    positions.iter().map(|&p| {
8046        let lod = editor.config.lod_for_distance(p.length());
8047        (p, lod.clone(), 1.0 - lod.memory_multiplier())
8048    }).collect()
8049}
8050
8051pub fn system_capacity_summary() -> Vec<(&'static str, usize)> {
8052    vec![
8053        ("MAX_OCTREE_DEPTH",          MAX_OCTREE_DEPTH as usize),
8054        ("BVH_MAX_LEAF_OBJECTS",      BVH_MAX_LEAF_OBJECTS),
8055        ("DEFAULT_MAX_LOADED_CHUNKS", DEFAULT_MAX_LOADED_CHUNKS),
8056        ("DEFAULT_MAX_MEMORY_MB",     DEFAULT_MAX_MEMORY_MB as usize),
8057        ("MAX_ASYNC_LOAD_QUEUE",      MAX_ASYNC_LOAD_QUEUE),
8058        ("TERRAIN_PATCH_SIZE",        TERRAIN_PATCH_SIZE),
8059        ("PROFILER_HISTORY_FRAMES",   PROFILER_HISTORY_FRAMES),
8060    ]
8061}