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