use crate::{
CopyVarsError, Gpu, RegPipeline,
buf::{BufferSizeError, FlexBuffer, buffer_ro, buffer_rw, buffer_uniform},
color::{ColorWorkspace as GenericColorWorkspace, ShapeColorBuffers},
shaders, tag,
voxel::GeomBufferTag,
};
use fidget_core::{
render::{ImageSize, VoxelSize},
shape::{MissingVar, ShapeVars},
};
use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};
pub struct Context {
gpu: Gpu,
merge_bind_group_layout: wgpu::BindGroupLayout,
merge_pipeline: wgpu::ComputePipeline,
shade_bind_group_layout: wgpu::BindGroupLayout,
shade_pipeline: wgpu::ComputePipeline,
heightmap_bind_group_layout: wgpu::BindGroupLayout,
heightmap_pipeline: wgpu::ComputePipeline,
ssao_ctx: SsaoContext,
color_ctx: ColorContext,
}
pub type ColorWorkspace = GenericColorWorkspace<ColorConfig>;
const COMMON_SHADER: &str = include_str!("shaders/common.wgsl");
const MERGE_SHADER: &str = include_str!("shaders/merge.wgsl");
const SHADE_SHADER: &str = include_str!("shaders/shade.wgsl");
const HEIGHTMAP_SHADER: &str = include_str!("shaders/heightmap.wgsl");
const SSAO_SHADER: &str = include_str!("shaders/ssao.wgsl");
const BLUR_SHADER: &str = include_str!("shaders/blur.wgsl");
const COLOR_SHADER: &str = include_str!("shaders/color.wgsl");
fn merge_shader() -> String {
MERGE_SHADER.to_owned() + COMMON_SHADER + shaders::COMMON
}
fn shade_shader() -> String {
SHADE_SHADER.to_owned() + COMMON_SHADER + shaders::COMMON
}
fn heightmap_shader() -> String {
HEIGHTMAP_SHADER.to_owned() + COMMON_SHADER + shaders::COMMON
}
fn ssao_shader() -> String {
SSAO_SHADER.to_owned() + COMMON_SHADER + shaders::COMMON
}
fn blur_shader() -> String {
BLUR_SHADER.to_owned() + COMMON_SHADER + shaders::COMMON
}
fn color_shader(reg_count: u8) -> String {
let mut shader_code = shaders::opcode_constants();
shader_code += &format!("const REG_COUNT: u32 = {reg_count};");
shader_code
+ COLOR_SHADER
+ COMMON_SHADER
+ super::TRANSFORM_INPUT
+ shaders::COMMON
+ shaders::TAPE_INTERPRETER
+ shaders::DUMMY_STACK
+ shaders::FLOAT_OPS
}
#[derive(Copy, Clone, FromBytes, Immutable, IntoBytes, KnownLayout)]
#[repr(C)]
pub struct PackedVoxel {
pub normal: [i8; 2],
pub index: u16,
pub z: u32,
}
#[derive(Copy, Clone, FromBytes, Immutable, IntoBytes, KnownLayout)]
#[cfg_attr(test, derive(facet::Facet))]
#[repr(C)]
#[doc(hidden)]
pub struct ColorConfig {
mat: [f32; 16],
axes: [u32; 3],
_pad: u32,
image_size: [u32; 2],
}
#[derive(Copy, Clone, FromBytes, Immutable, IntoBytes, KnownLayout)]
#[cfg_attr(test, derive(facet::Facet))]
#[repr(C)]
pub(crate) struct MergeConfig {
pub image_size: [u32; 2],
pub denoise: u32,
pub z_scale: f32,
pub index_base: u32,
_pad: u32,
}
#[derive(Copy, Clone, FromBytes, Immutable, IntoBytes, KnownLayout)]
#[cfg_attr(test, derive(facet::Facet))]
#[repr(C)]
struct ShadeConfig {
image_size: [u32; 3],
flags: u32,
}
#[derive(Copy, Clone, FromBytes, Immutable, IntoBytes, KnownLayout)]
#[cfg_attr(test, derive(facet::Facet))]
#[repr(C)]
struct HeightmapConfig {
image_size: [u32; 3],
has_color: u32,
}
#[derive(Copy, Clone, Debug)]
pub struct MergeSettings {
pub denoise: bool,
pub z_scale: f32,
}
impl Default for MergeSettings {
fn default() -> Self {
Self {
denoise: true,
z_scale: 1.0,
}
}
}
const SHADE_CONFIG_HAS_SSAO: u32 = 1u32;
const SHADE_CONFIG_HAS_COLOR: u32 = 2u32;
tag!(pub MergeVoxelBufferTag, PackedVoxel, VoxelSize, STORAGE | COPY_SRC,
"Buffer tag for on-GPU merged ([`PackedVoxel`]) images"
);
pub struct MergeWorkspace {
config: wgpu::Buffer,
out: FlexBuffer<MergeVoxelBufferTag>,
image_count: usize,
}
impl MergeWorkspace {
pub fn output(&self) -> &FlexBuffer<MergeVoxelBufferTag> {
&self.out
}
pub fn reset(&mut self) {
self.image_count = 0;
}
}
tag!(
pub ShadedImageTag, u32, VoxelSize, STORAGE | COPY_SRC,
"Buffer tag for on-GPU shaded (RGBA) images"
);
pub struct ShadeWorkspace {
config: wgpu::Buffer,
has_color: bool,
out: FlexBuffer<ShadedImageTag>,
}
impl ShadeWorkspace {
pub fn output(&self) -> &FlexBuffer<ShadedImageTag> {
&self.out
}
}
#[derive(Debug, thiserror::Error)]
pub enum MergeError {
#[error(transparent)]
ImageSizeMismatch(#[from] ImageSizeMismatch),
#[error(transparent)]
OutputSize(BufferSizeError),
}
#[derive(Debug, thiserror::Error)]
pub enum ShadeError {
#[error(transparent)]
OutputSize(BufferSizeError),
#[error(
"input and output buffers must be the same size when
`has_color` is true, but they do not match"
)]
InvalidColorSize,
}
#[derive(Debug, thiserror::Error)]
pub enum HeightmapError {
#[error(transparent)]
OutputSize(BufferSizeError),
#[error(
"input and output buffers must be the same size when
`has_color` is true, but they do not match"
)]
InvalidColorSize,
}
#[derive(Debug, thiserror::Error)]
pub enum SsaoError {
#[error(transparent)]
OutputSize(BufferSizeError),
}
#[derive(Debug, thiserror::Error)]
pub enum ColorError {
#[error("could not resize output buffer")]
OutputSize(#[source] BufferSizeError),
#[error("could not resize vars buffer")]
VarBufferSize(#[source] BufferSizeError),
#[error("could not resize config buffer")]
ConfigBufferSize(#[source] BufferSizeError),
#[error(transparent)]
MissingVar(#[from] MissingVar),
#[error(
"merged image is built from {merge_count} shapes,
but shape color buffer expected {shape_count} shapes"
)]
BadShapeCount {
merge_count: usize,
shape_count: usize,
},
#[error("shape color has already been populated")]
AlreadyHasColor,
}
#[derive(Debug, thiserror::Error)]
#[error(
"image size mismatch: expected {} × {} × {}, got {} × {} × {}",
expected.width(), expected.height(), expected.depth(),
actual.width(), actual.height(), actual.depth()
)]
pub struct ImageSizeMismatch {
expected: VoxelSize,
actual: VoxelSize,
}
impl Context {
pub fn new(gpu: &Gpu) -> Self {
let merge_bind_group_layout = gpu.device.create_bind_group_layout(
&wgpu::BindGroupLayoutDescriptor {
label: None,
entries: &[
buffer_uniform(0), buffer_ro(1), buffer_rw(2), ],
},
);
let shader_code = merge_shader();
let pipeline_layout = gpu.device.create_pipeline_layout(
&wgpu::PipelineLayoutDescriptor {
label: Some("effects merge pipeline"),
bind_group_layouts: &[Some(&merge_bind_group_layout)],
immediate_size: 0u32,
},
);
let shader_module =
gpu.device
.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("effects merge shader module"),
source: wgpu::ShaderSource::Wgsl(shader_code.into()),
});
let merge_pipeline = gpu.device.create_compute_pipeline(
&wgpu::ComputePipelineDescriptor {
label: Some("effects merge compute pipeline"),
layout: Some(&pipeline_layout),
module: &shader_module,
entry_point: Some("merge_main"),
compilation_options: Default::default(),
cache: None,
},
);
let shade_bind_group_layout = gpu.device.create_bind_group_layout(
&wgpu::BindGroupLayoutDescriptor {
label: None,
entries: &[
buffer_uniform(0),
buffer_ro(1), buffer_ro(2), buffer_rw(3), ],
},
);
let shader_code = shade_shader();
let pipeline_layout = gpu.device.create_pipeline_layout(
&wgpu::PipelineLayoutDescriptor {
label: Some("effects shade pipeline"),
bind_group_layouts: &[Some(&shade_bind_group_layout)],
immediate_size: 0u32,
},
);
let shader_module =
gpu.device
.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("effects shade shader module"),
source: wgpu::ShaderSource::Wgsl(shader_code.into()),
});
let shade_pipeline = gpu.device.create_compute_pipeline(
&wgpu::ComputePipelineDescriptor {
label: Some("effects shade compute pipeline"),
layout: Some(&pipeline_layout),
module: &shader_module,
entry_point: Some("shade_main"),
compilation_options: Default::default(),
cache: None,
},
);
let heightmap_bind_group_layout = gpu.device.create_bind_group_layout(
&wgpu::BindGroupLayoutDescriptor {
label: None,
entries: &[
buffer_uniform(0),
buffer_ro(1), buffer_rw(2), ],
},
);
let shader_code = heightmap_shader();
let pipeline_layout = gpu.device.create_pipeline_layout(
&wgpu::PipelineLayoutDescriptor {
label: Some("effects heightmap pipeline"),
bind_group_layouts: &[Some(&heightmap_bind_group_layout)],
immediate_size: 0u32,
},
);
let shader_module =
gpu.device
.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("effects heightmap shader module"),
source: wgpu::ShaderSource::Wgsl(shader_code.into()),
});
let heightmap_pipeline = gpu.device.create_compute_pipeline(
&wgpu::ComputePipelineDescriptor {
label: Some("effects heightmap compute pipeline"),
layout: Some(&pipeline_layout),
module: &shader_module,
entry_point: Some("heightmap_main"),
compilation_options: Default::default(),
cache: None,
},
);
let ssao_ctx = SsaoContext::new(&gpu.device);
let color_ctx = ColorContext::new(&gpu.device);
Self {
gpu: gpu.clone(),
merge_bind_group_layout,
merge_pipeline,
shade_bind_group_layout,
shade_pipeline,
heightmap_bind_group_layout,
heightmap_pipeline,
ssao_ctx,
color_ctx,
}
}
pub fn merge_workspace(&self) -> MergeWorkspace {
let config = self.gpu.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("config"),
size: std::mem::size_of::<MergeConfig>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let out = FlexBuffer::new(&self.gpu.device, "merge output", 64.into())
.expect("64 is always a valid size");
MergeWorkspace {
config,
out,
image_count: 0,
}
}
pub fn shade_workspace(&self) -> ShadeWorkspace {
let config = self.gpu.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("shade config"),
size: std::mem::size_of::<ShadeConfig>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let out = FlexBuffer::new(&self.gpu.device, "shade output", 64.into())
.expect("64 is always a valid size");
ShadeWorkspace {
config,
has_color: false,
out,
}
}
pub fn ssao_workspace(&self) -> SsaoWorkspace {
let ssao_config =
self.gpu.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("ssao config"),
size: std::mem::size_of::<SsaoConfig>() as u64,
usage: wgpu::BufferUsages::UNIFORM
| wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let image_size = 64.into();
let raw_occlusion =
FlexBuffer::new(&self.gpu.device, "ssao raw occlusion", image_size)
.expect("64 is always a valid size");
let blur_config =
self.gpu.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("blur config"),
size: std::mem::size_of::<BlurConfig>() as u64,
usage: wgpu::BufferUsages::UNIFORM
| wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let blurred_occlusion = FlexBuffer::new(
&self.gpu.device,
"ssao blurred occlusion",
image_size,
)
.expect("64 is always a valid size");
SsaoWorkspace {
ssao_config,
blur_config,
raw_occlusion,
blurred_occlusion,
}
}
pub fn submit_merge(
&self,
image: &FlexBuffer<GeomBufferTag>,
settings: MergeSettings,
buf: &mut MergeWorkspace,
) -> Result<(), MergeError> {
let size = image.size();
if buf.image_count == 0 {
buf.out
.grow_to_fit(&self.gpu.device, size)
.map_err(MergeError::OutputSize)?;
} else if buf.out.size() != size {
return Err(ImageSizeMismatch {
expected: size,
actual: buf.out.size(),
}
.into());
}
let mut encoder = self.gpu.device.create_command_encoder(
&wgpu::CommandEncoderDescriptor {
label: Some("merge compute encoder"),
},
);
{
let mut compute_pass =
encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("merge compute pass"),
timestamp_writes: None, });
compute_pass.set_pipeline(&self.merge_pipeline);
let cfg = MergeConfig {
image_size: [size.width(), size.height()],
denoise: settings.denoise as u32,
index_base: buf.image_count as u32,
z_scale: settings.z_scale,
_pad: 0,
};
{
let mut writer = self
.gpu
.queue
.write_buffer_with(
&buf.config,
0,
(std::mem::size_of::<MergeConfig>() as u64)
.try_into()
.unwrap(),
)
.unwrap();
writer.copy_from_slice(cfg.as_bytes());
}
let bg =
self.gpu
.device
.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("merge bind group"),
layout: &self.merge_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: buf.config.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: image.bind_active(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: buf.out.bind_active(),
},
],
});
compute_pass.set_bind_group(0, Some(&bg), &[]);
compute_pass.dispatch_workgroups(
size.width().div_ceil(8),
size.height().div_ceil(8),
1,
);
buf.image_count += 1;
}
self.gpu.queue.submit(Some(encoder.finish()));
Ok(())
}
pub fn submit_shade(
&self,
image: &MergeWorkspace,
ssao: Option<&SsaoWorkspace>,
buf: &mut ShadeWorkspace,
) -> Result<(), ShadeError> {
let size = image.out.size();
if buf.has_color {
if size != buf.out.size() {
return Err(ShadeError::InvalidColorSize);
}
} else {
buf.out
.grow_to_fit(&self.gpu.device, size)
.map_err(ShadeError::OutputSize)?;
}
let mut encoder = self.gpu.device.create_command_encoder(
&wgpu::CommandEncoderDescriptor {
label: Some("shade compute encoder"),
},
);
{
let mut compute_pass =
encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("shade compute pass"),
timestamp_writes: None, });
compute_pass.set_pipeline(&self.shade_pipeline);
let cfg = ShadeConfig {
image_size: [size.width(), size.height(), size.depth()],
flags: if ssao.is_some() {
SHADE_CONFIG_HAS_SSAO
} else {
0
} | if buf.has_color {
SHADE_CONFIG_HAS_COLOR
} else {
0
},
};
{
let mut writer = self
.gpu
.queue
.write_buffer_with(
&buf.config,
0,
buf.config.size().try_into().unwrap(),
)
.unwrap();
writer.copy_from_slice(cfg.as_bytes());
}
let bg =
self.gpu
.device
.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("shade bind group"),
layout: &self.shade_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: buf.config.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: image.out.bind_active(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: ssao
.map(|s| {
s.blurred_occlusion().bind_active()
})
.unwrap_or_else(|| image.out.bind_active()),
},
wgpu::BindGroupEntry {
binding: 3,
resource: buf.out.bind_active(),
},
],
});
compute_pass.set_bind_group(0, Some(&bg), &[]);
compute_pass.dispatch_workgroups(
size.width().div_ceil(8),
size.height().div_ceil(8),
1,
);
}
buf.has_color = false;
self.gpu.queue.submit(Some(encoder.finish()));
Ok(())
}
pub fn submit_heightmap(
&self,
image: &MergeWorkspace,
buf: &mut ShadeWorkspace,
) -> Result<(), HeightmapError> {
let size = image.out.size();
if buf.has_color {
if size != buf.out.size() {
return Err(HeightmapError::InvalidColorSize);
}
} else {
buf.out
.grow_to_fit(&self.gpu.device, size)
.map_err(HeightmapError::OutputSize)?;
}
let mut encoder = self.gpu.device.create_command_encoder(
&wgpu::CommandEncoderDescriptor {
label: Some("heightmap compute encoder"),
},
);
{
let mut compute_pass =
encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("heightmap compute pass"),
timestamp_writes: None, });
compute_pass.set_pipeline(&self.heightmap_pipeline);
let cfg = HeightmapConfig {
image_size: [size.width(), size.height(), size.depth()],
has_color: buf.has_color.into(),
};
{
let mut writer = self
.gpu
.queue
.write_buffer_with(
&buf.config,
0,
buf.config.size().try_into().unwrap(),
)
.unwrap();
writer.copy_from_slice(cfg.as_bytes());
}
let bg =
self.gpu
.device
.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("heightmap bind group"),
layout: &self.heightmap_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: buf.config.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: image.out.bind_active(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: buf.out.bind_active(),
},
],
});
compute_pass.set_bind_group(0, Some(&bg), &[]);
compute_pass.dispatch_workgroups(
size.width().div_ceil(8),
size.height().div_ceil(8),
1,
);
}
buf.has_color = false;
self.gpu.queue.submit(Some(encoder.finish()));
Ok(())
}
pub fn submit_ssao(
&self,
image: &MergeWorkspace,
buf: &mut SsaoWorkspace,
) -> Result<(), SsaoError> {
self.ssao_ctx.submit(image, buf, &self.gpu)
}
pub fn submit_color(
&self,
merge: &MergeWorkspace,
world_to_model: &nalgebra::Matrix4<f32>,
shape: &ShapeColorBuffers,
bufs: &mut ColorWorkspace,
out: &mut ShadeWorkspace,
) -> Result<(), ColorError> {
self.submit_color_with_vars(
merge,
world_to_model,
shape,
bufs,
&Default::default(),
out,
)
}
pub fn submit_color_with_vars(
&self,
merge: &MergeWorkspace,
world_to_model: &nalgebra::Matrix4<f32>,
shape: &ShapeColorBuffers,
bufs: &mut ColorWorkspace,
vars: &ShapeVars<f32>,
out: &mut ShadeWorkspace,
) -> Result<(), ColorError> {
self.color_ctx.submit(
merge,
world_to_model,
shape,
bufs,
vars,
out,
&self.gpu,
)
}
pub fn color_workspace(&self) -> ColorWorkspace {
ColorWorkspace::new(&self.gpu.device)
}
}
tag!(pub SsaoRawBufferTag, f32, ImageSize, STORAGE | COPY_SRC,
"Tag for a raw SSAO occlusion buffer");
tag!(pub SsaoBlurredBufferTag, f32, ImageSize, STORAGE | COPY_SRC,
"Tag for a blurred SSAO occlusion buffer");
pub struct SsaoWorkspace {
ssao_config: wgpu::Buffer, raw_occlusion: FlexBuffer<SsaoRawBufferTag>,
blur_config: wgpu::Buffer,
blurred_occlusion: FlexBuffer<SsaoBlurredBufferTag>,
}
impl SsaoWorkspace {
pub fn raw_occlusion(&self) -> &FlexBuffer<SsaoRawBufferTag> {
&self.raw_occlusion
}
pub fn blurred_occlusion(&self) -> &FlexBuffer<SsaoBlurredBufferTag> {
&self.blurred_occlusion
}
}
#[derive(Copy, Clone, FromBytes, Immutable, IntoBytes, KnownLayout)]
#[cfg_attr(test, derive(facet::Facet))]
#[repr(C)]
struct SsaoConfig {
image_size: [u32; 3],
radius: f32,
}
#[derive(Copy, Clone, FromBytes, Immutable, IntoBytes, KnownLayout)]
#[cfg_attr(test, derive(facet::Facet))]
#[repr(C)]
struct BlurConfig {
image_size: [u32; 2],
radius: u32,
_pad: u32,
}
struct SsaoContext {
ssao_bind_group: wgpu::BindGroup,
ssao_bind_group_layout: wgpu::BindGroupLayout,
ssao_pipeline: wgpu::ComputePipeline,
blur_bind_group_layout: wgpu::BindGroupLayout,
blur_pipeline: wgpu::ComputePipeline,
}
impl SsaoContext {
pub fn new(device: &wgpu::Device) -> Self {
let ssao_fixed_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: None,
entries: &[buffer_ro(0), buffer_ro(1)],
});
let ssao_user_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: None,
entries: &[buffer_uniform(0), buffer_ro(1), buffer_rw(2)],
});
const KERNEL_SIZE: usize = 64;
const NOISE_SIZE: usize = 16;
let ssao_kernel_size_bytes =
KERNEL_SIZE * std::mem::size_of::<[f32; 3]>();
let ssao_kernel = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("ssao kernel"),
size: ssao_kernel_size_bytes as u64,
usage: wgpu::BufferUsages::STORAGE,
mapped_at_creation: true,
});
let ssao_kernel_values =
fidget_raster::effects::ssao_kernel(KERNEL_SIZE);
ssao_kernel
.get_mapped_range_mut(0..ssao_kernel_size_bytes as u64)
.unwrap()
.copy_from_slice(ssao_kernel_values.as_slice().as_bytes());
ssao_kernel.unmap();
let ssao_noise_size_bytes =
NOISE_SIZE * std::mem::size_of::<[f32; 2]>();
let ssao_noise = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("ssao noise"),
size: ssao_noise_size_bytes as u64,
usage: wgpu::BufferUsages::STORAGE,
mapped_at_creation: true,
});
let ssao_noise_values = fidget_raster::effects::ssao_noise(NOISE_SIZE);
ssao_noise
.get_mapped_range_mut(0..ssao_noise_size_bytes as u64)
.unwrap()
.copy_from_slice(ssao_noise_values.as_slice().as_bytes());
ssao_noise.unmap();
let ssao_bind_group =
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("ssao fixed bind group"),
layout: &ssao_fixed_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: ssao_kernel.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: ssao_noise.as_entire_binding(),
},
],
});
let shader_code = ssao_shader();
let pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("effects ssao pipeline"),
bind_group_layouts: &[
Some(&ssao_user_bind_group_layout),
Some(&ssao_fixed_bind_group_layout),
],
immediate_size: 0u32,
});
let shader_module =
device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("effects ssao shader module"),
source: wgpu::ShaderSource::Wgsl(shader_code.into()),
});
let ssao_pipeline =
device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some("effects ssao compute pipeline"),
layout: Some(&pipeline_layout),
module: &shader_module,
entry_point: Some("ssao_main"),
compilation_options: Default::default(),
cache: None,
});
let blur_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: None,
entries: &[buffer_uniform(0), buffer_ro(1), buffer_rw(2)],
});
let shader_code = blur_shader();
let pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("effects blur pipeline"),
bind_group_layouts: &[Some(&blur_bind_group_layout)],
immediate_size: 0u32,
});
let shader_module =
device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("effects blur shader module"),
source: wgpu::ShaderSource::Wgsl(shader_code.into()),
});
let blur_pipeline =
device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some("effects blur compute pipeline"),
layout: Some(&pipeline_layout),
module: &shader_module,
entry_point: Some("blur_main"),
compilation_options: Default::default(),
cache: None,
});
Self {
ssao_bind_group,
ssao_bind_group_layout: ssao_user_bind_group_layout,
ssao_pipeline,
blur_bind_group_layout,
blur_pipeline,
}
}
fn submit(
&self,
image: &MergeWorkspace,
buf: &mut SsaoWorkspace,
gpu: &Gpu,
) -> Result<(), SsaoError> {
let image_size = image.out.size();
buf.raw_occlusion
.grow_to_fit(&gpu.device, image_size.into())
.map_err(SsaoError::OutputSize)?;
buf.blurred_occlusion
.grow_to_fit(&gpu.device, image_size.into())
.map_err(SsaoError::OutputSize)?;
let mut encoder = gpu.device.create_command_encoder(
&wgpu::CommandEncoderDescriptor {
label: Some("ssao command encoder"),
},
);
{
let mut compute_pass =
encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("ssao compute pass"),
timestamp_writes: None, });
compute_pass.set_pipeline(&self.ssao_pipeline);
let cfg = SsaoConfig {
image_size: [
image_size.width(),
image_size.height(),
image_size.depth(),
],
radius: 0.1,
};
{
let mut writer = gpu
.queue
.write_buffer_with(
&buf.ssao_config,
0,
(std::mem::size_of::<SsaoConfig>() as u64)
.try_into()
.unwrap(),
)
.unwrap();
writer.copy_from_slice(cfg.as_bytes());
}
let bg = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("ssao bind group"),
layout: &self.ssao_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: buf.ssao_config.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: image.out.bind_active(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: buf.raw_occlusion.bind_active(),
},
],
});
compute_pass.set_bind_group(0, Some(&bg), &[]);
compute_pass.set_bind_group(1, Some(&self.ssao_bind_group), &[]);
compute_pass.dispatch_workgroups(
image_size.width().div_ceil(8),
image_size.height().div_ceil(8),
1,
);
}
{
let mut compute_pass =
encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("ssao blur compute pass"),
timestamp_writes: None, });
compute_pass.set_pipeline(&self.blur_pipeline);
let cfg = BlurConfig {
image_size: [image_size.width(), image_size.height()],
radius: 2,
_pad: 0,
};
{
let mut writer = gpu
.queue
.write_buffer_with(
&buf.blur_config,
0,
(std::mem::size_of::<BlurConfig>() as u64)
.try_into()
.unwrap(),
)
.unwrap();
writer.copy_from_slice(cfg.as_bytes());
}
let bg = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("blur bind group"),
layout: &self.blur_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: buf.blur_config.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: buf.raw_occlusion.bind_active(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: buf.blurred_occlusion.bind_active(),
},
],
});
compute_pass.set_bind_group(0, Some(&bg), &[]);
compute_pass.dispatch_workgroups(
image_size.width().div_ceil(8),
image_size.height().div_ceil(8),
1,
);
}
gpu.queue.submit(Some(encoder.finish()));
Ok(())
}
}
struct ColorContext {
config_bind_group_layout: wgpu::BindGroupLayout,
image_bind_group_layout: wgpu::BindGroupLayout,
color_pipeline: RegPipeline,
}
impl ColorContext {
pub fn new(device: &wgpu::Device) -> Self {
let config_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("color config and shape"),
entries: &[buffer_ro(0), buffer_ro(1), buffer_ro(2)],
});
let image_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("color images"),
entries: &[buffer_ro(0), buffer_rw(1)],
});
let pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("color pipeline"),
bind_group_layouts: &[
Some(&config_bind_group_layout),
Some(&image_bind_group_layout),
],
immediate_size: 0u32,
});
let device_ = device.clone();
let color_pipeline = RegPipeline::build(Box::new(move |reg_count| {
let shader_code = color_shader(reg_count);
let shader_module =
device_.create_shader_module(wgpu::ShaderModuleDescriptor {
label: None,
source: wgpu::ShaderSource::Wgsl(shader_code.into()),
});
device_.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some(&format!("color ({reg_count})")),
layout: Some(&pipeline_layout),
module: &shader_module,
entry_point: Some("color_main"),
compilation_options: Default::default(),
cache: None,
})
}));
Self {
config_bind_group_layout,
image_bind_group_layout,
color_pipeline,
}
}
#[allow(clippy::too_many_arguments)] fn submit(
&self,
image: &MergeWorkspace,
world_to_model: &nalgebra::Matrix4<f32>,
shape: &ShapeColorBuffers,
bufs: &mut ColorWorkspace,
vars: &ShapeVars<f32>,
out: &mut ShadeWorkspace,
gpu: &Gpu,
) -> Result<(), ColorError> {
if image.image_count != shape.shape_count() {
return Err(ColorError::BadShapeCount {
merge_count: image.image_count,
shape_count: shape.shape_count(),
});
}
let size = image.out.size();
out.out
.grow_to_fit(&gpu.device, size)
.map_err(ColorError::OutputSize)?;
out.has_color = true;
let mat = world_to_model * size.screen_to_world();
bufs.copy_vars(gpu, shape.var_map(), vars)
.map_err(|e| match e {
CopyVarsError::BufferSize(b) => ColorError::VarBufferSize(b),
CopyVarsError::MissingVar(v) => ColorError::MissingVar(v),
})?;
let config = ColorConfig {
mat: mat.data.as_slice().try_into().unwrap(),
axes: shape.axes(),
image_size: [size.width(), size.height()],
_pad: 0,
};
bufs.copy_config_and_tape(gpu, &config, shape.bytecode())
.map_err(ColorError::ConfigBufferSize)?;
bufs.copy_shape_starts(gpu, shape.shape_start())
.expect("shape starts should always fit if shape bytecode fits");
let config_bg =
bufs.config_bind_group(&gpu.device, &self.config_bind_group_layout);
let mut encoder = gpu.device.create_command_encoder(
&wgpu::CommandEncoderDescriptor { label: None },
);
{
let mut compute_pass =
encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: None,
timestamp_writes: None, });
compute_pass.set_bind_group(0, config_bg, &[]);
let image_bg =
gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("color image bind group"),
layout: &self.image_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: image.out.bind_active(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: out.out.bind_active(),
},
],
});
compute_pass.set_bind_group(1, &image_bg, &[]);
compute_pass
.set_pipeline(self.color_pipeline.get(shape.reg_count()));
compute_pass.dispatch_workgroups(
size.width().div_ceil(8),
size.height().div_ceil(8),
1,
);
}
gpu.queue.submit(Some(encoder.finish()));
Ok(())
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::RenderShape;
use fidget_core::{context::Tree, vm::VmShape};
use fidget_raster::voxel::RenderSize;
#[test]
fn packed_voxel_size() {
assert_eq!(std::mem::size_of::<PackedVoxel>(), 8);
}
#[test]
fn compile_merge_shader() {
crate::compile_shader(&merge_shader(), "merge");
}
#[test]
fn compile_shade_shader() {
crate::compile_shader(&shade_shader(), "shade");
}
#[test]
fn compile_heightmap_shader() {
crate::compile_shader(&heightmap_shader(), "heightmap");
}
#[test]
fn compile_ssao_shader() {
crate::compile_shader(&ssao_shader(), "ssao");
}
#[test]
fn compile_blur_shader() {
crate::compile_shader(&blur_shader(), "blur");
}
#[test]
fn compile_color_shader() {
crate::compile_shader(&color_shader(16), "color");
}
#[test]
fn ssao_bias() {
#[cfg(not(target_os = "macos"))]
if std::env::var("CI").is_ok() {
return;
}
let gpu = pollster::block_on(Gpu::init_basic()).unwrap();
let voxel_ctx = crate::voxel::Context::new(&gpu);
let effects_ctx = crate::voxel::effects::Context::new(&gpu);
let size = 128;
let image_size = RenderSize::from(size);
let mut voxel_buf = voxel_ctx.workspace();
let mut merge_buf = effects_ctx.merge_workspace();
let (x, y, z) = Tree::axes();
let sphere =
(x.square() + y.square() + z.square()).sqrt() - Tree::constant(0.5);
let vm_shape = VmShape::from(sphere.min(z));
let shape = RenderShape::new(&vm_shape).unwrap();
voxel_ctx
.submit(
&shape,
&mut voxel_buf,
&crate::voxel::RenderConfig {
image_size,
world_to_model: nalgebra::Matrix4::identity(),
},
)
.unwrap();
effects_ctx
.submit_merge(
voxel_buf.output(),
Default::default(),
&mut merge_buf,
)
.unwrap();
let mut ssao_buf = effects_ctx.ssao_workspace();
effects_ctx.submit_ssao(&merge_buf, &mut ssao_buf).unwrap();
let ssao_out = gpu.read_vec(ssao_buf.raw_occlusion());
let quadrants =
[(0, 0), (size / 2, 0), (0, size / 2), (size / 2, size / 2)];
let mut averages = Vec::with_capacity(quadrants.len());
for (dx, dy) in quadrants {
let mut sum = 0.0;
let mut count = 0.0;
for x in 0..size / 2 {
for y in 0..size / 2 {
let x = (x + dx) as usize;
let y = (y + dy) as usize;
sum += ssao_out[x + y * size as usize];
count += 1.0;
}
}
averages.push(sum / count);
}
for (i, qa) in quadrants.iter().enumerate() {
for (j, qb) in quadrants.iter().enumerate() {
let oa = averages[i];
let ob = averages[j];
let d = (oa - ob).abs();
let epsilon = 0.01;
if d > epsilon {
panic!(
"average occlusion between quadrants with offsets \
{qa:?} and {qb:?} differs by too much: \
{oa:.3} ≉ {ob:.3}"
);
}
}
}
}
#[test]
fn color_config_layout() {
crate::test::compare_struct_layout::<ColorConfig>(
&color_shader(16),
"Config",
);
}
#[test]
fn merge_config_layout() {
crate::test::compare_struct_layout::<MergeConfig>(
&merge_shader(),
"MergeConfig",
);
}
#[test]
fn shade_config_layout() {
crate::test::compare_struct_layout::<ShadeConfig>(
&shade_shader(),
"ShadeConfig",
);
}
#[test]
fn heightmap_config_layout() {
crate::test::compare_struct_layout::<HeightmapConfig>(
&heightmap_shader(),
"HeightmapConfig",
);
}
#[test]
fn ssao_config_layout() {
crate::test::compare_struct_layout::<SsaoConfig>(
&ssao_shader(),
"SsaoConfig",
);
}
#[test]
fn blur_config_layout() {
crate::test::compare_struct_layout::<BlurConfig>(
&blur_shader(),
"BlurConfig",
);
}
}