Skip to main content

fidget_wgpu/voxel/effects/
mod.rs

1//! Voxel post-processing 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    CopyVarsError, Gpu, RegPipeline,
14    buf::{BufferSizeError, FlexBuffer, buffer_ro, buffer_rw, buffer_uniform},
15    color::{ColorWorkspace as GenericColorWorkspace, ShapeColorBuffers},
16    shaders, tag,
17    voxel::GeomBufferTag,
18};
19use fidget_core::{
20    render::{ImageSize, VoxelSize},
21    shape::{MissingVar, ShapeVars},
22};
23use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};
24
25/// WGPU context for applying various effects
26pub struct Context {
27    gpu: Gpu,
28
29    merge_bind_group_layout: wgpu::BindGroupLayout,
30    merge_pipeline: wgpu::ComputePipeline,
31
32    shade_bind_group_layout: wgpu::BindGroupLayout,
33    shade_pipeline: wgpu::ComputePipeline,
34
35    heightmap_bind_group_layout: wgpu::BindGroupLayout,
36    heightmap_pipeline: wgpu::ComputePipeline,
37
38    ssao_ctx: SsaoContext,
39
40    color_ctx: ColorContext,
41}
42
43/// Workspace for evaluating color expressions
44pub type ColorWorkspace = GenericColorWorkspace<ColorConfig>;
45
46const COMMON_SHADER: &str = include_str!("shaders/common.wgsl");
47const MERGE_SHADER: &str = include_str!("shaders/merge.wgsl");
48const SHADE_SHADER: &str = include_str!("shaders/shade.wgsl");
49const HEIGHTMAP_SHADER: &str = include_str!("shaders/heightmap.wgsl");
50const SSAO_SHADER: &str = include_str!("shaders/ssao.wgsl");
51const BLUR_SHADER: &str = include_str!("shaders/blur.wgsl");
52const COLOR_SHADER: &str = include_str!("shaders/color.wgsl");
53
54fn merge_shader() -> String {
55    MERGE_SHADER.to_owned() + COMMON_SHADER + shaders::COMMON
56}
57
58fn shade_shader() -> String {
59    SHADE_SHADER.to_owned() + COMMON_SHADER + shaders::COMMON
60}
61
62fn heightmap_shader() -> String {
63    HEIGHTMAP_SHADER.to_owned() + COMMON_SHADER + shaders::COMMON
64}
65
66fn ssao_shader() -> String {
67    SSAO_SHADER.to_owned() + COMMON_SHADER + shaders::COMMON
68}
69
70fn blur_shader() -> String {
71    BLUR_SHADER.to_owned() + COMMON_SHADER + shaders::COMMON
72}
73
74fn color_shader(reg_count: u8) -> String {
75    let mut shader_code = shaders::opcode_constants();
76    shader_code += &format!("const REG_COUNT: u32 = {reg_count};");
77    shader_code
78        + COLOR_SHADER
79        + COMMON_SHADER
80        + super::TRANSFORM_INPUT
81        + shaders::COMMON
82        + shaders::TAPE_INTERPRETER
83        + shaders::DUMMY_STACK
84        + shaders::FLOAT_OPS
85}
86
87/// Packed voxel structure used on the GPU
88#[derive(Copy, Clone, FromBytes, Immutable, IntoBytes, KnownLayout)]
89#[repr(C)]
90pub struct PackedVoxel {
91    /// XY components of the normal (normalized to a length of 127)
92    ///
93    /// The Z component is implied and positive
94    ///
95    /// An invalid normal is represented by `[-128, -128]`.
96    pub normal: [i8; 2],
97
98    /// Shape index
99    pub index: u16,
100
101    /// Depth of the voxel
102    ///
103    /// If this is 0, then the voxel is not populated
104    pub z: u32,
105}
106
107/// Configuration for the color evaluation pass
108///
109/// This is quietly public because it's part of a public API, but is unlikely to
110/// be useful for end-users of the library.
111#[derive(Copy, Clone, FromBytes, Immutable, IntoBytes, KnownLayout)]
112#[cfg_attr(test, derive(facet::Facet))]
113#[repr(C)]
114#[doc(hidden)]
115pub struct ColorConfig {
116    /// Screen-to-model transform matrix
117    mat: [f32; 16],
118
119    /// Input index of X, Y, Z axes
120    ///
121    /// `u32::MAX` is used as a marker if an axis is unused
122    axes: [u32; 3],
123
124    // padding for alignment
125    _pad: u32,
126
127    /// Image size (pixels)
128    image_size: [u32; 2],
129    // this is followed by a tape_data flexible array member
130}
131
132#[derive(Copy, Clone, FromBytes, Immutable, IntoBytes, KnownLayout)]
133#[cfg_attr(test, derive(facet::Facet))]
134#[repr(C)]
135pub(crate) struct MergeConfig {
136    /// Image size, in pixels
137    pub image_size: [u32; 2],
138
139    /// Whether or not to denoise when merging (non-zero is true)
140    pub denoise: u32,
141
142    /// Scale to compensate for z-flattening
143    pub z_scale: f32,
144
145    /// Offset applied to indices when merging
146    ///
147    /// When this is 0, we initialize the output image
148    pub index_base: u32,
149
150    // Padding to next multiple of 8 bytes
151    _pad: u32,
152}
153
154#[derive(Copy, Clone, FromBytes, Immutable, IntoBytes, KnownLayout)]
155#[cfg_attr(test, derive(facet::Facet))]
156#[repr(C)]
157struct ShadeConfig {
158    /// Image size, in pixels
159    image_size: [u32; 3],
160
161    /// Flags to set the presence of color and SSAO
162    flags: u32,
163}
164
165#[derive(Copy, Clone, FromBytes, Immutable, IntoBytes, KnownLayout)]
166#[cfg_attr(test, derive(facet::Facet))]
167#[repr(C)]
168struct HeightmapConfig {
169    /// Image size, in pixels
170    image_size: [u32; 3],
171
172    /// Flag to indicate whether the color is valid
173    has_color: u32,
174}
175
176/// Settings for image merging pass
177#[derive(Copy, Clone, Debug)]
178pub struct MergeSettings {
179    /// Denoise normals (replacing backwards-facing normals with a neighbor)
180    pub denoise: bool,
181
182    /// Non-uniform Z scaling applied to the model
183    ///
184    /// The Z normal is *divided* by this value when merging images, to cancel
185    /// out the effect of the scaling.
186    pub z_scale: f32,
187}
188
189impl Default for MergeSettings {
190    fn default() -> Self {
191        Self {
192            denoise: true,
193            z_scale: 1.0,
194        }
195    }
196}
197
198/// Must match constants in `shade.wgsl`
199const SHADE_CONFIG_HAS_SSAO: u32 = 1u32;
200const SHADE_CONFIG_HAS_COLOR: u32 = 2u32;
201
202tag!(pub MergeVoxelBufferTag, PackedVoxel, VoxelSize, STORAGE | COPY_SRC,
203    "Buffer tag for on-GPU merged ([`PackedVoxel`]) images"
204);
205
206/// Handle to a set of buffers used when merging images
207pub struct MergeWorkspace {
208    config: wgpu::Buffer,
209    out: FlexBuffer<MergeVoxelBufferTag>,
210    image_count: usize,
211}
212
213impl MergeWorkspace {
214    /// Returns a handle to the output buffer
215    pub fn output(&self) -> &FlexBuffer<MergeVoxelBufferTag> {
216        &self.out
217    }
218
219    /// Resets the merge buffer
220    ///
221    /// The next call to [`Context::submit_merge`] will clear the buffer and
222    /// begin accumulating from scratch.
223    pub fn reset(&mut self) {
224        self.image_count = 0;
225    }
226}
227
228tag!(
229    pub ShadedImageTag, u32, VoxelSize, STORAGE | COPY_SRC,
230    "Buffer tag for on-GPU shaded (RGBA) images"
231);
232
233/// Handle to a set of buffers used when shading images
234pub struct ShadeWorkspace {
235    config: wgpu::Buffer,
236    has_color: bool,
237    out: FlexBuffer<ShadedImageTag>,
238}
239
240impl ShadeWorkspace {
241    /// Returns a reference to the output buffer
242    pub fn output(&self) -> &FlexBuffer<ShadedImageTag> {
243        &self.out
244    }
245}
246
247/// Error returned when submitting a merge operation
248#[derive(Debug, thiserror::Error)]
249pub enum MergeError {
250    /// Image sizes in the slice are not consistent
251    #[error(transparent)]
252    ImageSizeMismatch(#[from] ImageSizeMismatch),
253
254    /// An error occurred while resizing the output buffer
255    #[error(transparent)]
256    OutputSize(BufferSizeError),
257}
258
259/// Error returned when submitting a shade operation
260#[derive(Debug, thiserror::Error)]
261pub enum ShadeError {
262    /// An error occurred while resizing the output buffer
263    #[error(transparent)]
264    OutputSize(BufferSizeError),
265
266    /// Input and output buffers are different sizes when `has_color` is true
267    ///
268    /// This is not allowed because `has_color = true` means that the output
269    /// buffer's pixel values should be used as diffuse color
270    #[error(
271        "input and output buffers must be the same size when
272        `has_color` is true, but they do not match"
273    )]
274    InvalidColorSize,
275}
276
277/// Error returned when submitting a heightmap operation
278#[derive(Debug, thiserror::Error)]
279pub enum HeightmapError {
280    /// An error occurred while resizing the output buffer
281    #[error(transparent)]
282    OutputSize(BufferSizeError),
283
284    /// Input and output buffers are different sizes when `has_color` is true
285    ///
286    /// This is not allowed because `has_color = true` means that the output
287    /// buffer's pixel values should be used as diffuse color
288    #[error(
289        "input and output buffers must be the same size when
290        `has_color` is true, but they do not match"
291    )]
292    InvalidColorSize,
293}
294
295/// Error returned when submitting an SSAO operation
296#[derive(Debug, thiserror::Error)]
297pub enum SsaoError {
298    /// An error occurred while resizing the output buffer
299    #[error(transparent)]
300    OutputSize(BufferSizeError),
301}
302
303/// Error returned when submitting a color evaluation operation
304#[derive(Debug, thiserror::Error)]
305pub enum ColorError {
306    /// An error occurred while resizing the output buffer
307    #[error("could not resize output buffer")]
308    OutputSize(#[source] BufferSizeError),
309
310    /// An error occurred while resizing the vars buffer
311    #[error("could not resize vars buffer")]
312    VarBufferSize(#[source] BufferSizeError),
313
314    /// An error occurred while resizing the config buffer
315    #[error("could not resize config buffer")]
316    ConfigBufferSize(#[source] BufferSizeError),
317
318    /// A variable is missing from the map
319    #[error(transparent)]
320    MissingVar(#[from] MissingVar),
321
322    /// Wrong number of shapes
323    #[error(
324        "merged image is built from {merge_count} shapes,
325         but shape color buffer expected {shape_count} shapes"
326    )]
327    BadShapeCount {
328        /// Number of shapes in the merged image
329        merge_count: usize,
330        /// Number of shapes in the color buffer
331        shape_count: usize,
332    },
333
334    /// Shape color has already been populated
335    #[error("shape color has already been populated")]
336    AlreadyHasColor,
337}
338
339/// Type indicating an image size mismatch
340#[derive(Debug, thiserror::Error)]
341#[error(
342    "image size mismatch: expected {} × {} × {}, got {} × {} × {}",
343    expected.width(), expected.height(), expected.depth(),
344    actual.width(),   actual.height(),   actual.depth()
345)]
346pub struct ImageSizeMismatch {
347    expected: VoxelSize,
348    actual: VoxelSize,
349}
350
351impl Context {
352    /// Builds a new context for applying effects
353    pub fn new(gpu: &Gpu) -> Self {
354        let merge_bind_group_layout = gpu.device.create_bind_group_layout(
355            &wgpu::BindGroupLayoutDescriptor {
356                label: None,
357                entries: &[
358                    buffer_uniform(0), // config
359                    buffer_ro(1),      // image
360                    buffer_rw(2),      // out
361                ],
362            },
363        );
364        let shader_code = merge_shader();
365        let pipeline_layout = gpu.device.create_pipeline_layout(
366            &wgpu::PipelineLayoutDescriptor {
367                label: Some("effects merge pipeline"),
368                bind_group_layouts: &[Some(&merge_bind_group_layout)],
369                immediate_size: 0u32,
370            },
371        );
372        let shader_module =
373            gpu.device
374                .create_shader_module(wgpu::ShaderModuleDescriptor {
375                    label: Some("effects merge shader module"),
376                    source: wgpu::ShaderSource::Wgsl(shader_code.into()),
377                });
378        let merge_pipeline = gpu.device.create_compute_pipeline(
379            &wgpu::ComputePipelineDescriptor {
380                label: Some("effects merge compute pipeline"),
381                layout: Some(&pipeline_layout),
382                module: &shader_module,
383                entry_point: Some("merge_main"),
384                compilation_options: Default::default(),
385                cache: None,
386            },
387        );
388
389        let shade_bind_group_layout = gpu.device.create_bind_group_layout(
390            &wgpu::BindGroupLayoutDescriptor {
391                label: None,
392                entries: &[
393                    buffer_uniform(0),
394                    buffer_ro(1), // image
395                    buffer_ro(2), // ssao occlusion
396                    buffer_rw(3), // out
397                ],
398            },
399        );
400        let shader_code = shade_shader();
401        let pipeline_layout = gpu.device.create_pipeline_layout(
402            &wgpu::PipelineLayoutDescriptor {
403                label: Some("effects shade pipeline"),
404                bind_group_layouts: &[Some(&shade_bind_group_layout)],
405                immediate_size: 0u32,
406            },
407        );
408        let shader_module =
409            gpu.device
410                .create_shader_module(wgpu::ShaderModuleDescriptor {
411                    label: Some("effects shade shader module"),
412                    source: wgpu::ShaderSource::Wgsl(shader_code.into()),
413                });
414        let shade_pipeline = gpu.device.create_compute_pipeline(
415            &wgpu::ComputePipelineDescriptor {
416                label: Some("effects shade compute pipeline"),
417                layout: Some(&pipeline_layout),
418                module: &shader_module,
419                entry_point: Some("shade_main"),
420                compilation_options: Default::default(),
421                cache: None,
422            },
423        );
424
425        let heightmap_bind_group_layout = gpu.device.create_bind_group_layout(
426            &wgpu::BindGroupLayoutDescriptor {
427                label: None,
428                entries: &[
429                    buffer_uniform(0),
430                    buffer_ro(1), // merged image
431                    buffer_rw(2), // out
432                ],
433            },
434        );
435        let shader_code = heightmap_shader();
436        let pipeline_layout = gpu.device.create_pipeline_layout(
437            &wgpu::PipelineLayoutDescriptor {
438                label: Some("effects heightmap pipeline"),
439                bind_group_layouts: &[Some(&heightmap_bind_group_layout)],
440                immediate_size: 0u32,
441            },
442        );
443        let shader_module =
444            gpu.device
445                .create_shader_module(wgpu::ShaderModuleDescriptor {
446                    label: Some("effects heightmap shader module"),
447                    source: wgpu::ShaderSource::Wgsl(shader_code.into()),
448                });
449        let heightmap_pipeline = gpu.device.create_compute_pipeline(
450            &wgpu::ComputePipelineDescriptor {
451                label: Some("effects heightmap compute pipeline"),
452                layout: Some(&pipeline_layout),
453                module: &shader_module,
454                entry_point: Some("heightmap_main"),
455                compilation_options: Default::default(),
456                cache: None,
457            },
458        );
459
460        let ssao_ctx = SsaoContext::new(&gpu.device);
461        let color_ctx = ColorContext::new(&gpu.device);
462
463        Self {
464            gpu: gpu.clone(),
465            merge_bind_group_layout,
466            merge_pipeline,
467            shade_bind_group_layout,
468            shade_pipeline,
469            heightmap_bind_group_layout,
470            heightmap_pipeline,
471            ssao_ctx,
472            color_ctx,
473        }
474    }
475
476    /// Builds a new set of [`MergeWorkspace`]
477    ///
478    /// These will be resized when first used (in
479    /// [`submit_merge`](Self::submit_merge))
480    pub fn merge_workspace(&self) -> MergeWorkspace {
481        let config = self.gpu.device.create_buffer(&wgpu::BufferDescriptor {
482            label: Some("config"),
483            size: std::mem::size_of::<MergeConfig>() as u64,
484            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
485            mapped_at_creation: false,
486        });
487        let out = FlexBuffer::new(&self.gpu.device, "merge output", 64.into())
488            .expect("64 is always a valid size");
489        MergeWorkspace {
490            config,
491            out,
492            image_count: 0,
493        }
494    }
495
496    /// Builds a new set of [`ShadeWorkspace`]
497    ///
498    /// These will be resized when first used (in
499    /// either [`submit_color`](Self::submit_color) or
500    /// [`submit_shade`](Self::submit_shade))
501    pub fn shade_workspace(&self) -> ShadeWorkspace {
502        let config = self.gpu.device.create_buffer(&wgpu::BufferDescriptor {
503            label: Some("shade config"),
504            size: std::mem::size_of::<ShadeConfig>() as u64,
505            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
506            mapped_at_creation: false,
507        });
508        let out = FlexBuffer::new(&self.gpu.device, "shade output", 64.into())
509            .expect("64 is always a valid size");
510        ShadeWorkspace {
511            config,
512            has_color: false,
513            out,
514        }
515    }
516
517    /// Builds a new set of [`SsaoWorkspace`]
518    ///
519    /// These will be resized when first used (in
520    /// [`submit_ssao`](Self::submit_ssao))
521    pub fn ssao_workspace(&self) -> SsaoWorkspace {
522        let ssao_config =
523            self.gpu.device.create_buffer(&wgpu::BufferDescriptor {
524                label: Some("ssao config"),
525                size: std::mem::size_of::<SsaoConfig>() as u64,
526                usage: wgpu::BufferUsages::UNIFORM
527                    | wgpu::BufferUsages::COPY_DST,
528                mapped_at_creation: false,
529            });
530        let image_size = 64.into();
531        let raw_occlusion =
532            FlexBuffer::new(&self.gpu.device, "ssao raw occlusion", image_size)
533                .expect("64 is always a valid size");
534        let blur_config =
535            self.gpu.device.create_buffer(&wgpu::BufferDescriptor {
536                label: Some("blur config"),
537                size: std::mem::size_of::<BlurConfig>() as u64,
538                usage: wgpu::BufferUsages::UNIFORM
539                    | wgpu::BufferUsages::COPY_DST,
540                mapped_at_creation: false,
541            });
542        let blurred_occlusion = FlexBuffer::new(
543            &self.gpu.device,
544            "ssao blurred occlusion",
545            image_size,
546        )
547        .expect("64 is always a valid size");
548        SsaoWorkspace {
549            ssao_config,
550            blur_config,
551            raw_occlusion,
552            blurred_occlusion,
553        }
554    }
555
556    /// Accumulates an image into a merged image buffer
557    ///
558    /// [`MergeWorkspace::reset`] should be called before the first call to
559    /// `submit_merge`. For the first merge after a reset, the output buffer is
560    /// resized to fit the images; subsequent merges must be of the same size.
561    pub fn submit_merge(
562        &self,
563        image: &FlexBuffer<GeomBufferTag>,
564        settings: MergeSettings,
565        buf: &mut MergeWorkspace,
566    ) -> Result<(), MergeError> {
567        let size = image.size();
568        if buf.image_count == 0 {
569            buf.out
570                .grow_to_fit(&self.gpu.device, size)
571                .map_err(MergeError::OutputSize)?;
572        } else if buf.out.size() != size {
573            return Err(ImageSizeMismatch {
574                expected: size,
575                actual: buf.out.size(),
576            }
577            .into());
578        }
579        let mut encoder = self.gpu.device.create_command_encoder(
580            &wgpu::CommandEncoderDescriptor {
581                label: Some("merge compute encoder"),
582            },
583        );
584        // Scope to bound the lifetime of compute_pass
585        {
586            let mut compute_pass =
587                encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
588                    label: Some("merge compute pass"),
589                    timestamp_writes: None, // TODO add timestamps?
590                });
591            compute_pass.set_pipeline(&self.merge_pipeline);
592            let cfg = MergeConfig {
593                image_size: [size.width(), size.height()],
594                denoise: settings.denoise as u32,
595                index_base: buf.image_count as u32,
596                z_scale: settings.z_scale,
597                _pad: 0,
598            };
599            {
600                let mut writer = self
601                    .gpu
602                    .queue
603                    .write_buffer_with(
604                        &buf.config,
605                        0,
606                        (std::mem::size_of::<MergeConfig>() as u64)
607                            .try_into()
608                            .unwrap(),
609                    )
610                    .unwrap();
611                writer.copy_from_slice(cfg.as_bytes());
612            }
613
614            let bg =
615                self.gpu
616                    .device
617                    .create_bind_group(&wgpu::BindGroupDescriptor {
618                        label: Some("merge bind group"),
619                        layout: &self.merge_bind_group_layout,
620                        entries: &[
621                            wgpu::BindGroupEntry {
622                                binding: 0,
623                                resource: buf.config.as_entire_binding(),
624                            },
625                            wgpu::BindGroupEntry {
626                                binding: 1,
627                                resource: image.bind_active(),
628                            },
629                            wgpu::BindGroupEntry {
630                                binding: 2,
631                                resource: buf.out.bind_active(),
632                            },
633                        ],
634                    });
635            compute_pass.set_bind_group(0, Some(&bg), &[]);
636            compute_pass.dispatch_workgroups(
637                size.width().div_ceil(8),
638                size.height().div_ceil(8),
639                1,
640            );
641            buf.image_count += 1;
642        }
643        self.gpu.queue.submit(Some(encoder.finish()));
644        Ok(())
645    }
646
647    /// Submits an operation to shade an image
648    ///
649    /// The output buffer is resized to fit the incoming image
650    pub fn submit_shade(
651        &self,
652        image: &MergeWorkspace,
653        ssao: Option<&SsaoWorkspace>,
654        buf: &mut ShadeWorkspace,
655    ) -> Result<(), ShadeError> {
656        let size = image.out.size();
657        if buf.has_color {
658            if size != buf.out.size() {
659                return Err(ShadeError::InvalidColorSize);
660            }
661        } else {
662            buf.out
663                .grow_to_fit(&self.gpu.device, size)
664                .map_err(ShadeError::OutputSize)?;
665        }
666        let mut encoder = self.gpu.device.create_command_encoder(
667            &wgpu::CommandEncoderDescriptor {
668                label: Some("shade compute encoder"),
669            },
670        );
671
672        // Scope to bound the lifetime of compute_pass
673        {
674            let mut compute_pass =
675                encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
676                    label: Some("shade compute pass"),
677                    timestamp_writes: None, // TODO add timestamps?
678                });
679            compute_pass.set_pipeline(&self.shade_pipeline);
680            let cfg = ShadeConfig {
681                image_size: [size.width(), size.height(), size.depth()],
682                flags: if ssao.is_some() {
683                    SHADE_CONFIG_HAS_SSAO
684                } else {
685                    0
686                } | if buf.has_color {
687                    SHADE_CONFIG_HAS_COLOR
688                } else {
689                    0
690                },
691            };
692            {
693                let mut writer = self
694                    .gpu
695                    .queue
696                    .write_buffer_with(
697                        &buf.config,
698                        0,
699                        buf.config.size().try_into().unwrap(),
700                    )
701                    .unwrap();
702                writer.copy_from_slice(cfg.as_bytes());
703            }
704            // TODO This is created on every pass
705            let bg =
706                self.gpu
707                    .device
708                    .create_bind_group(&wgpu::BindGroupDescriptor {
709                        label: Some("shade bind group"),
710                        layout: &self.shade_bind_group_layout,
711                        entries: &[
712                            wgpu::BindGroupEntry {
713                                binding: 0,
714                                resource: buf.config.as_entire_binding(),
715                            },
716                            wgpu::BindGroupEntry {
717                                binding: 1,
718                                resource: image.out.bind_active(),
719                            },
720                            wgpu::BindGroupEntry {
721                                binding: 2,
722                                resource: ssao
723                                    .map(|s| {
724                                        s.blurred_occlusion().bind_active()
725                                    })
726                                    .unwrap_or_else(|| image.out.bind_active()),
727                            },
728                            wgpu::BindGroupEntry {
729                                binding: 3,
730                                resource: buf.out.bind_active(),
731                            },
732                        ],
733                    });
734            compute_pass.set_bind_group(0, Some(&bg), &[]);
735            compute_pass.dispatch_workgroups(
736                size.width().div_ceil(8),
737                size.height().div_ceil(8),
738                1,
739            );
740        }
741        buf.has_color = false;
742        self.gpu.queue.submit(Some(encoder.finish()));
743        Ok(())
744    }
745
746    /// Submits an operation to generate RGB color from a heightmap
747    ///
748    /// The output buffer is resized to fit the incoming image
749    pub fn submit_heightmap(
750        &self,
751        image: &MergeWorkspace,
752        buf: &mut ShadeWorkspace,
753    ) -> Result<(), HeightmapError> {
754        let size = image.out.size();
755        if buf.has_color {
756            if size != buf.out.size() {
757                return Err(HeightmapError::InvalidColorSize);
758            }
759        } else {
760            buf.out
761                .grow_to_fit(&self.gpu.device, size)
762                .map_err(HeightmapError::OutputSize)?;
763        }
764        let mut encoder = self.gpu.device.create_command_encoder(
765            &wgpu::CommandEncoderDescriptor {
766                label: Some("heightmap compute encoder"),
767            },
768        );
769
770        // Scope to bound the lifetime of compute_pass
771        {
772            let mut compute_pass =
773                encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
774                    label: Some("heightmap compute pass"),
775                    timestamp_writes: None, // TODO add timestamps?
776                });
777            compute_pass.set_pipeline(&self.heightmap_pipeline);
778            let cfg = HeightmapConfig {
779                image_size: [size.width(), size.height(), size.depth()],
780                has_color: buf.has_color.into(),
781            };
782            {
783                let mut writer = self
784                    .gpu
785                    .queue
786                    .write_buffer_with(
787                        &buf.config,
788                        0,
789                        buf.config.size().try_into().unwrap(),
790                    )
791                    .unwrap();
792                writer.copy_from_slice(cfg.as_bytes());
793            }
794            // TODO This is created on every pass
795            let bg =
796                self.gpu
797                    .device
798                    .create_bind_group(&wgpu::BindGroupDescriptor {
799                        label: Some("heightmap bind group"),
800                        layout: &self.heightmap_bind_group_layout,
801                        entries: &[
802                            wgpu::BindGroupEntry {
803                                binding: 0,
804                                resource: buf.config.as_entire_binding(),
805                            },
806                            wgpu::BindGroupEntry {
807                                binding: 1,
808                                resource: image.out.bind_active(),
809                            },
810                            wgpu::BindGroupEntry {
811                                binding: 2,
812                                resource: buf.out.bind_active(),
813                            },
814                        ],
815                    });
816            compute_pass.set_bind_group(0, Some(&bg), &[]);
817            compute_pass.dispatch_workgroups(
818                size.width().div_ceil(8),
819                size.height().div_ceil(8),
820                1,
821            );
822        }
823        buf.has_color = false;
824        self.gpu.queue.submit(Some(encoder.finish()));
825        Ok(())
826    }
827
828    /// Submits a pass to compute an SSAO buffer
829    pub fn submit_ssao(
830        &self,
831        image: &MergeWorkspace,
832        buf: &mut SsaoWorkspace,
833    ) -> Result<(), SsaoError> {
834        self.ssao_ctx.submit(image, buf, &self.gpu)
835    }
836
837    /// Submits a color evaluation pass
838    ///
839    /// Image size is set from the `MergeWorkspace`; the transform matrix is
840    /// provided separately (but should be the same one used for image
841    /// evaluation).
842    pub fn submit_color(
843        &self,
844        merge: &MergeWorkspace,
845        world_to_model: &nalgebra::Matrix4<f32>,
846        shape: &ShapeColorBuffers,
847        bufs: &mut ColorWorkspace,
848        out: &mut ShadeWorkspace,
849    ) -> Result<(), ColorError> {
850        self.submit_color_with_vars(
851            merge,
852            world_to_model,
853            shape,
854            bufs,
855            &Default::default(),
856            out,
857        )
858    }
859
860    /// Submits a color evaluation pass with auxiliary variables
861    ///
862    /// Image size is set from the `MergeWorkspace`; the transform matrix is
863    /// provided separately (but should be the same one used for image
864    /// evaluation).
865    pub fn submit_color_with_vars(
866        &self,
867        merge: &MergeWorkspace,
868        world_to_model: &nalgebra::Matrix4<f32>,
869        shape: &ShapeColorBuffers,
870        bufs: &mut ColorWorkspace,
871        vars: &ShapeVars<f32>,
872        out: &mut ShadeWorkspace,
873    ) -> Result<(), ColorError> {
874        self.color_ctx.submit(
875            merge,
876            world_to_model,
877            shape,
878            bufs,
879            vars,
880            out,
881            &self.gpu,
882        )
883    }
884
885    /// Returns a new workspace for color evaluation
886    pub fn color_workspace(&self) -> ColorWorkspace {
887        ColorWorkspace::new(&self.gpu.device)
888    }
889}
890
891////////////////////////////////////////////////////////////////////////////////
892
893tag!(pub SsaoRawBufferTag, f32, ImageSize, STORAGE | COPY_SRC,
894    "Tag for a raw SSAO occlusion buffer");
895tag!(pub SsaoBlurredBufferTag, f32, ImageSize, STORAGE | COPY_SRC,
896    "Tag for a blurred SSAO occlusion buffer");
897
898/// Handle to a set of buffers used when running an SSAO pass
899pub struct SsaoWorkspace {
900    ssao_config: wgpu::Buffer, // TODO add a `ConfigBuffer` type?
901    raw_occlusion: FlexBuffer<SsaoRawBufferTag>,
902
903    blur_config: wgpu::Buffer,
904    blurred_occlusion: FlexBuffer<SsaoBlurredBufferTag>,
905}
906
907impl SsaoWorkspace {
908    /// Returns a shared handle to the raw SSAO occlusion buffer
909    pub fn raw_occlusion(&self) -> &FlexBuffer<SsaoRawBufferTag> {
910        &self.raw_occlusion
911    }
912
913    /// Returns a shared handle to the blurred SSAO occlusion buffer
914    pub fn blurred_occlusion(&self) -> &FlexBuffer<SsaoBlurredBufferTag> {
915        &self.blurred_occlusion
916    }
917}
918
919#[derive(Copy, Clone, FromBytes, Immutable, IntoBytes, KnownLayout)]
920#[cfg_attr(test, derive(facet::Facet))]
921#[repr(C)]
922struct SsaoConfig {
923    /// Image size, in voxels
924    image_size: [u32; 3],
925
926    /// Radius of SSAO sampling
927    radius: f32,
928}
929
930#[derive(Copy, Clone, FromBytes, Immutable, IntoBytes, KnownLayout)]
931#[cfg_attr(test, derive(facet::Facet))]
932#[repr(C)]
933struct BlurConfig {
934    /// Image size, in pixels
935    image_size: [u32; 2],
936
937    /// Pixel radius of blur
938    radius: u32,
939
940    /// Padding to 16 bytes
941    _pad: u32,
942}
943
944struct SsaoContext {
945    /// Fixed bind group for SSAO pass
946    ///
947    /// This contains the SSAO kernel and noise buffers, which are constants.
948    ssao_bind_group: wgpu::BindGroup,
949
950    /// Layout for bind group that accepts buffers from the user
951    ssao_bind_group_layout: wgpu::BindGroupLayout,
952
953    /// Pipeline for computing per-pixel SSAO
954    ssao_pipeline: wgpu::ComputePipeline,
955
956    /// Layout for blur pipeline
957    blur_bind_group_layout: wgpu::BindGroupLayout,
958
959    /// Pipeline for blurring an SSAO image
960    blur_pipeline: wgpu::ComputePipeline,
961}
962
963impl SsaoContext {
964    pub fn new(device: &wgpu::Device) -> Self {
965        let ssao_fixed_bind_group_layout =
966            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
967                label: None,
968                entries: &[buffer_ro(0), buffer_ro(1)],
969            });
970        let ssao_user_bind_group_layout =
971            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
972                label: None,
973                entries: &[buffer_uniform(0), buffer_ro(1), buffer_rw(2)],
974            });
975
976        const KERNEL_SIZE: usize = 64;
977        const NOISE_SIZE: usize = 16;
978
979        // Build constant buffers and their bind group
980        let ssao_kernel_size_bytes =
981            KERNEL_SIZE * std::mem::size_of::<[f32; 3]>();
982        let ssao_kernel = device.create_buffer(&wgpu::BufferDescriptor {
983            label: Some("ssao kernel"),
984            size: ssao_kernel_size_bytes as u64,
985            usage: wgpu::BufferUsages::STORAGE,
986            mapped_at_creation: true,
987        });
988        let ssao_kernel_values =
989            fidget_raster::effects::ssao_kernel(KERNEL_SIZE);
990        ssao_kernel
991            .get_mapped_range_mut(0..ssao_kernel_size_bytes as u64)
992            .unwrap()
993            .copy_from_slice(ssao_kernel_values.as_slice().as_bytes());
994        ssao_kernel.unmap();
995
996        let ssao_noise_size_bytes =
997            NOISE_SIZE * std::mem::size_of::<[f32; 2]>();
998        let ssao_noise = device.create_buffer(&wgpu::BufferDescriptor {
999            label: Some("ssao noise"),
1000            size: ssao_noise_size_bytes as u64,
1001            usage: wgpu::BufferUsages::STORAGE,
1002            mapped_at_creation: true,
1003        });
1004        let ssao_noise_values = fidget_raster::effects::ssao_noise(NOISE_SIZE);
1005        ssao_noise
1006            .get_mapped_range_mut(0..ssao_noise_size_bytes as u64)
1007            .unwrap()
1008            .copy_from_slice(ssao_noise_values.as_slice().as_bytes());
1009        ssao_noise.unmap();
1010
1011        let ssao_bind_group =
1012            device.create_bind_group(&wgpu::BindGroupDescriptor {
1013                label: Some("ssao fixed bind group"),
1014                layout: &ssao_fixed_bind_group_layout,
1015                entries: &[
1016                    wgpu::BindGroupEntry {
1017                        binding: 0,
1018                        resource: ssao_kernel.as_entire_binding(),
1019                    },
1020                    wgpu::BindGroupEntry {
1021                        binding: 1,
1022                        resource: ssao_noise.as_entire_binding(),
1023                    },
1024                ],
1025            });
1026
1027        let shader_code = ssao_shader();
1028        let pipeline_layout =
1029            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1030                label: Some("effects ssao pipeline"),
1031                bind_group_layouts: &[
1032                    Some(&ssao_user_bind_group_layout),
1033                    Some(&ssao_fixed_bind_group_layout),
1034                ],
1035                immediate_size: 0u32,
1036            });
1037        let shader_module =
1038            device.create_shader_module(wgpu::ShaderModuleDescriptor {
1039                label: Some("effects ssao shader module"),
1040                source: wgpu::ShaderSource::Wgsl(shader_code.into()),
1041            });
1042        let ssao_pipeline =
1043            device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
1044                label: Some("effects ssao compute pipeline"),
1045                layout: Some(&pipeline_layout),
1046                module: &shader_module,
1047                entry_point: Some("ssao_main"),
1048                compilation_options: Default::default(),
1049                cache: None,
1050            });
1051
1052        let blur_bind_group_layout =
1053            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1054                label: None,
1055                entries: &[buffer_uniform(0), buffer_ro(1), buffer_rw(2)],
1056            });
1057        let shader_code = blur_shader();
1058        let pipeline_layout =
1059            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1060                label: Some("effects blur pipeline"),
1061                bind_group_layouts: &[Some(&blur_bind_group_layout)],
1062                immediate_size: 0u32,
1063            });
1064        let shader_module =
1065            device.create_shader_module(wgpu::ShaderModuleDescriptor {
1066                label: Some("effects blur shader module"),
1067                source: wgpu::ShaderSource::Wgsl(shader_code.into()),
1068            });
1069        let blur_pipeline =
1070            device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
1071                label: Some("effects blur compute pipeline"),
1072                layout: Some(&pipeline_layout),
1073                module: &shader_module,
1074                entry_point: Some("blur_main"),
1075                compilation_options: Default::default(),
1076                cache: None,
1077            });
1078
1079        Self {
1080            ssao_bind_group,
1081            ssao_bind_group_layout: ssao_user_bind_group_layout,
1082            ssao_pipeline,
1083            blur_bind_group_layout,
1084            blur_pipeline,
1085        }
1086    }
1087
1088    fn submit(
1089        &self,
1090        image: &MergeWorkspace,
1091        buf: &mut SsaoWorkspace,
1092        gpu: &Gpu,
1093    ) -> Result<(), SsaoError> {
1094        let image_size = image.out.size();
1095        buf.raw_occlusion
1096            .grow_to_fit(&gpu.device, image_size.into())
1097            .map_err(SsaoError::OutputSize)?;
1098        buf.blurred_occlusion
1099            .grow_to_fit(&gpu.device, image_size.into())
1100            .map_err(SsaoError::OutputSize)?;
1101
1102        // TODO make this passed in?
1103        let mut encoder = gpu.device.create_command_encoder(
1104            &wgpu::CommandEncoderDescriptor {
1105                label: Some("ssao command encoder"),
1106            },
1107        );
1108
1109        // Scope to bound the lifetime of compute_pass
1110        {
1111            let mut compute_pass =
1112                encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
1113                    label: Some("ssao compute pass"),
1114                    timestamp_writes: None, // TODO add timestamps?
1115                });
1116            compute_pass.set_pipeline(&self.ssao_pipeline);
1117            let cfg = SsaoConfig {
1118                image_size: [
1119                    image_size.width(),
1120                    image_size.height(),
1121                    image_size.depth(),
1122                ],
1123                radius: 0.1,
1124            };
1125            {
1126                let mut writer = gpu
1127                    .queue
1128                    .write_buffer_with(
1129                        &buf.ssao_config,
1130                        0,
1131                        (std::mem::size_of::<SsaoConfig>() as u64)
1132                            .try_into()
1133                            .unwrap(),
1134                    )
1135                    .unwrap();
1136                writer.copy_from_slice(cfg.as_bytes());
1137            }
1138
1139            let bg = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
1140                label: Some("ssao bind group"),
1141                layout: &self.ssao_bind_group_layout,
1142                entries: &[
1143                    wgpu::BindGroupEntry {
1144                        binding: 0,
1145                        resource: buf.ssao_config.as_entire_binding(),
1146                    },
1147                    wgpu::BindGroupEntry {
1148                        binding: 1,
1149                        resource: image.out.bind_active(),
1150                    },
1151                    wgpu::BindGroupEntry {
1152                        binding: 2,
1153                        resource: buf.raw_occlusion.bind_active(),
1154                    },
1155                ],
1156            });
1157            compute_pass.set_bind_group(0, Some(&bg), &[]);
1158            compute_pass.set_bind_group(1, Some(&self.ssao_bind_group), &[]);
1159            compute_pass.dispatch_workgroups(
1160                image_size.width().div_ceil(8),
1161                image_size.height().div_ceil(8),
1162                1,
1163            );
1164        }
1165
1166        {
1167            let mut compute_pass =
1168                encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
1169                    label: Some("ssao blur compute pass"),
1170                    timestamp_writes: None, // TODO add timestamps?
1171                });
1172            compute_pass.set_pipeline(&self.blur_pipeline);
1173            let cfg = BlurConfig {
1174                image_size: [image_size.width(), image_size.height()],
1175                radius: 2,
1176                _pad: 0,
1177            };
1178            {
1179                let mut writer = gpu
1180                    .queue
1181                    .write_buffer_with(
1182                        &buf.blur_config,
1183                        0,
1184                        (std::mem::size_of::<BlurConfig>() as u64)
1185                            .try_into()
1186                            .unwrap(),
1187                    )
1188                    .unwrap();
1189                writer.copy_from_slice(cfg.as_bytes());
1190            }
1191
1192            let bg = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
1193                label: Some("blur bind group"),
1194                layout: &self.blur_bind_group_layout,
1195                entries: &[
1196                    wgpu::BindGroupEntry {
1197                        binding: 0,
1198                        resource: buf.blur_config.as_entire_binding(),
1199                    },
1200                    wgpu::BindGroupEntry {
1201                        binding: 1,
1202                        resource: buf.raw_occlusion.bind_active(),
1203                    },
1204                    wgpu::BindGroupEntry {
1205                        binding: 2,
1206                        resource: buf.blurred_occlusion.bind_active(),
1207                    },
1208                ],
1209            });
1210            compute_pass.set_bind_group(0, Some(&bg), &[]);
1211            compute_pass.dispatch_workgroups(
1212                image_size.width().div_ceil(8),
1213                image_size.height().div_ceil(8),
1214                1,
1215            );
1216        }
1217
1218        gpu.queue.submit(Some(encoder.finish()));
1219        Ok(())
1220    }
1221}
1222
1223struct ColorContext {
1224    /// Configuration bind group layout (shape-specific, includes tape data)
1225    config_bind_group_layout: wgpu::BindGroupLayout,
1226
1227    /// Image bind groups (input and output)
1228    image_bind_group_layout: wgpu::BindGroupLayout,
1229
1230    /// Pipeline for computing per-pixel color
1231    color_pipeline: RegPipeline,
1232}
1233
1234impl ColorContext {
1235    pub fn new(device: &wgpu::Device) -> Self {
1236        let config_bind_group_layout =
1237            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1238                label: Some("color config and shape"),
1239                entries: &[buffer_ro(0), buffer_ro(1), buffer_ro(2)],
1240            });
1241        let image_bind_group_layout =
1242            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1243                label: Some("color images"),
1244                entries: &[buffer_ro(0), buffer_rw(1)],
1245            });
1246
1247        let pipeline_layout =
1248            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1249                label: Some("color pipeline"),
1250                bind_group_layouts: &[
1251                    Some(&config_bind_group_layout),
1252                    Some(&image_bind_group_layout),
1253                ],
1254                immediate_size: 0u32,
1255            });
1256        let device_ = device.clone();
1257        let color_pipeline = RegPipeline::build(Box::new(move |reg_count| {
1258            let shader_code = color_shader(reg_count);
1259            let shader_module =
1260                device_.create_shader_module(wgpu::ShaderModuleDescriptor {
1261                    label: None,
1262                    source: wgpu::ShaderSource::Wgsl(shader_code.into()),
1263                });
1264            device_.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
1265                label: Some(&format!("color ({reg_count})")),
1266                layout: Some(&pipeline_layout),
1267                module: &shader_module,
1268                entry_point: Some("color_main"),
1269                compilation_options: Default::default(),
1270                cache: None,
1271            })
1272        }));
1273
1274        Self {
1275            config_bind_group_layout,
1276            image_bind_group_layout,
1277            color_pipeline,
1278        }
1279    }
1280
1281    /// The output buffer is resized to fit `image`
1282    #[allow(clippy::too_many_arguments)] // what are ya gonna do?
1283    fn submit(
1284        &self,
1285        image: &MergeWorkspace,
1286        world_to_model: &nalgebra::Matrix4<f32>,
1287        shape: &ShapeColorBuffers,
1288        bufs: &mut ColorWorkspace,
1289        vars: &ShapeVars<f32>,
1290        out: &mut ShadeWorkspace,
1291        gpu: &Gpu,
1292    ) -> Result<(), ColorError> {
1293        if image.image_count != shape.shape_count() {
1294            return Err(ColorError::BadShapeCount {
1295                merge_count: image.image_count,
1296                shape_count: shape.shape_count(),
1297            });
1298        }
1299        let size = image.out.size();
1300        out.out
1301            .grow_to_fit(&gpu.device, size)
1302            .map_err(ColorError::OutputSize)?;
1303        out.has_color = true;
1304
1305        let mat = world_to_model * size.screen_to_world();
1306
1307        bufs.copy_vars(gpu, shape.var_map(), vars)
1308            .map_err(|e| match e {
1309                CopyVarsError::BufferSize(b) => ColorError::VarBufferSize(b),
1310                CopyVarsError::MissingVar(v) => ColorError::MissingVar(v),
1311            })?;
1312        let config = ColorConfig {
1313            mat: mat.data.as_slice().try_into().unwrap(),
1314            axes: shape.axes(),
1315            image_size: [size.width(), size.height()],
1316            _pad: 0,
1317        };
1318        bufs.copy_config_and_tape(gpu, &config, shape.bytecode())
1319            .map_err(ColorError::ConfigBufferSize)?;
1320        bufs.copy_shape_starts(gpu, shape.shape_start())
1321            .expect("shape starts should always fit if shape bytecode fits");
1322
1323        let config_bg =
1324            bufs.config_bind_group(&gpu.device, &self.config_bind_group_layout);
1325
1326        // Create a command encoder and dispatch the compute work
1327        let mut encoder = gpu.device.create_command_encoder(
1328            &wgpu::CommandEncoderDescriptor { label: None },
1329        );
1330        {
1331            let mut compute_pass =
1332                encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
1333                    label: None,
1334                    timestamp_writes: None, // TODO add timestamps?
1335                });
1336            compute_pass.set_bind_group(0, config_bg, &[]);
1337
1338            // TODO this creates a bind group for every evaluation, instead of
1339            // caching it somewhere.  However, *where* to cache it is not
1340            // obvious, because it combines fields from two different buffer
1341            // objects.
1342            let image_bg =
1343                gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
1344                    label: Some("color image bind group"),
1345                    layout: &self.image_bind_group_layout,
1346                    entries: &[
1347                        wgpu::BindGroupEntry {
1348                            binding: 0,
1349                            resource: image.out.bind_active(),
1350                        },
1351                        wgpu::BindGroupEntry {
1352                            binding: 1,
1353                            resource: out.out.bind_active(),
1354                        },
1355                    ],
1356                });
1357            compute_pass.set_bind_group(1, &image_bg, &[]);
1358            compute_pass
1359                .set_pipeline(self.color_pipeline.get(shape.reg_count()));
1360            compute_pass.dispatch_workgroups(
1361                size.width().div_ceil(8),
1362                size.height().div_ceil(8),
1363                1,
1364            );
1365        }
1366        gpu.queue.submit(Some(encoder.finish()));
1367        Ok(())
1368    }
1369}
1370
1371////////////////////////////////////////////////////////////////////////////////
1372
1373#[cfg(test)]
1374mod test {
1375    use super::*;
1376    use crate::RenderShape;
1377    use fidget_core::{context::Tree, vm::VmShape};
1378    use fidget_raster::voxel::RenderSize;
1379
1380    #[test]
1381    fn packed_voxel_size() {
1382        assert_eq!(std::mem::size_of::<PackedVoxel>(), 8);
1383    }
1384
1385    #[test]
1386    fn compile_merge_shader() {
1387        crate::compile_shader(&merge_shader(), "merge");
1388    }
1389
1390    #[test]
1391    fn compile_shade_shader() {
1392        crate::compile_shader(&shade_shader(), "shade");
1393    }
1394
1395    #[test]
1396    fn compile_heightmap_shader() {
1397        crate::compile_shader(&heightmap_shader(), "heightmap");
1398    }
1399
1400    #[test]
1401    fn compile_ssao_shader() {
1402        crate::compile_shader(&ssao_shader(), "ssao");
1403    }
1404
1405    #[test]
1406    fn compile_blur_shader() {
1407        crate::compile_shader(&blur_shader(), "blur");
1408    }
1409
1410    #[test]
1411    fn compile_color_shader() {
1412        crate::compile_shader(&color_shader(16), "color");
1413    }
1414
1415    /// Render a sphere-plane union and check for occlusion bias
1416    ///
1417    /// Because the image is perfectly symmetrical, we'd expect the average
1418    /// occlusion across each of the four corners to be very similar.  If it's
1419    /// not, then that's likely a sampling bias – which we have seen before!
1420    #[test]
1421    fn ssao_bias() {
1422        // We only run in CI if we're on MacOS (because other runners don't have
1423        // GPUs and will fail to build the context).
1424        #[cfg(not(target_os = "macos"))]
1425        if std::env::var("CI").is_ok() {
1426            return;
1427        }
1428
1429        let gpu = pollster::block_on(Gpu::init_basic()).unwrap();
1430        let voxel_ctx = crate::voxel::Context::new(&gpu);
1431        let effects_ctx = crate::voxel::effects::Context::new(&gpu);
1432
1433        let size = 128;
1434        let image_size = RenderSize::from(size);
1435        let mut voxel_buf = voxel_ctx.workspace();
1436        let mut merge_buf = effects_ctx.merge_workspace();
1437
1438        let (x, y, z) = Tree::axes();
1439        let sphere =
1440            (x.square() + y.square() + z.square()).sqrt() - Tree::constant(0.5);
1441        let vm_shape = VmShape::from(sphere.min(z));
1442        let shape = RenderShape::new(&vm_shape).unwrap();
1443
1444        voxel_ctx
1445            .submit(
1446                &shape,
1447                &mut voxel_buf,
1448                &crate::voxel::RenderConfig {
1449                    image_size,
1450                    world_to_model: nalgebra::Matrix4::identity(),
1451                },
1452            )
1453            .unwrap();
1454        effects_ctx
1455            .submit_merge(
1456                voxel_buf.output(),
1457                Default::default(),
1458                &mut merge_buf,
1459            )
1460            .unwrap();
1461        let mut ssao_buf = effects_ctx.ssao_workspace();
1462        effects_ctx.submit_ssao(&merge_buf, &mut ssao_buf).unwrap();
1463        let ssao_out = gpu.read_vec(ssao_buf.raw_occlusion());
1464
1465        let quadrants =
1466            [(0, 0), (size / 2, 0), (0, size / 2), (size / 2, size / 2)];
1467        let mut averages = Vec::with_capacity(quadrants.len());
1468        for (dx, dy) in quadrants {
1469            let mut sum = 0.0;
1470            let mut count = 0.0;
1471            for x in 0..size / 2 {
1472                for y in 0..size / 2 {
1473                    let x = (x + dx) as usize;
1474                    let y = (y + dy) as usize;
1475                    sum += ssao_out[x + y * size as usize];
1476                    count += 1.0;
1477                }
1478            }
1479            averages.push(sum / count);
1480        }
1481        for (i, qa) in quadrants.iter().enumerate() {
1482            for (j, qb) in quadrants.iter().enumerate() {
1483                let oa = averages[i];
1484                let ob = averages[j];
1485                let d = (oa - ob).abs();
1486                let epsilon = 0.01;
1487                if d > epsilon {
1488                    panic!(
1489                        "average occlusion between quadrants with offsets \
1490                        {qa:?} and {qb:?} differs by too much: \
1491                        {oa:.3} ≉ {ob:.3}"
1492                    );
1493                }
1494            }
1495        }
1496    }
1497
1498    #[test]
1499    fn color_config_layout() {
1500        crate::test::compare_struct_layout::<ColorConfig>(
1501            &color_shader(16),
1502            "Config",
1503        );
1504    }
1505
1506    #[test]
1507    fn merge_config_layout() {
1508        crate::test::compare_struct_layout::<MergeConfig>(
1509            &merge_shader(),
1510            "MergeConfig",
1511        );
1512    }
1513
1514    #[test]
1515    fn shade_config_layout() {
1516        crate::test::compare_struct_layout::<ShadeConfig>(
1517            &shade_shader(),
1518            "ShadeConfig",
1519        );
1520    }
1521
1522    #[test]
1523    fn heightmap_config_layout() {
1524        crate::test::compare_struct_layout::<HeightmapConfig>(
1525            &heightmap_shader(),
1526            "HeightmapConfig",
1527        );
1528    }
1529
1530    #[test]
1531    fn ssao_config_layout() {
1532        crate::test::compare_struct_layout::<SsaoConfig>(
1533            &ssao_shader(),
1534            "SsaoConfig",
1535        );
1536    }
1537
1538    #[test]
1539    fn blur_config_layout() {
1540        crate::test::compare_struct_layout::<BlurConfig>(
1541            &blur_shader(),
1542            "BlurConfig",
1543        );
1544    }
1545}