1use crate::incremental_texture::update_texture_slice_i16;
4use dicomview_core::{
5 preset, IncrementalVolume, IncrementalVolumeError, SlicePreviewMode, SlicePreviewState,
6 SliceProjectionMode, VolumeBlendMode, VolumeGeometry, VolumePresetId, VolumeViewState,
7};
8use glam::DVec3;
9use std::sync::Arc;
10use thiserror::Error;
11use volren_core::{
12 camera::{Camera, Projection},
13 render_params::{BlendMode, VolumeRenderParams},
14 transfer_function::{ColorTransferFunction, OpacityTransferFunction},
15 Aabb, WindowLevel,
16};
17use volren_gpu::{CrosshairParams, RenderError, Viewport, VolumeRenderer};
18
19pub struct RenderTarget<'a> {
21 pub view: &'a wgpu::TextureView,
23 pub viewport: Viewport,
25}
26
27pub struct FrameTargets<'a> {
29 pub axial: RenderTarget<'a>,
31 pub coronal: RenderTarget<'a>,
33 pub sagittal: RenderTarget<'a>,
35 pub volume: RenderTarget<'a>,
37}
38
39#[derive(Debug, Error)]
41pub enum RenderEngineError {
42 #[error("no prepared volume is available")]
44 NoPreparedVolume,
45 #[error(transparent)]
47 IncrementalVolume(#[from] IncrementalVolumeError),
48 #[error(transparent)]
50 Render(#[from] RenderError),
51}
52
53pub struct RenderEngine {
55 renderer: VolumeRenderer,
56 prepared_volume: Option<IncrementalVolume>,
57 geometry: Option<VolumeGeometry>,
58 volume_state: VolumeViewState,
59 axial_state: SlicePreviewState,
60 coronal_state: SlicePreviewState,
61 sagittal_state: SlicePreviewState,
62 active_preset: VolumePresetId,
63 #[allow(dead_code)]
64 device: Arc<wgpu::Device>,
65 #[allow(dead_code)]
66 queue: Arc<wgpu::Queue>,
67}
68
69impl RenderEngine {
70 #[must_use]
72 pub fn new(
73 device: &wgpu::Device,
74 queue: &wgpu::Queue,
75 output_format: wgpu::TextureFormat,
76 ) -> Self {
77 Self::from_arc(
78 Arc::new(device.clone()),
79 Arc::new(queue.clone()),
80 output_format,
81 )
82 }
83
84 #[must_use]
86 pub fn from_arc(
87 device: Arc<wgpu::Device>,
88 queue: Arc<wgpu::Queue>,
89 output_format: wgpu::TextureFormat,
90 ) -> Self {
91 let mut coronal_state = SlicePreviewState::default();
92 coronal_state.set_mode(SlicePreviewMode::Coronal);
93 let mut sagittal_state = SlicePreviewState::default();
94 sagittal_state.set_mode(SlicePreviewMode::Sagittal);
95
96 Self {
97 renderer: VolumeRenderer::from_arc(device.clone(), queue.clone(), output_format),
98 prepared_volume: None,
99 geometry: None,
100 volume_state: VolumeViewState::default(),
101 axial_state: SlicePreviewState::default(),
102 coronal_state,
103 sagittal_state,
104 active_preset: VolumePresetId::CtSoftTissue,
105 device,
106 queue,
107 }
108 }
109
110 pub fn prepare_volume(&mut self, geometry: VolumeGeometry) -> Result<(), RenderEngineError> {
112 self.prepared_volume = Some(IncrementalVolume::new(geometry)?);
113 self.geometry = Some(geometry);
114 self.renderer.allocate_volume(
115 geometry.dimensions,
116 geometry.spacing,
117 geometry.origin,
118 geometry.direction,
119 (0.0, 1.0),
120 true,
121 );
122 let params = render_params_for_state(self.active_preset, self.volume_state, (0.0, 1.0));
125 let _ = self.renderer.set_render_params(¶ms);
126 Ok(())
127 }
128
129 pub fn insert_slice(&mut self, z_index: u32, pixels: &[i16]) -> Result<(), RenderEngineError> {
131 let volume = self
132 .prepared_volume
133 .as_mut()
134 .ok_or(RenderEngineError::NoPreparedVolume)?;
135 volume.insert_slice(z_index, pixels)?;
136 let scalar_range = volume
137 .scalar_range()
138 .map(|(min, max)| (f64::from(min), f64::from(max)))
139 .unwrap_or((0.0, 1.0));
140 update_texture_slice_i16(&mut self.renderer, z_index, pixels, scalar_range)?;
141 let params = render_params_for_state(self.active_preset, self.volume_state, scalar_range);
143 let _ = self.renderer.set_render_params(¶ms);
144 Ok(())
145 }
146
147 #[must_use]
149 pub fn prepared_volume(&self) -> Option<&IncrementalVolume> {
150 self.prepared_volume.as_ref()
151 }
152
153 #[must_use]
155 pub fn geometry(&self) -> Option<VolumeGeometry> {
156 self.geometry
157 }
158
159 #[must_use]
161 pub fn scalar_range(&self) -> Option<(f64, f64)> {
162 self.prepared_volume
163 .as_ref()
164 .and_then(IncrementalVolume::scalar_range)
165 .map(|(min, max)| (f64::from(min), f64::from(max)))
166 }
167
168 pub fn volume_state_mut(&mut self) -> &mut VolumeViewState {
170 &mut self.volume_state
171 }
172
173 pub fn slice_state_mut(&mut self, mode: SlicePreviewMode) -> &mut SlicePreviewState {
175 match mode {
176 SlicePreviewMode::Axial => &mut self.axial_state,
177 SlicePreviewMode::Coronal => &mut self.coronal_state,
178 SlicePreviewMode::Sagittal => &mut self.sagittal_state,
179 }
180 }
181
182 pub fn set_volume_preset(&mut self, preset_id: VolumePresetId) {
184 self.active_preset = preset_id;
185 }
186
187 pub fn set_crosshair(&mut self, world: DVec3) -> Result<(), RenderEngineError> {
189 let bounds = self.bounds()?;
190 for state in [
191 &mut self.axial_state,
192 &mut self.coronal_state,
193 &mut self.sagittal_state,
194 ] {
195 state.set_crosshair_world(world);
196 state.center_on_world(world, bounds);
197 }
198 Ok(())
199 }
200
201 pub fn scroll_slice(
203 &mut self,
204 mode: SlicePreviewMode,
205 delta: f64,
206 ) -> Result<(), RenderEngineError> {
207 let bounds = self.bounds()?;
208 self.slice_state_mut(mode).scroll_by(delta, bounds);
209 Ok(())
210 }
211
212 pub fn set_window_level(&mut self, center: f64, width: f64) -> Result<(), RenderEngineError> {
214 let (scalar_min, scalar_max) = self
215 .scalar_range()
216 .ok_or(RenderEngineError::NoPreparedVolume)?;
217 for state in [
218 &mut self.axial_state,
219 &mut self.coronal_state,
220 &mut self.sagittal_state,
221 ] {
222 state.set_transfer_window(center, width, scalar_min, scalar_max);
223 }
224 self.volume_state
225 .set_transfer_window(center, width, scalar_min, scalar_max);
226 Ok(())
227 }
228
229 pub fn set_thick_slab(
231 &mut self,
232 mode: SlicePreviewMode,
233 thickness: f64,
234 projection_mode: SliceProjectionMode,
235 ) {
236 let state = self.slice_state_mut(mode);
237 if thickness <= 0.0 {
238 state.projection_mode = SliceProjectionMode::Thin;
239 state.slab_half_thickness = 0.0;
240 } else {
241 state.projection_mode = projection_mode;
242 state.slab_half_thickness = (thickness * 0.5).max(0.5);
243 }
244 }
245
246 pub fn reset(&mut self) {
248 self.volume_state.reset();
249 self.axial_state.reset();
250 self.coronal_state.reset();
251 self.coronal_state.set_mode(SlicePreviewMode::Coronal);
252 self.sagittal_state.reset();
253 self.sagittal_state.set_mode(SlicePreviewMode::Sagittal);
254 }
255
256 pub fn render_frame(
258 &mut self,
259 encoder: &mut wgpu::CommandEncoder,
260 targets: FrameTargets<'_>,
261 show_crosshairs: bool,
262 ) -> Result<(), RenderEngineError> {
263 let volume = self
264 .prepared_volume
265 .as_ref()
266 .ok_or(RenderEngineError::NoPreparedVolume)?;
267 let geometry = self.geometry.ok_or(RenderEngineError::NoPreparedVolume)?;
268 let bounds = bounds_from_geometry(geometry);
269 let scalar_range = volume
270 .scalar_range()
271 .map(|(min, max)| (f64::from(min), f64::from(max)))
272 .unwrap_or((0.0, 1.0));
273
274 self.render_slice_view(
275 encoder,
276 &targets.axial,
277 &self.axial_state,
278 bounds,
279 scalar_range,
280 show_crosshairs,
281 crosshair_colors(SlicePreviewMode::Axial),
282 )?;
283 self.render_slice_view(
284 encoder,
285 &targets.coronal,
286 &self.coronal_state,
287 bounds,
288 scalar_range,
289 show_crosshairs,
290 crosshair_colors(SlicePreviewMode::Coronal),
291 )?;
292 self.render_slice_view(
293 encoder,
294 &targets.sagittal,
295 &self.sagittal_state,
296 bounds,
297 scalar_range,
298 show_crosshairs,
299 crosshair_colors(SlicePreviewMode::Sagittal),
300 )?;
301
302 let camera = camera_for_state(geometry, self.volume_state);
303 let params = render_params_for_state(self.active_preset, self.volume_state, scalar_range);
304 self.renderer.render_volume(
305 encoder,
306 targets.volume.view,
307 &camera,
308 ¶ms,
309 targets.volume.viewport,
310 )?;
311 Ok(())
312 }
313
314 fn render_slice_view(
315 &self,
316 encoder: &mut wgpu::CommandEncoder,
317 target: &RenderTarget<'_>,
318 state: &SlicePreviewState,
319 bounds: Aabb,
320 scalar_range: (f64, f64),
321 show_crosshairs: bool,
322 colors: ([f32; 4], [f32; 4]),
323 ) -> Result<(), RenderEngineError> {
324 let (center, width) = state.transfer_window(scalar_range.0, scalar_range.1);
325 let window_level = WindowLevel::new(center, width.max(1.0));
326 let mut slice_plane = state.slice_plane(bounds);
327
328 let vp_w = f64::from(target.viewport.width.max(1));
330 let vp_h = f64::from(target.viewport.height.max(1));
331 let vp_aspect = vp_w / vp_h;
332 let data_aspect = slice_plane.width / slice_plane.height.max(1e-6);
333 if vp_aspect > data_aspect {
334 slice_plane.width = slice_plane.height * vp_aspect;
335 } else {
336 slice_plane.height = slice_plane.width / vp_aspect;
337 }
338
339 self.renderer.render_slice(
340 encoder,
341 target.view,
342 &slice_plane,
343 &window_level,
344 target.viewport,
345 state.thick_slab().as_ref(),
346 )?;
347
348 if show_crosshairs {
349 let crosshair_world = state.crosshair_world(bounds);
350 let (uv, _) = slice_plane.world_to_point(crosshair_world);
351 if (0.0..=1.0).contains(&uv.x) && (0.0..=1.0).contains(&uv.y) {
352 self.renderer.render_crosshair(
353 encoder,
354 target.view,
355 target.viewport,
356 &CrosshairParams {
357 position: [uv.x as f32, uv.y as f32],
358 horizontal_color: colors.0,
359 vertical_color: colors.1,
360 thickness: 1.5,
361 },
362 )?;
363 }
364 }
365
366 Ok(())
367 }
368
369 fn bounds(&self) -> Result<Aabb, RenderEngineError> {
370 let geometry = self.geometry.ok_or(RenderEngineError::NoPreparedVolume)?;
371 Ok(bounds_from_geometry(geometry))
372 }
373}
374
375fn render_params_for_state(
376 preset_id: VolumePresetId,
377 view_state: VolumeViewState,
378 scalar_range: (f64, f64),
379) -> VolumeRenderParams {
380 let mut params = match view_state.blend_mode {
381 VolumeBlendMode::Composite => {
382 preset(preset_id, scalar_range.0, scalar_range.1).to_render_params()
383 }
384 VolumeBlendMode::MaximumIntensity
385 | VolumeBlendMode::MinimumIntensity
386 | VolumeBlendMode::AverageIntensity => {
387 let blend_mode = match view_state.blend_mode {
388 VolumeBlendMode::Composite => BlendMode::Composite,
389 VolumeBlendMode::MaximumIntensity => BlendMode::MaximumIntensity,
390 VolumeBlendMode::MinimumIntensity => BlendMode::MinimumIntensity,
391 VolumeBlendMode::AverageIntensity => BlendMode::AverageIntensity,
392 };
393 VolumeRenderParams::builder()
394 .blend_mode(blend_mode)
395 .step_size_factor(0.35)
396 .color_tf(ColorTransferFunction::greyscale(
397 scalar_range.0,
398 scalar_range.1,
399 ))
400 .opacity_tf(OpacityTransferFunction::linear_ramp(
401 scalar_range.0,
402 scalar_range.1,
403 ))
404 .build()
405 }
406 };
407 let (center, width) = view_state.transfer_window(scalar_range.0, scalar_range.1);
408 params.window_level = Some(WindowLevel::new(center, width.max(1.0)));
409 params
410}
411
412fn camera_for_state(geometry: VolumeGeometry, view_state: VolumeViewState) -> Camera {
413 let bounds = bounds_from_geometry(geometry);
414 let center = bounds.center();
415 let diagonal = bounds.diagonal().max(1.0);
416 let default_forward = DVec3::Y;
417 let default_up = DVec3::NEG_Z;
418 let forward = view_state.orientation * default_forward;
419 let up = view_state.orientation * default_up;
420 let right = forward.cross(up).normalize_or(DVec3::X);
421 let fov_y_deg = 30.0_f64;
422 let half_diag = diagonal * 0.5;
423 let fit_distance = half_diag / (fov_y_deg.to_radians() * 0.5).tan();
424 let distance = fit_distance * 1.15 / view_state.zoom.clamp(0.25, 8.0);
425 let position = center - forward * distance;
426 let pan_scale = distance * 0.001;
427 let pan_offset = right * (-view_state.pan_x * pan_scale) + up * (-view_state.pan_y * pan_scale);
428
429 Camera::new(position + pan_offset, center + pan_offset, up)
430 .with_projection(Projection::Perspective { fov_y_deg })
431 .with_clip_range(
432 (distance - diagonal).max(diagonal * 0.01).max(0.1),
433 distance + diagonal * 2.0,
434 )
435}
436
437fn bounds_from_geometry(geometry: VolumeGeometry) -> Aabb {
438 let dims = geometry.dimensions.as_dvec3();
439 let corners = [
440 DVec3::ZERO,
441 DVec3::new(dims.x - 1.0, 0.0, 0.0),
442 DVec3::new(0.0, dims.y - 1.0, 0.0),
443 DVec3::new(0.0, 0.0, dims.z - 1.0),
444 DVec3::new(dims.x - 1.0, dims.y - 1.0, 0.0),
445 DVec3::new(dims.x - 1.0, 0.0, dims.z - 1.0),
446 DVec3::new(0.0, dims.y - 1.0, dims.z - 1.0),
447 dims - DVec3::ONE,
448 ];
449 let world_corners: Vec<DVec3> = corners
450 .iter()
451 .map(|&corner| geometry.origin + geometry.direction * (corner * geometry.spacing))
452 .collect();
453 let min = world_corners
454 .iter()
455 .fold(DVec3::splat(f64::INFINITY), |acc, point| acc.min(*point));
456 let max = world_corners
457 .iter()
458 .fold(DVec3::splat(f64::NEG_INFINITY), |acc, point| {
459 acc.max(*point)
460 });
461 Aabb::new(min, max)
462}
463
464fn crosshair_colors(mode: SlicePreviewMode) -> ([f32; 4], [f32; 4]) {
465 match mode {
466 SlicePreviewMode::Axial => ([0.0, 1.0, 0.0, 1.0], [1.0, 0.0, 0.0, 1.0]),
467 SlicePreviewMode::Coronal => ([0.0, 0.5, 1.0, 1.0], [1.0, 0.0, 0.0, 1.0]),
468 SlicePreviewMode::Sagittal => ([0.0, 0.5, 1.0, 1.0], [0.0, 1.0, 0.0, 1.0]),
469 }
470}
471
472pub struct SingleSliceEngine {
479 renderer: VolumeRenderer,
480 prepared_volume: Option<IncrementalVolume>,
481 geometry: Option<VolumeGeometry>,
482 slice_state: SlicePreviewState,
483 #[allow(dead_code)]
484 device: Arc<wgpu::Device>,
485 #[allow(dead_code)]
486 queue: Arc<wgpu::Queue>,
487}
488
489impl SingleSliceEngine {
490 #[must_use]
492 pub fn new(
493 device: &wgpu::Device,
494 queue: &wgpu::Queue,
495 output_format: wgpu::TextureFormat,
496 ) -> Self {
497 Self::from_arc(
498 Arc::new(device.clone()),
499 Arc::new(queue.clone()),
500 output_format,
501 )
502 }
503
504 #[must_use]
506 pub fn from_arc(
507 device: Arc<wgpu::Device>,
508 queue: Arc<wgpu::Queue>,
509 output_format: wgpu::TextureFormat,
510 ) -> Self {
511 Self {
512 renderer: VolumeRenderer::from_arc(device.clone(), queue.clone(), output_format),
513 prepared_volume: None,
514 geometry: None,
515 slice_state: SlicePreviewState::default(),
516 device,
517 queue,
518 }
519 }
520
521 pub fn prepare_volume(&mut self, geometry: VolumeGeometry) -> Result<(), RenderEngineError> {
523 self.prepared_volume = Some(IncrementalVolume::new(geometry)?);
524 self.geometry = Some(geometry);
525 self.renderer.allocate_volume(
526 geometry.dimensions,
527 geometry.spacing,
528 geometry.origin,
529 geometry.direction,
530 (0.0, 1.0),
531 true,
532 );
533 Ok(())
534 }
535
536 pub fn insert_slice(&mut self, z_index: u32, pixels: &[i16]) -> Result<(), RenderEngineError> {
538 let volume = self
539 .prepared_volume
540 .as_mut()
541 .ok_or(RenderEngineError::NoPreparedVolume)?;
542 volume.insert_slice(z_index, pixels)?;
543 let scalar_range = volume
544 .scalar_range()
545 .map(|(min, max)| (f64::from(min), f64::from(max)))
546 .unwrap_or((0.0, 1.0));
547 update_texture_slice_i16(&mut self.renderer, z_index, pixels, scalar_range)?;
548 Ok(())
549 }
550
551 #[must_use]
553 pub fn prepared_volume(&self) -> Option<&IncrementalVolume> {
554 self.prepared_volume.as_ref()
555 }
556
557 pub fn slice_state_mut(&mut self) -> &mut SlicePreviewState {
559 &mut self.slice_state
560 }
561
562 #[must_use]
564 pub fn scalar_range(&self) -> Option<(f64, f64)> {
565 self.prepared_volume
566 .as_ref()
567 .and_then(IncrementalVolume::scalar_range)
568 .map(|(min, max)| (f64::from(min), f64::from(max)))
569 }
570
571 pub fn set_slice_mode(&mut self, mode: SlicePreviewMode) {
573 self.slice_state.set_mode(mode);
574 }
575
576 pub fn scroll_slice(&mut self, delta: f64) -> Result<(), RenderEngineError> {
578 let bounds = self.bounds()?;
579 self.slice_state.scroll_by(delta, bounds);
580 Ok(())
581 }
582
583 pub fn set_window_level(&mut self, center: f64, width: f64) -> Result<(), RenderEngineError> {
585 let (scalar_min, scalar_max) = self
586 .scalar_range()
587 .ok_or(RenderEngineError::NoPreparedVolume)?;
588 self.slice_state
589 .set_transfer_window(center, width, scalar_min, scalar_max);
590 Ok(())
591 }
592
593 pub fn reset(&mut self) {
595 self.slice_state.reset();
596 }
597
598 pub fn render_slice(
600 &mut self,
601 encoder: &mut wgpu::CommandEncoder,
602 target: &RenderTarget<'_>,
603 ) -> Result<(), RenderEngineError> {
604 let _volume = self
605 .prepared_volume
606 .as_ref()
607 .ok_or(RenderEngineError::NoPreparedVolume)?;
608 let geometry = self.geometry.ok_or(RenderEngineError::NoPreparedVolume)?;
609 let bounds = bounds_from_geometry(geometry);
610 let scalar_range = self.scalar_range().unwrap_or((0.0, 1.0));
611 let (center, width) = self.slice_state.transfer_window(scalar_range.0, scalar_range.1);
612 let window_level = WindowLevel::new(center, width.max(1.0));
613 let mut slice_plane = self.slice_state.slice_plane(bounds);
614
615 let vp_w = f64::from(target.viewport.width.max(1));
618 let vp_h = f64::from(target.viewport.height.max(1));
619 let vp_aspect = vp_w / vp_h;
620 let data_aspect = slice_plane.width / slice_plane.height.max(1e-6);
621 if vp_aspect > data_aspect {
622 slice_plane.width = slice_plane.height * vp_aspect;
623 } else {
624 slice_plane.height = slice_plane.width / vp_aspect;
625 }
626
627 self.renderer.render_slice(
628 encoder,
629 target.view,
630 &slice_plane,
631 &window_level,
632 target.viewport,
633 self.slice_state.thick_slab().as_ref(),
634 )?;
635 Ok(())
636 }
637
638 fn bounds(&self) -> Result<Aabb, RenderEngineError> {
639 let geometry = self.geometry.ok_or(RenderEngineError::NoPreparedVolume)?;
640 Ok(bounds_from_geometry(geometry))
641 }
642}
643
644#[cfg(test)]
645mod tests {
646 use super::*;
647 use approx::assert_abs_diff_eq;
648 use glam::{DMat3, UVec3};
649 use std::sync::mpsc;
650
651 fn geometry() -> VolumeGeometry {
652 VolumeGeometry::new(
653 UVec3::new(10, 20, 30),
654 DVec3::new(0.8, 0.6, 1.2),
655 DVec3::ZERO,
656 DMat3::IDENTITY,
657 )
658 }
659
660 #[test]
661 fn bounds_match_geometry() {
662 let bounds = bounds_from_geometry(geometry());
663 assert_abs_diff_eq!(bounds.max.x, 7.2, epsilon = 1e-6);
664 assert_abs_diff_eq!(bounds.max.y, 11.4, epsilon = 1e-6);
665 assert_abs_diff_eq!(bounds.max.z, 34.8, epsilon = 1e-6);
666 }
667
668 #[test]
669 fn camera_targets_volume_center() {
670 let geometry = geometry();
671 let camera = camera_for_state(geometry, VolumeViewState::default());
672 let center = bounds_from_geometry(geometry).center();
673 assert!((camera.focal_point() - center).length() < 1e-6);
674 assert!(camera.distance() > bounds_from_geometry(geometry).diagonal());
675 }
676
677 fn test_device() -> Option<(wgpu::Device, wgpu::Queue)> {
678 pollster::block_on(async {
679 let instance = wgpu::Instance::default();
680 let adapter = instance
681 .request_adapter(&wgpu::RequestAdapterOptions {
682 power_preference: wgpu::PowerPreference::LowPower,
683 compatible_surface: None,
684 force_fallback_adapter: false,
685 })
686 .await
687 .ok()?;
688 adapter
689 .request_device(&wgpu::DeviceDescriptor::default())
690 .await
691 .ok()
692 })
693 }
694
695 fn create_render_texture(device: &wgpu::Device, size: u32) -> wgpu::Texture {
696 device.create_texture(&wgpu::TextureDescriptor {
697 label: Some("dicomview_gpu_test_target"),
698 size: wgpu::Extent3d {
699 width: size,
700 height: size,
701 depth_or_array_layers: 1,
702 },
703 mip_level_count: 1,
704 sample_count: 1,
705 dimension: wgpu::TextureDimension::D2,
706 format: wgpu::TextureFormat::Rgba8Unorm,
707 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
708 view_formats: &[],
709 })
710 }
711
712 fn read_texture(
713 device: &wgpu::Device,
714 queue: &wgpu::Queue,
715 texture: &wgpu::Texture,
716 width: u32,
717 height: u32,
718 ) -> Vec<u8> {
719 let unpadded_bytes_per_row = width * 4;
720 let padded_bytes_per_row = unpadded_bytes_per_row.div_ceil(256) * 256;
721 let buffer_size = u64::from(padded_bytes_per_row) * u64::from(height);
722 let buffer = device.create_buffer(&wgpu::BufferDescriptor {
723 label: Some("dicomview_gpu_test_readback"),
724 size: buffer_size,
725 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
726 mapped_at_creation: false,
727 });
728
729 let mut encoder =
730 device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
731 encoder.copy_texture_to_buffer(
732 texture.as_image_copy(),
733 wgpu::TexelCopyBufferInfo {
734 buffer: &buffer,
735 layout: wgpu::TexelCopyBufferLayout {
736 offset: 0,
737 bytes_per_row: Some(padded_bytes_per_row),
738 rows_per_image: Some(height),
739 },
740 },
741 wgpu::Extent3d {
742 width,
743 height,
744 depth_or_array_layers: 1,
745 },
746 );
747 queue.submit(std::iter::once(encoder.finish()));
748
749 let (sender, receiver) = mpsc::channel();
750 buffer
751 .slice(..)
752 .map_async(wgpu::MapMode::Read, move |result| {
753 let _ = sender.send(result);
754 });
755 let _ = device.poll(wgpu::PollType::wait_indefinitely());
756 receiver.recv().expect("map callback").expect("map success");
757
758 let mapped = buffer.slice(..).get_mapped_range();
759 let mut pixels = vec![0u8; (unpadded_bytes_per_row * height) as usize];
760 for row in 0..height as usize {
761 let src_offset = row * padded_bytes_per_row as usize;
762 let dst_offset = row * unpadded_bytes_per_row as usize;
763 pixels[dst_offset..dst_offset + unpadded_bytes_per_row as usize]
764 .copy_from_slice(&mapped[src_offset..src_offset + unpadded_bytes_per_row as usize]);
765 }
766 drop(mapped);
767 buffer.unmap();
768 pixels
769 }
770
771 fn checksum(bytes: &[u8]) -> u64 {
772 bytes.iter().enumerate().fold(0u64, |acc, (index, value)| {
773 acc.wrapping_add((index as u64 + 1) * u64::from(*value))
774 })
775 }
776
777 #[test]
778 #[ignore = "requires a working GPU adapter"]
779 fn render_engine_progressive_snapshot_checksum() {
780 let Some((device, queue)) = test_device() else {
781 return;
782 };
783 let mut engine = RenderEngine::new(&device, &queue, wgpu::TextureFormat::Rgba8Unorm);
784 let geometry = VolumeGeometry::new(
785 UVec3::new(16, 16, 16),
786 DVec3::ONE,
787 DVec3::ZERO,
788 DMat3::IDENTITY,
789 );
790 engine.prepare_volume(geometry).expect("prepare volume");
791
792 for z in 0..geometry.dimensions.z {
793 let mut slice = vec![0i16; geometry.slice_len()];
794 for y in 0..geometry.dimensions.y {
795 for x in 0..geometry.dimensions.x {
796 let index = (y * geometry.dimensions.x + x) as usize;
797 let dx = x as f64 - 7.5;
798 let dy = y as f64 - 7.5;
799 let dz = z as f64 - 7.5;
800 if (dx * dx + dy * dy + dz * dz).sqrt() <= 5.0 {
801 slice[index] = 1500;
802 }
803 }
804 }
805 engine.insert_slice(z, &slice).expect("insert slice");
806 }
807
808 engine
809 .set_crosshair(DVec3::new(8.0, 8.0, 8.0))
810 .expect("set crosshair");
811 engine.set_thick_slab(
812 SlicePreviewMode::Axial,
813 6.0,
814 SliceProjectionMode::MaximumIntensity,
815 );
816
817 let axial_texture = create_render_texture(&device, 96);
818 let coronal_texture = create_render_texture(&device, 96);
819 let sagittal_texture = create_render_texture(&device, 96);
820 let volume_texture = create_render_texture(&device, 96);
821 let axial_view = axial_texture.create_view(&wgpu::TextureViewDescriptor::default());
822 let coronal_view = coronal_texture.create_view(&wgpu::TextureViewDescriptor::default());
823 let sagittal_view = sagittal_texture.create_view(&wgpu::TextureViewDescriptor::default());
824 let volume_view = volume_texture.create_view(&wgpu::TextureViewDescriptor::default());
825
826 let mut encoder =
827 device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
828 engine
829 .render_frame(
830 &mut encoder,
831 FrameTargets {
832 axial: RenderTarget {
833 view: &axial_view,
834 viewport: Viewport::full(96, 96),
835 },
836 coronal: RenderTarget {
837 view: &coronal_view,
838 viewport: Viewport::full(96, 96),
839 },
840 sagittal: RenderTarget {
841 view: &sagittal_view,
842 viewport: Viewport::full(96, 96),
843 },
844 volume: RenderTarget {
845 view: &volume_view,
846 viewport: Viewport::full(96, 96),
847 },
848 },
849 true,
850 )
851 .expect("render frame");
852 queue.submit(std::iter::once(encoder.finish()));
853
854 let axial_pixels = read_texture(&device, &queue, &axial_texture, 96, 96);
855 let volume_pixels = read_texture(&device, &queue, &volume_texture, 96, 96);
856 assert!(
857 checksum(&axial_pixels) > 0,
858 "axial slice should not be empty"
859 );
860 assert!(
861 checksum(&volume_pixels) > 0,
862 "volume render should not be empty"
863 );
864 }
865}