Skip to main content

alpha2_gpu/
adapter_pin.rs

1//! `ALPHA2_GPU_ADAPTER` and the order adapters are tried in without a pin.
2
3/// How the renderer's adapter is chosen (`ALPHA2_GPU_ADAPTER`).
4#[derive(Clone, Debug, Eq, PartialEq)]
5pub enum AdapterPolicy {
6    /// Rank hardware adapters by GPU encode and decode, then high-performance order
7    /// ([`crate::rank_adapters`]). Requests without video context use the high-performance order
8    /// alone. The default on Linux and macOS.
9    Auto,
10    /// The first hardware adapter in the high-performance order, whatever it encodes or decodes.
11    /// The default on Windows ([`Self::platform_default`]).
12    Performance,
13    /// The adapter at this position of its backend's enumeration.
14    Index(usize),
15    /// The first whose name contains this, ignoring case.
16    Name(String),
17}
18
19impl AdapterPolicy {
20    /// The policy when `ALPHA2_GPU_ADAPTER` is unset: `auto` on Linux and macOS, `performance` on
21    /// Windows. On Windows `auto` often picks an Intel integrated GPU, where the bundled wgpu-hal
22    /// can leave DX12 textures uncleared, so `auto` there is opt-in.
23    #[must_use]
24    pub const fn platform_default() -> Self {
25        if cfg!(target_os = "windows") {
26            Self::Performance
27        } else {
28            Self::Auto
29        }
30    }
31
32    /// The policy `value` names; unset and empty give [`Self::platform_default`]. Any other text
33    /// that is not a number is a name pin.
34    #[must_use]
35    pub fn parse(value: Option<&str>) -> Self {
36        Self::parse_set(value).unwrap_or_else(Self::platform_default)
37    }
38
39    /// The policy `value` names, or `None` when it names none (unset or empty).
40    #[must_use]
41    pub fn parse_set(value: Option<&str>) -> Option<Self> {
42        match value.map(str::trim) {
43            None | Some("") => None,
44            Some("auto") => Some(Self::Auto),
45            Some("performance") => Some(Self::Performance),
46            Some(text) => Some(
47                text.parse::<usize>()
48                    .map_or_else(|_| Self::Name(text.to_owned()), Self::Index),
49            ),
50        }
51    }
52
53    /// The policy `ALPHA2_GPU_ADAPTER` names, else [`Self::platform_default`].
54    #[must_use]
55    pub fn from_env() -> Self {
56        Self::parse(std::env::var("ALPHA2_GPU_ADAPTER").ok().as_deref())
57    }
58
59    /// The policy `ALPHA2_GPU_ADAPTER` names, or `None` when it is unset.
60    #[must_use]
61    pub fn set_in_env() -> Option<Self> {
62        Self::parse_set(std::env::var("ALPHA2_GPU_ADAPTER").ok().as_deref())
63    }
64
65    /// Whether this policy names one adapter.
66    #[must_use]
67    pub const fn is_pin(&self) -> bool {
68        matches!(self, Self::Index(_) | Self::Name(_))
69    }
70
71    /// Whether this pin names the adapter at enumeration position `index` called `name`.
72    #[must_use]
73    pub fn names(&self, index: usize, name: &str) -> bool {
74        match self {
75            Self::Index(pinned) => index == *pinned,
76            Self::Name(pinned) => name.to_lowercase().contains(&pinned.to_lowercase()),
77            Self::Auto | Self::Performance => false,
78        }
79    }
80
81    /// The setting as `ALPHA2_GPU_ADAPTER` spells it.
82    #[must_use]
83    pub fn setting(&self) -> String {
84        match self {
85            Self::Auto => "ALPHA2_GPU_ADAPTER=auto".to_owned(),
86            Self::Performance => "ALPHA2_GPU_ADAPTER=performance".to_owned(),
87            Self::Index(index) => format!("ALPHA2_GPU_ADAPTER={index}"),
88            Self::Name(name) => format!("ALPHA2_GPU_ADAPTER={name}"),
89        }
90    }
91
92    /// The policy as a capability report names it: `auto`, `performance`, `index:<n>` or
93    /// `name:<text>`.
94    #[must_use]
95    pub fn report_name(&self) -> String {
96        match self {
97            Self::Auto => "auto".to_owned(),
98            Self::Performance => "performance".to_owned(),
99            Self::Index(index) => format!("index:{index}"),
100            Self::Name(name) => format!("name:{name}"),
101        }
102    }
103}
104
105/// Rank in wgpu-core's high-performance order: discrete, integrated, other, virtual, CPU.
106#[must_use]
107pub fn high_performance_rank(device_type: wgpu::DeviceType) -> u8 {
108    match device_type {
109        wgpu::DeviceType::DiscreteGpu => 1,
110        wgpu::DeviceType::IntegratedGpu => 2,
111        wgpu::DeviceType::Other => 3,
112        wgpu::DeviceType::VirtualGpu => 4,
113        wgpu::DeviceType::Cpu => 5,
114    }
115}
116
117/// Enumeration positions of `kinds` sorted by [`high_performance_rank`].
118#[must_use]
119pub fn product_order(kinds: &[(usize, wgpu::DeviceType)]) -> Vec<usize> {
120    let mut ordered = kinds.to_vec();
121    // Stable, so adapters of one kind keep the backend's order.
122    ordered.sort_by_key(|(_, kind)| high_performance_rank(*kind));
123    ordered.into_iter().map(|(index, _)| index).collect()
124}
125
126/// Whether a GL adapter's `GL_VERSION` meets desktop GL 4.3 or GLES 3.1. Unparseable versions are
127/// accepted and left to the device request.
128///
129/// # Errors
130/// The reason the version is too old.
131pub fn gl_version_supported(version: &str) -> Result<(), String> {
132    let (es, rest) = match version.trim().strip_prefix("OpenGL ES") {
133        Some(rest) => (true, rest.trim_start()),
134        None => (false, version.trim()),
135    };
136    let mut numbers = rest
137        .split(|c: char| !c.is_ascii_digit())
138        .filter(|part| !part.is_empty())
139        .map(str::parse::<u32>);
140    let (Some(Ok(major)), Some(Ok(minor))) = (numbers.next(), numbers.next()) else {
141        return Ok(());
142    };
143    let (needed, name) = if es {
144        ((3, 1), "OpenGL ES")
145    } else {
146        ((4, 3), "OpenGL")
147    };
148    if (major, minor) >= needed {
149        Ok(())
150    } else {
151        Err(format!(
152            "{name} {major}.{minor} cannot run alpha2's shaders (needs desktop GL 4.3 or GLES 3.1)"
153        ))
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160    use wgpu::DeviceType::{Cpu, DiscreteGpu, IntegratedGpu, Other, VirtualGpu};
161
162    #[test]
163    fn a_policy_is_auto_performance_an_index_or_a_name_and_unset_is_the_platform_default() {
164        assert_eq!(
165            AdapterPolicy::platform_default(),
166            if cfg!(target_os = "windows") {
167                AdapterPolicy::Performance
168            } else {
169                AdapterPolicy::Auto
170            }
171        );
172        for (value, policy) in [
173            (None, AdapterPolicy::platform_default()),
174            (Some(""), AdapterPolicy::platform_default()),
175            (Some(" auto "), AdapterPolicy::Auto),
176            (Some("performance"), AdapterPolicy::Performance),
177            (Some("1"), AdapterPolicy::Index(1)),
178            (Some("NVIDIA"), AdapterPolicy::Name("NVIDIA".to_owned())),
179            // Unknown policy words are name pins.
180            (Some("encoder"), AdapterPolicy::Name("encoder".to_owned())),
181            (Some("default"), AdapterPolicy::Name("default".to_owned())),
182        ] {
183            assert_eq!(AdapterPolicy::parse(value), policy, "{value:?}");
184        }
185        assert!(!AdapterPolicy::Performance.is_pin());
186        assert!(AdapterPolicy::Index(0).is_pin());
187        assert!(AdapterPolicy::Name("intel".to_owned()).names(3, "Intel(R) Iris(R) Xe Graphics"));
188        assert!(!AdapterPolicy::Name("intel".to_owned()).names(0, "NVIDIA GeForce MX450"));
189        assert!(AdapterPolicy::Index(2).names(2, "any"));
190        assert_eq!(AdapterPolicy::Index(2).setting(), "ALPHA2_GPU_ADAPTER=2");
191        assert_eq!(AdapterPolicy::Index(2).report_name(), "index:2");
192        assert_eq!(
193            AdapterPolicy::Name("Iris".to_owned()).report_name(),
194            "name:Iris"
195        );
196    }
197
198    #[test]
199    fn gl_versions_below_desktop_4_3_and_gles_3_1_are_refused() {
200        let refused = gl_version_supported("3.3.0 NVIDIA 580.105.08").expect_err("GL 3.3");
201        assert!(refused.contains("OpenGL 3.3"), "{refused}");
202        assert!(
203            refused.contains("needs desktop GL 4.3 or GLES 3.1"),
204            "{refused}"
205        );
206        assert_eq!(
207            gl_version_supported("4.5 (Core Profile) Mesa 23.2.1"),
208            Ok(())
209        );
210        assert_eq!(gl_version_supported("OpenGL ES 3.2 Mesa"), Ok(()));
211        let es = gl_version_supported("OpenGL ES 3.0 ANGLE").expect_err("GLES 3.0");
212        assert!(es.contains("OpenGL ES 3.0"), "{es}");
213        assert_eq!(gl_version_supported("unknown"), Ok(()));
214    }
215
216    #[test]
217    fn product_order_follows_the_high_performance_rank_and_keeps_enumeration_order_within_a_rank() {
218        assert_eq!(
219            product_order(&[
220                (0, Cpu),
221                (1, IntegratedGpu),
222                (2, DiscreteGpu),
223                (3, VirtualGpu),
224                (4, IntegratedGpu),
225                (5, Other),
226                (6, DiscreteGpu),
227            ]),
228            [2, 6, 1, 4, 5, 3, 0]
229        );
230    }
231}