Skip to main content

proof_engine/rendergraph/
passes.rs

1//! Built-in render pass implementations for a deferred rendering pipeline.
2//!
3//! Each pass declares its resource inputs/outputs and contains simulated
4//! rendering logic. In a production engine, the `execute` methods would issue
5//! real GPU draw/dispatch calls; here they perform the bookkeeping and log
6//! what would happen.
7
8use crate::rendergraph::executor::PassContext;
9use crate::rendergraph::graph::{
10    PassCondition, PassType, QueueAffinity, RenderGraph, RenderGraphBuilder, RenderPass,
11    ResolutionScale,
12};
13use crate::rendergraph::resources::{
14    ResourceDescriptor, ResourceHandle, SizePolicy, TextureFormat, UsageFlags,
15};
16
17use std::fmt;
18
19// ---------------------------------------------------------------------------
20// Common pass trait
21// ---------------------------------------------------------------------------
22
23/// Trait implemented by all built-in passes. Provides resource declaration
24/// and execution.
25pub trait BuiltinPass {
26    /// Unique name of this pass.
27    fn name(&self) -> &str;
28
29    /// The type of work this pass performs.
30    fn pass_type(&self) -> PassType {
31        PassType::Graphics
32    }
33
34    /// Queue affinity.
35    fn queue_affinity(&self) -> QueueAffinity {
36        QueueAffinity::Graphics
37    }
38
39    /// Names of resources this pass reads.
40    fn input_names(&self) -> Vec<&str>;
41
42    /// Names of resources this pass writes.
43    fn output_names(&self) -> Vec<&str>;
44
45    /// Execute the pass (simulated rendering logic).
46    fn execute(&self, ctx: &PassContext);
47
48    /// Optional condition for this pass.
49    fn condition(&self) -> PassCondition {
50        PassCondition::Always
51    }
52
53    /// Resolution scale for this pass.
54    fn resolution_scale(&self) -> ResolutionScale {
55        ResolutionScale::full()
56    }
57}
58
59// ---------------------------------------------------------------------------
60// Render state (simulated GPU state for pass logic)
61// ---------------------------------------------------------------------------
62
63/// Simulated draw call for bookkeeping.
64#[derive(Debug, Clone)]
65pub struct DrawCall {
66    pub vertex_count: u32,
67    pub instance_count: u32,
68    pub pass_name: String,
69    pub draw_type: DrawType,
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub enum DrawType {
74    Triangles,
75    FullscreenQuad,
76    Instanced,
77    Indirect,
78    Dispatch,
79}
80
81/// Simulated viewport.
82#[derive(Debug, Clone, Copy)]
83pub struct Viewport {
84    pub x: f32,
85    pub y: f32,
86    pub width: f32,
87    pub height: f32,
88    pub min_depth: f32,
89    pub max_depth: f32,
90}
91
92impl Viewport {
93    pub fn from_context(ctx: &PassContext) -> Self {
94        Self {
95            x: 0.0,
96            y: 0.0,
97            width: ctx.render_width as f32,
98            height: ctx.render_height as f32,
99            min_depth: 0.0,
100            max_depth: 1.0,
101        }
102    }
103}
104
105/// Simulated clear values.
106#[derive(Debug, Clone, Copy)]
107pub struct ClearValues {
108    pub color: [f32; 4],
109    pub depth: f32,
110    pub stencil: u8,
111}
112
113impl Default for ClearValues {
114    fn default() -> Self {
115        Self {
116            color: [0.0, 0.0, 0.0, 1.0],
117            depth: 1.0,
118            stencil: 0,
119        }
120    }
121}
122
123// ---------------------------------------------------------------------------
124// 1. DepthPrePass
125// ---------------------------------------------------------------------------
126
127/// Renders scene geometry to the depth buffer only. Used for early-Z
128/// optimization and as input for SSAO, shadows, etc.
129pub struct DepthPrePass {
130    pub clear_depth: f32,
131    pub depth_bias: f32,
132    pub depth_bias_slope: f32,
133}
134
135impl DepthPrePass {
136    pub fn new() -> Self {
137        Self {
138            clear_depth: 1.0,
139            depth_bias: 0.0,
140            depth_bias_slope: 0.0,
141        }
142    }
143
144    /// Register this pass's resources in a graph builder and add the pass.
145    pub fn register(
146        &self,
147        builder: &mut RenderGraphBuilder,
148        depth_handle: ResourceHandle,
149    ) {
150        builder
151            .graphics_pass(self.name())
152            .writes(depth_handle, "depth")
153            .tag("geometry")
154            .finish();
155    }
156}
157
158impl Default for DepthPrePass {
159    fn default() -> Self {
160        Self::new()
161    }
162}
163
164impl BuiltinPass for DepthPrePass {
165    fn name(&self) -> &str {
166        "depth_prepass"
167    }
168
169    fn input_names(&self) -> Vec<&str> {
170        vec![]
171    }
172
173    fn output_names(&self) -> Vec<&str> {
174        vec!["depth"]
175    }
176
177    fn execute(&self, ctx: &PassContext) {
178        let _viewport = Viewport::from_context(ctx);
179        let _clear = ClearValues {
180            depth: self.clear_depth,
181            ..Default::default()
182        };
183        // Simulated: bind depth-only pipeline, draw all opaque geometry
184        // In production: iterate scene objects, bind vertex buffers, draw
185        let _draw = DrawCall {
186            vertex_count: 0, // determined by scene
187            instance_count: 1,
188            pass_name: self.name().to_string(),
189            draw_type: DrawType::Triangles,
190        };
191    }
192}
193
194// ---------------------------------------------------------------------------
195// 2. GBufferPass
196// ---------------------------------------------------------------------------
197
198/// Fills the G-Buffer with albedo, normal, roughness/metallic, and velocity.
199pub struct GBufferPass {
200    pub write_velocity: bool,
201    pub write_emissive: bool,
202}
203
204impl GBufferPass {
205    pub fn new() -> Self {
206        Self {
207            write_velocity: true,
208            write_emissive: true,
209        }
210    }
211
212    pub fn register(
213        &self,
214        builder: &mut RenderGraphBuilder,
215        depth: ResourceHandle,
216        albedo: ResourceHandle,
217        normal: ResourceHandle,
218        roughness_metallic: ResourceHandle,
219    ) {
220        let mut pass_builder = builder
221            .graphics_pass(self.name())
222            .reads(depth, "depth")
223            .writes(albedo, "gbuffer_albedo")
224            .writes(normal, "gbuffer_normal")
225            .writes(roughness_metallic, "gbuffer_rm")
226            .tag("geometry");
227        pass_builder = pass_builder.depends_on("depth_prepass");
228        pass_builder.finish();
229    }
230}
231
232impl Default for GBufferPass {
233    fn default() -> Self {
234        Self::new()
235    }
236}
237
238impl BuiltinPass for GBufferPass {
239    fn name(&self) -> &str {
240        "gbuffer"
241    }
242
243    fn input_names(&self) -> Vec<&str> {
244        vec!["depth"]
245    }
246
247    fn output_names(&self) -> Vec<&str> {
248        let mut out = vec!["gbuffer_albedo", "gbuffer_normal", "gbuffer_rm"];
249        if self.write_velocity {
250            out.push("gbuffer_velocity");
251        }
252        if self.write_emissive {
253            out.push("gbuffer_emissive");
254        }
255        out
256    }
257
258    fn execute(&self, ctx: &PassContext) {
259        let _viewport = Viewport::from_context(ctx);
260        let _clear = ClearValues {
261            color: [0.0, 0.0, 0.0, 0.0],
262            ..Default::default()
263        };
264        // Simulated: bind GBuffer MRT pipeline, draw all opaque geometry
265        let _draw = DrawCall {
266            vertex_count: 0,
267            instance_count: 1,
268            pass_name: self.name().to_string(),
269            draw_type: DrawType::Triangles,
270        };
271    }
272}
273
274// ---------------------------------------------------------------------------
275// 3. ShadowPass
276// ---------------------------------------------------------------------------
277
278/// Renders shadow maps for a single light source. Typically instantiated
279/// once per shadow-casting light.
280pub struct ShadowPass {
281    pub light_index: usize,
282    pub cascade_count: u32,
283    pub shadow_map_resolution: u32,
284    pub depth_bias: f32,
285    pub normal_bias: f32,
286    pub pcf_radius: f32,
287}
288
289impl ShadowPass {
290    pub fn new(light_index: usize) -> Self {
291        Self {
292            light_index,
293            cascade_count: 4,
294            shadow_map_resolution: 2048,
295            depth_bias: 0.005,
296            normal_bias: 0.02,
297            pcf_radius: 1.5,
298        }
299    }
300
301    pub fn pass_name(&self) -> String {
302        format!("shadow_light_{}", self.light_index)
303    }
304
305    pub fn register(
306        &self,
307        builder: &mut RenderGraphBuilder,
308        shadow_map: ResourceHandle,
309    ) {
310        builder
311            .graphics_pass(&self.pass_name())
312            .writes(shadow_map, &format!("shadow_map_{}", self.light_index))
313            .tag("shadows")
314            .finish();
315    }
316}
317
318impl BuiltinPass for ShadowPass {
319    fn name(&self) -> &str {
320        // This is a slight workaround since we need a per-light name
321        "shadow_pass"
322    }
323
324    fn input_names(&self) -> Vec<&str> {
325        vec![]
326    }
327
328    fn output_names(&self) -> Vec<&str> {
329        vec!["shadow_map"]
330    }
331
332    fn execute(&self, ctx: &PassContext) {
333        // Render shadow map for each cascade
334        for cascade in 0..self.cascade_count {
335            let _viewport = Viewport {
336                x: 0.0,
337                y: 0.0,
338                width: self.shadow_map_resolution as f32,
339                height: self.shadow_map_resolution as f32,
340                min_depth: 0.0,
341                max_depth: 1.0,
342            };
343            let _draw = DrawCall {
344                vertex_count: 0,
345                instance_count: 1,
346                pass_name: format!("{}_cascade_{}", self.pass_name(), cascade),
347                draw_type: DrawType::Triangles,
348            };
349        }
350    }
351}
352
353// ---------------------------------------------------------------------------
354// 4. SSAOPass
355// ---------------------------------------------------------------------------
356
357/// Screen-Space Ambient Occlusion, typically at half or quarter resolution.
358pub struct SSAOPass {
359    pub kernel_size: u32,
360    pub radius: f32,
361    pub bias: f32,
362    pub intensity: f32,
363    pub blur_passes: u32,
364    pub noise_texture_size: u32,
365}
366
367impl SSAOPass {
368    pub fn new() -> Self {
369        Self {
370            kernel_size: 64,
371            radius: 0.5,
372            bias: 0.025,
373            intensity: 1.5,
374            blur_passes: 2,
375            noise_texture_size: 4,
376        }
377    }
378
379    pub fn register(
380        &self,
381        builder: &mut RenderGraphBuilder,
382        depth: ResourceHandle,
383        normal: ResourceHandle,
384        ssao_out: ResourceHandle,
385    ) {
386        builder
387            .compute_pass(self.name())
388            .reads(depth, "depth")
389            .reads(normal, "gbuffer_normal")
390            .writes(ssao_out, "ssao")
391            .resolution(ResolutionScale::half())
392            .queue(QueueAffinity::Compute)
393            .condition(PassCondition::FeatureEnabled("ssao".to_string()))
394            .tag("lighting")
395            .finish();
396    }
397}
398
399impl Default for SSAOPass {
400    fn default() -> Self {
401        Self::new()
402    }
403}
404
405impl BuiltinPass for SSAOPass {
406    fn name(&self) -> &str {
407        "ssao"
408    }
409
410    fn pass_type(&self) -> PassType {
411        PassType::Compute
412    }
413
414    fn queue_affinity(&self) -> QueueAffinity {
415        QueueAffinity::Compute
416    }
417
418    fn input_names(&self) -> Vec<&str> {
419        vec!["depth", "gbuffer_normal"]
420    }
421
422    fn output_names(&self) -> Vec<&str> {
423        vec!["ssao"]
424    }
425
426    fn resolution_scale(&self) -> ResolutionScale {
427        ResolutionScale::half()
428    }
429
430    fn condition(&self) -> PassCondition {
431        PassCondition::FeatureEnabled("ssao".to_string())
432    }
433
434    fn execute(&self, ctx: &PassContext) {
435        // Generate SSAO: for each pixel, sample kernel_size points in a hemisphere
436        let dispatch_x = (ctx.render_width + 7) / 8;
437        let dispatch_y = (ctx.render_height + 7) / 8;
438        let _dispatch = DrawCall {
439            vertex_count: dispatch_x * dispatch_y,
440            instance_count: 1,
441            pass_name: "ssao_generate".to_string(),
442            draw_type: DrawType::Dispatch,
443        };
444
445        // Blur passes
446        for i in 0..self.blur_passes {
447            let _blur = DrawCall {
448                vertex_count: dispatch_x * dispatch_y,
449                instance_count: 1,
450                pass_name: format!("ssao_blur_{}", i),
451                draw_type: DrawType::Dispatch,
452            };
453        }
454    }
455}
456
457// ---------------------------------------------------------------------------
458// 5. LightingPass
459// ---------------------------------------------------------------------------
460
461/// Deferred lighting: reads G-Buffer, shadow maps, SSAO; outputs HDR color.
462pub struct LightingPass {
463    pub max_point_lights: u32,
464    pub max_spot_lights: u32,
465    pub enable_ibl: bool,
466    pub enable_volumetric: bool,
467}
468
469impl LightingPass {
470    pub fn new() -> Self {
471        Self {
472            max_point_lights: 256,
473            max_spot_lights: 64,
474            enable_ibl: true,
475            enable_volumetric: false,
476        }
477    }
478
479    pub fn register(
480        &self,
481        builder: &mut RenderGraphBuilder,
482        albedo: ResourceHandle,
483        normal: ResourceHandle,
484        rm: ResourceHandle,
485        depth: ResourceHandle,
486        ssao: ResourceHandle,
487        hdr_color: ResourceHandle,
488    ) {
489        builder
490            .graphics_pass(self.name())
491            .reads(albedo, "gbuffer_albedo")
492            .reads(normal, "gbuffer_normal")
493            .reads(rm, "gbuffer_rm")
494            .reads(depth, "depth")
495            .reads(ssao, "ssao")
496            .writes(hdr_color, "hdr_color")
497            .tag("lighting")
498            .finish();
499    }
500}
501
502impl Default for LightingPass {
503    fn default() -> Self {
504        Self::new()
505    }
506}
507
508impl BuiltinPass for LightingPass {
509    fn name(&self) -> &str {
510        "lighting"
511    }
512
513    fn input_names(&self) -> Vec<&str> {
514        vec![
515            "gbuffer_albedo",
516            "gbuffer_normal",
517            "gbuffer_rm",
518            "depth",
519            "ssao",
520        ]
521    }
522
523    fn output_names(&self) -> Vec<&str> {
524        vec!["hdr_color"]
525    }
526
527    fn execute(&self, ctx: &PassContext) {
528        let _viewport = Viewport::from_context(ctx);
529        // Fullscreen quad pass that evaluates the lighting equation per pixel
530        let _draw = DrawCall {
531            vertex_count: 3, // fullscreen triangle
532            instance_count: 1,
533            pass_name: self.name().to_string(),
534            draw_type: DrawType::FullscreenQuad,
535        };
536        // If IBL enabled, also bind environment map
537        if self.enable_ibl {
538            let _ibl = DrawCall {
539                vertex_count: 3,
540                instance_count: 1,
541                pass_name: "lighting_ibl".to_string(),
542                draw_type: DrawType::FullscreenQuad,
543            };
544        }
545    }
546}
547
548// ---------------------------------------------------------------------------
549// 6. SkyboxPass
550// ---------------------------------------------------------------------------
551
552/// Renders the skybox / environment behind all geometry.
553pub struct SkyboxPass {
554    pub exposure: f32,
555    pub rotation: f32,
556    pub blur_level: f32,
557}
558
559impl SkyboxPass {
560    pub fn new() -> Self {
561        Self {
562            exposure: 1.0,
563            rotation: 0.0,
564            blur_level: 0.0,
565        }
566    }
567
568    pub fn register(
569        &self,
570        builder: &mut RenderGraphBuilder,
571        depth: ResourceHandle,
572        hdr_color: ResourceHandle,
573    ) {
574        builder
575            .graphics_pass(self.name())
576            .reads(depth, "depth")
577            .writes(hdr_color, "hdr_color")
578            .depends_on("lighting")
579            .tag("lighting")
580            .finish();
581    }
582}
583
584impl Default for SkyboxPass {
585    fn default() -> Self {
586        Self::new()
587    }
588}
589
590impl BuiltinPass for SkyboxPass {
591    fn name(&self) -> &str {
592        "skybox"
593    }
594
595    fn input_names(&self) -> Vec<&str> {
596        vec!["depth"]
597    }
598
599    fn output_names(&self) -> Vec<&str> {
600        vec!["hdr_color"]
601    }
602
603    fn execute(&self, ctx: &PassContext) {
604        let _viewport = Viewport::from_context(ctx);
605        // Draw a cube or fullscreen quad at max depth
606        let _draw = DrawCall {
607            vertex_count: 36, // cube
608            instance_count: 1,
609            pass_name: self.name().to_string(),
610            draw_type: DrawType::Triangles,
611        };
612    }
613}
614
615// ---------------------------------------------------------------------------
616// 7. TransparencyPass (Order-Independent Transparency)
617// ---------------------------------------------------------------------------
618
619/// Weighted blended order-independent transparency.
620pub struct TransparencyPass {
621    pub weight_function: WeightFunction,
622    pub max_fragments_per_pixel: u32,
623}
624
625#[derive(Debug, Clone, Copy, PartialEq, Eq)]
626pub enum WeightFunction {
627    /// McGuire & Bavoil 2013 weighted blended
628    DepthWeight,
629    /// Constant weight (fastest, lowest quality)
630    Constant,
631    /// Color-based weight
632    ColorWeight,
633}
634
635impl TransparencyPass {
636    pub fn new() -> Self {
637        Self {
638            weight_function: WeightFunction::DepthWeight,
639            max_fragments_per_pixel: 8,
640        }
641    }
642
643    pub fn register(
644        &self,
645        builder: &mut RenderGraphBuilder,
646        depth: ResourceHandle,
647        hdr_color: ResourceHandle,
648        accum: ResourceHandle,
649        reveal: ResourceHandle,
650    ) {
651        // Accumulation pass
652        builder
653            .graphics_pass("transparency_accum")
654            .reads(depth, "depth")
655            .writes(accum, "oit_accum")
656            .writes(reveal, "oit_reveal")
657            .depends_on("skybox")
658            .tag("transparency")
659            .finish();
660
661        // Composite pass
662        builder
663            .graphics_pass("transparency_composite")
664            .reads(accum, "oit_accum")
665            .reads(reveal, "oit_reveal")
666            .reads(hdr_color, "hdr_color")
667            .writes(hdr_color, "hdr_color")
668            .depends_on("transparency_accum")
669            .tag("transparency")
670            .finish();
671    }
672}
673
674impl Default for TransparencyPass {
675    fn default() -> Self {
676        Self::new()
677    }
678}
679
680impl BuiltinPass for TransparencyPass {
681    fn name(&self) -> &str {
682        "transparency"
683    }
684
685    fn input_names(&self) -> Vec<&str> {
686        vec!["depth", "hdr_color", "oit_accum", "oit_reveal"]
687    }
688
689    fn output_names(&self) -> Vec<&str> {
690        vec!["hdr_color"]
691    }
692
693    fn execute(&self, ctx: &PassContext) {
694        let _viewport = Viewport::from_context(ctx);
695
696        // Phase 1: Accumulation — render transparent geometry with additive blending
697        let _accum_draw = DrawCall {
698            vertex_count: 0,
699            instance_count: 1,
700            pass_name: "oit_accumulate".to_string(),
701            draw_type: DrawType::Triangles,
702        };
703
704        // Phase 2: Composite — fullscreen pass to blend over opaque
705        let _composite_draw = DrawCall {
706            vertex_count: 3,
707            instance_count: 1,
708            pass_name: "oit_composite".to_string(),
709            draw_type: DrawType::FullscreenQuad,
710        };
711    }
712}
713
714// ---------------------------------------------------------------------------
715// 8. BloomPass
716// ---------------------------------------------------------------------------
717
718/// Multi-pass bloom effect: threshold -> downsample chain -> upsample chain.
719pub struct BloomPass {
720    pub threshold: f32,
721    pub intensity: f32,
722    pub mip_count: u32,
723    pub radius: f32,
724    pub soft_threshold: f32,
725}
726
727impl BloomPass {
728    pub fn new() -> Self {
729        Self {
730            threshold: 1.0,
731            intensity: 0.8,
732            mip_count: 5,
733            radius: 1.0,
734            soft_threshold: 0.5,
735        }
736    }
737
738    /// Register the full bloom chain in the graph.
739    pub fn register(
740        &self,
741        builder: &mut RenderGraphBuilder,
742        hdr_color: ResourceHandle,
743        bloom_out: ResourceHandle,
744    ) {
745        // Threshold + first downsample
746        builder
747            .graphics_pass("bloom_threshold")
748            .reads(hdr_color, "hdr_color")
749            .writes(bloom_out, "bloom_chain")
750            .resolution(ResolutionScale::half())
751            .depends_on("transparency_composite")
752            .tag("bloom")
753            .finish();
754
755        // Downsample chain (each mip is half the previous)
756        for i in 1..self.mip_count {
757            let scale = 1.0 / (2u32.pow(i + 1)) as f32;
758            builder
759                .graphics_pass(&format!("bloom_down_{}", i))
760                .reads(bloom_out, "bloom_chain")
761                .writes(bloom_out, "bloom_chain")
762                .resolution(ResolutionScale::custom(scale, scale))
763                .depends_on(&if i == 1 {
764                    "bloom_threshold".to_string()
765                } else {
766                    format!("bloom_down_{}", i - 1)
767                })
768                .tag("bloom")
769                .finish();
770        }
771
772        // Upsample chain (additive blend back up)
773        for i in (0..self.mip_count - 1).rev() {
774            let scale = 1.0 / (2u32.pow(i + 1)) as f32;
775            let dep = if i == self.mip_count - 2 {
776                format!("bloom_down_{}", self.mip_count - 1)
777            } else {
778                format!("bloom_up_{}", i + 1)
779            };
780            builder
781                .graphics_pass(&format!("bloom_up_{}", i))
782                .reads(bloom_out, "bloom_chain")
783                .writes(bloom_out, "bloom_chain")
784                .resolution(ResolutionScale::custom(scale, scale))
785                .depends_on(&dep)
786                .tag("bloom")
787                .finish();
788        }
789    }
790}
791
792impl Default for BloomPass {
793    fn default() -> Self {
794        Self::new()
795    }
796}
797
798impl BuiltinPass for BloomPass {
799    fn name(&self) -> &str {
800        "bloom"
801    }
802
803    fn pass_type(&self) -> PassType {
804        PassType::Graphics
805    }
806
807    fn input_names(&self) -> Vec<&str> {
808        vec!["hdr_color"]
809    }
810
811    fn output_names(&self) -> Vec<&str> {
812        vec!["bloom_chain"]
813    }
814
815    fn resolution_scale(&self) -> ResolutionScale {
816        ResolutionScale::half()
817    }
818
819    fn execute(&self, ctx: &PassContext) {
820        let _viewport = Viewport::from_context(ctx);
821
822        // Threshold extraction
823        let _threshold_draw = DrawCall {
824            vertex_count: 3,
825            instance_count: 1,
826            pass_name: "bloom_threshold".to_string(),
827            draw_type: DrawType::FullscreenQuad,
828        };
829
830        // Downsample chain
831        for i in 0..self.mip_count {
832            let _down = DrawCall {
833                vertex_count: 3,
834                instance_count: 1,
835                pass_name: format!("bloom_down_{}", i),
836                draw_type: DrawType::FullscreenQuad,
837            };
838        }
839
840        // Upsample chain (tent filter / bilinear + additive blend)
841        for i in (0..self.mip_count).rev() {
842            let _up = DrawCall {
843                vertex_count: 3,
844                instance_count: 1,
845                pass_name: format!("bloom_up_{}", i),
846                draw_type: DrawType::FullscreenQuad,
847            };
848        }
849    }
850}
851
852// ---------------------------------------------------------------------------
853// 9. ToneMappingPass
854// ---------------------------------------------------------------------------
855
856/// HDR to LDR tone mapping with auto-exposure.
857pub struct ToneMappingPass {
858    pub operator: ToneMapOperator,
859    pub exposure: f32,
860    pub gamma: f32,
861    pub auto_exposure: bool,
862    pub adaptation_speed: f32,
863    pub min_luminance: f32,
864    pub max_luminance: f32,
865}
866
867#[derive(Debug, Clone, Copy, PartialEq, Eq)]
868pub enum ToneMapOperator {
869    Reinhard,
870    ReinhardExtended,
871    ACES,
872    Uncharted2,
873    AgX,
874    Neutral,
875}
876
877impl ToneMappingPass {
878    pub fn new() -> Self {
879        Self {
880            operator: ToneMapOperator::ACES,
881            exposure: 1.0,
882            gamma: 2.2,
883            auto_exposure: true,
884            adaptation_speed: 1.0,
885            min_luminance: 0.01,
886            max_luminance: 10.0,
887        }
888    }
889
890    pub fn register(
891        &self,
892        builder: &mut RenderGraphBuilder,
893        hdr_color: ResourceHandle,
894        bloom: ResourceHandle,
895        ldr_color: ResourceHandle,
896    ) {
897        builder
898            .graphics_pass(self.name())
899            .reads(hdr_color, "hdr_color")
900            .reads(bloom, "bloom_chain")
901            .writes(ldr_color, "ldr_color")
902            .tag("post")
903            .finish();
904    }
905
906    /// Apply the tone mapping operator to a single color value.
907    pub fn apply_operator(&self, hdr: [f32; 3]) -> [f32; 3] {
908        let exposed = [
909            hdr[0] * self.exposure,
910            hdr[1] * self.exposure,
911            hdr[2] * self.exposure,
912        ];
913        match self.operator {
914            ToneMapOperator::Reinhard => {
915                let r = exposed[0] / (1.0 + exposed[0]);
916                let g = exposed[1] / (1.0 + exposed[1]);
917                let b = exposed[2] / (1.0 + exposed[2]);
918                [gamma(r, self.gamma), gamma(g, self.gamma), gamma(b, self.gamma)]
919            }
920            ToneMapOperator::ReinhardExtended => {
921                let max_white = self.max_luminance;
922                let r = exposed[0] * (1.0 + exposed[0] / (max_white * max_white))
923                    / (1.0 + exposed[0]);
924                let g = exposed[1] * (1.0 + exposed[1] / (max_white * max_white))
925                    / (1.0 + exposed[1]);
926                let b = exposed[2] * (1.0 + exposed[2] / (max_white * max_white))
927                    / (1.0 + exposed[2]);
928                [gamma(r, self.gamma), gamma(g, self.gamma), gamma(b, self.gamma)]
929            }
930            ToneMapOperator::ACES => {
931                // Simplified ACES filmic
932                let a = 2.51f32;
933                let b_val = 0.03f32;
934                let c = 2.43f32;
935                let d = 0.59f32;
936                let e = 0.14f32;
937                let aces = |x: f32| -> f32 {
938                    let numerator = x * (a * x + b_val);
939                    let denominator = x * (c * x + d) + e;
940                    (numerator / denominator).clamp(0.0, 1.0)
941                };
942                [
943                    gamma(aces(exposed[0]), self.gamma),
944                    gamma(aces(exposed[1]), self.gamma),
945                    gamma(aces(exposed[2]), self.gamma),
946                ]
947            }
948            ToneMapOperator::Uncharted2 => {
949                let uncharted = |x: f32| -> f32 {
950                    let a = 0.15f32;
951                    let b_val = 0.50f32;
952                    let c = 0.10f32;
953                    let d = 0.20f32;
954                    let e = 0.02f32;
955                    let f = 0.30f32;
956                    ((x * (a * x + c * b_val) + d * e) / (x * (a * x + b_val) + d * f)) - e / f
957                };
958                let white = uncharted(self.max_luminance);
959                let r = uncharted(exposed[0]) / white;
960                let g = uncharted(exposed[1]) / white;
961                let b = uncharted(exposed[2]) / white;
962                [gamma(r, self.gamma), gamma(g, self.gamma), gamma(b, self.gamma)]
963            }
964            ToneMapOperator::AgX | ToneMapOperator::Neutral => {
965                // Simplified neutral tone map
966                let r = exposed[0] / (1.0 + exposed[0]);
967                let g = exposed[1] / (1.0 + exposed[1]);
968                let b = exposed[2] / (1.0 + exposed[2]);
969                [gamma(r, self.gamma), gamma(g, self.gamma), gamma(b, self.gamma)]
970            }
971        }
972    }
973}
974
975impl Default for ToneMappingPass {
976    fn default() -> Self {
977        Self::new()
978    }
979}
980
981/// Apply gamma correction.
982fn gamma(linear: f32, gamma_val: f32) -> f32 {
983    if linear <= 0.0 {
984        0.0
985    } else {
986        linear.powf(1.0 / gamma_val)
987    }
988}
989
990impl BuiltinPass for ToneMappingPass {
991    fn name(&self) -> &str {
992        "tonemapping"
993    }
994
995    fn input_names(&self) -> Vec<&str> {
996        vec!["hdr_color", "bloom_chain"]
997    }
998
999    fn output_names(&self) -> Vec<&str> {
1000        vec!["ldr_color"]
1001    }
1002
1003    fn execute(&self, ctx: &PassContext) {
1004        let _viewport = Viewport::from_context(ctx);
1005        // Fullscreen pass: sample HDR + bloom, apply tone map operator
1006        let _draw = DrawCall {
1007            vertex_count: 3,
1008            instance_count: 1,
1009            pass_name: self.name().to_string(),
1010            draw_type: DrawType::FullscreenQuad,
1011        };
1012    }
1013}
1014
1015// ---------------------------------------------------------------------------
1016// 10. FXAAPass
1017// ---------------------------------------------------------------------------
1018
1019/// Fast Approximate Anti-Aliasing post-process.
1020pub struct FXAAPass {
1021    pub subpixel_quality: f32,
1022    pub edge_threshold: f32,
1023    pub edge_threshold_min: f32,
1024}
1025
1026impl FXAAPass {
1027    pub fn new() -> Self {
1028        Self {
1029            subpixel_quality: 0.75,
1030            edge_threshold: 0.166,
1031            edge_threshold_min: 0.0833,
1032        }
1033    }
1034
1035    pub fn register(
1036        &self,
1037        builder: &mut RenderGraphBuilder,
1038        ldr_color: ResourceHandle,
1039        aa_out: ResourceHandle,
1040    ) {
1041        builder
1042            .graphics_pass(self.name())
1043            .reads(ldr_color, "ldr_color")
1044            .writes(aa_out, "aa_color")
1045            .condition(PassCondition::FeatureEnabled("fxaa".to_string()))
1046            .tag("post")
1047            .finish();
1048    }
1049
1050    /// Compute luma for a color (used in edge detection).
1051    pub fn luma(color: [f32; 3]) -> f32 {
1052        color[0] * 0.299 + color[1] * 0.587 + color[2] * 0.114
1053    }
1054
1055    /// Check if a pixel is on an edge (simplified FXAA edge detection).
1056    pub fn is_edge(
1057        center_luma: f32,
1058        north: f32,
1059        south: f32,
1060        east: f32,
1061        west: f32,
1062        threshold: f32,
1063        threshold_min: f32,
1064    ) -> bool {
1065        let range = north.max(south).max(east).max(west).max(center_luma)
1066            - north.min(south).min(east).min(west).min(center_luma);
1067        let threshold_val = threshold.max(threshold_min);
1068        range > threshold_val
1069    }
1070}
1071
1072impl Default for FXAAPass {
1073    fn default() -> Self {
1074        Self::new()
1075    }
1076}
1077
1078impl BuiltinPass for FXAAPass {
1079    fn name(&self) -> &str {
1080        "fxaa"
1081    }
1082
1083    fn input_names(&self) -> Vec<&str> {
1084        vec!["ldr_color"]
1085    }
1086
1087    fn output_names(&self) -> Vec<&str> {
1088        vec!["aa_color"]
1089    }
1090
1091    fn condition(&self) -> PassCondition {
1092        PassCondition::FeatureEnabled("fxaa".to_string())
1093    }
1094
1095    fn execute(&self, ctx: &PassContext) {
1096        let _viewport = Viewport::from_context(ctx);
1097        let _draw = DrawCall {
1098            vertex_count: 3,
1099            instance_count: 1,
1100            pass_name: self.name().to_string(),
1101            draw_type: DrawType::FullscreenQuad,
1102        };
1103    }
1104}
1105
1106// ---------------------------------------------------------------------------
1107// 11. DebugOverlayPass
1108// ---------------------------------------------------------------------------
1109
1110/// Renders debug visualizations: wireframe, normals, G-Buffer channels, etc.
1111pub struct DebugOverlayPass {
1112    pub mode: DebugVisualization,
1113    pub opacity: f32,
1114    pub show_grid: bool,
1115    pub show_wireframe: bool,
1116}
1117
1118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1119pub enum DebugVisualization {
1120    None,
1121    Depth,
1122    Normals,
1123    Albedo,
1124    Roughness,
1125    Metallic,
1126    AmbientOcclusion,
1127    Velocity,
1128    Wireframe,
1129    LightComplexity,
1130    Overdraw,
1131    MipLevel,
1132}
1133
1134impl DebugOverlayPass {
1135    pub fn new() -> Self {
1136        Self {
1137            mode: DebugVisualization::None,
1138            opacity: 1.0,
1139            show_grid: false,
1140            show_wireframe: false,
1141        }
1142    }
1143
1144    pub fn register(
1145        &self,
1146        builder: &mut RenderGraphBuilder,
1147        input_color: ResourceHandle,
1148        depth: ResourceHandle,
1149        debug_out: ResourceHandle,
1150    ) {
1151        builder
1152            .graphics_pass(self.name())
1153            .reads(input_color, "aa_color")
1154            .reads(depth, "depth")
1155            .writes(debug_out, "debug_color")
1156            .condition(PassCondition::FeatureEnabled("debug_overlay".to_string()))
1157            .tag("debug")
1158            .finish();
1159    }
1160
1161    /// Map a depth value to a visible color (for depth visualization).
1162    pub fn depth_to_color(depth: f32, near: f32, far: f32) -> [f32; 3] {
1163        let linear = (2.0 * near * far) / (far + near - depth * (far - near));
1164        let normalized = (linear - near) / (far - near);
1165        let v = normalized.clamp(0.0, 1.0);
1166        [v, v, v]
1167    }
1168
1169    /// Map a normal vector to a visible color.
1170    pub fn normal_to_color(normal: [f32; 3]) -> [f32; 3] {
1171        [
1172            normal[0] * 0.5 + 0.5,
1173            normal[1] * 0.5 + 0.5,
1174            normal[2] * 0.5 + 0.5,
1175        ]
1176    }
1177
1178    /// Generate a heat map color from a scalar value (0..1).
1179    pub fn heat_map(value: f32) -> [f32; 3] {
1180        let v = value.clamp(0.0, 1.0);
1181        if v < 0.25 {
1182            [0.0, v * 4.0, 1.0]
1183        } else if v < 0.5 {
1184            [0.0, 1.0, 1.0 - (v - 0.25) * 4.0]
1185        } else if v < 0.75 {
1186            [(v - 0.5) * 4.0, 1.0, 0.0]
1187        } else {
1188            [1.0, 1.0 - (v - 0.75) * 4.0, 0.0]
1189        }
1190    }
1191}
1192
1193impl Default for DebugOverlayPass {
1194    fn default() -> Self {
1195        Self::new()
1196    }
1197}
1198
1199impl BuiltinPass for DebugOverlayPass {
1200    fn name(&self) -> &str {
1201        "debug_overlay"
1202    }
1203
1204    fn input_names(&self) -> Vec<&str> {
1205        vec!["aa_color", "depth"]
1206    }
1207
1208    fn output_names(&self) -> Vec<&str> {
1209        vec!["debug_color"]
1210    }
1211
1212    fn condition(&self) -> PassCondition {
1213        PassCondition::FeatureEnabled("debug_overlay".to_string())
1214    }
1215
1216    fn execute(&self, ctx: &PassContext) {
1217        let _viewport = Viewport::from_context(ctx);
1218        if self.mode == DebugVisualization::None && !self.show_grid && !self.show_wireframe {
1219            return;
1220        }
1221        let _draw = DrawCall {
1222            vertex_count: 3,
1223            instance_count: 1,
1224            pass_name: self.name().to_string(),
1225            draw_type: DrawType::FullscreenQuad,
1226        };
1227    }
1228}
1229
1230// ---------------------------------------------------------------------------
1231// 12. FinalCompositePass
1232// ---------------------------------------------------------------------------
1233
1234/// Final compositing: combines all layers and presents to the swapchain.
1235pub struct FinalCompositePass {
1236    pub letterbox: bool,
1237    pub letterbox_color: [f32; 3],
1238    pub dither: bool,
1239    pub output_format: TextureFormat,
1240}
1241
1242impl FinalCompositePass {
1243    pub fn new() -> Self {
1244        Self {
1245            letterbox: false,
1246            letterbox_color: [0.0, 0.0, 0.0],
1247            dither: true,
1248            output_format: TextureFormat::Bgra8Srgb,
1249        }
1250    }
1251
1252    pub fn register(
1253        &self,
1254        builder: &mut RenderGraphBuilder,
1255        input_color: ResourceHandle,
1256        swapchain: ResourceHandle,
1257    ) {
1258        builder
1259            .graphics_pass(self.name())
1260            .reads(input_color, "aa_color")
1261            .writes(swapchain, "swapchain")
1262            .side_effects()
1263            .tag("present")
1264            .finish();
1265    }
1266
1267    /// Apply dithering to reduce banding in 8-bit output.
1268    pub fn apply_dither(color: [f32; 3], uv: [f32; 2]) -> [f32; 3] {
1269        // Interleaved gradient noise
1270        let noise = Self::ign(uv[0], uv[1]);
1271        let dither = (noise - 0.5) / 255.0;
1272        [
1273            (color[0] + dither).clamp(0.0, 1.0),
1274            (color[1] + dither).clamp(0.0, 1.0),
1275            (color[2] + dither).clamp(0.0, 1.0),
1276        ]
1277    }
1278
1279    /// Interleaved gradient noise (Jimenez 2014).
1280    fn ign(x: f32, y: f32) -> f32 {
1281        let f = 0.06711056 * x + 0.00583715 * y;
1282        (52.9829189 * (f - f.floor())).fract()
1283    }
1284}
1285
1286impl Default for FinalCompositePass {
1287    fn default() -> Self {
1288        Self::new()
1289    }
1290}
1291
1292impl BuiltinPass for FinalCompositePass {
1293    fn name(&self) -> &str {
1294        "final_composite"
1295    }
1296
1297    fn input_names(&self) -> Vec<&str> {
1298        vec!["aa_color"]
1299    }
1300
1301    fn output_names(&self) -> Vec<&str> {
1302        vec!["swapchain"]
1303    }
1304
1305    fn execute(&self, ctx: &PassContext) {
1306        let _viewport = Viewport::from_context(ctx);
1307        let _draw = DrawCall {
1308            vertex_count: 3,
1309            instance_count: 1,
1310            pass_name: self.name().to_string(),
1311            draw_type: DrawType::FullscreenQuad,
1312        };
1313    }
1314}
1315
1316// ---------------------------------------------------------------------------
1317// Full pipeline builder
1318// ---------------------------------------------------------------------------
1319
1320/// Convenience function that builds a complete deferred rendering pipeline
1321/// with all 12 built-in passes.
1322pub fn build_deferred_pipeline(
1323    width: u32,
1324    height: u32,
1325    features: &[&str],
1326) -> RenderGraph {
1327    let mut b = RenderGraphBuilder::new("deferred_pipeline", width, height);
1328
1329    // Enable requested features
1330    for f in features {
1331        b.enable_feature(f);
1332    }
1333
1334    // Declare resources
1335    let depth = b.texture("depth", TextureFormat::Depth32Float);
1336    let albedo = b.texture("gbuffer_albedo", TextureFormat::Rgba8Srgb);
1337    let normal = b.texture("gbuffer_normal", TextureFormat::Rgba16Float);
1338    let rm = b.texture("gbuffer_rm", TextureFormat::Rgba8Unorm);
1339    let ssao_tex = b.texture_scaled("ssao", TextureFormat::R16Float, 0.5, 0.5);
1340    let hdr_color = b.texture("hdr_color", TextureFormat::Rgba16Float);
1341    let shadow_map = b.texture_absolute("shadow_map_0", TextureFormat::Depth32Float, 2048, 2048);
1342    let oit_accum = b.texture("oit_accum", TextureFormat::Rgba16Float);
1343    let oit_reveal = b.texture("oit_reveal", TextureFormat::R8Unorm);
1344    let bloom_chain = b.texture_scaled("bloom_chain", TextureFormat::Rgba16Float, 0.5, 0.5);
1345    let ldr_color = b.texture("ldr_color", TextureFormat::Rgba8Unorm);
1346    let aa_color = b.texture("aa_color", TextureFormat::Rgba8Unorm);
1347    let debug_color = b.texture("debug_color", TextureFormat::Rgba8Unorm);
1348    let swapchain = b.import("swapchain", TextureFormat::Bgra8Srgb);
1349
1350    // 1. Depth pre-pass
1351    let depth_pre = DepthPrePass::new();
1352    depth_pre.register(&mut b, depth);
1353
1354    // 2. GBuffer
1355    let gbuffer = GBufferPass::new();
1356    gbuffer.register(&mut b, depth, albedo, normal, rm);
1357
1358    // 3. Shadow (1 light for demo)
1359    let shadow = ShadowPass::new(0);
1360    shadow.register(&mut b, shadow_map);
1361
1362    // 4. SSAO
1363    let ssao = SSAOPass::new();
1364    ssao.register(&mut b, depth, normal, ssao_tex);
1365
1366    // 5. Lighting
1367    let lighting = LightingPass::new();
1368    lighting.register(&mut b, albedo, normal, rm, depth, ssao_tex, hdr_color);
1369
1370    // 6. Skybox
1371    let skybox = SkyboxPass::new();
1372    skybox.register(&mut b, depth, hdr_color);
1373
1374    // 7. Transparency (OIT)
1375    let transparency = TransparencyPass::new();
1376    transparency.register(&mut b, depth, hdr_color, oit_accum, oit_reveal);
1377
1378    // 8. Bloom
1379    let bloom = BloomPass::new();
1380    bloom.register(&mut b, hdr_color, bloom_chain);
1381
1382    // 9. Tone mapping
1383    let tonemap = ToneMappingPass::new();
1384    tonemap.register(&mut b, hdr_color, bloom_chain, ldr_color);
1385
1386    // 10. FXAA
1387    let fxaa = FXAAPass::new();
1388    fxaa.register(&mut b, ldr_color, aa_color);
1389
1390    // 11. Debug overlay
1391    let debug = DebugOverlayPass::new();
1392    debug.register(&mut b, aa_color, depth, debug_color);
1393
1394    // 12. Final composite
1395    let composite = FinalCompositePass::new();
1396    composite.register(&mut b, aa_color, swapchain);
1397
1398    b.build()
1399}
1400
1401// ---------------------------------------------------------------------------
1402// Tests
1403// ---------------------------------------------------------------------------
1404
1405#[cfg(test)]
1406mod tests {
1407    use super::*;
1408
1409    #[test]
1410    fn test_build_deferred_pipeline() {
1411        let mut graph = build_deferred_pipeline(1920, 1080, &["ssao", "fxaa"]);
1412        // Should have many passes
1413        assert!(graph.pass_count() >= 10);
1414        // Should be able to sort
1415        let result = graph.topological_sort();
1416        // May have warnings but should not have fatal cycles
1417        // (the shared hdr_color resource creates some complexity)
1418        if let Ok(sorted) = &result {
1419            assert!(!sorted.is_empty());
1420        }
1421    }
1422
1423    #[test]
1424    fn test_depth_prepass() {
1425        let pass = DepthPrePass::new();
1426        assert_eq!(pass.name(), "depth_prepass");
1427        assert!(pass.input_names().is_empty());
1428        assert_eq!(pass.output_names(), vec!["depth"]);
1429    }
1430
1431    #[test]
1432    fn test_gbuffer_pass() {
1433        let pass = GBufferPass::new();
1434        assert_eq!(pass.name(), "gbuffer");
1435        assert!(!pass.output_names().is_empty());
1436    }
1437
1438    #[test]
1439    fn test_shadow_pass() {
1440        let pass = ShadowPass::new(0);
1441        assert_eq!(pass.light_index, 0);
1442        assert_eq!(pass.cascade_count, 4);
1443        assert_eq!(pass.pass_name(), "shadow_light_0");
1444    }
1445
1446    #[test]
1447    fn test_ssao_pass() {
1448        let pass = SSAOPass::new();
1449        assert_eq!(pass.name(), "ssao");
1450        assert_eq!(pass.pass_type(), PassType::Compute);
1451        assert_eq!(pass.queue_affinity(), QueueAffinity::Compute);
1452    }
1453
1454    #[test]
1455    fn test_tone_mapping_operators() {
1456        let pass = ToneMappingPass::new();
1457        let hdr = [2.0, 1.0, 0.5];
1458        let ldr = pass.apply_operator(hdr);
1459        // All channels should be in [0, 1]
1460        for c in &ldr {
1461            assert!(*c >= 0.0 && *c <= 1.0);
1462        }
1463    }
1464
1465    #[test]
1466    fn test_fxaa_luma() {
1467        let luma = FXAAPass::luma([1.0, 1.0, 1.0]);
1468        assert!((luma - 1.0).abs() < 0.001);
1469
1470        let luma = FXAAPass::luma([0.0, 0.0, 0.0]);
1471        assert!((luma - 0.0).abs() < 0.001);
1472    }
1473
1474    #[test]
1475    fn test_debug_depth_to_color() {
1476        let color = DebugOverlayPass::depth_to_color(0.5, 0.1, 100.0);
1477        for c in &color {
1478            assert!(*c >= 0.0 && *c <= 1.0);
1479        }
1480    }
1481
1482    #[test]
1483    fn test_debug_heat_map() {
1484        let cold = DebugOverlayPass::heat_map(0.0);
1485        assert!(cold[2] > cold[0]); // blue > red at low values
1486
1487        let hot = DebugOverlayPass::heat_map(1.0);
1488        assert!(hot[0] > hot[2]); // red > blue at high values
1489    }
1490
1491    #[test]
1492    fn test_final_composite_dither() {
1493        let color = FinalCompositePass::apply_dither([0.5, 0.5, 0.5], [100.0, 200.0]);
1494        for c in &color {
1495            assert!(*c >= 0.0 && *c <= 1.0);
1496        }
1497    }
1498
1499    #[test]
1500    fn test_transparency_pass() {
1501        let pass = TransparencyPass::new();
1502        assert_eq!(pass.weight_function, WeightFunction::DepthWeight);
1503        assert_eq!(pass.name(), "transparency");
1504    }
1505
1506    #[test]
1507    fn test_bloom_pass() {
1508        let pass = BloomPass::new();
1509        assert_eq!(pass.mip_count, 5);
1510        assert_eq!(pass.name(), "bloom");
1511    }
1512
1513    #[test]
1514    fn test_skybox_pass() {
1515        let pass = SkyboxPass::new();
1516        assert_eq!(pass.name(), "skybox");
1517        assert_eq!(pass.exposure, 1.0);
1518    }
1519}