Skip to main content

renderling/bloom/
cpu.rs

1//! Bloom.
2use core::ops::Deref;
3use std::sync::{Arc, RwLock};
4
5use craballoc::{
6    prelude::{Hybrid, HybridArray, SlabAllocator},
7    runtime::WgpuRuntime,
8    slab::SlabBuffer,
9};
10use crabslab::Id;
11use glam::{UVec2, Vec2};
12
13use crate::texture::{self, Texture};
14
15fn create_bindgroup_layout(device: &wgpu::Device, label: Option<&str>) -> wgpu::BindGroupLayout {
16    device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
17        label,
18        entries: &[
19            wgpu::BindGroupLayoutEntry {
20                binding: 0,
21                visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
22                ty: wgpu::BindingType::Buffer {
23                    ty: wgpu::BufferBindingType::Storage { read_only: true },
24                    has_dynamic_offset: false,
25                    min_binding_size: None,
26                },
27                count: None,
28            },
29            wgpu::BindGroupLayoutEntry {
30                binding: 1,
31                visibility: wgpu::ShaderStages::FRAGMENT,
32                ty: wgpu::BindingType::Texture {
33                    sample_type: wgpu::TextureSampleType::Float { filterable: true },
34                    view_dimension: wgpu::TextureViewDimension::D2,
35                    multisampled: false,
36                },
37                count: None,
38            },
39            wgpu::BindGroupLayoutEntry {
40                binding: 2,
41                visibility: wgpu::ShaderStages::FRAGMENT,
42                ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
43                count: None,
44            },
45        ],
46    })
47}
48
49fn create_bloom_downsample_pipeline(device: &wgpu::Device) -> wgpu::RenderPipeline {
50    let label = Some("bloom downsample");
51    let bindgroup_layout = create_bindgroup_layout(device, label);
52    let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
53        label,
54        bind_group_layouts: &[&bindgroup_layout],
55        push_constant_ranges: &[],
56    });
57    let vertex_module = crate::linkage::bloom_vertex::linkage(device);
58    let fragment_module = crate::linkage::bloom_downsample_fragment::linkage(device);
59    device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
60        label,
61        layout: Some(&layout),
62        vertex: wgpu::VertexState {
63            module: &vertex_module.module,
64            entry_point: Some(vertex_module.entry_point),
65            buffers: &[],
66            compilation_options: Default::default(),
67        },
68        primitive: wgpu::PrimitiveState {
69            topology: wgpu::PrimitiveTopology::TriangleList,
70            strip_index_format: None,
71            front_face: wgpu::FrontFace::Ccw,
72            cull_mode: None,
73            unclipped_depth: false,
74            polygon_mode: wgpu::PolygonMode::Fill,
75            conservative: false,
76        },
77        depth_stencil: None,
78        multisample: wgpu::MultisampleState::default(),
79        fragment: Some(wgpu::FragmentState {
80            module: &fragment_module.module,
81            entry_point: Some(fragment_module.entry_point),
82            targets: &[Some(wgpu::ColorTargetState {
83                format: wgpu::TextureFormat::Rgba16Float,
84                blend: None,
85                write_mask: wgpu::ColorWrites::all(),
86            })],
87            compilation_options: Default::default(),
88        }),
89        multiview: None,
90        cache: None,
91    })
92}
93
94fn create_bloom_upsample_pipeline(device: &wgpu::Device) -> wgpu::RenderPipeline {
95    let label = Some("bloom upsample");
96    let bindgroup_layout = create_bindgroup_layout(device, label);
97    let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
98        label,
99        bind_group_layouts: &[&bindgroup_layout],
100        push_constant_ranges: &[],
101    });
102    let vertex_module = crate::linkage::bloom_vertex::linkage(device);
103    let fragment_module = crate::linkage::bloom_upsample_fragment::linkage(device);
104    device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
105        label,
106        layout: Some(&layout),
107        vertex: wgpu::VertexState {
108            module: &vertex_module.module,
109            entry_point: Some(vertex_module.entry_point),
110            buffers: &[],
111            compilation_options: Default::default(),
112        },
113        primitive: wgpu::PrimitiveState {
114            topology: wgpu::PrimitiveTopology::TriangleList,
115            strip_index_format: None,
116            front_face: wgpu::FrontFace::Ccw,
117            cull_mode: None,
118            unclipped_depth: false,
119            polygon_mode: wgpu::PolygonMode::Fill,
120            conservative: false,
121        },
122        depth_stencil: None,
123        multisample: wgpu::MultisampleState::default(),
124        fragment: Some(wgpu::FragmentState {
125            module: &fragment_module.module,
126            entry_point: Some(fragment_module.entry_point),
127            targets: &[Some(wgpu::ColorTargetState {
128                format: wgpu::TextureFormat::Rgba16Float,
129                blend: Some(wgpu::BlendState::ALPHA_BLENDING),
130                write_mask: wgpu::ColorWrites::all(),
131            })],
132            compilation_options: Default::default(),
133        }),
134        multiview: None,
135        cache: None,
136    })
137}
138
139fn config_resolutions(resolution: UVec2) -> impl Iterator<Item = UVec2> {
140    let num_textures = resolution.x.min(resolution.y).ilog2();
141    (0..=num_textures).map(move |i| UVec2::new(resolution.x >> i, resolution.y >> i))
142}
143
144fn create_texture(
145    runtime: impl AsRef<WgpuRuntime>,
146    width: u32,
147    height: u32,
148    label: Option<&str>,
149    extra_usages: wgpu::TextureUsages,
150) -> texture::Texture {
151    let sampler = runtime
152        .as_ref()
153        .device
154        .create_sampler(&wgpu::SamplerDescriptor {
155            label,
156            address_mode_u: wgpu::AddressMode::ClampToEdge,
157            address_mode_v: wgpu::AddressMode::ClampToEdge,
158            address_mode_w: wgpu::AddressMode::ClampToEdge,
159            mag_filter: wgpu::FilterMode::Linear,
160            min_filter: wgpu::FilterMode::Linear,
161            mipmap_filter: wgpu::FilterMode::Linear,
162            ..Default::default()
163        });
164    Texture::new_with(
165        runtime,
166        label,
167        Some(
168            wgpu::TextureUsages::RENDER_ATTACHMENT
169                | wgpu::TextureUsages::TEXTURE_BINDING
170                | wgpu::TextureUsages::COPY_SRC
171                | extra_usages,
172        ),
173        Some(sampler),
174        wgpu::TextureFormat::Rgba16Float,
175        4,
176        2,
177        width,
178        height,
179        1,
180        &[],
181    )
182}
183
184fn create_textures(runtime: impl AsRef<WgpuRuntime>, resolution: UVec2) -> Vec<texture::Texture> {
185    let resolutions = config_resolutions(resolution).collect::<Vec<_>>();
186    log::trace!(
187        "creating {} bloom textures at resolution {resolution}",
188        resolutions.len()
189    );
190    let mut textures = vec![];
191    for (
192        i,
193        UVec2 {
194            x: width,
195            y: height,
196        },
197    ) in resolutions.into_iter().skip(1).enumerate()
198    {
199        let title = format!("bloom texture[{i}]");
200        let label = Some(title.as_str());
201        let texture = create_texture(
202            runtime.as_ref(),
203            width,
204            height,
205            label,
206            wgpu::TextureUsages::empty(),
207        );
208        textures.push(texture);
209    }
210    textures
211}
212
213fn create_bindgroup(
214    device: &wgpu::Device,
215    layout: &wgpu::BindGroupLayout,
216    buffer: &wgpu::Buffer,
217    tex: &Texture,
218) -> wgpu::BindGroup {
219    let label = Some("bloom");
220    device.create_bind_group(&wgpu::BindGroupDescriptor {
221        label,
222        layout,
223        entries: &[
224            wgpu::BindGroupEntry {
225                binding: 0,
226                resource: wgpu::BindingResource::Buffer(buffer.as_entire_buffer_binding()),
227            },
228            wgpu::BindGroupEntry {
229                binding: 1,
230                resource: wgpu::BindingResource::TextureView(&tex.view),
231            },
232            wgpu::BindGroupEntry {
233                binding: 2,
234                resource: wgpu::BindingResource::Sampler(&tex.sampler),
235            },
236        ],
237    })
238}
239
240fn create_bindgroups(
241    device: &wgpu::Device,
242    pipeline: &wgpu::RenderPipeline,
243    buffer: &wgpu::Buffer,
244    textures: &[Texture],
245) -> Vec<wgpu::BindGroup> {
246    let layout = pipeline.get_bind_group_layout(0);
247    textures
248        .iter()
249        .map(|tex| create_bindgroup(device, &layout, buffer, tex))
250        .collect()
251}
252
253fn create_mix_pipeline(device: &wgpu::Device) -> wgpu::RenderPipeline {
254    let label = Some("bloom mix");
255    let bindgroup_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
256        label,
257        entries: &[
258            wgpu::BindGroupLayoutEntry {
259                binding: 0,
260                visibility: wgpu::ShaderStages::FRAGMENT,
261                ty: wgpu::BindingType::Buffer {
262                    ty: wgpu::BufferBindingType::Storage { read_only: true },
263                    has_dynamic_offset: false,
264                    min_binding_size: None,
265                },
266                count: None,
267            },
268            wgpu::BindGroupLayoutEntry {
269                binding: 1,
270                visibility: wgpu::ShaderStages::FRAGMENT,
271                ty: wgpu::BindingType::Texture {
272                    sample_type: wgpu::TextureSampleType::Float { filterable: true },
273                    view_dimension: wgpu::TextureViewDimension::D2,
274                    multisampled: false,
275                },
276                count: None,
277            },
278            wgpu::BindGroupLayoutEntry {
279                binding: 2,
280                visibility: wgpu::ShaderStages::FRAGMENT,
281                ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
282                count: None,
283            },
284            wgpu::BindGroupLayoutEntry {
285                binding: 3,
286                visibility: wgpu::ShaderStages::FRAGMENT,
287                ty: wgpu::BindingType::Texture {
288                    sample_type: wgpu::TextureSampleType::Float { filterable: true },
289                    view_dimension: wgpu::TextureViewDimension::D2,
290                    multisampled: false,
291                },
292                count: None,
293            },
294            wgpu::BindGroupLayoutEntry {
295                binding: 4,
296                visibility: wgpu::ShaderStages::FRAGMENT,
297                ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
298                count: None,
299            },
300        ],
301    });
302    let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
303        label,
304        bind_group_layouts: &[&bindgroup_layout],
305        push_constant_ranges: &[],
306    });
307    let vertex_module = crate::linkage::bloom_vertex::linkage(device);
308    let fragment_module = crate::linkage::bloom_mix_fragment::linkage(device);
309    device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
310        label,
311        layout: Some(&layout),
312        vertex: wgpu::VertexState {
313            module: &vertex_module.module,
314            entry_point: Some(vertex_module.entry_point),
315            buffers: &[],
316            compilation_options: Default::default(),
317        },
318        primitive: wgpu::PrimitiveState {
319            topology: wgpu::PrimitiveTopology::TriangleList,
320            strip_index_format: None,
321            front_face: wgpu::FrontFace::Ccw,
322            cull_mode: None,
323            unclipped_depth: false,
324            polygon_mode: wgpu::PolygonMode::Fill,
325            conservative: false,
326        },
327        depth_stencil: None,
328        multisample: wgpu::MultisampleState::default(),
329        fragment: Some(wgpu::FragmentState {
330            module: &fragment_module.module,
331            entry_point: Some(fragment_module.entry_point),
332            targets: &[Some(wgpu::ColorTargetState {
333                format: wgpu::TextureFormat::Rgba16Float,
334                blend: Some(wgpu::BlendState::ALPHA_BLENDING),
335                write_mask: wgpu::ColorWrites::all(),
336            })],
337            compilation_options: Default::default(),
338        }),
339        multiview: None,
340        cache: None,
341    })
342}
343
344fn create_mix_bindgroup(
345    device: &wgpu::Device,
346    pipeline: &wgpu::RenderPipeline,
347    slab_buffer: &wgpu::Buffer,
348    hdr_texture: &Texture,
349    bloom_texture: &Texture,
350) -> wgpu::BindGroup {
351    device.create_bind_group(&wgpu::BindGroupDescriptor {
352        label: Some("bloom mix"),
353        layout: &pipeline.get_bind_group_layout(0),
354        entries: &[
355            wgpu::BindGroupEntry {
356                binding: 0,
357                resource: wgpu::BindingResource::Buffer(slab_buffer.as_entire_buffer_binding()),
358            },
359            wgpu::BindGroupEntry {
360                binding: 1,
361                resource: wgpu::BindingResource::TextureView(&hdr_texture.view),
362            },
363            wgpu::BindGroupEntry {
364                binding: 2,
365                resource: wgpu::BindingResource::Sampler(&hdr_texture.sampler),
366            },
367            wgpu::BindGroupEntry {
368                binding: 3,
369                resource: wgpu::BindingResource::TextureView(&bloom_texture.view),
370            },
371            wgpu::BindGroupEntry {
372                binding: 4,
373                resource: wgpu::BindingResource::Sampler(&bloom_texture.sampler),
374            },
375        ],
376    })
377}
378
379/// Performs a "physically based" bloom effect on a texture. CPU only.
380///
381/// Contains pipelines, down/upsampling textures, a buffer
382/// to communicate configuration to the shaders, and bindgroups.
383///
384/// Clones of [`Bloom`] all point to the same resources.
385#[derive(Clone)]
386pub struct Bloom {
387    slab: SlabAllocator<WgpuRuntime>,
388    slab_buffer: SlabBuffer<wgpu::Buffer>,
389
390    downsample_pixel_sizes: HybridArray<Vec2>,
391    downsample_pipeline: Arc<wgpu::RenderPipeline>,
392
393    upsample_filter_radius: Hybrid<Vec2>,
394    upsample_pipeline: Arc<wgpu::RenderPipeline>,
395
396    textures: Arc<RwLock<Vec<texture::Texture>>>,
397    bindgroups: Arc<RwLock<Vec<wgpu::BindGroup>>>,
398    hdr_texture_downsample_bindgroup: Arc<RwLock<wgpu::BindGroup>>,
399
400    mix_pipeline: Arc<wgpu::RenderPipeline>,
401    mix_bindgroup: Arc<RwLock<wgpu::BindGroup>>,
402    mix_texture: Arc<RwLock<Texture>>,
403    mix_strength: Hybrid<f32>,
404}
405
406impl Bloom {
407    pub fn new(runtime: impl AsRef<WgpuRuntime>, hdr_texture: &Texture) -> Self {
408        let runtime = runtime.as_ref();
409        let resolution = UVec2::new(hdr_texture.width(), hdr_texture.height());
410
411        let slab = SlabAllocator::new(runtime, "bloom-slab", wgpu::BufferUsages::empty());
412        let downsample_pixel_sizes = slab.new_array(
413            config_resolutions(resolution).map(|r| 1.0 / Vec2::new(r.x as f32, r.y as f32)),
414        );
415        let upsample_filter_radius =
416            slab.new_value(1.0 / Vec2::new(resolution.x as f32, resolution.y as f32));
417        let mix_strength = slab.new_value(0.04f32);
418        let slab_buffer = slab.commit();
419
420        let downsample_pipeline = Arc::new(create_bloom_downsample_pipeline(&runtime.device));
421        let upsample_pipeline = Arc::new(create_bloom_upsample_pipeline(&runtime.device));
422        let mix_pipeline = Arc::new(create_mix_pipeline(&runtime.device));
423
424        let hdr_texture_downsample_bindgroup = create_bindgroup(
425            &runtime.device,
426            &downsample_pipeline.get_bind_group_layout(0),
427            &slab_buffer,
428            hdr_texture,
429        );
430        let mix_texture = create_texture(
431            runtime,
432            resolution.x,
433            resolution.y,
434            Some("bloom mix"),
435            wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::COPY_DST,
436        );
437
438        let textures = create_textures(runtime, resolution);
439        let bindgroups = create_bindgroups(
440            &runtime.device,
441            &downsample_pipeline,
442            &slab_buffer,
443            &textures,
444        );
445
446        let mix_bindgroup = create_mix_bindgroup(
447            &runtime.device,
448            &mix_pipeline,
449            &slab_buffer,
450            hdr_texture,
451            &textures[0],
452        );
453
454        Self {
455            slab,
456            slab_buffer,
457            downsample_pixel_sizes,
458            downsample_pipeline,
459            upsample_filter_radius,
460            upsample_pipeline,
461            textures: Arc::new(RwLock::new(textures)),
462            bindgroups: Arc::new(RwLock::new(bindgroups)),
463            hdr_texture_downsample_bindgroup: Arc::new(RwLock::new(
464                hdr_texture_downsample_bindgroup,
465            )),
466            mix_pipeline,
467            mix_texture: Arc::new(RwLock::new(mix_texture)),
468            mix_bindgroup: Arc::new(RwLock::new(mix_bindgroup)),
469            mix_strength,
470        }
471    }
472
473    pub(crate) fn slab_allocator(&self) -> &SlabAllocator<WgpuRuntime> {
474        &self.slab
475    }
476
477    pub fn set_mix_strength(&self, strength: f32) {
478        self.mix_strength.set(strength);
479    }
480
481    pub fn get_mix_strength(&self) -> f32 {
482        self.mix_strength.get()
483    }
484
485    /// Set the filter radius in pixels.
486    pub fn set_filter_radius(&self, filter_radius: f32) {
487        let size = self.get_size();
488        let filter_radius = Vec2::new(filter_radius / size.x as f32, filter_radius / size.y as f32);
489        self.upsample_filter_radius.set(filter_radius);
490    }
491
492    pub fn get_filter_radius(&self) -> Vec2 {
493        self.upsample_filter_radius.get()
494    }
495
496    pub fn get_size(&self) -> UVec2 {
497        let mix_texture = self.get_mix_texture();
498        UVec2::new(mix_texture.width(), mix_texture.height())
499    }
500
501    /// Recreates this bloom using the new HDR texture.
502    pub fn set_hdr_texture(&self, runtime: impl AsRef<WgpuRuntime>, hdr_texture: &Texture) {
503        // UNWRAP: panic on purpose (here and on till the end of this fn)
504        let slab_buffer = self.slab.get_buffer().unwrap();
505        let resolution = UVec2::new(hdr_texture.width(), hdr_texture.height());
506        let runtime = runtime.as_ref();
507        let textures = create_textures(runtime, resolution);
508
509        *self.bindgroups.write().expect("bloom bindgroups write") = create_bindgroups(
510            &runtime.device,
511            &self.downsample_pipeline,
512            &slab_buffer,
513            &textures,
514        );
515        *self
516            .hdr_texture_downsample_bindgroup
517            .write()
518            .expect("bloom hdr downsample bindgroup write") = create_bindgroup(
519            &runtime.device,
520            &self.downsample_pipeline.get_bind_group_layout(0),
521            &slab_buffer,
522            hdr_texture,
523        );
524        *self.mix_texture.write().expect("bloom mix_texture write") = create_texture(
525            runtime,
526            resolution.x,
527            resolution.y,
528            Some("bloom mix"),
529            wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::COPY_DST,
530        );
531        *self
532            .mix_bindgroup
533            .write()
534            .expect("bloom mix_bindgroup write") = create_mix_bindgroup(
535            &runtime.device,
536            &self.mix_pipeline,
537            &slab_buffer,
538            hdr_texture,
539            &textures[0],
540        );
541        *self.textures.write().expect("bloom textures write") = textures;
542    }
543
544    /// Returns a clone of the current mix texture.
545    ///
546    /// The mix texture is the result of mixing the bloom by the hdr using the
547    /// mix strength.
548    pub fn get_mix_texture(&self) -> Texture {
549        // UNWRAP: not safe but we want to panic
550        self.mix_texture
551            .read()
552            .expect("bloom mix_texture read")
553            .clone()
554    }
555
556    pub(crate) fn render_downsamples(&self, device: &wgpu::Device, queue: &wgpu::Queue) {
557        struct DownsampleItem<'a> {
558            view: &'a wgpu::TextureView,
559            bindgroup: &'a wgpu::BindGroup,
560            pixel_size: Id<Vec2>,
561        }
562        // Get all the bindgroups (which are what we're reading from),
563        // starting with the hdr frame.
564        // Since `bindgroups` are one element greater (we pushed
565        // `hdr_texture_bindgroup` to the front) the last bindgroup will
566        // not be used, which is good - we don't need to read from the
567        // smallest texture during downsampling. UNWRAP: not safe but we
568        // want to panic
569        let textures_guard = self.textures.read().expect("bloom textures read");
570        let hdr_texture_downsample_bindgroup_guard = self
571            .hdr_texture_downsample_bindgroup
572            .read()
573            .expect("bloom hdr downsample bindgroup read");
574        let hdr_texture_downsample_bindgroup: &wgpu::BindGroup =
575            &hdr_texture_downsample_bindgroup_guard;
576        let bindgroups_guard = self.bindgroups.read().expect("bloom bindgroups read");
577        let bindgroups =
578            std::iter::once(hdr_texture_downsample_bindgroup).chain(bindgroups_guard.iter());
579        let items = textures_guard
580            .iter()
581            .zip(bindgroups)
582            .zip(self.downsample_pixel_sizes.array().iter())
583            .map(|((tex, bindgroup), pixel_size)| DownsampleItem {
584                view: &tex.view,
585                bindgroup,
586                pixel_size,
587            });
588        for (
589            i,
590            DownsampleItem {
591                view,
592                bindgroup,
593                pixel_size,
594            },
595        ) in items.enumerate()
596        {
597            let title = format!("bloom downsample {i}");
598            let label = Some(title.as_str());
599            let mut encoder =
600                device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label });
601            {
602                let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
603                    label,
604                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
605                        view,
606                        resolve_target: None,
607                        ops: wgpu::Operations {
608                            load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
609                            store: wgpu::StoreOp::Store,
610                        },
611                        depth_slice: None,
612                    })],
613                    depth_stencil_attachment: None,
614                    timestamp_writes: None,
615                    occlusion_query_set: None,
616                });
617                render_pass.set_pipeline(&self.downsample_pipeline);
618                render_pass.set_bind_group(0, Some(bindgroup), &[]);
619                let id = pixel_size.into();
620                render_pass.draw(0..6, id..id + 1);
621            }
622            queue.submit(std::iter::once(encoder.finish()));
623        }
624    }
625
626    fn render_upsamples(&self, device: &wgpu::Device, queue: &wgpu::Queue) {
627        struct UpsampleItem<'a> {
628            view: &'a wgpu::TextureView,
629            bindgroup: &'a wgpu::BindGroup,
630        }
631        // Get all the bindgroups (which are what we're reading from),
632        // starting with the last mip.
633        // UNWRAP: not safe but we want to panic
634        let bindgroups_guard = self.bindgroups.read().expect("bloom bindgroups read");
635        let bindgroups = bindgroups_guard.iter().rev();
636        // Get all the texture views (which are what we're writing to),
637        // starting with the second-to-last mip.
638        let textures_guard = self.textures.read().expect("bloom textures read");
639        let views = textures_guard.iter().rev().skip(1).map(|t| &t.view);
640        let items = bindgroups
641            .zip(views)
642            .map(|(bindgroup, view)| UpsampleItem { view, bindgroup });
643        for (i, UpsampleItem { view, bindgroup }) in items.enumerate() {
644            let title = format!("bloom upsample {}", textures_guard.len() - i - 1);
645            let label = Some(title.as_str());
646            let mut encoder =
647                device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label });
648            {
649                let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
650                    label,
651                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
652                        view,
653                        resolve_target: None,
654                        ops: wgpu::Operations {
655                            load: wgpu::LoadOp::Load,
656                            store: wgpu::StoreOp::Store,
657                        },
658                        depth_slice: None,
659                    })],
660                    depth_stencil_attachment: None,
661                    timestamp_writes: None,
662                    occlusion_query_set: None,
663                });
664                render_pass.set_pipeline(&self.upsample_pipeline);
665                render_pass.set_bind_group(0, Some(bindgroup), &[]);
666                let id = self.upsample_filter_radius.id().into();
667                render_pass.draw(0..6, id..id + 1);
668            }
669            queue.submit(std::iter::once(encoder.finish()));
670        }
671    }
672
673    fn render_mix(&self, device: &wgpu::Device, queue: &wgpu::Queue) {
674        let label = Some("bloom mix");
675        // UNWRAP: not safe but we want to panic
676        let mix_texture = self.mix_texture.read().expect("bloom mix_texture read");
677        let mix_bindgroup = self.mix_bindgroup.read().expect("bloom mix_bindgroup read");
678        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label });
679        {
680            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
681                label,
682                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
683                    view: &mix_texture.view,
684                    resolve_target: None,
685                    ops: wgpu::Operations {
686                        load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
687                        store: wgpu::StoreOp::Store,
688                    },
689                    depth_slice: None,
690                })],
691                depth_stencil_attachment: None,
692                timestamp_writes: None,
693                occlusion_query_set: None,
694            });
695            render_pass.set_pipeline(&self.mix_pipeline);
696            render_pass.set_bind_group(0, Some(mix_bindgroup.deref()), &[]);
697            let id = self.mix_strength.id().into();
698            render_pass.draw(0..6, id..id + 1);
699        }
700
701        queue.submit(std::iter::once(encoder.finish()));
702    }
703
704    pub fn bloom(&self, device: &wgpu::Device, queue: &wgpu::Queue) {
705        self.slab.commit();
706        assert!(
707            self.slab_buffer.is_valid(),
708            "bloom slab buffer should never resize"
709        );
710        self.render_downsamples(device, queue);
711        self.render_upsamples(device, queue);
712        self.render_mix(device, queue);
713    }
714}
715
716#[cfg(test)]
717mod test {
718    use glam::Vec3;
719
720    use crate::{context::Context, test::BlockOnFuture};
721
722    use super::*;
723
724    #[test]
725    fn bloom_texture_sizes_sanity() {
726        let sizes = config_resolutions(UVec2::new(1024, 800)).collect::<Vec<_>>();
727        assert_eq!(
728            vec![
729                UVec2::new(1024, 800),
730                UVec2::new(512, 400),
731                UVec2::new(256, 200),
732                UVec2::new(128, 100),
733                UVec2::new(64, 50),
734                UVec2::new(32, 25),
735                UVec2::new(16, 12),
736                UVec2::new(8, 6),
737                UVec2::new(4, 3),
738                UVec2::new(2, 1),
739            ],
740            sizes
741        );
742        let pixel_sizes = config_resolutions(UVec2::new(1024, 800))
743            .map(|r| 1.0 / Vec2::new(r.x as f32, r.y as f32))
744            .collect::<Vec<_>>();
745        assert_eq!(
746            vec![
747                Vec2::new(0.0009765625, 0.00125),
748                Vec2::new(0.001953125, 0.0025),
749                Vec2::new(0.00390625, 0.005),
750                Vec2::new(0.0078125, 0.01),
751                Vec2::new(0.015625, 0.02),
752                Vec2::new(0.03125, 0.04),
753                Vec2::new(0.0625, 0.083333336),
754                Vec2::new(0.125, 0.16666667),
755                Vec2::new(0.25, 0.33333334),
756                Vec2::new(0.5, 1.0)
757            ],
758            pixel_sizes
759        );
760    }
761
762    #[test]
763    fn bloom_sanity() {
764        let width = 256;
765        let height = 128;
766        let ctx = Context::headless(width, height).block();
767        let stage = ctx.new_stage().with_bloom(false);
768        let projection = crate::camera::perspective(width as f32, height as f32);
769        let view = crate::camera::look_at(Vec3::new(0.0, 2.0, 18.0), Vec3::ZERO, Vec3::Y);
770        let _camera = stage
771            .new_camera()
772            .with_projection_and_view(projection, view);
773        let skybox = stage
774            .new_skybox_from_path("../../img/hdr/night.hdr")
775            .unwrap();
776        stage.use_skybox(&skybox);
777        let ibl = stage.new_ibl(&skybox);
778        stage.use_ibl(&ibl);
779
780        let _doc = stage
781            .load_gltf_document_from_path("../../gltf/EmissiveStrengthTest.glb")
782            .unwrap();
783
784        let frame = ctx.get_next_frame().unwrap();
785        stage.render(&frame.view());
786        let img = frame.read_image().block().unwrap();
787        img_diff::assert_img_eq("bloom/without.png", img);
788        frame.present();
789
790        // now render the whole thing with default values
791        stage.set_has_bloom(true);
792        stage.set_bloom_mix_strength(0.1);
793        stage.set_bloom_filter_radius(2.0);
794        let frame = ctx.get_next_frame().unwrap();
795        stage.render(&frame.view());
796        let img = frame.read_image().block().unwrap();
797        img_diff::assert_img_eq("bloom/with.png", img);
798    }
799}