Skip to main content

gpu_volume_render/
volume_render.rs

1//! 3D Volume Rendering Module
2//!
3//! Implements GPU-accelerated volume rendering techniques:
4//! - MIP (Maximum Intensity Projection)
5//! - AIP (Average Intensity Projection)
6//! - MinIP (Minimum Intensity Projection)
7//! - DVR (Direct Volume Rendering) with transfer functions
8
9use std::sync::Arc;
10use tracing::{debug, info};
11use wgpu::{
12    BindGroupDescriptor, BindGroupEntry, BindGroupLayout, BindGroupLayoutDescriptor,
13    BindGroupLayoutEntry, BindingType, Buffer, BufferBindingType, BufferDescriptor, BufferUsages,
14    ComputePipeline, ComputePipelineDescriptor, Device, PipelineLayoutDescriptor, Queue,
15    ShaderModuleDescriptor, ShaderSource, ShaderStages,
16};
17
18use crate::gpu_executor::GpuVolume;
19use crate::mpr_ops::MprError;
20
21/// Rendering mode for volume visualization
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum RenderMode {
24    /// Maximum Intensity Projection - shows brightest voxel along ray
25    MIP,
26    /// Average Intensity Projection - shows average of voxels along ray
27    AIP,
28    /// Minimum Intensity Projection - shows darkest voxel along ray
29    MinIP,
30    /// Direct Volume Rendering with transfer function
31    DVR,
32    /// DVR without shading (faster)
33    DVRNoShading,
34}
35
36/// Transfer function preset for medical imaging
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum TransferFunctionPreset {
39    /// Bone visualization - high HU values in white/yellow
40    Bone,
41    /// Soft tissue - muscle, organs
42    SoftTissue,
43    /// Blood vessels - contrast enhanced angiography
44    Vessel,
45    /// Lung/Airways - low HU values for airways
46    Lung,
47    /// Skin surface rendering
48    Skin,
49    /// Custom - uses provided transfer function
50    Custom,
51}
52
53/// Single entry in transfer function (RGBA, values 0-1)
54#[repr(C)]
55#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
56pub struct TransferFunctionEntry {
57    pub r: f32,
58    pub g: f32,
59    pub b: f32,
60    pub a: f32,
61}
62
63impl TransferFunctionEntry {
64    pub fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
65        Self { r, g, b, a }
66    }
67
68    pub fn transparent() -> Self {
69        Self::new(0.0, 0.0, 0.0, 0.0)
70    }
71}
72
73/// Transfer function for DVR
74#[derive(Debug, Clone)]
75pub struct TransferFunction {
76    /// LUT entries
77    pub entries: Vec<TransferFunctionEntry>,
78    /// HU range minimum
79    pub min_hu: f32,
80    /// HU range maximum
81    pub max_hu: f32,
82}
83
84impl TransferFunction {
85    /// Create transfer function with given range
86    pub fn new(min_hu: f32, max_hu: f32, size: usize) -> Self {
87        Self {
88            entries: vec![TransferFunctionEntry::transparent(); size],
89            min_hu,
90            max_hu,
91        }
92    }
93
94    /// Get preset transfer function
95    pub fn from_preset(preset: TransferFunctionPreset) -> Self {
96        match preset {
97            TransferFunctionPreset::Bone => Self::bone_preset(),
98            TransferFunctionPreset::SoftTissue => Self::soft_tissue_preset(),
99            TransferFunctionPreset::Vessel => Self::vessel_preset(),
100            TransferFunctionPreset::Lung => Self::lung_preset(),
101            TransferFunctionPreset::Skin => Self::skin_preset(),
102            TransferFunctionPreset::Custom => Self::new(-1000.0, 3000.0, 256),
103        }
104    }
105
106    /// Bone visualization preset (HU: 200-2000+)
107    pub fn bone_preset() -> Self {
108        let mut tf = Self::new(-200.0, 2000.0, 256);
109        let range = tf.max_hu - tf.min_hu;
110
111        for (i, entry) in tf.entries.iter_mut().enumerate() {
112            let t = i as f32 / 255.0;
113            let hu = tf.min_hu + t * range;
114
115            if hu < 150.0 {
116                // Below bone threshold - transparent
117                *entry = TransferFunctionEntry::transparent();
118            } else if hu < 300.0 {
119                // Transition zone (cartilage)
120                let alpha = (hu - 150.0) / 150.0;
121                *entry = TransferFunctionEntry::new(
122                    0.9,           // Light gray
123                    0.85,
124                    0.75,
125                    alpha * 0.3,   // Low opacity
126                );
127            } else if hu < 700.0 {
128                // Trabecular bone
129                let t_bone = (hu - 300.0) / 400.0;
130                *entry = TransferFunctionEntry::new(
131                    0.95 - t_bone * 0.1,    // Cream to white
132                    0.9 - t_bone * 0.15,
133                    0.75 - t_bone * 0.2,
134                    0.4 + t_bone * 0.3,     // Medium opacity
135                );
136            } else {
137                // Cortical bone (dense)
138                let t_dense = ((hu - 700.0) / 1300.0).min(1.0);
139                *entry = TransferFunctionEntry::new(
140                    1.0,                      // Pure white
141                    1.0 - t_dense * 0.1,
142                    0.95 - t_dense * 0.2,
143                    0.7 + t_dense * 0.3,     // High opacity
144                );
145            }
146        }
147
148        tf
149    }
150
151    /// Soft tissue preset (HU: -100 to 200)
152    pub fn soft_tissue_preset() -> Self {
153        let mut tf = Self::new(-200.0, 400.0, 256);
154        let range = tf.max_hu - tf.min_hu;
155
156        for (i, entry) in tf.entries.iter_mut().enumerate() {
157            let t = i as f32 / 255.0;
158            let hu = tf.min_hu + t * range;
159
160            if hu < -100.0 {
161                // Air/fat - nearly transparent
162                *entry = TransferFunctionEntry::transparent();
163            } else if hu < 0.0 {
164                // Fat tissue - yellowish, low opacity
165                let alpha = (hu + 100.0) / 100.0;
166                *entry = TransferFunctionEntry::new(
167                    1.0,
168                    0.85,
169                    0.4,
170                    alpha * 0.15,
171                );
172            } else if hu < 50.0 {
173                // Water/fluid - bluish
174                let t_water = hu / 50.0;
175                *entry = TransferFunctionEntry::new(
176                    0.5 + t_water * 0.3,
177                    0.6 + t_water * 0.2,
178                    0.9,
179                    0.2,
180                );
181            } else if hu < 100.0 {
182                // Muscle - reddish/brownish
183                let t_muscle = (hu - 50.0) / 50.0;
184                *entry = TransferFunctionEntry::new(
185                    0.8 + t_muscle * 0.15,   // Red
186                    0.45 - t_muscle * 0.1,   // Less green
187                    0.4 - t_muscle * 0.1,    // Less blue
188                    0.25 + t_muscle * 0.1,
189                );
190            } else {
191                // Dense tissue - lighter
192                let t_dense = ((hu - 100.0) / 300.0).min(1.0);
193                *entry = TransferFunctionEntry::new(
194                    0.9 + t_dense * 0.1,
195                    0.7 + t_dense * 0.2,
196                    0.65 + t_dense * 0.2,
197                    0.35 + t_dense * 0.2,
198                );
199            }
200        }
201
202        tf
203    }
204
205    /// Blood vessel preset (contrast enhanced, HU: 100-400)
206    pub fn vessel_preset() -> Self {
207        let mut tf = Self::new(-100.0, 500.0, 256);
208        let range = tf.max_hu - tf.min_hu;
209
210        for (i, entry) in tf.entries.iter_mut().enumerate() {
211            let t = i as f32 / 255.0;
212            let hu = tf.min_hu + t * range;
213
214            if hu < 80.0 {
215                // Below vessel threshold
216                *entry = TransferFunctionEntry::transparent();
217            } else if hu < 150.0 {
218                // Transition
219                let alpha = (hu - 80.0) / 70.0;
220                *entry = TransferFunctionEntry::new(
221                    0.9,
222                    0.2,
223                    0.2,
224                    alpha * 0.3,
225                );
226            } else if hu < 300.0 {
227                // Blood vessel (contrast)
228                let t_vessel = (hu - 150.0) / 150.0;
229                *entry = TransferFunctionEntry::new(
230                    1.0,                     // Bright red
231                    0.15 + t_vessel * 0.2,
232                    0.1 + t_vessel * 0.15,
233                    0.5 + t_vessel * 0.3,
234                );
235            } else {
236                // High contrast region
237                let t_high = ((hu - 300.0) / 200.0).min(1.0);
238                *entry = TransferFunctionEntry::new(
239                    1.0,
240                    0.4 + t_high * 0.4,     // Toward yellow
241                    0.25 + t_high * 0.3,
242                    0.8 + t_high * 0.2,
243                );
244            }
245        }
246
247        tf
248    }
249
250    /// Lung/airways preset (HU: -1000 to -300)
251    pub fn lung_preset() -> Self {
252        let mut tf = Self::new(-1100.0, 0.0, 256);
253        let range = tf.max_hu - tf.min_hu;
254
255        for (i, entry) in tf.entries.iter_mut().enumerate() {
256            let t = i as f32 / 255.0;
257            let hu = tf.min_hu + t * range;
258
259            if hu < -950.0 {
260                // Outside body
261                *entry = TransferFunctionEntry::transparent();
262            } else if hu < -700.0 {
263                // Lung parenchyma
264                let t_lung = (hu + 950.0) / 250.0;
265                *entry = TransferFunctionEntry::new(
266                    0.3 + t_lung * 0.2,      // Blueish
267                    0.4 + t_lung * 0.2,
268                    0.7 + t_lung * 0.1,
269                    0.05 + t_lung * 0.1,
270                );
271            } else if hu < -400.0 {
272                // Lung vessels/structures
273                let t_struct = (hu + 700.0) / 300.0;
274                *entry = TransferFunctionEntry::new(
275                    0.5 + t_struct * 0.3,
276                    0.4 + t_struct * 0.2,
277                    0.6 + t_struct * 0.1,
278                    0.15 + t_struct * 0.15,
279                );
280            } else {
281                // Bronchi walls / soft tissue
282                let t_wall = ((hu + 400.0) / 400.0).min(1.0);
283                *entry = TransferFunctionEntry::new(
284                    0.8 + t_wall * 0.15,
285                    0.6 + t_wall * 0.2,
286                    0.5 + t_wall * 0.2,
287                    0.3 + t_wall * 0.2,
288                );
289            }
290        }
291
292        tf
293    }
294
295    /// Skin surface preset (HU: -200 to 200)
296    pub fn skin_preset() -> Self {
297        let mut tf = Self::new(-500.0, 500.0, 256);
298        let range = tf.max_hu - tf.min_hu;
299
300        for (i, entry) in tf.entries.iter_mut().enumerate() {
301            let t = i as f32 / 255.0;
302            let hu = tf.min_hu + t * range;
303
304            if hu < -200.0 {
305                // Air - transparent
306                *entry = TransferFunctionEntry::transparent();
307            } else if hu < -50.0 {
308                // Skin surface edge
309                let alpha = (hu + 200.0) / 150.0;
310                *entry = TransferFunctionEntry::new(
311                    0.95,                    // Skin tone
312                    0.75,
313                    0.6,
314                    alpha * 0.6,             // Sharp surface
315                );
316            } else if hu < 100.0 {
317                // Below skin - make transparent for surface rendering
318                *entry = TransferFunctionEntry::new(
319                    0.9,
320                    0.7,
321                    0.55,
322                    0.1,                     // Very low opacity for depth
323                );
324            } else {
325                // Dense structures - transparent for pure skin rendering
326                *entry = TransferFunctionEntry::transparent();
327            }
328        }
329
330        tf
331    }
332}
333
334/// Camera state for 3D rendering
335#[derive(Debug, Clone, Copy)]
336pub struct Camera {
337    /// Camera position in volume coordinates
338    pub position: [f32; 3],
339    /// Look direction (normalized)
340    pub look_dir: [f32; 3],
341    /// Up vector (normalized)
342    pub up: [f32; 3],
343    /// Field of view in radians
344    pub fov: f32,
345}
346
347impl Default for Camera {
348    fn default() -> Self {
349        Self {
350            position: [0.0, 0.0, -500.0],
351            look_dir: [0.0, 0.0, 1.0],
352            up: [0.0, -1.0, 0.0],
353            fov: std::f32::consts::PI / 4.0, // 45 degrees
354        }
355    }
356}
357
358impl Camera {
359    /// Create camera looking at volume center from given angle
360    pub fn orbit(center: [f32; 3], distance: f32, azimuth: f32, elevation: f32) -> Self {
361        // Convert spherical to cartesian
362        let cos_elev = elevation.cos();
363        let sin_elev = elevation.sin();
364        let cos_azim = azimuth.cos();
365        let sin_azim = azimuth.sin();
366
367        let position = [
368            center[0] + distance * cos_elev * sin_azim,
369            center[1] + distance * sin_elev,
370            center[2] + distance * cos_elev * cos_azim,
371        ];
372
373        // Look direction is from camera to center
374        let look_dir = [
375            center[0] - position[0],
376            center[1] - position[1],
377            center[2] - position[2],
378        ];
379        let len = (look_dir[0].powi(2) + look_dir[1].powi(2) + look_dir[2].powi(2)).sqrt();
380        let look_dir = [look_dir[0] / len, look_dir[1] / len, look_dir[2] / len];
381
382        // Up vector (world Y up, adjusted for elevation)
383        let up = [0.0, -1.0, 0.0];
384
385        Self {
386            position,
387            look_dir,
388            up,
389            fov: std::f32::consts::PI / 4.0,
390        }
391    }
392}
393
394/// Parameters for MIP rendering (must match shader MipParams)
395#[repr(C)]
396#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
397struct MipUniforms {
398    // Volume dimensions
399    volume_width: u32,
400    volume_height: u32,
401    volume_depth: u32,
402
403    // Output dimensions
404    output_width: u32,
405    output_height: u32,
406
407    // Camera position
408    camera_x: f32,
409    camera_y: f32,
410    camera_z: f32,
411
412    // Camera look direction
413    look_x: f32,
414    look_y: f32,
415    look_z: f32,
416
417    // Camera up vector
418    up_x: f32,
419    up_y: f32,
420    up_z: f32,
421
422    // Field of view
423    fov: f32,
424
425    // Ray marching
426    step_size: f32,
427    max_steps: u32,
428
429    // Window/level
430    window_width: f32,
431    window_center: f32,
432
433    // Spacing
434    spacing_x: f32,
435    spacing_y: f32,
436    spacing_z: f32,
437
438    // Threshold
439    threshold: f32,
440
441    // Segmentation mask parameters (from Auto Clean)
442    mask_enabled: u32,     // 0 = disabled, 1 = enabled
443    visible_labels: u32,   // Bitmask of visible labels (bit N = label N visible)
444
445    // Sculpt mask parameters (from Lasso Sculpt) - separate from segmentation
446    sculpt_mask_enabled: u32,
447    sculpt_visible_labels: u32,
448
449    // Clipping plane parameters
450    clip_enabled: u32,
451    clip_inverted: u32,
452    clip_normal_x: f32,
453    clip_normal_y: f32,
454    clip_normal_z: f32,
455    clip_distance: f32,
456
457    // Padding to align struct to 16 bytes (33 fields * 4 = 132, need 144)
458    _pad1: u32,
459    _pad2: u32,
460    _pad3: u32,
461}
462
463/// Clipping plane configuration
464#[derive(Debug, Clone, Copy, Default)]
465pub struct ClipPlaneConfig {
466    pub enabled: bool,
467    pub inverted: bool,
468    pub normal: [f32; 3],
469    pub distance: f32,
470}
471
472/// 3D Box region clipping configuration (Sculpt Box)
473///
474/// Defines a 3D rectangular region for clipping. All values are in voxel coordinates.
475/// When enabled, only voxels within (or outside if inverted) the box are rendered.
476#[derive(Debug, Clone, Copy)]
477pub struct ClipBoxConfig {
478    pub enabled: bool,
479    pub inverted: bool, // true = show inside box, false = hide inside box
480    /// Min/max bounds for X axis (column index)
481    pub x_min: f32,
482    pub x_max: f32,
483    /// Min/max bounds for Y axis (row index)
484    pub y_min: f32,
485    pub y_max: f32,
486    /// Min/max bounds for Z axis (slice/depth index)
487    pub z_min: f32,
488    pub z_max: f32,
489}
490
491impl Default for ClipBoxConfig {
492    fn default() -> Self {
493        Self {
494            enabled: false,
495            inverted: false, // Default: hide inside box (remove region)
496            x_min: 0.0,
497            x_max: 1.0,
498            y_min: 0.0,
499            y_max: 1.0,
500            z_min: 0.0,
501            z_max: 1.0,
502        }
503    }
504}
505
506impl ClipBoxConfig {
507    /// Create a box config from percentage values (0-100)
508    #[allow(clippy::too_many_arguments)]
509    pub fn from_percentages(
510        x_min_pct: f32,
511        x_max_pct: f32,
512        y_min_pct: f32,
513        y_max_pct: f32,
514        z_min_pct: f32,
515        z_max_pct: f32,
516        dimensions: [usize; 3],
517        inverted: bool,
518    ) -> Self {
519        let [depth, rows, cols] = dimensions;
520        Self {
521            enabled: true,
522            inverted,
523            x_min: (x_min_pct / 100.0) * cols as f32,
524            x_max: (x_max_pct / 100.0) * cols as f32,
525            y_min: (y_min_pct / 100.0) * rows as f32,
526            y_max: (y_max_pct / 100.0) * rows as f32,
527            z_min: (z_min_pct / 100.0) * depth as f32,
528            z_max: (z_max_pct / 100.0) * depth as f32,
529        }
530    }
531}
532
533/// Slice plane navigation configuration (for MPR cross-section visualization)
534#[derive(Debug, Clone, Copy)]
535pub struct SlicePlaneConfig {
536    /// Whether slice plane visualization is enabled
537    pub enabled: bool,
538    /// Axial (Z) slice position in voxels
539    pub axial: f32,
540    /// Sagittal (X) slice position in voxels
541    pub sagittal: f32,
542    /// Coronal (Y) slice position in voxels
543    pub coronal: f32,
544    /// Line thickness in voxels (default: 1.5)
545    pub thickness: f32,
546    /// Plane opacity (default: 0.6)
547    pub opacity: f32,
548}
549
550impl Default for SlicePlaneConfig {
551    fn default() -> Self {
552        Self {
553            enabled: false,
554            axial: 0.0,
555            sagittal: 0.0,
556            coronal: 0.0,
557            thickness: 1.5,
558            opacity: 0.6,
559        }
560    }
561}
562
563impl SlicePlaneConfig {
564    /// Create slice plane config with default visual settings
565    pub fn new(axial: f32, sagittal: f32, coronal: f32) -> Self {
566        Self {
567            enabled: true,
568            axial,
569            sagittal,
570            coronal,
571            thickness: 1.5,
572            opacity: 0.6,
573        }
574    }
575
576    /// Create slice plane config from normalized positions (0.0-1.0) and volume dimensions
577    pub fn from_normalized(
578        axial_norm: f32,
579        sagittal_norm: f32,
580        coronal_norm: f32,
581        dimensions: [usize; 3],
582    ) -> Self {
583        Self {
584            enabled: true,
585            axial: axial_norm * (dimensions[0] - 1) as f32,
586            sagittal: sagittal_norm * (dimensions[2] - 1) as f32,
587            coronal: coronal_norm * (dimensions[1] - 1) as f32,
588            thickness: 1.5,
589            opacity: 0.6,
590        }
591    }
592}
593
594/// DVR rendering options
595#[derive(Debug, Clone, Copy)]
596pub struct DvrOptions {
597    pub clip_plane: ClipPlaneConfig,
598    pub clip_box: ClipBoxConfig,
599    pub opacity_multiplier: f32,
600    pub light_direction: [f32; 3],
601    pub slice_planes: SlicePlaneConfig,
602    /// Ray marching step size (lower = higher quality, slower)
603    /// Default: 0.5 (half voxel steps)
604    /// Range: 0.25 (ultra) to 2.0 (draft)
605    pub step_size: f32,
606}
607
608impl Default for DvrOptions {
609    fn default() -> Self {
610        Self {
611            clip_plane: ClipPlaneConfig::default(),
612            clip_box: ClipBoxConfig::default(),
613            opacity_multiplier: 1.0,
614            light_direction: [0.5, -0.7, -0.5],
615            slice_planes: SlicePlaneConfig::default(),
616            step_size: 0.5, // Default: high quality
617        }
618    }
619}
620
621/// Parameters for DVR rendering (must match shader DvrParams)
622#[repr(C)]
623#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
624struct DvrUniforms {
625    // Volume dimensions
626    volume_width: u32,
627    volume_height: u32,
628    volume_depth: u32,
629
630    // Output dimensions
631    output_width: u32,
632    output_height: u32,
633
634    // Camera position
635    camera_x: f32,
636    camera_y: f32,
637    camera_z: f32,
638
639    // Camera look direction
640    look_x: f32,
641    look_y: f32,
642    look_z: f32,
643
644    // Camera up vector
645    up_x: f32,
646    up_y: f32,
647    up_z: f32,
648
649    // Field of view
650    fov: f32,
651
652    // Ray marching
653    step_size: f32,
654    max_steps: u32,
655
656    // Transfer function range
657    tf_min: f32,
658    tf_max: f32,
659    tf_size: u32,
660
661    // Spacing
662    spacing_x: f32,
663    spacing_y: f32,
664    spacing_z: f32,
665
666    // Lighting
667    ambient: f32,
668    diffuse: f32,
669    specular: f32,
670    shininess: f32,
671
672    // Light direction
673    light_x: f32,
674    light_y: f32,
675    light_z: f32,
676
677    // Termination threshold
678    opacity_threshold: f32,
679
680    // Gradient threshold for shading
681    gradient_threshold: f32,
682
683    // Shading flag
684    enable_shading: u32,
685
686    // Clipping plane parameters
687    clip_enabled: u32,
688    clip_inverted: u32,
689    clip_normal_x: f32,
690    clip_normal_y: f32,
691    clip_normal_z: f32,
692    clip_distance: f32,
693
694    // Global opacity multiplier
695    opacity_multiplier: f32,
696
697    // Segmentation mask parameters (from Auto Clean)
698    mask_enabled: u32,     // 0 = disabled, 1 = enabled
699    visible_labels: u32,   // Bitmask of visible labels (bit N = label N visible)
700
701    // Sculpt mask parameters (from Lasso Sculpt) - separate from segmentation
702    sculpt_mask_enabled: u32,
703    sculpt_visible_labels: u32,
704
705    // Slice plane navigation parameters
706    slice_planes_enabled: u32,
707    axial_slice: f32,
708    sagittal_slice: f32,
709    coronal_slice: f32,
710    slice_plane_thickness: f32,
711    slice_plane_opacity: f32,
712    _padding2: u32,
713    _padding3: u32,
714}
715
716/// Mask configuration for segmentation-based rendering
717#[derive(Debug, Clone, Copy, Default)]
718pub struct MaskConfig {
719    /// Whether mask filtering is enabled
720    pub enabled: bool,
721    /// Bitmask of visible labels (bit N = label N is visible)
722    /// Default: 0xFFFFFFFF (all labels visible)
723    pub visible_labels: u32,
724}
725
726impl MaskConfig {
727    /// Create mask config with all labels visible
728    pub fn all_visible() -> Self {
729        Self {
730            enabled: true,
731            visible_labels: 0xFFFFFFFF, // All 32 labels visible
732        }
733    }
734
735    /// Create mask config showing only specific label
736    pub fn only_label(label: u8) -> Self {
737        Self {
738            enabled: true,
739            visible_labels: 1u32 << label,
740        }
741    }
742
743    /// Create mask config hiding specific label
744    pub fn hide_label(label: u8) -> Self {
745        Self {
746            enabled: true,
747            visible_labels: !(1u32 << label),
748        }
749    }
750}
751
752/// Parameters for sculpt mask generation (must match shader SculptParams)
753#[repr(C)]
754#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
755pub struct SculptUniforms {
756    // Volume dimensions
757    pub volume_width: u32,
758    pub volume_height: u32,
759    pub volume_depth: u32,
760
761    // Camera position
762    pub cam_pos_x: f32,
763    pub cam_pos_y: f32,
764    pub cam_pos_z: f32,
765
766    // Look direction (normalized)
767    pub look_x: f32,
768    pub look_y: f32,
769    pub look_z: f32,
770
771    // Right vector (normalized)
772    pub right_x: f32,
773    pub right_y: f32,
774    pub right_z: f32,
775
776    // Up vector (normalized)
777    pub up_x: f32,
778    pub up_y: f32,
779    pub up_z: f32,
780
781    // Projection parameters
782    pub fov_half_tan: f32,
783
784    // Aspect ratio (screen_width / screen_height) - CRITICAL for non-square viewports
785    pub aspect_ratio: f32,
786
787    // Polygon point count
788    pub polygon_point_count: u32,
789
790    // Action: 0 = remove_inside, 1 = keep_inside
791    pub action: u32,
792
793    // Polygon bounding box for early rejection
794    pub poly_min_x: f32,
795    pub poly_max_x: f32,
796    pub poly_min_y: f32,
797    pub poly_max_y: f32,
798
799    // Voxel spacing in mm [x, y, z] — converts voxel indices to physical coordinates
800    pub spacing_x: f32,
801    pub spacing_y: f32,
802    pub spacing_z: f32,
803
804    // Padding to align to 16 bytes (struct total must be multiple of 16)
805    pub _padding: f32,
806}
807
808/// GPU Volume Renderer
809pub struct VolumeRenderer {
810    device: Arc<Device>,
811    queue: Arc<Queue>,
812
813    // MIP pipeline and resources
814    mip_pipeline: ComputePipeline,
815    aip_pipeline: ComputePipeline,
816    minip_pipeline: ComputePipeline,
817    mip_bind_group_layout: BindGroupLayout,
818    mip_uniform_buffer: Buffer,
819
820    // DVR pipeline and resources
821    dvr_pipeline: ComputePipeline,
822    dvr_no_shading_pipeline: ComputePipeline,
823    dvr_bind_group_layout: BindGroupLayout,
824    dvr_uniform_buffer: Buffer,
825    transfer_function_buffer: Buffer,
826
827    // Sculpt mask pipeline and resources
828    sculpt_pipeline: ComputePipeline,
829    sculpt_bind_group_layout: BindGroupLayout,
830    sculpt_uniform_buffer: Buffer,
831    sculpt_polygon_buffer: Buffer,
832
833    // Current transfer function
834    current_transfer_function: TransferFunction,
835
836    // Segmentation mask buffer (packed u8 labels in u32) - from Auto Clean
837    mask_buffer: Option<Buffer>,
838    mask_config: MaskConfig,
839
840    // Sculpt mask buffer (separate from segmentation) - from Lasso Sculpt
841    sculpt_mask_buffer: Option<Buffer>,
842    sculpt_mask_config: MaskConfig,
843
844    // Dummy mask buffer (used when no mask is available to avoid buffer conflicts)
845    dummy_mask_buffer: Buffer,
846
847    // Shared resources
848    output_buffer: Buffer,
849    staging_buffer: Buffer,
850
851    // Output size
852    output_width: u32,
853    output_height: u32,
854}
855
856impl VolumeRenderer {
857    const WORKGROUP_SIZE: u32 = 16;
858    const TRANSFER_FUNCTION_SIZE: usize = 256;
859
860    /// Create new volume renderer
861    pub fn new(
862        device: Arc<Device>,
863        queue: Arc<Queue>,
864        output_width: u32,
865        output_height: u32,
866    ) -> Result<Self, MprError> {
867        let start = std::time::Instant::now();
868
869        // Load MIP shader
870        let mip_shader_source = include_str!("shaders/mip_raycaster.wgsl");
871        let mip_shader_module = device.create_shader_module(ShaderModuleDescriptor {
872            label: Some("mip_raycaster"),
873            source: ShaderSource::Wgsl(mip_shader_source.into()),
874        });
875
876        // Load DVR shader
877        let dvr_shader_source = include_str!("shaders/dvr_raycaster.wgsl");
878        let dvr_shader_module = device.create_shader_module(ShaderModuleDescriptor {
879            label: Some("dvr_raycaster"),
880            source: ShaderSource::Wgsl(dvr_shader_source.into()),
881        });
882
883        // Create MIP bind group layout
884        let mip_bind_group_layout = Self::create_mip_bind_group_layout(&device);
885
886        // Create DVR bind group layout (includes transfer function)
887        let dvr_bind_group_layout = Self::create_dvr_bind_group_layout(&device);
888
889        // Create MIP pipelines
890        let mip_pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
891            label: Some("mip_pipeline_layout"),
892            bind_group_layouts: &[&mip_bind_group_layout],
893            push_constant_ranges: &[],
894        });
895
896        let mip_pipeline = device.create_compute_pipeline(&ComputePipelineDescriptor {
897            label: Some("mip_pipeline"),
898            layout: Some(&mip_pipeline_layout),
899            module: &mip_shader_module,
900            entry_point: Some("main"),
901            compilation_options: Default::default(),
902            cache: None,
903        });
904
905        let aip_pipeline = device.create_compute_pipeline(&ComputePipelineDescriptor {
906            label: Some("aip_pipeline"),
907            layout: Some(&mip_pipeline_layout),
908            module: &mip_shader_module,
909            entry_point: Some("main_aip"),
910            compilation_options: Default::default(),
911            cache: None,
912        });
913
914        let minip_pipeline = device.create_compute_pipeline(&ComputePipelineDescriptor {
915            label: Some("minip_pipeline"),
916            layout: Some(&mip_pipeline_layout),
917            module: &mip_shader_module,
918            entry_point: Some("main_minip"),
919            compilation_options: Default::default(),
920            cache: None,
921        });
922
923        // Create DVR pipelines
924        let dvr_pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
925            label: Some("dvr_pipeline_layout"),
926            bind_group_layouts: &[&dvr_bind_group_layout],
927            push_constant_ranges: &[],
928        });
929
930        let dvr_pipeline = device.create_compute_pipeline(&ComputePipelineDescriptor {
931            label: Some("dvr_pipeline"),
932            layout: Some(&dvr_pipeline_layout),
933            module: &dvr_shader_module,
934            entry_point: Some("main"),
935            compilation_options: Default::default(),
936            cache: None,
937        });
938
939        let dvr_no_shading_pipeline = device.create_compute_pipeline(&ComputePipelineDescriptor {
940            label: Some("dvr_no_shading_pipeline"),
941            layout: Some(&dvr_pipeline_layout),
942            module: &dvr_shader_module,
943            entry_point: Some("main_no_shading"),
944            compilation_options: Default::default(),
945            cache: None,
946        });
947
948        // Load Sculpt shader
949        let sculpt_shader_source = include_str!("shaders/sculpt_mask.wgsl");
950        let sculpt_shader_module = device.create_shader_module(ShaderModuleDescriptor {
951            label: Some("sculpt_mask"),
952            source: ShaderSource::Wgsl(sculpt_shader_source.into()),
953        });
954
955        // Create Sculpt bind group layout
956        let sculpt_bind_group_layout = Self::create_sculpt_bind_group_layout(&device);
957
958        // Create Sculpt pipeline
959        let sculpt_pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
960            label: Some("sculpt_pipeline_layout"),
961            bind_group_layouts: &[&sculpt_bind_group_layout],
962            push_constant_ranges: &[],
963        });
964
965        let sculpt_pipeline = device.create_compute_pipeline(&ComputePipelineDescriptor {
966            label: Some("sculpt_pipeline"),
967            layout: Some(&sculpt_pipeline_layout),
968            module: &sculpt_shader_module,
969            entry_point: Some("main"),
970            compilation_options: Default::default(),
971            cache: None,
972        });
973
974        // Create MIP uniform buffer
975        let mip_uniform_buffer = device.create_buffer(&BufferDescriptor {
976            label: Some("mip_uniforms"),
977            size: std::mem::size_of::<MipUniforms>() as u64,
978            usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
979            mapped_at_creation: false,
980        });
981
982        // Create DVR uniform buffer
983        let dvr_uniform_buffer = device.create_buffer(&BufferDescriptor {
984            label: Some("dvr_uniforms"),
985            size: std::mem::size_of::<DvrUniforms>() as u64,
986            usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
987            mapped_at_creation: false,
988        });
989
990        // Create Sculpt uniform buffer
991        let sculpt_uniform_buffer = device.create_buffer(&BufferDescriptor {
992            label: Some("sculpt_uniforms"),
993            size: std::mem::size_of::<SculptUniforms>() as u64,
994            usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
995            mapped_at_creation: false,
996        });
997
998        // Create Sculpt polygon buffer (max 1024 points * 2 floats = 8KB)
999        let sculpt_polygon_buffer = device.create_buffer(&BufferDescriptor {
1000            label: Some("sculpt_polygon"),
1001            size: (1024 * 2 * std::mem::size_of::<f32>()) as u64,
1002            usage: BufferUsages::STORAGE | BufferUsages::COPY_DST,
1003            mapped_at_creation: false,
1004        });
1005
1006        // Create transfer function buffer (256 RGBA entries)
1007        let default_tf = TransferFunction::from_preset(TransferFunctionPreset::Bone);
1008        let transfer_function_buffer = device.create_buffer(&BufferDescriptor {
1009            label: Some("transfer_function"),
1010            size: (Self::TRANSFER_FUNCTION_SIZE * std::mem::size_of::<TransferFunctionEntry>()) as u64,
1011            usage: BufferUsages::STORAGE | BufferUsages::COPY_DST,
1012            mapped_at_creation: false,
1013        });
1014
1015        // Upload default transfer function
1016        queue.write_buffer(
1017            &transfer_function_buffer,
1018            0,
1019            bytemuck::cast_slice(&default_tf.entries),
1020        );
1021
1022        // Create output buffer
1023        let output_size = (output_width * output_height * 4) as u64; // RGBA u32
1024        let output_buffer = device.create_buffer(&BufferDescriptor {
1025            label: Some("volume_render_output"),
1026            size: output_size,
1027            usage: BufferUsages::STORAGE | BufferUsages::COPY_SRC,
1028            mapped_at_creation: false,
1029        });
1030
1031        // Create staging buffer for readback
1032        let staging_buffer = device.create_buffer(&BufferDescriptor {
1033            label: Some("volume_render_staging"),
1034            size: output_size,
1035            usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
1036            mapped_at_creation: false,
1037        });
1038
1039        // Create dummy mask buffer (4 bytes minimum, used when no real mask)
1040        // This avoids buffer conflicts when mask_buffer is None
1041        let dummy_mask_buffer = device.create_buffer(&BufferDescriptor {
1042            label: Some("dummy_mask"),
1043            size: 4, // Minimum 1 u32
1044            usage: BufferUsages::STORAGE | BufferUsages::COPY_DST,
1045            mapped_at_creation: false,
1046        });
1047
1048        info!(
1049            "Volume renderer created ({}x{}) with DVR and Sculpt support in {:.1}ms",
1050            output_width,
1051            output_height,
1052            start.elapsed().as_secs_f64() * 1000.0
1053        );
1054
1055        Ok(Self {
1056            device,
1057            queue,
1058            mip_pipeline,
1059            aip_pipeline,
1060            minip_pipeline,
1061            mip_bind_group_layout,
1062            mip_uniform_buffer,
1063            dvr_pipeline,
1064            dvr_no_shading_pipeline,
1065            dvr_bind_group_layout,
1066            dvr_uniform_buffer,
1067            transfer_function_buffer,
1068            sculpt_pipeline,
1069            sculpt_bind_group_layout,
1070            sculpt_uniform_buffer,
1071            sculpt_polygon_buffer,
1072            current_transfer_function: default_tf,
1073            mask_buffer: None,
1074            mask_config: MaskConfig::default(),
1075            sculpt_mask_buffer: None,
1076            sculpt_mask_config: MaskConfig::default(),
1077            dummy_mask_buffer,
1078            output_buffer,
1079            staging_buffer,
1080            output_width,
1081            output_height,
1082        })
1083    }
1084
1085    fn create_mip_bind_group_layout(device: &Device) -> BindGroupLayout {
1086        device.create_bind_group_layout(&BindGroupLayoutDescriptor {
1087            label: Some("mip_bind_group_layout"),
1088            entries: &[
1089                // binding 0: Volume data
1090                BindGroupLayoutEntry {
1091                    binding: 0,
1092                    visibility: ShaderStages::COMPUTE,
1093                    ty: BindingType::Buffer {
1094                        ty: BufferBindingType::Storage { read_only: true },
1095                        has_dynamic_offset: false,
1096                        min_binding_size: None,
1097                    },
1098                    count: None,
1099                },
1100                // binding 1: Uniforms
1101                BindGroupLayoutEntry {
1102                    binding: 1,
1103                    visibility: ShaderStages::COMPUTE,
1104                    ty: BindingType::Buffer {
1105                        ty: BufferBindingType::Uniform,
1106                        has_dynamic_offset: false,
1107                        min_binding_size: None,
1108                    },
1109                    count: None,
1110                },
1111                // binding 2: Output image
1112                BindGroupLayoutEntry {
1113                    binding: 2,
1114                    visibility: ShaderStages::COMPUTE,
1115                    ty: BindingType::Buffer {
1116                        ty: BufferBindingType::Storage { read_only: false },
1117                        has_dynamic_offset: false,
1118                        min_binding_size: None,
1119                    },
1120                    count: None,
1121                },
1122                // binding 3: Segmentation mask (packed u8 labels in u32) - from Auto Clean
1123                BindGroupLayoutEntry {
1124                    binding: 3,
1125                    visibility: ShaderStages::COMPUTE,
1126                    ty: BindingType::Buffer {
1127                        ty: BufferBindingType::Storage { read_only: true },
1128                        has_dynamic_offset: false,
1129                        min_binding_size: None,
1130                    },
1131                    count: None,
1132                },
1133                // binding 4: Sculpt mask (packed u8 labels in u32) - from Lasso Sculpt
1134                BindGroupLayoutEntry {
1135                    binding: 4,
1136                    visibility: ShaderStages::COMPUTE,
1137                    ty: BindingType::Buffer {
1138                        ty: BufferBindingType::Storage { read_only: true },
1139                        has_dynamic_offset: false,
1140                        min_binding_size: None,
1141                    },
1142                    count: None,
1143                },
1144            ],
1145        })
1146    }
1147
1148    fn create_dvr_bind_group_layout(device: &Device) -> BindGroupLayout {
1149        device.create_bind_group_layout(&BindGroupLayoutDescriptor {
1150            label: Some("dvr_bind_group_layout"),
1151            entries: &[
1152                // binding 0: Volume data
1153                BindGroupLayoutEntry {
1154                    binding: 0,
1155                    visibility: ShaderStages::COMPUTE,
1156                    ty: BindingType::Buffer {
1157                        ty: BufferBindingType::Storage { read_only: true },
1158                        has_dynamic_offset: false,
1159                        min_binding_size: None,
1160                    },
1161                    count: None,
1162                },
1163                // binding 1: Uniforms
1164                BindGroupLayoutEntry {
1165                    binding: 1,
1166                    visibility: ShaderStages::COMPUTE,
1167                    ty: BindingType::Buffer {
1168                        ty: BufferBindingType::Uniform,
1169                        has_dynamic_offset: false,
1170                        min_binding_size: None,
1171                    },
1172                    count: None,
1173                },
1174                // binding 2: Output image
1175                BindGroupLayoutEntry {
1176                    binding: 2,
1177                    visibility: ShaderStages::COMPUTE,
1178                    ty: BindingType::Buffer {
1179                        ty: BufferBindingType::Storage { read_only: false },
1180                        has_dynamic_offset: false,
1181                        min_binding_size: None,
1182                    },
1183                    count: None,
1184                },
1185                // binding 3: Transfer function LUT
1186                BindGroupLayoutEntry {
1187                    binding: 3,
1188                    visibility: ShaderStages::COMPUTE,
1189                    ty: BindingType::Buffer {
1190                        ty: BufferBindingType::Storage { read_only: true },
1191                        has_dynamic_offset: false,
1192                        min_binding_size: None,
1193                    },
1194                    count: None,
1195                },
1196                // binding 4: Segmentation mask (packed u8 labels in u32) - from Auto Clean
1197                BindGroupLayoutEntry {
1198                    binding: 4,
1199                    visibility: ShaderStages::COMPUTE,
1200                    ty: BindingType::Buffer {
1201                        ty: BufferBindingType::Storage { read_only: true },
1202                        has_dynamic_offset: false,
1203                        min_binding_size: None,
1204                    },
1205                    count: None,
1206                },
1207                // binding 5: Sculpt mask (packed u8 labels in u32) - from Lasso Sculpt
1208                BindGroupLayoutEntry {
1209                    binding: 5,
1210                    visibility: ShaderStages::COMPUTE,
1211                    ty: BindingType::Buffer {
1212                        ty: BufferBindingType::Storage { read_only: true },
1213                        has_dynamic_offset: false,
1214                        min_binding_size: None,
1215                    },
1216                    count: None,
1217                },
1218            ],
1219        })
1220    }
1221
1222    fn create_sculpt_bind_group_layout(device: &Device) -> BindGroupLayout {
1223        device.create_bind_group_layout(&BindGroupLayoutDescriptor {
1224            label: Some("sculpt_bind_group_layout"),
1225            entries: &[
1226                // binding 0: Uniforms (SculptParams)
1227                BindGroupLayoutEntry {
1228                    binding: 0,
1229                    visibility: ShaderStages::COMPUTE,
1230                    ty: BindingType::Buffer {
1231                        ty: BufferBindingType::Uniform,
1232                        has_dynamic_offset: false,
1233                        min_binding_size: None,
1234                    },
1235                    count: None,
1236                },
1237                // binding 1: Polygon points (array of f32)
1238                BindGroupLayoutEntry {
1239                    binding: 1,
1240                    visibility: ShaderStages::COMPUTE,
1241                    ty: BindingType::Buffer {
1242                        ty: BufferBindingType::Storage { read_only: true },
1243                        has_dynamic_offset: false,
1244                        min_binding_size: None,
1245                    },
1246                    count: None,
1247                },
1248                // binding 2: Mask output (packed u8 labels in atomic<u32>)
1249                BindGroupLayoutEntry {
1250                    binding: 2,
1251                    visibility: ShaderStages::COMPUTE,
1252                    ty: BindingType::Buffer {
1253                        ty: BufferBindingType::Storage { read_only: false },
1254                        has_dynamic_offset: false,
1255                        min_binding_size: None,
1256                    },
1257                    count: None,
1258                },
1259            ],
1260        })
1261    }
1262
1263    /// Set transfer function from preset
1264    pub fn set_transfer_function_preset(&mut self, preset: TransferFunctionPreset) {
1265        self.current_transfer_function = TransferFunction::from_preset(preset);
1266        self.upload_transfer_function();
1267    }
1268
1269    /// Set custom transfer function
1270    pub fn set_transfer_function(&mut self, tf: TransferFunction) {
1271        self.current_transfer_function = tf;
1272        self.upload_transfer_function();
1273    }
1274
1275    /// Get current transfer function
1276    pub fn transfer_function(&self) -> &TransferFunction {
1277        &self.current_transfer_function
1278    }
1279
1280    fn upload_transfer_function(&self) {
1281        // Ensure we have exactly 256 entries
1282        let mut entries = self.current_transfer_function.entries.clone();
1283        entries.resize(Self::TRANSFER_FUNCTION_SIZE, TransferFunctionEntry::transparent());
1284
1285        self.queue.write_buffer(
1286            &self.transfer_function_buffer,
1287            0,
1288            bytemuck::cast_slice(&entries),
1289        );
1290    }
1291
1292    /// Upload segmentation mask to GPU
1293    /// Mask data is u8 per voxel (label values 0-255)
1294    /// Will be packed into u32 for efficient GPU access
1295    pub fn upload_mask(&mut self, mask_data: &[u8], dimensions: [usize; 3]) {
1296        let total_voxels = dimensions[0] * dimensions[1] * dimensions[2];
1297
1298        // Validate input size
1299        if mask_data.len() != total_voxels {
1300            tracing::warn!(
1301                "Mask size mismatch: expected {} voxels, got {}",
1302                total_voxels,
1303                mask_data.len()
1304            );
1305            return;
1306        }
1307
1308        // Pack u8 labels into u32 (4 labels per u32)
1309        let packed_size = total_voxels.div_ceil(4);
1310        let mut packed_data: Vec<u32> = vec![0; packed_size];
1311
1312        for (i, &label) in mask_data.iter().enumerate() {
1313            let word_index = i / 4;
1314            let byte_offset = (i % 4) * 8;
1315            packed_data[word_index] |= (label as u32) << byte_offset;
1316        }
1317
1318        // Create or recreate buffer if needed
1319        let buffer_size = (packed_size * std::mem::size_of::<u32>()) as u64;
1320
1321        let needs_recreate = match &self.mask_buffer {
1322            Some(buf) => buf.size() != buffer_size,
1323            None => true,
1324        };
1325
1326        if needs_recreate {
1327            self.mask_buffer = Some(self.device.create_buffer(&BufferDescriptor {
1328                label: Some("segmentation_mask"),
1329                size: buffer_size,
1330                usage: BufferUsages::STORAGE | BufferUsages::COPY_DST,
1331                mapped_at_creation: false,
1332            }));
1333        }
1334
1335        // Upload packed data
1336        if let Some(ref buffer) = self.mask_buffer {
1337            self.queue.write_buffer(buffer, 0, bytemuck::cast_slice(&packed_data));
1338        }
1339
1340        debug!(
1341            "Uploaded mask: {}x{}x{} ({} voxels, {} packed u32s)",
1342            dimensions[0], dimensions[1], dimensions[2], total_voxels, packed_size
1343        );
1344    }
1345
1346    /// Set mask configuration (for segmentation)
1347    pub fn set_mask_config(&mut self, config: MaskConfig) {
1348        self.mask_config = config;
1349    }
1350
1351    /// Get current mask configuration
1352    pub fn mask_config(&self) -> &MaskConfig {
1353        &self.mask_config
1354    }
1355
1356    /// Get effective mask buffer and config for rendering
1357    /// Priority: Sculpt mask > Segmentation mask > Dummy buffer
1358    #[allow(dead_code)]
1359    fn get_effective_mask(&self) -> (&Buffer, bool, u32) {
1360        // Sculpt mask takes priority if active
1361        if self.sculpt_mask_config.enabled {
1362            if let Some(ref buf) = self.sculpt_mask_buffer {
1363                return (buf, true, self.sculpt_mask_config.visible_labels);
1364            }
1365        }
1366
1367        // Fall back to segmentation mask
1368        if self.mask_config.enabled {
1369            if let Some(ref buf) = self.mask_buffer {
1370                return (buf, true, self.mask_config.visible_labels);
1371            }
1372        }
1373
1374        // No mask active - use dummy buffer
1375        (&self.dummy_mask_buffer, false, 0xFFFFFFFF)
1376    }
1377
1378    /// Check if segmentation mask is available
1379    pub fn has_mask(&self) -> bool {
1380        self.mask_buffer.is_some()
1381    }
1382
1383    /// Check if sculpt mask is available
1384    pub fn has_sculpt_mask(&self) -> bool {
1385        self.sculpt_mask_buffer.is_some()
1386    }
1387
1388    /// Clear segmentation mask (from Auto Clean)
1389    pub fn clear_mask(&mut self) {
1390        self.mask_buffer = None;
1391        self.mask_config = MaskConfig::default();
1392    }
1393
1394    /// Clear sculpt mask (from Lasso Sculpt) - does NOT affect segmentation mask
1395    pub fn clear_sculpt_mask(&mut self) {
1396        self.sculpt_mask_buffer = None;
1397        self.sculpt_mask_config = MaskConfig::default();
1398        info!("Sculpt mask cleared (segmentation mask preserved)");
1399    }
1400
1401    /// Generate sculpt mask on GPU
1402    ///
1403    /// Projects a 2D lasso polygon into 3D space and creates a mask for
1404    /// voxels that fall inside (or outside) the extruded lasso region.
1405    ///
1406    /// # Parameters
1407    /// - `dimensions`: Volume dimensions [depth, rows, cols]
1408    /// - `camera`: Camera state for projection
1409    /// - `polygon_points`: 2D lasso points in normalized coordinates (0-1 range)
1410    /// - `action`: 0 = remove_inside, 1 = keep_inside
1411    ///
1412    /// # Returns
1413    /// Number of voxels affected (hidden by the mask)
1414    pub fn generate_sculpt_mask_gpu(
1415        &mut self,
1416        dimensions: [usize; 3],
1417        spacing: [f32; 3],
1418        camera: &Camera,
1419        polygon_points: &[[f32; 2]],
1420        action: u32,
1421        aspect_ratio: f32,
1422    ) -> Result<usize, MprError> {
1423        let start = std::time::Instant::now();
1424
1425        let [depth, rows, cols] = dimensions;
1426        let total_voxels = depth * rows * cols;
1427
1428        // Validate polygon
1429        if polygon_points.len() < 3 {
1430            return Err(MprError::GpuError("Lasso must have at least 3 points".to_string()));
1431        }
1432
1433        if polygon_points.len() > 1024 {
1434            return Err(MprError::GpuError("Lasso has too many points (max 1024)".to_string()));
1435        }
1436
1437        info!(
1438            "GPU sculpt mask generation: {}x{}x{} volume, {} polygon points, action={}",
1439            depth, rows, cols, polygon_points.len(), action
1440        );
1441
1442        // Calculate camera basis vectors — MUST be normalized for correct projection
1443        let look = camera.look_dir;
1444        let up = camera.up;
1445
1446        // Right vector = normalize(look × up)
1447        let rx = look[1] * up[2] - look[2] * up[1];
1448        let ry = look[2] * up[0] - look[0] * up[2];
1449        let rz = look[0] * up[1] - look[1] * up[0];
1450        let r_len = (rx * rx + ry * ry + rz * rz).sqrt().max(1e-8);
1451        let right = [rx / r_len, ry / r_len, rz / r_len];
1452
1453        // Corrected up = normalize(right × look) — orthogonal to both
1454        let ux = right[1] * look[2] - right[2] * look[1];
1455        let uy = right[2] * look[0] - right[0] * look[2];
1456        let uz = right[0] * look[1] - right[1] * look[0];
1457        let u_len = (ux * ux + uy * uy + uz * uz).sqrt().max(1e-8);
1458        let up_corrected = [ux / u_len, uy / u_len, uz / u_len];
1459
1460        // Calculate polygon bounding box
1461        let mut poly_min_x = f32::MAX;
1462        let mut poly_max_x = f32::MIN;
1463        let mut poly_min_y = f32::MAX;
1464        let mut poly_max_y = f32::MIN;
1465        for p in polygon_points {
1466            poly_min_x = poly_min_x.min(p[0]);
1467            poly_max_x = poly_max_x.max(p[0]);
1468            poly_min_y = poly_min_y.min(p[1]);
1469            poly_max_y = poly_max_y.max(p[1]);
1470        }
1471        // Add margin for edge cases
1472        poly_min_x -= 0.05;
1473        poly_max_x += 0.05;
1474        poly_min_y -= 0.05;
1475        poly_max_y += 0.05;
1476
1477        // Create uniforms
1478        let uniforms = SculptUniforms {
1479            volume_width: cols as u32,
1480            volume_height: rows as u32,
1481            volume_depth: depth as u32,
1482            cam_pos_x: camera.position[0],
1483            cam_pos_y: camera.position[1],
1484            cam_pos_z: camera.position[2],
1485            look_x: look[0],
1486            look_y: look[1],
1487            look_z: look[2],
1488            right_x: right[0],
1489            right_y: right[1],
1490            right_z: right[2],
1491            up_x: up_corrected[0],
1492            up_y: up_corrected[1],
1493            up_z: up_corrected[2],
1494            fov_half_tan: (camera.fov / 2.0).tan(),
1495            aspect_ratio,
1496            polygon_point_count: polygon_points.len() as u32,
1497            action,
1498            poly_min_x,
1499            poly_max_x,
1500            poly_min_y,
1501            poly_max_y,
1502            spacing_x: spacing[2], // x = cols spacing
1503            spacing_y: spacing[1], // y = rows spacing
1504            spacing_z: spacing[0], // z = depth spacing
1505            _padding: 0.0,
1506        };
1507
1508        // Upload uniforms
1509        self.queue.write_buffer(
1510            &self.sculpt_uniform_buffer,
1511            0,
1512            bytemuck::bytes_of(&uniforms),
1513        );
1514
1515        // Flatten polygon points to f32 array
1516        let polygon_data: Vec<f32> = polygon_points
1517            .iter()
1518            .flat_map(|p| [p[0], p[1]])
1519            .collect();
1520
1521        // Upload polygon points
1522        self.queue.write_buffer(
1523            &self.sculpt_polygon_buffer,
1524            0,
1525            bytemuck::cast_slice(&polygon_data),
1526        );
1527
1528        // Calculate packed mask buffer size (4 labels per u32)
1529        let packed_size = total_voxels.div_ceil(4);
1530        let mask_buffer_size = (packed_size * std::mem::size_of::<u32>()) as u64;
1531
1532        // Create/resize SCULPT mask buffer if needed (separate from segmentation mask!)
1533        let needs_recreate = match &self.sculpt_mask_buffer {
1534            Some(buf) => buf.size() != mask_buffer_size,
1535            None => true,
1536        };
1537
1538        if needs_recreate {
1539            self.sculpt_mask_buffer = Some(self.device.create_buffer(&BufferDescriptor {
1540                label: Some("sculpt_mask_output"),
1541                size: mask_buffer_size,
1542                usage: BufferUsages::STORAGE | BufferUsages::COPY_SRC | BufferUsages::COPY_DST,
1543                mapped_at_creation: false,
1544            }));
1545
1546            // Zero-initialize the new buffer (label 0 = visible by default)
1547            let zeros = vec![0u32; packed_size];
1548            self.queue.write_buffer(
1549                self.sculpt_mask_buffer.as_ref().unwrap(),
1550                0,
1551                bytemuck::cast_slice(&zeros),
1552            );
1553
1554            info!("Created new sculpt mask buffer (separate from segmentation)");
1555        }
1556        // NOTE: Don't zero existing buffer - we want to accumulate sculpt operations!
1557        // Each sculpt adds more hidden voxels (label 2) without affecting previous ones.
1558
1559        let sculpt_buffer = self.sculpt_mask_buffer.as_ref().unwrap();
1560
1561        // Create bind group
1562        let bind_group = self.device.create_bind_group(&BindGroupDescriptor {
1563            label: Some("sculpt_bind_group"),
1564            layout: &self.sculpt_bind_group_layout,
1565            entries: &[
1566                BindGroupEntry {
1567                    binding: 0,
1568                    resource: self.sculpt_uniform_buffer.as_entire_binding(),
1569                },
1570                BindGroupEntry {
1571                    binding: 1,
1572                    resource: self.sculpt_polygon_buffer.as_entire_binding(),
1573                },
1574                BindGroupEntry {
1575                    binding: 2,
1576                    resource: sculpt_buffer.as_entire_binding(),
1577                },
1578            ],
1579        });
1580
1581        // Execute compute shader
1582        let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
1583            label: Some("sculpt_encoder"),
1584        });
1585
1586        {
1587            let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
1588                label: Some("sculpt_pass"),
1589                timestamp_writes: None,
1590            });
1591
1592            compute_pass.set_pipeline(&self.sculpt_pipeline);
1593            compute_pass.set_bind_group(0, &bind_group, &[]);
1594
1595            // Workgroup size is 8x8x4, dispatch enough workgroups to cover volume
1596            let workgroups_x = (cols as u32).div_ceil(8);
1597            let workgroups_y = (rows as u32).div_ceil(8);
1598            let workgroups_z = (depth as u32).div_ceil(4);
1599
1600            compute_pass.dispatch_workgroups(workgroups_x, workgroups_y, workgroups_z);
1601        }
1602
1603        self.queue.submit(std::iter::once(encoder.finish()));
1604
1605        // Wait for GPU to complete
1606        self.device.poll(wgpu::Maintain::Wait);
1607
1608        let elapsed_ms = start.elapsed().as_secs_f64() * 1000.0;
1609
1610        info!(
1611            "GPU sculpt mask generated in {:.1}ms ({} voxels, {} workgroups)",
1612            elapsed_ms,
1613            total_voxels,
1614            cols.div_ceil(8) * rows.div_ceil(8) * depth.div_ceil(4)
1615        );
1616
1617        // Configure SCULPT mask for rendering:
1618        // - enabled: true (activate sculpt masking)
1619        // - visible_labels: 0b11 = labels 0 and 1 visible, label 2 hidden
1620        self.sculpt_mask_config = MaskConfig {
1621            enabled: true,
1622            visible_labels: 0b11,
1623        };
1624
1625        // Count affected voxels by reading back the mask (optional, for statistics)
1626        // For now, estimate based on bounding box coverage
1627        let bbox_coverage = (poly_max_x - poly_min_x) * (poly_max_y - poly_min_y);
1628        let estimated_affected = (total_voxels as f32 * bbox_coverage * 0.5) as usize;
1629
1630        Ok(estimated_affected)
1631    }
1632
1633    /// Render volume using specified mode
1634    pub fn render(
1635        &self,
1636        volume: &GpuVolume,
1637        camera: &Camera,
1638        mode: RenderMode,
1639        window_center: f32,
1640        window_width: f32,
1641        spacing: [f32; 3],
1642    ) -> Result<RenderResult, MprError> {
1643        self.render_with_options(
1644            volume,
1645            camera,
1646            mode,
1647            window_center,
1648            window_width,
1649            spacing,
1650            DvrOptions::default(),
1651        )
1652    }
1653
1654    /// Render volume with additional DVR options (clipping, opacity, lighting, quality)
1655    #[allow(clippy::too_many_arguments)]
1656    pub fn render_with_options(
1657        &self,
1658        volume: &GpuVolume,
1659        camera: &Camera,
1660        mode: RenderMode,
1661        window_center: f32,
1662        window_width: f32,
1663        spacing: [f32; 3],
1664        options: DvrOptions,
1665    ) -> Result<RenderResult, MprError> {
1666        match mode {
1667            RenderMode::MIP | RenderMode::AIP | RenderMode::MinIP => {
1668                self.render_mip(volume, camera, mode, window_center, window_width, spacing, &options)
1669            }
1670            RenderMode::DVR | RenderMode::DVRNoShading => {
1671                self.render_dvr(volume, camera, mode, spacing, options)
1672            }
1673        }
1674    }
1675
1676    /// Render using MIP/AIP/MinIP modes
1677    #[allow(clippy::too_many_arguments)]
1678    fn render_mip(
1679        &self,
1680        volume: &GpuVolume,
1681        camera: &Camera,
1682        mode: RenderMode,
1683        window_center: f32,
1684        window_width: f32,
1685        spacing: [f32; 3],
1686        options: &DvrOptions,
1687    ) -> Result<RenderResult, MprError> {
1688        let start = std::time::Instant::now();
1689
1690        // Check for chunked volume - not yet supported for 3D rendering
1691        // TODO: Implement multi-pass chunked rendering
1692        let volume_buffer = volume.buffer().ok_or_else(|| {
1693            MprError::GpuError(
1694                "Volume exceeds 2GB GPU buffer limit. Consider downsampling \
1695                 the volume or using fewer slices.".to_string()
1696            )
1697        })?;
1698
1699        let dims = volume.dimensions();
1700        let step_size = options.step_size;
1701        let clip = &options.clip_plane;
1702
1703        // Calculate max steps based on volume diagonal and step size
1704        // Smaller step_size = more steps = higher quality
1705        let max_diagonal = ((dims[0].pow(2) + dims[1].pow(2) + dims[2].pow(2)) as f32).sqrt();
1706        let max_steps = (max_diagonal / step_size * 2.0) as u32; // 2x diagonal for safety
1707
1708        // Update uniforms with mask and clip parameters
1709        let uniforms = MipUniforms {
1710            volume_width: dims[2] as u32,  // cols (X)
1711            volume_height: dims[1] as u32, // rows (Y)
1712            volume_depth: dims[0] as u32,  // depth (Z)
1713            output_width: self.output_width,
1714            output_height: self.output_height,
1715            camera_x: camera.position[0],
1716            camera_y: camera.position[1],
1717            camera_z: camera.position[2],
1718            look_x: camera.look_dir[0],
1719            look_y: camera.look_dir[1],
1720            look_z: camera.look_dir[2],
1721            up_x: camera.up[0],
1722            up_y: camera.up[1],
1723            up_z: camera.up[2],
1724            fov: camera.fov,
1725            step_size,
1726            max_steps,
1727            window_width,
1728            window_center,
1729            spacing_x: spacing[0],
1730            spacing_y: spacing[1],
1731            spacing_z: spacing[2],
1732            threshold: 0.0, // No early termination
1733            // Segmentation mask (from Auto Clean)
1734            mask_enabled: if self.mask_config.enabled && self.mask_buffer.is_some() { 1 } else { 0 },
1735            visible_labels: self.mask_config.visible_labels,
1736            // Sculpt mask (from Lasso Sculpt) - both masks are checked independently
1737            sculpt_mask_enabled: if self.sculpt_mask_config.enabled && self.sculpt_mask_buffer.is_some() { 1 } else { 0 },
1738            sculpt_visible_labels: self.sculpt_mask_config.visible_labels,
1739            clip_enabled: if clip.enabled { 1 } else { 0 },
1740            clip_inverted: if clip.inverted { 1 } else { 0 },
1741            clip_normal_x: clip.normal[0],
1742            clip_normal_y: clip.normal[1],
1743            clip_normal_z: clip.normal[2],
1744            clip_distance: clip.distance,
1745            _pad1: 0,
1746            _pad2: 0,
1747            _pad3: 0,
1748        };
1749
1750        self.queue
1751            .write_buffer(&self.mip_uniform_buffer, 0, bytemuck::bytes_of(&uniforms));
1752
1753        // Get both mask buffers (use dummy if not available)
1754        let seg_mask_buffer = self.mask_buffer.as_ref().unwrap_or(&self.dummy_mask_buffer);
1755        let sculpt_mask_buffer = self.sculpt_mask_buffer.as_ref().unwrap_or(&self.dummy_mask_buffer);
1756
1757        // Create bind group with BOTH masks
1758        let bind_group = self.device.create_bind_group(&BindGroupDescriptor {
1759            label: Some("mip_bind_group"),
1760            layout: &self.mip_bind_group_layout,
1761            entries: &[
1762                BindGroupEntry {
1763                    binding: 0,
1764                    resource: volume_buffer.as_entire_binding(),
1765                },
1766                BindGroupEntry {
1767                    binding: 1,
1768                    resource: self.mip_uniform_buffer.as_entire_binding(),
1769                },
1770                BindGroupEntry {
1771                    binding: 2,
1772                    resource: self.output_buffer.as_entire_binding(),
1773                },
1774                BindGroupEntry {
1775                    binding: 3,
1776                    resource: seg_mask_buffer.as_entire_binding(),
1777                },
1778                BindGroupEntry {
1779                    binding: 4,
1780                    resource: sculpt_mask_buffer.as_entire_binding(),
1781                },
1782            ],
1783        });
1784
1785        // Select pipeline based on mode
1786        let pipeline = match mode {
1787            RenderMode::MIP => &self.mip_pipeline,
1788            RenderMode::AIP => &self.aip_pipeline,
1789            RenderMode::MinIP => &self.minip_pipeline,
1790            _ => unreachable!(),
1791        };
1792
1793        // Execute compute and read back
1794        self.execute_and_readback(pipeline, &bind_group, mode, start)
1795    }
1796
1797    /// Render using DVR mode with transfer function
1798    fn render_dvr(
1799        &self,
1800        volume: &GpuVolume,
1801        camera: &Camera,
1802        mode: RenderMode,
1803        spacing: [f32; 3],
1804        options: DvrOptions,
1805    ) -> Result<RenderResult, MprError> {
1806        let start = std::time::Instant::now();
1807
1808        // Check for chunked volume - not yet supported for 3D rendering
1809        let volume_buffer = volume.buffer().ok_or_else(|| {
1810            MprError::GpuError(
1811                "Volume exceeds 2GB GPU buffer limit. Consider downsampling \
1812                 the volume or using fewer slices.".to_string()
1813            )
1814        })?;
1815
1816        let dims = volume.dimensions();
1817        let tf = &self.current_transfer_function;
1818
1819        // Use step size from options (smaller = higher quality, slower)
1820        let step_size = options.step_size;
1821        let max_diagonal = ((dims[0].pow(2) + dims[1].pow(2) + dims[2].pow(2)) as f32).sqrt();
1822        let max_steps = (max_diagonal / step_size * 2.0) as u32;
1823
1824        let enable_shading = if mode == RenderMode::DVR { 1u32 } else { 0u32 };
1825
1826        // Extract clipping plane config
1827        let clip = &options.clip_plane;
1828
1829        // Update DVR uniforms with mask parameters
1830        let uniforms = DvrUniforms {
1831            volume_width: dims[2] as u32,
1832            volume_height: dims[1] as u32,
1833            volume_depth: dims[0] as u32,
1834            output_width: self.output_width,
1835            output_height: self.output_height,
1836            camera_x: camera.position[0],
1837            camera_y: camera.position[1],
1838            camera_z: camera.position[2],
1839            look_x: camera.look_dir[0],
1840            look_y: camera.look_dir[1],
1841            look_z: camera.look_dir[2],
1842            up_x: camera.up[0],
1843            up_y: camera.up[1],
1844            up_z: camera.up[2],
1845            fov: camera.fov,
1846            step_size,
1847            max_steps,
1848            tf_min: tf.min_hu,
1849            tf_max: tf.max_hu,
1850            tf_size: tf.entries.len() as u32,
1851            spacing_x: spacing[0],
1852            spacing_y: spacing[1],
1853            spacing_z: spacing[2],
1854            ambient: 0.2,
1855            diffuse: 0.7,
1856            specular: 0.3,
1857            shininess: 32.0,
1858            light_x: options.light_direction[0],
1859            light_y: options.light_direction[1],
1860            light_z: options.light_direction[2],
1861            opacity_threshold: 0.95,
1862            gradient_threshold: 10.0,
1863            enable_shading,
1864            clip_enabled: if clip.enabled { 1 } else { 0 },
1865            clip_inverted: if clip.inverted { 1 } else { 0 },
1866            clip_normal_x: clip.normal[0],
1867            clip_normal_y: clip.normal[1],
1868            clip_normal_z: clip.normal[2],
1869            clip_distance: clip.distance,
1870            opacity_multiplier: options.opacity_multiplier,
1871            // Segmentation mask (from Auto Clean)
1872            mask_enabled: if self.mask_config.enabled && self.mask_buffer.is_some() { 1 } else { 0 },
1873            visible_labels: self.mask_config.visible_labels,
1874            // Sculpt mask (from Lasso Sculpt) - both masks are checked independently
1875            sculpt_mask_enabled: if self.sculpt_mask_config.enabled && self.sculpt_mask_buffer.is_some() { 1 } else { 0 },
1876            sculpt_visible_labels: self.sculpt_mask_config.visible_labels,
1877            // Slice plane navigation
1878            slice_planes_enabled: if options.slice_planes.enabled { 1 } else { 0 },
1879            axial_slice: options.slice_planes.axial,
1880            sagittal_slice: options.slice_planes.sagittal,
1881            coronal_slice: options.slice_planes.coronal,
1882            slice_plane_thickness: options.slice_planes.thickness,
1883            slice_plane_opacity: options.slice_planes.opacity,
1884            _padding2: 0,
1885            _padding3: 0,
1886        };
1887
1888        self.queue
1889            .write_buffer(&self.dvr_uniform_buffer, 0, bytemuck::bytes_of(&uniforms));
1890
1891        // Get both mask buffers (use dummy if not available)
1892        let seg_mask_buffer = self.mask_buffer.as_ref().unwrap_or(&self.dummy_mask_buffer);
1893        let sculpt_mask_buffer = self.sculpt_mask_buffer.as_ref().unwrap_or(&self.dummy_mask_buffer);
1894
1895        // Create DVR bind group (includes transfer function and BOTH masks)
1896        let bind_group = self.device.create_bind_group(&BindGroupDescriptor {
1897            label: Some("dvr_bind_group"),
1898            layout: &self.dvr_bind_group_layout,
1899            entries: &[
1900                BindGroupEntry {
1901                    binding: 0,
1902                    resource: volume_buffer.as_entire_binding(),
1903                },
1904                BindGroupEntry {
1905                    binding: 1,
1906                    resource: self.dvr_uniform_buffer.as_entire_binding(),
1907                },
1908                BindGroupEntry {
1909                    binding: 2,
1910                    resource: self.output_buffer.as_entire_binding(),
1911                },
1912                BindGroupEntry {
1913                    binding: 3,
1914                    resource: self.transfer_function_buffer.as_entire_binding(),
1915                },
1916                BindGroupEntry {
1917                    binding: 4,
1918                    resource: seg_mask_buffer.as_entire_binding(),
1919                },
1920                BindGroupEntry {
1921                    binding: 5,
1922                    resource: sculpt_mask_buffer.as_entire_binding(),
1923                },
1924            ],
1925        });
1926
1927        // Select pipeline
1928        let pipeline = if mode == RenderMode::DVR {
1929            &self.dvr_pipeline
1930        } else {
1931            &self.dvr_no_shading_pipeline
1932        };
1933
1934        // Execute compute and read back
1935        self.execute_and_readback(pipeline, &bind_group, mode, start)
1936    }
1937
1938    /// Common execute and readback for all render modes
1939    fn execute_and_readback(
1940        &self,
1941        pipeline: &ComputePipeline,
1942        bind_group: &wgpu::BindGroup,
1943        mode: RenderMode,
1944        start: std::time::Instant,
1945    ) -> Result<RenderResult, MprError> {
1946        // Execute compute
1947        let mut encoder = self
1948            .device
1949            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1950                label: Some("volume_render_encoder"),
1951            });
1952
1953        {
1954            let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
1955                label: Some("volume_render_pass"),
1956                timestamp_writes: None,
1957            });
1958
1959            compute_pass.set_pipeline(pipeline);
1960            compute_pass.set_bind_group(0, bind_group, &[]);
1961
1962            let workgroups_x = self.output_width.div_ceil(Self::WORKGROUP_SIZE);
1963            let workgroups_y = self.output_height.div_ceil(Self::WORKGROUP_SIZE);
1964
1965            compute_pass.dispatch_workgroups(workgroups_x, workgroups_y, 1);
1966        }
1967
1968        // Copy to staging for readback
1969        let output_size = (self.output_width * self.output_height * 4) as u64;
1970        encoder.copy_buffer_to_buffer(&self.output_buffer, 0, &self.staging_buffer, 0, output_size);
1971
1972        self.queue.submit(std::iter::once(encoder.finish()));
1973
1974        // Read back results
1975        let buffer_slice = self.staging_buffer.slice(..);
1976        let (tx, rx) = std::sync::mpsc::channel();
1977        buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
1978            tx.send(result).unwrap();
1979        });
1980
1981        self.device.poll(wgpu::Maintain::Wait);
1982        rx.recv()
1983            .map_err(|_| MprError::GpuError("Channel receive failed".to_string()))?
1984            .map_err(|e| MprError::GpuError(format!("Buffer map failed: {:?}", e)))?;
1985
1986        let data = buffer_slice.get_mapped_range();
1987
1988        // Extract RGBA data
1989        let rgba_data: Vec<u8> = data.to_vec();
1990
1991        drop(data);
1992        self.staging_buffer.unmap();
1993
1994        let elapsed_ms = start.elapsed().as_secs_f64() * 1000.0;
1995
1996        debug!(
1997            "{:?} render: {}x{} in {:.1}ms",
1998            mode, self.output_width, self.output_height, elapsed_ms
1999        );
2000
2001        Ok(RenderResult {
2002            width: self.output_width,
2003            height: self.output_height,
2004            rgba_data,
2005            render_time_ms: elapsed_ms,
2006        })
2007    }
2008
2009    /// Resize output buffers
2010    pub fn resize(&mut self, width: u32, height: u32) -> Result<(), MprError> {
2011        if width == self.output_width && height == self.output_height {
2012            return Ok(());
2013        }
2014
2015        let output_size = (width * height * 4) as u64;
2016
2017        self.output_buffer = self.device.create_buffer(&BufferDescriptor {
2018            label: Some("volume_render_output"),
2019            size: output_size,
2020            usage: BufferUsages::STORAGE | BufferUsages::COPY_SRC,
2021            mapped_at_creation: false,
2022        });
2023
2024        self.staging_buffer = self.device.create_buffer(&BufferDescriptor {
2025            label: Some("volume_render_staging"),
2026            size: output_size,
2027            usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
2028            mapped_at_creation: false,
2029        });
2030
2031        self.output_width = width;
2032        self.output_height = height;
2033
2034        info!("Volume renderer resized to {}x{}", width, height);
2035        Ok(())
2036    }
2037}
2038
2039/// Result of volume rendering
2040pub struct RenderResult {
2041    pub width: u32,
2042    pub height: u32,
2043    pub rgba_data: Vec<u8>,
2044    pub render_time_ms: f64,
2045}
2046
2047impl RenderResult {
2048    /// Convert to grayscale (single channel)
2049    pub fn to_grayscale(&self) -> Vec<u8> {
2050        self.rgba_data
2051            .chunks_exact(4)
2052            .map(|chunk| chunk[0]) // R channel (same as G and B for grayscale)
2053            .collect()
2054    }
2055}
2056
2057#[cfg(test)]
2058mod tests {
2059    use super::*;
2060
2061    #[test]
2062    fn test_camera_orbit() {
2063        let center = [100.0, 100.0, 100.0];
2064        let camera = Camera::orbit(center, 200.0, 0.0, 0.0);
2065
2066        // Camera should be behind center on Z axis
2067        assert!(camera.position[2] > center[2]);
2068
2069        // Look direction should point toward center (negative Z)
2070        assert!(camera.look_dir[2] < 0.0);
2071    }
2072
2073    #[test]
2074    fn test_uniform_size() {
2075        // Ensure uniform struct is properly aligned
2076        assert_eq!(std::mem::size_of::<MipUniforms>() % 16, 0);
2077    }
2078}