use super::*;
impl DeviceResources {
pub(crate) fn ensure_point_cloud_pipeline(&mut self, device: &wgpu::Device) {
if self.point_cloud_pipeline.is_some() {
return;
}
let pc_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("point_cloud_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::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
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 shader = crate::resources::builders::wgsl_module(
device,
"point_cloud_shader",
crate::resources::builders::wgsl_source!("point_cloud"),
);
let layout = crate::resources::builders::standard_scene_layout(
device,
"point_cloud_pipeline_layout",
&self.camera_bind_group_layout,
&pc_bgl,
);
let pc_vertex_layout = wgpu::VertexBufferLayout {
array_stride: 12,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &[wgpu::VertexAttribute {
offset: 0,
shader_location: 0,
format: wgpu::VertexFormat::Float32x3,
}],
};
self.point_cloud_pipeline = Some(crate::resources::builders::build_dual_pipeline(
device,
&crate::resources::builders::DualPipelineDesc {
label: "point_cloud_pipeline",
layout: &layout,
shader: &shader,
vertex_entry: "vs_main",
fragment_entry: "fs_main",
vertex_buffers: &[pc_vertex_layout.clone()],
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
topology: wgpu::PrimitiveTopology::TriangleList,
cull_mode: None,
depth_write: true,
depth_compare: wgpu::CompareFunction::Less,
sample_count: self.sample_count,
ldr_format: self.target_format,
},
));
self.point_cloud_bgl = Some(pc_bgl);
}
pub(crate) fn upload_point_cloud_per_frame(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
item: &crate::renderer::PointCloudItem,
) -> PointCloudGpuData {
let point_count = item.positions.len() as u32;
let pos_bytes: Vec<u8> = item
.positions
.iter()
.flat_map(|p| bytemuck::bytes_of(p).iter().copied())
.collect();
let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("pc_vertex_buf"),
size: pos_bytes.len().max(12) as u64,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
queue.write_buffer(&vertex_buffer, 0, &pos_bytes);
let (scalar_buf, has_scalars, scalar_min, scalar_max) = if !item.scalars.is_empty() {
let min = item
.scalar_range
.map(|r| r.0)
.unwrap_or_else(|| item.scalars.iter().cloned().fold(f32::INFINITY, f32::min));
let max = item.scalar_range.map(|r| r.1).unwrap_or_else(|| {
item.scalars
.iter()
.cloned()
.fold(f32::NEG_INFINITY, f32::max)
});
let buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("pc_scalar_buf"),
size: (std::mem::size_of::<f32>() * item.scalars.len()).max(4) as u64,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
queue.write_buffer(&buf, 0, bytemuck::cast_slice(&item.scalars));
(buf, 1u32, min, max)
} else {
let buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("pc_scalar_buf_fallback"),
size: 4,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
(buf, 0u32, 0.0f32, 1.0f32)
};
let (colour_buf, has_colours) = if !item.colours.is_empty() && has_scalars == 0 {
let bytes: &[u8] = bytemuck::cast_slice(&item.colours);
let buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("pc_colour_buf"),
size: bytes.len().max(16) as u64,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
queue.write_buffer(&buf, 0, bytes);
(buf, 1u32)
} else {
let buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("pc_colour_buf_fallback"),
size: 16,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
(buf, 0u32)
};
let (radius_buf, has_radius) = if !item.radius_scalars.is_empty() {
let r_min = item.radius_scalar_range.map(|r| r.0).unwrap_or_else(|| {
item.radius_scalars
.iter()
.cloned()
.fold(f32::INFINITY, f32::min)
});
let r_max = item.radius_scalar_range.map(|r| r.1).unwrap_or_else(|| {
item.radius_scalars
.iter()
.cloned()
.fold(f32::NEG_INFINITY, f32::max)
});
let range = (r_max - r_min).max(f32::EPSILON);
let (out_min, out_max) = item.radius_range;
let mapped: Vec<f32> = item
.radius_scalars
.iter()
.map(|&s| {
let t = ((s - r_min) / range).clamp(0.0, 1.0);
out_min + t * (out_max - out_min)
})
.collect();
let buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("pc_radius_buf"),
size: (std::mem::size_of::<f32>() * mapped.len()).max(4) as u64,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
queue.write_buffer(&buf, 0, bytemuck::cast_slice(&mapped));
(buf, 1u32)
} else if !item.radii.is_empty() {
let buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("pc_radius_buf"),
size: (std::mem::size_of::<f32>() * item.radii.len()).max(4) as u64,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
queue.write_buffer(&buf, 0, bytemuck::cast_slice(&item.radii));
(buf, 1u32)
} else {
let buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("pc_radius_buf_fallback"),
size: 4,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
(buf, 0u32)
};
let (transparency_buf, has_transparency) = if !item.transparencies.is_empty() {
let buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("pc_transparency_buf"),
size: (std::mem::size_of::<f32>() * item.transparencies.len()).max(4) as u64,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
queue.write_buffer(&buf, 0, bytemuck::cast_slice(&item.transparencies));
(buf, 1u32)
} else {
let buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("pc_transparency_buf_fallback"),
size: 4,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
(buf, 0u32)
};
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct PointCloudUniform {
model: [[f32; 4]; 4],
default_colour: [f32; 4],
point_size: f32,
has_scalars: u32,
scalar_min: f32,
scalar_max: f32,
has_colours: u32,
has_radius: u32,
has_transparency: u32,
gaussian: u32,
render_mode: u32,
_pad: [u32; 3],
}
let uniform_data = PointCloudUniform {
model: item.model,
default_colour: item.default_colour,
point_size: item.point_size,
has_scalars,
scalar_min,
scalar_max,
has_colours,
has_radius,
has_transparency,
gaussian: if item.gaussian { 1 } else { 0 },
render_mode: match item.render_mode {
crate::renderer::PointRenderMode::ScreenSpaceCircle => 0,
crate::renderer::PointRenderMode::Sphere => 1,
},
_pad: [0; 3],
};
let uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("pc_uniform_buf"),
size: std::mem::size_of::<PointCloudUniform>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
queue.write_buffer(&uniform_buf, 0, bytemuck::bytes_of(&uniform_data));
let lut_view = self
.content
.builtin_colourmap_ids
.and_then(|ids| {
let preset_id = item
.colourmap_id
.unwrap_or(ids[crate::resources::BuiltinColourmap::Viridis as usize]);
self.content.colourmap_views.get(preset_id.0)
})
.unwrap_or(&self.content.fallback_lut_view);
let lut_sampler = &self.material_sampler;
let bgl = self
.point_cloud_bgl
.as_ref()
.expect("ensure_point_cloud_pipeline not called");
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("pc_bind_group"),
layout: bgl,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: uniform_buf.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(lut_view),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::Sampler(lut_sampler),
},
wgpu::BindGroupEntry {
binding: 3,
resource: scalar_buf.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 4,
resource: colour_buf.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 5,
resource: radius_buf.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 6,
resource: transparency_buf.as_entire_binding(),
},
],
});
PointCloudGpuData {
vertex_buffer,
point_count,
bind_group,
_uniform_buf: uniform_buf,
_scalar_buf: scalar_buf,
_colour_buf: colour_buf,
_radius_buf: radius_buf,
_transparency_buf: transparency_buf,
}
}
pub fn upload_point_cloud(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
item: &crate::renderer::PointCloudItem,
) -> crate::resources::PointCloudId {
self.ensure_point_cloud_pipeline(device);
let gpu = self.upload_point_cloud_per_frame(device, queue, item);
self.content.point_cloud_store.insert(gpu)
}
pub fn drop_point_cloud(&mut self, id: crate::resources::PointCloudId) -> bool {
self.content.point_cloud_store.remove(id)
}
pub fn replace_point_cloud(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
id: crate::resources::PointCloudId,
item: &crate::renderer::PointCloudItem,
) -> bool {
if !self.content.point_cloud_store.contains(id) {
return false;
}
self.ensure_point_cloud_pipeline(device);
let gpu = self.upload_point_cloud_per_frame(device, queue, item);
self.content.point_cloud_store.replace(id, gpu)
}
pub fn begin_upload_point_cloud(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
item: crate::renderer::PointCloudItem,
) -> crate::resources::JobId {
let slot = crate::resources::ResultSlot::<crate::resources::PointCloudId>::new();
let slot_for_apply = slot.clone();
let device_for_apply = device.clone();
let queue_for_apply = queue.clone();
let id = {
let mut runner = self.jobs.lock().expect("upload job runner poisoned");
runner.submit_cpu(move |progress| {
progress.set(0.9);
Ok(crate::resources::upload_jobs::JobProduct::with_apply(
Box::new(move |resources: &mut DeviceResources| {
let pid = resources.upload_point_cloud(
&device_for_apply,
&queue_for_apply,
&item,
);
slot_for_apply.set(pid);
}),
))
})
};
self.job_results
.point_cloud
.lock()
.expect("point cloud result map poisoned")
.insert(id, slot);
id
}
pub fn upload_result_point_cloud(
&mut self,
id: crate::resources::JobId,
) -> crate::error::ViewportResult<crate::resources::PointCloudId> {
let mut map = self
.job_results
.point_cloud
.lock()
.expect("point cloud 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(pid) => {
map.remove(&id);
Ok(pid)
}
None => Err(crate::error::ViewportError::JobNotReady),
}
}
}
#[cfg(test)]
mod tests {
use crate::DeviceResources;
use crate::renderer::PointCloudItem;
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_point_cloud() -> PointCloudItem {
let mut item = PointCloudItem::default();
item.positions = vec![
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0],
];
item.point_size = 6.0;
item
}
#[test]
fn upload_point_cloud_returns_valid_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 id = resources.upload_point_cloud(&device, &queue, &sample_point_cloud());
assert!(resources.content.point_cloud_store.contains(id));
assert!(resources.drop_point_cloud(id));
assert!(!resources.content.point_cloud_store.contains(id));
}
#[test]
fn begin_upload_point_cloud_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_point_cloud(&device, &queue, sample_point_cloud());
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_point_cloud(job).expect("ready");
assert!(resources.content.point_cloud_store.contains(id));
let err = resources.upload_result_point_cloud(job).unwrap_err();
assert!(matches!(
err,
crate::error::ViewportError::JobResultMissing { .. }
));
}
}
#[derive(Clone)]
pub struct PointCloudGpuData {
pub(crate) vertex_buffer: wgpu::Buffer,
pub(crate) point_count: u32,
pub(crate) bind_group: wgpu::BindGroup,
pub(crate) _uniform_buf: wgpu::Buffer,
pub(crate) _scalar_buf: wgpu::Buffer,
pub(crate) _colour_buf: wgpu::Buffer,
pub(crate) _radius_buf: wgpu::Buffer,
pub(crate) _transparency_buf: wgpu::Buffer,
}