1use super::*;
2
3impl DeviceResources {
4 pub(crate) fn ensure_point_cloud_pipeline(&mut self, device: &wgpu::Device) {
8 if self.point_cloud_pipeline.is_some() {
9 return;
10 }
11
12 let pc_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
13 label: Some("point_cloud_bgl"),
14 entries: &[
15 wgpu::BindGroupLayoutEntry {
16 binding: 0,
17 visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
18 ty: wgpu::BindingType::Buffer {
19 ty: wgpu::BufferBindingType::Uniform,
20 has_dynamic_offset: false,
21 min_binding_size: None,
22 },
23 count: None,
24 },
25 wgpu::BindGroupLayoutEntry {
26 binding: 1,
27 visibility: wgpu::ShaderStages::VERTEX,
28 ty: wgpu::BindingType::Texture {
29 sample_type: wgpu::TextureSampleType::Float { filterable: true },
30 view_dimension: wgpu::TextureViewDimension::D2,
31 multisampled: false,
32 },
33 count: None,
34 },
35 wgpu::BindGroupLayoutEntry {
36 binding: 2,
37 visibility: wgpu::ShaderStages::VERTEX,
38 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
39 count: None,
40 },
41 wgpu::BindGroupLayoutEntry {
42 binding: 3,
43 visibility: wgpu::ShaderStages::VERTEX,
44 ty: wgpu::BindingType::Buffer {
45 ty: wgpu::BufferBindingType::Storage { read_only: true },
46 has_dynamic_offset: false,
47 min_binding_size: None,
48 },
49 count: None,
50 },
51 wgpu::BindGroupLayoutEntry {
52 binding: 4,
53 visibility: wgpu::ShaderStages::VERTEX,
54 ty: wgpu::BindingType::Buffer {
55 ty: wgpu::BufferBindingType::Storage { read_only: true },
56 has_dynamic_offset: false,
57 min_binding_size: None,
58 },
59 count: None,
60 },
61 wgpu::BindGroupLayoutEntry {
62 binding: 5,
63 visibility: wgpu::ShaderStages::VERTEX,
64 ty: wgpu::BindingType::Buffer {
65 ty: wgpu::BufferBindingType::Storage { read_only: true },
66 has_dynamic_offset: false,
67 min_binding_size: None,
68 },
69 count: None,
70 },
71 wgpu::BindGroupLayoutEntry {
72 binding: 6,
73 visibility: wgpu::ShaderStages::VERTEX,
74 ty: wgpu::BindingType::Buffer {
75 ty: wgpu::BufferBindingType::Storage { read_only: true },
76 has_dynamic_offset: false,
77 min_binding_size: None,
78 },
79 count: None,
80 },
81 ],
82 });
83
84 let shader = crate::resources::builders::wgsl_module(
85 device,
86 "point_cloud_shader",
87 crate::resources::builders::wgsl_source!("point_cloud"),
88 );
89
90 let layout = crate::resources::builders::standard_scene_layout(
91 device,
92 "point_cloud_pipeline_layout",
93 &self.camera_bind_group_layout,
94 &pc_bgl,
95 );
96
97 let pc_vertex_layout = wgpu::VertexBufferLayout {
98 array_stride: 12,
99 step_mode: wgpu::VertexStepMode::Instance,
100 attributes: &[wgpu::VertexAttribute {
101 offset: 0,
102 shader_location: 0,
103 format: wgpu::VertexFormat::Float32x3,
104 }],
105 };
106
107 self.point_cloud_pipeline = Some(crate::resources::builders::build_dual_pipeline(
108 device,
109 &crate::resources::builders::DualPipelineDesc {
110 label: "point_cloud_pipeline",
111 layout: &layout,
112 shader: &shader,
113 vertex_entry: "vs_main",
114 fragment_entry: "fs_main",
115 vertex_buffers: &[pc_vertex_layout.clone()],
116 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
117 topology: wgpu::PrimitiveTopology::TriangleList,
118 cull_mode: None,
119 depth_write: true,
120 depth_compare: wgpu::CompareFunction::Less,
121 sample_count: self.sample_count,
122 ldr_format: self.target_format,
123 },
124 ));
125 self.point_cloud_bgl = Some(pc_bgl);
126 }
127
128 pub(crate) fn upload_point_cloud_per_frame(
132 &mut self,
133 device: &wgpu::Device,
134 queue: &wgpu::Queue,
135 item: &crate::renderer::PointCloudItem,
136 ) -> PointCloudGpuData {
137 let point_count = item.positions.len() as u32;
138
139 let pos_bytes: Vec<u8> = item
140 .positions
141 .iter()
142 .flat_map(|p| bytemuck::bytes_of(p).iter().copied())
143 .collect();
144 let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
145 label: Some("pc_vertex_buf"),
146 size: pos_bytes.len().max(12) as u64,
147 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
148 mapped_at_creation: false,
149 });
150 queue.write_buffer(&vertex_buffer, 0, &pos_bytes);
151
152 let (scalar_buf, has_scalars, scalar_min, scalar_max) = if !item.scalars.is_empty() {
153 let min = item
154 .scalar_range
155 .map(|r| r.0)
156 .unwrap_or_else(|| item.scalars.iter().cloned().fold(f32::INFINITY, f32::min));
157 let max = item.scalar_range.map(|r| r.1).unwrap_or_else(|| {
158 item.scalars
159 .iter()
160 .cloned()
161 .fold(f32::NEG_INFINITY, f32::max)
162 });
163 let buf = device.create_buffer(&wgpu::BufferDescriptor {
164 label: Some("pc_scalar_buf"),
165 size: (std::mem::size_of::<f32>() * item.scalars.len()).max(4) as u64,
166 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
167 mapped_at_creation: false,
168 });
169 queue.write_buffer(&buf, 0, bytemuck::cast_slice(&item.scalars));
170 (buf, 1u32, min, max)
171 } else {
172 let buf = device.create_buffer(&wgpu::BufferDescriptor {
173 label: Some("pc_scalar_buf_fallback"),
174 size: 4,
175 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
176 mapped_at_creation: false,
177 });
178 (buf, 0u32, 0.0f32, 1.0f32)
179 };
180
181 let (colour_buf, has_colours) = if !item.colours.is_empty() && has_scalars == 0 {
182 let bytes: &[u8] = bytemuck::cast_slice(&item.colours);
183 let buf = device.create_buffer(&wgpu::BufferDescriptor {
184 label: Some("pc_colour_buf"),
185 size: bytes.len().max(16) as u64,
186 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
187 mapped_at_creation: false,
188 });
189 queue.write_buffer(&buf, 0, bytes);
190 (buf, 1u32)
191 } else {
192 let buf = device.create_buffer(&wgpu::BufferDescriptor {
193 label: Some("pc_colour_buf_fallback"),
194 size: 16,
195 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
196 mapped_at_creation: false,
197 });
198 (buf, 0u32)
199 };
200
201 let (radius_buf, has_radius) = if !item.radius_scalars.is_empty() {
204 let r_min = item.radius_scalar_range.map(|r| r.0).unwrap_or_else(|| {
205 item.radius_scalars
206 .iter()
207 .cloned()
208 .fold(f32::INFINITY, f32::min)
209 });
210 let r_max = item.radius_scalar_range.map(|r| r.1).unwrap_or_else(|| {
211 item.radius_scalars
212 .iter()
213 .cloned()
214 .fold(f32::NEG_INFINITY, f32::max)
215 });
216 let range = (r_max - r_min).max(f32::EPSILON);
217 let (out_min, out_max) = item.radius_range;
218 let mapped: Vec<f32> = item
219 .radius_scalars
220 .iter()
221 .map(|&s| {
222 let t = ((s - r_min) / range).clamp(0.0, 1.0);
223 out_min + t * (out_max - out_min)
224 })
225 .collect();
226 let buf = device.create_buffer(&wgpu::BufferDescriptor {
227 label: Some("pc_radius_buf"),
228 size: (std::mem::size_of::<f32>() * mapped.len()).max(4) as u64,
229 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
230 mapped_at_creation: false,
231 });
232 queue.write_buffer(&buf, 0, bytemuck::cast_slice(&mapped));
233 (buf, 1u32)
234 } else if !item.radii.is_empty() {
235 let buf = device.create_buffer(&wgpu::BufferDescriptor {
236 label: Some("pc_radius_buf"),
237 size: (std::mem::size_of::<f32>() * item.radii.len()).max(4) as u64,
238 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
239 mapped_at_creation: false,
240 });
241 queue.write_buffer(&buf, 0, bytemuck::cast_slice(&item.radii));
242 (buf, 1u32)
243 } else {
244 let buf = device.create_buffer(&wgpu::BufferDescriptor {
245 label: Some("pc_radius_buf_fallback"),
246 size: 4,
247 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
248 mapped_at_creation: false,
249 });
250 (buf, 0u32)
251 };
252
253 let (transparency_buf, has_transparency) = if !item.transparencies.is_empty() {
254 let buf = device.create_buffer(&wgpu::BufferDescriptor {
255 label: Some("pc_transparency_buf"),
256 size: (std::mem::size_of::<f32>() * item.transparencies.len()).max(4) as u64,
257 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
258 mapped_at_creation: false,
259 });
260 queue.write_buffer(&buf, 0, bytemuck::cast_slice(&item.transparencies));
261 (buf, 1u32)
262 } else {
263 let buf = device.create_buffer(&wgpu::BufferDescriptor {
264 label: Some("pc_transparency_buf_fallback"),
265 size: 4,
266 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
267 mapped_at_creation: false,
268 });
269 (buf, 0u32)
270 };
271
272 #[repr(C)]
273 #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
274 struct PointCloudUniform {
275 model: [[f32; 4]; 4],
276 default_colour: [f32; 4],
277 point_size: f32,
278 has_scalars: u32,
279 scalar_min: f32,
280 scalar_max: f32,
281 has_colours: u32,
282 has_radius: u32,
283 has_transparency: u32,
284 gaussian: u32,
285 render_mode: u32,
287 _pad: [u32; 3],
288 }
289 let uniform_data = PointCloudUniform {
290 model: item.model,
291 default_colour: item.default_colour,
292 point_size: item.point_size,
293 has_scalars,
294 scalar_min,
295 scalar_max,
296 has_colours,
297 has_radius,
298 has_transparency,
299 gaussian: if item.gaussian { 1 } else { 0 },
300 render_mode: match item.render_mode {
301 crate::renderer::PointRenderMode::ScreenSpaceCircle => 0,
302 crate::renderer::PointRenderMode::Sphere => 1,
303 },
304 _pad: [0; 3],
305 };
306 let uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
307 label: Some("pc_uniform_buf"),
308 size: std::mem::size_of::<PointCloudUniform>() as u64,
309 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
310 mapped_at_creation: false,
311 });
312 queue.write_buffer(&uniform_buf, 0, bytemuck::bytes_of(&uniform_data));
313
314 let lut_view = self
315 .content
316 .builtin_colourmap_ids
317 .and_then(|ids| {
318 let preset_id = item
319 .colourmap_id
320 .unwrap_or(ids[crate::resources::BuiltinColourmap::Viridis as usize]);
321 self.content.colourmap_views.get(preset_id.0)
322 })
323 .unwrap_or(&self.content.fallback_lut_view);
324
325 let lut_sampler = &self.material_sampler;
326
327 let bgl = self
328 .point_cloud_bgl
329 .as_ref()
330 .expect("ensure_point_cloud_pipeline not called");
331 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
332 label: Some("pc_bind_group"),
333 layout: bgl,
334 entries: &[
335 wgpu::BindGroupEntry {
336 binding: 0,
337 resource: uniform_buf.as_entire_binding(),
338 },
339 wgpu::BindGroupEntry {
340 binding: 1,
341 resource: wgpu::BindingResource::TextureView(lut_view),
342 },
343 wgpu::BindGroupEntry {
344 binding: 2,
345 resource: wgpu::BindingResource::Sampler(lut_sampler),
346 },
347 wgpu::BindGroupEntry {
348 binding: 3,
349 resource: scalar_buf.as_entire_binding(),
350 },
351 wgpu::BindGroupEntry {
352 binding: 4,
353 resource: colour_buf.as_entire_binding(),
354 },
355 wgpu::BindGroupEntry {
356 binding: 5,
357 resource: radius_buf.as_entire_binding(),
358 },
359 wgpu::BindGroupEntry {
360 binding: 6,
361 resource: transparency_buf.as_entire_binding(),
362 },
363 ],
364 });
365
366 PointCloudGpuData {
367 vertex_buffer,
368 point_count,
369 bind_group,
370 _uniform_buf: uniform_buf,
371 _scalar_buf: scalar_buf,
372 _colour_buf: colour_buf,
373 _radius_buf: radius_buf,
374 _transparency_buf: transparency_buf,
375 }
376 }
377
378 pub fn upload_point_cloud(
380 &mut self,
381 device: &wgpu::Device,
382 queue: &wgpu::Queue,
383 item: &crate::renderer::PointCloudItem,
384 ) -> crate::resources::PointCloudId {
385 self.ensure_point_cloud_pipeline(device);
386 let gpu = self.upload_point_cloud_per_frame(device, queue, item);
387 self.content.point_cloud_store.insert(gpu)
388 }
389
390 pub fn drop_point_cloud(&mut self, id: crate::resources::PointCloudId) -> bool {
392 self.content.point_cloud_store.remove(id)
393 }
394
395 pub fn replace_point_cloud(
397 &mut self,
398 device: &wgpu::Device,
399 queue: &wgpu::Queue,
400 id: crate::resources::PointCloudId,
401 item: &crate::renderer::PointCloudItem,
402 ) -> bool {
403 if !self.content.point_cloud_store.contains(id) {
404 return false;
405 }
406 self.ensure_point_cloud_pipeline(device);
407 let gpu = self.upload_point_cloud_per_frame(device, queue, item);
408 self.content.point_cloud_store.replace(id, gpu)
409 }
410
411 pub fn begin_upload_point_cloud(
413 &mut self,
414 device: &wgpu::Device,
415 queue: &wgpu::Queue,
416 item: crate::renderer::PointCloudItem,
417 ) -> crate::resources::JobId {
418 let slot = crate::resources::ResultSlot::<crate::resources::PointCloudId>::new();
419 let slot_for_apply = slot.clone();
420 let device_for_apply = device.clone();
421 let queue_for_apply = queue.clone();
422 let id = {
423 let mut runner = self.jobs.lock().expect("upload job runner poisoned");
424 runner.submit_cpu(move |progress| {
425 progress.set(0.9);
426 Ok(crate::resources::upload_jobs::JobProduct::with_apply(
427 Box::new(move |resources: &mut DeviceResources| {
428 let pid = resources.upload_point_cloud(
429 &device_for_apply,
430 &queue_for_apply,
431 &item,
432 );
433 slot_for_apply.set(pid);
434 }),
435 ))
436 })
437 };
438 self.job_results
439 .point_cloud
440 .lock()
441 .expect("point cloud result map poisoned")
442 .insert(id, slot);
443 id
444 }
445
446 pub fn upload_result_point_cloud(
449 &mut self,
450 id: crate::resources::JobId,
451 ) -> crate::error::ViewportResult<crate::resources::PointCloudId> {
452 let mut map = self
453 .job_results
454 .point_cloud
455 .lock()
456 .expect("point cloud result map poisoned");
457 let slot = match map.get(&id) {
458 Some(s) => s.clone(),
459 None => {
460 return Err(crate::error::ViewportError::JobResultMissing {
461 reason: "unknown id or wrong upload type",
462 });
463 }
464 };
465 match slot.take() {
466 Some(pid) => {
467 map.remove(&id);
468 Ok(pid)
469 }
470 None => Err(crate::error::ViewportError::JobNotReady),
471 }
472 }
473}
474
475#[cfg(test)]
476mod tests {
477 use crate::DeviceResources;
478 use crate::renderer::PointCloudItem;
479 use crate::resources::UploadStatus;
480
481 fn try_make_device() -> Option<(wgpu::Device, wgpu::Queue)> {
482 let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::default());
483 let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
484 power_preference: wgpu::PowerPreference::LowPower,
485 compatible_surface: None,
486 force_fallback_adapter: false,
487 }))
488 .ok()?;
489 pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor::default())).ok()
490 }
491
492 fn sample_point_cloud() -> PointCloudItem {
493 let mut item = PointCloudItem::default();
494 item.positions = vec![
495 [0.0, 0.0, 0.0],
496 [1.0, 0.0, 0.0],
497 [0.0, 1.0, 0.0],
498 [0.0, 0.0, 1.0],
499 ];
500 item.point_size = 6.0;
501 item
502 }
503
504 #[test]
505 fn upload_point_cloud_returns_valid_handle() {
506 let Some((device, queue)) = try_make_device() else {
507 eprintln!("skipping: no wgpu adapter available");
508 return;
509 };
510 let mut resources = DeviceResources::new(&device, wgpu::TextureFormat::Rgba8UnormSrgb, 1);
511 let id = resources.upload_point_cloud(&device, &queue, &sample_point_cloud());
512 assert!(resources.content.point_cloud_store.contains(id));
513 assert!(resources.drop_point_cloud(id));
514 assert!(!resources.content.point_cloud_store.contains(id));
515 }
516
517 #[test]
518 fn begin_upload_point_cloud_drains_to_handle() {
519 let Some((device, queue)) = try_make_device() else {
520 eprintln!("skipping: no wgpu adapter available");
521 return;
522 };
523 let mut resources = DeviceResources::new(&device, wgpu::TextureFormat::Rgba8UnormSrgb, 1);
524 let job = resources.begin_upload_point_cloud(&device, &queue, sample_point_cloud());
525 for _ in 0..200 {
526 resources.process_uploads(&device, &queue);
527 match resources.upload_status(job) {
528 UploadStatus::Ready => break,
529 UploadStatus::Failed(e) => panic!("upload failed: {e:?}"),
530 UploadStatus::Pending { .. } => {
531 std::thread::sleep(std::time::Duration::from_millis(5));
532 }
533 UploadStatus::Unknown => panic!("job id disappeared"),
534 }
535 }
536 let id = resources.upload_result_point_cloud(job).expect("ready");
537 assert!(resources.content.point_cloud_store.contains(id));
538 let err = resources.upload_result_point_cloud(job).unwrap_err();
539 assert!(matches!(
540 err,
541 crate::error::ViewportError::JobResultMissing { .. }
542 ));
543 }
544}
545
546#[derive(Clone)]
548pub struct PointCloudGpuData {
549 pub(crate) vertex_buffer: wgpu::Buffer,
552 pub(crate) point_count: u32,
554 pub(crate) bind_group: wgpu::BindGroup,
556 pub(crate) _uniform_buf: wgpu::Buffer,
558 pub(crate) _scalar_buf: wgpu::Buffer,
559 pub(crate) _colour_buf: wgpu::Buffer,
560 pub(crate) _radius_buf: wgpu::Buffer,
561 pub(crate) _transparency_buf: wgpu::Buffer,
562}