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().unwrap() = create_bindgroups(
510            &runtime.device,
511            &self.downsample_pipeline,
512            &slab_buffer,
513            &textures,
514        );
515        *self.hdr_texture_downsample_bindgroup.write().unwrap() = create_bindgroup(
516            &runtime.device,
517            &self.downsample_pipeline.get_bind_group_layout(0),
518            &slab_buffer,
519            hdr_texture,
520        );
521        *self.mix_texture.write().unwrap() = create_texture(
522            runtime,
523            resolution.x,
524            resolution.y,
525            Some("bloom mix"),
526            wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::COPY_DST,
527        );
528        *self.mix_bindgroup.write().unwrap() = create_mix_bindgroup(
529            &runtime.device,
530            &self.mix_pipeline,
531            &slab_buffer,
532            hdr_texture,
533            &textures[0],
534        );
535        *self.textures.write().unwrap() = textures;
536    }
537
538    /// Returns a clone of the current mix texture.
539    ///
540    /// The mix texture is the result of mixing the bloom by the hdr using the
541    /// mix strength.
542    pub fn get_mix_texture(&self) -> Texture {
543        // UNWRAP: not safe but we want to panic
544        self.mix_texture.read().unwrap().clone()
545    }
546
547    pub(crate) fn render_downsamples(&self, device: &wgpu::Device, queue: &wgpu::Queue) {
548        struct DownsampleItem<'a> {
549            view: &'a wgpu::TextureView,
550            bindgroup: &'a wgpu::BindGroup,
551            pixel_size: Id<Vec2>,
552        }
553        // Get all the bindgroups (which are what we're reading from),
554        // starting with the hdr frame.
555        // Since `bindgroups` are one element greater (we pushed `hdr_texture_bindgroup`
556        // to the front) the last bindgroup will not be used, which is good - we
557        // don't need to read from the smallest texture during downsampling.
558        // UNWRAP: not safe but we want to panic
559        let textures_guard = self.textures.read().unwrap();
560        let hdr_texture_downsample_bindgroup_guard =
561            self.hdr_texture_downsample_bindgroup.read().unwrap();
562        let hdr_texture_downsample_bindgroup: &wgpu::BindGroup =
563            &hdr_texture_downsample_bindgroup_guard;
564        let bindgroups_guard = self.bindgroups.read().unwrap();
565        let bindgroups =
566            std::iter::once(hdr_texture_downsample_bindgroup).chain(bindgroups_guard.iter());
567        let items = textures_guard
568            .iter()
569            .zip(bindgroups)
570            .zip(self.downsample_pixel_sizes.array().iter())
571            .map(|((tex, bindgroup), pixel_size)| DownsampleItem {
572                view: &tex.view,
573                bindgroup,
574                pixel_size,
575            });
576        for (
577            i,
578            DownsampleItem {
579                view,
580                bindgroup,
581                pixel_size,
582            },
583        ) in items.enumerate()
584        {
585            let title = format!("bloom downsample {i}");
586            let label = Some(title.as_str());
587            let mut encoder =
588                device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label });
589            {
590                let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
591                    label,
592                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
593                        view,
594                        resolve_target: None,
595                        ops: wgpu::Operations {
596                            load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
597                            store: wgpu::StoreOp::Store,
598                        },
599                        depth_slice: None,
600                    })],
601                    depth_stencil_attachment: None,
602                    timestamp_writes: None,
603                    occlusion_query_set: None,
604                });
605                render_pass.set_pipeline(&self.downsample_pipeline);
606                render_pass.set_bind_group(0, Some(bindgroup), &[]);
607                let id = pixel_size.into();
608                render_pass.draw(0..6, id..id + 1);
609            }
610            queue.submit(std::iter::once(encoder.finish()));
611        }
612    }
613
614    fn render_upsamples(&self, device: &wgpu::Device, queue: &wgpu::Queue) {
615        struct UpsampleItem<'a> {
616            view: &'a wgpu::TextureView,
617            bindgroup: &'a wgpu::BindGroup,
618        }
619        // Get all the bindgroups (which are what we're reading from),
620        // starting with the last mip.
621        // UNWRAP: not safe but we want to panic
622        let bindgroups_guard = self.bindgroups.read().unwrap();
623        let bindgroups = bindgroups_guard.iter().rev();
624        // Get all the texture views (which are what we're writing to),
625        // starting with the second-to-last mip.
626        let textures_guard = self.textures.read().unwrap();
627        let views = textures_guard.iter().rev().skip(1).map(|t| &t.view);
628        let items = bindgroups
629            .zip(views)
630            .map(|(bindgroup, view)| UpsampleItem { view, bindgroup });
631        for (i, UpsampleItem { view, bindgroup }) in items.enumerate() {
632            let title = format!("bloom upsample {}", textures_guard.len() - i - 1);
633            let label = Some(title.as_str());
634            let mut encoder =
635                device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label });
636            {
637                let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
638                    label,
639                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
640                        view,
641                        resolve_target: None,
642                        ops: wgpu::Operations {
643                            load: wgpu::LoadOp::Load,
644                            store: wgpu::StoreOp::Store,
645                        },
646                        depth_slice: None,
647                    })],
648                    depth_stencil_attachment: None,
649                    timestamp_writes: None,
650                    occlusion_query_set: None,
651                });
652                render_pass.set_pipeline(&self.upsample_pipeline);
653                render_pass.set_bind_group(0, Some(bindgroup), &[]);
654                let id = self.upsample_filter_radius.id().into();
655                render_pass.draw(0..6, id..id + 1);
656            }
657            queue.submit(std::iter::once(encoder.finish()));
658        }
659    }
660
661    fn render_mix(&self, device: &wgpu::Device, queue: &wgpu::Queue) {
662        let label = Some("bloom mix");
663        // UNWRAP: not safe but we want to panic
664        let mix_texture = self.mix_texture.read().unwrap();
665        let mix_bindgroup = self.mix_bindgroup.read().unwrap();
666        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label });
667        {
668            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
669                label,
670                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
671                    view: &mix_texture.view,
672                    resolve_target: None,
673                    ops: wgpu::Operations {
674                        load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
675                        store: wgpu::StoreOp::Store,
676                    },
677                    depth_slice: None,
678                })],
679                depth_stencil_attachment: None,
680                timestamp_writes: None,
681                occlusion_query_set: None,
682            });
683            render_pass.set_pipeline(&self.mix_pipeline);
684            render_pass.set_bind_group(0, Some(mix_bindgroup.deref()), &[]);
685            let id = self.mix_strength.id().into();
686            render_pass.draw(0..6, id..id + 1);
687        }
688
689        queue.submit(std::iter::once(encoder.finish()));
690    }
691
692    pub fn bloom(&self, device: &wgpu::Device, queue: &wgpu::Queue) {
693        self.slab.commit();
694        assert!(
695            self.slab_buffer.is_valid(),
696            "bloom slab buffer should never resize"
697        );
698        self.render_downsamples(device, queue);
699        self.render_upsamples(device, queue);
700        self.render_mix(device, queue);
701    }
702}
703
704#[cfg(test)]
705mod test {
706    use glam::Vec3;
707
708    use crate::{context::Context, test::BlockOnFuture};
709
710    use super::*;
711
712    #[test]
713    fn bloom_texture_sizes_sanity() {
714        let sizes = config_resolutions(UVec2::new(1024, 800)).collect::<Vec<_>>();
715        assert_eq!(
716            vec![
717                UVec2::new(1024, 800),
718                UVec2::new(512, 400),
719                UVec2::new(256, 200),
720                UVec2::new(128, 100),
721                UVec2::new(64, 50),
722                UVec2::new(32, 25),
723                UVec2::new(16, 12),
724                UVec2::new(8, 6),
725                UVec2::new(4, 3),
726                UVec2::new(2, 1),
727            ],
728            sizes
729        );
730        let pixel_sizes = config_resolutions(UVec2::new(1024, 800))
731            .map(|r| 1.0 / Vec2::new(r.x as f32, r.y as f32))
732            .collect::<Vec<_>>();
733        assert_eq!(
734            vec![
735                Vec2::new(0.0009765625, 0.00125),
736                Vec2::new(0.001953125, 0.0025),
737                Vec2::new(0.00390625, 0.005),
738                Vec2::new(0.0078125, 0.01),
739                Vec2::new(0.015625, 0.02),
740                Vec2::new(0.03125, 0.04),
741                Vec2::new(0.0625, 0.083333336),
742                Vec2::new(0.125, 0.16666667),
743                Vec2::new(0.25, 0.33333334),
744                Vec2::new(0.5, 1.0)
745            ],
746            pixel_sizes
747        );
748    }
749
750    #[test]
751    fn bloom_sanity() {
752        let width = 256;
753        let height = 128;
754        let ctx = Context::headless(width, height).block();
755        let stage = ctx.new_stage().with_bloom(false);
756        let projection = crate::camera::perspective(width as f32, height as f32);
757        let view = crate::camera::look_at(Vec3::new(0.0, 2.0, 18.0), Vec3::ZERO, Vec3::Y);
758        let _camera = stage
759            .new_camera()
760            .with_projection_and_view(projection, view);
761        let skybox = stage
762            .new_skybox_from_path("../../img/hdr/night.hdr")
763            .unwrap();
764        stage.use_skybox(&skybox);
765        let ibl = stage.new_ibl(&skybox);
766        stage.use_ibl(&ibl);
767
768        let _doc = stage
769            .load_gltf_document_from_path("../../gltf/EmissiveStrengthTest.glb")
770            .unwrap();
771
772        let frame = ctx.get_next_frame().unwrap();
773        stage.render(&frame.view());
774        let img = frame.read_image().block().unwrap();
775        img_diff::assert_img_eq("bloom/without.png", img);
776        frame.present();
777
778        // now render the whole thing with default values
779        stage.set_has_bloom(true);
780        stage.set_bloom_mix_strength(0.1);
781        stage.set_bloom_filter_radius(2.0);
782        let frame = ctx.get_next_frame().unwrap();
783        stage.render(&frame.view());
784        let img = frame.read_image().block().unwrap();
785        img_diff::assert_img_eq("bloom/with.png", img);
786    }
787}