Skip to main content

khora_core/renderer/
error.rs

1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Defines the hierarchy of error types for the rendering subsystem.
16
17use crate::renderer::api::core::ShaderModuleId;
18use crate::renderer::api::pipeline::RenderPipelineId;
19use std::fmt;
20
21/// An error related to the creation, loading, or compilation of a shader module.
22#[derive(Debug)]
23pub enum ShaderError {
24    /// An error occurred while trying to load the shader source from a path.
25    LoadError {
26        /// The path of the file that failed to load.
27        path: String,
28        /// The underlying I/O or source error.
29        source_error: String,
30    },
31    /// The shader source failed to compile into a backend-specific module.
32    CompilationError {
33        /// A descriptive label for the shader, if available.
34        label: String,
35        /// Detailed error messages from the shader compiler.
36        details: String,
37    },
38    /// The requested shader module could not be found.
39    NotFound {
40        /// The ID of the shader module that was not found.
41        id: ShaderModuleId,
42    },
43    /// The specified entry point (e.g., `vs_main`) is not valid for the shader module.
44    InvalidEntryPoint {
45        /// The ID of the shader module.
46        id: ShaderModuleId,
47        /// The entry point name that was not found.
48        entry_point: String,
49    },
50}
51
52impl fmt::Display for ShaderError {
53    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54        match self {
55            ShaderError::LoadError { path, source_error } => {
56                write!(
57                    f,
58                    "Failed to load shader source from '{path}': {source_error}"
59                )
60            }
61            ShaderError::CompilationError { label, details } => {
62                write!(f, "Shader compilation failed for '{label}': {details}")
63            }
64            ShaderError::NotFound { id } => {
65                write!(f, "Shader module not found for ID: {id:?}")
66            }
67            ShaderError::InvalidEntryPoint { id, entry_point } => {
68                write!(
69                    f,
70                    "Invalid entry point '{entry_point}' for shader module {id:?}"
71                )
72            }
73        }
74    }
75}
76
77impl std::error::Error for ShaderError {}
78
79/// An error related to the creation or management of a graphics pipeline.
80#[derive(Debug)]
81pub enum PipelineError {
82    /// Failed to create a pipeline layout from the provided shader reflection data.
83    LayoutCreationFailed(String),
84    /// The graphics backend failed to compile the full pipeline state object.
85    CompilationFailed {
86        /// A descriptive label for the pipeline, if available.
87        label: Option<String>,
88        /// Detailed error messages from the backend.
89        details: String,
90    },
91    /// A shader module provided for the pipeline was invalid or missing.
92    InvalidShaderModuleForPipeline {
93        /// The ID of the invalid shader module.
94        id: ShaderModuleId,
95        /// The label of the pipeline being created.
96        pipeline_label: Option<String>,
97    },
98    /// The specified render pipeline ID is not valid.
99    InvalidRenderPipeline {
100        /// The ID of the invalid render pipeline.
101        id: RenderPipelineId,
102    },
103    /// The fragment shader stage is present but no entry point was specified.
104    MissingEntryPointForFragmentShader {
105        /// The label of the pipeline being created.
106        pipeline_label: Option<String>,
107        /// The ID of the fragment shader module.
108        shader_id: ShaderModuleId,
109    },
110    /// The color target format is not compatible with the pipeline or device.
111    IncompatibleColorTarget(String),
112    /// The depth/stencil format is not compatible with the pipeline or device.
113    IncompatibleDepthStencilFormat(String),
114    /// A required graphics feature is not supported by the device.
115    FeatureNotSupported(String),
116}
117
118impl fmt::Display for PipelineError {
119    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120        match self {
121            PipelineError::LayoutCreationFailed(msg) => {
122                write!(f, "Pipeline layout creation failed: {msg}")
123            }
124            PipelineError::CompilationFailed { label, details } => {
125                write!(
126                    f,
127                    "Pipeline compilation failed for '{}': {}",
128                    label.as_deref().unwrap_or("Unknown"),
129                    details
130                )
131            }
132            PipelineError::InvalidShaderModuleForPipeline { id, pipeline_label } => {
133                write!(
134                    f,
135                    "Invalid shader module {:?} for pipeline '{}'",
136                    id,
137                    pipeline_label.as_deref().unwrap_or("Unknown")
138                )
139            }
140            PipelineError::InvalidRenderPipeline { id } => {
141                write!(f, "Invalid render pipeline ID: {id:?}")
142            }
143            PipelineError::MissingEntryPointForFragmentShader {
144                pipeline_label,
145                shader_id,
146            } => {
147                write!(
148                    f,
149                    "Missing entry point for fragment shader in pipeline '{}', shader ID: {:?}",
150                    pipeline_label.as_deref().unwrap_or("Unknown"),
151                    shader_id
152                )
153            }
154            PipelineError::IncompatibleColorTarget(msg) => {
155                write!(f, "Incompatible color target format: {msg}")
156            }
157            PipelineError::IncompatibleDepthStencilFormat(msg) => {
158                write!(f, "Incompatible depth/stencil format: {msg}")
159            }
160            PipelineError::FeatureNotSupported(msg) => {
161                write!(f, "Feature not supported: {msg}")
162            }
163        }
164    }
165}
166
167impl std::error::Error for PipelineError {}
168
169/// An error related to the creation or use of a GPU resource (buffers, textures, etc.).
170#[derive(Debug)]
171pub enum ResourceError {
172    /// A shader-specific error occurred.
173    Shader(ShaderError),
174    /// A pipeline-specific error occurred.
175    Pipeline(PipelineError),
176    /// A generic resource could not be found.
177    NotFound,
178    /// The handle or ID used to reference a resource is invalid.
179    InvalidHandle,
180    /// An error originating from the specific graphics backend implementation.
181    BackendError(String),
182    /// An attempt was made to access a resource out of its bounds (e.g., in a buffer).
183    OutOfBounds,
184}
185
186impl fmt::Display for ResourceError {
187    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188        match self {
189            ResourceError::Shader(err) => write!(f, "Shader resource error: {err}"),
190            ResourceError::Pipeline(err) => write!(f, "Pipeline resource error: {err}"),
191            ResourceError::NotFound => write!(f, "Resource not found with ID."),
192            ResourceError::InvalidHandle => write!(f, "Invalid resource handle or ID."),
193            ResourceError::BackendError(msg) => {
194                write!(f, "Backend-specific resource error: {msg}")
195            }
196            ResourceError::OutOfBounds => {
197                write!(f, "Resource access out of bounds.")
198            }
199        }
200    }
201}
202
203impl std::error::Error for ResourceError {
204    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
205        match self {
206            ResourceError::Shader(err) => Some(err),
207            _ => None,
208        }
209    }
210}
211
212impl From<ShaderError> for ResourceError {
213    fn from(err: ShaderError) -> Self {
214        ResourceError::Shader(err)
215    }
216}
217
218impl From<PipelineError> for ResourceError {
219    fn from(err: PipelineError) -> Self {
220        ResourceError::Pipeline(err)
221    }
222}
223
224/// A high-level error that can occur within the main rendering system or graphics device.
225#[derive(Debug)]
226pub enum RenderError {
227    /// An operation was attempted before the rendering system was initialized.
228    NotInitialized,
229    /// A failure occurred during the initialization of the graphics backend.
230    InitializationFailed(String),
231    /// Failed to acquire the next frame from the swapchain/surface for rendering.
232    SurfaceAcquisitionFailed(String),
233    /// A critical, unrecoverable rendering operation failed.
234    RenderingFailed(String),
235    /// An error occurred while managing a GPU resource.
236    ResourceError(ResourceError),
237    /// The graphics device was lost (e.g., GPU driver crashed or was updated).
238    /// This is a catastrophic error that typically requires reinitialization.
239    DeviceLost,
240    /// The graphics device or surface ran out of memory.
241    ///
242    /// This is fatal but non-panicking: the frame loop surfaces it so the host
243    /// can decide to shut down cleanly rather than crashing mid-frame.
244    DeviceOutOfMemory(String),
245    /// An unexpected or internal error occurred.
246    Internal(String),
247}
248
249impl RenderError {
250    /// Whether this error is fatal — i.e. the render system cannot make
251    /// progress on subsequent frames and the host should tear down or
252    /// reinitialize rather than retry.
253    ///
254    /// Transient acquisition failures (surface lost/outdated/timeout, occluded
255    /// or zero-size windows) are **not** fatal: the loop skips the frame and
256    /// retries. Device loss and out-of-memory **are** fatal.
257    pub fn is_fatal(&self) -> bool {
258        matches!(
259            self,
260            RenderError::DeviceLost | RenderError::DeviceOutOfMemory(_)
261        )
262    }
263}
264
265impl fmt::Display for RenderError {
266    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
267        match self {
268            RenderError::NotInitialized => {
269                write!(f, "The rendering system is not initialized.")
270            }
271            RenderError::InitializationFailed(msg) => {
272                write!(f, "Failed to initialize graphics backend: {msg}")
273            }
274            RenderError::SurfaceAcquisitionFailed(msg) => {
275                write!(f, "Failed to acquire surface for rendering: {msg}")
276            }
277            RenderError::RenderingFailed(msg) => {
278                write!(f, "A critical rendering operation failed: {msg}")
279            }
280            RenderError::ResourceError(err) => {
281                write!(f, "Graphics resource operation failed: {err}")
282            }
283            RenderError::DeviceLost => write!(
284                f,
285                "The graphics device was lost and needs to be reinitialized."
286            ),
287            RenderError::DeviceOutOfMemory(msg) => {
288                write!(f, "The graphics device ran out of memory: {msg}")
289            }
290            RenderError::Internal(msg) => {
291                write!(f, "An internal or unexpected error occurred: {msg}")
292            }
293        }
294    }
295}
296
297impl std::error::Error for RenderError {
298    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
299        match self {
300            RenderError::ResourceError(err) => Some(err),
301            _ => None,
302        }
303    }
304}
305
306impl From<ResourceError> for RenderError {
307    fn from(err: ResourceError) -> Self {
308        RenderError::ResourceError(err)
309    }
310}
311
312#[cfg(test)]
313mod tests {
314    use std::error::Error;
315
316    use super::*;
317    use crate::renderer::api::core::ShaderModuleId;
318
319    #[test]
320    fn shader_error_display() {
321        let err = ShaderError::LoadError {
322            path: "path/to/shader.wgsl".to_string(),
323            source_error: "File not found".to_string(),
324        };
325        assert_eq!(
326            format!("{err}"),
327            "Failed to load shader source from 'path/to/shader.wgsl': File not found"
328        );
329
330        let err_comp = ShaderError::CompilationError {
331            label: "MyShader".to_string(),
332            details: "Syntax error at line 5".to_string(),
333        };
334        assert_eq!(
335            format!("{err_comp}"),
336            "Shader compilation failed for 'MyShader': Syntax error at line 5"
337        );
338    }
339
340    #[test]
341    fn resource_error_display_wrapping_shader_error() {
342        let shader_err = ShaderError::NotFound {
343            id: ShaderModuleId(42),
344        };
345        let res_err: ResourceError = shader_err.into();
346        assert_eq!(
347            format!("{res_err}"),
348            "Shader resource error: Shader module not found for ID: ShaderModuleId(42)"
349        );
350        assert!(res_err.source().is_some());
351    }
352
353    #[test]
354    fn render_error_display_wrapping_resource_error() {
355        let shader_err = ShaderError::NotFound {
356            id: ShaderModuleId(101),
357        };
358        let res_err: ResourceError = shader_err.into();
359        let render_err: RenderError = res_err.into();
360        assert_eq!(
361            format!("{render_err}"),
362            "Graphics resource operation failed: Shader resource error: Shader module not found for ID: ShaderModuleId(101)"
363        );
364        assert!(render_err.source().is_some());
365        assert!(render_err.source().unwrap().source().is_some());
366    }
367
368    #[test]
369    fn render_error_is_fatal_classification() {
370        // Fatal: device loss and out-of-memory require host teardown.
371        assert!(RenderError::DeviceLost.is_fatal());
372        assert!(RenderError::DeviceOutOfMemory("vk OOM".to_string()).is_fatal());
373
374        // Non-fatal: transient acquisition / lifecycle errors are retried.
375        assert!(!RenderError::NotInitialized.is_fatal());
376        assert!(!RenderError::SurfaceAcquisitionFailed("Timeout".to_string()).is_fatal());
377        assert!(!RenderError::RenderingFailed("pass".to_string()).is_fatal());
378        assert!(!RenderError::Internal("misc".to_string()).is_fatal());
379    }
380
381    #[test]
382    fn device_out_of_memory_display() {
383        let err = RenderError::DeviceOutOfMemory("allocation failed".to_string());
384        assert_eq!(
385            format!("{err}"),
386            "The graphics device ran out of memory: allocation failed"
387        );
388    }
389}