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}