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 = GpuRenderer::lock_or_clear_cache(&ctx.renderer.texture_view_cache);
107                    cache
108                        .entry((self.output_id, (mip - 1)))
109                        .or_insert_with(|| {
110                            blur_tex.create_view(&wgpu::TextureViewDescriptor {
111                                label: Some(&format!("blur_region_src_mip_{}", mip - 1)),
112                                base_mip_level: mip - 1,
113                                mip_level_count: Some(1),
114                                ..Default::default()
115                            })
116                        })
117                        .clone()
118                };
119                let dst_view = {
120                    let mut cache = GpuRenderer::lock_or_clear_cache(&ctx.renderer.texture_view_cache);
121                    cache
122                        .entry((self.output_id, mip))
123                        .or_insert_with(|| {
124                            blur_tex.create_view(&wgpu::TextureViewDescriptor {
125                                label: Some(&format!("blur_region_dst_mip_{}", mip)),
126                                base_mip_level: mip,
127                                mip_level_count: Some(1),
128                                ..Default::default()
129                            })
130                        })
131                        .clone()
132                };
133
134                let w = (rw >> mip).max(1);
135                let h = (rh >> mip).max(1);
136                let kernel = mip as f32;
137
138                let uniform_data: [f32; 8] = [
139                    w as f32,
140                    h as f32,
141                    (mip - 1) as f32,
142                    kernel,
143                    0.0,
144                    0.0,
145                    0.0,
146                    0.0,
147                ];
148                ctx.queue
149                    .write_buffer(kawase_uniform, 0, bytemuck::cast_slice(&uniform_data));
150
151                let src_bg = ctx.get_or_create_bind_group(
152                    (self.output_id, mip, false),
153                    &ctx.renderer.kawase_bind_group_layout,
154                    &[
155                        wgpu::BindGroupEntry {
156                            binding: 0,
157                            resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
158                                buffer: kawase_uniform,
159                                offset: 0,
160                                size: wgpu::BufferSize::new(32),
161                            }),
162                        },
163                        wgpu::BindGroupEntry {
164                            binding: 1,
165                            resource: wgpu::BindingResource::TextureView(&src_view),
166                        },
167                        wgpu::BindGroupEntry {
168                            binding: 2,
169                            resource: wgpu::BindingResource::Sampler(&ctx.renderer.sampler),
170                        },
171                    ],
172                    Some(&format!("blur_region_kawase_bg_{}", mip)),
173                );
174
175                let mut pass = ctx.encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
176                    label: Some(&format!("Backdrop Region Blur {}", mip)),
177                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
178                        view: &dst_view,
179                        resolve_target: None,
180                        ops: wgpu::Operations {
181                            load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
182                            store: wgpu::StoreOp::Store,
183                        },
184                        depth_slice: None,
185                    })],
186                    ..Default::default()
187                });
188                pass.set_viewport(0.0, 0.0, w as f32, h as f32, 0.0, 1.0);
189                pass.set_pipeline(&ctx.renderer.kawase_down_pipeline);
190                pass.set_bind_group(0, &src_bg, &[]);
191                pass.draw(0..3, 0..1);
192            }
193
194            // Upsample chain
195            for mip in (1..mip_count).rev() {
196                let src_view = {
197                    let mut cache = GpuRenderer::lock_or_clear_cache(&ctx.renderer.texture_view_cache);
198                    cache
199                        .entry((self.output_id, mip))
200                        .or_insert_with(|| {
201                            blur_tex.create_view(&wgpu::TextureViewDescriptor {
202                                label: Some(&format!("blur_region_src_mip_{}", mip)),
203                                base_mip_level: mip,
204                                mip_level_count: Some(1),
205                                ..Default::default()
206                            })
207                        })
208                        .clone()
209                };
210                let dst_view = {
211                    let mut cache = GpuRenderer::lock_or_clear_cache(&ctx.renderer.texture_view_cache);
212                    cache
213                        .entry((self.output_id, (mip - 1)))
214                        .or_insert_with(|| {
215                            blur_tex.create_view(&wgpu::TextureViewDescriptor {
216                                label: Some(&format!("blur_region_dst_mip_{}", mip - 1)),
217                                base_mip_level: mip - 1,
218                                mip_level_count: Some(1),
219                                ..Default::default()
220                            })
221                        })
222                        .clone()
223                };
224
225                let w = (rw >> (mip - 1)).max(1);
226                let h = (rh >> (mip - 1)).max(1);
227                let kernel = mip as f32;
228
229                let uniform_data: [f32; 8] =
230                    [w as f32, h as f32, mip as f32, kernel, 0.0, 0.0, 0.0, 0.0];
231                ctx.queue
232                    .write_buffer(kawase_uniform, 0, bytemuck::cast_slice(&uniform_data));
233
234                let src_bg = ctx.get_or_create_bind_group(
235                    (self.output_id, mip, true),
236                    &ctx.renderer.kawase_bind_group_layout,
237                    &[
238                        wgpu::BindGroupEntry {
239                            binding: 0,
240                            resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
241                                buffer: kawase_uniform,
242                                offset: 0,
243                                size: wgpu::BufferSize::new(32),
244                            }),
245                        },
246                        wgpu::BindGroupEntry {
247                            binding: 1,
248                            resource: wgpu::BindingResource::TextureView(&src_view),
249                        },
250                        wgpu::BindGroupEntry {
251                            binding: 2,
252                            resource: wgpu::BindingResource::Sampler(&ctx.renderer.sampler),
253                        },
254                    ],
255                    Some(&format!("blur_region_kawase_up_bg_{}", mip)),
256                );
257
258                // Clear the destination view on load to prevent additive compounding of light during the upsample chain
259                let mut pass = ctx.encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
260                    label: Some(&format!("Backdrop Region Blur Up {}", mip)),
261                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
262                        view: &dst_view,
263                        resolve_target: None,
264                        ops: wgpu::Operations {
265                            load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
266                            store: wgpu::StoreOp::Store,
267                        },
268                        depth_slice: None,
269                    })],
270                    ..Default::default()
271                });
272                pass.set_viewport(0.0, 0.0, w as f32, h as f32, 0.0, 1.0);
273                pass.set_pipeline(&ctx.renderer.kawase_up_pipeline);
274                pass.set_bind_group(0, &src_bg, &[]);
275                pass.draw(0..3, 0..1);
276            }
277        }
278    }
279}