backdrop_blur_core/error.rs
1//! The return half of the seam contract. [`BlurError`] is a `thiserror` enum, but because
2//! core has **no GPU dependency** it cannot name a backend error type (`wgpu::*`/`glow::*`
3//! live in the GPU crates core forbids). It therefore carries a **boxed trait-object source**
4//! ([`BackendError`]) — still a typed `Error` value that composes with `?` and `#[source]`,
5//! never a flattened `String` model (DESIGN §4.5).
6//!
7//! A zero-sized/offscreen region is a **no-op**, not an error (`prepare` returns `Ok(None)`),
8//! so there is deliberately no `ZeroSizedRegion` variant.
9
10use crate::gl_region::GlRegion;
11
12/// The boxed, typed source a backend attaches to a [`BlurError`]. Core cannot name
13/// `wgpu::Error`/`glow` errors, so it accepts any `Send + Sync` standard error.
14pub type BackendError = Box<dyn std::error::Error + Send + Sync + 'static>;
15
16/// Everything that can go wrong producing a frosted surface. Each `Display` is a complete
17/// sentence; `ResourceCreation.stage` localizes a 3 AM kiosk failure to the exact resource
18/// that died.
19#[derive(Debug, thiserror::Error)]
20pub enum BlurError {
21 /// A GPU resource could not be created during `prepare`.
22 #[error("failed to create the {stage} while preparing the blur")]
23 ResourceCreation {
24 /// Which resource failed.
25 stage: BlurStage,
26 /// The backend's underlying error.
27 #[source]
28 source: BackendError,
29 },
30
31 /// The caller's target color format is not on the backend's supported-composite allowlist.
32 /// Distinct from a backend's own must-match-format validation (DESIGN §4.4/§4.5).
33 #[error("target format {format} is not a supported render target for the blur composite")]
34 UnsupportedTarget {
35 /// The rejected format, captured as text at the backend boundary because core cannot
36 /// name `wgpu::TextureFormat` (a deliberate `String` exception, documented in DESIGN §4.5).
37 format: String,
38 },
39
40 /// The grab-pass backend could not produce a sampleable source from the live framebuffer.
41 /// (Reserved for the deferred glow path; the socket exists now so adding it is not a core
42 /// rewrite.)
43 #[error("the grab source could not be produced from the framebuffer for region {region}")]
44 GrabFailed {
45 /// The region the grab was attempted for. A [`GlRegion`] (GL bottom-left), **not** a
46 /// reinterpreted [`Region`]: this is a human-facing error, and `GlRegion`'s `Display`
47 /// marks the origin bottom-left so a debugger cannot misread it against `Region`'s
48 /// top-left convention.
49 ///
50 /// [`Region`]: crate::Region
51 region: GlRegion,
52 /// The backend's underlying error.
53 #[source]
54 source: BackendError,
55 },
56
57 /// The live GL context lacks a capability the grab-pass backend requires (too-old GL/GLES,
58 /// a missing float-render extension). Raised at backend construction, before any frame.
59 #[error("the GL context does not support the blur backend: {detail}")]
60 UnsupportedContext {
61 /// What was required vs. found, captured as text because core cannot name a `glow`
62 /// version/extension type (the same documented `String` exception as
63 /// [`UnsupportedTarget`](Self::UnsupportedTarget), DESIGN §4.5).
64 detail: String,
65 },
66}
67
68/// Which GPU resource a [`BlurError::ResourceCreation`] refers to — named so a failure points
69/// at one resource, not "something in prepare".
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum BlurStage {
72 /// A ping-pong scratch texture in the blur chain.
73 PingPongTexture,
74 /// The downsample render pipeline.
75 DownsamplePipeline,
76 /// The upsample render pipeline.
77 UpsamplePipeline,
78 /// The final composite render pipeline (built per target format).
79 CompositePipeline,
80 /// The uniform buffer carrying the resolved mask/tint/offsets.
81 UniformBuffer,
82 /// A bind group wiring textures/uniforms to a pipeline.
83 BindGroup,
84 /// A shader stage failed to compile (the immediate-mode glow path: `glCompileShader`).
85 ShaderCompile,
86 /// A linked GL program failed to link its compiled stages (`glLinkProgram`).
87 ProgramLink,
88 /// A GL framebuffer object could not be created or was incomplete (grab / resolve / scratch).
89 Framebuffer,
90 /// A GL vertex array object (the shared fullscreen-triangle VAO) could not be created.
91 VertexArray,
92}
93
94impl std::fmt::Display for BlurStage {
95 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96 let label = match self {
97 Self::PingPongTexture => "ping-pong scratch texture",
98 Self::DownsamplePipeline => "downsample pipeline",
99 Self::UpsamplePipeline => "upsample pipeline",
100 Self::CompositePipeline => "composite pipeline",
101 Self::UniformBuffer => "uniform buffer",
102 Self::BindGroup => "bind group",
103 Self::ShaderCompile => "shader",
104 Self::ProgramLink => "shader program",
105 Self::Framebuffer => "framebuffer",
106 Self::VertexArray => "vertex array",
107 };
108 f.write_str(label)
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115 use crate::geometry::Scale;
116
117 #[test]
118 fn resource_creation_display_names_the_stage() {
119 let err = BlurError::ResourceCreation {
120 stage: BlurStage::CompositePipeline,
121 source: "device lost".into(),
122 };
123 assert_eq!(
124 err.to_string(),
125 "failed to create the composite pipeline while preparing the blur"
126 );
127 }
128
129 #[test]
130 fn unsupported_target_display_includes_the_format() {
131 let err = BlurError::UnsupportedTarget {
132 format: "Rgba8Snorm".to_owned(),
133 };
134 assert!(err.to_string().contains("Rgba8Snorm"));
135 }
136
137 #[test]
138 fn error_source_chains_to_the_backend_error() {
139 let err = BlurError::ResourceCreation {
140 stage: BlurStage::PingPongTexture,
141 source: "out of memory".into(),
142 };
143 let source = std::error::Error::source(&err).expect("a backend source is attached");
144 assert_eq!(source.to_string(), "out of memory");
145 }
146
147 #[test]
148 fn grab_failed_display_includes_the_region() {
149 let err = BlurError::GrabFailed {
150 region: GlRegion::from_bottom_px([0, 0], [10, 10], Scale::default()),
151 source: "no framebuffer".into(),
152 };
153 // The message embeds the bottom-left-marked region, so a debugger reads the orientation.
154 assert!(err.to_string().contains("region"));
155 assert!(err.to_string().contains("origin-bl"));
156 }
157
158 #[test]
159 fn stage_display_covers_every_variant() {
160 for stage in [
161 BlurStage::PingPongTexture,
162 BlurStage::DownsamplePipeline,
163 BlurStage::UpsamplePipeline,
164 BlurStage::CompositePipeline,
165 BlurStage::UniformBuffer,
166 BlurStage::BindGroup,
167 BlurStage::ShaderCompile,
168 BlurStage::ProgramLink,
169 BlurStage::Framebuffer,
170 BlurStage::VertexArray,
171 ] {
172 assert!(!stage.to_string().is_empty());
173 }
174 }
175
176 #[test]
177 fn unsupported_context_display_includes_the_detail() {
178 let err = BlurError::UnsupportedContext {
179 detail: "requires GL 3.3 / GLES 3.0, found GL 2.1".to_owned(),
180 };
181 assert!(
182 err.to_string()
183 .contains("requires GL 3.3 / GLES 3.0, found GL 2.1")
184 );
185 }
186}