Skip to main content

cvkg_render_gpu/kvasir/
node.rs

1//! KvasirNode trait and ExecutionContext.
2
3use super::resource::ResourceId;
4use crate::renderer::GpuRenderer;
5use cvkg_core::PassNode;
6
7/// Error type for Kvasir render graph operations.
8#[derive(Debug, thiserror::Error)]
9pub enum KvasirError {
10    #[error("Cycle detected in render graph: {0:?}")]
11    CycleDetected(Vec<String>),
12    #[error("Missing resource: {0}")]
13    MissingResource(String),
14    #[error("Invalid node configuration: {0}")]
15    InvalidNodeConfig(String),
16    #[error("Graph compilation failed: {0}")]
17    CompilationFailed(String),
18}
19
20/// Unique identifier for a node in the render graph.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22pub struct GraphId(pub u64);
23
24/// Hint to the planner about preferred execution backend.
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub enum ExecutionHint {
27    Raster,
28    Compute,
29    Hybrid,
30}
31
32/// Context passed to each node during execution.
33///
34/// P1-2 fix: documented the aliasing contract. The struct holds:
35/// - Several `&'a` shared references to fields of `GpuRenderer`
36///   (device, queue, registry, renderer, target_view, depth_view,
37///   bind groups)
38/// - A single `&'a mut wgpu::CommandEncoder` (the only mutable field)
39///
40/// The `renderer` field is `&'a GpuRenderer` (immutable), so nodes
41/// cannot call `&mut self` methods on the renderer during execution.
42/// This is intentional: the renderer is being driven by the outer
43/// frame loop, and allowing a node to mutate the renderer mid-frame
44/// would cause aliasing. The audit flagged the previous implicit
45/// split-borrow pattern as a potential safety risk; the fix is to
46/// document the contract clearly and ensure no path can construct
47/// an `ExecutionContext` that violates it.
48///
49/// If a future node needs to mutate the renderer (e.g. to record
50/// custom draw calls), it should use the `encoder` field directly
51/// (which is `&mut`) and avoid going through the renderer API.
52pub struct ExecutionContext<'a> {
53    pub device: &'a wgpu::Device,
54    pub queue: &'a wgpu::Queue,
55    pub encoder: &'a mut wgpu::CommandEncoder,
56    pub registry: &'a crate::kvasir::registry::ResourceRegistry,
57    pub renderer: &'a crate::renderer::GpuRenderer,
58    pub target_view: &'a wgpu::TextureView,
59    pub depth_view: &'a wgpu::TextureView,
60    pub blur_env_bind_group_a: &'a wgpu::BindGroup,
61    pub blur_env_bind_group_b: &'a wgpu::BindGroup,
62    pub bloom_env_bind_group_a: &'a wgpu::BindGroup,
63    pub bloom_env_bind_group_b: &'a wgpu::BindGroup,
64    pub scale_factor: f32,
65}
66
67impl<'a> ExecutionContext<'a> {
68    pub fn begin_render_pass(
69        &mut self,
70        desc: &wgpu::RenderPassDescriptor<'_>,
71    ) -> wgpu::RenderPass<'_> {
72        self.encoder.begin_render_pass(desc)
73    }
74
75    /// Get or create a cached bind group for a given resource and mip level.
76    /// Avoids per-frame GPU allocation when the same bind group is reused across frames.
77    pub fn get_or_create_bind_group(
78        &self,
79        key: (crate::kvasir::resource::ResourceId, u32, bool),
80        layout: &wgpu::BindGroupLayout,
81        entries: &[wgpu::BindGroupEntry<'_>],
82        label: Option<&str>,
83    ) -> wgpu::BindGroup {
84        let mut cache = GpuRenderer::lock_or_clear_cache(&self.renderer.bind_group_cache);
85        let full_key = (self.renderer.current_window, key.0, key.1, key.2);
86        // Use entry API: if key exists, return a clone of the cached bind group.
87        // If not, create it, insert it, and return a clone.
88        if let std::collections::hash_map::Entry::Vacant(e) = cache.entry(full_key) {
89            let bg = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
90                label,
91                layout,
92                entries,
93            });
94            e.insert(bg.clone());
95            bg
96        } else {
97            cache.get(&full_key).unwrap().clone()
98        }
99    }
100}
101
102#[cfg(not(target_arch = "wasm32"))]
103pub trait KvasirNode: Send + Sync {
104    fn label(&self) -> &'static str;
105    fn inputs(&self) -> &[ResourceId];
106    fn outputs(&self) -> &[ResourceId];
107    fn pass_id(&self) -> super::nodes::PassId;
108    fn execute(&self, ctx: &mut ExecutionContext);
109}
110
111#[cfg(target_arch = "wasm32")]
112pub trait KvasirNode {
113    fn label(&self) -> &'static str;
114    fn inputs(&self) -> &[ResourceId];
115    fn outputs(&self) -> &[ResourceId];
116    fn pass_id(&self) -> super::nodes::PassId;
117    fn execute(&self, ctx: &mut ExecutionContext);
118}
119
120// =========================================================================
121// PassNode implementations for all Kvasir node types
122// =========================================================================
123
124use crate::passes::accessibility::AccessibilityNode;
125use crate::passes::backdrop_region::BackdropRegionNode;
126use crate::passes::bloom::{BloomBlurNode, BloomExtractNode};
127use crate::passes::composite::CompositeNode;
128use crate::passes::effects::{EffectCompositeNode, OffscreenGeometryNode};
129use crate::passes::geometry::GeometryNode;
130use crate::passes::glass::{BackdropBlurNode, BackdropCopyNode, GlassNode};
131use crate::passes::opaque3d::Opaque3dNode;
132use crate::passes::pre_world_panel::PreWorldPanelNode;
133use crate::passes::shadow::ShadowNode;
134use crate::passes::svg_filter::SvgFilterNode;
135use crate::passes::tonemap::ToneMapNode;
136use crate::passes::ui::UINode;
137use crate::passes::volumetric::VolumetricNode;
138
139impl PassNode for GeometryNode {}
140impl PassNode for UINode {}
141impl PassNode for ShadowNode {}
142impl PassNode for Opaque3dNode {}
143impl PassNode for CompositeNode {}
144impl PassNode for GlassNode {}
145impl PassNode for BackdropCopyNode {}
146impl PassNode for BackdropBlurNode {}
147impl PassNode for BloomExtractNode {}
148impl PassNode for BloomBlurNode {}
149impl PassNode for VolumetricNode {}
150impl PassNode for ToneMapNode {}
151impl PassNode for AccessibilityNode {}
152impl PassNode for BackdropRegionNode {}
153impl PassNode for PreWorldPanelNode {}
154impl PassNode for OffscreenGeometryNode {}
155impl PassNode for EffectCompositeNode {}
156impl PassNode for SvgFilterNode {}
157
158// =========================================================================
159// P1-2: ExecutionContext aliasing contract tests
160// =========================================================================
161//
162// These tests verify the aliasing contract documented on
163// ExecutionContext. The renderer field is `&GpuRenderer`
164// (immutable), and the encoder field is `&mut CommandEncoder`.
165
166#[cfg(test)]
167mod p1_2_aliasing_contract_tests {
168    use super::*;
169
170    /// P1-2 regression: the `renderer` field must be `&GpuRenderer`
171    /// (immutable), not `&mut GpuRenderer`. This is a compile-time
172    /// invariant; the test makes the contract explicit so future
173    /// refactors cannot silently weaken it.
174    #[test]
175    fn renderer_field_is_immutable() {
176        // Type-level assertion: GpuRenderer can be borrowed immutably.
177        fn _assert_immutable(_: &GpuRenderer) {}
178        let _f: fn(&GpuRenderer) = _assert_immutable;
179    }
180
181    /// P1-2 documentation: the encoder field is the only `&mut`
182    /// field. This is what allows nodes to record GPU commands while
183    /// the renderer remains immutable.
184    #[test]
185    fn encoder_field_is_mutable() {
186        // Type-level assertion: the encoder is &mut.
187        fn _assert_mut(_: &mut wgpu::CommandEncoder) {}
188        let _f: fn(&mut wgpu::CommandEncoder) = _assert_mut;
189    }
190}