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