concinnity_core/render/error.rs
1//! The typed error vocabulary of the `RenderBackend` boundary. Backends map
2//! their native failure codes (VkResult, HRESULT, MTLCommandBuffer status) into
3//! these classes at the detection sites; the frame loop dispatches recovery
4//! policy on the class, never on prose. `Other` carries legacy string errors so
5//! interior call sites can migrate incrementally.
6
7use alloc::string::String;
8use alloc::string::ToString;
9use thiserror::Error;
10
11/// Why the GPU device stopped servicing work, as reported by the backend API.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
13pub enum DeviceLostReason {
14 /// The physical device left (unplugged eGPU, driver upgrade teardown).
15 #[error("device removed")]
16 Removed,
17 /// The device reset underneath the app (TDR without a hang verdict).
18 #[error("device reset")]
19 Reset,
20 /// The OS killed the device after deciding our workload hung it.
21 #[error("device hung")]
22 Hung,
23 /// The presentation surface died; the device may be healthy, but the
24 /// backend cannot present without recreating the surface.
25 #[error("surface lost")]
26 SurfaceLost,
27 /// The backend reported loss without a usable reason code.
28 #[error("unknown")]
29 Unknown,
30}
31
32/// A failure crossing the `RenderBackend` boundary, classified for recovery.
33#[derive(Debug, Clone, PartialEq, Eq, Error)]
34pub enum RenderError {
35 /// The device is gone; no further GPU work can succeed on it. `detail` is
36 /// backend prose for the log (e.g. the `GetDeviceRemovedReason` message).
37 #[error("device lost ({reason}): {detail}")]
38 DeviceLost {
39 /// Why the device was lost.
40 reason: DeviceLostReason,
41 /// Backend prose for the log.
42 detail: String,
43 },
44 /// A GPU allocation failed for lack of device memory.
45 #[error("out of device memory: {0}")]
46 OutOfDeviceMemory(String),
47 /// The swapchain no longer matches the surface; the frame did not present.
48 /// Transient: the backend recreates the swapchain and the next frame
49 /// normally succeeds.
50 #[error("swapchain out of date")]
51 SwapchainOutOfDate,
52 /// A shader failed to compile or link into a pipeline.
53 #[error("shader compile: {0}")]
54 ShaderCompile(String),
55 /// An unclassified failure carrying the original message.
56 #[error("{0}")]
57 Other(String),
58}
59
60/// A backend call's result.
61pub type RenderResult<T> = Result<T, RenderError>;
62
63impl From<String> for RenderError {
64 fn from(message: String) -> Self {
65 RenderError::Other(message)
66 }
67}
68
69impl From<&str> for RenderError {
70 fn from(message: &str) -> Self {
71 RenderError::Other(message.to_string())
72 }
73}
74
75// Bridge for interior call sites still reporting `Result<_, String>`: a typed
76// error crossing one decays to its message, so a detection site can go typed
77// before every caller above it has migrated.
78impl From<RenderError> for String {
79 fn from(error: RenderError) -> Self {
80 error.to_string()
81 }
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 #[test]
89 fn string_coerces_to_other() {
90 fn fails() -> RenderResult<()> {
91 Err::<(), String>("boom".to_string())?;
92 Ok(())
93 }
94 assert_eq!(fails(), Err(RenderError::Other("boom".to_string())));
95 }
96
97 #[test]
98 fn display_includes_reason_and_detail() {
99 let e = RenderError::DeviceLost {
100 reason: DeviceLostReason::Hung,
101 detail: "queue submit".to_string(),
102 };
103 assert_eq!(e.to_string(), "device lost (device hung): queue submit");
104 }
105}