Skip to main content

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            .map_err(|e| RendererError::AdapterError(e.to_string()))?;
211
212        // Request device
213        let (device, queue) = adapter
214            .request_device(&wgpu::DeviceDescriptor {
215                required_features: wgpu::Features::empty(),
216                required_limits: wgpu::Limits::default(),
217                label: Some("wavesim3d_device"),
218                ..Default::default()
219            })
220            .await
221            .map_err(|e| RendererError::DeviceError(e.to_string()))?;
222
223        // Configure surface
224        let surface_caps = surface.get_capabilities(&adapter);
225        let surface_format = surface_caps
226            .formats
227            .iter()
228            .find(|f| f.is_srgb())
229            .copied()
230            .unwrap_or(surface_caps.formats[0]);
231
232        let surface_config = wgpu::SurfaceConfiguration {
233            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
234            format: surface_format,
235            width: size.width.max(1),
236            height: size.height.max(1),
237            present_mode: wgpu::PresentMode::AutoVsync,
238            alpha_mode: surface_caps.alpha_modes[0],
239            view_formats: vec![],
240            desired_maximum_frame_latency: 2,
241        };
242        surface.configure(&device, &surface_config);
243
244        // Create shader module
245        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
246            label: Some("wavesim3d_shader"),
247            source: wgpu::ShaderSource::Wgsl(SHADER_SOURCE.into()),
248        });
249
250        // Create camera uniform buffer
251        let camera_uniform = CameraUniform::new();
252        let camera_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
253            label: Some("camera_buffer"),
254            contents: bytemuck::cast_slice(&[camera_uniform]),
255            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
256        });
257
258        // Create bind group layout
259        let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
260            label: Some("camera_bind_group_layout"),
261            entries: &[wgpu::BindGroupLayoutEntry {
262                binding: 0,
263                visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
264                ty: wgpu::BindingType::Buffer {
265                    ty: wgpu::BufferBindingType::Uniform,
266                    has_dynamic_offset: false,
267                    min_binding_size: None,
268                },
269                count: None,
270            }],
271        });
272
273        // Create bind group
274        let camera_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
275            label: Some("camera_bind_group"),
276            layout: &bind_group_layout,
277            entries: &[wgpu::BindGroupEntry {
278                binding: 0,
279                resource: camera_buffer.as_entire_binding(),
280            }],
281        });
282
283        // Create pipeline layout
284        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
285            label: Some("render_pipeline_layout"),
286            bind_group_layouts: &[&bind_group_layout],
287            push_constant_ranges: &[],
288        });
289
290        // Create triangle render pipeline
291        let triangle_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
292            label: Some("triangle_pipeline"),
293            layout: Some(&pipeline_layout),
294            vertex: wgpu::VertexState {
295                module: &shader,
296                entry_point: Some("vs_main"),
297                compilation_options: Default::default(),
298                buffers: &[Vertex3D::desc()],
299            },
300            fragment: Some(wgpu::FragmentState {
301                module: &shader,
302                entry_point: Some("fs_main"),
303                compilation_options: Default::default(),
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            cache: None,
333        });
334
335        // Create line render pipeline
336        let line_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
337            label: Some("line_pipeline"),
338            layout: Some(&pipeline_layout),
339            vertex: wgpu::VertexState {
340                module: &shader,
341                entry_point: Some("vs_main"),
342                compilation_options: Default::default(),
343                buffers: &[Vertex3D::desc()],
344            },
345            fragment: Some(wgpu::FragmentState {
346                module: &shader,
347                entry_point: Some("fs_main_line"),
348                compilation_options: Default::default(),
349                targets: &[Some(wgpu::ColorTargetState {
350                    format: surface_config.format,
351                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
352                    write_mask: wgpu::ColorWrites::ALL,
353                })],
354            }),
355            primitive: wgpu::PrimitiveState {
356                topology: wgpu::PrimitiveTopology::LineList,
357                ..Default::default()
358            },
359            depth_stencil: Some(wgpu::DepthStencilState {
360                format: wgpu::TextureFormat::Depth32Float,
361                depth_write_enabled: true,
362                depth_compare: wgpu::CompareFunction::Less,
363                stencil: wgpu::StencilState::default(),
364                bias: wgpu::DepthBiasState::default(),
365            }),
366            multisample: wgpu::MultisampleState::default(),
367            multiview: None,
368            cache: None,
369        });
370
371        // Set up camera
372        let mut camera = Camera3D::for_grid(grid_size);
373        camera.set_aspect(size.width as f32 / size.height as f32);
374        let camera_controller = CameraController::from_camera(&camera);
375
376        // Create initial depth texture
377        let depth_texture_view = Self::create_depth_texture_static(&device, &surface_config);
378
379        // Create volume renderer
380        let volume_renderer = VolumeRenderer::new(
381            &device,
382            surface_config.format,
383            &bind_group_layout,
384            grid_dimensions,
385        );
386
387        Ok(Self {
388            device,
389            queue,
390            surface_config,
391            surface,
392            triangle_pipeline,
393            line_pipeline,
394            camera_buffer,
395            camera_bind_group,
396            depth_texture_view,
397            slice_buffer: None,
398            slice_vertex_count: 0,
399            line_buffer: None,
400            line_vertex_count: 0,
401            marker_buffer: None,
402            marker_vertex_count: 0,
403            volume_renderer: Some(volume_renderer),
404            grid_dimensions,
405            camera_bind_group_layout: bind_group_layout,
406            camera,
407            camera_controller,
408            slice_renderer: SliceRenderer::new(),
409            config: RenderConfig::default(),
410            grid_size,
411            sources: Vec::new(),
412            listener: None,
413        })
414    }
415
416    /// Resize the render surface.
417    pub fn resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>) {
418        if new_size.width > 0 && new_size.height > 0 {
419            self.surface_config.width = new_size.width;
420            self.surface_config.height = new_size.height;
421            self.surface.configure(&self.device, &self.surface_config);
422            self.camera
423                .set_aspect(new_size.width as f32 / new_size.height as f32);
424            // Recreate depth texture for new size
425            self.depth_texture_view =
426                Self::create_depth_texture_static(&self.device, &self.surface_config);
427        }
428    }
429
430    /// Add a source marker.
431    pub fn add_source(&mut self, position: Position3D, color: [f32; 4]) {
432        self.sources.push(MarkerSphere::new(position, 0.1, color));
433    }
434
435    /// Clear source markers.
436    pub fn clear_sources(&mut self) {
437        self.sources.clear();
438    }
439
440    /// Set the listener head position.
441    pub fn set_listener(&mut self, position: Position3D, scale: f32) {
442        self.listener = Some(HeadWireframe::new(position, scale));
443    }
444
445    /// Clear the listener.
446    pub fn clear_listener(&mut self) {
447        self.listener = None;
448    }
449
450    /// Update the camera uniform buffer.
451    fn update_camera_uniform(&self) {
452        let mut uniform = CameraUniform::new();
453        uniform.update(&self.camera, self.grid_size);
454        self.queue
455            .write_buffer(&self.camera_buffer, 0, bytemuck::cast_slice(&[uniform]));
456    }
457
458    /// Create depth texture (static method for use in constructor).
459    fn create_depth_texture_static(
460        device: &wgpu::Device,
461        surface_config: &wgpu::SurfaceConfiguration,
462    ) -> wgpu::TextureView {
463        let depth_texture = device.create_texture(&wgpu::TextureDescriptor {
464            label: Some("depth_texture"),
465            size: wgpu::Extent3d {
466                width: surface_config.width,
467                height: surface_config.height,
468                depth_or_array_layers: 1,
469            },
470            mip_level_count: 1,
471            sample_count: 1,
472            dimension: wgpu::TextureDimension::D2,
473            format: wgpu::TextureFormat::Depth32Float,
474            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
475            view_formats: &[],
476        });
477
478        depth_texture.create_view(&wgpu::TextureViewDescriptor::default())
479    }
480
481    /// Update a cached vertex buffer, recreating only if size changed significantly.
482    fn update_vertex_buffer_static(
483        device: &wgpu::Device,
484        queue: &wgpu::Queue,
485        vertices: &[Vertex3D],
486        buffer: &mut Option<wgpu::Buffer>,
487        vertex_count: &mut u32,
488        label: &str,
489    ) {
490        let new_count = vertices.len() as u32;
491        let data = bytemuck::cast_slice(vertices);
492        let required_size = data.len() as u64;
493
494        // Check if we need to recreate the buffer
495        let needs_recreate = match buffer {
496            Some(ref existing) => existing.size() < required_size,
497            None => true,
498        };
499
500        if needs_recreate && !vertices.is_empty() {
501            // Allocate with some extra capacity to reduce reallocations
502            let alloc_size = (required_size as f64 * 1.5) as u64;
503            *buffer = Some(device.create_buffer(&wgpu::BufferDescriptor {
504                label: Some(label),
505                size: alloc_size,
506                usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
507                mapped_at_creation: false,
508            }));
509        }
510
511        // Write data to buffer
512        if let Some(ref buf) = buffer {
513            if !vertices.is_empty() {
514                queue.write_buffer(buf, 0, data);
515            }
516        }
517
518        *vertex_count = new_count;
519    }
520
521    /// Render a frame.
522    pub fn render(
523        &mut self,
524        grid: &crate::simulation::SimulationGrid3D,
525    ) -> Result<(), RendererError> {
526        // Get surface texture
527        let output = self
528            .surface
529            .get_current_texture()
530            .map_err(|e| RendererError::SurfaceError(e.to_string()))?;
531
532        let view = output
533            .texture
534            .create_view(&wgpu::TextureViewDescriptor::default());
535
536        // Update camera
537        self.update_camera_uniform();
538
539        // Update max pressure
540        if self.config.auto_scale {
541            self.slice_renderer.set_max_pressure(grid.max_pressure());
542        } else {
543            self.slice_renderer
544                .set_max_pressure(self.config.max_pressure);
545        }
546
547        // Generate vertices
548        let slice_vertices = self.slice_renderer.generate_vertices(grid, self.grid_size);
549
550        let mut line_vertices = Vec::new();
551
552        // Bounding box
553        if self.config.show_bounding_box {
554            let grid_lines = GridLines::new(self.grid_size);
555            line_vertices.extend(grid_lines.generate_box());
556        }
557
558        // Floor grid
559        if self.config.show_floor_grid {
560            let grid_lines = GridLines::new(self.grid_size);
561            line_vertices.extend(grid_lines.generate_floor_grid());
562        }
563
564        // Source markers
565        let mut marker_vertices = Vec::new();
566        if self.config.show_sources {
567            for source in &self.sources {
568                marker_vertices.extend(source.generate_vertices(16));
569            }
570        }
571
572        // Listener head
573        if self.config.show_listener {
574            if let Some(ref head) = self.listener {
575                line_vertices.extend(head.generate_vertices());
576            }
577        }
578
579        // Update cached vertex buffers (recreate only if size changed significantly)
580        Self::update_vertex_buffer_static(
581            &self.device,
582            &self.queue,
583            &slice_vertices,
584            &mut self.slice_buffer,
585            &mut self.slice_vertex_count,
586            "slice_vertex_buffer",
587        );
588        Self::update_vertex_buffer_static(
589            &self.device,
590            &self.queue,
591            &line_vertices,
592            &mut self.line_buffer,
593            &mut self.line_vertex_count,
594            "line_vertex_buffer",
595        );
596        Self::update_vertex_buffer_static(
597            &self.device,
598            &self.queue,
599            &marker_vertices,
600            &mut self.marker_buffer,
601            &mut self.marker_vertex_count,
602            "marker_vertex_buffer",
603        );
604
605        // Create command encoder
606        let mut encoder = self
607            .device
608            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
609                label: Some("render_encoder"),
610            });
611
612        // Update volume texture if in volume mode
613        if self.config.mode == VisualizationMode::VolumeRender {
614            if let Some(ref mut volume_renderer) = self.volume_renderer {
615                volume_renderer.update_volume(&self.queue, grid);
616                volume_renderer.update_params(&self.queue);
617            }
618        }
619
620        {
621            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
622                label: Some("render_pass"),
623                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
624                    view: &view,
625                    resolve_target: None,
626                    ops: wgpu::Operations {
627                        load: wgpu::LoadOp::Clear(wgpu::Color {
628                            r: self.config.background_color[0] as f64,
629                            g: self.config.background_color[1] as f64,
630                            b: self.config.background_color[2] as f64,
631                            a: self.config.background_color[3] as f64,
632                        }),
633                        store: wgpu::StoreOp::Store,
634                    },
635                    depth_slice: None,
636                })],
637                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
638                    view: &self.depth_texture_view,
639                    depth_ops: Some(wgpu::Operations {
640                        load: wgpu::LoadOp::Clear(1.0),
641                        store: wgpu::StoreOp::Store,
642                    }),
643                    stencil_ops: None,
644                }),
645                timestamp_writes: None,
646                occlusion_query_set: None,
647            });
648
649            // Draw based on visualization mode
650            match self.config.mode {
651                VisualizationMode::VolumeRender => {
652                    // Draw volume
653                    if let Some(ref volume_renderer) = self.volume_renderer {
654                        volume_renderer.render(&mut render_pass, &self.camera_bind_group);
655                    }
656                }
657                _ => {
658                    // Draw slices (slice modes)
659                    if self.slice_vertex_count > 0 {
660                        if let Some(ref buffer) = self.slice_buffer {
661                            render_pass.set_pipeline(&self.triangle_pipeline);
662                            render_pass.set_bind_group(0, &self.camera_bind_group, &[]);
663                            render_pass.set_vertex_buffer(0, buffer.slice(..));
664                            render_pass.draw(0..self.slice_vertex_count, 0..1);
665                        }
666                    }
667                }
668            }
669
670            // Draw markers
671            if self.marker_vertex_count > 0 {
672                if let Some(ref buffer) = self.marker_buffer {
673                    render_pass.set_pipeline(&self.triangle_pipeline);
674                    render_pass.set_bind_group(0, &self.camera_bind_group, &[]);
675                    render_pass.set_vertex_buffer(0, buffer.slice(..));
676                    render_pass.draw(0..self.marker_vertex_count, 0..1);
677                }
678            }
679
680            // Draw lines
681            if self.line_vertex_count > 0 {
682                if let Some(ref buffer) = self.line_buffer {
683                    render_pass.set_pipeline(&self.line_pipeline);
684                    render_pass.set_bind_group(0, &self.camera_bind_group, &[]);
685                    render_pass.set_vertex_buffer(0, buffer.slice(..));
686                    render_pass.draw(0..self.line_vertex_count, 0..1);
687                }
688            }
689        }
690
691        self.queue.submit(std::iter::once(encoder.finish()));
692        output.present();
693
694        Ok(())
695    }
696
697    /// Get the device for external rendering.
698    pub fn device(&self) -> &wgpu::Device {
699        &self.device
700    }
701
702    /// Get the queue for external rendering.
703    pub fn queue(&self) -> &wgpu::Queue {
704        &self.queue
705    }
706
707    /// Get the surface format.
708    pub fn surface_format(&self) -> wgpu::TextureFormat {
709        self.surface_config.format
710    }
711}
712
713/// Renderer error types.
714#[derive(Debug)]
715pub enum RendererError {
716    SurfaceError(String),
717    AdapterError(String),
718    DeviceError(String),
719    ShaderError(String),
720}
721
722impl std::fmt::Display for RendererError {
723    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
724        match self {
725            RendererError::SurfaceError(msg) => write!(f, "Surface error: {}", msg),
726            RendererError::AdapterError(msg) => write!(f, "Adapter error: {}", msg),
727            RendererError::DeviceError(msg) => write!(f, "Device error: {}", msg),
728            RendererError::ShaderError(msg) => write!(f, "Shader error: {}", msg),
729        }
730    }
731}
732
733impl std::error::Error for RendererError {}