Skip to main content

proof_engine/deferred/
pipeline.rs

1//! Full deferred rendering pipeline.
2//!
3//! Implements multi-pass deferred rendering with:
4//! - Depth pre-pass (early Z rejection, front-to-back sorting)
5//! - Geometry pass (fill G-Buffer from scene geometry)
6//! - Lighting pass (fullscreen quad evaluating all lights against G-Buffer)
7//! - Forward pass (transparent objects sorted back-to-front)
8//! - Post-process pass (bloom, tone mapping, anti-aliasing)
9//! - HDR framebuffer management
10//! - Auto-exposure with histogram or average luminance
11//! - Render queue with opaque/transparent/overlay buckets
12
13use std::collections::HashMap;
14
15use super::{
16    Viewport, Mat4,
17    vec3_sub, vec3_dot, vec3_length, clampf, lerpf, saturate,
18};
19use super::gbuffer::GBuffer;
20use super::materials::MaterialSortKey;
21
22// ---------------------------------------------------------------------------
23// Light types
24// ---------------------------------------------------------------------------
25
26/// Types of lights supported by the deferred lighting pass.
27#[derive(Debug, Clone)]
28pub enum LightType {
29    /// Directional light (sun). Direction + color + intensity.
30    Directional {
31        direction: [f32; 3],
32        color: [f32; 3],
33        intensity: f32,
34        cast_shadows: bool,
35    },
36    /// Point light (omnidirectional). Position + color + intensity + range.
37    Point {
38        position: [f32; 3],
39        color: [f32; 3],
40        intensity: f32,
41        range: f32,
42        cast_shadows: bool,
43    },
44    /// Spot light. Position + direction + color + angles.
45    Spot {
46        position: [f32; 3],
47        direction: [f32; 3],
48        color: [f32; 3],
49        intensity: f32,
50        range: f32,
51        inner_angle: f32,
52        outer_angle: f32,
53        cast_shadows: bool,
54    },
55    /// Area light (rectangle). Position + normal + up + size.
56    Area {
57        position: [f32; 3],
58        normal: [f32; 3],
59        up: [f32; 3],
60        width: f32,
61        height: f32,
62        color: [f32; 3],
63        intensity: f32,
64    },
65    /// Ambient light (global illumination approximation).
66    Ambient {
67        color: [f32; 3],
68        intensity: f32,
69    },
70}
71
72impl LightType {
73    /// Get the world-space position of this light (if applicable).
74    pub fn position(&self) -> Option<[f32; 3]> {
75        match self {
76            Self::Directional { .. } | Self::Ambient { .. } => None,
77            Self::Point { position, .. }
78            | Self::Spot { position, .. }
79            | Self::Area { position, .. } => Some(*position),
80        }
81    }
82
83    /// Get the effective range/radius of influence.
84    pub fn range(&self) -> f32 {
85        match self {
86            Self::Directional { .. } | Self::Ambient { .. } => f32::MAX,
87            Self::Point { range, .. } | Self::Spot { range, .. } => *range,
88            Self::Area { width, height, .. } => (*width + *height) * 2.0,
89        }
90    }
91
92    /// Get the color of this light.
93    pub fn color(&self) -> [f32; 3] {
94        match self {
95            Self::Directional { color, .. }
96            | Self::Point { color, .. }
97            | Self::Spot { color, .. }
98            | Self::Area { color, .. }
99            | Self::Ambient { color, .. } => *color,
100        }
101    }
102
103    /// Get the intensity.
104    pub fn intensity(&self) -> f32 {
105        match self {
106            Self::Directional { intensity, .. }
107            | Self::Point { intensity, .. }
108            | Self::Spot { intensity, .. }
109            | Self::Area { intensity, .. }
110            | Self::Ambient { intensity, .. } => *intensity,
111        }
112    }
113
114    /// Whether this light casts shadows.
115    pub fn casts_shadows(&self) -> bool {
116        match self {
117            Self::Directional { cast_shadows, .. }
118            | Self::Point { cast_shadows, .. }
119            | Self::Spot { cast_shadows, .. } => *cast_shadows,
120            _ => false,
121        }
122    }
123
124    /// Compute the light contribution at a given surface point.
125    /// Returns (light_dir_to_surface, attenuation, color).
126    pub fn evaluate(&self, surface_pos: [f32; 3]) -> ([f32; 3], f32, [f32; 3]) {
127        match self {
128            Self::Directional { direction, color, intensity, .. } => {
129                let dir = [-direction[0], -direction[1], -direction[2]];
130                (dir, *intensity, *color)
131            }
132            Self::Point { position, color, intensity, range, .. } => {
133                let to_light = vec3_sub(*position, surface_pos);
134                let dist = vec3_length(to_light);
135                if dist > *range || dist < 1e-6 {
136                    return ([0.0, 0.0, 0.0], 0.0, *color);
137                }
138                let dir = [to_light[0] / dist, to_light[1] / dist, to_light[2] / dist];
139                let att = point_attenuation(dist, *range) * *intensity;
140                (dir, att, *color)
141            }
142            Self::Spot {
143                position, direction, color, intensity, range,
144                inner_angle, outer_angle, ..
145            } => {
146                let to_light = vec3_sub(*position, surface_pos);
147                let dist = vec3_length(to_light);
148                if dist > *range || dist < 1e-6 {
149                    return ([0.0, 0.0, 0.0], 0.0, *color);
150                }
151                let dir = [to_light[0] / dist, to_light[1] / dist, to_light[2] / dist];
152                let cos_angle = -vec3_dot(dir, *direction);
153                let cos_inner = inner_angle.cos();
154                let cos_outer = outer_angle.cos();
155                let spot_att = saturate((cos_angle - cos_outer) / (cos_inner - cos_outer).max(1e-6));
156                let att = point_attenuation(dist, *range) * spot_att * *intensity;
157                (dir, att, *color)
158            }
159            Self::Area { position, color, intensity, .. } => {
160                let to_light = vec3_sub(*position, surface_pos);
161                let dist = vec3_length(to_light);
162                if dist < 1e-6 {
163                    return ([0.0, 0.0, 1.0], *intensity, *color);
164                }
165                let dir = [to_light[0] / dist, to_light[1] / dist, to_light[2] / dist];
166                let att = *intensity / (dist * dist + 1.0);
167                (dir, att, *color)
168            }
169            Self::Ambient { color, intensity } => {
170                ([0.0, 1.0, 0.0], *intensity, *color)
171            }
172        }
173    }
174}
175
176/// Smooth distance-based attenuation for point/spot lights.
177fn point_attenuation(distance: f32, range: f32) -> f32 {
178    let ratio = clampf(distance / range, 0.0, 1.0);
179    let att_factor = 1.0 - ratio * ratio;
180    (att_factor * att_factor).max(0.0) / (distance * distance + 1.0)
181}
182
183// ---------------------------------------------------------------------------
184// Render items and sorting
185// ---------------------------------------------------------------------------
186
187/// How to sort render items within a bucket.
188#[derive(Debug, Clone, Copy, PartialEq, Eq)]
189pub enum SortMode {
190    /// Sort front-to-back (for opaque geometry, early Z rejection).
191    FrontToBack,
192    /// Sort back-to-front (for transparent geometry).
193    BackToFront,
194    /// Sort by material to minimize state changes.
195    ByMaterial,
196    /// No sorting.
197    None,
198}
199
200/// Which render bucket an item belongs to.
201#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
202pub enum RenderBucket {
203    /// Opaque geometry (rendered in geometry pass).
204    Opaque,
205    /// Transparent geometry (rendered in forward pass).
206    Transparent,
207    /// UI / overlay (rendered last, no depth test).
208    Overlay,
209    /// Debug geometry (wireframes, gizmos).
210    Debug,
211    /// Sky / background (rendered before geometry, writes to far plane).
212    Sky,
213}
214
215impl RenderBucket {
216    pub fn default_sort_mode(&self) -> SortMode {
217        match self {
218            Self::Opaque => SortMode::FrontToBack,
219            Self::Transparent => SortMode::BackToFront,
220            Self::Overlay => SortMode::None,
221            Self::Debug => SortMode::None,
222            Self::Sky => SortMode::None,
223        }
224    }
225}
226
227/// A single item to be rendered.
228#[derive(Debug, Clone)]
229pub struct RenderItem {
230    /// Unique identifier for this item.
231    pub id: u64,
232    /// World transform.
233    pub transform: Mat4,
234    /// Mesh/geometry handle (opaque).
235    pub mesh_handle: u64,
236    /// Material index into the material library.
237    pub material_index: u32,
238    /// Material sort key for batching.
239    pub sort_key: MaterialSortKey,
240    /// Distance from camera (computed during sorting).
241    pub camera_distance: f32,
242    /// Which bucket this item belongs to.
243    pub bucket: RenderBucket,
244    /// Whether this item is visible (frustum culling result).
245    pub visible: bool,
246    /// Bounding sphere center (world space).
247    pub bounds_center: [f32; 3],
248    /// Bounding sphere radius.
249    pub bounds_radius: f32,
250    /// Instance count (for instanced rendering, 1 = no instancing).
251    pub instance_count: u32,
252    /// Instance data buffer handle (if instanced).
253    pub instance_buffer: u64,
254    /// Vertex count (for stats).
255    pub vertex_count: u32,
256    /// Index count (for stats).
257    pub index_count: u32,
258    /// Whether this item uses alpha testing (cutout).
259    pub alpha_test: bool,
260    /// Whether this item is two-sided (no backface culling).
261    pub two_sided: bool,
262}
263
264impl RenderItem {
265    pub fn new(id: u64, mesh_handle: u64, material_index: u32) -> Self {
266        Self {
267            id,
268            transform: Mat4::IDENTITY,
269            mesh_handle,
270            material_index,
271            sort_key: MaterialSortKey::default(),
272            camera_distance: 0.0,
273            bucket: RenderBucket::Opaque,
274            visible: true,
275            bounds_center: [0.0; 3],
276            bounds_radius: 1.0,
277            instance_count: 1,
278            instance_buffer: 0,
279            vertex_count: 0,
280            index_count: 0,
281            alpha_test: false,
282            two_sided: false,
283        }
284    }
285
286    pub fn with_transform(mut self, t: Mat4) -> Self {
287        self.transform = t;
288        // Extract position from last column for bounds center
289        self.bounds_center = [t.cols[3][0], t.cols[3][1], t.cols[3][2]];
290        self
291    }
292
293    pub fn with_bucket(mut self, b: RenderBucket) -> Self {
294        self.bucket = b;
295        self
296    }
297
298    pub fn with_bounds(mut self, center: [f32; 3], radius: f32) -> Self {
299        self.bounds_center = center;
300        self.bounds_radius = radius;
301        self
302    }
303
304    /// Compute the distance from a camera position.
305    pub fn compute_camera_distance(&mut self, camera_pos: [f32; 3]) {
306        let dx = self.bounds_center[0] - camera_pos[0];
307        let dy = self.bounds_center[1] - camera_pos[1];
308        let dz = self.bounds_center[2] - camera_pos[2];
309        self.camera_distance = dx * dx + dy * dy + dz * dz;
310    }
311
312    /// Check if this item's bounding sphere intersects a frustum (simplified).
313    pub fn frustum_cull(&mut self, frustum_planes: &[[f32; 4]; 6]) -> bool {
314        for plane in frustum_planes {
315            let dist = plane[0] * self.bounds_center[0]
316                + plane[1] * self.bounds_center[1]
317                + plane[2] * self.bounds_center[2]
318                + plane[3];
319            if dist < -self.bounds_radius {
320                self.visible = false;
321                return false;
322            }
323        }
324        self.visible = true;
325        true
326    }
327}
328
329// ---------------------------------------------------------------------------
330// Render queue
331// ---------------------------------------------------------------------------
332
333/// Collects and sorts render items into buckets for the pipeline passes.
334#[derive(Debug)]
335pub struct RenderQueue {
336    /// All render items, partitioned by bucket.
337    pub buckets: HashMap<RenderBucket, Vec<RenderItem>>,
338    /// Sort modes per bucket (overridable).
339    pub sort_modes: HashMap<RenderBucket, SortMode>,
340    /// Camera position used for distance-based sorting.
341    pub camera_position: [f32; 3],
342    /// Frustum planes for culling [left, right, bottom, top, near, far].
343    pub frustum_planes: [[f32; 4]; 6],
344    /// Total items submitted this frame.
345    pub total_submitted: u32,
346    /// Total items visible after culling.
347    pub total_visible: u32,
348    /// Total items culled.
349    pub total_culled: u32,
350    /// Next item ID.
351    next_id: u64,
352}
353
354impl RenderQueue {
355    pub fn new() -> Self {
356        let mut sort_modes = HashMap::new();
357        sort_modes.insert(RenderBucket::Opaque, SortMode::FrontToBack);
358        sort_modes.insert(RenderBucket::Transparent, SortMode::BackToFront);
359        sort_modes.insert(RenderBucket::Overlay, SortMode::None);
360        sort_modes.insert(RenderBucket::Debug, SortMode::None);
361        sort_modes.insert(RenderBucket::Sky, SortMode::None);
362
363        Self {
364            buckets: HashMap::new(),
365            sort_modes,
366            camera_position: [0.0; 3],
367            frustum_planes: [[0.0; 4]; 6],
368            total_submitted: 0,
369            total_visible: 0,
370            total_culled: 0,
371            next_id: 1,
372        }
373    }
374
375    /// Clear all buckets for a new frame.
376    pub fn clear(&mut self) {
377        for bucket in self.buckets.values_mut() {
378            bucket.clear();
379        }
380        self.total_submitted = 0;
381        self.total_visible = 0;
382        self.total_culled = 0;
383    }
384
385    /// Set the camera position and frustum for this frame.
386    pub fn set_camera(&mut self, position: [f32; 3], frustum_planes: [[f32; 4]; 6]) {
387        self.camera_position = position;
388        self.frustum_planes = frustum_planes;
389    }
390
391    /// Submit a render item to the queue.
392    pub fn submit(&mut self, mut item: RenderItem) {
393        item.id = self.next_id;
394        self.next_id += 1;
395        self.total_submitted += 1;
396
397        // Compute distance from camera
398        item.compute_camera_distance(self.camera_position);
399
400        // Frustum cull
401        let visible = item.frustum_cull(&self.frustum_planes);
402        if visible {
403            self.total_visible += 1;
404        } else {
405            self.total_culled += 1;
406        }
407
408        let bucket = item.bucket;
409        self.buckets.entry(bucket).or_default().push(item);
410    }
411
412    /// Submit a batch of render items.
413    pub fn submit_batch(&mut self, items: Vec<RenderItem>) {
414        for item in items {
415            self.submit(item);
416        }
417    }
418
419    /// Sort all buckets according to their sort modes.
420    pub fn sort(&mut self) {
421        for (bucket, items) in &mut self.buckets {
422            // Filter to only visible items
423            items.retain(|item| item.visible);
424
425            let mode = self.sort_modes.get(bucket)
426                .copied()
427                .unwrap_or(bucket.default_sort_mode());
428
429            match mode {
430                SortMode::FrontToBack => {
431                    items.sort_by(|a, b| {
432                        a.camera_distance.partial_cmp(&b.camera_distance)
433                            .unwrap_or(std::cmp::Ordering::Equal)
434                    });
435                }
436                SortMode::BackToFront => {
437                    items.sort_by(|a, b| {
438                        b.camera_distance.partial_cmp(&a.camera_distance)
439                            .unwrap_or(std::cmp::Ordering::Equal)
440                    });
441                }
442                SortMode::ByMaterial => {
443                    items.sort_by(|a, b| a.sort_key.cmp(&b.sort_key));
444                }
445                SortMode::None => {}
446            }
447        }
448    }
449
450    /// Get items in a specific bucket (sorted).
451    pub fn get_bucket(&self, bucket: RenderBucket) -> &[RenderItem] {
452        self.buckets.get(&bucket).map(|v| v.as_slice()).unwrap_or(&[])
453    }
454
455    /// Get opaque items (convenience).
456    pub fn opaque_items(&self) -> &[RenderItem] {
457        self.get_bucket(RenderBucket::Opaque)
458    }
459
460    /// Get transparent items (convenience).
461    pub fn transparent_items(&self) -> &[RenderItem] {
462        self.get_bucket(RenderBucket::Transparent)
463    }
464
465    /// Get overlay items (convenience).
466    pub fn overlay_items(&self) -> &[RenderItem] {
467        self.get_bucket(RenderBucket::Overlay)
468    }
469
470    /// Return the total number of triangles across all visible items.
471    pub fn total_triangles(&self) -> u64 {
472        self.buckets.values()
473            .flat_map(|items| items.iter())
474            .filter(|i| i.visible)
475            .map(|i| {
476                let tris = if i.index_count > 0 {
477                    i.index_count / 3
478                } else {
479                    i.vertex_count / 3
480                };
481                tris as u64 * i.instance_count as u64
482            })
483            .sum()
484    }
485
486    /// Return the total number of draw calls across all visible items.
487    pub fn total_draw_calls(&self) -> u32 {
488        self.buckets.values()
489            .flat_map(|items| items.iter())
490            .filter(|i| i.visible)
491            .count() as u32
492    }
493}
494
495impl Default for RenderQueue {
496    fn default() -> Self {
497        Self::new()
498    }
499}
500
501// ---------------------------------------------------------------------------
502// HDR Framebuffer
503// ---------------------------------------------------------------------------
504
505/// Manages an HDR (RGBA16F) framebuffer for the lighting pass output
506/// before tone mapping.
507#[derive(Debug)]
508pub struct HdrFramebuffer {
509    /// Framebuffer object handle.
510    pub fbo_handle: u64,
511    /// HDR color texture handle.
512    pub color_handle: u64,
513    /// Depth renderbuffer handle (shared from G-Buffer or separate).
514    pub depth_handle: u64,
515    /// Current dimensions.
516    pub width: u32,
517    pub height: u32,
518    /// Whether the framebuffer has been allocated.
519    pub allocated: bool,
520    /// Generation counter.
521    pub generation: u32,
522    /// Handle counter.
523    next_handle: u64,
524    /// Optional secondary color attachment for bright pixels (bloom source).
525    pub bloom_handle: u64,
526    /// Bloom threshold (pixels above this luminance go to bloom buffer).
527    pub bloom_threshold: f32,
528    /// Number of bloom mip levels.
529    pub bloom_mip_levels: u32,
530    /// Bloom mip chain handles.
531    pub bloom_mips: Vec<u64>,
532}
533
534impl HdrFramebuffer {
535    pub fn new(width: u32, height: u32) -> Self {
536        Self {
537            fbo_handle: 0,
538            color_handle: 0,
539            depth_handle: 0,
540            width,
541            height,
542            allocated: false,
543            generation: 0,
544            next_handle: 1000,
545            bloom_handle: 0,
546            bloom_threshold: 1.0,
547            bloom_mip_levels: 5,
548            bloom_mips: Vec::new(),
549        }
550    }
551
552    /// Allocate the HDR framebuffer.
553    pub fn create(&mut self) -> Result<(), String> {
554        self.fbo_handle = self.alloc_handle();
555        self.color_handle = self.alloc_handle();
556        self.depth_handle = self.alloc_handle();
557        self.bloom_handle = self.alloc_handle();
558
559        // Create bloom mip chain
560        self.bloom_mips.clear();
561        for _ in 0..self.bloom_mip_levels {
562            let handle = self.alloc_handle();
563            self.bloom_mips.push(handle);
564        }
565
566        self.allocated = true;
567        self.generation += 1;
568        Ok(())
569    }
570
571    /// Destroy the HDR framebuffer.
572    pub fn destroy(&mut self) {
573        self.fbo_handle = 0;
574        self.color_handle = 0;
575        self.depth_handle = 0;
576        self.bloom_handle = 0;
577        self.bloom_mips.clear();
578        self.allocated = false;
579    }
580
581    /// Resize to new dimensions.
582    pub fn resize(&mut self, width: u32, height: u32) {
583        if self.width == width && self.height == height {
584            return;
585        }
586        self.width = width;
587        self.height = height;
588        if self.allocated {
589            self.generation += 1;
590            // In a real engine, reallocate textures
591        }
592    }
593
594    /// Bind as the render target for the lighting pass.
595    pub fn bind(&self) {
596        // GL: bind FBO
597    }
598
599    /// Unbind.
600    pub fn unbind(&self) {
601        // GL: bind default FBO
602    }
603
604    /// Estimated memory usage in bytes.
605    pub fn memory_bytes(&self) -> u64 {
606        let base = self.width as u64 * self.height as u64;
607        let color = base * 8; // RGBA16F = 8 bytes/pixel
608        let depth = base * 4; // D32F = 4 bytes/pixel
609        let bloom = base * 8; // RGBA16F bloom
610        let bloom_mips = (bloom as f64 * 0.334) as u64; // mip chain overhead
611        color + depth + bloom + bloom_mips
612    }
613
614    fn alloc_handle(&mut self) -> u64 {
615        let h = self.next_handle;
616        self.next_handle += 1;
617        h
618    }
619}
620
621// ---------------------------------------------------------------------------
622// Exposure / tonemapping
623// ---------------------------------------------------------------------------
624
625/// Mode for automatic exposure calculation.
626#[derive(Debug, Clone, Copy, PartialEq, Eq)]
627pub enum ExposureMode {
628    /// Fixed manual exposure.
629    Manual,
630    /// Average luminance of the scene.
631    AverageLuminance,
632    /// Histogram-based (ignore extreme bright/dark percentiles).
633    Histogram,
634    /// Spot metering (weight center of screen more).
635    SpotMetering,
636}
637
638/// Controls automatic exposure adaptation.
639#[derive(Debug, Clone)]
640pub struct ExposureController {
641    /// Current exposure mode.
642    pub mode: ExposureMode,
643    /// Current computed exposure value.
644    pub exposure: f32,
645    /// Target exposure (what we are adapting toward).
646    pub target_exposure: f32,
647    /// Manual exposure override (used when mode == Manual).
648    pub manual_exposure: f32,
649    /// Adaptation speed (how fast exposure changes, in EV/sec).
650    pub adaptation_speed_up: f32,
651    pub adaptation_speed_down: f32,
652    /// Minimum exposure (prevents screen from going too dark).
653    pub min_exposure: f32,
654    /// Maximum exposure (prevents screen from going too bright).
655    pub max_exposure: f32,
656    /// EV compensation (artist-controlled bias).
657    pub ev_compensation: f32,
658    /// Key value for average luminance mode (typically 0.18 for 18% gray).
659    pub key_value: f32,
660    /// Histogram low percentile to ignore (e.g., 0.1 = bottom 10%).
661    pub histogram_low_percentile: f32,
662    /// Histogram high percentile to ignore (e.g., 0.9 = top 10%).
663    pub histogram_high_percentile: f32,
664    /// Number of histogram bins.
665    pub histogram_bins: u32,
666    /// The histogram data (populated each frame).
667    pub histogram: Vec<u32>,
668    /// Average luminance computed last frame.
669    pub average_luminance: f32,
670    /// Spot metering radius (fraction of screen width).
671    pub spot_radius: f32,
672}
673
674impl ExposureController {
675    pub fn new() -> Self {
676        Self {
677            mode: ExposureMode::AverageLuminance,
678            exposure: 1.0,
679            target_exposure: 1.0,
680            manual_exposure: 1.0,
681            adaptation_speed_up: 2.0,
682            adaptation_speed_down: 1.0,
683            min_exposure: 0.001,
684            max_exposure: 100.0,
685            ev_compensation: 0.0,
686            key_value: 0.18,
687            histogram_low_percentile: 0.1,
688            histogram_high_percentile: 0.9,
689            histogram_bins: 256,
690            histogram: vec![0; 256],
691            average_luminance: 0.18,
692            spot_radius: 0.1,
693        }
694    }
695
696    /// Update exposure for the current frame.
697    pub fn update(&mut self, dt: f32) {
698        match self.mode {
699            ExposureMode::Manual => {
700                self.exposure = self.manual_exposure;
701                return;
702            }
703            ExposureMode::AverageLuminance => {
704                self.target_exposure = self.compute_exposure_from_luminance(self.average_luminance);
705            }
706            ExposureMode::Histogram => {
707                let avg = self.compute_histogram_average();
708                self.target_exposure = self.compute_exposure_from_luminance(avg);
709            }
710            ExposureMode::SpotMetering => {
711                self.target_exposure = self.compute_exposure_from_luminance(self.average_luminance);
712            }
713        }
714
715        // Apply EV compensation
716        self.target_exposure *= (2.0f32).powf(self.ev_compensation);
717
718        // Clamp
719        self.target_exposure = clampf(self.target_exposure, self.min_exposure, self.max_exposure);
720
721        // Adapt
722        let speed = if self.target_exposure > self.exposure {
723            self.adaptation_speed_up
724        } else {
725            self.adaptation_speed_down
726        };
727
728        let factor = 1.0 - (-speed * dt).exp();
729        self.exposure = lerpf(self.exposure, self.target_exposure, factor);
730        self.exposure = clampf(self.exposure, self.min_exposure, self.max_exposure);
731    }
732
733    /// Compute exposure from average scene luminance.
734    fn compute_exposure_from_luminance(&self, luminance: f32) -> f32 {
735        if luminance < 1e-6 {
736            return self.max_exposure;
737        }
738        self.key_value / luminance
739    }
740
741    /// Compute average luminance from histogram (ignoring extreme percentiles).
742    fn compute_histogram_average(&self) -> f32 {
743        let total: u32 = self.histogram.iter().sum();
744        if total == 0 {
745            return 0.18;
746        }
747
748        let low_count = (total as f32 * self.histogram_low_percentile) as u32;
749        let high_count = (total as f32 * self.histogram_high_percentile) as u32;
750
751        let mut running = 0u32;
752        let mut weighted_sum = 0.0f64;
753        let mut valid_count = 0u32;
754
755        for (i, &count) in self.histogram.iter().enumerate() {
756            let prev_running = running;
757            running += count;
758
759            // Skip pixels in the bottom percentile
760            if running <= low_count {
761                continue;
762            }
763            // Stop at the top percentile
764            if prev_running >= high_count {
765                break;
766            }
767
768            let contributing = if prev_running < low_count {
769                count - (low_count - prev_running)
770            } else if running > high_count {
771                high_count - prev_running
772            } else {
773                count
774            };
775
776            // Map bin index to log luminance, then to linear
777            let t = i as f32 / self.histogram_bins as f32;
778            let log_lum = t * 20.0 - 10.0; // map [0,1] to [-10, 10] log range
779            let lum = log_lum.exp();
780
781            weighted_sum += lum as f64 * contributing as f64;
782            valid_count += contributing;
783        }
784
785        if valid_count == 0 {
786            return 0.18;
787        }
788
789        (weighted_sum / valid_count as f64) as f32
790    }
791
792    /// Feed the controller a luminance value for the current frame
793    /// (computed from downsampled HDR buffer).
794    pub fn feed_luminance(&mut self, luminance: f32) {
795        self.average_luminance = luminance.max(1e-6);
796    }
797
798    /// Feed a histogram for the current frame.
799    pub fn feed_histogram(&mut self, histogram: Vec<u32>) {
800        self.histogram = histogram;
801    }
802
803    /// Get the current exposure multiplier for the tone mapping shader.
804    pub fn exposure_multiplier(&self) -> f32 {
805        self.exposure
806    }
807
808    /// Reset to defaults.
809    pub fn reset(&mut self) {
810        *self = Self::new();
811    }
812}
813
814impl Default for ExposureController {
815    fn default() -> Self {
816        Self::new()
817    }
818}
819
820// ---------------------------------------------------------------------------
821// Tone mapping
822// ---------------------------------------------------------------------------
823
824/// Available tone mapping operators.
825#[derive(Debug, Clone, Copy, PartialEq, Eq)]
826pub enum ToneMappingOperator {
827    /// Simple Reinhard: L / (1 + L)
828    Reinhard,
829    /// Extended Reinhard with white point.
830    ReinhardExtended,
831    /// ACES filmic curve (approximation).
832    AcesFilmic,
833    /// Uncharted 2 / Hable filmic.
834    Hable,
835    /// Exposure only (linear, just multiply by exposure).
836    Linear,
837    /// AgX (new standard tone mapper).
838    AgX,
839}
840
841impl ToneMappingOperator {
842    /// Apply tone mapping to a linear HDR color.
843    pub fn apply(&self, color: [f32; 3], exposure: f32) -> [f32; 3] {
844        let c = [
845            color[0] * exposure,
846            color[1] * exposure,
847            color[2] * exposure,
848        ];
849        match self {
850            Self::Reinhard => Self::reinhard(c),
851            Self::ReinhardExtended => Self::reinhard_extended(c, 4.0),
852            Self::AcesFilmic => Self::aces_filmic(c),
853            Self::Hable => Self::hable(c),
854            Self::Linear => [
855                saturate(c[0]),
856                saturate(c[1]),
857                saturate(c[2]),
858            ],
859            Self::AgX => Self::agx(c),
860        }
861    }
862
863    fn reinhard(c: [f32; 3]) -> [f32; 3] {
864        [
865            c[0] / (1.0 + c[0]),
866            c[1] / (1.0 + c[1]),
867            c[2] / (1.0 + c[2]),
868        ]
869    }
870
871    fn reinhard_extended(c: [f32; 3], white_point: f32) -> [f32; 3] {
872        let wp2 = white_point * white_point;
873        [
874            c[0] * (1.0 + c[0] / wp2) / (1.0 + c[0]),
875            c[1] * (1.0 + c[1] / wp2) / (1.0 + c[1]),
876            c[2] * (1.0 + c[2] / wp2) / (1.0 + c[2]),
877        ]
878    }
879
880    fn aces_filmic(c: [f32; 3]) -> [f32; 3] {
881        // Stephen Hill's ACES approximation
882        fn aces_channel(x: f32) -> f32 {
883            let a = 2.51;
884            let b = 0.03;
885            let c = 2.43;
886            let d = 0.59;
887            let e = 0.14;
888            saturate((x * (a * x + b)) / (x * (c * x + d) + e))
889        }
890        [aces_channel(c[0]), aces_channel(c[1]), aces_channel(c[2])]
891    }
892
893    fn hable(c: [f32; 3]) -> [f32; 3] {
894        fn hable_partial(x: f32) -> f32 {
895            let a = 0.15;
896            let b = 0.50;
897            let cc = 0.10;
898            let d = 0.20;
899            let e = 0.02;
900            let f = 0.30;
901            ((x * (a * x + cc * b) + d * e) / (x * (a * x + b) + d * f)) - e / f
902        }
903        let exposure_bias = 2.0;
904        let white_scale = 1.0 / hable_partial(11.2);
905        [
906            hable_partial(c[0] * exposure_bias) * white_scale,
907            hable_partial(c[1] * exposure_bias) * white_scale,
908            hable_partial(c[2] * exposure_bias) * white_scale,
909        ]
910    }
911
912    fn agx(c: [f32; 3]) -> [f32; 3] {
913        // Simplified AgX-like curve
914        fn agx_channel(x: f32) -> f32 {
915            let x = x.max(0.0);
916            let a = x.ln().max(-10.0).min(10.0);
917            let mapped = 0.5 + 0.5 * (a * 0.3).tanh();
918            mapped
919        }
920        [agx_channel(c[0]), agx_channel(c[1]), agx_channel(c[2])]
921    }
922
923    /// Generate GLSL code for this tone mapping operator.
924    pub fn glsl_function(&self) -> &'static str {
925        match self {
926            Self::Reinhard => {
927                r#"vec3 tonemap(vec3 c) { return c / (1.0 + c); }"#
928            }
929            Self::ReinhardExtended => {
930                r#"vec3 tonemap(vec3 c) {
931    float wp2 = 16.0;
932    return c * (1.0 + c / wp2) / (1.0 + c);
933}"#
934            }
935            Self::AcesFilmic => {
936                r#"vec3 tonemap(vec3 x) {
937    float a = 2.51; float b = 0.03;
938    float c = 2.43; float d = 0.59; float e = 0.14;
939    return clamp((x*(a*x+b))/(x*(c*x+d)+e), 0.0, 1.0);
940}"#
941            }
942            Self::Hable => {
943                r#"float hable(float x) {
944    float A=0.15,B=0.50,C=0.10,D=0.20,E=0.02,F=0.30;
945    return ((x*(A*x+C*B)+D*E)/(x*(A*x+B)+D*F))-E/F;
946}
947vec3 tonemap(vec3 c) {
948    float w = 1.0/hable(11.2);
949    return vec3(hable(c.x*2.0)*w, hable(c.y*2.0)*w, hable(c.z*2.0)*w);
950}"#
951            }
952            Self::Linear => {
953                r#"vec3 tonemap(vec3 c) { return clamp(c, 0.0, 1.0); }"#
954            }
955            Self::AgX => {
956                r#"vec3 tonemap(vec3 c) {
957    vec3 a = log(max(c, vec3(0.0001)));
958    return 0.5 + 0.5 * tanh(a * 0.3);
959}"#
960            }
961        }
962    }
963}
964
965// ---------------------------------------------------------------------------
966// Depth pre-pass
967// ---------------------------------------------------------------------------
968
969/// Depth pre-pass that writes only depth, enabling early-Z rejection
970/// in the subsequent geometry pass.
971#[derive(Debug)]
972pub struct DepthPrePass {
973    /// Whether the depth pre-pass is enabled.
974    pub enabled: bool,
975    /// Shader program handle for the depth-only pass.
976    pub shader_handle: u64,
977    /// Items to render in this pass (opaque, front-to-back sorted).
978    pub items: Vec<u64>,
979    /// Whether to use the depth from the G-Buffer or a separate depth buffer.
980    pub use_gbuffer_depth: bool,
981    /// Statistics: number of items rendered in the pre-pass.
982    pub rendered_count: u32,
983    /// Time taken for the depth pre-pass (microseconds).
984    pub time_us: u64,
985    /// Depth function (Less, LessEqual, etc.).
986    pub depth_func: DepthFunction,
987    /// Whether depth writing is enabled.
988    pub depth_write: bool,
989    /// Whether to do alpha test in the depth pre-pass (for cutout materials).
990    pub alpha_test_in_prepass: bool,
991    /// Alpha test threshold.
992    pub alpha_threshold: f32,
993}
994
995/// Depth comparison function.
996#[derive(Debug, Clone, Copy, PartialEq, Eq)]
997pub enum DepthFunction {
998    Less,
999    LessEqual,
1000    Greater,
1001    GreaterEqual,
1002    Equal,
1003    NotEqual,
1004    Always,
1005    Never,
1006}
1007
1008impl DepthPrePass {
1009    pub fn new() -> Self {
1010        Self {
1011            enabled: true,
1012            shader_handle: 0,
1013            items: Vec::new(),
1014            use_gbuffer_depth: true,
1015            rendered_count: 0,
1016            time_us: 0,
1017            depth_func: DepthFunction::Less,
1018            depth_write: true,
1019            alpha_test_in_prepass: false,
1020            alpha_threshold: 0.5,
1021        }
1022    }
1023
1024    /// Execute the depth pre-pass using items from the render queue.
1025    pub fn execute(&mut self, queue: &RenderQueue, _gbuffer: &mut GBuffer) {
1026        let start = std::time::Instant::now();
1027
1028        self.items.clear();
1029        self.rendered_count = 0;
1030
1031        if !self.enabled {
1032            return;
1033        }
1034
1035        let opaque = queue.opaque_items();
1036        for item in opaque {
1037            if !item.visible {
1038                continue;
1039            }
1040            // Skip alpha-tested items unless we handle them
1041            if item.alpha_test && !self.alpha_test_in_prepass {
1042                continue;
1043            }
1044            self.items.push(item.id);
1045            self.rendered_count += 1;
1046            // In a real engine: bind depth shader, set uniforms, draw
1047        }
1048
1049        self.time_us = start.elapsed().as_micros() as u64;
1050    }
1051
1052    /// Get GLSL source for the depth-only vertex shader.
1053    pub fn vertex_shader() -> &'static str {
1054        r#"#version 330 core
1055layout(location = 0) in vec3 a_position;
1056uniform mat4 u_model;
1057uniform mat4 u_view_projection;
1058void main() {
1059    gl_Position = u_view_projection * u_model * vec4(a_position, 1.0);
1060}
1061"#
1062    }
1063
1064    /// Get GLSL source for the depth-only fragment shader (alpha test variant).
1065    pub fn fragment_shader_alpha_test() -> &'static str {
1066        r#"#version 330 core
1067uniform sampler2D u_albedo_tex;
1068uniform float u_alpha_threshold;
1069in vec2 v_texcoord;
1070void main() {
1071    float alpha = texture(u_albedo_tex, v_texcoord).a;
1072    if (alpha < u_alpha_threshold) discard;
1073}
1074"#
1075    }
1076}
1077
1078impl Default for DepthPrePass {
1079    fn default() -> Self {
1080        Self::new()
1081    }
1082}
1083
1084// ---------------------------------------------------------------------------
1085// Geometry pass
1086// ---------------------------------------------------------------------------
1087
1088/// The geometry pass fills the G-Buffer with per-pixel data from scene geometry.
1089#[derive(Debug)]
1090pub struct GeometryPass {
1091    /// Whether this pass is enabled.
1092    pub enabled: bool,
1093    /// Shader program handle.
1094    pub shader_handle: u64,
1095    /// Number of draw calls executed.
1096    pub draw_call_count: u32,
1097    /// Number of triangles rendered.
1098    pub triangle_count: u64,
1099    /// Time taken (microseconds).
1100    pub time_us: u64,
1101    /// Whether depth testing is enabled (should be, using pre-pass depth).
1102    pub depth_test: bool,
1103    /// Depth function for geometry pass (Equal if depth pre-pass ran, Less otherwise).
1104    pub depth_func: DepthFunction,
1105    /// Whether to write depth (false if depth pre-pass already wrote it).
1106    pub depth_write: bool,
1107    /// Whether backface culling is enabled.
1108    pub backface_cull: bool,
1109    /// Polygon offset for depth fighting prevention.
1110    pub polygon_offset: Option<(f32, f32)>,
1111    /// Whether instanced rendering is used.
1112    pub use_instancing: bool,
1113    /// Maximum instances per draw call.
1114    pub max_instances_per_draw: u32,
1115}
1116
1117impl GeometryPass {
1118    pub fn new() -> Self {
1119        Self {
1120            enabled: true,
1121            shader_handle: 0,
1122            draw_call_count: 0,
1123            triangle_count: 0,
1124            time_us: 0,
1125            depth_test: true,
1126            depth_func: DepthFunction::Equal,
1127            depth_write: false,
1128            backface_cull: true,
1129            polygon_offset: None,
1130            use_instancing: true,
1131            max_instances_per_draw: 1024,
1132        }
1133    }
1134
1135    /// Execute the geometry pass.
1136    pub fn execute(&mut self, queue: &RenderQueue, gbuffer: &mut GBuffer) {
1137        let start = std::time::Instant::now();
1138
1139        self.draw_call_count = 0;
1140        self.triangle_count = 0;
1141
1142        if !self.enabled {
1143            return;
1144        }
1145
1146        // Bind G-Buffer as render target
1147        let _ = gbuffer.bind();
1148
1149        // Clear G-Buffer
1150        gbuffer.clear_all();
1151
1152        let opaque = queue.opaque_items();
1153        for item in opaque {
1154            if !item.visible {
1155                continue;
1156            }
1157
1158            self.draw_call_count += 1;
1159            let tris = if item.index_count > 0 {
1160                item.index_count / 3
1161            } else {
1162                item.vertex_count / 3
1163            };
1164            self.triangle_count += tris as u64 * item.instance_count as u64;
1165
1166            // In a real engine:
1167            // 1. Bind geometry pass shader
1168            // 2. Set model/view/projection uniforms from item.transform
1169            // 3. Bind material textures
1170            // 4. Draw mesh (or draw instanced if instance_count > 1)
1171        }
1172
1173        gbuffer.unbind();
1174        gbuffer.stats.geometry_draw_calls = self.draw_call_count;
1175
1176        self.time_us = start.elapsed().as_micros() as u64;
1177    }
1178
1179    /// Generate the geometry pass vertex shader.
1180    pub fn vertex_shader() -> &'static str {
1181        r#"#version 330 core
1182layout(location = 0) in vec3 a_position;
1183layout(location = 1) in vec3 a_normal;
1184layout(location = 2) in vec2 a_texcoord;
1185layout(location = 3) in vec3 a_tangent;
1186
1187uniform mat4 u_model;
1188uniform mat4 u_view;
1189uniform mat4 u_projection;
1190uniform mat3 u_normal_matrix;
1191
1192out vec3 v_world_pos;
1193out vec3 v_normal;
1194out vec2 v_texcoord;
1195out vec3 v_tangent;
1196
1197void main() {
1198    vec4 world_pos = u_model * vec4(a_position, 1.0);
1199    v_world_pos = world_pos.xyz;
1200    v_normal = u_normal_matrix * a_normal;
1201    v_texcoord = a_texcoord;
1202    v_tangent = u_normal_matrix * a_tangent;
1203    gl_Position = u_projection * u_view * world_pos;
1204}
1205"#
1206    }
1207
1208    /// Generate the geometry pass fragment shader.
1209    pub fn fragment_shader() -> &'static str {
1210        r#"#version 330 core
1211in vec3 v_world_pos;
1212in vec3 v_normal;
1213in vec2 v_texcoord;
1214in vec3 v_tangent;
1215
1216layout(location = 0) out vec4 out_position;
1217layout(location = 1) out vec2 out_normal;
1218layout(location = 2) out vec4 out_albedo;
1219layout(location = 3) out vec4 out_emission;
1220layout(location = 4) out float out_matid;
1221layout(location = 5) out float out_roughness;
1222layout(location = 6) out float out_metallic;
1223
1224uniform sampler2D u_albedo_map;
1225uniform sampler2D u_normal_map;
1226uniform sampler2D u_roughness_map;
1227uniform sampler2D u_metallic_map;
1228uniform sampler2D u_emission_map;
1229uniform vec4 u_albedo_color;
1230uniform float u_roughness;
1231uniform float u_metallic;
1232uniform vec3 u_emission;
1233uniform float u_material_id;
1234uniform bool u_has_normal_map;
1235
1236// Octahedral encoding
1237vec2 oct_encode(vec3 n) {
1238    float sum = abs(n.x) + abs(n.y) + abs(n.z);
1239    vec2 o = n.xy / sum;
1240    if (n.z < 0.0) {
1241        o = (1.0 - abs(o.yx)) * vec2(o.x >= 0.0 ? 1.0 : -1.0, o.y >= 0.0 ? 1.0 : -1.0);
1242    }
1243    return o;
1244}
1245
1246void main() {
1247    out_position = vec4(v_world_pos, 1.0);
1248
1249    vec3 N = normalize(v_normal);
1250    if (u_has_normal_map) {
1251        vec3 T = normalize(v_tangent);
1252        vec3 B = cross(N, T);
1253        mat3 TBN = mat3(T, B, N);
1254        vec3 tangent_normal = texture(u_normal_map, v_texcoord).xyz * 2.0 - 1.0;
1255        N = normalize(TBN * tangent_normal);
1256    }
1257    out_normal = oct_encode(N);
1258
1259    out_albedo = texture(u_albedo_map, v_texcoord) * u_albedo_color;
1260    out_emission = vec4(u_emission + texture(u_emission_map, v_texcoord).rgb, 1.0);
1261    out_matid = u_material_id / 255.0;
1262    out_roughness = texture(u_roughness_map, v_texcoord).r * u_roughness;
1263    out_metallic = texture(u_metallic_map, v_texcoord).r * u_metallic;
1264}
1265"#
1266    }
1267}
1268
1269impl Default for GeometryPass {
1270    fn default() -> Self {
1271        Self::new()
1272    }
1273}
1274
1275// ---------------------------------------------------------------------------
1276// Lighting pass
1277// ---------------------------------------------------------------------------
1278
1279/// The lighting pass reads from the G-Buffer and evaluates all lights
1280/// to produce an HDR color result.
1281#[derive(Debug)]
1282pub struct LightingPass {
1283    /// Whether this pass is enabled.
1284    pub enabled: bool,
1285    /// Shader program handle.
1286    pub shader_handle: u64,
1287    /// All lights in the scene.
1288    pub lights: Vec<LightType>,
1289    /// Maximum number of lights to evaluate per pixel.
1290    pub max_lights_per_pixel: u32,
1291    /// Whether to use light volumes (render spheres/cones for point/spot lights).
1292    pub use_light_volumes: bool,
1293    /// Time taken (microseconds).
1294    pub time_us: u64,
1295    /// Number of lights evaluated this frame.
1296    pub lights_evaluated: u32,
1297    /// Ambient color (added to all pixels).
1298    pub ambient_color: [f32; 3],
1299    /// Ambient intensity.
1300    pub ambient_intensity: f32,
1301    /// Whether to apply SSAO (screen-space ambient occlusion).
1302    pub ssao_enabled: bool,
1303    /// SSAO radius.
1304    pub ssao_radius: f32,
1305    /// SSAO bias.
1306    pub ssao_bias: f32,
1307    /// SSAO kernel size.
1308    pub ssao_kernel_size: u32,
1309    /// Environment map handle (for IBL).
1310    pub environment_map: u64,
1311    /// Whether image-based lighting is enabled.
1312    pub ibl_enabled: bool,
1313    /// IBL intensity multiplier.
1314    pub ibl_intensity: f32,
1315}
1316
1317impl LightingPass {
1318    pub fn new() -> Self {
1319        Self {
1320            enabled: true,
1321            shader_handle: 0,
1322            lights: Vec::new(),
1323            max_lights_per_pixel: 128,
1324            use_light_volumes: false,
1325            time_us: 0,
1326            lights_evaluated: 0,
1327            ambient_color: [0.03, 0.03, 0.05],
1328            ambient_intensity: 1.0,
1329            ssao_enabled: false,
1330            ssao_radius: 0.5,
1331            ssao_bias: 0.025,
1332            ssao_kernel_size: 64,
1333            environment_map: 0,
1334            ibl_enabled: false,
1335            ibl_intensity: 1.0,
1336        }
1337    }
1338
1339    /// Add a light to the scene.
1340    pub fn add_light(&mut self, light: LightType) {
1341        self.lights.push(light);
1342    }
1343
1344    /// Remove all lights.
1345    pub fn clear_lights(&mut self) {
1346        self.lights.clear();
1347    }
1348
1349    /// Execute the lighting pass.
1350    pub fn execute(
1351        &mut self,
1352        gbuffer: &GBuffer,
1353        hdr_fb: &HdrFramebuffer,
1354        view_matrix: &Mat4,
1355        projection_matrix: &Mat4,
1356        camera_pos: [f32; 3],
1357    ) {
1358        let start = std::time::Instant::now();
1359
1360        if !self.enabled {
1361            return;
1362        }
1363
1364        // Bind HDR framebuffer as render target
1365        hdr_fb.bind();
1366
1367        // Bind G-Buffer textures for reading
1368        let _bindings = gbuffer.bind_for_reading();
1369
1370        // In a real engine:
1371        // 1. Bind lighting shader
1372        // 2. Set G-Buffer sampler uniforms
1373        // 3. Set camera uniforms (inverse VP matrix, camera position)
1374        // 4. Upload light data (UBO or SSBO)
1375        // 5. Draw fullscreen quad
1376
1377        self.lights_evaluated = self.lights.len().min(self.max_lights_per_pixel as usize) as u32;
1378
1379        let _ = view_matrix;
1380        let _ = projection_matrix;
1381        let _ = camera_pos;
1382
1383        hdr_fb.unbind();
1384
1385        self.time_us = start.elapsed().as_micros() as u64;
1386    }
1387
1388    /// Evaluate PBR lighting at a single point (for CPU-side validation).
1389    pub fn evaluate_pbr(
1390        &self,
1391        position: [f32; 3],
1392        normal: [f32; 3],
1393        albedo: [f32; 3],
1394        roughness: f32,
1395        metallic: f32,
1396        camera_pos: [f32; 3],
1397    ) -> [f32; 3] {
1398        let v = super::vec3_normalize(vec3_sub(camera_pos, position));
1399        let mut total = [
1400            self.ambient_color[0] * self.ambient_intensity * albedo[0],
1401            self.ambient_color[1] * self.ambient_intensity * albedo[1],
1402            self.ambient_color[2] * self.ambient_intensity * albedo[2],
1403        ];
1404
1405        for light in &self.lights {
1406            let (l, attenuation, light_color) = light.evaluate(position);
1407            if attenuation < 1e-6 {
1408                continue;
1409            }
1410
1411            let n_dot_l = vec3_dot(normal, l).max(0.0);
1412            if n_dot_l < 1e-6 {
1413                continue;
1414            }
1415
1416            // Simplified Cook-Torrance BRDF
1417            let h = super::vec3_normalize(super::vec3_add(v, l));
1418            let n_dot_h = vec3_dot(normal, h).max(0.0);
1419            let n_dot_v = vec3_dot(normal, v).max(0.001);
1420
1421            // GGX distribution
1422            let a = roughness * roughness;
1423            let a2 = a * a;
1424            let denom = n_dot_h * n_dot_h * (a2 - 1.0) + 1.0;
1425            let d = a2 / (std::f32::consts::PI * denom * denom).max(1e-6);
1426
1427            // Schlick-GGX geometry
1428            let k = (roughness + 1.0) * (roughness + 1.0) / 8.0;
1429            let g1_v = n_dot_v / (n_dot_v * (1.0 - k) + k);
1430            let g1_l = n_dot_l / (n_dot_l * (1.0 - k) + k);
1431            let g = g1_v * g1_l;
1432
1433            // Fresnel (Schlick)
1434            let f0_base = lerpf(0.04, 1.0, metallic);
1435            let f0 = [
1436                lerpf(0.04, albedo[0], metallic),
1437                lerpf(0.04, albedo[1], metallic),
1438                lerpf(0.04, albedo[2], metallic),
1439            ];
1440            let _ = f0_base;
1441            let v_dot_h = vec3_dot(v, h).max(0.0);
1442            let fresnel_factor = (1.0 - v_dot_h).powf(5.0);
1443            let f = [
1444                f0[0] + (1.0 - f0[0]) * fresnel_factor,
1445                f0[1] + (1.0 - f0[1]) * fresnel_factor,
1446                f0[2] + (1.0 - f0[2]) * fresnel_factor,
1447            ];
1448
1449            // Specular BRDF: DFG / (4 * NdotV * NdotL)
1450            let spec_denom = (4.0 * n_dot_v * n_dot_l).max(1e-6);
1451            let spec = [
1452                d * g * f[0] / spec_denom,
1453                d * g * f[1] / spec_denom,
1454                d * g * f[2] / spec_denom,
1455            ];
1456
1457            // Diffuse (Lambertian)
1458            let kd = [
1459                (1.0 - f[0]) * (1.0 - metallic),
1460                (1.0 - f[1]) * (1.0 - metallic),
1461                (1.0 - f[2]) * (1.0 - metallic),
1462            ];
1463            let diffuse = [
1464                kd[0] * albedo[0] / std::f32::consts::PI,
1465                kd[1] * albedo[1] / std::f32::consts::PI,
1466                kd[2] * albedo[2] / std::f32::consts::PI,
1467            ];
1468
1469            // Total contribution
1470            for i in 0..3 {
1471                total[i] += (diffuse[i] + spec[i]) * light_color[i] * attenuation * n_dot_l;
1472            }
1473        }
1474
1475        total
1476    }
1477
1478    /// Generate the lighting pass fragment shader.
1479    pub fn fragment_shader() -> &'static str {
1480        r#"#version 330 core
1481in vec2 v_texcoord;
1482out vec4 frag_color;
1483
1484uniform sampler2D g_position;
1485uniform sampler2D g_normal;
1486uniform sampler2D g_albedo;
1487uniform sampler2D g_emission;
1488uniform sampler2D g_roughness;
1489uniform sampler2D g_metallic;
1490uniform sampler2D g_depth;
1491
1492uniform vec3 u_camera_pos;
1493uniform mat4 u_inv_view_proj;
1494
1495struct Light {
1496    int type;       // 0=dir, 1=point, 2=spot
1497    vec3 position;
1498    vec3 direction;
1499    vec3 color;
1500    float intensity;
1501    float range;
1502    float inner_angle;
1503    float outer_angle;
1504};
1505
1506#define MAX_LIGHTS 128
1507uniform Light u_lights[MAX_LIGHTS];
1508uniform int u_light_count;
1509uniform vec3 u_ambient;
1510
1511const float PI = 3.14159265359;
1512
1513vec3 oct_decode(vec2 o) {
1514    float z = 1.0 - abs(o.x) - abs(o.y);
1515    vec2 xy = z >= 0.0 ? o : (1.0 - abs(o.yx)) * vec2(o.x >= 0.0 ? 1.0 : -1.0, o.y >= 0.0 ? 1.0 : -1.0);
1516    return normalize(vec3(xy, z));
1517}
1518
1519float ggx_distribution(float NdotH, float roughness) {
1520    float a2 = roughness * roughness * roughness * roughness;
1521    float d = NdotH * NdotH * (a2 - 1.0) + 1.0;
1522    return a2 / (PI * d * d);
1523}
1524
1525float geometry_schlick(float NdotV, float NdotL, float roughness) {
1526    float k = (roughness + 1.0) * (roughness + 1.0) / 8.0;
1527    float g1 = NdotV / (NdotV * (1.0 - k) + k);
1528    float g2 = NdotL / (NdotL * (1.0 - k) + k);
1529    return g1 * g2;
1530}
1531
1532vec3 fresnel_schlick(float cosTheta, vec3 F0) {
1533    return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
1534}
1535
1536void main() {
1537    vec3 pos = texture(g_position, v_texcoord).xyz;
1538    vec3 N = oct_decode(texture(g_normal, v_texcoord).xy);
1539    vec4 albedo_alpha = texture(g_albedo, v_texcoord);
1540    vec3 albedo = albedo_alpha.rgb;
1541    vec3 emission = texture(g_emission, v_texcoord).rgb;
1542    float roughness = texture(g_roughness, v_texcoord).r;
1543    float metallic = texture(g_metallic, v_texcoord).r;
1544
1545    vec3 V = normalize(u_camera_pos - pos);
1546    vec3 F0 = mix(vec3(0.04), albedo, metallic);
1547
1548    vec3 Lo = vec3(0.0);
1549    for (int i = 0; i < u_light_count && i < MAX_LIGHTS; i++) {
1550        vec3 L;
1551        float attenuation;
1552
1553        if (u_lights[i].type == 0) {
1554            L = -u_lights[i].direction;
1555            attenuation = u_lights[i].intensity;
1556        } else {
1557            vec3 toLight = u_lights[i].position - pos;
1558            float dist = length(toLight);
1559            L = toLight / dist;
1560            float r = dist / u_lights[i].range;
1561            attenuation = u_lights[i].intensity * max((1.0 - r*r), 0.0) / (dist*dist + 1.0);
1562
1563            if (u_lights[i].type == 2) {
1564                float cosAngle = dot(-L, u_lights[i].direction);
1565                float spot = clamp((cosAngle - cos(u_lights[i].outer_angle)) /
1566                    (cos(u_lights[i].inner_angle) - cos(u_lights[i].outer_angle)), 0.0, 1.0);
1567                attenuation *= spot;
1568            }
1569        }
1570
1571        vec3 H = normalize(V + L);
1572        float NdotL = max(dot(N, L), 0.0);
1573        float NdotH = max(dot(N, H), 0.0);
1574        float NdotV = max(dot(N, V), 0.001);
1575
1576        float D = ggx_distribution(NdotH, roughness);
1577        float G = geometry_schlick(NdotV, NdotL, roughness);
1578        vec3 F = fresnel_schlick(max(dot(H, V), 0.0), F0);
1579
1580        vec3 spec = D * G * F / max(4.0 * NdotV * NdotL, 0.001);
1581        vec3 kD = (1.0 - F) * (1.0 - metallic);
1582        vec3 diffuse = kD * albedo / PI;
1583
1584        Lo += (diffuse + spec) * u_lights[i].color * attenuation * NdotL;
1585    }
1586
1587    vec3 ambient = u_ambient * albedo;
1588    vec3 color = ambient + Lo + emission;
1589    frag_color = vec4(color, 1.0);
1590}
1591"#
1592    }
1593}
1594
1595impl Default for LightingPass {
1596    fn default() -> Self {
1597        Self::new()
1598    }
1599}
1600
1601// ---------------------------------------------------------------------------
1602// Forward pass (transparent objects)
1603// ---------------------------------------------------------------------------
1604
1605/// Forward rendering pass for transparent objects, particles, and
1606/// alpha-blended glyphs. These are sorted back-to-front and rendered
1607/// after the deferred lighting pass.
1608#[derive(Debug)]
1609pub struct ForwardPass {
1610    /// Whether this pass is enabled.
1611    pub enabled: bool,
1612    /// Shader program handle.
1613    pub shader_handle: u64,
1614    /// Draw call count this frame.
1615    pub draw_call_count: u32,
1616    /// Triangle count this frame.
1617    pub triangle_count: u64,
1618    /// Time taken (microseconds).
1619    pub time_us: u64,
1620    /// Whether depth testing is enabled (yes, read-only).
1621    pub depth_test: bool,
1622    /// Whether depth writing is enabled (usually no for transparent objects).
1623    pub depth_write: bool,
1624    /// Blend mode.
1625    pub blend_mode: ForwardBlendMode,
1626    /// Whether to use premultiplied alpha.
1627    pub premultiplied_alpha: bool,
1628    /// Whether to render particles in this pass.
1629    pub render_particles: bool,
1630    /// Whether to render alpha-blended text/glyphs in this pass.
1631    pub render_glyphs: bool,
1632    /// Maximum number of transparent layers for OIT (Order-Independent Transparency).
1633    /// 0 = disabled (use simple sorted blending).
1634    pub oit_layers: u32,
1635}
1636
1637/// Blend modes for the forward pass.
1638#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1639pub enum ForwardBlendMode {
1640    /// Standard alpha blending: src*alpha + dst*(1-alpha).
1641    AlphaBlend,
1642    /// Additive blending: src + dst.
1643    Additive,
1644    /// Premultiplied alpha: src + dst*(1-alpha).
1645    PremultipliedAlpha,
1646    /// Multiply: src * dst.
1647    Multiply,
1648}
1649
1650impl ForwardPass {
1651    pub fn new() -> Self {
1652        Self {
1653            enabled: true,
1654            shader_handle: 0,
1655            draw_call_count: 0,
1656            triangle_count: 0,
1657            time_us: 0,
1658            depth_test: true,
1659            depth_write: false,
1660            blend_mode: ForwardBlendMode::AlphaBlend,
1661            premultiplied_alpha: false,
1662            render_particles: true,
1663            render_glyphs: true,
1664            oit_layers: 0,
1665        }
1666    }
1667
1668    /// Execute the forward pass.
1669    pub fn execute(
1670        &mut self,
1671        queue: &RenderQueue,
1672        hdr_fb: &HdrFramebuffer,
1673        _gbuffer: &GBuffer,
1674        _lights: &[LightType],
1675        _view: &Mat4,
1676        _proj: &Mat4,
1677        _camera_pos: [f32; 3],
1678    ) {
1679        let start = std::time::Instant::now();
1680
1681        self.draw_call_count = 0;
1682        self.triangle_count = 0;
1683
1684        if !self.enabled {
1685            return;
1686        }
1687
1688        hdr_fb.bind();
1689
1690        // Render transparent items back-to-front
1691        let transparent = queue.transparent_items();
1692        for item in transparent {
1693            if !item.visible {
1694                continue;
1695            }
1696            self.draw_call_count += 1;
1697            let tris = if item.index_count > 0 {
1698                item.index_count / 3
1699            } else {
1700                item.vertex_count / 3
1701            };
1702            self.triangle_count += tris as u64 * item.instance_count as u64;
1703        }
1704
1705        // Render overlay items
1706        let overlay = queue.overlay_items();
1707        for item in overlay {
1708            if !item.visible {
1709                continue;
1710            }
1711            self.draw_call_count += 1;
1712        }
1713
1714        hdr_fb.unbind();
1715
1716        self.time_us = start.elapsed().as_micros() as u64;
1717    }
1718}
1719
1720impl Default for ForwardPass {
1721    fn default() -> Self {
1722        Self::new()
1723    }
1724}
1725
1726// ---------------------------------------------------------------------------
1727// Post-process pass
1728// ---------------------------------------------------------------------------
1729
1730/// Post-processing pass: bloom, tone mapping, and anti-aliasing.
1731#[derive(Debug)]
1732pub struct PostProcessPass {
1733    /// Whether post-processing is enabled.
1734    pub enabled: bool,
1735    /// Shader program handle.
1736    pub shader_handle: u64,
1737    /// Time taken (microseconds).
1738    pub time_us: u64,
1739    /// Bloom settings.
1740    pub bloom_enabled: bool,
1741    pub bloom_intensity: f32,
1742    pub bloom_threshold: f32,
1743    pub bloom_radius: f32,
1744    pub bloom_mip_count: u32,
1745    /// Tone mapping operator.
1746    pub tone_mapping: ToneMappingOperator,
1747    /// Exposure controller.
1748    pub exposure: ExposureController,
1749    /// Gamma correction value.
1750    pub gamma: f32,
1751    /// Vignette settings.
1752    pub vignette_enabled: bool,
1753    pub vignette_intensity: f32,
1754    pub vignette_smoothness: f32,
1755    /// Chromatic aberration.
1756    pub chromatic_aberration_enabled: bool,
1757    pub chromatic_aberration_intensity: f32,
1758    /// Film grain.
1759    pub film_grain_enabled: bool,
1760    pub film_grain_intensity: f32,
1761    /// Dithering (reduces banding in gradients).
1762    pub dithering_enabled: bool,
1763    /// Color grading LUT texture handle.
1764    pub color_lut_handle: u64,
1765    pub color_lut_enabled: bool,
1766    /// Saturation adjustment (1.0 = no change).
1767    pub saturation: f32,
1768    /// Contrast adjustment (1.0 = no change).
1769    pub contrast: f32,
1770    /// Brightness adjustment (0.0 = no change).
1771    pub brightness: f32,
1772}
1773
1774impl PostProcessPass {
1775    pub fn new() -> Self {
1776        Self {
1777            enabled: true,
1778            shader_handle: 0,
1779            time_us: 0,
1780            bloom_enabled: true,
1781            bloom_intensity: 0.5,
1782            bloom_threshold: 1.0,
1783            bloom_radius: 5.0,
1784            bloom_mip_count: 5,
1785            tone_mapping: ToneMappingOperator::AcesFilmic,
1786            exposure: ExposureController::new(),
1787            gamma: 2.2,
1788            vignette_enabled: false,
1789            vignette_intensity: 0.3,
1790            vignette_smoothness: 2.0,
1791            chromatic_aberration_enabled: false,
1792            chromatic_aberration_intensity: 0.005,
1793            film_grain_enabled: false,
1794            film_grain_intensity: 0.05,
1795            dithering_enabled: true,
1796            color_lut_handle: 0,
1797            color_lut_enabled: false,
1798            saturation: 1.0,
1799            contrast: 1.0,
1800            brightness: 0.0,
1801        }
1802    }
1803
1804    /// Execute the post-processing pass.
1805    pub fn execute(&mut self, hdr_fb: &HdrFramebuffer, _viewport: &Viewport, dt: f32) {
1806        let start = std::time::Instant::now();
1807
1808        if !self.enabled {
1809            return;
1810        }
1811
1812        // Update exposure
1813        self.exposure.update(dt);
1814
1815        // In a real engine:
1816        // 1. Extract bright pixels for bloom (threshold)
1817        // 2. Downsample bloom chain
1818        // 3. Upsample + blur bloom chain
1819        // 4. Composite bloom + HDR color
1820        // 5. Tone map
1821        // 6. Gamma correct
1822        // 7. Apply vignette, chromatic aberration, film grain
1823        // 8. Apply color grading LUT
1824        // 9. Dither output
1825
1826        let _ = hdr_fb;
1827
1828        self.time_us = start.elapsed().as_micros() as u64;
1829    }
1830
1831    /// Apply bloom extraction (returns which pixels are bright enough).
1832    pub fn bloom_extract(&self, color: [f32; 3]) -> [f32; 3] {
1833        let luminance = 0.2126 * color[0] + 0.7152 * color[1] + 0.0722 * color[2];
1834        if luminance > self.bloom_threshold {
1835            let excess = luminance - self.bloom_threshold;
1836            let factor = excess / luminance.max(1e-6);
1837            [
1838                color[0] * factor * self.bloom_intensity,
1839                color[1] * factor * self.bloom_intensity,
1840                color[2] * factor * self.bloom_intensity,
1841            ]
1842        } else {
1843            [0.0, 0.0, 0.0]
1844        }
1845    }
1846
1847    /// Apply vignette effect.
1848    pub fn apply_vignette(&self, color: [f32; 3], uv: [f32; 2]) -> [f32; 3] {
1849        if !self.vignette_enabled {
1850            return color;
1851        }
1852        let center = [uv[0] - 0.5, uv[1] - 0.5];
1853        let dist = (center[0] * center[0] + center[1] * center[1]).sqrt() * 1.414;
1854        let vignette = 1.0 - self.vignette_intensity * dist.powf(self.vignette_smoothness);
1855        let v = vignette.max(0.0);
1856        [color[0] * v, color[1] * v, color[2] * v]
1857    }
1858
1859    /// Apply saturation/contrast/brightness adjustments.
1860    pub fn color_adjust(&self, color: [f32; 3]) -> [f32; 3] {
1861        // Brightness
1862        let c = [
1863            color[0] + self.brightness,
1864            color[1] + self.brightness,
1865            color[2] + self.brightness,
1866        ];
1867
1868        // Contrast
1869        let c = [
1870            (c[0] - 0.5) * self.contrast + 0.5,
1871            (c[1] - 0.5) * self.contrast + 0.5,
1872            (c[2] - 0.5) * self.contrast + 0.5,
1873        ];
1874
1875        // Saturation
1876        let lum = 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2];
1877        [
1878            lerpf(lum, c[0], self.saturation),
1879            lerpf(lum, c[1], self.saturation),
1880            lerpf(lum, c[2], self.saturation),
1881        ]
1882    }
1883
1884    /// Generate the post-process fragment shader.
1885    pub fn fragment_shader(&self) -> String {
1886        let mut s = String::from(r#"#version 330 core
1887in vec2 v_texcoord;
1888out vec4 frag_color;
1889
1890uniform sampler2D u_hdr_color;
1891uniform sampler2D u_bloom;
1892uniform float u_exposure;
1893uniform float u_gamma;
1894uniform float u_bloom_intensity;
1895uniform float u_time;
1896"#);
1897
1898        s.push_str(self.tone_mapping.glsl_function());
1899        s.push('\n');
1900
1901        s.push_str(r#"
1902void main() {
1903    vec3 hdr = texture(u_hdr_color, v_texcoord).rgb;
1904    vec3 bloom = texture(u_bloom, v_texcoord).rgb;
1905    hdr += bloom * u_bloom_intensity;
1906    hdr *= u_exposure;
1907    vec3 mapped = tonemap(hdr);
1908    mapped = pow(mapped, vec3(1.0 / u_gamma));
1909    frag_color = vec4(mapped, 1.0);
1910}
1911"#);
1912
1913        s
1914    }
1915}
1916
1917impl Default for PostProcessPass {
1918    fn default() -> Self {
1919        Self::new()
1920    }
1921}
1922
1923// ---------------------------------------------------------------------------
1924// Full deferred pipeline
1925// ---------------------------------------------------------------------------
1926
1927/// Frame statistics for the entire deferred pipeline.
1928#[derive(Debug, Clone, Default)]
1929pub struct DeferredFrameStats {
1930    /// Total frame time (microseconds).
1931    pub total_time_us: u64,
1932    /// Time per pass.
1933    pub depth_prepass_us: u64,
1934    pub geometry_pass_us: u64,
1935    pub lighting_pass_us: u64,
1936    pub forward_pass_us: u64,
1937    pub postprocess_pass_us: u64,
1938    pub aa_pass_us: u64,
1939    /// Draw call counts.
1940    pub total_draw_calls: u32,
1941    pub opaque_draw_calls: u32,
1942    pub transparent_draw_calls: u32,
1943    /// Triangle count.
1944    pub total_triangles: u64,
1945    /// Items submitted / visible / culled.
1946    pub items_submitted: u32,
1947    pub items_visible: u32,
1948    pub items_culled: u32,
1949    /// G-Buffer memory usage.
1950    pub gbuffer_memory_mb: f32,
1951    /// Current exposure.
1952    pub exposure: f32,
1953    /// Current frame number.
1954    pub frame_number: u64,
1955}
1956
1957/// The complete deferred rendering pipeline, orchestrating all passes.
1958#[derive(Debug)]
1959pub struct DeferredPipeline {
1960    /// The G-Buffer.
1961    pub gbuffer: GBuffer,
1962    /// HDR framebuffer.
1963    pub hdr_framebuffer: HdrFramebuffer,
1964    /// Depth pre-pass.
1965    pub depth_prepass: DepthPrePass,
1966    /// Geometry pass.
1967    pub geometry_pass: GeometryPass,
1968    /// Lighting pass.
1969    pub lighting_pass: LightingPass,
1970    /// Forward pass.
1971    pub forward_pass: ForwardPass,
1972    /// Post-processing pass.
1973    pub postprocess_pass: PostProcessPass,
1974    /// Render queue.
1975    pub render_queue: RenderQueue,
1976    /// Current viewport.
1977    pub viewport: Viewport,
1978    /// View matrix.
1979    pub view_matrix: Mat4,
1980    /// Projection matrix.
1981    pub projection_matrix: Mat4,
1982    /// Camera position.
1983    pub camera_position: [f32; 3],
1984    /// Frame statistics.
1985    pub frame_stats: DeferredFrameStats,
1986    /// Whether the pipeline has been initialized.
1987    pub initialized: bool,
1988    /// Frame counter.
1989    pub frame_number: u64,
1990    /// Delta time for the current frame.
1991    pub dt: f32,
1992}
1993
1994impl DeferredPipeline {
1995    /// Create a new deferred pipeline with the given viewport dimensions.
1996    pub fn new(width: u32, height: u32) -> Self {
1997        let viewport = Viewport::new(width, height);
1998        Self {
1999            gbuffer: GBuffer::new(viewport),
2000            hdr_framebuffer: HdrFramebuffer::new(width, height),
2001            depth_prepass: DepthPrePass::new(),
2002            geometry_pass: GeometryPass::new(),
2003            lighting_pass: LightingPass::new(),
2004            forward_pass: ForwardPass::new(),
2005            postprocess_pass: PostProcessPass::new(),
2006            render_queue: RenderQueue::new(),
2007            viewport,
2008            view_matrix: Mat4::IDENTITY,
2009            projection_matrix: Mat4::IDENTITY,
2010            camera_position: [0.0; 3],
2011            frame_stats: DeferredFrameStats::default(),
2012            initialized: false,
2013            frame_number: 0,
2014            dt: 0.016,
2015        }
2016    }
2017
2018    /// Initialize all GPU resources.
2019    pub fn initialize(&mut self) -> Result<(), String> {
2020        self.gbuffer.create().map_err(|e| e.to_string())?;
2021        self.hdr_framebuffer.create()?;
2022        self.initialized = true;
2023        Ok(())
2024    }
2025
2026    /// Shut down and release all resources.
2027    pub fn shutdown(&mut self) {
2028        self.gbuffer.destroy();
2029        self.hdr_framebuffer.destroy();
2030        self.initialized = false;
2031    }
2032
2033    /// Resize the pipeline to new viewport dimensions.
2034    pub fn resize(&mut self, width: u32, height: u32) {
2035        self.viewport = Viewport::new(width, height);
2036        let _ = self.gbuffer.resize(width, height);
2037        self.hdr_framebuffer.resize(width, height);
2038    }
2039
2040    /// Set the camera for this frame.
2041    pub fn set_camera(
2042        &mut self,
2043        position: [f32; 3],
2044        view: Mat4,
2045        projection: Mat4,
2046        frustum_planes: [[f32; 4]; 6],
2047    ) {
2048        self.camera_position = position;
2049        self.view_matrix = view;
2050        self.projection_matrix = projection;
2051        self.render_queue.set_camera(position, frustum_planes);
2052    }
2053
2054    /// Submit a render item.
2055    pub fn submit(&mut self, item: RenderItem) {
2056        self.render_queue.submit(item);
2057    }
2058
2059    /// Execute the full deferred rendering pipeline for one frame.
2060    pub fn execute_frame(&mut self, dt: f32) {
2061        let frame_start = std::time::Instant::now();
2062        self.dt = dt;
2063        self.frame_number += 1;
2064
2065        // Sort the render queue
2066        self.render_queue.sort();
2067
2068        // 1. Depth pre-pass
2069        self.depth_prepass.execute(&self.render_queue, &mut self.gbuffer);
2070
2071        // 2. Geometry pass
2072        self.geometry_pass.execute(&self.render_queue, &mut self.gbuffer);
2073
2074        // 3. Lighting pass
2075        self.lighting_pass.execute(
2076            &self.gbuffer,
2077            &self.hdr_framebuffer,
2078            &self.view_matrix,
2079            &self.projection_matrix,
2080            self.camera_position,
2081        );
2082
2083        // 4. Forward pass
2084        let lights_clone: Vec<LightType> = self.lighting_pass.lights.clone();
2085        self.forward_pass.execute(
2086            &self.render_queue,
2087            &self.hdr_framebuffer,
2088            &self.gbuffer,
2089            &lights_clone,
2090            &self.view_matrix,
2091            &self.projection_matrix,
2092            self.camera_position,
2093        );
2094
2095        // 5. Post-processing
2096        self.postprocess_pass.execute(&self.hdr_framebuffer, &self.viewport, dt);
2097
2098        // Collect stats
2099        self.frame_stats = DeferredFrameStats {
2100            total_time_us: frame_start.elapsed().as_micros() as u64,
2101            depth_prepass_us: self.depth_prepass.time_us,
2102            geometry_pass_us: self.geometry_pass.time_us,
2103            lighting_pass_us: self.lighting_pass.time_us,
2104            forward_pass_us: self.forward_pass.time_us,
2105            postprocess_pass_us: self.postprocess_pass.time_us,
2106            aa_pass_us: 0,
2107            total_draw_calls: self.geometry_pass.draw_call_count + self.forward_pass.draw_call_count,
2108            opaque_draw_calls: self.geometry_pass.draw_call_count,
2109            transparent_draw_calls: self.forward_pass.draw_call_count,
2110            total_triangles: self.geometry_pass.triangle_count + self.forward_pass.triangle_count,
2111            items_submitted: self.render_queue.total_submitted,
2112            items_visible: self.render_queue.total_visible,
2113            items_culled: self.render_queue.total_culled,
2114            gbuffer_memory_mb: self.gbuffer.stats().total_memory_bytes as f32 / (1024.0 * 1024.0),
2115            exposure: self.postprocess_pass.exposure.exposure,
2116            frame_number: self.frame_number,
2117        };
2118
2119        // Clear the queue for next frame
2120        self.render_queue.clear();
2121    }
2122
2123    /// Get a summary string of the current frame stats.
2124    pub fn stats_summary(&self) -> String {
2125        let s = &self.frame_stats;
2126        format!(
2127            "Frame {} | {:.1}ms total | Draws: {} | Tris: {} | Visible: {}/{} | Exposure: {:.2} | GBuf: {:.1}MB",
2128            s.frame_number,
2129            s.total_time_us as f64 / 1000.0,
2130            s.total_draw_calls,
2131            s.total_triangles,
2132            s.items_visible,
2133            s.items_submitted,
2134            s.exposure,
2135            s.gbuffer_memory_mb,
2136        )
2137    }
2138}
2139
2140impl Default for DeferredPipeline {
2141    fn default() -> Self {
2142        Self::new(1920, 1080)
2143    }
2144}
2145
2146// ---------------------------------------------------------------------------
2147// Tests
2148// ---------------------------------------------------------------------------
2149
2150#[cfg(test)]
2151mod tests {
2152    use super::*;
2153
2154    #[test]
2155    fn test_render_queue_sorting() {
2156        let mut queue = RenderQueue::new();
2157        // Set all frustum planes to accept everything
2158        queue.set_camera(
2159            [0.0, 0.0, 0.0],
2160            [[0.0, 0.0, 1.0, 1000.0]; 6],
2161        );
2162
2163        let item1 = RenderItem::new(0, 1, 0).with_bounds([0.0, 0.0, 10.0], 1.0);
2164        let item2 = RenderItem::new(0, 2, 0).with_bounds([0.0, 0.0, 5.0], 1.0);
2165        let item3 = RenderItem::new(0, 3, 0).with_bounds([0.0, 0.0, 20.0], 1.0);
2166
2167        queue.submit(item1);
2168        queue.submit(item2);
2169        queue.submit(item3);
2170        queue.sort();
2171
2172        let opaque = queue.opaque_items();
2173        assert_eq!(opaque.len(), 3);
2174        // Front-to-back: closest first
2175        assert!(opaque[0].camera_distance <= opaque[1].camera_distance);
2176        assert!(opaque[1].camera_distance <= opaque[2].camera_distance);
2177    }
2178
2179    #[test]
2180    fn test_render_queue_transparent_sorting() {
2181        let mut queue = RenderQueue::new();
2182        queue.set_camera(
2183            [0.0, 0.0, 0.0],
2184            [[0.0, 0.0, 1.0, 1000.0]; 6],
2185        );
2186
2187        let item1 = RenderItem::new(0, 1, 0)
2188            .with_bucket(RenderBucket::Transparent)
2189            .with_bounds([0.0, 0.0, 10.0], 1.0);
2190        let item2 = RenderItem::new(0, 2, 0)
2191            .with_bucket(RenderBucket::Transparent)
2192            .with_bounds([0.0, 0.0, 5.0], 1.0);
2193
2194        queue.submit(item1);
2195        queue.submit(item2);
2196        queue.sort();
2197
2198        let transparent = queue.transparent_items();
2199        assert_eq!(transparent.len(), 2);
2200        // Back-to-front: farthest first
2201        assert!(transparent[0].camera_distance >= transparent[1].camera_distance);
2202    }
2203
2204    #[test]
2205    fn test_light_evaluation() {
2206        let light = LightType::Directional {
2207            direction: [0.0, -1.0, 0.0],
2208            color: [1.0, 1.0, 1.0],
2209            intensity: 1.0,
2210            cast_shadows: false,
2211        };
2212        let (dir, att, _color) = light.evaluate([0.0, 0.0, 0.0]);
2213        assert!((dir[1] - 1.0).abs() < 0.001); // reversed direction
2214        assert!((att - 1.0).abs() < 0.001);
2215    }
2216
2217    #[test]
2218    fn test_point_light_attenuation() {
2219        let light = LightType::Point {
2220            position: [0.0, 5.0, 0.0],
2221            color: [1.0, 1.0, 1.0],
2222            intensity: 10.0,
2223            range: 20.0,
2224            cast_shadows: false,
2225        };
2226        let (_, att_near, _) = light.evaluate([0.0, 4.0, 0.0]);
2227        let (_, att_far, _) = light.evaluate([0.0, -10.0, 0.0]);
2228        assert!(att_near > att_far, "Near attenuation should be greater");
2229    }
2230
2231    #[test]
2232    fn test_tone_mapping() {
2233        let color = [2.0, 1.0, 0.5];
2234        let reinhard = ToneMappingOperator::Reinhard.apply(color, 1.0);
2235        for c in &reinhard {
2236            assert!(*c >= 0.0 && *c <= 1.0);
2237        }
2238        let aces = ToneMappingOperator::AcesFilmic.apply(color, 1.0);
2239        for c in &aces {
2240            assert!(*c >= 0.0 && *c <= 1.0);
2241        }
2242    }
2243
2244    #[test]
2245    fn test_exposure_controller() {
2246        let mut ec = ExposureController::new();
2247        ec.mode = ExposureMode::AverageLuminance;
2248        ec.feed_luminance(0.5);
2249        ec.update(0.016);
2250        assert!(ec.exposure > 0.0);
2251    }
2252
2253    #[test]
2254    fn test_pipeline_creation() {
2255        let mut pipeline = DeferredPipeline::new(1920, 1080);
2256        assert!(!pipeline.initialized);
2257        pipeline.initialize().unwrap();
2258        assert!(pipeline.initialized);
2259    }
2260
2261    #[test]
2262    fn test_pipeline_frame() {
2263        let mut pipeline = DeferredPipeline::new(800, 600);
2264        pipeline.initialize().unwrap();
2265
2266        pipeline.lighting_pass.add_light(LightType::Directional {
2267            direction: [0.0, -1.0, 0.0],
2268            color: [1.0, 1.0, 1.0],
2269            intensity: 1.0,
2270            cast_shadows: false,
2271        });
2272
2273        pipeline.set_camera(
2274            [0.0, 5.0, 10.0],
2275            Mat4::IDENTITY,
2276            Mat4::IDENTITY,
2277            [[0.0, 0.0, 1.0, 1000.0]; 6],
2278        );
2279
2280        let item = RenderItem::new(0, 1, 0)
2281            .with_bounds([0.0, 0.0, 0.0], 5.0);
2282        pipeline.submit(item);
2283
2284        pipeline.execute_frame(0.016);
2285        assert_eq!(pipeline.frame_stats.frame_number, 1);
2286    }
2287
2288    #[test]
2289    fn test_hdr_framebuffer() {
2290        let mut fb = HdrFramebuffer::new(1920, 1080);
2291        fb.create().unwrap();
2292        assert!(fb.allocated);
2293        assert!(fb.memory_bytes() > 0);
2294        fb.resize(2560, 1440);
2295        assert_eq!(fb.width, 2560);
2296    }
2297
2298    #[test]
2299    fn test_bloom_extract() {
2300        let pp = PostProcessPass::new();
2301        let bright = pp.bloom_extract([2.0, 2.0, 2.0]);
2302        assert!(bright[0] > 0.0);
2303        let dark = pp.bloom_extract([0.1, 0.1, 0.1]);
2304        assert!((dark[0] - 0.0).abs() < 0.001);
2305    }
2306
2307    #[test]
2308    fn test_pbr_lighting() {
2309        let mut lp = LightingPass::new();
2310        lp.add_light(LightType::Directional {
2311            direction: [0.0, -1.0, 0.0],
2312            color: [1.0, 1.0, 1.0],
2313            intensity: 2.0,
2314            cast_shadows: false,
2315        });
2316
2317        let result = lp.evaluate_pbr(
2318            [0.0, 0.0, 0.0],
2319            [0.0, 1.0, 0.0],
2320            [0.8, 0.2, 0.2],
2321            0.5,
2322            0.0,
2323            [0.0, 5.0, 5.0],
2324        );
2325
2326        assert!(result[0] > 0.0);
2327        assert!(result[1] > 0.0);
2328        assert!(result[2] > 0.0);
2329    }
2330}