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