ringkernel_wavesim3d/visualization/
renderer.rs

1//! Main 3D renderer for wave simulation visualization.
2//!
3//! Combines slice rendering, markers, and UI into a complete visualization system.
4
5use super::camera::{Camera3D, CameraController};
6use super::slice::SliceRenderer;
7use super::volume::VolumeRenderer;
8use super::{CameraUniform, ColorMap, GridLines, HeadWireframe, MarkerSphere, Vertex3D};
9use crate::simulation::physics::Position3D;
10use wgpu::util::DeviceExt;
11
12/// Visualization mode.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
14pub enum VisualizationMode {
15    /// Single slice view
16    SingleSlice,
17    /// Multiple orthogonal slices
18    MultiSlice,
19    /// Volume rendering (ray marching)
20    #[default]
21    VolumeRender,
22    /// Isosurface rendering
23    Isosurface,
24}
25
26/// Rendering configuration.
27#[derive(Debug, Clone)]
28pub struct RenderConfig {
29    /// Visualization mode
30    pub mode: VisualizationMode,
31    /// Color map for pressure values
32    pub color_map: ColorMap,
33    /// Background color
34    pub background_color: [f32; 4],
35    /// Show bounding box
36    pub show_bounding_box: bool,
37    /// Show floor grid
38    pub show_floor_grid: bool,
39    /// Show source markers
40    pub show_sources: bool,
41    /// Show listener head
42    pub show_listener: bool,
43    /// Slice opacity
44    pub slice_opacity: f32,
45    /// Auto-scale pressure colors
46    pub auto_scale: bool,
47    /// Manual max pressure (if not auto-scaling)
48    pub max_pressure: f32,
49}
50
51impl Default for RenderConfig {
52    fn default() -> Self {
53        Self {
54            mode: VisualizationMode::VolumeRender,
55            color_map: ColorMap::BlueWhiteRed,
56            background_color: [0.1, 0.1, 0.15, 1.0],
57            show_bounding_box: true,
58            show_floor_grid: true,
59            show_sources: true,
60            show_listener: true,
61            slice_opacity: 0.85,
62            auto_scale: true,
63            max_pressure: 1.0,
64        }
65    }
66}
67
68/// WGSL shader for basic 3D rendering.
69const SHADER_SOURCE: &str = r#"
70struct CameraUniform {
71    view_proj: mat4x4<f32>,
72    view: mat4x4<f32>,
73    camera_pos: vec4<f32>,
74    grid_size: vec4<f32>,
75}
76
77@group(0) @binding(0)
78var<uniform> camera: CameraUniform;
79
80struct VertexInput {
81    @location(0) position: vec3<f32>,
82    @location(1) color: vec4<f32>,
83    @location(2) normal: vec3<f32>,
84    @location(3) tex_coord: vec2<f32>,
85}
86
87struct VertexOutput {
88    @builtin(position) clip_position: vec4<f32>,
89    @location(0) color: vec4<f32>,
90    @location(1) world_pos: vec3<f32>,
91    @location(2) normal: vec3<f32>,
92}
93
94@vertex
95fn vs_main(in: VertexInput) -> VertexOutput {
96    var out: VertexOutput;
97    out.clip_position = camera.view_proj * vec4<f32>(in.position, 1.0);
98    out.color = in.color;
99    out.world_pos = in.position;
100    out.normal = in.normal;
101    return out;
102}
103
104@fragment
105fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
106    // Simple lighting
107    let light_dir = normalize(vec3<f32>(0.5, 1.0, 0.3));
108    let normal = normalize(in.normal);
109    let diffuse = max(dot(normal, light_dir), 0.3);
110
111    var color = in.color;
112    color = vec4<f32>(color.rgb * diffuse, color.a);
113
114    return color;
115}
116
117@fragment
118fn fs_main_line(in: VertexOutput) -> @location(0) vec4<f32> {
119    // No lighting for lines
120    return in.color;
121}
122"#;
123
124/// Main 3D renderer.
125pub struct Renderer3D {
126    /// WGPU device
127    device: wgpu::Device,
128    /// WGPU queue
129    queue: wgpu::Queue,
130    /// Surface configuration
131    surface_config: wgpu::SurfaceConfiguration,
132    /// Surface
133    surface: wgpu::Surface<'static>,
134    /// Render pipeline for triangles
135    triangle_pipeline: wgpu::RenderPipeline,
136    /// Render pipeline for lines
137    line_pipeline: wgpu::RenderPipeline,
138    /// Camera uniform buffer
139    camera_buffer: wgpu::Buffer,
140    /// Camera bind group
141    camera_bind_group: wgpu::BindGroup,
142    /// Depth texture view (cached)
143    depth_texture_view: wgpu::TextureView,
144    /// Cached slice vertex buffer
145    slice_buffer: Option<wgpu::Buffer>,
146    /// Cached slice vertex count
147    slice_vertex_count: u32,
148    /// Cached line vertex buffer
149    line_buffer: Option<wgpu::Buffer>,
150    /// Cached line vertex count
151    line_vertex_count: u32,
152    /// Cached marker vertex buffer
153    marker_buffer: Option<wgpu::Buffer>,
154    /// Cached marker vertex count
155    marker_vertex_count: u32,
156    /// Volume renderer
157    volume_renderer: Option<VolumeRenderer>,
158    /// Grid dimensions (cells)
159    #[allow(dead_code)]
160    grid_dimensions: (usize, usize, usize),
161    /// Camera bind group layout (for volume renderer)
162    #[allow(dead_code)]
163    camera_bind_group_layout: wgpu::BindGroupLayout,
164    /// Camera
165    pub camera: Camera3D,
166    /// Camera controller
167    pub camera_controller: CameraController,
168    /// Slice renderer
169    pub slice_renderer: SliceRenderer,
170    /// Render configuration
171    pub config: RenderConfig,
172    /// Grid physical size
173    grid_size: (f32, f32, f32),
174    /// Source markers
175    sources: Vec<MarkerSphere>,
176    /// Listener head
177    listener: Option<HeadWireframe>,
178}
179
180impl Renderer3D {
181    /// Create a new renderer.
182    pub async fn new(
183        window: &winit::window::Window,
184        grid_size: (f32, f32, f32),
185        grid_dimensions: (usize, usize, usize),
186    ) -> Result<Self, RendererError> {
187        let size = window.inner_size();
188
189        // Create WGPU instance
190        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
191            backends: wgpu::Backends::all(),
192            ..Default::default()
193        });
194
195        // Create surface - use unsafe to get 'static lifetime
196        let surface = unsafe {
197            instance
198                .create_surface_unsafe(wgpu::SurfaceTargetUnsafe::from_window(window).unwrap())
199                .map_err(|e| RendererError::SurfaceError(e.to_string()))?
200        };
201
202        // Request adapter
203        let adapter = instance
204            .request_adapter(&wgpu::RequestAdapterOptions {
205                power_preference: wgpu::PowerPreference::HighPerformance,
206                compatible_surface: Some(&surface),
207                force_fallback_adapter: false,
208            })
209            .await
210            .ok_or_else(|| RendererError::AdapterError("No adapter found".into()))?;
211
212        // Request device
213        let (device, queue) = adapter
214            .request_device(
215                &wgpu::DeviceDescriptor {
216                    required_features: wgpu::Features::empty(),
217                    required_limits: wgpu::Limits::default(),
218                    label: Some("wavesim3d_device"),
219                },
220                None,
221            )
222            .await
223            .map_err(|e| RendererError::DeviceError(e.to_string()))?;
224
225        // Configure surface
226        let surface_caps = surface.get_capabilities(&adapter);
227        let surface_format = surface_caps
228            .formats
229            .iter()
230            .find(|f| f.is_srgb())
231            .copied()
232            .unwrap_or(surface_caps.formats[0]);
233
234        let surface_config = wgpu::SurfaceConfiguration {
235            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
236            format: surface_format,
237            width: size.width.max(1),
238            height: size.height.max(1),
239            present_mode: wgpu::PresentMode::AutoVsync,
240            alpha_mode: surface_caps.alpha_modes[0],
241            view_formats: vec![],
242            desired_maximum_frame_latency: 2,
243        };
244        surface.configure(&device, &surface_config);
245
246        // Create shader module
247        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
248            label: Some("wavesim3d_shader"),
249            source: wgpu::ShaderSource::Wgsl(SHADER_SOURCE.into()),
250        });
251
252        // Create camera uniform buffer
253        let camera_uniform = CameraUniform::new();
254        let camera_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
255            label: Some("camera_buffer"),
256            contents: bytemuck::cast_slice(&[camera_uniform]),
257            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
258        });
259
260        // Create bind group layout
261        let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
262            label: Some("camera_bind_group_layout"),
263            entries: &[wgpu::BindGroupLayoutEntry {
264                binding: 0,
265                visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
266                ty: wgpu::BindingType::Buffer {
267                    ty: wgpu::BufferBindingType::Uniform,
268                    has_dynamic_offset: false,
269                    min_binding_size: None,
270                },
271                count: None,
272            }],
273        });
274
275        // Create bind group
276        let camera_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
277            label: Some("camera_bind_group"),
278            layout: &bind_group_layout,
279            entries: &[wgpu::BindGroupEntry {
280                binding: 0,
281                resource: camera_buffer.as_entire_binding(),
282            }],
283        });
284
285        // Create pipeline layout
286        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
287            label: Some("render_pipeline_layout"),
288            bind_group_layouts: &[&bind_group_layout],
289            push_constant_ranges: &[],
290        });
291
292        // Create triangle render pipeline
293        let triangle_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
294            label: Some("triangle_pipeline"),
295            layout: Some(&pipeline_layout),
296            vertex: wgpu::VertexState {
297                module: &shader,
298                entry_point: "vs_main",
299                buffers: &[Vertex3D::desc()],
300            },
301            fragment: Some(wgpu::FragmentState {
302                module: &shader,
303                entry_point: "fs_main",
304                targets: &[Some(wgpu::ColorTargetState {
305                    format: surface_config.format,
306                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
307                    write_mask: wgpu::ColorWrites::ALL,
308                })],
309            }),
310            primitive: wgpu::PrimitiveState {
311                topology: wgpu::PrimitiveTopology::TriangleList,
312                strip_index_format: None,
313                front_face: wgpu::FrontFace::Ccw,
314                cull_mode: None, // No culling for transparent slices
315                polygon_mode: wgpu::PolygonMode::Fill,
316                unclipped_depth: false,
317                conservative: false,
318            },
319            depth_stencil: Some(wgpu::DepthStencilState {
320                format: wgpu::TextureFormat::Depth32Float,
321                depth_write_enabled: true,
322                depth_compare: wgpu::CompareFunction::Less,
323                stencil: wgpu::StencilState::default(),
324                bias: wgpu::DepthBiasState::default(),
325            }),
326            multisample: wgpu::MultisampleState {
327                count: 1,
328                mask: !0,
329                alpha_to_coverage_enabled: false,
330            },
331            multiview: None,
332        });
333
334        // Create line render pipeline
335        let line_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
336            label: Some("line_pipeline"),
337            layout: Some(&pipeline_layout),
338            vertex: wgpu::VertexState {
339                module: &shader,
340                entry_point: "vs_main",
341                buffers: &[Vertex3D::desc()],
342            },
343            fragment: Some(wgpu::FragmentState {
344                module: &shader,
345                entry_point: "fs_main_line",
346                targets: &[Some(wgpu::ColorTargetState {
347                    format: surface_config.format,
348                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
349                    write_mask: wgpu::ColorWrites::ALL,
350                })],
351            }),
352            primitive: wgpu::PrimitiveState {
353                topology: wgpu::PrimitiveTopology::LineList,
354                ..Default::default()
355            },
356            depth_stencil: Some(wgpu::DepthStencilState {
357                format: wgpu::TextureFormat::Depth32Float,
358                depth_write_enabled: true,
359                depth_compare: wgpu::CompareFunction::Less,
360                stencil: wgpu::StencilState::default(),
361                bias: wgpu::DepthBiasState::default(),
362            }),
363            multisample: wgpu::MultisampleState::default(),
364            multiview: None,
365        });
366
367        // Set up camera
368        let mut camera = Camera3D::for_grid(grid_size);
369        camera.set_aspect(size.width as f32 / size.height as f32);
370        let camera_controller = CameraController::from_camera(&camera);
371
372        // Create initial depth texture
373        let depth_texture_view = Self::create_depth_texture_static(&device, &surface_config);
374
375        // Create volume renderer
376        let volume_renderer = VolumeRenderer::new(
377            &device,
378            surface_config.format,
379            &bind_group_layout,
380            grid_dimensions,
381        );
382
383        Ok(Self {
384            device,
385            queue,
386            surface_config,
387            surface,
388            triangle_pipeline,
389            line_pipeline,
390            camera_buffer,
391            camera_bind_group,
392            depth_texture_view,
393            slice_buffer: None,
394            slice_vertex_count: 0,
395            line_buffer: None,
396            line_vertex_count: 0,
397            marker_buffer: None,
398            marker_vertex_count: 0,
399            volume_renderer: Some(volume_renderer),
400            grid_dimensions,
401            camera_bind_group_layout: bind_group_layout,
402            camera,
403            camera_controller,
404            slice_renderer: SliceRenderer::new(),
405            config: RenderConfig::default(),
406            grid_size,
407            sources: Vec::new(),
408            listener: None,
409        })
410    }
411
412    /// Resize the render surface.
413    pub fn resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>) {
414        if new_size.width > 0 && new_size.height > 0 {
415            self.surface_config.width = new_size.width;
416            self.surface_config.height = new_size.height;
417            self.surface.configure(&self.device, &self.surface_config);
418            self.camera
419                .set_aspect(new_size.width as f32 / new_size.height as f32);
420            // Recreate depth texture for new size
421            self.depth_texture_view =
422                Self::create_depth_texture_static(&self.device, &self.surface_config);
423        }
424    }
425
426    /// Add a source marker.
427    pub fn add_source(&mut self, position: Position3D, color: [f32; 4]) {
428        self.sources.push(MarkerSphere::new(position, 0.1, color));
429    }
430
431    /// Clear source markers.
432    pub fn clear_sources(&mut self) {
433        self.sources.clear();
434    }
435
436    /// Set the listener head position.
437    pub fn set_listener(&mut self, position: Position3D, scale: f32) {
438        self.listener = Some(HeadWireframe::new(position, scale));
439    }
440
441    /// Clear the listener.
442    pub fn clear_listener(&mut self) {
443        self.listener = None;
444    }
445
446    /// Update the camera uniform buffer.
447    fn update_camera_uniform(&self) {
448        let mut uniform = CameraUniform::new();
449        uniform.update(&self.camera, self.grid_size);
450        self.queue
451            .write_buffer(&self.camera_buffer, 0, bytemuck::cast_slice(&[uniform]));
452    }
453
454    /// Create depth texture (static method for use in constructor).
455    fn create_depth_texture_static(
456        device: &wgpu::Device,
457        surface_config: &wgpu::SurfaceConfiguration,
458    ) -> wgpu::TextureView {
459        let depth_texture = device.create_texture(&wgpu::TextureDescriptor {
460            label: Some("depth_texture"),
461            size: wgpu::Extent3d {
462                width: surface_config.width,
463                height: surface_config.height,
464                depth_or_array_layers: 1,
465            },
466            mip_level_count: 1,
467            sample_count: 1,
468            dimension: wgpu::TextureDimension::D2,
469            format: wgpu::TextureFormat::Depth32Float,
470            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
471            view_formats: &[],
472        });
473
474        depth_texture.create_view(&wgpu::TextureViewDescriptor::default())
475    }
476
477    /// Update a cached vertex buffer, recreating only if size changed significantly.
478    fn update_vertex_buffer_static(
479        device: &wgpu::Device,
480        queue: &wgpu::Queue,
481        vertices: &[Vertex3D],
482        buffer: &mut Option<wgpu::Buffer>,
483        vertex_count: &mut u32,
484        label: &str,
485    ) {
486        let new_count = vertices.len() as u32;
487        let data = bytemuck::cast_slice(vertices);
488        let required_size = data.len() as u64;
489
490        // Check if we need to recreate the buffer
491        let needs_recreate = match buffer {
492            Some(ref existing) => existing.size() < required_size,
493            None => true,
494        };
495
496        if needs_recreate && !vertices.is_empty() {
497            // Allocate with some extra capacity to reduce reallocations
498            let alloc_size = (required_size as f64 * 1.5) as u64;
499            *buffer = Some(device.create_buffer(&wgpu::BufferDescriptor {
500                label: Some(label),
501                size: alloc_size,
502                usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
503                mapped_at_creation: false,
504            }));
505        }
506
507        // Write data to buffer
508        if let Some(ref buf) = buffer {
509            if !vertices.is_empty() {
510                queue.write_buffer(buf, 0, data);
511            }
512        }
513
514        *vertex_count = new_count;
515    }
516
517    /// Render a frame.
518    pub fn render(
519        &mut self,
520        grid: &crate::simulation::SimulationGrid3D,
521    ) -> Result<(), RendererError> {
522        // Get surface texture
523        let output = self
524            .surface
525            .get_current_texture()
526            .map_err(|e| RendererError::SurfaceError(e.to_string()))?;
527
528        let view = output
529            .texture
530            .create_view(&wgpu::TextureViewDescriptor::default());
531
532        // Update camera
533        self.update_camera_uniform();
534
535        // Update max pressure
536        if self.config.auto_scale {
537            self.slice_renderer.set_max_pressure(grid.max_pressure());
538        } else {
539            self.slice_renderer
540                .set_max_pressure(self.config.max_pressure);
541        }
542
543        // Generate vertices
544        let slice_vertices = self.slice_renderer.generate_vertices(grid, self.grid_size);
545
546        let mut line_vertices = Vec::new();
547
548        // Bounding box
549        if self.config.show_bounding_box {
550            let grid_lines = GridLines::new(self.grid_size);
551            line_vertices.extend(grid_lines.generate_box());
552        }
553
554        // Floor grid
555        if self.config.show_floor_grid {
556            let grid_lines = GridLines::new(self.grid_size);
557            line_vertices.extend(grid_lines.generate_floor_grid());
558        }
559
560        // Source markers
561        let mut marker_vertices = Vec::new();
562        if self.config.show_sources {
563            for source in &self.sources {
564                marker_vertices.extend(source.generate_vertices(16));
565            }
566        }
567
568        // Listener head
569        if self.config.show_listener {
570            if let Some(ref head) = self.listener {
571                line_vertices.extend(head.generate_vertices());
572            }
573        }
574
575        // Update cached vertex buffers (recreate only if size changed significantly)
576        Self::update_vertex_buffer_static(
577            &self.device,
578            &self.queue,
579            &slice_vertices,
580            &mut self.slice_buffer,
581            &mut self.slice_vertex_count,
582            "slice_vertex_buffer",
583        );
584        Self::update_vertex_buffer_static(
585            &self.device,
586            &self.queue,
587            &line_vertices,
588            &mut self.line_buffer,
589            &mut self.line_vertex_count,
590            "line_vertex_buffer",
591        );
592        Self::update_vertex_buffer_static(
593            &self.device,
594            &self.queue,
595            &marker_vertices,
596            &mut self.marker_buffer,
597            &mut self.marker_vertex_count,
598            "marker_vertex_buffer",
599        );
600
601        // Create command encoder
602        let mut encoder = self
603            .device
604            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
605                label: Some("render_encoder"),
606            });
607
608        // Update volume texture if in volume mode
609        if self.config.mode == VisualizationMode::VolumeRender {
610            if let Some(ref mut volume_renderer) = self.volume_renderer {
611                volume_renderer.update_volume(&self.queue, grid);
612                volume_renderer.update_params(&self.queue);
613            }
614        }
615
616        {
617            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
618                label: Some("render_pass"),
619                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
620                    view: &view,
621                    resolve_target: None,
622                    ops: wgpu::Operations {
623                        load: wgpu::LoadOp::Clear(wgpu::Color {
624                            r: self.config.background_color[0] as f64,
625                            g: self.config.background_color[1] as f64,
626                            b: self.config.background_color[2] as f64,
627                            a: self.config.background_color[3] as f64,
628                        }),
629                        store: wgpu::StoreOp::Store,
630                    },
631                })],
632                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
633                    view: &self.depth_texture_view,
634                    depth_ops: Some(wgpu::Operations {
635                        load: wgpu::LoadOp::Clear(1.0),
636                        store: wgpu::StoreOp::Store,
637                    }),
638                    stencil_ops: None,
639                }),
640                timestamp_writes: None,
641                occlusion_query_set: None,
642            });
643
644            // Draw based on visualization mode
645            match self.config.mode {
646                VisualizationMode::VolumeRender => {
647                    // Draw volume
648                    if let Some(ref volume_renderer) = self.volume_renderer {
649                        volume_renderer.render(&mut render_pass, &self.camera_bind_group);
650                    }
651                }
652                _ => {
653                    // Draw slices (slice modes)
654                    if self.slice_vertex_count > 0 {
655                        if let Some(ref buffer) = self.slice_buffer {
656                            render_pass.set_pipeline(&self.triangle_pipeline);
657                            render_pass.set_bind_group(0, &self.camera_bind_group, &[]);
658                            render_pass.set_vertex_buffer(0, buffer.slice(..));
659                            render_pass.draw(0..self.slice_vertex_count, 0..1);
660                        }
661                    }
662                }
663            }
664
665            // Draw markers
666            if self.marker_vertex_count > 0 {
667                if let Some(ref buffer) = self.marker_buffer {
668                    render_pass.set_pipeline(&self.triangle_pipeline);
669                    render_pass.set_bind_group(0, &self.camera_bind_group, &[]);
670                    render_pass.set_vertex_buffer(0, buffer.slice(..));
671                    render_pass.draw(0..self.marker_vertex_count, 0..1);
672                }
673            }
674
675            // Draw lines
676            if self.line_vertex_count > 0 {
677                if let Some(ref buffer) = self.line_buffer {
678                    render_pass.set_pipeline(&self.line_pipeline);
679                    render_pass.set_bind_group(0, &self.camera_bind_group, &[]);
680                    render_pass.set_vertex_buffer(0, buffer.slice(..));
681                    render_pass.draw(0..self.line_vertex_count, 0..1);
682                }
683            }
684        }
685
686        self.queue.submit(std::iter::once(encoder.finish()));
687        output.present();
688
689        Ok(())
690    }
691
692    /// Get the device for external rendering.
693    pub fn device(&self) -> &wgpu::Device {
694        &self.device
695    }
696
697    /// Get the queue for external rendering.
698    pub fn queue(&self) -> &wgpu::Queue {
699        &self.queue
700    }
701
702    /// Get the surface format.
703    pub fn surface_format(&self) -> wgpu::TextureFormat {
704        self.surface_config.format
705    }
706}
707
708/// Renderer error types.
709#[derive(Debug)]
710pub enum RendererError {
711    SurfaceError(String),
712    AdapterError(String),
713    DeviceError(String),
714    ShaderError(String),
715}
716
717impl std::fmt::Display for RendererError {
718    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
719        match self {
720            RendererError::SurfaceError(msg) => write!(f, "Surface error: {}", msg),
721            RendererError::AdapterError(msg) => write!(f, "Adapter error: {}", msg),
722            RendererError::DeviceError(msg) => write!(f, "Device error: {}", msg),
723            RendererError::ShaderError(msg) => write!(f, "Shader error: {}", msg),
724        }
725    }
726}
727
728impl std::error::Error for RendererError {}