1use super::*;
2use crate::renderer::ShDegree;
3
4pub(crate) struct GaussianSplatResources {
8 pub(crate) pipeline: Option<DualPipeline>,
10 pub(crate) bgl: Option<wgpu::BindGroupLayout>,
12 pub(crate) depth_pipeline: Option<wgpu::ComputePipeline>,
14 pub(crate) sort_clear_pipeline: Option<wgpu::ComputePipeline>,
16 pub(crate) sort_histogram_pipeline: Option<wgpu::ComputePipeline>,
18 pub(crate) sort_prefix_pipeline: Option<wgpu::ComputePipeline>,
20 pub(crate) sort_scatter_pipeline: Option<wgpu::ComputePipeline>,
22 pub(crate) sort_init_pipeline: Option<wgpu::ComputePipeline>,
24 pub(crate) depth_bgl: Option<wgpu::BindGroupLayout>,
26 pub(crate) sort_bgl: Option<wgpu::BindGroupLayout>,
28}
29
30impl Default for GaussianSplatResources {
31 fn default() -> Self {
32 Self {
33 pipeline: None,
34 bgl: None,
35 depth_pipeline: None,
36 sort_clear_pipeline: None,
37 sort_histogram_pipeline: None,
38 sort_prefix_pipeline: None,
39 sort_scatter_pipeline: None,
40 sort_init_pipeline: None,
41 depth_bgl: None,
42 sort_bgl: None,
43 }
44 }
45}
46
47fn validate_gaussian_splat_data(
50 data: &crate::renderer::GaussianSplatData,
51) -> crate::error::ViewportResult<()> {
52 if data.positions.is_empty() {
53 return Err(crate::error::ViewportError::InvalidGaussianSplatData {
54 reason: "empty splat list",
55 });
56 }
57 let n = data.positions.len();
58 if data.scales.len() != n || data.rotations.len() != n || data.opacities.len() != n {
59 return Err(crate::error::ViewportError::InvalidGaussianSplatData {
60 reason: "mismatched buffer lengths",
61 });
62 }
63 Ok(())
64}
65
66fn build_gaussian_splat_set(
72 device: &wgpu::Device,
73 queue: &wgpu::Queue,
74 data: &crate::renderer::GaussianSplatData,
75) -> GaussianSplatGpuSet {
76 let count = data.positions.len() as u32;
77
78 let pos_data: Vec<[f32; 4]> = data
80 .positions
81 .iter()
82 .map(|p| [p[0], p[1], p[2], 1.0])
83 .collect();
84 let scale_data: Vec<[f32; 4]> = data
85 .scales
86 .iter()
87 .map(|s| [s[0], s[1], s[2], 0.0])
88 .collect();
89 let rotation_data: Vec<[f32; 4]> = data
90 .rotations
91 .iter()
92 .map(|r| [r[0], r[1], r[2], r[3]])
93 .collect();
94
95 let buf_size_pos = (pos_data.len() * std::mem::size_of::<[f32; 4]>()).max(16) as u64;
96 let buf_size_scale = (scale_data.len() * std::mem::size_of::<[f32; 4]>()).max(16) as u64;
97 let buf_size_rot = (rotation_data.len() * std::mem::size_of::<[f32; 4]>()).max(16) as u64;
98 let buf_size_opa = (data.opacities.len() * 4).max(4) as u64;
99 let buf_size_sh = (data.sh_coefficients.len() * 4).max(4) as u64;
100
101 let position_buf = device.create_buffer(&wgpu::BufferDescriptor {
102 label: Some("splat_position_buf"),
103 size: buf_size_pos,
104 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
105 mapped_at_creation: false,
106 });
107 queue.write_buffer(&position_buf, 0, bytemuck::cast_slice(&pos_data));
108
109 let scale_buf = device.create_buffer(&wgpu::BufferDescriptor {
110 label: Some("splat_scale_buf"),
111 size: buf_size_scale,
112 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
113 mapped_at_creation: false,
114 });
115 queue.write_buffer(&scale_buf, 0, bytemuck::cast_slice(&scale_data));
116
117 let rotation_buf = device.create_buffer(&wgpu::BufferDescriptor {
118 label: Some("splat_rotation_buf"),
119 size: buf_size_rot,
120 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
121 mapped_at_creation: false,
122 });
123 queue.write_buffer(&rotation_buf, 0, bytemuck::cast_slice(&rotation_data));
124
125 let opacity_buf = device.create_buffer(&wgpu::BufferDescriptor {
126 label: Some("splat_opacity_buf"),
127 size: buf_size_opa,
128 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
129 mapped_at_creation: false,
130 });
131 queue.write_buffer(&opacity_buf, 0, bytemuck::cast_slice(&data.opacities));
132
133 let sh_buf = device.create_buffer(&wgpu::BufferDescriptor {
134 label: Some("splat_sh_buf"),
135 size: buf_size_sh,
136 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
137 mapped_at_creation: false,
138 });
139 if !data.sh_coefficients.is_empty() {
140 queue.write_buffer(&sh_buf, 0, bytemuck::cast_slice(&data.sh_coefficients));
141 }
142
143 GaussianSplatGpuSet {
144 position_buf,
145 scale_buf,
146 rotation_buf,
147 opacity_buf,
148 sh_buf,
149 sh_degree: data.sh_degree,
150 count,
151 viewport_sort: Vec::new(),
152 cpu_positions: data.positions.clone(),
153 cpu_scales: data.scales.clone(),
154 }
155}
156
157#[repr(C)]
159#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
160struct SplatUniform {
161 model: [[f32; 4]; 4],
162 viewport_w: f32,
163 viewport_h: f32,
164 sh_degree: u32,
165 count: u32,
166}
167
168#[repr(C)]
170#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
171struct DepthUniform {
172 model: [[f32; 4]; 4],
173 eye: [f32; 3],
174 count: u32,
175}
176
177#[repr(C)]
179#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
180struct SortUniform {
181 shift: u32,
182 count: u32,
183 pass_num: u32,
184 _pad: u32,
185}
186
187impl DeviceResources {
188 pub(crate) fn ensure_gaussian_splat_pipelines(&mut self, device: &wgpu::Device) {
192 if self.gaussian_splat.bgl.is_some() {
193 return;
194 }
195
196 let splat_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
203 label: Some("gaussian_splat_bgl"),
204 entries: &[
205 wgpu::BindGroupLayoutEntry {
206 binding: 0,
207 visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
208 ty: wgpu::BindingType::Buffer {
209 ty: wgpu::BufferBindingType::Uniform,
210 has_dynamic_offset: false,
211 min_binding_size: None,
212 },
213 count: None,
214 },
215 wgpu::BindGroupLayoutEntry {
216 binding: 1,
217 visibility: wgpu::ShaderStages::VERTEX,
218 ty: wgpu::BindingType::Buffer {
219 ty: wgpu::BufferBindingType::Storage { read_only: true },
220 has_dynamic_offset: false,
221 min_binding_size: None,
222 },
223 count: None,
224 },
225 wgpu::BindGroupLayoutEntry {
226 binding: 2,
227 visibility: wgpu::ShaderStages::VERTEX,
228 ty: wgpu::BindingType::Buffer {
229 ty: wgpu::BufferBindingType::Storage { read_only: true },
230 has_dynamic_offset: false,
231 min_binding_size: None,
232 },
233 count: None,
234 },
235 wgpu::BindGroupLayoutEntry {
236 binding: 3,
237 visibility: wgpu::ShaderStages::VERTEX,
238 ty: wgpu::BindingType::Buffer {
239 ty: wgpu::BufferBindingType::Storage { read_only: true },
240 has_dynamic_offset: false,
241 min_binding_size: None,
242 },
243 count: None,
244 },
245 wgpu::BindGroupLayoutEntry {
246 binding: 4,
247 visibility: wgpu::ShaderStages::VERTEX,
248 ty: wgpu::BindingType::Buffer {
249 ty: wgpu::BufferBindingType::Storage { read_only: true },
250 has_dynamic_offset: false,
251 min_binding_size: None,
252 },
253 count: None,
254 },
255 wgpu::BindGroupLayoutEntry {
256 binding: 5,
257 visibility: wgpu::ShaderStages::VERTEX,
258 ty: wgpu::BindingType::Buffer {
259 ty: wgpu::BufferBindingType::Storage { read_only: true },
260 has_dynamic_offset: false,
261 min_binding_size: None,
262 },
263 count: None,
264 },
265 wgpu::BindGroupLayoutEntry {
266 binding: 6,
267 visibility: wgpu::ShaderStages::VERTEX,
268 ty: wgpu::BindingType::Buffer {
269 ty: wgpu::BufferBindingType::Storage { read_only: true },
270 has_dynamic_offset: false,
271 min_binding_size: None,
272 },
273 count: None,
274 },
275 ],
276 });
277
278 let render_shader = crate::resources::builders::wgsl_module(
279 device,
280 "gaussian_splat_shader",
281 crate::resources::builders::wgsl_source!("gaussian_splat"),
282 );
283
284 let render_layout = crate::resources::builders::standard_scene_layout(
285 device,
286 "gaussian_splat_pipeline_layout",
287 &self.camera_bind_group_layout,
288 &splat_bgl,
289 );
290
291 let gaussian_splat_pipeline = crate::resources::builders::build_dual_pipeline(
293 device,
294 &crate::resources::builders::DualPipelineDesc {
295 label: "gaussian_splat_pipeline",
296 layout: &render_layout,
297 shader: &render_shader,
298 vertex_entry: "vs_main",
299 fragment_entry: "fs_main",
300 vertex_buffers: &[],
301 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
302 topology: wgpu::PrimitiveTopology::TriangleList,
303 cull_mode: None,
304 depth_write: false,
305 depth_compare: wgpu::CompareFunction::Less,
306 sample_count: 1,
307 ldr_format: self.target_format,
308 },
309 );
310
311 let sort_shader = crate::resources::builders::wgsl_module(
316 device,
317 "gaussian_splat_sort_shader",
318 crate::resources::builders::wgsl_source!("gaussian_splat_sort"),
319 );
320
321 let depth_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
323 label: Some("gaussian_splat_depth_bgl"),
324 entries: &[
325 wgpu::BindGroupLayoutEntry {
326 binding: 0,
327 visibility: wgpu::ShaderStages::COMPUTE,
328 ty: wgpu::BindingType::Buffer {
329 ty: wgpu::BufferBindingType::Uniform,
330 has_dynamic_offset: false,
331 min_binding_size: None,
332 },
333 count: None,
334 },
335 wgpu::BindGroupLayoutEntry {
336 binding: 1,
337 visibility: wgpu::ShaderStages::COMPUTE,
338 ty: wgpu::BindingType::Buffer {
339 ty: wgpu::BufferBindingType::Storage { read_only: true },
340 has_dynamic_offset: false,
341 min_binding_size: None,
342 },
343 count: None,
344 },
345 wgpu::BindGroupLayoutEntry {
346 binding: 2,
347 visibility: wgpu::ShaderStages::COMPUTE,
348 ty: wgpu::BindingType::Buffer {
349 ty: wgpu::BufferBindingType::Storage { read_only: false },
350 has_dynamic_offset: false,
351 min_binding_size: None,
352 },
353 count: None,
354 },
355 ],
356 });
357
358 let depth_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
359 label: Some("gaussian_splat_depth_layout"),
360 bind_group_layouts: &[&depth_bgl],
361 push_constant_ranges: &[],
362 });
363
364 let gaussian_splat_depth_pipeline = crate::resources::builders::compute_pipeline(
365 device,
366 "gaussian_splat_depth_pipeline",
367 &depth_layout,
368 &sort_shader,
369 "compute_depths",
370 );
371
372 let sort_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
375 label: Some("gaussian_splat_sort_bgl"),
376 entries: &[
377 wgpu::BindGroupLayoutEntry {
378 binding: 0,
379 visibility: wgpu::ShaderStages::COMPUTE,
380 ty: wgpu::BindingType::Buffer {
381 ty: wgpu::BufferBindingType::Uniform,
382 has_dynamic_offset: false,
383 min_binding_size: None,
384 },
385 count: None,
386 },
387 wgpu::BindGroupLayoutEntry {
388 binding: 1,
389 visibility: wgpu::ShaderStages::COMPUTE,
390 ty: wgpu::BindingType::Buffer {
391 ty: wgpu::BufferBindingType::Storage { read_only: false },
392 has_dynamic_offset: false,
393 min_binding_size: None,
394 },
395 count: None,
396 },
397 wgpu::BindGroupLayoutEntry {
398 binding: 2,
399 visibility: wgpu::ShaderStages::COMPUTE,
400 ty: wgpu::BindingType::Buffer {
401 ty: wgpu::BufferBindingType::Storage { read_only: false },
402 has_dynamic_offset: false,
403 min_binding_size: None,
404 },
405 count: None,
406 },
407 wgpu::BindGroupLayoutEntry {
408 binding: 3,
409 visibility: wgpu::ShaderStages::COMPUTE,
410 ty: wgpu::BindingType::Buffer {
411 ty: wgpu::BufferBindingType::Storage { read_only: false },
412 has_dynamic_offset: false,
413 min_binding_size: None,
414 },
415 count: None,
416 },
417 wgpu::BindGroupLayoutEntry {
418 binding: 4,
419 visibility: wgpu::ShaderStages::COMPUTE,
420 ty: wgpu::BindingType::Buffer {
421 ty: wgpu::BufferBindingType::Storage { read_only: false },
422 has_dynamic_offset: false,
423 min_binding_size: None,
424 },
425 count: None,
426 },
427 wgpu::BindGroupLayoutEntry {
428 binding: 5,
429 visibility: wgpu::ShaderStages::COMPUTE,
430 ty: wgpu::BindingType::Buffer {
431 ty: wgpu::BufferBindingType::Storage { read_only: false },
432 has_dynamic_offset: false,
433 min_binding_size: None,
434 },
435 count: None,
436 },
437 ],
438 });
439
440 let sort_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
441 label: Some("gaussian_splat_sort_layout"),
442 bind_group_layouts: &[&sort_bgl],
443 push_constant_ranges: &[],
444 });
445
446 let gaussian_splat_sort_init_pipeline = crate::resources::builders::compute_pipeline(
447 device,
448 "gaussian_splat_sort_init_pipeline",
449 &sort_layout,
450 &sort_shader,
451 "init_indices",
452 );
453
454 let gaussian_splat_sort_clear_pipeline = crate::resources::builders::compute_pipeline(
455 device,
456 "gaussian_splat_sort_clear_pipeline",
457 &sort_layout,
458 &sort_shader,
459 "clear_histogram",
460 );
461
462 let gaussian_splat_sort_histogram_pipeline = crate::resources::builders::compute_pipeline(
463 device,
464 "gaussian_splat_sort_histogram_pipeline",
465 &sort_layout,
466 &sort_shader,
467 "histogram_pass",
468 );
469
470 let gaussian_splat_sort_prefix_pipeline = crate::resources::builders::compute_pipeline(
471 device,
472 "gaussian_splat_sort_prefix_pipeline",
473 &sort_layout,
474 &sort_shader,
475 "prefix_sum_pass",
476 );
477
478 let gaussian_splat_sort_scatter_pipeline = crate::resources::builders::compute_pipeline(
479 device,
480 "gaussian_splat_sort_scatter_pipeline",
481 &sort_layout,
482 &sort_shader,
483 "scatter_pass",
484 );
485
486 self.gaussian_splat.bgl = Some(splat_bgl);
487 self.gaussian_splat.pipeline = Some(gaussian_splat_pipeline);
488 self.gaussian_splat.depth_bgl = Some(depth_bgl);
489 self.gaussian_splat.depth_pipeline = Some(gaussian_splat_depth_pipeline);
490 self.gaussian_splat.sort_bgl = Some(sort_bgl);
491 self.gaussian_splat.sort_init_pipeline = Some(gaussian_splat_sort_init_pipeline);
492 self.gaussian_splat.sort_clear_pipeline = Some(gaussian_splat_sort_clear_pipeline);
493 self.gaussian_splat.sort_histogram_pipeline = Some(gaussian_splat_sort_histogram_pipeline);
494 self.gaussian_splat.sort_prefix_pipeline = Some(gaussian_splat_sort_prefix_pipeline);
495 self.gaussian_splat.sort_scatter_pipeline = Some(gaussian_splat_sort_scatter_pipeline);
496 }
497
498 pub fn upload_gaussian_splat(
520 &mut self,
521 device: &wgpu::Device,
522 queue: &wgpu::Queue,
523 data: &crate::renderer::GaussianSplatData,
524 ) -> crate::error::ViewportResult<crate::renderer::GaussianSplatId> {
525 validate_gaussian_splat_data(data)?;
526 let gpu_set = build_gaussian_splat_set(device, queue, data);
527 Ok(self.content.gaussian_splat_store.insert(gpu_set))
528 }
529
530 pub fn replace_gaussian_splat(
547 &mut self,
548 device: &wgpu::Device,
549 queue: &wgpu::Queue,
550 id: crate::renderer::GaussianSplatId,
551 data: &crate::renderer::GaussianSplatData,
552 ) -> crate::error::ViewportResult<()> {
553 validate_gaussian_splat_data(data)?;
554 let gpu_set = build_gaussian_splat_set(device, queue, data);
555 if self.content.gaussian_splat_store.replace(id, gpu_set) {
556 Ok(())
557 } else {
558 Err(crate::error::ViewportError::StaleHandle {
559 index: id.index() as usize,
560 count: self.content.gaussian_splat_store.slot_count(),
561 })
562 }
563 }
564
565 pub fn free_gaussian_splat(&mut self, id: crate::renderer::GaussianSplatId) {
567 self.content.gaussian_splat_store.remove(id);
568 }
569
570 pub fn begin_upload_gaussian_splat(
584 &mut self,
585 device: &wgpu::Device,
586 queue: &wgpu::Queue,
587 data: crate::renderer::GaussianSplatData,
588 ) -> crate::error::ViewportResult<crate::resources::JobId> {
589 validate_gaussian_splat_data(&data)?;
590
591 let slot = crate::resources::ResultSlot::<crate::renderer::GaussianSplatId>::new();
592 let slot_for_apply = slot.clone();
593 let device_for_worker = device.clone();
594 let queue_for_worker = queue.clone();
595
596 let id = {
597 let mut runner = self.jobs.lock().expect("upload job runner poisoned");
598 runner.submit_cpu(move |progress| {
599 progress.set(0.1);
600 let gpu_set =
601 build_gaussian_splat_set(&device_for_worker, &queue_for_worker, &data);
602 progress.set(0.95);
603
604 Ok(crate::resources::upload_jobs::JobProduct::with_apply(
605 Box::new(move |resources: &mut DeviceResources| {
606 let id = resources.content.gaussian_splat_store.insert(gpu_set);
607 slot_for_apply.set(id);
608 }),
609 ))
610 })
611 };
612
613 self.job_results
614 .gaussian_splat
615 .lock()
616 .expect("gaussian splat result map poisoned")
617 .insert(id, slot);
618 Ok(id)
619 }
620
621 pub fn upload_result_gaussian_splat(
624 &mut self,
625 id: crate::resources::JobId,
626 ) -> crate::error::ViewportResult<crate::renderer::GaussianSplatId> {
627 let mut map = self
628 .job_results
629 .gaussian_splat
630 .lock()
631 .expect("gaussian splat result map poisoned");
632 let slot = match map.get(&id) {
633 Some(s) => s.clone(),
634 None => {
635 return Err(crate::error::ViewportError::JobResultMissing {
636 reason: "unknown id or wrong upload type",
637 });
638 }
639 };
640 match slot.take() {
641 Some(splat_id) => {
642 map.remove(&id);
643 Ok(splat_id)
644 }
645 None => Err(crate::error::ViewportError::JobNotReady),
646 }
647 }
648
649 pub(crate) fn ensure_gaussian_splat_sort_buffers(
654 &mut self,
655 device: &wgpu::Device,
656 store_index: usize,
657 viewport_index: usize,
658 ) {
659 let set = match self.content.gaussian_splat_store.get_by_index(store_index) {
660 Some(s) => s,
661 None => return,
662 };
663 let count = set.count as usize;
664
665 if viewport_index >= set.viewport_sort.len() || set.viewport_sort[viewport_index].is_none()
667 {
668 let set_mut = self
670 .content
671 .gaussian_splat_store
672 .get_mut_by_index(store_index)
673 .unwrap();
674 while set_mut.viewport_sort.len() <= viewport_index {
675 set_mut.viewport_sort.push(None);
676 }
677
678 if set_mut.viewport_sort[viewport_index].is_none() {
679 let buf_size = (count * 4).max(4) as u64;
680
681 let depth_buf = device.create_buffer(&wgpu::BufferDescriptor {
682 label: Some("splat_depth_buf"),
683 size: buf_size,
684 usage: wgpu::BufferUsages::STORAGE
685 | wgpu::BufferUsages::COPY_DST
686 | wgpu::BufferUsages::COPY_SRC,
687 mapped_at_creation: false,
688 });
689 let sort_buf_usage = wgpu::BufferUsages::STORAGE
690 | wgpu::BufferUsages::COPY_SRC
691 | wgpu::BufferUsages::COPY_DST;
692 let keys_ping = device.create_buffer(&wgpu::BufferDescriptor {
693 label: Some("splat_keys_ping"),
694 size: buf_size,
695 usage: sort_buf_usage,
696 mapped_at_creation: false,
697 });
698 let keys_pong = device.create_buffer(&wgpu::BufferDescriptor {
699 label: Some("splat_keys_pong"),
700 size: buf_size,
701 usage: sort_buf_usage,
702 mapped_at_creation: false,
703 });
704 let vals_ping = device.create_buffer(&wgpu::BufferDescriptor {
705 label: Some("splat_vals_ping"),
706 size: buf_size,
707 usage: sort_buf_usage,
708 mapped_at_creation: false,
709 });
710 let vals_pong = device.create_buffer(&wgpu::BufferDescriptor {
711 label: Some("splat_vals_pong"),
712 size: buf_size,
713 usage: sort_buf_usage,
714 mapped_at_creation: false,
715 });
716 let histogram_buf = device.create_buffer(&wgpu::BufferDescriptor {
718 label: Some("splat_histogram"),
719 size: 256 * 4,
720 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
721 mapped_at_creation: false,
722 });
723 let uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
725 label: Some("splat_uniform_buf"),
726 size: std::mem::size_of::<SplatUniform>() as u64,
727 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
728 mapped_at_creation: false,
729 });
730
731 let render_bg = {
734 let bgl = self.gaussian_splat.bgl.as_ref().unwrap();
735 let set_ref = self
736 .content
737 .gaussian_splat_store
738 .get_by_index(store_index)
739 .unwrap();
740 device.create_bind_group(&wgpu::BindGroupDescriptor {
741 label: Some("splat_render_bg"),
742 layout: bgl,
743 entries: &[
744 wgpu::BindGroupEntry {
745 binding: 0,
746 resource: uniform_buf.as_entire_binding(),
747 },
748 wgpu::BindGroupEntry {
749 binding: 1,
750 resource: vals_ping.as_entire_binding(),
751 },
752 wgpu::BindGroupEntry {
753 binding: 2,
754 resource: set_ref.position_buf.as_entire_binding(),
755 },
756 wgpu::BindGroupEntry {
757 binding: 3,
758 resource: set_ref.scale_buf.as_entire_binding(),
759 },
760 wgpu::BindGroupEntry {
761 binding: 4,
762 resource: set_ref.rotation_buf.as_entire_binding(),
763 },
764 wgpu::BindGroupEntry {
765 binding: 5,
766 resource: set_ref.opacity_buf.as_entire_binding(),
767 },
768 wgpu::BindGroupEntry {
769 binding: 6,
770 resource: set_ref.sh_buf.as_entire_binding(),
771 },
772 ],
773 })
774 };
775
776 let vp_sort = GaussianSplatViewportSort {
777 depth_buf,
778 keys_ping,
779 keys_pong,
780 vals_ping,
781 vals_pong,
782 histogram_buf,
783 render_bg,
784 last_eye: [f32::NAN; 3],
785 uniform_buf,
786 };
787
788 let set_mut2 = self
789 .content
790 .gaussian_splat_store
791 .get_mut_by_index(store_index)
792 .unwrap();
793 set_mut2.viewport_sort[viewport_index] = Some(vp_sort);
794 }
795 }
796 }
797
798 pub(crate) fn run_gaussian_splat_sort(
803 &mut self,
804 device: &wgpu::Device,
805 queue: &wgpu::Queue,
806 store_index: usize,
807 viewport_index: usize,
808 eye: [f32; 3],
809 model: [[f32; 4]; 4],
810 vp_w: f32,
811 vp_h: f32,
812 sh_degree: ShDegree,
813 ) {
814 self.ensure_gaussian_splat_sort_buffers(device, store_index, viewport_index);
816
817 let set = match self.content.gaussian_splat_store.get_by_index(store_index) {
818 Some(s) => s,
819 None => return,
820 };
821 let count = set.count;
822 if count == 0 {
823 return;
824 }
825
826 let vp_sort = match set.viewport_sort.get(viewport_index) {
827 Some(Some(s)) => s,
828 _ => return,
829 };
830
831 let splat_uni = SplatUniform {
833 model,
834 viewport_w: vp_w,
835 viewport_h: vp_h,
836 sh_degree: match sh_degree {
837 ShDegree::Zero => 0,
838 ShDegree::One => 1,
839 ShDegree::Three => 3,
840 },
841 count,
842 };
843 queue.write_buffer(&vp_sort.uniform_buf, 0, bytemuck::bytes_of(&splat_uni));
844
845 let depth_uni = DepthUniform { model, eye, count };
847 let depth_uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
848 label: Some("splat_depth_uniform_tmp"),
849 size: std::mem::size_of::<DepthUniform>() as u64,
850 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
851 mapped_at_creation: false,
852 });
853 queue.write_buffer(&depth_uniform_buf, 0, bytemuck::bytes_of(&depth_uni));
854
855 let depth_bg = {
857 let bgl = self.gaussian_splat.depth_bgl.as_ref().unwrap();
858 let set_ref = self
859 .content
860 .gaussian_splat_store
861 .get_by_index(store_index)
862 .unwrap();
863 let vp_sort_ref = set_ref.viewport_sort[viewport_index].as_ref().unwrap();
864 device.create_bind_group(&wgpu::BindGroupDescriptor {
865 label: Some("splat_depth_bg"),
866 layout: bgl,
867 entries: &[
868 wgpu::BindGroupEntry {
869 binding: 0,
870 resource: depth_uniform_buf.as_entire_binding(),
871 },
872 wgpu::BindGroupEntry {
873 binding: 1,
874 resource: set_ref.position_buf.as_entire_binding(),
875 },
876 wgpu::BindGroupEntry {
877 binding: 2,
878 resource: vp_sort_ref.depth_buf.as_entire_binding(),
879 },
880 ],
881 })
882 };
883
884 let workgroups = (count + 255) / 256;
885
886 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
887 label: Some("splat_sort_encoder"),
888 });
889
890 {
892 let depth_pipeline = self.gaussian_splat.depth_pipeline.as_ref().unwrap();
893 let mut cpass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
894 label: Some("splat_depth_pass"),
895 timestamp_writes: None,
896 });
897 cpass.set_pipeline(depth_pipeline);
898 cpass.set_bind_group(0, &depth_bg, &[]);
899 cpass.dispatch_workgroups(workgroups, 1, 1);
900 }
901
902 {
904 let set_ref = self
905 .content
906 .gaussian_splat_store
907 .get_by_index(store_index)
908 .unwrap();
909 let vp_sort_ref = set_ref.viewport_sort[viewport_index].as_ref().unwrap();
910 encoder.copy_buffer_to_buffer(
911 &vp_sort_ref.depth_buf,
912 0,
913 &vp_sort_ref.keys_ping,
914 0,
915 (count as u64) * 4,
916 );
917 }
918
919 let sort_bgl = self.gaussian_splat.sort_bgl.as_ref().unwrap();
923
924 for pass in 0u32..4u32 {
925 let sort_uni = SortUniform {
926 shift: pass * 8,
927 count,
928 pass_num: pass,
929 _pad: 0,
930 };
931 let sort_uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
932 label: Some("splat_sort_uniform_tmp"),
933 size: std::mem::size_of::<SortUniform>() as u64,
934 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
935 mapped_at_creation: false,
936 });
937 queue.write_buffer(&sort_uniform_buf, 0, bytemuck::bytes_of(&sort_uni));
938
939 let set_ref = self
940 .content
941 .gaussian_splat_store
942 .get_by_index(store_index)
943 .unwrap();
944 let vp_sort_ref = set_ref.viewport_sort[viewport_index].as_ref().unwrap();
945
946 let sort_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
947 label: Some("splat_sort_bg"),
948 layout: sort_bgl,
949 entries: &[
950 wgpu::BindGroupEntry {
951 binding: 0,
952 resource: sort_uniform_buf.as_entire_binding(),
953 },
954 wgpu::BindGroupEntry {
955 binding: 1,
956 resource: vp_sort_ref.keys_ping.as_entire_binding(),
957 },
958 wgpu::BindGroupEntry {
959 binding: 2,
960 resource: vp_sort_ref.keys_pong.as_entire_binding(),
961 },
962 wgpu::BindGroupEntry {
963 binding: 3,
964 resource: vp_sort_ref.vals_ping.as_entire_binding(),
965 },
966 wgpu::BindGroupEntry {
967 binding: 4,
968 resource: vp_sort_ref.vals_pong.as_entire_binding(),
969 },
970 wgpu::BindGroupEntry {
971 binding: 5,
972 resource: vp_sort_ref.histogram_buf.as_entire_binding(),
973 },
974 ],
975 });
976
977 if pass == 0 {
979 let init_pipeline = self.gaussian_splat.sort_init_pipeline.as_ref().unwrap();
980 let mut cpass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
981 label: Some("splat_init_pass"),
982 timestamp_writes: None,
983 });
984 cpass.set_pipeline(init_pipeline);
985 cpass.set_bind_group(0, &sort_bg, &[]);
986 cpass.dispatch_workgroups(workgroups, 1, 1);
987 }
988
989 {
991 let clear_pipeline = self.gaussian_splat.sort_clear_pipeline.as_ref().unwrap();
992 let mut cpass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
993 label: Some("splat_clear_hist"),
994 timestamp_writes: None,
995 });
996 cpass.set_pipeline(clear_pipeline);
997 cpass.set_bind_group(0, &sort_bg, &[]);
998 cpass.dispatch_workgroups(1, 1, 1);
999 }
1000
1001 {
1003 let hist_pipeline = self
1004 .gaussian_splat
1005 .sort_histogram_pipeline
1006 .as_ref()
1007 .unwrap();
1008 let mut cpass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
1009 label: Some("splat_hist_pass"),
1010 timestamp_writes: None,
1011 });
1012 cpass.set_pipeline(hist_pipeline);
1013 cpass.set_bind_group(0, &sort_bg, &[]);
1014 cpass.dispatch_workgroups(workgroups, 1, 1);
1015 }
1016
1017 {
1019 let prefix_pipeline = self.gaussian_splat.sort_prefix_pipeline.as_ref().unwrap();
1020 let mut cpass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
1021 label: Some("splat_prefix_pass"),
1022 timestamp_writes: None,
1023 });
1024 cpass.set_pipeline(prefix_pipeline);
1025 cpass.set_bind_group(0, &sort_bg, &[]);
1026 cpass.dispatch_workgroups(1, 1, 1);
1027 }
1028
1029 {
1031 let scatter_pipeline = self.gaussian_splat.sort_scatter_pipeline.as_ref().unwrap();
1032 let mut cpass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
1033 label: Some("splat_scatter_pass"),
1034 timestamp_writes: None,
1035 });
1036 cpass.set_pipeline(scatter_pipeline);
1037 cpass.set_bind_group(0, &sort_bg, &[]);
1038 cpass.dispatch_workgroups(workgroups, 1, 1);
1039 }
1040 }
1041
1042 queue.submit(std::iter::once(encoder.finish()));
1043
1044 if let Some(set_mut) = self
1046 .content
1047 .gaussian_splat_store
1048 .get_mut_by_index(store_index)
1049 {
1050 if let Some(Some(vp_sort_mut)) = set_mut.viewport_sort.get_mut(viewport_index) {
1051 vp_sort_mut.last_eye = eye;
1052 }
1053 }
1054 }
1055}
1056
1057#[cfg(test)]
1058mod async_tests {
1059 use crate::DeviceResources;
1060 use crate::renderer::GaussianSplatData;
1061 use crate::resources::UploadStatus;
1062
1063 fn try_make_device() -> Option<(wgpu::Device, wgpu::Queue)> {
1064 let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::default());
1065 let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
1066 power_preference: wgpu::PowerPreference::LowPower,
1067 compatible_surface: None,
1068 force_fallback_adapter: false,
1069 }))
1070 .ok()?;
1071 pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor::default())).ok()
1072 }
1073
1074 fn sample_splats(n: usize) -> GaussianSplatData {
1075 let mut data = GaussianSplatData::default();
1076 data.positions = (0..n).map(|i| [i as f32, 0.0, 0.0]).collect();
1077 data.scales = vec![[0.1, 0.1, 0.1]; n];
1078 data.rotations = vec![[0.0, 0.0, 0.0, 1.0]; n];
1079 data.opacities = vec![0.5; n];
1080 data
1081 }
1082
1083 #[test]
1084 fn stale_splat_handle_does_not_alias_after_slot_reuse() {
1085 let Some((device, queue)) = try_make_device() else {
1086 eprintln!("skipping: no wgpu adapter available");
1087 return;
1088 };
1089 let mut resources = DeviceResources::new(&device, wgpu::TextureFormat::Rgba8UnormSrgb, 1);
1090
1091 let id1 = resources
1093 .upload_gaussian_splat(&device, &queue, &sample_splats(8))
1094 .unwrap();
1095 assert!(resources.content.gaussian_splat_store.get(id1).is_some());
1096 resources.free_gaussian_splat(id1);
1097 assert!(
1098 resources.content.gaussian_splat_store.get(id1).is_none(),
1099 "a removed handle must not resolve"
1100 );
1101
1102 let id2 = resources
1104 .upload_gaussian_splat(&device, &queue, &sample_splats(4))
1105 .unwrap();
1106 assert_eq!(id1.index(), id2.index(), "the freed slot should be reused");
1107 assert_ne!(id1, id2, "the reused slot must carry a new generation");
1108 assert!(resources.content.gaussian_splat_store.get(id2).is_some());
1109 assert!(
1110 resources.content.gaussian_splat_store.get(id1).is_none(),
1111 "the stale handle must not alias the set now occupying its slot"
1112 );
1113 }
1114
1115 #[test]
1116 fn begin_upload_gaussian_splat_validates() {
1117 let Some((device, queue)) = try_make_device() else {
1118 eprintln!("skipping: no wgpu adapter available");
1119 return;
1120 };
1121 let mut resources = DeviceResources::new(&device, wgpu::TextureFormat::Rgba8UnormSrgb, 1);
1122 let err = resources
1123 .begin_upload_gaussian_splat(&device, &queue, GaussianSplatData::default())
1124 .unwrap_err();
1125 assert!(matches!(
1126 err,
1127 crate::error::ViewportError::InvalidGaussianSplatData { .. }
1128 ));
1129 }
1130
1131 #[test]
1132 fn begin_upload_gaussian_splat_drains_to_handle() {
1133 let Some((device, queue)) = try_make_device() else {
1134 eprintln!("skipping: no wgpu adapter available");
1135 return;
1136 };
1137 let mut resources = DeviceResources::new(&device, wgpu::TextureFormat::Rgba8UnormSrgb, 1);
1138 let job = resources
1139 .begin_upload_gaussian_splat(&device, &queue, sample_splats(8))
1140 .expect("job submitted");
1141 for _ in 0..200 {
1142 resources.process_uploads(&device, &queue);
1143 match resources.upload_status(job) {
1144 UploadStatus::Ready => break,
1145 UploadStatus::Failed(e) => panic!("upload failed: {e:?}"),
1146 UploadStatus::Pending { .. } => {
1147 std::thread::sleep(std::time::Duration::from_millis(5));
1148 }
1149 UploadStatus::Unknown => panic!("job id disappeared"),
1150 }
1151 }
1152 let _id = resources.upload_result_gaussian_splat(job).expect("ready");
1153 }
1154
1155 #[test]
1156 fn replace_gaussian_splat_keeps_handle_and_updates_bytes() {
1157 let Some((device, queue)) = try_make_device() else {
1158 eprintln!("skipping: no wgpu adapter available");
1159 return;
1160 };
1161 let mut resources = DeviceResources::new(&device, wgpu::TextureFormat::Rgba8UnormSrgb, 1);
1162
1163 let id = resources
1164 .upload_gaussian_splat(&device, &queue, &sample_splats(8))
1165 .unwrap();
1166 let bytes_before = resources.resident_bytes().gaussian_splat_bytes;
1167 assert!(
1168 bytes_before > 0,
1169 "an uploaded set must count resident bytes"
1170 );
1171
1172 resources
1174 .replace_gaussian_splat(&device, &queue, id, &sample_splats(2))
1175 .expect("replace on a live handle succeeds");
1176 assert!(resources.content.gaussian_splat_store.get(id).is_some());
1177 let bytes_after = resources.resident_bytes().gaussian_splat_bytes;
1178 assert!(
1179 bytes_after < bytes_before,
1180 "replacing with fewer splats must reduce resident bytes"
1181 );
1182
1183 resources.free_gaussian_splat(id);
1185 assert_eq!(resources.resident_bytes().gaussian_splat_bytes, 0);
1186 let err = resources
1187 .replace_gaussian_splat(&device, &queue, id, &sample_splats(2))
1188 .unwrap_err();
1189 assert!(matches!(
1190 err,
1191 crate::error::ViewportError::StaleHandle { .. }
1192 ));
1193 }
1194}
1195
1196pub(crate) struct GaussianSplatViewportSort {
1198 pub depth_buf: wgpu::Buffer,
1200 pub keys_ping: wgpu::Buffer,
1202 pub keys_pong: wgpu::Buffer,
1203 pub vals_ping: wgpu::Buffer,
1205 pub vals_pong: wgpu::Buffer,
1206 pub histogram_buf: wgpu::Buffer,
1208 pub render_bg: wgpu::BindGroup,
1211 pub last_eye: [f32; 3],
1213 pub uniform_buf: wgpu::Buffer,
1215}
1216
1217pub(crate) struct GaussianSplatGpuSet {
1219 pub position_buf: wgpu::Buffer,
1221 pub scale_buf: wgpu::Buffer,
1223 pub rotation_buf: wgpu::Buffer,
1225 pub opacity_buf: wgpu::Buffer,
1227 pub sh_buf: wgpu::Buffer,
1229 pub sh_degree: crate::renderer::ShDegree,
1231 pub count: u32,
1233 pub viewport_sort: Vec<Option<GaussianSplatViewportSort>>,
1235 #[allow(dead_code)]
1237 pub cpu_positions: Vec<[f32; 3]>,
1238 #[allow(dead_code)]
1240 pub cpu_scales: Vec<[f32; 3]>,
1241}
1242
1243impl GaussianSplatGpuSet {
1244 pub fn gpu_bytes(&self) -> u64 {
1248 self.position_buf.size()
1249 + self.scale_buf.size()
1250 + self.rotation_buf.size()
1251 + self.opacity_buf.size()
1252 + self.sh_buf.size()
1253 }
1254}
1255
1256pub(crate) struct GaussianSplatDrawData {
1258 pub store_index: usize,
1260 pub viewport_index: usize,
1262 #[allow(dead_code)]
1264 pub model: [[f32; 4]; 4],
1265 pub count: u32,
1267 pub wireframe: bool,
1269}
1270
1271pub(crate) struct GaussianSplatStore {
1279 store:
1280 crate::resources::handle::SlotStore<GaussianSplatGpuSet, crate::renderer::GaussianSplatId>,
1281}
1282
1283impl GaussianSplatStore {
1284 pub fn new() -> Self {
1285 Self {
1286 store: crate::resources::handle::SlotStore::default(),
1287 }
1288 }
1289
1290 pub fn insert(&mut self, set: GaussianSplatGpuSet) -> crate::renderer::GaussianSplatId {
1291 let bytes = set.gpu_bytes();
1292 self.store.insert(set, bytes)
1293 }
1294
1295 pub fn replace(
1299 &mut self,
1300 id: crate::renderer::GaussianSplatId,
1301 set: GaussianSplatGpuSet,
1302 ) -> bool {
1303 let bytes = set.gpu_bytes();
1304 self.store.replace(id, set, bytes).is_some()
1305 }
1306
1307 pub fn allocated_bytes(&self) -> u64 {
1309 self.store.allocated_bytes()
1310 }
1311
1312 pub fn get(&self, id: crate::renderer::GaussianSplatId) -> Option<&GaussianSplatGpuSet> {
1315 self.store.get(id)
1316 }
1317
1318 pub fn get_by_index(&self, idx: usize) -> Option<&GaussianSplatGpuSet> {
1322 self.store.get_by_index(idx)
1323 }
1324
1325 pub fn get_mut_by_index(&mut self, idx: usize) -> Option<&mut GaussianSplatGpuSet> {
1327 self.store.get_mut_by_index(idx)
1328 }
1329
1330 pub fn slot_count(&self) -> usize {
1333 self.store.slot_count()
1334 }
1335
1336 pub fn remove(&mut self, id: crate::renderer::GaussianSplatId) -> bool {
1340 self.store.remove(id).is_some()
1341 }
1342}