Skip to main content

oxiphysics_gpu/
error.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Error types for oxiphysics-gpu
5
6use thiserror::Error;
7
8/// Main error type for the gpu module.
9#[derive(Debug, Error)]
10pub enum Error {
11    /// Generic error with a free-form message.
12    #[error("{0}")]
13    General(String),
14
15    /// A GPU buffer allocation failed.
16    #[error(
17        "buffer allocation failed: requested {requested_bytes} bytes (available {available_bytes})"
18    )]
19    BufferAllocationFailed {
20        /// Bytes requested.
21        requested_bytes: usize,
22        /// Bytes actually available.
23        available_bytes: usize,
24    },
25
26    /// An invalid buffer handle was used.
27    #[error("invalid buffer handle: {0}")]
28    InvalidBufferHandle(usize),
29
30    /// A shader compilation error (mock).
31    #[error("shader compilation error in '{shader}': {message}")]
32    ShaderCompilationError {
33        /// Name of the offending shader.
34        shader: String,
35        /// Compiler message.
36        message: String,
37    },
38
39    /// A dispatch exceeded the hardware work-group limit.
40    #[error("dispatch size {dispatch_size} exceeds hardware limit {limit}")]
41    DispatchLimitExceeded {
42        /// Requested dispatch size (number of work-groups).
43        dispatch_size: usize,
44        /// Hardware maximum.
45        limit: usize,
46    },
47
48    /// Out-of-bounds grid access.
49    #[error("grid index ({i}, {j}, {k}) out of bounds for grid ({nx}, {ny}, {nz})")]
50    GridIndexOutOfBounds {
51        /// Requested x index.
52        i: usize,
53        /// Requested y index.
54        j: usize,
55        /// Requested z index.
56        k: usize,
57        /// Grid x dimension.
58        nx: usize,
59        /// Grid y dimension.
60        ny: usize,
61        /// Grid z dimension.
62        nz: usize,
63    },
64
65    /// A kernel argument count mismatch.
66    #[error("kernel '{kernel}' expects {expected} arguments but got {got}")]
67    KernelArgCountMismatch {
68        /// Kernel name.
69        kernel: String,
70        /// Expected number of arguments.
71        expected: usize,
72        /// Provided number of arguments.
73        got: usize,
74    },
75
76    /// An unsupported backend feature was requested.
77    #[error("unsupported feature: {feature}")]
78    UnsupportedFeature {
79        /// Description of the unsupported feature.
80        feature: String,
81    },
82}
83
84/// A pipeline-stage error: carries the stage name plus the underlying cause.
85#[derive(Debug, Error)]
86#[error("pipeline stage '{stage}' failed: {source}")]
87pub struct PipelineStageError {
88    /// Name of the pipeline stage (e.g. `"vertex_fetch"`, `"sph_density"`).
89    pub stage: String,
90    /// Root cause.
91    pub source: Box<Error>,
92}
93
94/// Severity level for a GPU error.
95#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
96pub enum ErrorSeverity {
97    /// Informational — execution can continue.
98    Info,
99    /// Warning — partial results may be degraded.
100    Warning,
101    /// Fatal — must abort current dispatch.
102    Fatal,
103}
104
105impl std::fmt::Display for ErrorSeverity {
106    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107        match self {
108            ErrorSeverity::Info => write!(f, "INFO"),
109            ErrorSeverity::Warning => write!(f, "WARNING"),
110            ErrorSeverity::Fatal => write!(f, "FATAL"),
111        }
112    }
113}
114
115/// An error annotated with severity and an optional kernel name.
116#[derive(Debug)]
117pub struct AnnotatedError {
118    /// The underlying error.
119    pub error: Error,
120    /// Severity classification.
121    pub severity: ErrorSeverity,
122    /// Optional kernel that triggered the error.
123    pub kernel: Option<String>,
124}
125
126impl std::fmt::Display for AnnotatedError {
127    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128        if let Some(ref k) = self.kernel {
129            write!(f, "[{}] kernel '{}': {}", self.severity, k, self.error)
130        } else {
131            write!(f, "[{}] {}", self.severity, self.error)
132        }
133    }
134}
135
136impl AnnotatedError {
137    /// Wrap an error as fatal with an optional kernel label.
138    pub fn fatal(error: Error, kernel: Option<&str>) -> Self {
139        Self {
140            error,
141            severity: ErrorSeverity::Fatal,
142            kernel: kernel.map(str::to_string),
143        }
144    }
145
146    /// Wrap an error as a warning.
147    pub fn warning(error: Error, kernel: Option<&str>) -> Self {
148        Self {
149            error,
150            severity: ErrorSeverity::Warning,
151            kernel: kernel.map(str::to_string),
152        }
153    }
154}
155
156/// Result type alias
157pub type Result<T> = std::result::Result<T, Error>;
158
159/// GPU operation error (used by `LbmGpuSolver`, `BvhGpuTraverser`, etc.).
160#[derive(Debug, Error)]
161pub enum GpuError {
162    /// GPU backend initialisation failed.
163    #[error("GPU backend init failed: {0}")]
164    BackendInit(String),
165
166    /// Shader dispatch / pipeline error.
167    #[error("shader dispatch error: {0}")]
168    ShaderDispatch(String),
169
170    /// Read-back from the GPU buffer failed.
171    #[error("GPU buffer read-back failed: {0}")]
172    ReadBack(String),
173
174    /// An invalid buffer handle was used.
175    #[error("invalid GPU buffer handle: {0}")]
176    InvalidHandle(usize),
177}
178
179impl Error {
180    /// Construct a [`Error::General`] from any `Display`-able value.
181    pub fn general(msg: impl std::fmt::Display) -> Self {
182        Error::General(msg.to_string())
183    }
184
185    /// True when this is a recoverable allocation error.
186    pub fn is_allocation_error(&self) -> bool {
187        matches!(self, Error::BufferAllocationFailed { .. })
188    }
189
190    /// True when this is a shader compilation error.
191    pub fn is_shader_error(&self) -> bool {
192        matches!(self, Error::ShaderCompilationError { .. })
193    }
194
195    /// True when this is an out-of-bounds grid error.
196    pub fn is_grid_error(&self) -> bool {
197        matches!(self, Error::GridIndexOutOfBounds { .. })
198    }
199
200    /// True when this is a kernel argument mismatch error.
201    pub fn is_arg_mismatch(&self) -> bool {
202        matches!(self, Error::KernelArgCountMismatch { .. })
203    }
204
205    /// True when this is an unsupported feature error.
206    pub fn is_unsupported(&self) -> bool {
207        matches!(self, Error::UnsupportedFeature { .. })
208    }
209
210    /// Wrap this error in a [`PipelineStageError`].
211    pub fn in_stage(self, stage: impl Into<String>) -> PipelineStageError {
212        PipelineStageError {
213            stage: stage.into(),
214            source: Box::new(self),
215        }
216    }
217
218    /// Annotate with fatal severity.
219    pub fn fatal(self, kernel: Option<&str>) -> AnnotatedError {
220        AnnotatedError::fatal(self, kernel)
221    }
222
223    /// Annotate with warning severity.
224    pub fn warning(self, kernel: Option<&str>) -> AnnotatedError {
225        AnnotatedError::warning(self, kernel)
226    }
227
228    /// Convert into a `Result::Err`.
229    pub fn into_err<T>(self) -> Result<T> {
230        Err(self)
231    }
232}
233
234// ── Convenience constructors ─────────────────────────────────────────────────
235
236/// Build a [`Error::BufferAllocationFailed`] error.
237pub fn alloc_err(requested_bytes: usize, available_bytes: usize) -> Error {
238    Error::BufferAllocationFailed {
239        requested_bytes,
240        available_bytes,
241    }
242}
243
244/// Build a [`Error::KernelArgCountMismatch`] error.
245pub fn arg_mismatch_err(kernel: impl Into<String>, expected: usize, got: usize) -> Error {
246    Error::KernelArgCountMismatch {
247        kernel: kernel.into(),
248        expected,
249        got,
250    }
251}
252
253/// Build a [`Error::GridIndexOutOfBounds`] error.
254pub fn grid_oob_err(i: usize, j: usize, k: usize, nx: usize, ny: usize, nz: usize) -> Error {
255    Error::GridIndexOutOfBounds {
256        i,
257        j,
258        k,
259        nx,
260        ny,
261        nz,
262    }
263}
264
265/// Build a [`Error::DispatchLimitExceeded`] error.
266pub fn dispatch_limit_err(dispatch_size: usize, limit: usize) -> Error {
267    Error::DispatchLimitExceeded {
268        dispatch_size,
269        limit,
270    }
271}
272
273/// Build a [`Error::ShaderCompilationError`].
274pub fn shader_err(shader: impl Into<String>, message: impl Into<String>) -> Error {
275    Error::ShaderCompilationError {
276        shader: shader.into(),
277        message: message.into(),
278    }
279}
280
281/// Build an [`Error::UnsupportedFeature`].
282pub fn unsupported_err(feature: impl Into<String>) -> Error {
283    Error::UnsupportedFeature {
284        feature: feature.into(),
285    }
286}
287
288// ── Error collection ─────────────────────────────────────────────────────────
289
290/// Collect multiple errors from a batch dispatch.  Returns `Ok(())` if the
291/// vec is empty, or `Err` containing the first error otherwise.
292pub fn collect_errors(errors: Vec<Error>) -> Result<()> {
293    errors.into_iter().next().map_or(Ok(()), Err)
294}
295
296/// Check a boolean condition; return `Err(Error::General(msg))` if false.
297pub fn check(condition: bool, msg: impl std::fmt::Display) -> Result<()> {
298    if condition {
299        Ok(())
300    } else {
301        Err(Error::general(msg))
302    }
303}
304
305#[cfg(test)]
306mod error_tests {
307    use super::*;
308
309    #[test]
310    fn test_general_error_message() {
311        let e = Error::general("something went wrong");
312        assert_eq!(e.to_string(), "something went wrong");
313    }
314
315    #[test]
316    fn test_buffer_allocation_failed_message() {
317        let e = Error::BufferAllocationFailed {
318            requested_bytes: 1024,
319            available_bytes: 512,
320        };
321        let msg = e.to_string();
322        assert!(msg.contains("1024"), "should mention requested bytes");
323        assert!(msg.contains("512"), "should mention available bytes");
324        assert!(e.is_allocation_error());
325    }
326
327    #[test]
328    fn test_invalid_buffer_handle() {
329        let e = Error::InvalidBufferHandle(42);
330        assert!(e.to_string().contains("42"));
331    }
332
333    #[test]
334    fn test_shader_compilation_error() {
335        let e = Error::ShaderCompilationError {
336            shader: "sph_density".to_string(),
337            message: "undefined symbol".to_string(),
338        };
339        let msg = e.to_string();
340        assert!(msg.contains("sph_density"));
341        assert!(msg.contains("undefined symbol"));
342        assert!(e.is_shader_error());
343    }
344
345    #[test]
346    fn test_dispatch_limit_exceeded() {
347        let e = Error::DispatchLimitExceeded {
348            dispatch_size: 100_000,
349            limit: 65535,
350        };
351        let msg = e.to_string();
352        assert!(msg.contains("100000"));
353        assert!(msg.contains("65535"));
354    }
355
356    #[test]
357    fn test_grid_index_out_of_bounds() {
358        let e = Error::GridIndexOutOfBounds {
359            i: 10,
360            j: 5,
361            k: 3,
362            nx: 8,
363            ny: 8,
364            nz: 8,
365        };
366        let msg = e.to_string();
367        assert!(msg.contains("10"));
368        assert!(msg.contains('8'.to_string().as_str()));
369    }
370
371    #[test]
372    fn test_is_not_shader_error() {
373        let e = Error::general("not a shader error");
374        assert!(!e.is_shader_error());
375    }
376
377    #[test]
378    fn test_unsupported_feature() {
379        let e = Error::UnsupportedFeature {
380            feature: "ray_tracing".to_string(),
381        };
382        assert!(e.to_string().contains("ray_tracing"));
383    }
384
385    // ── New error variant / helper tests ─────────────────────────────────
386
387    #[test]
388    fn test_is_grid_error() {
389        let e = grid_oob_err(1, 2, 3, 4, 5, 6);
390        assert!(e.is_grid_error());
391        assert!(!e.is_allocation_error());
392    }
393
394    #[test]
395    fn test_is_arg_mismatch() {
396        let e = arg_mismatch_err("test_kernel", 3, 2);
397        assert!(e.is_arg_mismatch());
398        assert!(!e.is_shader_error());
399    }
400
401    #[test]
402    fn test_is_unsupported() {
403        let e = unsupported_err("ray_tracing");
404        assert!(e.is_unsupported());
405    }
406
407    #[test]
408    fn test_in_stage_wraps_error() {
409        let e = Error::general("boom");
410        let wrapped = e.in_stage("sph_density");
411        assert!(wrapped.to_string().contains("sph_density"));
412        assert!(wrapped.to_string().contains("boom"));
413    }
414
415    #[test]
416    fn test_alloc_err_convenience() {
417        let e = alloc_err(512, 256);
418        assert!(e.is_allocation_error());
419        assert!(e.to_string().contains("512"));
420    }
421
422    #[test]
423    fn test_dispatch_limit_err_convenience() {
424        let e = dispatch_limit_err(99999, 65535);
425        assert!(e.to_string().contains("99999"));
426    }
427
428    #[test]
429    fn test_shader_err_convenience() {
430        let e = shader_err("my_shader", "syntax error");
431        assert!(e.is_shader_error());
432        assert!(e.to_string().contains("syntax error"));
433    }
434
435    #[test]
436    fn test_into_err() {
437        let result: Result<i32> = Error::general("nope").into_err();
438        assert!(result.is_err());
439    }
440
441    #[test]
442    fn test_collect_errors_empty() {
443        assert!(collect_errors(vec![]).is_ok());
444    }
445
446    #[test]
447    fn test_collect_errors_nonempty() {
448        let errs = vec![Error::general("first"), Error::general("second")];
449        let result = collect_errors(errs);
450        assert!(result.is_err());
451        assert!(result.unwrap_err().to_string().contains("first"));
452    }
453
454    #[test]
455    fn test_check_passes() {
456        assert!(check(true, "should not fail").is_ok());
457    }
458
459    #[test]
460    fn test_check_fails() {
461        let r = check(false, "condition violated");
462        assert!(r.is_err());
463        assert!(r.unwrap_err().to_string().contains("condition violated"));
464    }
465
466    #[test]
467    fn test_annotated_error_fatal_display() {
468        let e = Error::general("crash");
469        let ann = e.fatal(Some("sph_kernel"));
470        let s = ann.to_string();
471        assert!(s.contains("FATAL"));
472        assert!(s.contains("sph_kernel"));
473        assert!(s.contains("crash"));
474    }
475
476    #[test]
477    fn test_annotated_error_warning_no_kernel() {
478        let e = Error::general("degraded");
479        let ann = e.warning(None);
480        let s = ann.to_string();
481        assert!(s.contains("WARNING"));
482        assert!(s.contains("degraded"));
483    }
484
485    #[test]
486    fn test_error_severity_ordering() {
487        assert!(ErrorSeverity::Info < ErrorSeverity::Warning);
488        assert!(ErrorSeverity::Warning < ErrorSeverity::Fatal);
489    }
490
491    #[test]
492    fn test_error_severity_display() {
493        assert_eq!(ErrorSeverity::Info.to_string(), "INFO");
494        assert_eq!(ErrorSeverity::Warning.to_string(), "WARNING");
495        assert_eq!(ErrorSeverity::Fatal.to_string(), "FATAL");
496    }
497}