1use thiserror::Error;
7
8#[derive(Debug, Error)]
10pub enum Error {
11 #[error("{0}")]
13 General(String),
14
15 #[error(
17 "buffer allocation failed: requested {requested_bytes} bytes (available {available_bytes})"
18 )]
19 BufferAllocationFailed {
20 requested_bytes: usize,
22 available_bytes: usize,
24 },
25
26 #[error("invalid buffer handle: {0}")]
28 InvalidBufferHandle(usize),
29
30 #[error("shader compilation error in '{shader}': {message}")]
32 ShaderCompilationError {
33 shader: String,
35 message: String,
37 },
38
39 #[error("dispatch size {dispatch_size} exceeds hardware limit {limit}")]
41 DispatchLimitExceeded {
42 dispatch_size: usize,
44 limit: usize,
46 },
47
48 #[error("grid index ({i}, {j}, {k}) out of bounds for grid ({nx}, {ny}, {nz})")]
50 GridIndexOutOfBounds {
51 i: usize,
53 j: usize,
55 k: usize,
57 nx: usize,
59 ny: usize,
61 nz: usize,
63 },
64
65 #[error("kernel '{kernel}' expects {expected} arguments but got {got}")]
67 KernelArgCountMismatch {
68 kernel: String,
70 expected: usize,
72 got: usize,
74 },
75
76 #[error("unsupported feature: {feature}")]
78 UnsupportedFeature {
79 feature: String,
81 },
82}
83
84#[derive(Debug, Error)]
86#[error("pipeline stage '{stage}' failed: {source}")]
87pub struct PipelineStageError {
88 pub stage: String,
90 pub source: Box<Error>,
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
96pub enum ErrorSeverity {
97 Info,
99 Warning,
101 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#[derive(Debug)]
117pub struct AnnotatedError {
118 pub error: Error,
120 pub severity: ErrorSeverity,
122 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 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 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
156pub type Result<T> = std::result::Result<T, Error>;
158
159#[derive(Debug, Error)]
161pub enum GpuError {
162 #[error("GPU backend init failed: {0}")]
164 BackendInit(String),
165
166 #[error("shader dispatch error: {0}")]
168 ShaderDispatch(String),
169
170 #[error("GPU buffer read-back failed: {0}")]
172 ReadBack(String),
173
174 #[error("invalid GPU buffer handle: {0}")]
176 InvalidHandle(usize),
177}
178
179impl Error {
180 pub fn general(msg: impl std::fmt::Display) -> Self {
182 Error::General(msg.to_string())
183 }
184
185 pub fn is_allocation_error(&self) -> bool {
187 matches!(self, Error::BufferAllocationFailed { .. })
188 }
189
190 pub fn is_shader_error(&self) -> bool {
192 matches!(self, Error::ShaderCompilationError { .. })
193 }
194
195 pub fn is_grid_error(&self) -> bool {
197 matches!(self, Error::GridIndexOutOfBounds { .. })
198 }
199
200 pub fn is_arg_mismatch(&self) -> bool {
202 matches!(self, Error::KernelArgCountMismatch { .. })
203 }
204
205 pub fn is_unsupported(&self) -> bool {
207 matches!(self, Error::UnsupportedFeature { .. })
208 }
209
210 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 pub fn fatal(self, kernel: Option<&str>) -> AnnotatedError {
220 AnnotatedError::fatal(self, kernel)
221 }
222
223 pub fn warning(self, kernel: Option<&str>) -> AnnotatedError {
225 AnnotatedError::warning(self, kernel)
226 }
227
228 pub fn into_err<T>(self) -> Result<T> {
230 Err(self)
231 }
232}
233
234pub fn alloc_err(requested_bytes: usize, available_bytes: usize) -> Error {
238 Error::BufferAllocationFailed {
239 requested_bytes,
240 available_bytes,
241 }
242}
243
244pub 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
253pub 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
265pub fn dispatch_limit_err(dispatch_size: usize, limit: usize) -> Error {
267 Error::DispatchLimitExceeded {
268 dispatch_size,
269 limit,
270 }
271}
272
273pub 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
281pub fn unsupported_err(feature: impl Into<String>) -> Error {
283 Error::UnsupportedFeature {
284 feature: feature.into(),
285 }
286}
287
288pub fn collect_errors(errors: Vec<Error>) -> Result<()> {
293 errors.into_iter().next().map_or(Ok(()), Err)
294}
295
296pub 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 #[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}