Skip to main content

cvkg_render_gpu/passes/
glass.rs

1use crate::kvasir::node::{ExecutionContext, KvasirNode};
2use crate::kvasir::nodes::{PassId, RES_BLUR_A, RES_SCENE};
3use crate::kvasir::resource::ResourceId;
4use crate::renderer::GpuRenderer;
5
6pub struct BackdropCopyNode {
7    pub inputs: Vec<ResourceId>,
8    pub outputs: Vec<ResourceId>,
9}
10
11impl BackdropCopyNode {
12    pub fn new() -> Self {
13        Self {
14            inputs: vec![RES_SCENE],
15            outputs: vec![RES_BLUR_A],
16        }
17    }
18}
19
20impl KvasirNode for BackdropCopyNode {
21    fn label(&self) -> &'static str {
22        "Backdrop Copy"
23    }
24
25    fn inputs(&self) -> &[ResourceId] {
26        &self.inputs
27    }
28
29    fn outputs(&self) -> &[ResourceId] {
30        &self.outputs
31    }
32
33    fn pass_id(&self) -> PassId {
34        PassId::BackdropCopy
35    }
36
37    fn execute(&self, ctx: &mut ExecutionContext) {
38        let target_texture = match ctx.registry.get_texture(RES_BLUR_A) {
39            Some(v) => v,
40            None => {
41                log::error!("Missing texture for {}", stringify!(RES_BLUR_A));
42                return;
43            }
44        };
45        let target_view = {
46            let mut cache = GpuRenderer::lock_or_clear_cache(&ctx.renderer.texture_view_cache);
47            cache
48                .entry((RES_BLUR_A, 0))
49                .or_insert_with(|| {
50                    target_texture.create_view(&wgpu::TextureViewDescriptor {
51                        label: Some("backdrop_copy_mip0"),
52                        base_mip_level: 0,
53                        mip_level_count: Some(1),
54                        ..Default::default()
55                    })
56                })
57                .clone()
58        };
59
60        let mut p = ctx.encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
61            label: Some("Surtr Backdrop Copy"),
62            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
63                view: &target_view,
64                resolve_target: None,
65                ops: wgpu::Operations {
66                    load: wgpu::LoadOp::Clear(wgpu::Color {
67                        r: 0.0,
68                        g: 0.0,
69                        b: 0.0,
70                        a: 0.0,
71                    }),
72                    store: wgpu::StoreOp::Store,
73                },
74                depth_slice: None,
75            })],
76            ..Default::default()
77        });
78
79        p.set_pipeline(&ctx.renderer.copy_pipeline);
80
81        let scene_view = match ctx.registry.get_texture_view(RES_SCENE) {
82            Some(v) => v,
83            None => {
84                log::error!("Missing texture view for {}", stringify!(RES_SCENE));
85                return;
86            }
87        };
88        let source_bind_group = {
89            let mut cache = GpuRenderer::lock_or_clear_cache(&ctx.renderer.bind_group_cache);
90            cache
91                .entry((RES_SCENE, 0, false))
92                .or_insert_with(|| {
93                    ctx.device.create_bind_group(&wgpu::BindGroupDescriptor {
94                        label: Some("backdrop_copy_bg"),
95                        layout: &ctx.renderer.texture_bind_group_layout,
96                        entries: &[
97                            wgpu::BindGroupEntry {
98                                binding: 0,
99                                resource: wgpu::BindingResource::TextureViewArray(&vec![
100                                    &scene_view;
101                                    32
102                                ]),
103                            },
104                            wgpu::BindGroupEntry {
105                                binding: 1,
106                                resource: wgpu::BindingResource::Sampler(
107                                    &ctx.renderer.dummy_sampler,
108                                ),
109                            },
110                        ],
111                    })
112                })
113                .clone()
114        };
115
116        p.set_bind_group(0, &source_bind_group, &[]);
117        p.set_bind_group(1, &ctx.renderer.dummy_env_bind_group, &[]);
118        p.set_bind_group(2, &ctx.renderer.berserker_bind_group, &[]);
119        p.draw(0..3, 0..1);
120    }
121}
122
123pub struct BackdropBlurNode {
124    pub inputs: Vec<ResourceId>,
125    pub outputs: Vec<ResourceId>,
126    pub width: u32,
127    pub height: u32,
128}
129
130impl BackdropBlurNode {
131    pub fn new(width: u32, height: u32) -> Self {
132        Self {
133            inputs: vec![RES_BLUR_A],
134            outputs: vec![RES_BLUR_A],
135            width,
136            height,
137        }
138    }
139}
140
141impl KvasirNode for BackdropBlurNode {
142    fn label(&self) -> &'static str {
143        "Backdrop Blur"
144    }
145
146    fn inputs(&self) -> &[ResourceId] {
147        &self.inputs
148    }
149
150    fn outputs(&self) -> &[ResourceId] {
151        &self.outputs
152    }
153
154    fn pass_id(&self) -> PassId {
155        PassId::BackdropBlur
156    }
157
158    fn execute(&self, ctx: &mut ExecutionContext) {
159        let blur_tex = match ctx.registry.get_texture(RES_BLUR_A) {
160            Some(v) => v,
161            None => {
162                log::error!("Missing texture for {}", stringify!(RES_BLUR_A));
163                return;
164            }
165        };
166
167        // Reuse persistent uniform buffer (avoids per-frame GPU allocation)
168        let kawase_uniform = &ctx.renderer.kawase_uniform;
169
170        // Derive mip count from the actual texture, not hardcoded
171        let num_mips = blur_tex.mip_level_count();
172        // Non-trivial algorithm: Kawase Blur Mip Cap
173        // WHY: More mip levels allow downsampling the scene to coarser dimensions, yielding a wider
174        // backdrop blur ("bigger bloom"). We support up to 6 levels (960x540 down to 30x16) for intense glassmorphism.
175        // CONTRACT: effective_mips is clamped between 2 and the actual mip count of the texture.
176        let effective_mips = (num_mips as usize).min(6);
177        if effective_mips < 2 {
178            return;
179        }
180
181        let mip_views: Vec<wgpu::TextureView> = (0..effective_mips)
182            .map(|mip| {
183                let mut cache = GpuRenderer::lock_or_clear_cache(&ctx.renderer.texture_view_cache);
184                cache
185                    .entry((RES_BLUR_A, mip as u32))
186                    .or_insert_with(|| {
187                        blur_tex.create_view(&wgpu::TextureViewDescriptor {
188                            label: Some(&format!("blur_mip_{}", mip)),
189                            base_mip_level: mip as u32,
190                            mip_level_count: Some(1),
191                            ..Default::default()
192                        })
193                    })
194                    .clone()
195            })
196            .collect();
197
198        let blur_width = self.width;
199        let blur_height = self.height;
200
201        // Compute mip scales dynamically based on actual mip count
202        let mip_scales: Vec<(f32, f32, f32)> = (0..effective_mips)
203            .map(|i| {
204                let div = (1u32 << i) as f32;
205                (
206                    blur_width as f32 / div,
207                    blur_height as f32 / div,
208                    (i + 1) as f32,
209                )
210            })
211            .collect();
212
213        for mip in 1..effective_mips {
214            let kernel_width = mip_scales[mip].2;
215            let uniform_data: [f32; 8] = [
216                mip_scales[(mip - 1)].0,
217                mip_scales[(mip - 1)].1,
218                (mip - 1) as f32,
219                kernel_width,
220                0.0,
221                0.0,
222                0.0,
223                0.0,
224            ];
225            ctx.queue
226                .write_buffer(kawase_uniform, 0, bytemuck::cast_slice(&uniform_data[..8]));
227
228            let w = mip_scales[mip].0.max(1.0) as u32;
229            let h = mip_scales[mip].1.max(1.0) as u32;
230
231            // Retrieve or create bind group for this mip level from cache
232            let bg = ctx.get_or_create_bind_group(
233                (RES_BLUR_A, mip as u32, false),
234                &ctx.renderer.kawase_bind_group_layout,
235                &[
236                    wgpu::BindGroupEntry {
237                        binding: 0,
238                        resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
239                            buffer: kawase_uniform,
240                            offset: 0,
241                            size: wgpu::BufferSize::new(32),
242                        }),
243                    },
244                    wgpu::BindGroupEntry {
245                        binding: 1,
246                        resource: wgpu::BindingResource::TextureView(&mip_views[(mip - 1)]),
247                    },
248                    wgpu::BindGroupEntry {
249                        binding: 2,
250                        resource: wgpu::BindingResource::Sampler(&ctx.renderer.sampler),
251                    },
252                ],
253                Some("kawase_down"),
254            );
255
256            let mut p = ctx.encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
257                label: Some(&format!("Kawase Down {}", mip)),
258                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
259                    view: &mip_views[mip],
260                    resolve_target: None,
261                    ops: wgpu::Operations {
262                        load: wgpu::LoadOp::Clear(wgpu::Color {
263                            r: 0.0,
264                            g: 0.0,
265                            b: 0.0,
266                            a: 0.0,
267                        }),
268                        store: wgpu::StoreOp::Store,
269                    },
270                    depth_slice: None,
271                })],
272                ..Default::default()
273            });
274            p.set_viewport(0.0, 0.0, w as f32, h as f32, 0.0, 1.0);
275            p.set_pipeline(&ctx.renderer.kawase_down_pipeline);
276            p.set_bind_group(0, Some(&bg), &[]);
277            p.draw(0..3, 0..1);
278        }
279
280        for mip in (1..effective_mips).rev() {
281            let kernel_width = mip_scales[mip].2;
282            let uniform_data: [f32; 8] = [
283                mip_scales[mip].0,
284                mip_scales[mip].1,
285                mip as f32,
286                kernel_width,
287                0.0,
288                0.0,
289                0.0,
290                0.0,
291            ];
292            ctx.queue
293                .write_buffer(kawase_uniform, 0, bytemuck::cast_slice(&uniform_data[..8]));
294
295            let w = mip_scales[(mip - 1)].0.max(1.0) as u32;
296            let h = mip_scales[(mip - 1)].1.max(1.0) as u32;
297
298            // Retrieve or create bind group for this mip level from cache
299            let bg = ctx.get_or_create_bind_group(
300                (RES_BLUR_A, mip as u32, true),
301                &ctx.renderer.kawase_bind_group_layout,
302                &[
303                    wgpu::BindGroupEntry {
304                        binding: 0,
305                        resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
306                            buffer: kawase_uniform,
307                            offset: 0,
308                            size: wgpu::BufferSize::new(32),
309                        }),
310                    },
311                    wgpu::BindGroupEntry {
312                        binding: 1,
313                        resource: wgpu::BindingResource::TextureView(&mip_views[mip]),
314                    },
315                    wgpu::BindGroupEntry {
316                        binding: 2,
317                        resource: wgpu::BindingResource::Sampler(&ctx.renderer.sampler),
318                    },
319                ],
320                Some(&format!("kawase_up_bg_{}", mip)),
321            );
322
323            let mut p = ctx.encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
324                label: Some(&format!("Kawase Up {}", mip)),
325                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
326                    view: &mip_views[(mip - 1)],
327                    resolve_target: None,
328                    ops: wgpu::Operations {
329                        load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
330                        store: wgpu::StoreOp::Store,
331                    },
332                    depth_slice: None,
333                })],
334                ..Default::default()
335            });
336            p.set_viewport(0.0, 0.0, w as f32, h as f32, 0.0, 1.0);
337            p.set_pipeline(&ctx.renderer.kawase_up_pipeline);
338            p.set_bind_group(0, Some(&bg), &[]);
339            p.draw(0..3, 0..1);
340        }
341
342        log::trace!(
343            "[Kvasir] backdrop_blur: Kawase pyramid ({}x{})",
344            blur_width,
345            blur_height
346        );
347    }
348}
349
350pub struct GlassNode {
351    pub inputs: Vec<ResourceId>,
352    pub outputs: Vec<ResourceId>,
353    pub scale: f32,
354}
355
356impl GlassNode {
357    pub fn new(scale: f32) -> Self {
358        Self {
359            inputs: vec![RES_SCENE, RES_BLUR_A],
360            outputs: vec![RES_SCENE],
361            scale,
362        }
363    }
364}
365
366impl KvasirNode for GlassNode {
367    fn label(&self) -> &'static str {
368        "Glass"
369    }
370
371    fn inputs(&self) -> &[ResourceId] {
372        &self.inputs
373    }
374
375    fn outputs(&self) -> &[ResourceId] {
376        &self.outputs
377    }
378
379    fn pass_id(&self) -> PassId {
380        PassId::Glass
381    }
382
383    fn execute(&self, ctx: &mut ExecutionContext) {
384        let rt_w = ctx.renderer.current_width() as i32;
385        let rt_h = ctx.renderer.current_height() as i32;
386
387        let scene_view = match ctx.registry.get_texture_view(RES_SCENE) {
388            Some(v) => v,
389            None => {
390                log::error!("Missing texture view for {}", stringify!(RES_SCENE));
391                return;
392            }
393        };
394        let msaa_view = match ctx
395            .registry
396            .get_texture_view(crate::kvasir::nodes::RES_SCENE_MSAA)
397        {
398            Some(v) => v,
399            None => {
400                log::error!(
401                    "Missing texture view for {}",
402                    stringify!(crate::kvasir::nodes::RES_SCENE_MSAA)
403                );
404                return;
405            }
406        };
407        let blur_view = match ctx.registry.get_texture_view(RES_BLUR_A) {
408            Some(v) => v,
409            None => {
410                log::error!("Missing texture view for {}", stringify!(RES_BLUR_A));
411                return;
412            }
413        };
414
415        let ctx_blur_env_bind_group_a = ctx.blur_env_bind_group_a;
416
417        // Render glass elements directly to the final scene texture view.
418        // Because MSAA is disabled for the glass pipeline to avoid edge shimmering,
419        // we write to the non-MSAA scene view and omit the resolve target.
420        let mut p = ctx.encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
421            label: Some("Surtr P3 Liquid Glass"),
422            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
423                view: &scene_view,
424                resolve_target: None,
425                ops: wgpu::Operations {
426                    load: wgpu::LoadOp::Load,
427                    store: wgpu::StoreOp::Store,
428                },
429                depth_slice: None,
430            })],
431            depth_stencil_attachment: None,
432            ..Default::default()
433        });
434
435        p.set_pipeline(&ctx.renderer.glass_pipeline);
436        p.set_vertex_buffer(0, ctx.renderer.geometry_buffers.vertex_buffer.slice(..));
437        p.set_vertex_buffer(1, ctx.renderer.geometry_buffers.instance_buffer.slice(..));
438        p.set_index_buffer(
439            ctx.renderer.geometry_buffers.index_buffer.slice(..),
440            wgpu::IndexFormat::Uint32,
441        );
442        p.set_bind_group(0, &ctx.renderer.dummy_texture_bind_group, &[]);
443        p.set_bind_group(1, ctx_blur_env_bind_group_a, &[]);
444        p.set_bind_group(2, &ctx.renderer.berserker_bind_group, &[]);
445
446        let scale = self.scale;
447        for call in ctx
448            .renderer
449            .draw_calls
450            .iter()
451            .filter(|c| matches!(c.material, cvkg_core::DrawMaterial::Glass { .. }))
452        {
453            // --- Portal Region Matching ---
454            // Uses rounded integer scissor coordinates as a hash key for O(1) lookup
455            // instead of O(n) linear scan. Falls back to linear scan for edge cases.
456            let mut portal_index = None;
457            if let Some(scissor) = call.scissor_rect {
458                // Round to nearest integer for hash-based matching
459                let key = (
460                    scissor.x.round() as i32,
461                    scissor.y.round() as i32,
462                    scissor.width.round() as i32,
463                    scissor.height.round() as i32,
464                );
465                for (idx, region) in ctx.renderer.portal_regions.iter().enumerate() {
466                    let rkey = (
467                        region.x.round() as i32,
468                        region.y.round() as i32,
469                        region.width.round() as i32,
470                        region.height.round() as i32,
471                    );
472                    if key == rkey {
473                        portal_index = Some(idx);
474                        break;
475                    }
476                }
477            }
478
479            let env_bg = if let Some(portal_idx) = portal_index {
480                let portal_res_id = ResourceId(2000 + portal_idx as u32);
481                if let Some(portal_view) = ctx.registry.get_texture_view(portal_res_id) {
482                    let cache_key = (portal_res_id, 0, false);
483                    let mut cache = GpuRenderer::lock_or_clear_cache(&ctx.renderer.bind_group_cache);
484                    let bg = cache.entry(cache_key).or_insert_with(|| {
485                        ctx.device.create_bind_group(&wgpu::BindGroupDescriptor {
486                            label: Some(&format!("portal_blur_env_bg_{}", portal_idx)),
487                            layout: &ctx.renderer.env_bind_group_layout,
488                            entries: &[
489                                wgpu::BindGroupEntry {
490                                    binding: 0,
491                                    resource: wgpu::BindingResource::TextureView(&portal_view),
492                                },
493                                wgpu::BindGroupEntry {
494                                    binding: 1,
495                                    resource: wgpu::BindingResource::Sampler(&ctx.renderer.sampler),
496                                },
497                            ],
498                        })
499                    });
500                    Some(bg.clone())
501                } else {
502                    None
503                }
504            } else {
505                None
506            };
507
508            let active_env_bg = env_bg.as_ref().unwrap_or(ctx_blur_env_bind_group_a);
509            p.set_bind_group(1, active_env_bg, &[]);
510
511            let bg = if let Some(id) = call.texture_id {
512                if id == 0 {
513                    &ctx.renderer.mega_heim_bind_group
514                } else {
515                    ctx.renderer
516                        .texture_bind_groups
517                        .get(id as usize)
518                        .unwrap_or(&ctx.renderer.dummy_texture_bind_group)
519                }
520            } else {
521                &ctx.renderer.dummy_texture_bind_group
522            };
523
524            p.set_bind_group(0, bg, &[]);
525
526            if let Some(rect) = call.scissor_rect
527                && rt_w > 0
528                && rt_h > 0
529            {
530                let x1 = (rect.x * scale).round() as i32;
531                let y1 = (rect.y * scale).round() as i32;
532                let x2 = ((rect.x + rect.width) * scale).round() as i32;
533                let y2 = ((rect.y + rect.height) * scale).round() as i32;
534                let w = (x2 - x1).clamp(0, rt_w);
535                let h = (y2 - y1).clamp(0, rt_h);
536                if w > 0 && h > 0 {
537                    p.set_scissor_rect(x1 as u32, y1 as u32, w as u32, h as u32);
538                } else {
539                    p.set_scissor_rect(0, 0, 0, 0);
540                }
541            } else {
542                p.set_scissor_rect(0, 0, rt_w as u32, rt_h as u32);
543            }
544            p.draw_indexed(
545                call.index_start..call.index_start + call.index_count,
546                0,
547                call.instance_start..call.instance_start + call.instance_count,
548            );
549        }
550    }
551}