Skip to main content

cvkg_render_gpu/kvasir/
nodes.rs

1use crate::kvasir::node::{ExecutionContext, KvasirNode};
2use crate::kvasir::resource::ResourceId;
3use crate::passes::accessibility::AccessibilityNode;
4use crate::passes::backdrop_region::BackdropRegionNode;
5use crate::passes::bloom::{BloomBlurNode, BloomExtractNode};
6use crate::passes::composite::CompositeNode;
7use crate::passes::geometry::GeometryNode;
8use crate::passes::glass::{BackdropBlurNode, BackdropCopyNode, GlassNode};
9use crate::passes::ui::UINode;
10use crate::passes::volumetric::VolumetricNode;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub enum PassId {
14    Geometry,
15    BackdropCopy,
16    BackdropBlur,
17    Volumetric,
18    Glass,
19    UI,
20    Flow,
21    ComputeParticle,
22    BloomExtract,
23    BloomBlur,
24    Composite,
25    Accessibility,
26    Present,
27    PostProcess {
28        pipeline_id: u64,
29    },
30    /// Per-element isolated backdrop region blur.
31    BackdropRegion,
32}
33
34pub struct PresentNode {
35    pub inputs: Vec<ResourceId>,
36    pub outputs: Vec<ResourceId>,
37}
38
39impl KvasirNode for PresentNode {
40    fn label(&self) -> &'static str {
41        "Present"
42    }
43    fn inputs(&self) -> &[ResourceId] {
44        &self.inputs
45    }
46    fn outputs(&self) -> &[ResourceId] {
47        &self.outputs
48    }
49    fn pass_id(&self) -> PassId {
50        PassId::Present
51    }
52    fn execute(&self, _ctx: &mut ExecutionContext) {
53        // Presentation is handled implicitly when submitting the command buffer
54    }
55}
56
57// Built-in resource constants to wire the graph
58pub const RES_SCENE: ResourceId = ResourceId(1);
59pub const RES_SCENE_MSAA: ResourceId = ResourceId(5);
60pub const RES_BLUR_A: ResourceId = ResourceId(2);
61pub const RES_BLOOM_A: ResourceId = ResourceId(3);
62pub const RES_SWAPCHAIN: ResourceId = ResourceId(4);
63
64/// Render graph configuration parameters.
65pub struct RenderGraphConfig<'a> {
66    pub has_glass: bool,
67    pub has_bloom: bool,
68    pub has_accessibility: bool,
69    /// Whether volumetric raymarching pass is active for fog/light shaft effects.
70    pub has_volumetric: bool,
71    pub active_offscreens: &'a [crate::types::OffscreenEffectConfig],
72    pub portal_regions: &'a [cvkg_core::Rect],
73    pub width: u32,
74    pub height: u32,
75    pub scale: f32,
76}
77
78/// Build the dynamic RenderGraph (KvasirGraph)
79pub fn build_render_graph(config: &RenderGraphConfig<'_>) -> super::graph::KvasirGraph {
80    let mut builder = super::graph::GraphBuilder::new();
81
82    let geometry = builder.add_node(Box::new(GeometryNode::new()));
83    let mut last_scene_node = geometry;
84
85    for offscreen in config.active_offscreens {
86        let tex_id = ResourceId(1000 + (offscreen.target_id as u32));
87        debug_assert!(offscreen.target_id <= u32::MAX as u64, "target_id overflow");
88
89        let off_geom = builder.add_node(Box::new(
90            crate::passes::effects::OffscreenGeometryNode::new(offscreen.target_id, tex_id),
91        ));
92
93        let composite =
94            builder.add_node(Box::new(crate::passes::effects::EffectCompositeNode::new(
95                offscreen.target_id,
96                tex_id,
97                offscreen.effect.clone(),
98                offscreen.blend_mode,
99                offscreen.effect_args,
100            )));
101
102        builder.connect(off_geom, tex_id, composite);
103        builder.connect(last_scene_node, RES_SCENE, composite);
104        last_scene_node = composite;
105    }
106
107    if config.has_glass {
108        let copy = builder.add_node(Box::new(BackdropCopyNode::new()));
109        builder.connect(last_scene_node, RES_SCENE, copy);
110
111        let blur = builder.add_node(Box::new(BackdropBlurNode::new(
112            config.width / 2,
113            config.height / 2,
114        )));
115        builder.connect(copy, RES_BLUR_A, blur);
116
117        // Per-element backdrop blur for portal-aware glass elements
118        for (i, region) in config.portal_regions.iter().enumerate() {
119            let region_id = ResourceId(2000 + i as u32);
120            let region_node =
121                builder.add_node(Box::new(BackdropRegionNode::new(*region, region_id)));
122            builder.connect(last_scene_node, RES_SCENE, region_node);
123        }
124
125        let glass = builder.add_node(Box::new(GlassNode::new(config.scale)));
126        builder.connect(blur, RES_BLUR_A, glass);
127        builder.connect(last_scene_node, RES_SCENE, glass);
128        last_scene_node = glass;
129    }
130
131    let ui = builder.add_node(Box::new(UINode::new()));
132    builder.connect(last_scene_node, RES_SCENE, ui);
133    last_scene_node = ui;
134
135    // Volumetric raymarching (conditional, for fog/light shaft effects)
136    let has_volumetric = config.has_volumetric;
137    if has_volumetric {
138        let volumetric = builder.add_node(Box::new(VolumetricNode::new()));
139        builder.connect(last_scene_node, RES_SCENE, volumetric);
140        last_scene_node = volumetric;
141    }
142
143    // Bloom extraction and blur (conditional)
144    let mut last_bloom_node = None;
145    if config.has_bloom {
146        let extract = builder.add_node(Box::new(BloomExtractNode::new()));
147        builder.connect(last_scene_node, RES_SCENE, extract);
148
149        let blur = builder.add_node(Box::new(BloomBlurNode::new(
150            config.width / 2,
151            config.height / 2,
152        )));
153        builder.connect(extract, RES_BLOOM_A, blur);
154        last_bloom_node = Some(blur);
155    }
156
157    // Accessibility transform (conditional, runs before final composite)
158    if config.has_accessibility {
159        let a11y = builder.add_node(Box::new(AccessibilityNode::new()));
160        builder.connect(last_scene_node, RES_SCENE, a11y);
161        // Accessibility writes back to RES_SCENE for the composite to consume
162        last_scene_node = a11y;
163    }
164
165    // Final composite: blends scene + bloom onto the swapchain target.
166    // If accessibility ran, it already cleared the swapchain, so we load.
167    // If accessibility did NOT run, we need to clear first.
168    let composite = builder.add_node(Box::new(CompositeNode::new(
169        config.has_bloom,
170        !config.has_accessibility,
171    )));
172    builder.connect(last_scene_node, RES_SCENE, composite);
173    if let Some(bloom_node) = last_bloom_node {
174        builder.connect(bloom_node, RES_BLOOM_A, composite);
175    }
176
177    // Present node marks the graph endpoint (presentation is handled by Surface::present)
178    let present = builder.add_node(Box::new(PresentNode {
179        inputs: vec![RES_SCENE],
180        outputs: vec![],
181    }));
182    builder.connect(last_scene_node, RES_SCENE, present);
183
184    builder.build()
185}