Skip to main content

impeller/
backend.rs

1//! Choosing a rendering backend at runtime.
2//!
3//! The backend is selected when a context is created rather than when the
4//! binary is built, because one binary frequently has to run on machines with
5//! different graphics stacks — an embedded image that boots on a board with a
6//! working Vulkan driver and on one where only GLES is usable should not be two
7//! builds.
8//!
9//! That selection is an enumeration rather than a trait object. The HAL trait
10//! has associated types, which is what keeps it zero-cost and lets each backend
11//! name its own texture and fence, and a trait with associated types cannot be
12//! made into an object. Enumerating the compiled-in backends costs one branch
13//! per call and keeps the types intact.
14
15use impeller_hal::Error;
16
17/// Which backend to use.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
19pub enum BackendPreference {
20    /// Try each compiled-in backend in order of preference.
21    ///
22    /// Vulkan first: it is the reference implementation, the backend features
23    /// land on first, and the one every other is diffed against.
24    #[default]
25    Auto,
26    /// Vulkan, or fail.
27    Vulkan,
28    /// GLES, or fail.
29    Gles,
30}
31
32/// Which backend a context ended up using.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum Backend {
35    Vulkan,
36    Gles,
37}
38
39impl Backend {
40    pub fn name(self) -> &'static str {
41        match self {
42            Self::Vulkan => "vulkan",
43            Self::Gles => "gles",
44        }
45    }
46}
47
48impl std::fmt::Display for Backend {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        f.write_str(self.name())
51    }
52}
53
54/// The error reported when nothing usable was found.
55///
56/// Carries why each candidate was rejected. A bare "no backend available" is
57/// nearly impossible to act on, and the usual cause — a driver present but
58/// missing an extension, or a display server holding a device — is only visible
59/// in the individual failures.
60pub(crate) fn no_backend(attempts: Vec<(Backend, Error)>) -> Error {
61    if attempts.is_empty() {
62        return Error::Unsupported("no rendering backend was compiled in");
63    }
64    let detail = attempts
65        .iter()
66        .map(|(backend, error)| format!("  {backend}: {error}"))
67        .collect::<Vec<_>>()
68        .join("\n");
69    Error::Backend {
70        backend: "impeller",
71        detail: format!("no usable rendering backend:\n{detail}"),
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[test]
80    fn a_failure_names_every_candidate_and_its_reason() {
81        let error = no_backend(vec![
82            (Backend::Vulkan, Error::Unsupported("no device")),
83            (
84                Backend::Gles,
85                Error::Backend {
86                    backend: "gles",
87                    detail: "libEGL missing".into(),
88                },
89            ),
90        ]);
91        let text = error.to_string();
92        // Acting on this requires knowing which backend failed and why; a bare
93        // "nothing available" leaves a user with nowhere to start.
94        assert!(text.contains("vulkan"), "{text}");
95        assert!(text.contains("no device"), "{text}");
96        assert!(text.contains("gles"), "{text}");
97        assert!(text.contains("libEGL missing"), "{text}");
98    }
99
100    #[test]
101    fn a_build_with_no_backends_says_so_rather_than_blaming_the_machine() {
102        let error = no_backend(Vec::new());
103        assert!(error.to_string().contains("compiled in"));
104    }
105
106    #[test]
107    fn auto_is_the_default_preference() {
108        assert_eq!(BackendPreference::default(), BackendPreference::Auto);
109    }
110}