Skip to main content

cvkg_render_gpu/passes/
backdrop_region.rs

1//! Per-element isolated backdrop blur pass.
2//! Copies a scissored region from the scene texture into a
3//! blur target the glass pass can sample, then runs a Kawase
4//! downsample chain on the copied region.
5
6use crate::kvasir::node::{ExecutionContext, KvasirNode};
7use crate::kvasir::nodes::PassId;
8use crate::kvasir::resource::ResourceId;
9use crate::renderer::GpuRenderer;
10
11/// Copies a rectangular region from the scene texture into a
12/// blur target resource, then runs a Kawase downsample chain.
13pub struct BackdropRegionNode {
14    pub inputs: Vec<ResourceId>,
15    pub outputs: Vec<ResourceId>,
16    /// Region in logical pixels.
17    pub region: cvkg_core::Rect,
18    /// Output resource ID (allocated by the graph builder).
19    pub output_id: ResourceId,
20}
21
22impl BackdropRegionNode {
23    pub fn new(region: cvkg_core::Rect, output_id: ResourceId) -> Self {
24        Self {
25            inputs: vec![crate::kvasir::nodes::RES_SCENE],
26            outputs: vec![output_id],
27            region,
28            output_id,
29        }
30    }
31}
32
33impl KvasirNode for BackdropRegionNode {
34    fn label(&self) -> &'static str {
35        "Backdrop Region"
36    }
37    fn inputs(&self) -> &[ResourceId] {
38        &self.inputs
39    }
40    fn outputs(&self) -> &[ResourceId] {
41        &self.outputs
42    }
43    fn pass_id(&self) -> PassId {
44        PassId::BackdropRegion
45    }
46    fn execute(&self, ctx: &mut ExecutionContext) {
47        let scene_tex = match ctx.registry.get_texture(crate::kvasir::nodes::RES_SCENE) {
48            Some(v) => v,
49            None => {
50                log::error!("[BackdropRegion] Missing scene texture");
51                return;
52            }
53        };
54        let blur_tex = match ctx.registry.get_texture(self.output_id) {
55            Some(v) => v,
56            None => {
57                log::error!("[BackdropRegion] Missing blur target texture");
58                return;
59            }
60        };
61
62        let scale = ctx.scale_factor;
63        let rx = (self.region.x * scale) as u32;
64        let ry = (self.region.y * scale) as u32;
65        let rw = (self.region.width * scale) as u32;
66        let rh = (self.region.height * scale) as u32;
67
68        // Phase 1: GPU copy of the scissored region from scene to blur target.
69        // Uses copy_texture_to_texture for an actual pixel-exact copy (no shader needed).
70        let _src_extent = wgpu::Extent3d {
71            width: scene_tex.width(),
72            height: scene_tex.height(),
73            depth_or_array_layers: 1,
74        };
75        let dst_extent = wgpu::Extent3d {
76            width: rw,
77            height: rh,
78            depth_or_array_layers: 1,
79        };
80        ctx.encoder.copy_texture_to_texture(
81            wgpu::TexelCopyTextureInfo {
82                texture: &scene_tex,
83                mip_level: 0,
84                origin: wgpu::Origin3d { x: rx, y: ry, z: 0 },
85                aspect: wgpu::TextureAspect::All,
86            },
87            wgpu::TexelCopyTextureInfo {
88                texture: &blur_tex,
89                mip_level: 0,
90                origin: wgpu::Origin3d::ZERO,
91                aspect: wgpu::TextureAspect::All,
92            },
93            dst_extent,
94        );
95
96        // Phase 2: Generate mips for the blurred backdrop region.
97        // This lets the glass shader sample at different blur levels.
98        // We do a simple blur via a Kawase-style approach on the copied region.
99        let mip_count = blur_tex.mip_level_count().min(4);
100        if mip_count >= 2 {
101            // Reuse persistent uniform buffer (avoids per-frame GPU allocation)
102            let kawase_uniform = &ctx.renderer.kawase_uniform;
103
104            for mip in 1..mip_count {
105                let src_view = {
106                    let mut cache =
107                        GpuRenderer::lock_or_clear_cache(&ctx.renderer.texture_view_cache);
108                    cache
109                        .entry((ctx.renderer.current_window, self.output_id, (mip - 1)))
110                        .or_insert_with(|| {
111                            blur_tex.create_view(&wgpu::TextureViewDescriptor {
112                                label: Some(&format!("blur_region_src_mip_{}", mip - 1)),
113                                base_mip_level: mip - 1,
114                                mip_level_count: Some(1),
115                                ..Default::default()
116                            })
117                        })
118                        .clone()
119                };
120                let dst_view = {
121                    let mut cache =
122                        GpuRenderer::lock_or_clear_cache(&ctx.renderer.texture_view_cache);
123                    cache
124                        .entry((ctx.renderer.current_window, self.output_id, mip))
125                        .or_insert_with(|| {
126                            blur_tex.create_view(&wgpu::TextureViewDescriptor {
127                                label: Some(&format!("blur_region_dst_mip_{}", mip)),
128                                base_mip_level: mip,
129                                mip_level_count: Some(1),
130                                ..Default::default()
131                            })
132                        })
133                        .clone()
134                };
135
136                let w = (rw >> mip).max(1);
137                let h = (rh >> mip).max(1);
138                let kernel = mip as f32;
139
140                let uniform_data: [f32; 8] = [
141                    w as f32,
142                    h as f32,
143                    (mip - 1) as f32,
144                    kernel,
145                    0.0,
146                    0.0,
147                    0.0,
148                    0.0,
149                ];
150                ctx.queue
151                    .write_buffer(kawase_uniform, 0, bytemuck::cast_slice(&uniform_data));
152
153                let src_bg = ctx.get_or_create_bind_group(
154                    (self.output_id, mip, false),
155                    &ctx.renderer.kawase_bind_group_layout,
156                    &[
157                        wgpu::BindGroupEntry {
158                            binding: 0,
159                            resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
160                                buffer: kawase_uniform,
161                                offset: 0,
162                                size: wgpu::BufferSize::new(32),
163                            }),
164                        },
165                        wgpu::BindGroupEntry {
166                            binding: 1,
167                            resource: wgpu::BindingResource::TextureView(&src_view),
168                        },
169                        wgpu::BindGroupEntry {
170                            binding: 2,
171                            resource: wgpu::BindingResource::Sampler(&ctx.renderer.sampler),
172                        },
173                    ],
174                    Some(&format!("blur_region_kawase_bg_{}", mip)),
175                );
176
177                let mut pass = ctx.encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
178                    label: Some(&format!("Backdrop Region Blur {}", mip)),
179                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
180                        view: &dst_view,
181                        resolve_target: None,
182                        ops: wgpu::Operations {
183                            load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
184                            store: wgpu::StoreOp::Store,
185                        },
186                        depth_slice: None,
187                    })],
188                    ..Default::default()
189                });
190                pass.set_viewport(0.0, 0.0, w as f32, h as f32, 0.0, 1.0);
191                pass.set_pipeline(&ctx.renderer.kawase_down_pipeline);
192                pass.set_bind_group(0, &src_bg, &[]);
193                pass.draw(0..3, 0..1);
194            }
195
196            // Upsample chain
197            for mip in (1..mip_count).rev() {
198                let src_view = {
199                    let mut cache =
200                        GpuRenderer::lock_or_clear_cache(&ctx.renderer.texture_view_cache);
201                    cache
202                        .entry((ctx.renderer.current_window, self.output_id, mip))
203                        .or_insert_with(|| {
204                            blur_tex.create_view(&wgpu::TextureViewDescriptor {
205                                label: Some(&format!("blur_region_src_mip_{}", mip)),
206                                base_mip_level: mip,
207                                mip_level_count: Some(1),
208                                ..Default::default()
209                            })
210                        })
211                        .clone()
212                };
213                let dst_view = {
214                    let mut cache =
215                        GpuRenderer::lock_or_clear_cache(&ctx.renderer.texture_view_cache);
216                    cache
217                        .entry((ctx.renderer.current_window, self.output_id, (mip - 1)))
218                        .or_insert_with(|| {
219                            blur_tex.create_view(&wgpu::TextureViewDescriptor {
220                                label: Some(&format!("blur_region_dst_mip_{}", mip - 1)),
221                                base_mip_level: mip - 1,
222                                mip_level_count: Some(1),
223                                ..Default::default()
224                            })
225                        })
226                        .clone()
227                };
228
229                let w = (rw >> (mip - 1)).max(1);
230                let h = (rh >> (mip - 1)).max(1);
231                let kernel = mip as f32;
232
233                let uniform_data: [f32; 8] =
234                    [w as f32, h as f32, mip as f32, kernel, 0.0, 0.0, 0.0, 0.0];
235                ctx.queue
236                    .write_buffer(kawase_uniform, 0, bytemuck::cast_slice(&uniform_data));
237
238                let src_bg = ctx.get_or_create_bind_group(
239                    (self.output_id, mip, true),
240                    &ctx.renderer.kawase_bind_group_layout,
241                    &[
242                        wgpu::BindGroupEntry {
243                            binding: 0,
244                            resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
245                                buffer: kawase_uniform,
246                                offset: 0,
247                                size: wgpu::BufferSize::new(32),
248                            }),
249                        },
250                        wgpu::BindGroupEntry {
251                            binding: 1,
252                            resource: wgpu::BindingResource::TextureView(&src_view),
253                        },
254                        wgpu::BindGroupEntry {
255                            binding: 2,
256                            resource: wgpu::BindingResource::Sampler(&ctx.renderer.sampler),
257                        },
258                    ],
259                    Some(&format!("blur_region_kawase_up_bg_{}", mip)),
260                );
261
262                // Clear the destination view on load to prevent additive compounding of light during the upsample chain
263                let mut pass = ctx.encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
264                    label: Some(&format!("Backdrop Region Blur Up {}", mip)),
265                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
266                        view: &dst_view,
267                        resolve_target: None,
268                        ops: wgpu::Operations {
269                            load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
270                            store: wgpu::StoreOp::Store,
271                        },
272                        depth_slice: None,
273                    })],
274                    ..Default::default()
275                });
276                pass.set_viewport(0.0, 0.0, w as f32, h as f32, 0.0, 1.0);
277                pass.set_pipeline(&ctx.renderer.kawase_up_pipeline);
278                pass.set_bind_group(0, &src_bg, &[]);
279                pass.draw(0..3, 0..1);
280            }
281        }
282    }
283}