1use 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
25pub 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
43pub 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#[derive(Copy, Clone, FromBytes, Immutable, IntoBytes, KnownLayout)]
89#[repr(C)]
90pub struct PackedVoxel {
91 pub normal: [i8; 2],
97
98 pub index: u16,
100
101 pub z: u32,
105}
106
107#[derive(Copy, Clone, FromBytes, Immutable, IntoBytes, KnownLayout)]
112#[cfg_attr(test, derive(facet::Facet))]
113#[repr(C)]
114#[doc(hidden)]
115pub struct ColorConfig {
116 mat: [f32; 16],
118
119 axes: [u32; 3],
123
124 _pad: u32,
126
127 image_size: [u32; 2],
129 }
131
132#[derive(Copy, Clone, FromBytes, Immutable, IntoBytes, KnownLayout)]
133#[cfg_attr(test, derive(facet::Facet))]
134#[repr(C)]
135pub(crate) struct MergeConfig {
136 pub image_size: [u32; 2],
138
139 pub denoise: u32,
141
142 pub z_scale: f32,
144
145 pub index_base: u32,
149
150 _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: [u32; 3],
160
161 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: [u32; 3],
171
172 has_color: u32,
174}
175
176#[derive(Copy, Clone, Debug)]
178pub struct MergeSettings {
179 pub denoise: bool,
181
182 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
198const 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
206pub struct MergeWorkspace {
208 config: wgpu::Buffer,
209 out: FlexBuffer<MergeVoxelBufferTag>,
210 image_count: usize,
211}
212
213impl MergeWorkspace {
214 pub fn output(&self) -> &FlexBuffer<MergeVoxelBufferTag> {
216 &self.out
217 }
218
219 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
233pub struct ShadeWorkspace {
235 config: wgpu::Buffer,
236 has_color: bool,
237 out: FlexBuffer<ShadedImageTag>,
238}
239
240impl ShadeWorkspace {
241 pub fn output(&self) -> &FlexBuffer<ShadedImageTag> {
243 &self.out
244 }
245}
246
247#[derive(Debug, thiserror::Error)]
249pub enum MergeError {
250 #[error(transparent)]
252 ImageSizeMismatch(#[from] ImageSizeMismatch),
253
254 #[error(transparent)]
256 OutputSize(BufferSizeError),
257}
258
259#[derive(Debug, thiserror::Error)]
261pub enum ShadeError {
262 #[error(transparent)]
264 OutputSize(BufferSizeError),
265
266 #[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#[derive(Debug, thiserror::Error)]
279pub enum HeightmapError {
280 #[error(transparent)]
282 OutputSize(BufferSizeError),
283
284 #[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#[derive(Debug, thiserror::Error)]
297pub enum SsaoError {
298 #[error(transparent)]
300 OutputSize(BufferSizeError),
301}
302
303#[derive(Debug, thiserror::Error)]
305pub enum ColorError {
306 #[error("could not resize output buffer")]
308 OutputSize(#[source] BufferSizeError),
309
310 #[error("could not resize vars buffer")]
312 VarBufferSize(#[source] BufferSizeError),
313
314 #[error("could not resize config buffer")]
316 ConfigBufferSize(#[source] BufferSizeError),
317
318 #[error(transparent)]
320 MissingVar(#[from] MissingVar),
321
322 #[error(
324 "merged image is built from {merge_count} shapes,
325 but shape color buffer expected {shape_count} shapes"
326 )]
327 BadShapeCount {
328 merge_count: usize,
330 shape_count: usize,
332 },
333
334 #[error("shape color has already been populated")]
336 AlreadyHasColor,
337}
338
339#[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 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), buffer_ro(1), buffer_rw(2), ],
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), buffer_ro(2), buffer_rw(3), ],
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), buffer_rw(2), ],
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 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 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 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 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 {
586 let mut compute_pass =
587 encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
588 label: Some("merge compute pass"),
589 timestamp_writes: None, });
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 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 {
674 let mut compute_pass =
675 encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
676 label: Some("shade compute pass"),
677 timestamp_writes: None, });
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 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 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 {
772 let mut compute_pass =
773 encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
774 label: Some("heightmap compute pass"),
775 timestamp_writes: None, });
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 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 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 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 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 pub fn color_workspace(&self) -> ColorWorkspace {
887 ColorWorkspace::new(&self.gpu.device)
888 }
889}
890
891tag!(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
898pub struct SsaoWorkspace {
900 ssao_config: wgpu::Buffer, raw_occlusion: FlexBuffer<SsaoRawBufferTag>,
902
903 blur_config: wgpu::Buffer,
904 blurred_occlusion: FlexBuffer<SsaoBlurredBufferTag>,
905}
906
907impl SsaoWorkspace {
908 pub fn raw_occlusion(&self) -> &FlexBuffer<SsaoRawBufferTag> {
910 &self.raw_occlusion
911 }
912
913 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: [u32; 3],
925
926 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: [u32; 2],
936
937 radius: u32,
939
940 _pad: u32,
942}
943
944struct SsaoContext {
945 ssao_bind_group: wgpu::BindGroup,
949
950 ssao_bind_group_layout: wgpu::BindGroupLayout,
952
953 ssao_pipeline: wgpu::ComputePipeline,
955
956 blur_bind_group_layout: wgpu::BindGroupLayout,
958
959 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 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 let mut encoder = gpu.device.create_command_encoder(
1104 &wgpu::CommandEncoderDescriptor {
1105 label: Some("ssao command encoder"),
1106 },
1107 );
1108
1109 {
1111 let mut compute_pass =
1112 encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
1113 label: Some("ssao compute pass"),
1114 timestamp_writes: None, });
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, });
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 config_bind_group_layout: wgpu::BindGroupLayout,
1226
1227 image_bind_group_layout: wgpu::BindGroupLayout,
1229
1230 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 #[allow(clippy::too_many_arguments)] 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 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, });
1336 compute_pass.set_bind_group(0, config_bg, &[]);
1337
1338 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#[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 #[test]
1421 fn ssao_bias() {
1422 #[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}