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        // SAFETY: The window handle outlives the surface. The window is valid
197        // for the duration of the renderer's lifetime.
198        let surface = unsafe {
199            instance
200                .create_surface_unsafe(wgpu::SurfaceTargetUnsafe::from_window(window).unwrap())
201                .map_err(|e| RendererError::SurfaceError(e.to_string()))?
202        };
203
204        // Request adapter
205        let adapter = instance
206            .request_adapter(&wgpu::RequestAdapterOptions {
207                power_preference: wgpu::PowerPreference::HighPerformance,
208                compatible_surface: Some(&surface),
209                force_fallback_adapter: false,
210            })
211            .await
212            .map_err(|e| RendererError::AdapterError(e.to_string()))?;
213
214        // Request device
215        let (device, queue) = adapter
216            .request_device(&wgpu::DeviceDescriptor {
217                required_features: wgpu::Features::empty(),
218                required_limits: wgpu::Limits::default(),
219                label: Some("wavesim3d_device"),
220                ..Default::default()
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: Some("vs_main"),
299                compilation_options: Default::default(),
300                buffers: &[Vertex3D::desc()],
301            },
302            fragment: Some(wgpu::FragmentState {
303                module: &shader,
304                entry_point: Some("fs_main"),
305                compilation_options: Default::default(),
306                targets: &[Some(wgpu::ColorTargetState {
307                    format: surface_config.format,
308                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
309                    write_mask: wgpu::ColorWrites::ALL,
310                })],
311            }),
312            primitive: wgpu::PrimitiveState {
313                topology: wgpu::PrimitiveTopology::TriangleList,
314                strip_index_format: None,
315                front_face: wgpu::FrontFace::Ccw,
316                cull_mode: None, // No culling for transparent slices
317                polygon_mode: wgpu::PolygonMode::Fill,
318                unclipped_depth: false,
319                conservative: false,
320            },
321            depth_stencil: Some(wgpu::DepthStencilState {
322                format: wgpu::TextureFormat::Depth32Float,
323                depth_write_enabled: true,
324                depth_compare: wgpu::CompareFunction::Less,
325                stencil: wgpu::StencilState::default(),
326                bias: wgpu::DepthBiasState::default(),
327            }),
328            multisample: wgpu::MultisampleState {
329                count: 1,
330                mask: !0,
331                alpha_to_coverage_enabled: false,
332            },
333            multiview: None,
334            cache: None,
335        });
336
337        // Create line render pipeline
338        let line_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
339            label: Some("line_pipeline"),
340            layout: Some(&pipeline_layout),
341            vertex: wgpu::VertexState {
342                module: &shader,
343                entry_point: Some("vs_main"),
344                compilation_options: Default::default(),
345                buffers: &[Vertex3D::desc()],
346            },
347            fragment: Some(wgpu::FragmentState {
348                module: &shader,
349                entry_point: Some("fs_main_line"),
350                compilation_options: Default::default(),
351                targets: &[Some(wgpu::ColorTargetState {
352                    format: surface_config.format,
353                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
354                    write_mask: wgpu::ColorWrites::ALL,
355                })],
356            }),
357            primitive: wgpu::PrimitiveState {
358                topology: wgpu::PrimitiveTopology::LineList,
359                ..Default::default()
360            },
361            depth_stencil: Some(wgpu::DepthStencilState {
362                format: wgpu::TextureFormat::Depth32Float,
363                depth_write_enabled: true,
364                depth_compare: wgpu::CompareFunction::Less,
365                stencil: wgpu::StencilState::default(),
366                bias: wgpu::DepthBiasState::default(),
367            }),
368            multisample: wgpu::MultisampleState::default(),
369            multiview: None,
370            cache: None,
371        });
372
373        // Set up camera
374        let mut camera = Camera3D::for_grid(grid_size);
375        camera.set_aspect(size.width as f32 / size.height as f32);
376        let camera_controller = CameraController::from_camera(&camera);
377
378        // Create initial depth texture
379        let depth_texture_view = Self::create_depth_texture_static(&device, &surface_config);
380
381        // Create volume renderer
382        let volume_renderer = VolumeRenderer::new(
383            &device,
384            surface_config.format,
385            &bind_group_layout,
386            grid_dimensions,
387        );
388
389        Ok(Self {
390            device,
391            queue,
392            surface_config,
393            surface,
394            triangle_pipeline,
395            line_pipeline,
396            camera_buffer,
397            camera_bind_group,
398            depth_texture_view,
399            slice_buffer: None,
400            slice_vertex_count: 0,
401            line_buffer: None,
402            line_vertex_count: 0,
403            marker_buffer: None,
404            marker_vertex_count: 0,
405            volume_renderer: Some(volume_renderer),
406            grid_dimensions,
407            camera_bind_group_layout: bind_group_layout,
408            camera,
409            camera_controller,
410            slice_renderer: SliceRenderer::new(),
411            config: RenderConfig::default(),
412            grid_size,
413            sources: Vec::new(),
414            listener: None,
415        })
416    }
417
418    /// Resize the render surface.
419    pub fn resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>) {
420        if new_size.width > 0 && new_size.height > 0 {
421            self.surface_config.width = new_size.width;
422            self.surface_config.height = new_size.height;
423            self.surface.configure(&self.device, &self.surface_config);
424            self.camera
425                .set_aspect(new_size.width as f32 / new_size.height as f32);
426            // Recreate depth texture for new size
427            self.depth_texture_view =
428                Self::create_depth_texture_static(&self.device, &self.surface_config);
429        }
430    }
431
432    /// Add a source marker.
433    pub fn add_source(&mut self, position: Position3D, color: [f32; 4]) {
434        self.sources.push(MarkerSphere::new(position, 0.1, color));
435    }
436
437    /// Clear source markers.
438    pub fn clear_sources(&mut self) {
439        self.sources.clear();
440    }
441
442    /// Set the listener head position.
443    pub fn set_listener(&mut self, position: Position3D, scale: f32) {
444        self.listener = Some(HeadWireframe::new(position, scale));
445    }
446
447    /// Clear the listener.
448    pub fn clear_listener(&mut self) {
449        self.listener = None;
450    }
451
452    /// Update the camera uniform buffer.
453    fn update_camera_uniform(&self) {
454        let mut uniform = CameraUniform::new();
455        uniform.update(&self.camera, self.grid_size);
456        self.queue
457            .write_buffer(&self.camera_buffer, 0, bytemuck::cast_slice(&[uniform]));
458    }
459
460    /// Create depth texture (static method for use in constructor).
461    fn create_depth_texture_static(
462        device: &wgpu::Device,
463        surface_config: &wgpu::SurfaceConfiguration,
464    ) -> wgpu::TextureView {
465        let depth_texture = device.create_texture(&wgpu::TextureDescriptor {
466            label: Some("depth_texture"),
467            size: wgpu::Extent3d {
468                width: surface_config.width,
469                height: surface_config.height,
470                depth_or_array_layers: 1,
471            },
472            mip_level_count: 1,
473            sample_count: 1,
474            dimension: wgpu::TextureDimension::D2,
475            format: wgpu::TextureFormat::Depth32Float,
476            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
477            view_formats: &[],
478        });
479
480        depth_texture.create_view(&wgpu::TextureViewDescriptor::default())
481    }
482
483    /// Update a cached vertex buffer, recreating only if size changed significantly.
484    fn update_vertex_buffer_static(
485        device: &wgpu::Device,
486        queue: &wgpu::Queue,
487        vertices: &[Vertex3D],
488        buffer: &mut Option<wgpu::Buffer>,
489        vertex_count: &mut u32,
490        label: &str,
491    ) {
492        let new_count = vertices.len() as u32;
493        let data = bytemuck::cast_slice(vertices);
494        let required_size = data.len() as u64;
495
496        // Check if we need to recreate the buffer
497        let needs_recreate = match buffer {
498            Some(ref existing) => existing.size() < required_size,
499            None => true,
500        };
501
502        if needs_recreate && !vertices.is_empty() {
503            // Allocate with some extra capacity to reduce reallocations
504            let alloc_size = (required_size as f64 * 1.5) as u64;
505            *buffer = Some(device.create_buffer(&wgpu::BufferDescriptor {
506                label: Some(label),
507                size: alloc_size,
508                usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
509                mapped_at_creation: false,
510            }));
511        }
512
513        // Write data to buffer
514        if let Some(ref buf) = buffer {
515            if !vertices.is_empty() {
516                queue.write_buffer(buf, 0, data);
517            }
518        }
519
520        *vertex_count = new_count;
521    }
522
523    /// Render a frame.
524    pub fn render(
525        &mut self,
526        grid: &crate::simulation::SimulationGrid3D,
527    ) -> Result<(), RendererError> {
528        // Get surface texture
529        let output = self
530            .surface
531            .get_current_texture()
532            .map_err(|e| RendererError::SurfaceError(e.to_string()))?;
533
534        let view = output
535            .texture
536            .create_view(&wgpu::TextureViewDescriptor::default());
537
538        // Update camera
539        self.update_camera_uniform();
540
541        // Update max pressure
542        if self.config.auto_scale {
543            self.slice_renderer.set_max_pressure(grid.max_pressure());
544        } else {
545            self.slice_renderer
546                .set_max_pressure(self.config.max_pressure);
547        }
548
549        // Generate vertices
550        let slice_vertices = self.slice_renderer.generate_vertices(grid, self.grid_size);
551
552        let mut line_vertices = Vec::new();
553
554        // Bounding box
555        if self.config.show_bounding_box {
556            let grid_lines = GridLines::new(self.grid_size);
557            line_vertices.extend(grid_lines.generate_box());
558        }
559
560        // Floor grid
561        if self.config.show_floor_grid {
562            let grid_lines = GridLines::new(self.grid_size);
563            line_vertices.extend(grid_lines.generate_floor_grid());
564        }
565
566        // Source markers
567        let mut marker_vertices = Vec::new();
568        if self.config.show_sources {
569            for source in &self.sources {
570                marker_vertices.extend(source.generate_vertices(16));
571            }
572        }
573
574        // Listener head
575        if self.config.show_listener {
576            if let Some(ref head) = self.listener {
577                line_vertices.extend(head.generate_vertices());
578            }
579        }
580
581        // Update cached vertex buffers (recreate only if size changed significantly)
582        Self::update_vertex_buffer_static(
583            &self.device,
584            &self.queue,
585            &slice_vertices,
586            &mut self.slice_buffer,
587            &mut self.slice_vertex_count,
588            "slice_vertex_buffer",
589        );
590        Self::update_vertex_buffer_static(
591            &self.device,
592            &self.queue,
593            &line_vertices,
594            &mut self.line_buffer,
595            &mut self.line_vertex_count,
596            "line_vertex_buffer",
597        );
598        Self::update_vertex_buffer_static(
599            &self.device,
600            &self.queue,
601            &marker_vertices,
602            &mut self.marker_buffer,
603            &mut self.marker_vertex_count,
604            "marker_vertex_buffer",
605        );
606
607        // Create command encoder
608        let mut encoder = self
609            .device
610            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
611                label: Some("render_encoder"),
612            });
613
614        // Update volume texture if in volume mode
615        if self.config.mode == VisualizationMode::VolumeRender {
616            if let Some(ref mut volume_renderer) = self.volume_renderer {
617                volume_renderer.update_volume(&self.queue, grid);
618                volume_renderer.update_params(&self.queue);
619            }
620        }
621
622        {
623            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
624                label: Some("render_pass"),
625                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
626                    view: &view,
627                    resolve_target: None,
628                    ops: wgpu::Operations {
629                        load: wgpu::LoadOp::Clear(wgpu::Color {
630                            r: self.config.background_color[0] as f64,
631                            g: self.config.background_color[1] as f64,
632                            b: self.config.background_color[2] as f64,
633                            a: self.config.background_color[3] as f64,
634                        }),
635                        store: wgpu::StoreOp::Store,
636                    },
637                    depth_slice: None,
638                })],
639                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
640                    view: &self.depth_texture_view,
641                    depth_ops: Some(wgpu::Operations {
642                        load: wgpu::LoadOp::Clear(1.0),
643                        store: wgpu::StoreOp::Store,
644                    }),
645                    stencil_ops: None,
646                }),
647                timestamp_writes: None,
648                occlusion_query_set: None,
649            });
650
651            // Draw based on visualization mode
652            match self.config.mode {
653                VisualizationMode::VolumeRender => {
654                    // Draw volume
655                    if let Some(ref volume_renderer) = self.volume_renderer {
656                        volume_renderer.render(&mut render_pass, &self.camera_bind_group);
657                    }
658                }
659                _ => {
660                    // Draw slices (slice modes)
661                    if self.slice_vertex_count > 0 {
662                        if let Some(ref buffer) = self.slice_buffer {
663                            render_pass.set_pipeline(&self.triangle_pipeline);
664                            render_pass.set_bind_group(0, &self.camera_bind_group, &[]);
665                            render_pass.set_vertex_buffer(0, buffer.slice(..));
666                            render_pass.draw(0..self.slice_vertex_count, 0..1);
667                        }
668                    }
669                }
670            }
671
672            // Draw markers
673            if self.marker_vertex_count > 0 {
674                if let Some(ref buffer) = self.marker_buffer {
675                    render_pass.set_pipeline(&self.triangle_pipeline);
676                    render_pass.set_bind_group(0, &self.camera_bind_group, &[]);
677                    render_pass.set_vertex_buffer(0, buffer.slice(..));
678                    render_pass.draw(0..self.marker_vertex_count, 0..1);
679                }
680            }
681
682            // Draw lines
683            if self.line_vertex_count > 0 {
684                if let Some(ref buffer) = self.line_buffer {
685                    render_pass.set_pipeline(&self.line_pipeline);
686                    render_pass.set_bind_group(0, &self.camera_bind_group, &[]);
687                    render_pass.set_vertex_buffer(0, buffer.slice(..));
688                    render_pass.draw(0..self.line_vertex_count, 0..1);
689                }
690            }
691        }
692
693        self.queue.submit(std::iter::once(encoder.finish()));
694        output.present();
695
696        Ok(())
697    }
698
699    /// Get the device for external rendering.
700    pub fn device(&self) -> &wgpu::Device {
701        &self.device
702    }
703
704    /// Get the queue for external rendering.
705    pub fn queue(&self) -> &wgpu::Queue {
706        &self.queue
707    }
708
709    /// Get the surface format.
710    pub fn surface_format(&self) -> wgpu::TextureFormat {
711        self.surface_config.format
712    }
713}
714
715/// Renderer error types.
716#[derive(Debug)]
717pub enum RendererError {
718    SurfaceError(String),
719    AdapterError(String),
720    DeviceError(String),
721    ShaderError(String),
722}
723
724impl std::fmt::Display for RendererError {
725    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
726        match self {
727            RendererError::SurfaceError(msg) => write!(f, "Surface error: {}", msg),
728            RendererError::AdapterError(msg) => write!(f, "Adapter error: {}", msg),
729            RendererError::DeviceError(msg) => write!(f, "Device error: {}", msg),
730            RendererError::ShaderError(msg) => write!(f, "Shader error: {}", msg),
731        }
732    }
733}
734
735impl std::error::Error for RendererError {}