Skip to main content

cvkg_render_gpu/kvasir/
nodes.rs

1use crate::kvasir::{ExecutionContext, KvasirNode, ResourceId};
2use crate::passes::accessibility::AccessibilityNode;
3use crate::passes::backdrop_region::BackdropRegionNode;
4use crate::passes::bloom::{BloomBlurNode, BloomExtractNode};
5use crate::passes::composite::CompositeNode;
6use crate::passes::geometry::GeometryNode;
7use crate::passes::glass::{BackdropBlurNode, BackdropCopyNode, GlassNode};
8use crate::passes::opaque3d::Opaque3dNode;
9use crate::passes::pre_world_panel::PreWorldPanelNode;
10use crate::passes::shadow::{DirectionalLight, GpuMesh3d, ShadowNode};
11use crate::passes::ui::UINode;
12use crate::passes::volumetric::VolumetricNode;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub enum PassId {
16    PreWorldPanel,
17    Geometry,
18    BackdropCopy,
19    BackdropBlur,
20    Volumetric,
21    Glass,
22    UI,
23    Flow,
24    ComputeParticle,
25    BloomExtract,
26    BloomBlur,
27    Composite,
28    Accessibility,
29    Present,
30    PostProcess {
31        pipeline_id: u64,
32    },
33    /// Per-element isolated backdrop region blur.
34    BackdropRegion,
35    /// 3D shadow pass rendering depth maps.
36    Shadow,
37    /// 3D opaque pass rendering meshes with PBR.
38    Opaque3d,
39}
40
41pub struct PresentNode {
42    pub inputs: Vec<ResourceId>,
43    pub outputs: Vec<ResourceId>,
44}
45
46impl KvasirNode for PresentNode {
47    fn label(&self) -> &'static str {
48        "Present"
49    }
50
51    fn inputs(&self) -> &[ResourceId] {
52        &self.inputs
53    }
54
55    fn outputs(&self) -> &[ResourceId] {
56        &self.outputs
57    }
58
59    fn pass_id(&self) -> PassId {
60        PassId::Present
61    }
62
63    fn execute(&self, _ctx: &mut ExecutionContext) {
64        // Presentation is handled implicitly when submitting the command buffer
65    }
66}
67
68// Built-in resource constants to wire the graph
69pub const RES_SCENE: ResourceId = ResourceId(1);
70pub const RES_SCENE_MSAA: ResourceId = ResourceId(5);
71pub const RES_BLUR_A: ResourceId = ResourceId(2);
72pub const RES_BLOOM_A: ResourceId = ResourceId(3);
73pub const RES_SWAPCHAIN: ResourceId = ResourceId(4);
74
75/// Render graph configuration parameters.
76pub struct RenderGraphConfig<'a> {
77    pub has_glass: bool,
78    pub has_bloom: bool,
79    pub has_accessibility: bool,
80    /// Whether volumetric raymarching pass is active for fog/light shaft effects.
81    pub has_volumetric: bool,
82    pub active_offscreens: &'a [crate::types::OffscreenEffectConfig],
83    pub portal_regions: &'a [cvkg_core::Rect],
84    /// World-space UI panels that render to offscreen textures for 3D compositing.
85    pub world_space_panels: &'a [(u64, cvkg_vdom::WorldSpacePanel)],
86    pub width: u32,
87    pub height: u32,
88    pub scale: f32,
89    /// Active directional light for shadow pass (if set, shadow map is allocated).
90    pub directional_light: Option<DirectionalLight>,
91    /// GPU-ready 3D mesh instances for shadow map and opaque pass rendering.
92    pub mesh_instances_3d: Vec<GpuMesh3d>,
93    /// Scene radius for shadow frustum computation.
94    pub scene_radius: f32,
95}
96
97/// Build the dynamic RenderGraph (KvasirGraph)
98pub fn build_render_graph(config: &RenderGraphConfig<'_>) -> super::graph::KvasirGraph {
99    let mut builder = super::graph::GraphBuilder::new();
100
101    // PreWorldPanel pass: render WorldSpacePanel subtrees to offscreen textures.
102    // These textures will be sampled by Geometry pass for 3D quad compositing.
103    let mut panel_outputs = Vec::new();
104    for (i, panel) in config.world_space_panels.iter().enumerate() {
105        let size = panel.1.texture_resolution();
106        let tex_id = ResourceId(2000 + i as u32);
107        panel_outputs.push(tex_id);
108    }
109
110    if !panel_outputs.is_empty() {
111        let pre_panel = builder.add_node(Box::new(PreWorldPanelNode::new(panel_outputs)));
112        // No output connection needed - panels write to their allocated offscreen textures.
113        // Geometry pass will sample them via their ResourceIds.
114    }
115
116    let geometry = builder.add_node(Box::new(GeometryNode::new()));
117    let mut last_scene_node = geometry;
118
119    for offscreen in config.active_offscreens {
120        let tex_id = ResourceId(1000 + (offscreen.target_id as u32));
121        debug_assert!(offscreen.target_id <= u32::MAX as u64, "target_id overflow");
122
123        let off_geom = builder.add_node(Box::new(
124            crate::passes::effects::OffscreenGeometryNode::new(offscreen.target_id, tex_id),
125        ));
126
127        let composite =
128            builder.add_node(Box::new(crate::passes::effects::EffectCompositeNode::new(
129                offscreen.target_id,
130                tex_id,
131                offscreen.effect.clone(),
132                offscreen.blend_mode,
133                offscreen.effect_args,
134            )));
135
136        builder.connect(off_geom, tex_id, composite);
137        builder.connect(last_scene_node, RES_SCENE, composite);
138        last_scene_node = composite;
139    }
140
141    if config.has_glass {
142        let copy = builder.add_node(Box::new(BackdropCopyNode::new()));
143        builder.connect(last_scene_node, RES_SCENE, copy);
144
145        let blur = builder.add_node(Box::new(BackdropBlurNode::new(
146            config.width / 2,
147            config.height / 2,
148        )));
149        builder.connect(copy, RES_BLUR_A, blur);
150
151        // Per-element backdrop blur for portal-aware glass elements
152        for (i, region) in config.portal_regions.iter().enumerate() {
153            let region_id = ResourceId(2000 + i as u32);
154            let region_node =
155                builder.add_node(Box::new(BackdropRegionNode::new(*region, region_id)));
156            builder.connect(last_scene_node, RES_SCENE, region_node);
157        }
158
159        let glass = builder.add_node(Box::new(GlassNode::new(config.scale)));
160        builder.connect(blur, RES_BLUR_A, glass);
161        builder.connect(last_scene_node, RES_SCENE, glass);
162        last_scene_node = glass;
163    }
164
165    let ui = builder.add_node(Box::new(UINode::new()));
166    builder.connect(last_scene_node, RES_SCENE, ui);
167    last_scene_node = ui;
168
169    // Volumetric raymarching (conditional, for fog/light shaft effects)
170    let has_volumetric = config.has_volumetric;
171    if has_volumetric {
172        let volumetric = builder.add_node(Box::new(VolumetricNode::new()));
173        builder.connect(last_scene_node, RES_SCENE, volumetric);
174        last_scene_node = volumetric;
175    }
176
177    // 3D Shadow pass (runs before opaque 3D, outputs shadow map)
178    if let Some(light) = &config.directional_light {
179        if !config.mesh_instances_3d.is_empty() {
180            let shadow_rid = ResourceId(10000); // dedicated shadow map resource
181            let shadow_node = builder.add_node(Box::new(ShadowNode {
182                light: *light,
183                shadow_map: shadow_rid,
184                mesh_instances: config.mesh_instances_3d.clone(),
185                scene_radius: config.scene_radius,
186            }));
187            // Shadow runs before scene — scene reads the shadow map.
188
189            // 3D Opaque pass (runs after shadow, reads shadow map)
190            let opaque_3d_node = builder.add_node(Box::new(Opaque3dNode {
191                mesh_instances: config.mesh_instances_3d.clone(),
192                light: *light,
193                shadow_map: shadow_rid,
194            }));
195            builder.connect(shadow_node, shadow_rid, opaque_3d_node);
196            builder.connect(opaque_3d_node, RES_SCENE, last_scene_node);
197            // Opaque 3d writes to scene — update last_scene_node to chain off it.
198            last_scene_node = opaque_3d_node;
199        }
200    }
201
202    // Bloom extraction and blur (conditional)
203    let mut last_bloom_node = None;
204    if config.has_bloom {
205        let extract = builder.add_node(Box::new(BloomExtractNode::new()));
206        builder.connect(last_scene_node, RES_SCENE, extract);
207
208        let blur = builder.add_node(Box::new(BloomBlurNode::new(
209            config.width / 2,
210            config.height / 2,
211        )));
212        builder.connect(extract, RES_BLOOM_A, blur);
213        last_bloom_node = Some(blur);
214    }
215
216    // Accessibility transform (conditional, runs before final composite)
217    if config.has_accessibility {
218        let a11y = builder.add_node(Box::new(AccessibilityNode::new()));
219        builder.connect(last_scene_node, RES_SCENE, a11y);
220        // Accessibility writes back to RES_SCENE for the composite to consume
221        last_scene_node = a11y;
222    }
223
224    // Final composite: blends scene + bloom onto the swapchain target.
225    // If accessibility ran, it already cleared the swapchain, so we load.
226    // If accessibility did NOT run, we need to clear first.
227    let composite = builder.add_node(Box::new(CompositeNode::new(
228        config.has_bloom,
229        !config.has_accessibility,
230    )));
231    builder.connect(last_scene_node, RES_SCENE, composite);
232    if let Some(bloom_node) = last_bloom_node {
233        builder.connect(bloom_node, RES_BLOOM_A, composite);
234    }
235
236    // Present node marks the graph endpoint (presentation is handled by Surface::present)
237    let present = builder.add_node(Box::new(PresentNode {
238        inputs: vec![RES_SCENE],
239        outputs: vec![],
240    }));
241    builder.connect(last_scene_node, RES_SCENE, present);
242
243    builder.build()
244}