Skip to main content

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}