Skip to main content

cvkg_render_gpu/passes/
geometry.rs

1use crate::kvasir::node::{ExecutionContext, KvasirNode};
2use crate::kvasir::nodes::{PassId, RES_SCENE};
3use crate::kvasir::resource::ResourceId;
4
5pub struct GeometryNode {
6    pub inputs: Vec<ResourceId>,
7    pub outputs: Vec<ResourceId>,
8}
9
10impl GeometryNode {
11    pub fn new() -> Self {
12        Self {
13            inputs: vec![],
14            outputs: vec![RES_SCENE],
15        }
16    }
17}
18
19impl Default for GeometryNode {
20    fn default() -> Self {
21        Self::new()
22    }
23}
24
25impl KvasirNode for GeometryNode {
26    fn label(&self) -> &'static str {
27        "Geometry"
28    }
29
30    fn inputs(&self) -> &[ResourceId] {
31        &self.inputs
32    }
33
34    fn outputs(&self) -> &[ResourceId] {
35        &self.outputs
36    }
37
38    fn pass_id(&self) -> PassId {
39        PassId::Geometry
40    }
41
42    fn execute(&self, ctx: &mut ExecutionContext) {
43        let scale = ctx.renderer.current_scale_factor();
44        let rt_w = ctx.renderer.current_width() as i32;
45        let rt_h = ctx.renderer.current_height() as i32;
46
47        let scene_view = match ctx.registry.get_texture_view(RES_SCENE) {
48            Some(v) => v,
49            None => {
50                tracing::error!("Missing texture view for {}", stringify!(RES_SCENE));
51                return;
52            }
53        };
54        let msaa_view = match ctx
55            .registry
56            .get_texture_view(crate::kvasir::nodes::RES_SCENE_MSAA)
57        {
58            Some(v) => v,
59            None => {
60                tracing::error!(
61                    "Missing texture view for {}",
62                    stringify!(crate::kvasir::nodes::RES_SCENE_MSAA)
63                );
64                return;
65            }
66        };
67        let depth_view = ctx.depth_view;
68
69        let bg = ctx.renderer.default_background_color;
70                let mut p = ctx.encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
71                    label: Some("Surtr P1 Opaque Background"),
72                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
73                view: &msaa_view,
74                resolve_target: Some(&scene_view),
75                ops: wgpu::Operations {
76                    load: wgpu::LoadOp::Clear(wgpu::Color {
77                        r: bg[0] as f64,
78                        g: bg[1] as f64,
79                        b: bg[2] as f64,
80                        a: bg[3] as f64,
81                    }),
82                    store: wgpu::StoreOp::Store,
83                },
84                depth_slice: None,
85            })],
86            depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
87                view: depth_view,
88                depth_ops: Some(wgpu::Operations {
89                    load: wgpu::LoadOp::Clear(0.0),
90                    store: wgpu::StoreOp::Store,
91                }),
92                stencil_ops: None,
93            }),
94            timestamp_writes: ctx.renderer.skuld_queries.as_ref().map(|q| {
95                wgpu::RenderPassTimestampWrites {
96                    query_set: q,
97                    beginning_of_pass_write_index: Some(0),
98                    end_of_pass_write_index: None,
99                }
100            }),
101            occlusion_query_set: None,
102            multiview_mask: None,
103        });
104
105        if ctx.renderer.current_scene.scene_type == cvkg_core::SCENE_AURORA {
106            p.set_pipeline(&ctx.renderer.background_pipeline);
107            p.set_bind_group(0, &ctx.renderer.dummy_texture_bind_group, &[]);
108            p.set_bind_group(1, &ctx.renderer.dummy_env_bind_group, &[]);
109            p.set_bind_group(2, &ctx.renderer.berserker_bind_group, &[]);
110            p.set_bind_group(3, &ctx.renderer.gradient_bind_group, &[]);
111            p.draw(0..3, 0..1);
112        }
113
114        if !ctx.renderer.draw_calls.is_empty() {
115            tracing::trace!(
116                "[Kvasir] GeometryNode: draw_calls={}",
117                ctx.renderer.draw_calls.len()
118            );
119            for (i, call) in ctx.renderer.draw_calls.iter().enumerate() {
120                tracing::trace!(
121                    "[Kvasir]   call[{}]: material={:?}, target_id={:?}, index_start={}, index_count={}",
122                    i,
123                    call.material,
124                    call.target_id,
125                    call.index_start,
126                    call.index_count
127                );
128            }
129            p.set_vertex_buffer(0, ctx.renderer.geometry_buffers.vertex_buffer.slice(..));
130            p.set_vertex_buffer(1, ctx.renderer.geometry_buffers.instance_buffer.slice(..));
131            p.set_index_buffer(
132                ctx.renderer.geometry_buffers.index_buffer.slice(..),
133                wgpu::IndexFormat::Uint32,
134            );
135            p.set_bind_group(1, &ctx.renderer.dummy_env_bind_group, &[]);
136            p.set_bind_group(2, &ctx.renderer.berserker_bind_group, &[]);
137            p.set_bind_group(3, &ctx.renderer.gradient_bind_group, &[]);
138
139            // P2-6: Geometry pass filtering contract
140            // Only draws Opaque material calls with no render target binding.
141            // - DrawMaterial::Opaque: standard geometry (rects, text, SVGs).
142            // - target_id.is_none(): not a texture-mapped call (those go through
143            //   the offscreen/target pass).
144            // - Glass calls (material_id=7) are handled by GlassNode.
145            // - Calls with target_id.is_some() are rendered in the offscreen pass.
146            let mut opaque_calls_count = 0;
147            for call in ctx.renderer.draw_calls.iter().filter(|c| {
148                (matches!(c.material, cvkg_core::DrawMaterial::Opaque)
149                    || matches!(c.material, cvkg_core::DrawMaterial::Blend { .. }))
150                    && c.target_id.is_none()
151            }) {
152                opaque_calls_count += 1;
153                p.set_pipeline(&ctx.renderer.opaque_pipeline);
154
155                // Non-trivial algorithm: Scissor Clipping and Resetting
156                // WHY: Limits geometry drawing to bounds specified by push_clip_rect (e.g. clipping lightning to main demo canvas).
157                // CONTRACT: If scissor_rect is Some, we calculate hardware coordinates and set it. Otherwise, we reset scissor to full texture.
158                if let Some(rect) = call.scissor_rect
159                    && rt_w > 0
160                    && rt_h > 0
161                {
162                    let x1 = (rect.x * scale).round() as i32;
163                    let y1 = (rect.y * scale).round() as i32;
164                    let x2 = ((rect.x + rect.width) * scale).round() as i32;
165                    let y2 = ((rect.y + rect.height) * scale).round() as i32;
166                    let w = (x2 - x1).clamp(0, rt_w);
167                    let h = (y2 - y1).clamp(0, rt_h);
168                    if w > 0 && h > 0 {
169                        p.set_scissor_rect(x1 as u32, y1 as u32, w as u32, h as u32);
170                    } else {
171                        // P2-7 fix: when scissor dimensions are zero, use
172                        // a zero-area scissor rect (e.g., 0,0,0,0) which
173                        // causes wgpu to skip the draw entirely, instead
174                        // of the previous behavior of drawing to a single
175                        // 1x1 pixel which is visually wrong.
176                        p.set_scissor_rect(0, 0, 0, 0);
177                    }
178                } else {
179                    p.set_scissor_rect(0, 0, rt_w as u32, rt_h as u32);
180                }
181
182                let bg = if let Some(id) = call.texture_id {
183                    if id == 0 {
184                        &ctx.renderer.mega_heim_bind_group
185                    } else {
186                        ctx.renderer
187                            .texture_bind_groups
188                            .get(id as usize)
189                            .unwrap_or(&ctx.renderer.dummy_texture_bind_group)
190                    }
191                } else {
192                    &ctx.renderer.dummy_texture_bind_group
193                };
194                p.set_bind_group(0, bg, &[]);
195                p.draw_indexed(
196                    call.index_start..call.index_start + call.index_count,
197                    0,
198                    call.instance_start..call.instance_start + call.instance_count,
199                );
200            }
201            tracing::trace!(
202                "[Kvasir] GeometryNode: opaque_calls drawn={}",
203                opaque_calls_count
204            );
205        }
206    }
207}