use super::*;
use crate::renderer::ShDegree;
pub(crate) struct GaussianSplatResources {
pub(crate) pipeline: Option<DualPipeline>,
pub(crate) bgl: Option<wgpu::BindGroupLayout>,
pub(crate) depth_pipeline: Option<wgpu::ComputePipeline>,
pub(crate) sort_clear_pipeline: Option<wgpu::ComputePipeline>,
pub(crate) sort_histogram_pipeline: Option<wgpu::ComputePipeline>,
pub(crate) sort_prefix_pipeline: Option<wgpu::ComputePipeline>,
pub(crate) sort_scatter_pipeline: Option<wgpu::ComputePipeline>,
pub(crate) sort_init_pipeline: Option<wgpu::ComputePipeline>,
pub(crate) depth_bgl: Option<wgpu::BindGroupLayout>,
pub(crate) sort_bgl: Option<wgpu::BindGroupLayout>,
}
impl Default for GaussianSplatResources {
fn default() -> Self {
Self {
pipeline: None,
bgl: None,
depth_pipeline: None,
sort_clear_pipeline: None,
sort_histogram_pipeline: None,
sort_prefix_pipeline: None,
sort_scatter_pipeline: None,
sort_init_pipeline: None,
depth_bgl: None,
sort_bgl: None,
}
}
}
fn validate_gaussian_splat_data(
data: &crate::renderer::GaussianSplatData,
) -> crate::error::ViewportResult<()> {
if data.positions.is_empty() {
return Err(crate::error::ViewportError::InvalidGaussianSplatData {
reason: "empty splat list",
});
}
let n = data.positions.len();
if data.scales.len() != n || data.rotations.len() != n || data.opacities.len() != n {
return Err(crate::error::ViewportError::InvalidGaussianSplatData {
reason: "mismatched buffer lengths",
});
}
Ok(())
}
fn build_gaussian_splat_set(
device: &wgpu::Device,
queue: &wgpu::Queue,
data: &crate::renderer::GaussianSplatData,
) -> GaussianSplatGpuSet {
let count = data.positions.len() as u32;
let pos_data: Vec<[f32; 4]> = data
.positions
.iter()
.map(|p| [p[0], p[1], p[2], 1.0])
.collect();
let scale_data: Vec<[f32; 4]> = data
.scales
.iter()
.map(|s| [s[0], s[1], s[2], 0.0])
.collect();
let rotation_data: Vec<[f32; 4]> = data
.rotations
.iter()
.map(|r| [r[0], r[1], r[2], r[3]])
.collect();
let buf_size_pos = (pos_data.len() * std::mem::size_of::<[f32; 4]>()).max(16) as u64;
let buf_size_scale = (scale_data.len() * std::mem::size_of::<[f32; 4]>()).max(16) as u64;
let buf_size_rot = (rotation_data.len() * std::mem::size_of::<[f32; 4]>()).max(16) as u64;
let buf_size_opa = (data.opacities.len() * 4).max(4) as u64;
let buf_size_sh = (data.sh_coefficients.len() * 4).max(4) as u64;
let position_buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("splat_position_buf"),
size: buf_size_pos,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
queue.write_buffer(&position_buf, 0, bytemuck::cast_slice(&pos_data));
let scale_buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("splat_scale_buf"),
size: buf_size_scale,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
queue.write_buffer(&scale_buf, 0, bytemuck::cast_slice(&scale_data));
let rotation_buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("splat_rotation_buf"),
size: buf_size_rot,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
queue.write_buffer(&rotation_buf, 0, bytemuck::cast_slice(&rotation_data));
let opacity_buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("splat_opacity_buf"),
size: buf_size_opa,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
queue.write_buffer(&opacity_buf, 0, bytemuck::cast_slice(&data.opacities));
let sh_buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("splat_sh_buf"),
size: buf_size_sh,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
if !data.sh_coefficients.is_empty() {
queue.write_buffer(&sh_buf, 0, bytemuck::cast_slice(&data.sh_coefficients));
}
GaussianSplatGpuSet {
position_buf,
scale_buf,
rotation_buf,
opacity_buf,
sh_buf,
sh_degree: data.sh_degree,
count,
viewport_sort: Vec::new(),
cpu_positions: data.positions.clone(),
cpu_scales: data.scales.clone(),
}
}
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct SplatUniform {
model: [[f32; 4]; 4],
viewport_w: f32,
viewport_h: f32,
sh_degree: u32,
count: u32,
}
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct DepthUniform {
model: [[f32; 4]; 4],
eye: [f32; 3],
count: u32,
}
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct SortUniform {
shift: u32,
count: u32,
pass_num: u32,
_pad: u32,
}
impl DeviceResources {
pub(crate) fn ensure_gaussian_splat_pipelines(&mut self, device: &wgpu::Device) {
if self.gaussian_splat.bgl.is_some() {
return;
}
let splat_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("gaussian_splat_bgl"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 3,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 4,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 5,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 6,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
let render_shader = crate::resources::builders::wgsl_module(
device,
"gaussian_splat_shader",
crate::resources::builders::wgsl_source!("gaussian_splat"),
);
let render_layout = crate::resources::builders::standard_scene_layout(
device,
"gaussian_splat_pipeline_layout",
&self.camera_bind_group_layout,
&splat_bgl,
);
let gaussian_splat_pipeline = crate::resources::builders::build_dual_pipeline(
device,
&crate::resources::builders::DualPipelineDesc {
label: "gaussian_splat_pipeline",
layout: &render_layout,
shader: &render_shader,
vertex_entry: "vs_main",
fragment_entry: "fs_main",
vertex_buffers: &[],
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
topology: wgpu::PrimitiveTopology::TriangleList,
cull_mode: None,
depth_write: false,
depth_compare: wgpu::CompareFunction::Less,
sample_count: 1,
ldr_format: self.target_format,
},
);
let sort_shader = crate::resources::builders::wgsl_module(
device,
"gaussian_splat_sort_shader",
crate::resources::builders::wgsl_source!("gaussian_splat_sort"),
);
let depth_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("gaussian_splat_depth_bgl"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: false },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
let depth_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("gaussian_splat_depth_layout"),
bind_group_layouts: &[&depth_bgl],
push_constant_ranges: &[],
});
let gaussian_splat_depth_pipeline = crate::resources::builders::compute_pipeline(
device,
"gaussian_splat_depth_pipeline",
&depth_layout,
&sort_shader,
"compute_depths",
);
let sort_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("gaussian_splat_sort_bgl"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: false },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: false },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 3,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: false },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 4,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: false },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 5,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: false },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
let sort_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("gaussian_splat_sort_layout"),
bind_group_layouts: &[&sort_bgl],
push_constant_ranges: &[],
});
let gaussian_splat_sort_init_pipeline = crate::resources::builders::compute_pipeline(
device,
"gaussian_splat_sort_init_pipeline",
&sort_layout,
&sort_shader,
"init_indices",
);
let gaussian_splat_sort_clear_pipeline = crate::resources::builders::compute_pipeline(
device,
"gaussian_splat_sort_clear_pipeline",
&sort_layout,
&sort_shader,
"clear_histogram",
);
let gaussian_splat_sort_histogram_pipeline = crate::resources::builders::compute_pipeline(
device,
"gaussian_splat_sort_histogram_pipeline",
&sort_layout,
&sort_shader,
"histogram_pass",
);
let gaussian_splat_sort_prefix_pipeline = crate::resources::builders::compute_pipeline(
device,
"gaussian_splat_sort_prefix_pipeline",
&sort_layout,
&sort_shader,
"prefix_sum_pass",
);
let gaussian_splat_sort_scatter_pipeline = crate::resources::builders::compute_pipeline(
device,
"gaussian_splat_sort_scatter_pipeline",
&sort_layout,
&sort_shader,
"scatter_pass",
);
self.gaussian_splat.bgl = Some(splat_bgl);
self.gaussian_splat.pipeline = Some(gaussian_splat_pipeline);
self.gaussian_splat.depth_bgl = Some(depth_bgl);
self.gaussian_splat.depth_pipeline = Some(gaussian_splat_depth_pipeline);
self.gaussian_splat.sort_bgl = Some(sort_bgl);
self.gaussian_splat.sort_init_pipeline = Some(gaussian_splat_sort_init_pipeline);
self.gaussian_splat.sort_clear_pipeline = Some(gaussian_splat_sort_clear_pipeline);
self.gaussian_splat.sort_histogram_pipeline = Some(gaussian_splat_sort_histogram_pipeline);
self.gaussian_splat.sort_prefix_pipeline = Some(gaussian_splat_sort_prefix_pipeline);
self.gaussian_splat.sort_scatter_pipeline = Some(gaussian_splat_sort_scatter_pipeline);
}
pub fn upload_gaussian_splat(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
data: &crate::renderer::GaussianSplatData,
) -> crate::error::ViewportResult<crate::renderer::GaussianSplatId> {
validate_gaussian_splat_data(data)?;
let gpu_set = build_gaussian_splat_set(device, queue, data);
Ok(self.content.gaussian_splat_store.insert(gpu_set))
}
pub fn replace_gaussian_splat(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
id: crate::renderer::GaussianSplatId,
data: &crate::renderer::GaussianSplatData,
) -> crate::error::ViewportResult<()> {
validate_gaussian_splat_data(data)?;
let gpu_set = build_gaussian_splat_set(device, queue, data);
if self.content.gaussian_splat_store.replace(id, gpu_set) {
Ok(())
} else {
Err(crate::error::ViewportError::StaleHandle {
index: id.index() as usize,
count: self.content.gaussian_splat_store.slot_count(),
})
}
}
pub fn free_gaussian_splat(&mut self, id: crate::renderer::GaussianSplatId) {
self.content.gaussian_splat_store.remove(id);
}
pub fn begin_upload_gaussian_splat(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
data: crate::renderer::GaussianSplatData,
) -> crate::error::ViewportResult<crate::resources::JobId> {
validate_gaussian_splat_data(&data)?;
let slot = crate::resources::ResultSlot::<crate::renderer::GaussianSplatId>::new();
let slot_for_apply = slot.clone();
let device_for_worker = device.clone();
let queue_for_worker = queue.clone();
let id = {
let mut runner = self.jobs.lock().expect("upload job runner poisoned");
runner.submit_cpu(move |progress| {
progress.set(0.1);
let gpu_set =
build_gaussian_splat_set(&device_for_worker, &queue_for_worker, &data);
progress.set(0.95);
Ok(crate::resources::upload_jobs::JobProduct::with_apply(
Box::new(move |resources: &mut DeviceResources| {
let id = resources.content.gaussian_splat_store.insert(gpu_set);
slot_for_apply.set(id);
}),
))
})
};
self.job_results
.gaussian_splat
.lock()
.expect("gaussian splat result map poisoned")
.insert(id, slot);
Ok(id)
}
pub fn upload_result_gaussian_splat(
&mut self,
id: crate::resources::JobId,
) -> crate::error::ViewportResult<crate::renderer::GaussianSplatId> {
let mut map = self
.job_results
.gaussian_splat
.lock()
.expect("gaussian splat result map poisoned");
let slot = match map.get(&id) {
Some(s) => s.clone(),
None => {
return Err(crate::error::ViewportError::JobResultMissing {
reason: "unknown id or wrong upload type",
});
}
};
match slot.take() {
Some(splat_id) => {
map.remove(&id);
Ok(splat_id)
}
None => Err(crate::error::ViewportError::JobNotReady),
}
}
pub(crate) fn ensure_gaussian_splat_sort_buffers(
&mut self,
device: &wgpu::Device,
store_index: usize,
viewport_index: usize,
) {
let set = match self.content.gaussian_splat_store.get_by_index(store_index) {
Some(s) => s,
None => return,
};
let count = set.count as usize;
if viewport_index >= set.viewport_sort.len() || set.viewport_sort[viewport_index].is_none()
{
let set_mut = self
.content
.gaussian_splat_store
.get_mut_by_index(store_index)
.unwrap();
while set_mut.viewport_sort.len() <= viewport_index {
set_mut.viewport_sort.push(None);
}
if set_mut.viewport_sort[viewport_index].is_none() {
let buf_size = (count * 4).max(4) as u64;
let depth_buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("splat_depth_buf"),
size: buf_size,
usage: wgpu::BufferUsages::STORAGE
| wgpu::BufferUsages::COPY_DST
| wgpu::BufferUsages::COPY_SRC,
mapped_at_creation: false,
});
let sort_buf_usage = wgpu::BufferUsages::STORAGE
| wgpu::BufferUsages::COPY_SRC
| wgpu::BufferUsages::COPY_DST;
let keys_ping = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("splat_keys_ping"),
size: buf_size,
usage: sort_buf_usage,
mapped_at_creation: false,
});
let keys_pong = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("splat_keys_pong"),
size: buf_size,
usage: sort_buf_usage,
mapped_at_creation: false,
});
let vals_ping = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("splat_vals_ping"),
size: buf_size,
usage: sort_buf_usage,
mapped_at_creation: false,
});
let vals_pong = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("splat_vals_pong"),
size: buf_size,
usage: sort_buf_usage,
mapped_at_creation: false,
});
let histogram_buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("splat_histogram"),
size: 256 * 4,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("splat_uniform_buf"),
size: std::mem::size_of::<SplatUniform>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let render_bg = {
let bgl = self.gaussian_splat.bgl.as_ref().unwrap();
let set_ref = self
.content
.gaussian_splat_store
.get_by_index(store_index)
.unwrap();
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("splat_render_bg"),
layout: bgl,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: uniform_buf.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: vals_ping.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: set_ref.position_buf.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 3,
resource: set_ref.scale_buf.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 4,
resource: set_ref.rotation_buf.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 5,
resource: set_ref.opacity_buf.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 6,
resource: set_ref.sh_buf.as_entire_binding(),
},
],
})
};
let vp_sort = GaussianSplatViewportSort {
depth_buf,
keys_ping,
keys_pong,
vals_ping,
vals_pong,
histogram_buf,
render_bg,
last_eye: [f32::NAN; 3],
uniform_buf,
};
let set_mut2 = self
.content
.gaussian_splat_store
.get_mut_by_index(store_index)
.unwrap();
set_mut2.viewport_sort[viewport_index] = Some(vp_sort);
}
}
}
pub(crate) fn run_gaussian_splat_sort(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
store_index: usize,
viewport_index: usize,
eye: [f32; 3],
model: [[f32; 4]; 4],
vp_w: f32,
vp_h: f32,
sh_degree: ShDegree,
) {
self.ensure_gaussian_splat_sort_buffers(device, store_index, viewport_index);
let set = match self.content.gaussian_splat_store.get_by_index(store_index) {
Some(s) => s,
None => return,
};
let count = set.count;
if count == 0 {
return;
}
let vp_sort = match set.viewport_sort.get(viewport_index) {
Some(Some(s)) => s,
_ => return,
};
let splat_uni = SplatUniform {
model,
viewport_w: vp_w,
viewport_h: vp_h,
sh_degree: match sh_degree {
ShDegree::Zero => 0,
ShDegree::One => 1,
ShDegree::Three => 3,
},
count,
};
queue.write_buffer(&vp_sort.uniform_buf, 0, bytemuck::bytes_of(&splat_uni));
let depth_uni = DepthUniform { model, eye, count };
let depth_uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("splat_depth_uniform_tmp"),
size: std::mem::size_of::<DepthUniform>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
queue.write_buffer(&depth_uniform_buf, 0, bytemuck::bytes_of(&depth_uni));
let depth_bg = {
let bgl = self.gaussian_splat.depth_bgl.as_ref().unwrap();
let set_ref = self
.content
.gaussian_splat_store
.get_by_index(store_index)
.unwrap();
let vp_sort_ref = set_ref.viewport_sort[viewport_index].as_ref().unwrap();
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("splat_depth_bg"),
layout: bgl,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: depth_uniform_buf.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: set_ref.position_buf.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: vp_sort_ref.depth_buf.as_entire_binding(),
},
],
})
};
let workgroups = (count + 255) / 256;
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("splat_sort_encoder"),
});
{
let depth_pipeline = self.gaussian_splat.depth_pipeline.as_ref().unwrap();
let mut cpass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("splat_depth_pass"),
timestamp_writes: None,
});
cpass.set_pipeline(depth_pipeline);
cpass.set_bind_group(0, &depth_bg, &[]);
cpass.dispatch_workgroups(workgroups, 1, 1);
}
{
let set_ref = self
.content
.gaussian_splat_store
.get_by_index(store_index)
.unwrap();
let vp_sort_ref = set_ref.viewport_sort[viewport_index].as_ref().unwrap();
encoder.copy_buffer_to_buffer(
&vp_sort_ref.depth_buf,
0,
&vp_sort_ref.keys_ping,
0,
(count as u64) * 4,
);
}
let sort_bgl = self.gaussian_splat.sort_bgl.as_ref().unwrap();
for pass in 0u32..4u32 {
let sort_uni = SortUniform {
shift: pass * 8,
count,
pass_num: pass,
_pad: 0,
};
let sort_uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("splat_sort_uniform_tmp"),
size: std::mem::size_of::<SortUniform>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
queue.write_buffer(&sort_uniform_buf, 0, bytemuck::bytes_of(&sort_uni));
let set_ref = self
.content
.gaussian_splat_store
.get_by_index(store_index)
.unwrap();
let vp_sort_ref = set_ref.viewport_sort[viewport_index].as_ref().unwrap();
let sort_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("splat_sort_bg"),
layout: sort_bgl,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: sort_uniform_buf.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: vp_sort_ref.keys_ping.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: vp_sort_ref.keys_pong.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 3,
resource: vp_sort_ref.vals_ping.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 4,
resource: vp_sort_ref.vals_pong.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 5,
resource: vp_sort_ref.histogram_buf.as_entire_binding(),
},
],
});
if pass == 0 {
let init_pipeline = self.gaussian_splat.sort_init_pipeline.as_ref().unwrap();
let mut cpass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("splat_init_pass"),
timestamp_writes: None,
});
cpass.set_pipeline(init_pipeline);
cpass.set_bind_group(0, &sort_bg, &[]);
cpass.dispatch_workgroups(workgroups, 1, 1);
}
{
let clear_pipeline = self.gaussian_splat.sort_clear_pipeline.as_ref().unwrap();
let mut cpass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("splat_clear_hist"),
timestamp_writes: None,
});
cpass.set_pipeline(clear_pipeline);
cpass.set_bind_group(0, &sort_bg, &[]);
cpass.dispatch_workgroups(1, 1, 1);
}
{
let hist_pipeline = self
.gaussian_splat
.sort_histogram_pipeline
.as_ref()
.unwrap();
let mut cpass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("splat_hist_pass"),
timestamp_writes: None,
});
cpass.set_pipeline(hist_pipeline);
cpass.set_bind_group(0, &sort_bg, &[]);
cpass.dispatch_workgroups(workgroups, 1, 1);
}
{
let prefix_pipeline = self.gaussian_splat.sort_prefix_pipeline.as_ref().unwrap();
let mut cpass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("splat_prefix_pass"),
timestamp_writes: None,
});
cpass.set_pipeline(prefix_pipeline);
cpass.set_bind_group(0, &sort_bg, &[]);
cpass.dispatch_workgroups(1, 1, 1);
}
{
let scatter_pipeline = self.gaussian_splat.sort_scatter_pipeline.as_ref().unwrap();
let mut cpass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("splat_scatter_pass"),
timestamp_writes: None,
});
cpass.set_pipeline(scatter_pipeline);
cpass.set_bind_group(0, &sort_bg, &[]);
cpass.dispatch_workgroups(workgroups, 1, 1);
}
}
queue.submit(std::iter::once(encoder.finish()));
if let Some(set_mut) = self
.content
.gaussian_splat_store
.get_mut_by_index(store_index)
{
if let Some(Some(vp_sort_mut)) = set_mut.viewport_sort.get_mut(viewport_index) {
vp_sort_mut.last_eye = eye;
}
}
}
}
#[cfg(test)]
mod async_tests {
use crate::DeviceResources;
use crate::renderer::GaussianSplatData;
use crate::resources::UploadStatus;
fn try_make_device() -> Option<(wgpu::Device, wgpu::Queue)> {
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::default());
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::LowPower,
compatible_surface: None,
force_fallback_adapter: false,
}))
.ok()?;
pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor::default())).ok()
}
fn sample_splats(n: usize) -> GaussianSplatData {
let mut data = GaussianSplatData::default();
data.positions = (0..n).map(|i| [i as f32, 0.0, 0.0]).collect();
data.scales = vec![[0.1, 0.1, 0.1]; n];
data.rotations = vec![[0.0, 0.0, 0.0, 1.0]; n];
data.opacities = vec![0.5; n];
data
}
#[test]
fn stale_splat_handle_does_not_alias_after_slot_reuse() {
let Some((device, queue)) = try_make_device() else {
eprintln!("skipping: no wgpu adapter available");
return;
};
let mut resources = DeviceResources::new(&device, wgpu::TextureFormat::Rgba8UnormSrgb, 1);
let id1 = resources
.upload_gaussian_splat(&device, &queue, &sample_splats(8))
.unwrap();
assert!(resources.content.gaussian_splat_store.get(id1).is_some());
resources.free_gaussian_splat(id1);
assert!(
resources.content.gaussian_splat_store.get(id1).is_none(),
"a removed handle must not resolve"
);
let id2 = resources
.upload_gaussian_splat(&device, &queue, &sample_splats(4))
.unwrap();
assert_eq!(id1.index(), id2.index(), "the freed slot should be reused");
assert_ne!(id1, id2, "the reused slot must carry a new generation");
assert!(resources.content.gaussian_splat_store.get(id2).is_some());
assert!(
resources.content.gaussian_splat_store.get(id1).is_none(),
"the stale handle must not alias the set now occupying its slot"
);
}
#[test]
fn begin_upload_gaussian_splat_validates() {
let Some((device, queue)) = try_make_device() else {
eprintln!("skipping: no wgpu adapter available");
return;
};
let mut resources = DeviceResources::new(&device, wgpu::TextureFormat::Rgba8UnormSrgb, 1);
let err = resources
.begin_upload_gaussian_splat(&device, &queue, GaussianSplatData::default())
.unwrap_err();
assert!(matches!(
err,
crate::error::ViewportError::InvalidGaussianSplatData { .. }
));
}
#[test]
fn begin_upload_gaussian_splat_drains_to_handle() {
let Some((device, queue)) = try_make_device() else {
eprintln!("skipping: no wgpu adapter available");
return;
};
let mut resources = DeviceResources::new(&device, wgpu::TextureFormat::Rgba8UnormSrgb, 1);
let job = resources
.begin_upload_gaussian_splat(&device, &queue, sample_splats(8))
.expect("job submitted");
for _ in 0..200 {
resources.process_uploads(&device, &queue);
match resources.upload_status(job) {
UploadStatus::Ready => break,
UploadStatus::Failed(e) => panic!("upload failed: {e:?}"),
UploadStatus::Pending { .. } => {
std::thread::sleep(std::time::Duration::from_millis(5));
}
UploadStatus::Unknown => panic!("job id disappeared"),
}
}
let _id = resources.upload_result_gaussian_splat(job).expect("ready");
}
#[test]
fn replace_gaussian_splat_keeps_handle_and_updates_bytes() {
let Some((device, queue)) = try_make_device() else {
eprintln!("skipping: no wgpu adapter available");
return;
};
let mut resources = DeviceResources::new(&device, wgpu::TextureFormat::Rgba8UnormSrgb, 1);
let id = resources
.upload_gaussian_splat(&device, &queue, &sample_splats(8))
.unwrap();
let bytes_before = resources.resident_bytes().gaussian_splat_bytes;
assert!(
bytes_before > 0,
"an uploaded set must count resident bytes"
);
resources
.replace_gaussian_splat(&device, &queue, id, &sample_splats(2))
.expect("replace on a live handle succeeds");
assert!(resources.content.gaussian_splat_store.get(id).is_some());
let bytes_after = resources.resident_bytes().gaussian_splat_bytes;
assert!(
bytes_after < bytes_before,
"replacing with fewer splats must reduce resident bytes"
);
resources.free_gaussian_splat(id);
assert_eq!(resources.resident_bytes().gaussian_splat_bytes, 0);
let err = resources
.replace_gaussian_splat(&device, &queue, id, &sample_splats(2))
.unwrap_err();
assert!(matches!(
err,
crate::error::ViewportError::StaleHandle { .. }
));
}
}
pub(crate) struct GaussianSplatViewportSort {
pub depth_buf: wgpu::Buffer,
pub keys_ping: wgpu::Buffer,
pub keys_pong: wgpu::Buffer,
pub vals_ping: wgpu::Buffer,
pub vals_pong: wgpu::Buffer,
pub histogram_buf: wgpu::Buffer,
pub render_bg: wgpu::BindGroup,
pub last_eye: [f32; 3],
pub uniform_buf: wgpu::Buffer,
}
pub(crate) struct GaussianSplatGpuSet {
pub position_buf: wgpu::Buffer,
pub scale_buf: wgpu::Buffer,
pub rotation_buf: wgpu::Buffer,
pub opacity_buf: wgpu::Buffer,
pub sh_buf: wgpu::Buffer,
pub sh_degree: crate::renderer::ShDegree,
pub count: u32,
pub viewport_sort: Vec<Option<GaussianSplatViewportSort>>,
#[allow(dead_code)]
pub cpu_positions: Vec<[f32; 3]>,
#[allow(dead_code)]
pub cpu_scales: Vec<[f32; 3]>,
}
impl GaussianSplatGpuSet {
pub fn gpu_bytes(&self) -> u64 {
self.position_buf.size()
+ self.scale_buf.size()
+ self.rotation_buf.size()
+ self.opacity_buf.size()
+ self.sh_buf.size()
}
}
pub(crate) struct GaussianSplatDrawData {
pub store_index: usize,
pub viewport_index: usize,
#[allow(dead_code)]
pub model: [[f32; 4]; 4],
pub count: u32,
pub wireframe: bool,
}
pub(crate) struct GaussianSplatStore {
store:
crate::resources::handle::SlotStore<GaussianSplatGpuSet, crate::renderer::GaussianSplatId>,
}
impl GaussianSplatStore {
pub fn new() -> Self {
Self {
store: crate::resources::handle::SlotStore::default(),
}
}
pub fn insert(&mut self, set: GaussianSplatGpuSet) -> crate::renderer::GaussianSplatId {
let bytes = set.gpu_bytes();
self.store.insert(set, bytes)
}
pub fn replace(
&mut self,
id: crate::renderer::GaussianSplatId,
set: GaussianSplatGpuSet,
) -> bool {
let bytes = set.gpu_bytes();
self.store.replace(id, set, bytes).is_some()
}
pub fn allocated_bytes(&self) -> u64 {
self.store.allocated_bytes()
}
pub fn get(&self, id: crate::renderer::GaussianSplatId) -> Option<&GaussianSplatGpuSet> {
self.store.get(id)
}
pub fn get_by_index(&self, idx: usize) -> Option<&GaussianSplatGpuSet> {
self.store.get_by_index(idx)
}
pub fn get_mut_by_index(&mut self, idx: usize) -> Option<&mut GaussianSplatGpuSet> {
self.store.get_mut_by_index(idx)
}
pub fn slot_count(&self) -> usize {
self.store.slot_count()
}
pub fn remove(&mut self, id: crate::renderer::GaussianSplatId) -> bool {
self.store.remove(id).is_some()
}
}