Skip to main content

proof_engine/editor/
lod_manager.rs

1
2//! Level-of-detail manager — mesh LOD generation, terrain LOD, streaming, occlusion culling.
3
4use glam::{Vec2, Vec3, Vec4, Mat4};
5use std::collections::HashMap;
6
7// ---------------------------------------------------------------------------
8// LOD level descriptor
9// ---------------------------------------------------------------------------
10
11#[derive(Debug, Clone, Copy, PartialEq)]
12pub enum LodTransitionMode {
13    Discrete,
14    CrossFade,
15    SpeedTree,
16    Dither,
17}
18
19#[derive(Debug, Clone)]
20pub struct LodLevel {
21    pub index: usize,
22    pub mesh_id: u64,
23    pub screen_relative_transition_height: f32,
24    pub fade_transition_width: f32,
25    pub renderers_enabled: Vec<bool>,
26    pub triangle_count: u32,
27    pub vertex_count: u32,
28    pub reduction_ratio: f32,
29    pub shadow_casting: bool,
30    pub shadow_receiving: bool,
31    pub motion_vectors: bool,
32    pub skinned_motion_vectors: bool,
33}
34
35impl LodLevel {
36    pub fn new(index: usize, mesh_id: u64, transition: f32, tris: u32, verts: u32) -> Self {
37        Self {
38            index,
39            mesh_id,
40            screen_relative_transition_height: transition,
41            fade_transition_width: 0.1,
42            renderers_enabled: vec![true],
43            triangle_count: tris,
44            vertex_count: verts,
45            reduction_ratio: 1.0,
46            shadow_casting: true,
47            shadow_receiving: true,
48            motion_vectors: false,
49            skinned_motion_vectors: false,
50        }
51    }
52}
53
54// ---------------------------------------------------------------------------
55// LOD group
56// ---------------------------------------------------------------------------
57
58#[derive(Debug, Clone)]
59pub struct LodGroup {
60    pub id: u64,
61    pub name: String,
62    pub center: Vec3,
63    pub size: f32,
64    pub levels: Vec<LodLevel>,
65    pub transition_mode: LodTransitionMode,
66    pub animate_cross_fading: bool,
67    pub fade_mode: FadeMode,
68    pub current_lod: usize,
69    pub fade_t: f32,
70    pub enabled: bool,
71    pub position: Vec3,
72}
73
74#[derive(Debug, Clone, Copy, PartialEq)]
75pub enum FadeMode {
76    None,
77    CrossFade,
78    SpeedTree,
79}
80
81impl LodGroup {
82    pub fn new(id: u64, name: impl Into<String>, position: Vec3, size: f32) -> Self {
83        Self {
84            id,
85            name: name.into(),
86            center: Vec3::ZERO,
87            size,
88            levels: Vec::new(),
89            transition_mode: LodTransitionMode::CrossFade,
90            animate_cross_fading: true,
91            fade_mode: FadeMode::CrossFade,
92            current_lod: 0,
93            fade_t: 0.0,
94            enabled: true,
95            position,
96        }
97    }
98
99    pub fn add_level(&mut self, level: LodLevel) {
100        let i = self.levels.partition_point(|l| l.screen_relative_transition_height > level.screen_relative_transition_height);
101        self.levels.insert(i, level);
102        // Renumber
103        for (j, l) in self.levels.iter_mut().enumerate() {
104            l.index = j;
105        }
106    }
107
108    /// Compute the screen coverage for a given camera distance.
109    pub fn screen_coverage(&self, distance: f32, fov_tan: f32, screen_height: f32) -> f32 {
110        if distance < 0.001 { return 1.0; }
111        let world_size = self.size;
112        let projected_size = world_size / (distance * fov_tan);
113        (projected_size / screen_height).clamp(0.0, 1.0)
114    }
115
116    /// Determine which LOD should be active.
117    pub fn compute_lod(&self, screen_coverage: f32) -> usize {
118        for (i, level) in self.levels.iter().enumerate() {
119            if screen_coverage >= level.screen_relative_transition_height {
120                return i;
121            }
122        }
123        self.levels.len().saturating_sub(1) // Culled
124    }
125
126    pub fn update(&mut self, camera_pos: Vec3, fov_tan: f32, screen_height: f32, dt: f32) {
127        let dist = self.position.distance(camera_pos);
128        let cov = self.screen_coverage(dist, fov_tan, screen_height);
129        let new_lod = self.compute_lod(cov);
130        if new_lod != self.current_lod {
131            if self.transition_mode == LodTransitionMode::CrossFade {
132                self.fade_t = 1.0;
133            }
134            self.current_lod = new_lod;
135        }
136        if self.fade_t > 0.0 {
137            self.fade_t = (self.fade_t - dt * 4.0).max(0.0);
138        }
139    }
140
141    pub fn triangle_reduction_from_lod0(&self, lod_idx: usize) -> f32 {
142        if self.levels.is_empty() { return 1.0; }
143        let base = self.levels[0].triangle_count as f32;
144        let current = self.levels.get(lod_idx).map(|l| l.triangle_count as f32).unwrap_or(0.0);
145        if base > 0.0 { current / base } else { 1.0 }
146    }
147}
148
149// ---------------------------------------------------------------------------
150// Mesh simplification
151// ---------------------------------------------------------------------------
152
153#[derive(Debug, Clone, Copy, PartialEq)]
154pub enum SimplificationAlgorithm {
155    QEM,           // Quadric Error Metrics
156    MeshOptimizer,
157    Sloppy,
158    UniformGrid,
159}
160
161#[derive(Debug, Clone)]
162pub struct SimplificationSettings {
163    pub algorithm: SimplificationAlgorithm,
164    pub target_ratio: f32,           // 0..1, proportion of triangles to keep
165    pub max_error: f32,
166    pub preserve_borders: bool,
167    pub preserve_uvs: bool,
168    pub preserve_normals: bool,
169    pub preserve_attributes: bool,
170    pub lock_border: bool,
171    pub merge_threshold: f32,
172    pub attribute_weight: f32,
173}
174
175impl Default for SimplificationSettings {
176    fn default() -> Self {
177        Self {
178            algorithm: SimplificationAlgorithm::QEM,
179            target_ratio: 0.5,
180            max_error: 0.001,
181            preserve_borders: true,
182            preserve_uvs: true,
183            preserve_normals: true,
184            preserve_attributes: true,
185            lock_border: false,
186            merge_threshold: 1e-4,
187            attribute_weight: 0.1,
188        }
189    }
190}
191
192#[derive(Debug, Clone)]
193pub struct SimplificationResult {
194    pub original_tris: u32,
195    pub result_tris: u32,
196    pub original_verts: u32,
197    pub result_verts: u32,
198    pub max_deviation: f32,
199    pub rms_deviation: f32,
200    pub processing_ms: f32,
201    pub success: bool,
202    pub error_message: Option<String>,
203}
204
205impl SimplificationResult {
206    pub fn ratio(&self) -> f32 {
207        if self.original_tris == 0 { return 1.0; }
208        self.result_tris as f32 / self.original_tris as f32
209    }
210}
211
212/// Simulates LOD generation result (no actual mesh processing).
213pub fn generate_lod_levels(mesh_id: u64, original_tris: u32, original_verts: u32, ratios: &[f32]) -> Vec<(LodLevel, SimplificationResult)> {
214    ratios.iter().enumerate().map(|(i, &ratio)| {
215        let result_tris = (original_tris as f32 * ratio) as u32;
216        let result_verts = (original_verts as f32 * ratio) as u32;
217        let transition = match i {
218            0 => 1.0,
219            1 => 0.5,
220            2 => 0.25,
221            3 => 0.1,
222            _ => 0.05 / i as f32,
223        };
224        let level = LodLevel::new(i, mesh_id * 100 + i as u64, transition, result_tris, result_verts);
225        let result = SimplificationResult {
226            original_tris,
227            result_tris,
228            original_verts,
229            result_verts,
230            max_deviation: 0.001 * (1.0 - ratio),
231            rms_deviation: 0.0003 * (1.0 - ratio),
232            processing_ms: original_tris as f32 * 0.001 * (1.0 - ratio),
233            success: true,
234            error_message: None,
235        };
236        (level, result)
237    }).collect()
238}
239
240// ---------------------------------------------------------------------------
241// Occlusion culling
242// ---------------------------------------------------------------------------
243
244#[derive(Debug, Clone, Copy, PartialEq)]
245pub enum OcclusionMode {
246    None,
247    HiZ,          // Hierarchical Z-buffer
248    SoftwareRasterization,
249    PvsPortal,
250    Umbra,
251}
252
253#[derive(Debug, Clone)]
254pub struct OcclusionSettings {
255    pub mode: OcclusionMode,
256    pub occluder_size_threshold: f32,
257    pub occludee_size_threshold: f32,
258    pub backface_culling: bool,
259    pub hi_z_mip_levels: u32,
260    pub conservative_depth: bool,
261    pub async_readback: bool,
262    pub readback_frame_delay: u32,
263    pub debug_draw_occluders: bool,
264    pub debug_draw_occludees: bool,
265}
266
267impl Default for OcclusionSettings {
268    fn default() -> Self {
269        Self {
270            mode: OcclusionMode::HiZ,
271            occluder_size_threshold: 0.01,
272            occludee_size_threshold: 0.001,
273            backface_culling: true,
274            hi_z_mip_levels: 8,
275            conservative_depth: false,
276            async_readback: true,
277            readback_frame_delay: 2,
278            debug_draw_occluders: false,
279            debug_draw_occludees: false,
280        }
281    }
282}
283
284#[derive(Debug, Clone)]
285pub struct OcclusionQuery {
286    pub object_id: u64,
287    pub bounding_sphere: (Vec3, f32),
288    pub visible_last_frame: bool,
289    pub frames_invisible: u32,
290    pub frames_visible: u32,
291}
292
293// ---------------------------------------------------------------------------
294// Streaming
295// ---------------------------------------------------------------------------
296
297#[derive(Debug, Clone, Copy, PartialEq)]
298pub enum StreamingState {
299    Unloaded,
300    Queued,
301    Loading,
302    Loaded,
303    Unloading,
304    Error,
305}
306
307#[derive(Debug, Clone)]
308pub struct StreamableAsset {
309    pub id: u64,
310    pub name: String,
311    pub size_bytes: u64,
312    pub lod_group: Option<u64>,
313    pub streaming_state: StreamingState,
314    pub load_priority: f32,
315    pub last_visible_frame: u64,
316    pub retain_frames: u32,
317    pub memory_budget_category: MemoryCategory,
318}
319
320#[derive(Debug, Clone, Copy, PartialEq)]
321pub enum MemoryCategory {
322    Critical,
323    High,
324    Medium,
325    Low,
326    Background,
327}
328
329impl MemoryCategory {
330    pub fn eviction_priority(self) -> u8 {
331        match self {
332            MemoryCategory::Critical => 255,
333            MemoryCategory::High => 200,
334            MemoryCategory::Medium => 128,
335            MemoryCategory::Low => 64,
336            MemoryCategory::Background => 0,
337        }
338    }
339}
340
341#[derive(Debug, Clone)]
342pub struct StreamingManager {
343    pub assets: Vec<StreamableAsset>,
344    pub max_memory_bytes: u64,
345    pub current_memory_bytes: u64,
346    pub load_queue: Vec<u64>,
347    pub unload_queue: Vec<u64>,
348    pub current_frame: u64,
349    pub bandwidth_limit_bytes_per_frame: u64,
350    pub bytes_loaded_this_frame: u64,
351    pub priority_bias_distance: f32,
352    pub camera_pos: Vec3,
353}
354
355impl StreamingManager {
356    pub fn new(max_memory_mb: u32) -> Self {
357        Self {
358            assets: Vec::new(),
359            max_memory_bytes: max_memory_mb as u64 * 1_048_576,
360            current_memory_bytes: 0,
361            load_queue: Vec::new(),
362            unload_queue: Vec::new(),
363            current_frame: 0,
364            bandwidth_limit_bytes_per_frame: 64 * 1_048_576, // 64 MB/frame
365            bytes_loaded_this_frame: 0,
366            priority_bias_distance: 50.0,
367            camera_pos: Vec3::ZERO,
368        }
369    }
370
371    pub fn register_asset(&mut self, asset: StreamableAsset) {
372        self.assets.push(asset);
373    }
374
375    pub fn update(&mut self, camera_pos: Vec3) {
376        self.camera_pos = camera_pos;
377        self.current_frame += 1;
378        self.bytes_loaded_this_frame = 0;
379        // Simulate loading
380        let mut loaded = Vec::new();
381        for &id in &self.load_queue {
382            if let Some(asset) = self.assets.iter_mut().find(|a| a.id == id) {
383                if self.current_memory_bytes + asset.size_bytes <= self.max_memory_bytes
384                    && self.bytes_loaded_this_frame + asset.size_bytes <= self.bandwidth_limit_bytes_per_frame {
385                    asset.streaming_state = StreamingState::Loaded;
386                    self.current_memory_bytes += asset.size_bytes;
387                    self.bytes_loaded_this_frame += asset.size_bytes;
388                    loaded.push(id);
389                }
390            }
391        }
392        self.load_queue.retain(|id| !loaded.contains(id));
393        // Simulate unloading
394        let mut unloaded = Vec::new();
395        for &id in &self.unload_queue {
396            if let Some(asset) = self.assets.iter_mut().find(|a| a.id == id) {
397                self.current_memory_bytes = self.current_memory_bytes.saturating_sub(asset.size_bytes);
398                asset.streaming_state = StreamingState::Unloaded;
399                unloaded.push(id);
400            }
401        }
402        self.unload_queue.retain(|id| !unloaded.contains(id));
403        // Evict old assets if over budget
404        if self.current_memory_bytes > self.max_memory_bytes {
405            self.evict_lru();
406        }
407    }
408
409    pub fn request_load(&mut self, id: u64, priority: f32) {
410        if let Some(asset) = self.assets.iter_mut().find(|a| a.id == id) {
411            if asset.streaming_state == StreamingState::Unloaded {
412                asset.streaming_state = StreamingState::Queued;
413                asset.load_priority = priority;
414                self.load_queue.push(id);
415                // Sort by priority descending
416                let assets = &self.assets;
417                self.load_queue.sort_by(|a, b| {
418                    let pa = assets.iter().find(|x| x.id == *a).map(|x| x.load_priority).unwrap_or(0.0);
419                    let pb = assets.iter().find(|x| x.id == *b).map(|x| x.load_priority).unwrap_or(0.0);
420                    pb.partial_cmp(&pa).unwrap_or(std::cmp::Ordering::Equal)
421                });
422            }
423        }
424    }
425
426    pub fn request_unload(&mut self, id: u64) {
427        if let Some(asset) = self.assets.iter_mut().find(|a| a.id == id) {
428            if asset.streaming_state == StreamingState::Loaded {
429                asset.streaming_state = StreamingState::Unloading;
430                self.unload_queue.push(id);
431            }
432        }
433    }
434
435    fn evict_lru(&mut self) {
436        // Find least-recently-used non-critical loaded asset
437        let current_frame = self.current_frame;
438        let evict_id = self.assets.iter()
439            .filter(|a| a.streaming_state == StreamingState::Loaded && a.memory_budget_category != MemoryCategory::Critical)
440            .min_by_key(|a| a.last_visible_frame)
441            .map(|a| a.id);
442        if let Some(id) = evict_id {
443            self.request_unload(id);
444        }
445    }
446
447    pub fn memory_pressure(&self) -> f32 {
448        self.current_memory_bytes as f32 / self.max_memory_bytes as f32
449    }
450
451    pub fn loaded_count(&self) -> usize {
452        self.assets.iter().filter(|a| a.streaming_state == StreamingState::Loaded).count()
453    }
454}
455
456// ---------------------------------------------------------------------------
457// LOD Manager
458// ---------------------------------------------------------------------------
459
460#[derive(Debug, Clone)]
461pub struct LodManagerStats {
462    pub total_lod_groups: usize,
463    pub active_lod_groups: usize,
464    pub lod0_count: usize,
465    pub lod1_count: usize,
466    pub lod2_count: usize,
467    pub culled_count: usize,
468    pub total_triangles_without_lod: u64,
469    pub total_triangles_with_lod: u64,
470    pub savings_ratio: f32,
471}
472
473#[derive(Debug, Clone)]
474pub struct LodManager {
475    pub groups: Vec<LodGroup>,
476    pub occlusion_settings: OcclusionSettings,
477    pub occlusion_queries: Vec<OcclusionQuery>,
478    pub streaming: StreamingManager,
479    pub camera_pos: Vec3,
480    pub camera_fov: f32,
481    pub screen_height: f32,
482    pub enable_lod: bool,
483    pub enable_occlusion: bool,
484    pub lod_bias: f32,
485    pub max_active_groups: usize,
486    pub stats: LodManagerStats,
487}
488
489impl LodManager {
490    pub fn new() -> Self {
491        let mut mgr = Self {
492            groups: Vec::new(),
493            occlusion_settings: OcclusionSettings::default(),
494            occlusion_queries: Vec::new(),
495            streaming: StreamingManager::new(1024),
496            camera_pos: Vec3::ZERO,
497            camera_fov: 60.0,
498            screen_height: 1080.0,
499            enable_lod: true,
500            enable_occlusion: true,
501            lod_bias: 1.0,
502            max_active_groups: 10000,
503            stats: LodManagerStats {
504                total_lod_groups: 0, active_lod_groups: 0, lod0_count: 0,
505                lod1_count: 0, lod2_count: 0, culled_count: 0,
506                total_triangles_without_lod: 0, total_triangles_with_lod: 0, savings_ratio: 0.0,
507            },
508        };
509        mgr.populate_demo();
510        mgr
511    }
512
513    fn populate_demo(&mut self) {
514        // Add a variety of LOD groups
515        let positions = [
516            Vec3::new(0.0, 0.0, 0.0),
517            Vec3::new(10.0, 0.0, 0.0),
518            Vec3::new(-10.0, 0.0, 5.0),
519            Vec3::new(0.0, 0.0, 15.0),
520            Vec3::new(20.0, 0.0, -5.0),
521        ];
522        let sizes = [2.0_f32, 5.0, 1.5, 8.0, 3.0];
523        let base_tris = [5000u32, 12000, 3000, 20000, 8000];
524        for (i, ((pos, size), tris)) in positions.iter().zip(sizes.iter()).zip(base_tris.iter()).enumerate() {
525            let mut group = LodGroup::new(i as u64 + 1, format!("Object_{}", i), *pos, *size);
526            let levels = generate_lod_levels(i as u64 * 100, *tris, tris / 2, &[1.0, 0.5, 0.25, 0.1]);
527            for (level, _) in levels {
528                group.add_level(level);
529            }
530            self.groups.push(group);
531        }
532    }
533
534    pub fn add_group(&mut self, group: LodGroup) {
535        self.groups.push(group);
536    }
537
538    pub fn update(&mut self, camera_pos: Vec3, dt: f32) {
539        self.camera_pos = camera_pos;
540        self.streaming.update(camera_pos);
541        if !self.enable_lod { return; }
542        let fov_tan = (self.camera_fov * 0.5 * std::f32::consts::PI / 180.0).tan();
543        let screen_h = self.screen_height;
544        // Update LOD for each group
545        for group in &mut self.groups {
546            if !group.enabled { continue; }
547            group.update(camera_pos, fov_tan, screen_h, dt);
548        }
549        self.recompute_stats();
550    }
551
552    fn recompute_stats(&mut self) {
553        let mut stats = LodManagerStats {
554            total_lod_groups: self.groups.len(),
555            active_lod_groups: self.groups.iter().filter(|g| g.enabled).count(),
556            lod0_count: 0, lod1_count: 0, lod2_count: 0, culled_count: 0,
557            total_triangles_without_lod: 0,
558            total_triangles_with_lod: 0,
559            savings_ratio: 0.0,
560        };
561        for group in &self.groups {
562            if !group.enabled { continue; }
563            if group.levels.is_empty() { continue; }
564            let base_tris = group.levels[0].triangle_count as u64;
565            stats.total_triangles_without_lod += base_tris;
566            let cur_tris = group.levels.get(group.current_lod).map(|l| l.triangle_count as u64).unwrap_or(0);
567            stats.total_triangles_with_lod += cur_tris;
568            match group.current_lod {
569                0 => stats.lod0_count += 1,
570                1 => stats.lod1_count += 1,
571                2 => stats.lod2_count += 1,
572                _ => stats.culled_count += 1,
573            }
574        }
575        if stats.total_triangles_without_lod > 0 {
576            stats.savings_ratio = 1.0 - (stats.total_triangles_with_lod as f32 / stats.total_triangles_without_lod as f32);
577        }
578        self.stats = stats;
579    }
580
581    pub fn find_group(&self, id: u64) -> Option<&LodGroup> {
582        self.groups.iter().find(|g| g.id == id)
583    }
584
585    pub fn find_group_mut(&mut self, id: u64) -> Option<&mut LodGroup> {
586        self.groups.iter_mut().find(|g| g.id == id)
587    }
588
589    pub fn triangle_savings_str(&self) -> String {
590        format!("{:.1}% triangle reduction ({} → {})",
591            self.stats.savings_ratio * 100.0,
592            self.stats.total_triangles_without_lod,
593            self.stats.total_triangles_with_lod)
594    }
595
596    pub fn generate_lod_for_group(&mut self, group_id: u64, original_tris: u32, original_verts: u32, ratios: &[f32]) {
597        if let Some(group) = self.find_group_mut(group_id) {
598            group.levels.clear();
599            let levels = generate_lod_levels(group_id, original_tris, original_verts, ratios);
600            for (level, _) in levels {
601                group.add_level(level);
602            }
603        }
604    }
605}
606
607// ---------------------------------------------------------------------------
608// Tests
609// ---------------------------------------------------------------------------
610#[cfg(test)]
611mod tests {
612    use super::*;
613
614    #[test]
615    fn test_lod_group() {
616        let mut group = LodGroup::new(1, "test", Vec3::ZERO, 5.0);
617        let levels = generate_lod_levels(1, 10000, 5000, &[1.0, 0.5, 0.25]);
618        for (l, _) in levels { group.add_level(l); }
619        assert_eq!(group.levels.len(), 3);
620        let cov = group.screen_coverage(10.0, 0.5773, 1080.0);
621        assert!(cov > 0.0 && cov <= 1.0);
622    }
623
624    #[test]
625    fn test_lod_compute() {
626        let mut group = LodGroup::new(1, "test", Vec3::ZERO, 5.0);
627        let levels = generate_lod_levels(1, 10000, 5000, &[1.0, 0.5, 0.25, 0.1]);
628        for (l, _) in levels { group.add_level(l); }
629        assert_eq!(group.compute_lod(1.0), 0);
630        assert_eq!(group.compute_lod(0.4), 1);
631        assert_eq!(group.compute_lod(0.05), 3);
632    }
633
634    #[test]
635    fn test_streaming_manager() {
636        let mut mgr = StreamingManager::new(256);
637        mgr.register_asset(StreamableAsset {
638            id: 1, name: "TestMesh".into(), size_bytes: 1_048_576,
639            lod_group: None, streaming_state: StreamingState::Unloaded,
640            load_priority: 1.0, last_visible_frame: 0, retain_frames: 10,
641            memory_budget_category: MemoryCategory::Medium,
642        });
643        mgr.request_load(1, 1.0);
644        mgr.update(Vec3::ZERO);
645        assert_eq!(mgr.loaded_count(), 1);
646    }
647
648    #[test]
649    fn test_lod_manager() {
650        let mut mgr = LodManager::new();
651        assert!(!mgr.groups.is_empty());
652        mgr.update(Vec3::ZERO, 0.016);
653        assert!(mgr.stats.total_lod_groups > 0);
654    }
655}