Skip to main content

cvkg_render_gpu/passes/
pre_world_panel.rs

1use crate::kvasir::node::{ExecutionContext, KvasirNode};
2use crate::kvasir::resource::ResourceId;
3
4/// PreWorldPanelNode renders each WorldSpacePanel's VDOM subtree to its
5/// offscreen texture. The panel geometries are then submitted as textured
6/// quads by the Geometry pass.
7pub struct PreWorldPanelNode {
8    pub panel_textures: Vec<ResourceId>,
9    pub inputs: Vec<ResourceId>,
10    pub outputs: Vec<ResourceId>,
11}
12
13impl PreWorldPanelNode {
14    pub fn new(panel_textures: Vec<ResourceId>) -> Self {
15        Self {
16            panel_textures: panel_textures.clone(),
17            inputs: vec![],
18            outputs: panel_textures,
19        }
20    }
21}
22
23impl KvasirNode for PreWorldPanelNode {
24    fn label(&self) -> &'static str {
25        "PreWorldPanel"
26    }
27    fn inputs(&self) -> &[ResourceId] {
28        &self.inputs
29    }
30    fn outputs(&self) -> &[ResourceId] {
31        &self.outputs
32    }
33    fn pass_id(&self) -> crate::kvasir::nodes::PassId {
34        crate::kvasir::nodes::PassId::PreWorldPanel
35    }
36
37    fn execute(&self, ctx: &mut ExecutionContext) {
38        // For each panel texture, render the corresponding VDOM subtree.
39        // This is a simplified implementation - the actual implementation
40        // would iterate over panels, bind each offscreen texture as the
41        // render target, and run the UI rendering pipeline for that panel's
42        // VDOM subtree.
43
44        for (i, &panel_tex) in self.panel_textures.iter().enumerate() {
45            let view = match ctx.registry.get_texture_view(panel_tex) {
46                Some(v) => v,
47                None => {
48                    tracing::error!(
49                        "PreWorldPanel: missing texture view for panel {} ({})",
50                        i,
51                        panel_tex.0
52                    );
53                    continue;
54                }
55            };
56
57            // Clear the offscreen texture
58            let mut pass = ctx.encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
59                label: Some(&format!("Surtr PreWorldPanel {}", i)),
60                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
61                    view: &view,
62                    resolve_target: None,
63                    ops: wgpu::Operations {
64                        load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
65                        store: wgpu::StoreOp::Store,
66                    },
67                    depth_slice: None,
68                })],
69                depth_stencil_attachment: None,
70                timestamp_writes: None,
71                occlusion_query_set: None,
72                multiview_mask: None,
73            });
74
75            // Bind UI bind groups
76            pass.set_bind_group(1, &ctx.renderer.dummy_env_bind_group, &[]);
77            pass.set_bind_group(2, &ctx.renderer.berserker_bind_group, &[]);
78            pass.set_bind_group(3, &ctx.renderer.gradient_bind_group, &[]);
79
80            // TODO: Filter draw_calls to only those belonging to this panel's VDOM subtree.
81            // For now, render all draw calls (will be refined when panel filtering is wired).
82            if !ctx.renderer.draw_calls.is_empty() {
83                pass.set_vertex_buffer(0, ctx.renderer.geometry_buffers.vertex_buffer.slice(..));
84                pass.set_vertex_buffer(1, ctx.renderer.geometry_buffers.instance_buffer.slice(..));
85                pass.set_index_buffer(
86                    ctx.renderer.geometry_buffers.index_buffer.slice(..),
87                    wgpu::IndexFormat::Uint32,
88                );
89
90                for call in &ctx.renderer.draw_calls {
91                    // Skip 2D-only draw calls (those that don't target a panel)
92                    // In the future, we'd filter by panel_id or similar.
93                    if call.instance_count == 1 && call.instance_start == 0 {
94                        continue; // Skip 2D-only instances
95                    }
96                    pass.draw_indexed(
97                        call.index_start..call.index_start + call.index_count,
98                        0,
99                        call.instance_start..call.instance_start + call.instance_count,
100                    );
101                }
102            }
103        }
104    }
105}
106
107#[cfg(test)]
108mod pre_world_panel_tests {
109    use super::*;
110
111    #[test]
112    fn test_pre_world_panel_node_label() {
113        let node = PreWorldPanelNode::new(vec![]);
114        assert_eq!(node.label(), "PreWorldPanel");
115    }
116
117    #[test]
118    fn test_pre_world_panel_node_empty() {
119        let node = PreWorldPanelNode::new(vec![]);
120        assert!(node.inputs().is_empty());
121        assert!(node.outputs().is_empty());
122        assert!(node.panel_textures.is_empty());
123    }
124
125    #[test]
126    fn test_pre_world_panel_node_single_panel() {
127        let tex = ResourceId(2000);
128        let node = PreWorldPanelNode::new(vec![tex]);
129        assert_eq!(node.inputs().len(), 0);
130        assert_eq!(node.outputs().len(), 1);
131        assert_eq!(node.outputs()[0], tex);
132        assert_eq!(node.panel_textures.len(), 1);
133    }
134
135    #[test]
136    fn test_pre_world_panel_node_multiple_panels() {
137        let texes = vec![ResourceId(2000), ResourceId(2001), ResourceId(2002)];
138        let node = PreWorldPanelNode::new(texes.clone());
139        assert_eq!(node.outputs().len(), 3);
140        assert_eq!(node.panel_textures.len(), 3);
141        for (i, tex) in texes.iter().enumerate() {
142            assert_eq!(node.panel_textures[i], *tex);
143        }
144    }
145
146    #[test]
147    fn test_pre_world_panel_node_pass_id() {
148        let node = PreWorldPanelNode::new(vec![]);
149        assert!(matches!(
150            node.pass_id(),
151            crate::kvasir::nodes::PassId::PreWorldPanel
152        ));
153    }
154}