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