Skip to main content

fidget_wgpu/effects/
mod.rs

1//! On-GPU effects
2//!
3//! These effects let us set up a simple rendering pipeline:
4//!
5//! - Start with [`GeometryPixel`](fidget_raster::voxel::GeometryPixel) buffers
6//!   (16 bytes per pixel, stored on the GPU).
7//! - Merge and denoise a set of buffers into a single image containing
8//!   [`PackedVoxel`] data (normals, depth, and source image index packed into 8
9//!   bytes per pixel).
10//! - Apply shading to a [`PackedVoxel`] buffer, producing an RGBA image buffer
11
12use crate::{
13    Gpu,
14    buf::{
15        BufferSizeError, ImageBuffer, ImageReadBuffer, buffer_ro, buffer_rw,
16        buffer_uniform,
17    },
18    tag,
19    voxel::GeomBufferTag,
20};
21use fidget_core::render::{ImageSize, VoxelSize};
22use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};
23
24/// WGPU context for applying various effects
25pub struct Context {
26    gpu: Gpu,
27
28    merge_bind_group_layout: wgpu::BindGroupLayout,
29    merge_pipeline: wgpu::ComputePipeline,
30
31    shade_bind_group_layout: wgpu::BindGroupLayout,
32    shade_pipeline: wgpu::ComputePipeline,
33
34    ssao_ctx: SsaoContext,
35}
36
37const COMMON_SHADER: &str = include_str!("shaders/common.wgsl");
38const MERGE_SHADER: &str = include_str!("shaders/merge.wgsl");
39const SHADE_SHADER: &str = include_str!("shaders/shade.wgsl");
40const SSAO_SHADER: &str = include_str!("shaders/ssao.wgsl");
41const BLUR_SHADER: &str = include_str!("shaders/blur.wgsl");
42
43fn merge_shader() -> String {
44    MERGE_SHADER.to_owned() + COMMON_SHADER + crate::COMMON_SHADER
45}
46
47fn shade_shader() -> String {
48    SHADE_SHADER.to_owned() + COMMON_SHADER + crate::COMMON_SHADER
49}
50
51fn ssao_shader() -> String {
52    SSAO_SHADER.to_owned() + COMMON_SHADER + crate::COMMON_SHADER
53}
54
55fn blur_shader() -> String {
56    BLUR_SHADER.to_owned() + COMMON_SHADER + crate::COMMON_SHADER
57}
58
59/// Packed voxel structure used on the GPU
60#[derive(Copy, Clone, FromBytes, Immutable, IntoBytes, KnownLayout)]
61#[repr(C)]
62pub struct PackedVoxel {
63    /// XY components of the normal (normalized to a length of 127)
64    ///
65    /// The Z component is implied and positive
66    ///
67    /// An invalid normal is represented by `[-128, -128]`.
68    pub normal: [i8; 2],
69
70    /// Shape index
71    pub index: u16,
72
73    /// Depth of the voxel
74    ///
75    /// If this is 0, then the voxel is not populated
76    pub z: u32,
77}
78
79#[derive(Copy, Clone, FromBytes, Immutable, IntoBytes, KnownLayout)]
80#[repr(C)]
81struct MergeConfig {
82    /// Image size, in pixels
83    image_size: [u32; 2],
84
85    /// Whether or not to denoise when merging (non-zero is true)
86    denoise: u32,
87
88    /// Offset applied to indices when merging
89    ///
90    /// When this is 0, we initialize the output image
91    index_base: u32,
92
93    /// Number of valid image buffers (0-7)
94    image_count: u32,
95
96    // padding to the nearest multiple of 8
97    _pad: u32,
98}
99
100#[derive(Copy, Clone, FromBytes, Immutable, IntoBytes, KnownLayout)]
101#[repr(C)]
102struct ShadeConfig {
103    /// Image size, in pixels
104    image_size: [u32; 3],
105
106    /// Flag to determine whether the SSAO buffer is valid (0 / 1)
107    has_ssao: u32,
108}
109
110tag!(MergeVoxelBufferTag, PackedVoxel, STORAGE | COPY_SRC);
111
112/// Handle to a set of buffers used when merging images
113pub struct MergeBuffers {
114    config: wgpu::Buffer,
115    out: ImageBuffer<MergeVoxelBufferTag>,
116    depth: u32,
117}
118
119tag!(
120    pub ShadedImageTag, u32, STORAGE | COPY_SRC,
121    "Buffer tag for on-GPU shaded (RGBA) images"
122);
123
124/// Handle to a set of buffers used when shading images
125pub struct ShadeBuffers {
126    config: wgpu::Buffer,
127    out: ImageBuffer<ShadedImageTag>,
128}
129
130impl ShadeBuffers {
131    /// Returns a reference to the output buffer
132    pub fn output(&self) -> &ImageBuffer<ShadedImageTag> {
133        &self.out
134    }
135}
136
137/// Error returned when submitting a merge operation
138#[derive(Debug, thiserror::Error)]
139pub enum MergeError {
140    /// Image sizes in the slice are not consistent
141    #[error(transparent)]
142    ImageSizeMismatch(#[from] ImageSizeMismatch),
143
144    /// An error occurred while resizing the output buffer
145    #[error(transparent)]
146    OutputSize(BufferSizeError),
147}
148
149/// Error returned when submitting a shade operation
150#[derive(Debug, thiserror::Error)]
151pub enum ShadeError {
152    /// An error occurred while resizing the output buffer
153    #[error(transparent)]
154    OutputSize(BufferSizeError),
155}
156
157/// Error returned when submitting an SSAO operation
158#[derive(Debug, thiserror::Error)]
159pub enum SsaoError {
160    /// An error occurred while resizing the output buffer
161    #[error(transparent)]
162    OutputSize(BufferSizeError),
163}
164
165/// Type indicating an image size mismatch
166#[derive(Debug, thiserror::Error)]
167#[error(
168    "image size mismatch: expected {} × {}, got {} × {}",
169    expected.width(), expected.height(),
170    actual.width(), actual.height()
171)]
172pub struct ImageSizeMismatch {
173    expected: ImageSize,
174    actual: ImageSize,
175}
176
177impl Context {
178    /// Builds a new context for applying effects
179    pub fn new(gpu: &Gpu) -> Self {
180        let merge_bind_group_layout = gpu.device.create_bind_group_layout(
181            &wgpu::BindGroupLayoutDescriptor {
182                label: None,
183                entries: &[
184                    buffer_uniform(0),
185                    buffer_ro(1), // image0
186                    buffer_ro(2), // image1
187                    buffer_ro(3), // image2
188                    buffer_ro(4), // image3
189                    buffer_ro(5), // image4
190                    buffer_ro(6), // image5
191                    buffer_ro(7), // image6
192                    buffer_rw(8), // out
193                ],
194            },
195        );
196        let shader_code = merge_shader();
197        let pipeline_layout = gpu.device.create_pipeline_layout(
198            &wgpu::PipelineLayoutDescriptor {
199                label: Some("effects merge pipeline"),
200                bind_group_layouts: &[Some(&merge_bind_group_layout)],
201                immediate_size: 0u32,
202            },
203        );
204        let shader_module =
205            gpu.device
206                .create_shader_module(wgpu::ShaderModuleDescriptor {
207                    label: Some("effects merge shader module"),
208                    source: wgpu::ShaderSource::Wgsl(shader_code.into()),
209                });
210        let merge_pipeline = gpu.device.create_compute_pipeline(
211            &wgpu::ComputePipelineDescriptor {
212                label: Some("effects merge compute pipeline"),
213                layout: Some(&pipeline_layout),
214                module: &shader_module,
215                entry_point: Some("merge_main"),
216                compilation_options: Default::default(),
217                cache: None,
218            },
219        );
220
221        let shade_bind_group_layout = gpu.device.create_bind_group_layout(
222            &wgpu::BindGroupLayoutDescriptor {
223                label: None,
224                entries: &[
225                    buffer_uniform(0),
226                    buffer_ro(1), // image
227                    buffer_ro(2), // ssao occlusion
228                    buffer_rw(3), // out
229                ],
230            },
231        );
232        let shader_code = shade_shader();
233        let pipeline_layout = gpu.device.create_pipeline_layout(
234            &wgpu::PipelineLayoutDescriptor {
235                label: Some("effects shade pipeline"),
236                bind_group_layouts: &[Some(&shade_bind_group_layout)],
237                immediate_size: 0u32,
238            },
239        );
240        let shader_module =
241            gpu.device
242                .create_shader_module(wgpu::ShaderModuleDescriptor {
243                    label: Some("effects shade shader module"),
244                    source: wgpu::ShaderSource::Wgsl(shader_code.into()),
245                });
246        let shade_pipeline = gpu.device.create_compute_pipeline(
247            &wgpu::ComputePipelineDescriptor {
248                label: Some("effects shade compute pipeline"),
249                layout: Some(&pipeline_layout),
250                module: &shader_module,
251                entry_point: Some("shade_main"),
252                compilation_options: Default::default(),
253                cache: None,
254            },
255        );
256
257        let ssao_ctx = SsaoContext::new(&gpu.device);
258
259        Self {
260            gpu: gpu.clone(),
261            merge_bind_group_layout,
262            merge_pipeline,
263            shade_bind_group_layout,
264            shade_pipeline,
265            ssao_ctx,
266        }
267    }
268
269    /// Builds a new set of [`MergeBuffers`] for the given image size
270    pub fn merge_buffers(
271        &self,
272        image_size: VoxelSize,
273    ) -> Result<MergeBuffers, BufferSizeError> {
274        let config = self.gpu.device.create_buffer(&wgpu::BufferDescriptor {
275            label: Some("config"),
276            size: std::mem::size_of::<MergeConfig>() as u64,
277            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
278            mapped_at_creation: false,
279        });
280        let out = ImageBuffer::new(
281            &self.gpu.device,
282            "merge output".to_owned(),
283            ImageSize::new(image_size.width(), image_size.height()),
284        )?;
285        Ok(MergeBuffers {
286            config,
287            out,
288            depth: image_size.depth(),
289        })
290    }
291
292    /// Builds a new set of [`ShadeBuffers`] for the given image size
293    pub fn shade_buffers(
294        &self,
295        image_size: ImageSize,
296    ) -> Result<ShadeBuffers, BufferSizeError> {
297        let config = self.gpu.device.create_buffer(&wgpu::BufferDescriptor {
298            label: Some("shade config"),
299            size: std::mem::size_of::<ShadeConfig>() as u64,
300            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
301            mapped_at_creation: false,
302        });
303        let out = ImageBuffer::new(
304            &self.gpu.device,
305            "shade output".to_owned(),
306            ImageSize::new(image_size.width(), image_size.height()),
307        )?;
308        Ok(ShadeBuffers { config, out })
309    }
310
311    /// Builds a new set of [`SsaoBuffers`] for the given image size
312    pub fn ssao_buffers(
313        &self,
314        image_size: VoxelSize,
315    ) -> Result<SsaoBuffers, BufferSizeError> {
316        let ssao_config =
317            self.gpu.device.create_buffer(&wgpu::BufferDescriptor {
318                label: Some("ssao config"),
319                size: std::mem::size_of::<SsaoConfig>() as u64,
320                usage: wgpu::BufferUsages::UNIFORM
321                    | wgpu::BufferUsages::COPY_DST,
322                mapped_at_creation: false,
323            });
324        let raw_occlusion = ImageBuffer::new(
325            &self.gpu.device,
326            "ssao raw occlusion".to_owned(),
327            ImageSize::new(image_size.width(), image_size.height()),
328        )?;
329        let blur_config =
330            self.gpu.device.create_buffer(&wgpu::BufferDescriptor {
331                label: Some("blur config"),
332                size: std::mem::size_of::<BlurConfig>() as u64,
333                usage: wgpu::BufferUsages::UNIFORM
334                    | wgpu::BufferUsages::COPY_DST,
335                mapped_at_creation: false,
336            });
337        let blurred_occlusion = ImageBuffer::new(
338            &self.gpu.device,
339            "ssao blurred occlusion".to_owned(),
340            ImageSize::new(image_size.width(), image_size.height()),
341        )?;
342        Ok(SsaoBuffers {
343            ssao_config,
344            blur_config,
345            raw_occlusion,
346            blurred_occlusion,
347        })
348    }
349
350    /// Submits a set of merge operations to combine all of the images
351    ///
352    /// The output buffer is resized to fit the images
353    ///
354    /// If the incoming slice is empty, then no work is submitted
355    pub fn submit_merge(
356        &self,
357        images: &[&ImageBuffer<GeomBufferTag>],
358        denoise: bool,
359        buf: &mut MergeBuffers,
360    ) -> Result<(), MergeError> {
361        let Some(size) = images.first().map(|i| i.size()) else {
362            return Ok(());
363        };
364        for i in &images[1..] {
365            let actual = i.size();
366            if actual != size {
367                return Err(ImageSizeMismatch {
368                    expected: size,
369                    actual,
370                }
371                .into());
372            }
373        }
374        buf.out
375            .grow_to_fit(&self.gpu.device, size)
376            .map_err(MergeError::OutputSize)?;
377        let mut encoder = self.gpu.device.create_command_encoder(
378            &wgpu::CommandEncoderDescriptor {
379                label: Some("merge compute encoder"),
380            },
381        );
382        // Scope to bound the lifetime of compute_pass
383        {
384            let mut compute_pass =
385                encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
386                    label: Some("merge compute pass"),
387                    timestamp_writes: None, // TODO add timestamps?
388                });
389            compute_pass.set_pipeline(&self.merge_pipeline);
390            for (i, chunk) in images.chunks(7).enumerate() {
391                let cfg = MergeConfig {
392                    image_size: [size.width(), size.height()],
393                    denoise: denoise as u32,
394                    index_base: i as u32 * 7,
395                    image_count: chunk.len() as u32,
396                    _pad: 0,
397                };
398                {
399                    let mut writer = self
400                        .gpu
401                        .queue
402                        .write_buffer_with(
403                            &buf.config,
404                            0,
405                            (std::mem::size_of::<MergeConfig>() as u64)
406                                .try_into()
407                                .unwrap(),
408                        )
409                        .unwrap();
410                    writer.copy_from_slice(cfg.as_bytes());
411                }
412                let image_bind = |i| wgpu::BindGroupEntry {
413                    binding: i as u32 + 1,
414                    resource: chunk
415                        .get(i)
416                        .unwrap_or_else(|| chunk.first().unwrap())
417                        .bind_active(),
418                };
419
420                let bg = self.gpu.device.create_bind_group(
421                    &wgpu::BindGroupDescriptor {
422                        label: Some("merge bind group"),
423                        layout: &self.merge_bind_group_layout,
424                        entries: &[
425                            wgpu::BindGroupEntry {
426                                binding: 0,
427                                resource: buf.config.as_entire_binding(),
428                            },
429                            image_bind(0),
430                            image_bind(1),
431                            image_bind(2),
432                            image_bind(3),
433                            image_bind(4),
434                            image_bind(5),
435                            image_bind(6),
436                            wgpu::BindGroupEntry {
437                                binding: 8,
438                                resource: buf.out.bind_active(),
439                            },
440                        ],
441                    },
442                );
443                compute_pass.set_bind_group(0, Some(&bg), &[]);
444                compute_pass.dispatch_workgroups(
445                    size.width().div_ceil(8),
446                    size.height().div_ceil(8),
447                    1,
448                );
449            }
450        }
451        self.gpu.queue.submit(Some(encoder.finish()));
452        Ok(())
453    }
454
455    /// Submits an operation to shade an image
456    ///
457    /// The output buffer is resized to fit the images
458    pub fn submit_shade(
459        &self,
460        image: &MergeBuffers,
461        ssao: Option<&SsaoBuffers>,
462        buf: &mut ShadeBuffers,
463        out: Option<&mut ImageReadBuffer<ShadedImageTag>>,
464    ) -> Result<(), ShadeError> {
465        let size = image.out.size();
466        buf.out
467            .grow_to_fit(&self.gpu.device, size)
468            .map_err(ShadeError::OutputSize)?;
469        let mut encoder = self.gpu.device.create_command_encoder(
470            &wgpu::CommandEncoderDescriptor {
471                label: Some("shade compute encoder"),
472            },
473        );
474
475        // Scope to bound the lifetime of compute_pass
476        {
477            let mut compute_pass =
478                encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
479                    label: Some("shade compute pass"),
480                    timestamp_writes: None, // TODO add timestamps?
481                });
482            compute_pass.set_pipeline(&self.shade_pipeline);
483            let cfg = ShadeConfig {
484                image_size: [size.width(), size.height(), image.depth],
485                has_ssao: ssao.is_some() as u32,
486            };
487            {
488                let mut writer = self
489                    .gpu
490                    .queue
491                    .write_buffer_with(
492                        &buf.config,
493                        0,
494                        buf.config.size().try_into().unwrap(),
495                    )
496                    .unwrap();
497                writer.copy_from_slice(cfg.as_bytes());
498            }
499            let bg =
500                self.gpu
501                    .device
502                    .create_bind_group(&wgpu::BindGroupDescriptor {
503                        label: Some("shade bind group"),
504                        layout: &self.shade_bind_group_layout,
505                        entries: &[
506                            wgpu::BindGroupEntry {
507                                binding: 0,
508                                resource: buf.config.as_entire_binding(),
509                            },
510                            wgpu::BindGroupEntry {
511                                binding: 1,
512                                resource: image.out.bind_active(),
513                            },
514                            wgpu::BindGroupEntry {
515                                binding: 2,
516                                resource: ssao
517                                    .map(|s| {
518                                        s.blurred_occlusion().bind_active()
519                                    })
520                                    .unwrap_or_else(|| image.out.bind_active()),
521                            },
522                            wgpu::BindGroupEntry {
523                                binding: 3,
524                                resource: buf.out.bind_active(),
525                            },
526                        ],
527                    });
528            compute_pass.set_bind_group(0, Some(&bg), &[]);
529            compute_pass.dispatch_workgroups(
530                size.width().div_ceil(8),
531                size.height().div_ceil(8),
532                1,
533            );
534        }
535        if let Some(image) = out {
536            image.grow_to_fit(&self.gpu.device, buf.out.size()).expect(
537                "buf.out.size should always be \
538                 a valid size for grow_to_fit",
539            );
540            encoder.copy_buffer_to_buffer(
541                buf.out.data(),
542                0,
543                image.data(),
544                0,
545                buf.out.size_bytes(),
546            );
547        }
548        self.gpu.queue.submit(Some(encoder.finish()));
549        Ok(())
550    }
551
552    /// Submits a pass to compute an SSAO buffer
553    pub fn submit_ssao(
554        &self,
555        image: &MergeBuffers,
556        buf: &mut SsaoBuffers,
557    ) -> Result<(), SsaoError> {
558        self.ssao_ctx.submit(image, buf, &self.gpu)
559    }
560}
561
562////////////////////////////////////////////////////////////////////////////////
563
564tag!(pub SsaoRawBufferTag, f32, STORAGE | COPY_SRC,
565    "Tag for a raw SSAO occlusion buffer");
566tag!(pub SsaoBlurredBufferTag, f32, STORAGE | COPY_SRC,
567    "Tag for a blurred SSAO occlusion buffer");
568
569/// Handle to a set of buffers used when running an SSAO pass
570pub struct SsaoBuffers {
571    ssao_config: wgpu::Buffer,
572    raw_occlusion: ImageBuffer<SsaoRawBufferTag>,
573
574    blur_config: wgpu::Buffer,
575    blurred_occlusion: ImageBuffer<SsaoBlurredBufferTag>,
576}
577
578impl SsaoBuffers {
579    /// Returns a shared handle to the raw SSAO occlusion buffer
580    pub fn raw_occlusion(&self) -> &ImageBuffer<SsaoRawBufferTag> {
581        &self.raw_occlusion
582    }
583
584    /// Returns a shared handle to the blurred SSAO occlusion buffer
585    pub fn blurred_occlusion(&self) -> &ImageBuffer<SsaoBlurredBufferTag> {
586        &self.blurred_occlusion
587    }
588}
589
590#[derive(Copy, Clone, FromBytes, Immutable, IntoBytes, KnownLayout)]
591#[repr(C)]
592struct SsaoConfig {
593    /// Image size, in voxels
594    image_size: [u32; 3],
595
596    /// Radius of SSAO sampling
597    radius: f32,
598}
599
600#[derive(Copy, Clone, FromBytes, Immutable, IntoBytes, KnownLayout)]
601#[repr(C)]
602struct BlurConfig {
603    /// Image size, in pixels
604    image_size: [u32; 2],
605
606    /// Pixel radius of blur
607    radius: u32,
608
609    /// Padding to 16 bytes
610    _pad: u32,
611}
612
613struct SsaoContext {
614    /// Fixed bind group for SSAO pass
615    ///
616    /// This contains the SSAO kernel and noise buffers, which are constants.
617    ssao_bind_group: wgpu::BindGroup,
618
619    /// Layout for bind group that accepts buffers from the user
620    ssao_bind_group_layout: wgpu::BindGroupLayout,
621
622    /// Pipeline for computing per-pixel SSAO
623    ssao_pipeline: wgpu::ComputePipeline,
624
625    /// Layout for blur pipeline
626    blur_bind_group_layout: wgpu::BindGroupLayout,
627
628    /// Pipeline for blurring an SSAO image
629    blur_pipeline: wgpu::ComputePipeline,
630}
631
632impl SsaoContext {
633    pub fn new(device: &wgpu::Device) -> Self {
634        let ssao_fixed_bind_group_layout =
635            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
636                label: None,
637                entries: &[buffer_ro(0), buffer_ro(1)],
638            });
639        let ssao_user_bind_group_layout =
640            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
641                label: None,
642                entries: &[buffer_uniform(0), buffer_ro(1), buffer_rw(2)],
643            });
644
645        const KERNEL_SIZE: usize = 64;
646        const NOISE_SIZE: usize = 16;
647
648        // Build constant buffers and their bind group
649        let ssao_kernel_size_bytes =
650            KERNEL_SIZE * std::mem::size_of::<[f32; 3]>();
651        let ssao_kernel = device.create_buffer(&wgpu::BufferDescriptor {
652            label: Some("ssao kernel"),
653            size: ssao_kernel_size_bytes as u64,
654            usage: wgpu::BufferUsages::STORAGE,
655            mapped_at_creation: true,
656        });
657        let ssao_kernel_values =
658            fidget_raster::effects::ssao_kernel(KERNEL_SIZE);
659        ssao_kernel
660            .get_mapped_range_mut(0..ssao_kernel_size_bytes as u64)
661            .copy_from_slice(ssao_kernel_values.as_slice().as_bytes());
662        ssao_kernel.unmap();
663
664        let ssao_noise_size_bytes =
665            NOISE_SIZE * std::mem::size_of::<[f32; 2]>();
666        let ssao_noise = device.create_buffer(&wgpu::BufferDescriptor {
667            label: Some("ssao noise"),
668            size: ssao_noise_size_bytes as u64,
669            usage: wgpu::BufferUsages::STORAGE,
670            mapped_at_creation: true,
671        });
672        let ssao_noise_values = fidget_raster::effects::ssao_noise(NOISE_SIZE);
673        ssao_noise
674            .get_mapped_range_mut(0..ssao_noise_size_bytes as u64)
675            .copy_from_slice(ssao_noise_values.as_slice().as_bytes());
676        ssao_noise.unmap();
677
678        let ssao_bind_group =
679            device.create_bind_group(&wgpu::BindGroupDescriptor {
680                label: Some("ssao fixed bind group"),
681                layout: &ssao_fixed_bind_group_layout,
682                entries: &[
683                    wgpu::BindGroupEntry {
684                        binding: 0,
685                        resource: ssao_kernel.as_entire_binding(),
686                    },
687                    wgpu::BindGroupEntry {
688                        binding: 1,
689                        resource: ssao_noise.as_entire_binding(),
690                    },
691                ],
692            });
693
694        let shader_code = ssao_shader();
695        let pipeline_layout =
696            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
697                label: Some("effects ssao pipeline"),
698                bind_group_layouts: &[
699                    Some(&ssao_user_bind_group_layout),
700                    Some(&ssao_fixed_bind_group_layout),
701                ],
702                immediate_size: 0u32,
703            });
704        let shader_module =
705            device.create_shader_module(wgpu::ShaderModuleDescriptor {
706                label: Some("effects ssao shader module"),
707                source: wgpu::ShaderSource::Wgsl(shader_code.into()),
708            });
709        let ssao_pipeline =
710            device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
711                label: Some("effects ssao compute pipeline"),
712                layout: Some(&pipeline_layout),
713                module: &shader_module,
714                entry_point: Some("ssao_main"),
715                compilation_options: Default::default(),
716                cache: None,
717            });
718
719        let blur_bind_group_layout =
720            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
721                label: None,
722                entries: &[buffer_uniform(0), buffer_ro(1), buffer_rw(2)],
723            });
724        let shader_code = blur_shader();
725        let pipeline_layout =
726            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
727                label: Some("effects blur pipeline"),
728                bind_group_layouts: &[Some(&blur_bind_group_layout)],
729                immediate_size: 0u32,
730            });
731        let shader_module =
732            device.create_shader_module(wgpu::ShaderModuleDescriptor {
733                label: Some("effects blur shader module"),
734                source: wgpu::ShaderSource::Wgsl(shader_code.into()),
735            });
736        let blur_pipeline =
737            device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
738                label: Some("effects blur compute pipeline"),
739                layout: Some(&pipeline_layout),
740                module: &shader_module,
741                entry_point: Some("blur_main"),
742                compilation_options: Default::default(),
743                cache: None,
744            });
745
746        Self {
747            ssao_bind_group,
748            ssao_bind_group_layout: ssao_user_bind_group_layout,
749            ssao_pipeline,
750            blur_bind_group_layout,
751            blur_pipeline,
752        }
753    }
754
755    fn submit(
756        &self,
757        image: &MergeBuffers,
758        buf: &mut SsaoBuffers,
759        gpu: &Gpu,
760    ) -> Result<(), SsaoError> {
761        let image_size = image.out.size();
762        buf.raw_occlusion
763            .grow_to_fit(&gpu.device, image_size)
764            .map_err(SsaoError::OutputSize)?;
765        buf.blurred_occlusion
766            .grow_to_fit(&gpu.device, image_size)
767            .map_err(SsaoError::OutputSize)?;
768
769        // TODO make this passed in?
770        let mut encoder = gpu.device.create_command_encoder(
771            &wgpu::CommandEncoderDescriptor {
772                label: Some("ssao command encoder"),
773            },
774        );
775
776        // Scope to bound the lifetime of compute_pass
777        {
778            let mut compute_pass =
779                encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
780                    label: Some("ssao compute pass"),
781                    timestamp_writes: None, // TODO add timestamps?
782                });
783            compute_pass.set_pipeline(&self.ssao_pipeline);
784            let cfg = SsaoConfig {
785                image_size: [
786                    image_size.width(),
787                    image_size.height(),
788                    image.depth,
789                ],
790                radius: 0.1,
791            };
792            {
793                let mut writer = gpu
794                    .queue
795                    .write_buffer_with(
796                        &buf.ssao_config,
797                        0,
798                        (std::mem::size_of::<SsaoConfig>() as u64)
799                            .try_into()
800                            .unwrap(),
801                    )
802                    .unwrap();
803                writer.copy_from_slice(cfg.as_bytes());
804            }
805
806            let bg = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
807                label: Some("ssao bind group"),
808                layout: &self.ssao_bind_group_layout,
809                entries: &[
810                    wgpu::BindGroupEntry {
811                        binding: 0,
812                        resource: buf.ssao_config.as_entire_binding(),
813                    },
814                    wgpu::BindGroupEntry {
815                        binding: 1,
816                        resource: image.out.bind_active(),
817                    },
818                    wgpu::BindGroupEntry {
819                        binding: 2,
820                        resource: buf.raw_occlusion.bind_active(),
821                    },
822                ],
823            });
824            compute_pass.set_bind_group(0, Some(&bg), &[]);
825            compute_pass.set_bind_group(1, Some(&self.ssao_bind_group), &[]);
826            compute_pass.dispatch_workgroups(
827                image_size.width().div_ceil(8),
828                image_size.height().div_ceil(8),
829                1,
830            );
831        }
832
833        {
834            let mut compute_pass =
835                encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
836                    label: Some("ssao blur compute pass"),
837                    timestamp_writes: None, // TODO add timestamps?
838                });
839            compute_pass.set_pipeline(&self.blur_pipeline);
840            let cfg = BlurConfig {
841                image_size: [image_size.width(), image_size.height()],
842                radius: 2,
843                _pad: 0,
844            };
845            {
846                let mut writer = gpu
847                    .queue
848                    .write_buffer_with(
849                        &buf.blur_config,
850                        0,
851                        (std::mem::size_of::<BlurConfig>() as u64)
852                            .try_into()
853                            .unwrap(),
854                    )
855                    .unwrap();
856                writer.copy_from_slice(cfg.as_bytes());
857            }
858
859            let bg = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
860                label: Some("blur bind group"),
861                layout: &self.blur_bind_group_layout,
862                entries: &[
863                    wgpu::BindGroupEntry {
864                        binding: 0,
865                        resource: buf.blur_config.as_entire_binding(),
866                    },
867                    wgpu::BindGroupEntry {
868                        binding: 1,
869                        resource: buf.raw_occlusion.bind_active(),
870                    },
871                    wgpu::BindGroupEntry {
872                        binding: 2,
873                        resource: buf.blurred_occlusion.bind_active(),
874                    },
875                ],
876            });
877            compute_pass.set_bind_group(0, Some(&bg), &[]);
878            compute_pass.dispatch_workgroups(
879                image_size.width().div_ceil(8),
880                image_size.height().div_ceil(8),
881                1,
882            );
883        }
884
885        gpu.queue.submit(Some(encoder.finish()));
886        Ok(())
887    }
888}
889
890////////////////////////////////////////////////////////////////////////////////
891
892#[cfg(test)]
893mod test {
894    use super::*;
895    use fidget_core::{context::Tree, vm::VmShape};
896    use fidget_raster::voxel::RenderSize;
897
898    #[test]
899    fn packed_voxel_size() {
900        assert_eq!(std::mem::size_of::<PackedVoxel>(), 8);
901    }
902
903    #[test]
904    fn compile_shaders() {
905        for (src, desc) in [
906            (merge_shader(), "merge"),
907            (shade_shader(), "shade"),
908            (ssao_shader(), "ssao"),
909            (blur_shader(), "blur"),
910        ] {
911            crate::compile_shader(&src, desc);
912        }
913    }
914
915    /// Render a sphere-plane union and check for occlusion bias
916    ///
917    /// Because the image is perfectly symmetrical, we'd expect the average
918    /// occlusion across each of the four corners to be very similar.  If it's
919    /// not, then that's likely a sampling bias – which we have seen before!
920    #[test]
921    fn ssao_bias() {
922        // We only run in CI if we're on MacOS (because other runners don't have
923        // GPUs and will fail to build the context).
924        #[cfg(not(target_os = "macos"))]
925        if std::env::var("CI").is_ok() {
926            return;
927        }
928
929        let gpu = pollster::block_on(Gpu::init_basic()).unwrap();
930        let voxel_ctx = crate::voxel::Context::new(&gpu);
931        let effects_ctx = crate::effects::Context::new(&gpu);
932
933        let size = 128;
934        let image_size = RenderSize::from(size);
935        let mut buf = voxel_ctx.buffers(image_size).unwrap();
936        let mut merge_buf = effects_ctx.merge_buffers(size.into()).unwrap();
937
938        let (x, y, z) = Tree::axes();
939        let sphere =
940            (x.square() + y.square() + z.square()).sqrt() - Tree::constant(0.5);
941        let vm_shape = VmShape::from(sphere.min(z));
942        let shape = voxel_ctx.shape(&vm_shape).unwrap();
943
944        voxel_ctx
945            .submit(
946                &shape,
947                &mut buf,
948                None,
949                &crate::voxel::RenderConfig {
950                    world_to_model: nalgebra::Matrix4::identity(),
951                },
952            )
953            .unwrap();
954        effects_ctx
955            .submit_merge(&[buf.image_storage_buffer()], true, &mut merge_buf)
956            .unwrap();
957        let mut ssao_buf = effects_ctx.ssao_buffers(size.into()).unwrap();
958        effects_ctx.submit_ssao(&merge_buf, &mut ssao_buf).unwrap();
959        let ssao_out = gpu.read_vec::<f32>(ssao_buf.raw_occlusion().data());
960
961        let quadrants =
962            [(0, 0), (size / 2, 0), (0, size / 2), (size / 2, size / 2)];
963        let mut averages = Vec::with_capacity(quadrants.len());
964        for (dx, dy) in quadrants {
965            let mut sum = 0.0;
966            let mut count = 0.0;
967            for x in 0..size / 2 {
968                for y in 0..size / 2 {
969                    let x = (x + dx) as usize;
970                    let y = (y + dy) as usize;
971                    sum += ssao_out[x + y * size as usize];
972                    count += 1.0;
973                }
974            }
975            averages.push(sum / count);
976        }
977        for (i, qa) in quadrants.iter().enumerate() {
978            for (j, qb) in quadrants.iter().enumerate() {
979                let oa = averages[i];
980                let ob = averages[j];
981                let d = (oa - ob).abs();
982                let epsilon = 0.01;
983                if d > epsilon {
984                    panic!(
985                        "average occlusion between quadrants with offsets \
986                        {qa:?} and {qb:?} differs by too much: \
987                        {oa:.3} ≉ {ob:.3}"
988                    );
989                }
990            }
991        }
992    }
993}