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