Skip to main content

cvkg_render_gpu/passes/
bloom.rs

1use crate::kvasir::node::{ExecutionContext, KvasirNode};
2use crate::kvasir::nodes::{PassId, RES_BLOOM_A, RES_SCENE};
3use crate::kvasir::resource::ResourceId;
4
5pub struct BloomExtractNode {
6    pub inputs: Vec<ResourceId>,
7    pub outputs: Vec<ResourceId>,
8}
9
10impl BloomExtractNode {
11    pub fn new() -> Self {
12        Self {
13            inputs: vec![RES_SCENE],
14            outputs: vec![RES_BLOOM_A],
15        }
16    }
17}
18
19impl KvasirNode for BloomExtractNode {
20    fn label(&self) -> &'static str {
21        "Bloom Extract"
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::BloomExtract
34    }
35
36    fn execute(&self, ctx: &mut ExecutionContext) {
37        let bloom_texture = match ctx.registry.get_texture(RES_BLOOM_A) {
38            Some(v) => v,
39            None => {
40                log::error!("Missing texture for {}", stringify!(RES_BLOOM_A));
41                return;
42            }
43        };
44        // Create a single-mip view for the render pass (mip 0 only)
45        let bloom_view = bloom_texture.create_view(&wgpu::TextureViewDescriptor {
46            label: Some("bloom_extract_mip0"),
47            base_mip_level: 0,
48            mip_level_count: Some(1),
49            ..Default::default()
50        });
51
52        // Get scene view and create cached bind group BEFORE render pass (avoids borrow conflict)
53        let scene_view = match ctx.registry.get_texture_view(RES_SCENE) {
54            Some(v) => v,
55            None => {
56                log::error!("Missing texture view for {}", stringify!(RES_SCENE));
57                return;
58            }
59        };
60        let bg = ctx.get_or_create_bind_group(
61            (RES_SCENE, 0, false),
62            &ctx.renderer.texture_bind_group_layout,
63            &[
64                wgpu::BindGroupEntry {
65                    binding: 0,
66                    resource: wgpu::BindingResource::TextureViewArray(&vec![&scene_view; 32]),
67                },
68                wgpu::BindGroupEntry {
69                    binding: 1,
70                    resource: wgpu::BindingResource::Sampler(&ctx.renderer.dummy_sampler),
71                },
72            ],
73            Some("bloom_extract_scene_bg"),
74        );
75
76        let mut p = ctx.encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
77            label: Some("Surtr Bloom Extract"),
78            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
79                view: &bloom_view,
80                resolve_target: None,
81                ops: wgpu::Operations {
82                    load: wgpu::LoadOp::Clear(wgpu::Color {
83                        r: 0.0,
84                        g: 0.0,
85                        b: 0.0,
86                        a: 0.0,
87                    }),
88                    store: wgpu::StoreOp::Store,
89                },
90                depth_slice: None,
91            })],
92            ..Default::default()
93        });
94
95        p.set_pipeline(&ctx.renderer.bloom_extract_pipeline);
96        p.set_bind_group(0, &bg, &[]);
97        p.set_bind_group(1, &ctx.renderer.dummy_env_bind_group, &[]);
98        p.set_bind_group(2, &ctx.renderer.berserker_bind_group, &[]);
99        p.draw(0..3, 0..1);
100    }
101}
102
103pub struct BloomBlurNode {
104    pub inputs: Vec<ResourceId>,
105    pub outputs: Vec<ResourceId>,
106    pub width: u32,
107    pub height: u32,
108}
109
110impl BloomBlurNode {
111    pub fn new(width: u32, height: u32) -> Self {
112        Self {
113            inputs: vec![RES_BLOOM_A],
114            outputs: vec![RES_BLOOM_A],
115            width,
116            height,
117        }
118    }
119}
120
121impl KvasirNode for BloomBlurNode {
122    fn label(&self) -> &'static str {
123        "Bloom Blur"
124    }
125
126    fn inputs(&self) -> &[ResourceId] {
127        &self.inputs
128    }
129
130    fn outputs(&self) -> &[ResourceId] {
131        &self.outputs
132    }
133
134    fn pass_id(&self) -> PassId {
135        PassId::BloomBlur
136    }
137
138    fn execute(&self, ctx: &mut ExecutionContext) {
139        let bloom_tex = match ctx.registry.get_texture(RES_BLOOM_A) {
140            Some(v) => v,
141            None => {
142                log::error!("Missing texture for {}", stringify!(RES_BLOOM_A));
143                return;
144            }
145        };
146
147        // Derive mip count from the actual texture, not hardcoded
148        let num_mips = bloom_tex.mip_level_count();
149        if num_mips < 2 {
150            return;
151        }
152
153        // Reuse persistent uniform buffer (avoids per-frame GPU allocation)
154        let kawase_uniform = &ctx.renderer.kawase_uniform;
155
156        // Create per-mip views based on actual mip count
157        let effective_mips = (num_mips as usize).min(5);
158        let mip_views: Vec<wgpu::TextureView> = (0..effective_mips)
159            .map(|mip| {
160                bloom_tex.create_view(&wgpu::TextureViewDescriptor {
161                    label: Some(&format!("bloom_mip_{}", mip)),
162                    base_mip_level: mip as u32,
163                    mip_level_count: Some(1),
164                    ..Default::default()
165                })
166            })
167            .collect();
168
169        let bloom_width = self.width;
170        let bloom_height = self.height;
171
172        // Compute mip scales dynamically
173        let mut mip_scales = Vec::with_capacity(effective_mips);
174        for i in 0..effective_mips {
175            let div = (1u32 << i) as f32;
176            mip_scales.push((
177                bloom_width as f32 / div,
178                bloom_height as f32 / div,
179                (i + 1) as f32,
180            ));
181        }
182
183        // Downsample chain
184        for mip in 1..effective_mips {
185            let kernel_width = mip_scales[mip].2;
186            let uniform_data: [f32; 8] = [
187                mip_scales[(mip - 1)].0,
188                mip_scales[(mip - 1)].1,
189                (mip - 1) as f32,
190                kernel_width,
191                0.0,
192                0.0,
193                0.0,
194                0.0,
195            ];
196            ctx.queue
197                .write_buffer(kawase_uniform, 0, bytemuck::cast_slice(&uniform_data));
198
199            let w = mip_scales[mip].0.max(1.0) as u32;
200            let h = mip_scales[mip].1.max(1.0) as u32;
201
202            // Cache bind group per mip level (texture views + sampler are frame-stable)
203            let bg = ctx.get_or_create_bind_group(
204                (RES_BLOOM_A, mip as u32, false),
205                &ctx.renderer.kawase_bind_group_layout,
206                &[
207                    wgpu::BindGroupEntry {
208                        binding: 0,
209                        resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
210                            buffer: kawase_uniform,
211                            offset: 0,
212                            size: wgpu::BufferSize::new(32),
213                        }),
214                    },
215                    wgpu::BindGroupEntry {
216                        binding: 1,
217                        resource: wgpu::BindingResource::TextureView(&mip_views[(mip - 1)]),
218                    },
219                    wgpu::BindGroupEntry {
220                        binding: 2,
221                        resource: wgpu::BindingResource::Sampler(&ctx.renderer.sampler),
222                    },
223                ],
224                Some(&format!("kawase_bloom_bg_{}", mip)),
225            );
226
227            let mut p = ctx.encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
228                label: Some(&format!("Kawase Bloom Down {}", mip)),
229                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
230                    view: &mip_views[mip],
231                    resolve_target: None,
232                    ops: wgpu::Operations {
233                        load: wgpu::LoadOp::Clear(wgpu::Color {
234                            r: 0.0,
235                            g: 0.0,
236                            b: 0.0,
237                            a: 0.0,
238                        }),
239                        store: wgpu::StoreOp::Store,
240                    },
241                    depth_slice: None,
242                })],
243                ..Default::default()
244            });
245            p.set_viewport(0.0, 0.0, w as f32, h as f32, 0.0, 1.0);
246            p.set_pipeline(&ctx.renderer.kawase_down_pipeline);
247            p.set_bind_group(0, &bg, &[]);
248            p.draw(0..3, 0..1);
249        }
250
251        // Upsample chain
252        for mip in (1..effective_mips).rev() {
253            let kernel_width = mip_scales[mip].2;
254            let uniform_data: [f32; 8] = [
255                mip_scales[mip].0,
256                mip_scales[mip].1,
257                mip as f32,
258                kernel_width,
259                0.0,
260                0.0,
261                0.0,
262                0.0,
263            ];
264            ctx.queue
265                .write_buffer(kawase_uniform, 0, bytemuck::cast_slice(&uniform_data));
266
267            let w = mip_scales[(mip - 1)].0.max(1.0) as u32;
268            let h = mip_scales[(mip - 1)].1.max(1.0) as u32;
269
270            // Cache bind group per mip level (upsample)
271            let bg = ctx.get_or_create_bind_group(
272                (RES_BLOOM_A, (mip + 100) as u32, false), // offset key to avoid collision with downsample
273                &ctx.renderer.kawase_bind_group_layout,
274                &[
275                    wgpu::BindGroupEntry {
276                        binding: 0,
277                        resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
278                            buffer: kawase_uniform,
279                            offset: 0,
280                            size: wgpu::BufferSize::new(32),
281                        }),
282                    },
283                    wgpu::BindGroupEntry {
284                        binding: 1,
285                        resource: wgpu::BindingResource::TextureView(&mip_views[mip]),
286                    },
287                    wgpu::BindGroupEntry {
288                        binding: 2,
289                        resource: wgpu::BindingResource::Sampler(&ctx.renderer.sampler),
290                    },
291                ],
292                Some(&format!("kawase_bloom_up_{}", mip)),
293            );
294
295            // Clear the target mip level on load to prevent additive brightening from previous frames/passes
296            let mut p = ctx.encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
297                label: Some(&format!("Kawase Bloom Up {}", mip)),
298                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
299                    view: &mip_views[(mip - 1)],
300                    resolve_target: None,
301                    ops: wgpu::Operations {
302                        load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
303                        store: wgpu::StoreOp::Store,
304                    },
305                    depth_slice: None,
306                })],
307                ..Default::default()
308            });
309            p.set_viewport(0.0, 0.0, w as f32, h as f32, 0.0, 1.0);
310            p.set_pipeline(&ctx.renderer.kawase_up_pipeline);
311            p.set_bind_group(0, &bg, &[]);
312            p.draw(0..3, 0..1);
313        }
314
315        log::trace!(
316            "[Kvasir] bloom_blur: Kawase pyramid ({}x{})",
317            bloom_width,
318            bloom_height
319        );
320    }
321}