1use crate::resources::*;
2
3#[derive(Default)]
7pub(crate) struct VolumeResources {
8 pub(crate) pipeline: Option<DualPipeline>,
10 pub(crate) bgl: Option<wgpu::BindGroupLayout>,
12 pub(crate) cube_vb: Option<wgpu::Buffer>,
14 pub(crate) cube_ib: Option<wgpu::Buffer>,
16 pub(crate) default_opacity_lut: Option<wgpu::Texture>,
18 pub(crate) default_opacity_lut_view: Option<wgpu::TextureView>,
19 pub(crate) surface_slice_pipeline: Option<DualPipeline>,
21 pub(crate) surface_slice_bgl: Option<wgpu::BindGroupLayout>,
23 pub(crate) outline_mask_pipeline: Option<wgpu::RenderPipeline>,
25}
26
27impl DeviceResources {
28 pub fn upload_volume(
35 &mut self,
36 device: &wgpu::Device,
37 queue: &wgpu::Queue,
38 data: &[f32],
39 dims: [u32; 3],
40 ) -> VolumeId {
41 let expected = (dims[0] as usize) * (dims[1] as usize) * (dims[2] as usize);
42 assert_eq!(
43 data.len(),
44 expected,
45 "volume data length {} does not match dims {:?} (expected {})",
46 data.len(),
47 dims,
48 expected
49 );
50
51 let texture = device.create_texture(&wgpu::TextureDescriptor {
52 label: Some("volume_3d_texture"),
53 size: wgpu::Extent3d {
54 width: dims[0],
55 height: dims[1],
56 depth_or_array_layers: dims[2],
57 },
58 mip_level_count: 1,
59 sample_count: 1,
60 dimension: wgpu::TextureDimension::D3,
61 format: wgpu::TextureFormat::R32Float,
62 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
63 view_formats: &[],
64 });
65
66 let bytes: &[u8] = bytemuck::cast_slice(data);
67 queue.write_texture(
68 wgpu::TexelCopyTextureInfo {
69 texture: &texture,
70 mip_level: 0,
71 origin: wgpu::Origin3d::ZERO,
72 aspect: wgpu::TextureAspect::All,
73 },
74 bytes,
75 wgpu::TexelCopyBufferLayout {
76 offset: 0,
77 bytes_per_row: Some(dims[0] * 4),
78 rows_per_image: Some(dims[1]),
79 },
80 wgpu::Extent3d {
81 width: dims[0],
82 height: dims[1],
83 depth_or_array_layers: dims[2],
84 },
85 );
86
87 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
88 VolumeId(self.content.volume_textures.push((texture, view)))
89 }
90
91 pub fn begin_upload_volume(
108 &mut self,
109 device: &wgpu::Device,
110 queue: &wgpu::Queue,
111 data: Vec<f32>,
112 dims: [u32; 3],
113 ) -> crate::error::ViewportResult<crate::resources::JobId> {
114 let expected = (dims[0] as usize) * (dims[1] as usize) * (dims[2] as usize);
115 if data.len() != expected {
116 return Err(crate::error::ViewportError::VolumeDataLengthMismatch {
117 actual: data.len(),
118 expected,
119 dims,
120 });
121 }
122
123 let slot = crate::resources::ResultSlot::<VolumeId>::new();
124 let slot_for_apply = slot.clone();
125 let device_for_worker = device.clone();
126 let queue_for_worker = queue.clone();
127
128 let id = {
129 let mut runner = self.jobs.lock().expect("upload job runner poisoned");
130 runner.submit_cpu(move |progress| {
131 progress.set(0.1);
132 let texture = device_for_worker.create_texture(&wgpu::TextureDescriptor {
133 label: Some("volume_3d_texture"),
134 size: wgpu::Extent3d {
135 width: dims[0],
136 height: dims[1],
137 depth_or_array_layers: dims[2],
138 },
139 mip_level_count: 1,
140 sample_count: 1,
141 dimension: wgpu::TextureDimension::D3,
142 format: wgpu::TextureFormat::R32Float,
143 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
144 view_formats: &[],
145 });
146 let bytes: &[u8] = bytemuck::cast_slice(&data);
147 queue_for_worker.write_texture(
148 wgpu::TexelCopyTextureInfo {
149 texture: &texture,
150 mip_level: 0,
151 origin: wgpu::Origin3d::ZERO,
152 aspect: wgpu::TextureAspect::All,
153 },
154 bytes,
155 wgpu::TexelCopyBufferLayout {
156 offset: 0,
157 bytes_per_row: Some(dims[0] * 4),
158 rows_per_image: Some(dims[1]),
159 },
160 wgpu::Extent3d {
161 width: dims[0],
162 height: dims[1],
163 depth_or_array_layers: dims[2],
164 },
165 );
166 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
167 progress.set(0.95);
168 Ok(crate::resources::upload_jobs::JobProduct::with_apply(
169 Box::new(move |resources: &mut DeviceResources| {
170 let id = VolumeId(resources.content.volume_textures.push((texture, view)));
171 slot_for_apply.set(id);
172 }),
173 ))
174 })
175 };
176
177 self.job_results
178 .volume
179 .lock()
180 .expect("volume result map poisoned")
181 .insert(id, slot);
182 Ok(id)
183 }
184
185 pub fn upload_result_volume(
192 &mut self,
193 id: crate::resources::JobId,
194 ) -> crate::error::ViewportResult<VolumeId> {
195 let mut map = self
196 .job_results
197 .volume
198 .lock()
199 .expect("volume result map poisoned");
200 let slot = match map.get(&id) {
201 Some(s) => s.clone(),
202 None => {
203 return Err(crate::error::ViewportError::JobResultMissing {
204 reason: "unknown id or wrong upload type",
205 });
206 }
207 };
208 match slot.take() {
209 Some(vid) => {
210 map.remove(&id);
211 Ok(vid)
212 }
213 None => Err(crate::error::ViewportError::JobNotReady),
214 }
215 }
216
217 pub(crate) fn ensure_volume_pipeline(&mut self, device: &wgpu::Device) {
219 if self.volume.pipeline.is_some() {
220 return;
221 }
222
223 let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
224 label: Some("volume_bgl"),
225 entries: &[
226 wgpu::BindGroupLayoutEntry {
227 binding: 0,
228 visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
229 ty: wgpu::BindingType::Buffer {
230 ty: wgpu::BufferBindingType::Uniform,
231 has_dynamic_offset: false,
232 min_binding_size: None,
233 },
234 count: None,
235 },
236 wgpu::BindGroupLayoutEntry {
237 binding: 1,
238 visibility: wgpu::ShaderStages::FRAGMENT,
239 ty: wgpu::BindingType::Texture {
240 sample_type: wgpu::TextureSampleType::Float { filterable: false },
241 view_dimension: wgpu::TextureViewDimension::D3,
242 multisampled: false,
243 },
244 count: None,
245 },
246 wgpu::BindGroupLayoutEntry {
247 binding: 2,
248 visibility: wgpu::ShaderStages::FRAGMENT,
249 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering),
250 count: None,
251 },
252 wgpu::BindGroupLayoutEntry {
253 binding: 3,
254 visibility: wgpu::ShaderStages::FRAGMENT,
255 ty: wgpu::BindingType::Texture {
256 sample_type: wgpu::TextureSampleType::Float { filterable: true },
257 view_dimension: wgpu::TextureViewDimension::D2,
258 multisampled: false,
259 },
260 count: None,
261 },
262 wgpu::BindGroupLayoutEntry {
263 binding: 4,
264 visibility: wgpu::ShaderStages::FRAGMENT,
265 ty: wgpu::BindingType::Texture {
266 sample_type: wgpu::TextureSampleType::Float { filterable: true },
267 view_dimension: wgpu::TextureViewDimension::D2,
268 multisampled: false,
269 },
270 count: None,
271 },
272 wgpu::BindGroupLayoutEntry {
273 binding: 5,
274 visibility: wgpu::ShaderStages::FRAGMENT,
275 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
276 count: None,
277 },
278 ],
279 });
280
281 let shader_src = include_str!(concat!(env!("OUT_DIR"), "/volume.wgsl"));
282 let shader = crate::resources::builders::wgsl_module(device, "volume_shader", shader_src);
283
284 let pipeline_layout = crate::resources::builders::standard_scene_layout(
285 device,
286 "volume_pipeline_layout",
287 &self.camera_bind_group_layout,
288 &bgl,
289 );
290
291 let vol_vert_layout = wgpu::VertexBufferLayout {
292 array_stride: 12,
293 step_mode: wgpu::VertexStepMode::Vertex,
294 attributes: &[wgpu::VertexAttribute {
295 format: wgpu::VertexFormat::Float32x3,
296 offset: 0,
297 shader_location: 0,
298 }],
299 };
300 self.volume.pipeline = Some(crate::resources::builders::build_dual_pipeline(
301 device,
302 &crate::resources::builders::DualPipelineDesc {
303 label: "volume_pipeline",
304 layout: &pipeline_layout,
305 shader: &shader,
306 vertex_entry: "vs_main",
307 fragment_entry: "fs_main",
308 vertex_buffers: &[vol_vert_layout.clone()],
309 blend: Some(wgpu::BlendState {
310 color: wgpu::BlendComponent {
311 src_factor: wgpu::BlendFactor::SrcAlpha,
312 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
313 operation: wgpu::BlendOperation::Add,
314 },
315 alpha: wgpu::BlendComponent {
316 src_factor: wgpu::BlendFactor::One,
317 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
318 operation: wgpu::BlendOperation::Add,
319 },
320 }),
321 topology: wgpu::PrimitiveTopology::TriangleList,
322 cull_mode: None,
323 depth_write: false,
324 depth_compare: wgpu::CompareFunction::Less,
325 sample_count: self.sample_count,
326 ldr_format: self.target_format,
327 },
328 ));
329 self.volume.bgl = Some(bgl);
330 }
331
332 pub(crate) fn ensure_volume_outline_mask_pipeline(&mut self, device: &wgpu::Device) {
337 if self.volume.outline_mask_pipeline.is_some() {
338 return;
339 }
340 let bgl = self.volume.bgl.as_ref().expect(
341 "ensure_volume_pipeline must be called before ensure_volume_outline_mask_pipeline",
342 );
343
344 let shader = crate::resources::builders::wgsl_module(
345 device,
346 "volume_outline_mask_shader",
347 crate::resources::builders::wgsl_source!("volume_outline_mask"),
348 );
349
350 let layout = crate::resources::builders::standard_scene_layout(
351 device,
352 "volume_outline_mask_pipeline_layout",
353 &self.camera_bind_group_layout,
354 bgl,
355 );
356
357 let vert_attrs = [wgpu::VertexAttribute {
358 offset: 0,
359 shader_location: 0,
360 format: wgpu::VertexFormat::Float32x3,
361 }];
362 let vert_layout = wgpu::VertexBufferLayout {
363 array_stride: 12,
364 step_mode: wgpu::VertexStepMode::Vertex,
365 attributes: &vert_attrs,
366 };
367
368 self.volume.outline_mask_pipeline =
369 Some(crate::resources::builders::build_outline_mask_pipeline(
370 device,
371 "volume_outline_mask_pipeline",
372 &layout,
373 &shader,
374 wgpu::TextureFormat::R8Unorm,
375 &[vert_layout],
376 None,
377 true,
378 wgpu::CompareFunction::Less,
379 ));
380 }
381
382 pub(crate) fn ensure_volume_cube(&mut self, device: &wgpu::Device) {
384 if self.volume.cube_vb.is_some() {
385 return;
386 }
387
388 #[rustfmt::skip]
389 let vertices: [[f32; 3]; 8] = [
390 [0.0, 0.0, 0.0],
391 [1.0, 0.0, 0.0],
392 [1.0, 1.0, 0.0],
393 [0.0, 1.0, 0.0],
394 [0.0, 0.0, 1.0],
395 [1.0, 0.0, 1.0],
396 [1.0, 1.0, 1.0],
397 [0.0, 1.0, 1.0],
398 ];
399
400 #[rustfmt::skip]
401 let indices: [u32; 36] = [
402 0, 2, 1, 0, 3, 2,
403 4, 5, 6, 4, 6, 7,
404 0, 4, 7, 0, 7, 3,
405 1, 2, 6, 1, 6, 5,
406 0, 1, 5, 0, 5, 4,
407 3, 7, 6, 3, 6, 2,
408 ];
409
410 let vbuf = device.create_buffer(&wgpu::BufferDescriptor {
411 label: Some("volume_cube_vb"),
412 size: std::mem::size_of_val(&vertices) as u64,
413 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
414 mapped_at_creation: true,
415 });
416 {
417 let mut view = vbuf.slice(..).get_mapped_range_mut();
418 view.copy_from_slice(bytemuck::cast_slice(&vertices));
419 }
420 vbuf.unmap();
421
422 let ibuf = device.create_buffer(&wgpu::BufferDescriptor {
423 label: Some("volume_cube_ib"),
424 size: std::mem::size_of_val(&indices) as u64,
425 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
426 mapped_at_creation: true,
427 });
428 {
429 let mut view = ibuf.slice(..).get_mapped_range_mut();
430 view.copy_from_slice(bytemuck::cast_slice(&indices));
431 }
432 ibuf.unmap();
433
434 self.volume.cube_vb = Some(vbuf);
435 self.volume.cube_ib = Some(ibuf);
436 }
437
438 fn ensure_default_opacity_lut(&mut self, device: &wgpu::Device, queue: &wgpu::Queue) {
440 if self.volume.default_opacity_lut.is_some() {
441 return;
442 }
443
444 let mut data = [0u8; 256];
445 for (i, v) in data.iter_mut().enumerate() {
446 *v = i as u8;
447 }
448
449 let texture = device.create_texture(&wgpu::TextureDescriptor {
450 label: Some("volume_default_opacity_lut"),
451 size: wgpu::Extent3d {
452 width: 256,
453 height: 1,
454 depth_or_array_layers: 1,
455 },
456 mip_level_count: 1,
457 sample_count: 1,
458 dimension: wgpu::TextureDimension::D2,
459 format: wgpu::TextureFormat::R8Unorm,
460 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
461 view_formats: &[],
462 });
463
464 queue.write_texture(
465 wgpu::TexelCopyTextureInfo {
466 texture: &texture,
467 mip_level: 0,
468 origin: wgpu::Origin3d::ZERO,
469 aspect: wgpu::TextureAspect::All,
470 },
471 &data,
472 wgpu::TexelCopyBufferLayout {
473 offset: 0,
474 bytes_per_row: Some(256),
475 rows_per_image: Some(1),
476 },
477 wgpu::Extent3d {
478 width: 256,
479 height: 1,
480 depth_or_array_layers: 1,
481 },
482 );
483
484 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
485 self.volume.default_opacity_lut = Some(texture);
486 self.volume.default_opacity_lut_view = Some(view);
487 }
488
489 pub(crate) fn upload_volume_frame(
491 &mut self,
492 device: &wgpu::Device,
493 queue: &wgpu::Queue,
494 item: &crate::renderer::VolumeItem,
495 clip_objects: &[crate::renderer::ClipObject],
496 step_scale_multiplier: f32,
498 ) -> VolumeGpuData {
499 self.ensure_volume_cube(device);
500 self.ensure_default_opacity_lut(device, queue);
501
502 let vol_id = item.volume_id.0;
503 let dims = {
504 let uploaded = self.content.volume_textures.len();
505 let (tex, _) = self.content.volume_textures.get(vol_id).unwrap_or_else(|| {
506 panic!("invalid VolumeId: {vol_id} (only {uploaded} volumes uploaded)")
507 });
508 let size = tex.size();
509 [size.width, size.height, size.depth_or_array_layers]
510 };
511
512 let item_model = glam::Mat4::from_cols_array_2d(&item.model);
513 let bbox_min = glam::Vec3::from(item.bbox_min);
514 let bbox_max = glam::Vec3::from(item.bbox_max);
515 let extent = bbox_max - bbox_min;
516 let bbox_model = glam::Mat4::from_translation(bbox_min) * glam::Mat4::from_scale(extent);
517 let model = item_model * bbox_model;
518 let inv_model = model.inverse();
519
520 let max_dim = dims[0].max(dims[1]).max(dims[2]) as f32;
521 let step_size = (item.step_scale * step_scale_multiplier) / max_dim.max(1.0);
522
523 let mut clip_plane_data = [[0.0f32; 4]; 6];
524 let mut num_clip = 0u32;
525 for obj in clip_objects.iter().filter(|o| o.enabled) {
526 if num_clip >= 6 {
527 break;
528 }
529 if let crate::renderer::ClipShape::Plane {
530 normal, distance, ..
531 } = obj.shape
532 {
533 clip_plane_data[num_clip as usize] = [normal[0], normal[1], normal[2], distance];
534 num_clip += 1;
535 }
536 }
537
538 let mut uniform_data = [0u8; 304];
539 {
540 let mut offset = 0usize;
541 let model_arr = model.to_cols_array();
542 uniform_data[offset..offset + 64].copy_from_slice(bytemuck::bytes_of(&model_arr));
543 offset += 64;
544 let inv_model_arr = inv_model.to_cols_array();
545 uniform_data[offset..offset + 64].copy_from_slice(bytemuck::bytes_of(&inv_model_arr));
546 offset += 64;
547 uniform_data[offset..offset + 12].copy_from_slice(bytemuck::bytes_of(&item.bbox_min));
548 offset += 12;
549 uniform_data[offset..offset + 4].copy_from_slice(bytemuck::bytes_of(&step_size));
550 offset += 4;
551 uniform_data[offset..offset + 12].copy_from_slice(bytemuck::bytes_of(&item.bbox_max));
552 offset += 12;
553 uniform_data[offset..offset + 4]
554 .copy_from_slice(bytemuck::bytes_of(&item.opacity_scale));
555 offset += 4;
556 uniform_data[offset..offset + 4]
557 .copy_from_slice(bytemuck::bytes_of(&item.scalar_range.0));
558 offset += 4;
559 uniform_data[offset..offset + 4]
560 .copy_from_slice(bytemuck::bytes_of(&item.scalar_range.1));
561 offset += 4;
562 uniform_data[offset..offset + 4]
563 .copy_from_slice(bytemuck::bytes_of(&item.threshold_min));
564 offset += 4;
565 uniform_data[offset..offset + 4]
566 .copy_from_slice(bytemuck::bytes_of(&item.threshold_max));
567 offset += 4;
568 let shading_u32: u32 = if item.enable_shading && !item.settings.unlit {
574 1
575 } else {
576 0
577 };
578 uniform_data[offset..offset + 4].copy_from_slice(bytemuck::bytes_of(&shading_u32));
579 offset += 4;
580 uniform_data[offset..offset + 4].copy_from_slice(bytemuck::bytes_of(&num_clip));
581 offset += 4;
582 let use_nan_colour_u32: u32 = if item.nan_colour.is_some() { 1 } else { 0 };
583 uniform_data[offset..offset + 4]
584 .copy_from_slice(bytemuck::bytes_of(&use_nan_colour_u32));
585 offset += 4;
586 offset += 4;
587 let nan_colour = item.nan_colour.unwrap_or([0.0f32; 4]);
588 uniform_data[offset..offset + 16].copy_from_slice(bytemuck::bytes_of(&nan_colour));
589 offset += 16;
590 for cp in &clip_plane_data {
591 uniform_data[offset..offset + 16].copy_from_slice(bytemuck::bytes_of(cp));
592 offset += 16;
593 }
594 debug_assert_eq!(offset, 304);
595 }
596
597 let uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
598 label: Some("volume_uniform_buf"),
599 size: 304,
600 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
601 mapped_at_creation: true,
602 });
603 {
604 let mut view = uniform_buf.slice(..).get_mapped_range_mut();
605 view.copy_from_slice(&uniform_data);
606 }
607 uniform_buf.unmap();
608
609 let volume_view = &self
610 .content
611 .volume_textures
612 .get(vol_id)
613 .expect("VolumeId validated above")
614 .1;
615
616 let colour_lut_view = if let Some(cmap_id) = item.colour_lut {
617 self.content
618 .colourmap_views
619 .get(cmap_id.0)
620 .unwrap_or(&self.content.fallback_lut_view)
621 } else if let Some(ids) = &self.content.builtin_colourmap_ids {
622 self.content
623 .colourmap_views
624 .get(ids[0].0)
625 .unwrap_or(&self.content.fallback_lut_view)
626 } else {
627 &self.content.fallback_lut_view
628 };
629
630 let opacity_lut_view = if let Some(cmap_id) = item.opacity_lut {
631 self.content
632 .colourmap_views
633 .get(cmap_id.0)
634 .unwrap_or(self.volume.default_opacity_lut_view.as_ref().unwrap())
635 } else {
636 self.volume.default_opacity_lut_view.as_ref().unwrap()
637 };
638
639 let nearest_sampler =
640 crate::resources::builders::clamp_nearest_sampler(device, "volume_nearest_sampler");
641
642 let linear_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
643 label: Some("volume_lut_sampler"),
644 address_mode_u: wgpu::AddressMode::ClampToEdge,
645 address_mode_v: wgpu::AddressMode::ClampToEdge,
646 address_mode_w: wgpu::AddressMode::ClampToEdge,
647 mag_filter: wgpu::FilterMode::Linear,
648 min_filter: wgpu::FilterMode::Linear,
649 mipmap_filter: wgpu::FilterMode::Linear,
650 ..Default::default()
651 });
652
653 let bgl = self
654 .volume
655 .bgl
656 .as_ref()
657 .expect("ensure_volume_pipeline not called");
658
659 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
660 label: Some("volume_bind_group"),
661 layout: bgl,
662 entries: &[
663 wgpu::BindGroupEntry {
664 binding: 0,
665 resource: uniform_buf.as_entire_binding(),
666 },
667 wgpu::BindGroupEntry {
668 binding: 1,
669 resource: wgpu::BindingResource::TextureView(volume_view),
670 },
671 wgpu::BindGroupEntry {
672 binding: 2,
673 resource: wgpu::BindingResource::Sampler(&nearest_sampler),
674 },
675 wgpu::BindGroupEntry {
676 binding: 3,
677 resource: wgpu::BindingResource::TextureView(colour_lut_view),
678 },
679 wgpu::BindGroupEntry {
680 binding: 4,
681 resource: wgpu::BindingResource::TextureView(opacity_lut_view),
682 },
683 wgpu::BindGroupEntry {
684 binding: 5,
685 resource: wgpu::BindingResource::Sampler(&linear_sampler),
686 },
687 ],
688 });
689
690 #[rustfmt::skip]
691 let vertices: [[f32; 3]; 8] = [
692 [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0], [0.0, 1.0, 0.0],
693 [0.0, 0.0, 1.0], [1.0, 0.0, 1.0], [1.0, 1.0, 1.0], [0.0, 1.0, 1.0],
694 ];
695 #[rustfmt::skip]
696 let indices: [u32; 36] = [
697 0,2,1, 0,3,2, 4,5,6, 4,6,7,
698 0,4,7, 0,7,3, 1,2,6, 1,6,5,
699 0,1,5, 0,5,4, 3,7,6, 3,6,2,
700 ];
701
702 let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
703 label: Some("volume_cube_vb_frame"),
704 size: std::mem::size_of_val(&vertices) as u64,
705 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
706 mapped_at_creation: true,
707 });
708 {
709 let mut view = vertex_buffer.slice(..).get_mapped_range_mut();
710 view.copy_from_slice(bytemuck::cast_slice(&vertices));
711 }
712 vertex_buffer.unmap();
713
714 let index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
715 label: Some("volume_cube_ib_frame"),
716 size: std::mem::size_of_val(&indices) as u64,
717 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
718 mapped_at_creation: true,
719 });
720 {
721 let mut view = index_buffer.slice(..).get_mapped_range_mut();
722 view.copy_from_slice(bytemuck::cast_slice(&indices));
723 }
724 index_buffer.unmap();
725
726 VolumeGpuData {
727 bind_group,
728 vertex_buffer,
729 index_buffer,
730 _dims: dims,
731 _uniform_buf: uniform_buf,
732 wireframe: false,
733 }
734 }
735}
736
737#[cfg(test)]
738mod tests {
739 use crate::DeviceResources;
740 use crate::geometry::marching_cubes::VolumeData;
741 use crate::resources::UploadStatus;
742
743 fn try_make_device() -> Option<(wgpu::Device, wgpu::Queue)> {
744 let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::default());
745 let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
746 power_preference: wgpu::PowerPreference::LowPower,
747 compatible_surface: None,
748 force_fallback_adapter: false,
749 }))
750 .ok()?;
751 pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor::default())).ok()
752 }
753
754 fn sample_volume_data() -> Vec<f32> {
755 let n: usize = 8;
756 let mut data = Vec::with_capacity(n * n * n);
757 for z in 0..n {
758 for y in 0..n {
759 for x in 0..n {
760 let v = (x + y + z) as f32 / (3.0 * n as f32);
761 data.push(v);
762 }
763 }
764 }
765 data
766 }
767
768 fn sample_volume_struct() -> VolumeData {
769 VolumeData {
770 data: sample_volume_data(),
771 dims: [8, 8, 8],
772 origin: [0.0, 0.0, 0.0],
773 spacing: [1.0, 1.0, 1.0],
774 }
775 }
776
777 fn drive_until_ready(
778 resources: &mut DeviceResources,
779 device: &wgpu::Device,
780 queue: &wgpu::Queue,
781 id: crate::resources::JobId,
782 label: &str,
783 ) {
784 for _ in 0..200 {
785 resources.process_uploads(device, queue);
786 match resources.upload_status(id) {
787 UploadStatus::Ready => return,
788 UploadStatus::Failed(e) => panic!("{label} upload failed: {e:?}"),
789 UploadStatus::Pending { .. } => {
790 std::thread::sleep(std::time::Duration::from_millis(5));
791 }
792 UploadStatus::Unknown => panic!("{label} job id disappeared"),
793 }
794 }
795 panic!("{label} upload did not complete in time");
796 }
797
798 #[test]
799 fn sync_upload_volume_still_works() {
800 let Some((device, queue)) = try_make_device() else {
801 eprintln!("skipping: no wgpu adapter available");
802 return;
803 };
804 let mut resources = DeviceResources::new(&device, wgpu::TextureFormat::Rgba8UnormSrgb, 1);
805 let data = sample_volume_data();
806 let _id = resources.upload_volume(&device, &queue, &data, [8, 8, 8]);
807 }
808
809 #[test]
810 fn begin_upload_volume_validates_dims() {
811 let Some((device, queue)) = try_make_device() else {
812 eprintln!("skipping: no wgpu adapter available");
813 return;
814 };
815 let mut resources = DeviceResources::new(&device, wgpu::TextureFormat::Rgba8UnormSrgb, 1);
816 let err = resources
817 .begin_upload_volume(&device, &queue, vec![0.0_f32; 7], [8, 8, 8])
818 .unwrap_err();
819 assert!(matches!(
820 err,
821 crate::error::ViewportError::VolumeDataLengthMismatch { .. }
822 ));
823 }
824
825 #[test]
826 fn begin_upload_volume_drains_to_handle() {
827 let Some((device, queue)) = try_make_device() else {
828 eprintln!("skipping: no wgpu adapter available");
829 return;
830 };
831 let mut resources = DeviceResources::new(&device, wgpu::TextureFormat::Rgba8UnormSrgb, 1);
832 let job = resources
833 .begin_upload_volume(&device, &queue, sample_volume_data(), [8, 8, 8])
834 .expect("job submitted");
835 drive_until_ready(&mut resources, &device, &queue, job, "volume");
836 let _id = resources.upload_result_volume(job).expect("ready");
837 let err = resources.upload_result_volume(job).unwrap_err();
838 assert!(matches!(
839 err,
840 crate::error::ViewportError::JobResultMissing { .. }
841 ));
842 }
843
844 #[test]
845 fn begin_upload_volume_for_mc_drains_to_handle() {
846 let Some((device, queue)) = try_make_device() else {
847 eprintln!("skipping: no wgpu adapter available");
848 return;
849 };
850 let mut resources = DeviceResources::new(&device, wgpu::TextureFormat::Rgba8UnormSrgb, 1);
851 let job = resources.begin_upload_volume_for_mc(&device, &queue, sample_volume_struct());
852 drive_until_ready(&mut resources, &device, &queue, job, "volume_mc");
853 let _id = resources.upload_result_volume_mc(job).expect("ready");
854 }
855
856 #[test]
857 fn sync_upload_volume_for_mc_still_works() {
858 let Some((device, queue)) = try_make_device() else {
859 eprintln!("skipping: no wgpu adapter available");
860 return;
861 };
862 let mut resources = DeviceResources::new(&device, wgpu::TextureFormat::Rgba8UnormSrgb, 1);
863 let vol = sample_volume_struct();
864 let _id = resources
865 .upload_volume_for_mc(&device, &queue, &vol)
866 .expect("upload ok");
867 }
868}
869
870pub struct VolumeGpuData {
872 pub(crate) bind_group: wgpu::BindGroup,
874 pub(crate) vertex_buffer: wgpu::Buffer,
876 pub(crate) index_buffer: wgpu::Buffer,
878 pub(crate) _dims: [u32; 3],
880 pub(crate) _uniform_buf: wgpu::Buffer,
882 pub(crate) wireframe: bool,
884}
885
886pub(crate) struct ImageSliceGpuData {
888 pub(crate) bind_group: wgpu::BindGroup,
890 pub(crate) _uniform_buf: wgpu::Buffer,
892}
893
894pub(crate) struct VolumeSurfaceSliceGpuData {
896 pub(crate) bind_group: wgpu::BindGroup,
898 pub(crate) _uniform_buf: wgpu::Buffer,
900 pub(crate) mesh_id: crate::resources::mesh::mesh_store::MeshId,
902}