1use 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
24pub 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#[derive(Copy, Clone, FromBytes, Immutable, IntoBytes, KnownLayout)]
61#[repr(C)]
62pub struct PackedVoxel {
63 pub normal: [i8; 2],
69
70 pub index: u16,
72
73 pub z: u32,
77}
78
79#[derive(Copy, Clone, FromBytes, Immutable, IntoBytes, KnownLayout)]
80#[repr(C)]
81struct MergeConfig {
82 image_size: [u32; 2],
84
85 denoise: u32,
87
88 index_base: u32,
92
93 image_count: u32,
95
96 _pad: u32,
98}
99
100#[derive(Copy, Clone, FromBytes, Immutable, IntoBytes, KnownLayout)]
101#[repr(C)]
102struct ShadeConfig {
103 image_size: [u32; 3],
105
106 has_ssao: u32,
108}
109
110tag!(MergeVoxelBufferTag, PackedVoxel, STORAGE | COPY_SRC);
111
112pub 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
124pub struct ShadeBuffers {
126 config: wgpu::Buffer,
127 out: ImageBuffer<ShadedImageTag>,
128}
129
130impl ShadeBuffers {
131 pub fn output(&self) -> &ImageBuffer<ShadedImageTag> {
133 &self.out
134 }
135}
136
137#[derive(Debug, thiserror::Error)]
139pub enum MergeError {
140 #[error(transparent)]
142 ImageSizeMismatch(#[from] ImageSizeMismatch),
143
144 #[error(transparent)]
146 OutputSize(BufferSizeError),
147}
148
149#[derive(Debug, thiserror::Error)]
151pub enum ShadeError {
152 #[error(transparent)]
154 OutputSize(BufferSizeError),
155}
156
157#[derive(Debug, thiserror::Error)]
159pub enum SsaoError {
160 #[error(transparent)]
162 OutputSize(BufferSizeError),
163}
164
165#[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 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), buffer_ro(2), buffer_ro(3), buffer_ro(4), buffer_ro(5), buffer_ro(6), buffer_ro(7), buffer_rw(8), ],
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), buffer_ro(2), buffer_rw(3), ],
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 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 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 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 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 {
384 let mut compute_pass =
385 encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
386 label: Some("merge compute pass"),
387 timestamp_writes: None, });
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 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 {
477 let mut compute_pass =
478 encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
479 label: Some("shade compute pass"),
480 timestamp_writes: None, });
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 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
562tag!(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
569pub 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 pub fn raw_occlusion(&self) -> &ImageBuffer<SsaoRawBufferTag> {
581 &self.raw_occlusion
582 }
583
584 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: [u32; 3],
595
596 radius: f32,
598}
599
600#[derive(Copy, Clone, FromBytes, Immutable, IntoBytes, KnownLayout)]
601#[repr(C)]
602struct BlurConfig {
603 image_size: [u32; 2],
605
606 radius: u32,
608
609 _pad: u32,
611}
612
613struct SsaoContext {
614 ssao_bind_group: wgpu::BindGroup,
618
619 ssao_bind_group_layout: wgpu::BindGroupLayout,
621
622 ssao_pipeline: wgpu::ComputePipeline,
624
625 blur_bind_group_layout: wgpu::BindGroupLayout,
627
628 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 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 let mut encoder = gpu.device.create_command_encoder(
771 &wgpu::CommandEncoderDescriptor {
772 label: Some("ssao command encoder"),
773 },
774 );
775
776 {
778 let mut compute_pass =
779 encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
780 label: Some("ssao compute pass"),
781 timestamp_writes: None, });
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, });
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#[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 #[test]
921 fn ssao_bias() {
922 #[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}