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