Skip to main content

gpui_platform/
gpu.rs

1//! GPU capability information shared by `gpui` and its platform backends.
2
3/// Information about the GPU GPUI is running on.
4#[derive(Default, Debug, serde::Serialize, serde::Deserialize, Clone)]
5pub struct GpuSpecs {
6    /// Whether the GPU is really a fake (like `llvmpipe`) running on the CPU.
7    pub is_software_emulated: bool,
8    /// The name of the device, as reported by Vulkan.
9    pub device_name: String,
10    /// The name of the driver, as reported by Vulkan.
11    pub driver_name: String,
12    /// Further information about the driver, as reported by Vulkan.
13    pub driver_info: String,
14}
15
16/// The identity of an adapter, for [`GpuPreference::Device`].
17///
18/// It is two facts, not one. `permanent` is what a config stores and what should still name the
19/// same hardware after a restart; `session` is what the backend can match in this process. A
20/// per-process handle (a LUID, a Metal registry id, a DRM node) does not survive a reboot, and a
21/// PCI pair does not distinguish two identical cards — so neither alone is a complete identity.
22///
23/// The permanent part is serialised; the session is not, because a config cannot know it.
24#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
25pub struct GpuId {
26    /// What survives a restart, where the platform has anything that does.
27    pub permanent: PermanentGpuId,
28    /// The backend's handle for this adapter in this process. `None` for a stored id.
29    #[serde(skip)]
30    pub session: Option<SessionGpuId>,
31}
32
33impl GpuId {
34    /// A `GpuId` matched on the PCI device id alone, with no session.
35    ///
36    /// This is what `ZED_DEVICE_ID` lowers to: the variable names a device only, so the vendor is
37    /// left open and the permanent part is the whole preference.
38    pub const fn by_device(device_id: u32) -> Self {
39        Self {
40            permanent: PermanentGpuId::Pci {
41                vendor_id: None,
42                device_id,
43                subsys_id: None,
44                revision: None,
45            },
46            session: None,
47        }
48    }
49
50    /// The permanent and session identity of a PCI adapter.
51    pub const fn pci(vendor_id: u32, device_id: u32) -> Self {
52        Self {
53            permanent: PermanentGpuId::Pci {
54                vendor_id: Some(vendor_id),
55                device_id,
56                subsys_id: None,
57                revision: None,
58            },
59            session: Some(SessionGpuId::Pci {
60                vendor_id,
61                device_id,
62            }),
63        }
64    }
65
66    /// The identity of the one adapter on a single-GPU platform.
67    pub const fn single() -> Self {
68        Self {
69            permanent: PermanentGpuId::Single,
70            session: Some(SessionGpuId::Default),
71        }
72    }
73
74    /// Whether `adapter` is the one this id names.
75    ///
76    /// A session is the stronger claim: when it is present, only the adapter with that session
77    /// matches. Without one — a config, or a caller that knows only the model — the permanent part
78    /// decides, and two identical cards both match.
79    pub fn matches(&self, adapter: &GpuAdapterInfo) -> bool {
80        match &self.session {
81            Some(session) => adapter.id.session.as_ref() == Some(session),
82            None => self.permanent.matches(&adapter.id.permanent),
83        }
84    }
85
86    /// Parse a four-hex-digit PCI device id, such as a debug override carries.
87    ///
88    /// The id is hexadecimal; a decimal-looking value such as `6422` parses as the hex `0x6422`,
89    /// matches nothing, and silently falls through to the default adapter.
90    pub fn parse_pci_id(value: &str) -> Option<u32> {
91        let id = value.trim();
92        let id = id
93            .strip_prefix("0x")
94            .or_else(|| id.strip_prefix("0X"))
95            .unwrap_or(id);
96        if id.len() == 4 && id.chars().all(|character| character.is_ascii_hexdigit()) {
97            u32::from_str_radix(id, 16).ok()
98        } else {
99            None
100        }
101    }
102}
103
104/// The stable part of a [`GpuId`]. Every adapter `enumerate_gpus` reports carries one.
105#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
106pub enum PermanentGpuId {
107    /// PCI vendor/device, plus the subsystem/revision that distinguishes board variants.
108    ///
109    /// Stable across restarts; two identical cards share it. `None` is a wildcard — `ZED_DEVICE_ID`
110    /// names a device only.
111    Pci {
112        /// The PCI vendor id, or `None` when the id names a device only.
113        vendor_id: Option<u32>,
114        /// The PCI device id.
115        device_id: u32,
116        /// The PCI subsystem id, when the backend reports one.
117        subsys_id: Option<u32>,
118        /// The PCI revision, when the backend reports one.
119        revision: Option<u32>,
120    },
121    /// The Windows PnP device instance id — the one documented persistent per-adapter identity.
122    /// DXGI does not expose it; a later `SetupAPI` read fills it in.
123    Pnp(String),
124    /// The adapter/driver name, where the platform has nothing better. Best-effort: two identical
125    /// cards share it.
126    Name(String),
127    /// A platform with one GPU, or a backend that cannot enumerate: there is only "the GPU".
128    Single,
129}
130
131impl PermanentGpuId {
132    /// Whether an adapter's permanent part is the one this id names.
133    ///
134    /// An `Option` field that is `None` in `self` is a wildcard; one that is `None` in `other` and
135    /// required by `self` does not match, because the backend cannot confirm it.
136    pub fn matches(&self, other: &Self) -> bool {
137        match (self, other) {
138            (
139                Self::Pci {
140                    vendor_id,
141                    device_id,
142                    subsys_id,
143                    revision,
144                },
145                Self::Pci {
146                    vendor_id: other_vendor,
147                    device_id: other_device,
148                    subsys_id: other_subsys,
149                    revision: other_revision,
150                },
151            ) => {
152                wildcard(*vendor_id, *other_vendor)
153                    && device_id == other_device
154                    && wildcard(*subsys_id, *other_subsys)
155                    && wildcard(*revision, *other_revision)
156            }
157            (Self::Pnp(want), Self::Pnp(have)) => want == have,
158            (Self::Name(want), Self::Name(have)) => want == have,
159            (Self::Single, Self::Single) => true,
160            _ => false,
161        }
162    }
163
164    /// The PCI vendor id, when this id has one.
165    pub fn vendor_id(&self) -> Option<u32> {
166        match self {
167            Self::Pci { vendor_id, .. } => *vendor_id,
168            _ => None,
169        }
170    }
171
172    /// The PCI device id, when this id has one.
173    pub fn device_id(&self) -> Option<u32> {
174        match self {
175            Self::Pci { device_id, .. } => Some(*device_id),
176            _ => None,
177        }
178    }
179}
180
181fn wildcard(want: Option<u32>, have: Option<u32>) -> bool {
182    want.is_none_or(|want| have == Some(want))
183}
184
185/// The backend's handle for an adapter in this session.
186#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
187pub enum SessionGpuId {
188    /// A DRM render node (Linux). The node number is assigned per boot and is not a hardware id.
189    Drm {
190        /// The node's minor number: `renderD<minor>`.
191        render_minor: u32,
192    },
193    /// A DXGI adapter LUID (Windows), packed from `Luid.HighPart`/`LowPart`. It lasts for the
194    /// adapter's lifetime and changes on reboot, driver upgrade, or device disable/enable.
195    Luid(u64),
196    /// A Metal registry id (macOS) — an IOKit registry entry id, valid only until reboot.
197    Metal {
198        /// The IOKit registry id.
199        registry_id: u64,
200    },
201    /// The PCI pair, where the backend has no per-boot handle; identical cards share it.
202    Pci {
203        /// The PCI vendor id.
204        vendor_id: u32,
205        /// The PCI device id.
206        device_id: u32,
207    },
208    /// A platform with one GPU, or a backend whose enumeration cannot name one.
209    Default,
210}
211
212/// Which GPU an application prefers the renderer to run on.
213///
214/// The device is process-wide and chosen once, before the first window (decision `0008`), so this is
215/// stated on the application rather than per window. The default is [`LowPower`](Self::LowPower): on
216/// a hybrid laptop the panel is wired to the integrated GPU, so rendering on the discrete one keeps
217/// it powered for the window's lifetime and crosses PCIe on every frame.
218#[derive(Clone, Default)]
219pub enum GpuPreference {
220    /// Today's ranking: the compositor's hint, then device type (discrete, integrated, other,
221    /// virtual, CPU), then backend.
222    Auto,
223    /// Prefer the integrated adapter. The default.
224    #[default]
225    LowPower,
226    /// Prefer the discrete adapter.
227    HighPerformance,
228    /// A named adapter, matched by its permanent part and, when it carries one, its session handle.
229    /// One that is not present falls back to [`Auto`].
230    Device(GpuId),
231    /// A predicate that picks **one** adapter from the enumerated list.
232    ///
233    /// The closure receives every adapter the platform reports and returns the index of the one it
234    /// wants, or `None` when none will do. It chooses a GPU; it does not rank them.
235    ///
236    /// ```
237    /// use std::sync::Arc;
238    /// use gpui_platform::{GpuAdapterInfo, GpuDeviceType, GpuPreference};
239    ///
240    /// let preference = GpuPreference::Custom(Arc::new(|gpus: &[GpuAdapterInfo]| {
241    ///     // A producer that must share a dma-buf with the renderer names its GPU first, and
242    ///     // otherwise takes the integrated one — where the panel is wired on a hybrid laptop.
243    ///     gpus.iter()
244    ///         .position(|gpu| gpu.device_id() == Some(0x1346))
245    ///         .or_else(|| {
246    ///             gpus.iter()
247    ///                 .position(|gpu| gpu.device_type == GpuDeviceType::Integrated)
248    ///         })
249    /// }));
250    /// ```
251    ///
252    /// The adapter named is tried first. If the display cannot be configured on it, the remaining
253    /// adapters follow in [`Auto`](Self::Auto)'s order — this is a preference, not a pin. An
254    /// application that must know what it got reads [`GpuAdapterInfo`] back through the app's
255    /// active-GPU accessor.
256    ///
257    /// `None`, or an index outside the list, fails the window with an error naming the shortfall: the
258    /// application saw the same list, so the two disagreeing is a bug worth surfacing rather than
259    /// silently drawing on a GPU the application did not choose.
260    Custom(std::sync::Arc<dyn Fn(&[GpuAdapterInfo]) -> Option<usize> + Send + Sync>),
261}
262
263impl std::fmt::Debug for GpuPreference {
264    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
265        match self {
266            Self::Auto => f.write_str("Auto"),
267            Self::LowPower => f.write_str("LowPower"),
268            Self::HighPerformance => f.write_str("HighPerformance"),
269            Self::Device(id) => f.debug_tuple("Device").field(id).finish(),
270            Self::Custom(_) => f.write_str("Custom(..)"),
271        }
272    }
273}
274
275/// The class of adapter a [`GpuAdapterInfo`] describes, without naming `wgpu`.
276#[derive(Clone, Copy, Debug, PartialEq, Eq)]
277pub enum GpuDeviceType {
278    /// A discrete GPU.
279    Discrete,
280    /// An integrated GPU.
281    Integrated,
282    /// A virtual GPU.
283    Virtual,
284    /// A software (CPU) implementation.
285    Cpu,
286    /// An adapter that is none of the above.
287    Other,
288}
289
290/// The graphics API a [`GpuAdapterInfo`] runs on, without naming `wgpu`.
291#[derive(Clone, Copy, Debug, PartialEq, Eq)]
292pub enum GpuBackend {
293    /// Vulkan.
294    Vulkan,
295    /// Metal.
296    Metal,
297    /// Direct3D 12.
298    Dx12,
299    /// Direct3D 11, the API `gpui_windows`' own renderer is built on.
300    Dx11,
301    /// OpenGL or OpenGL ES.
302    Gl,
303    /// WebGPU in a browser.
304    BrowserWebGpu,
305    /// Anything else.
306    Other,
307}
308
309/// One adapter an application can choose between.
310///
311/// Deliberately identity and class only. A `wgpu::Features` set and a dma-buf format/modifier list
312/// both need a device, and creating one at enumeration time is the cost enumeration exists to avoid;
313/// what a chosen device will import or export is the interop crate's report, asked on a device the
314/// caller already has.
315#[derive(Clone, Debug, PartialEq, Eq)]
316pub struct GpuAdapterInfo {
317    /// The permanent and session identity of the adapter.
318    pub id: GpuId,
319    /// The adapter name the driver reports.
320    pub name: String,
321    /// The adapter's class.
322    pub device_type: GpuDeviceType,
323    /// The graphics API the adapter runs on.
324    pub backend: GpuBackend,
325    /// Whether the adapter is a software implementation.
326    pub is_software: bool,
327}
328
329impl GpuAdapterInfo {
330    /// Describe one adapter the backend enumerated.
331    pub fn new(
332        id: GpuId,
333        name: impl Into<String>,
334        device_type: GpuDeviceType,
335        backend: GpuBackend,
336        is_software: bool,
337    ) -> Self {
338        Self {
339            id,
340            name: name.into(),
341            device_type,
342            backend,
343            is_software,
344        }
345    }
346
347    /// The one adapter a platform without enumeration reports.
348    pub fn single() -> Self {
349        Self::new(
350            GpuId::single(),
351            "default",
352            GpuDeviceType::Other,
353            GpuBackend::Other,
354            false,
355        )
356    }
357
358    /// The PCI vendor id, when this adapter has one.
359    pub fn vendor_id(&self) -> Option<u32> {
360        self.id.permanent.vendor_id()
361    }
362
363    /// The PCI device id, when this adapter has one.
364    pub fn device_id(&self) -> Option<u32> {
365        self.id.permanent.device_id()
366    }
367}
368
369/// The sort key for one adapter under a [`GpuPreference`].
370///
371/// Slots are compared in order: the named match, the preference, the compositor match and the
372/// backend. The meanings are documented on [`rank`]. The fields are private so a backend cannot
373/// invent an order the shared ranking does not know.
374#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
375pub struct GpuRank {
376    matched: u8,
377    preference: u8,
378    compositor: u8,
379    backend: u8,
380}
381
382impl GpuRank {
383    fn new(matched: u8, preference: u8, compositor: u8, backend: u8) -> Self {
384        Self {
385            matched,
386            preference,
387            compositor,
388            backend,
389        }
390    }
391}
392
393/// Rank one adapter under `preference`.
394///
395/// `LowPower`, `HighPerformance`, `Device` and `Custom` are portable; `Auto` leaves the ordering to
396/// the backend, so callers establish their own order first and a stable sort under this key
397/// preserves it. `compositor` is the Linux compositor hint, expressed in the same identity type; a
398/// backend with no equivalent passes `None`.
399pub fn rank(
400    adapter: &GpuAdapterInfo,
401    preference: &GpuPreference,
402    compositor: Option<&PermanentGpuId>,
403) -> GpuRank {
404    let compositor_match: u8 = match compositor {
405        Some(hint) if hint.matches(&adapter.id.permanent) => 0,
406        _ => 1,
407    };
408    let auto_type: u8 = match adapter.device_type {
409        GpuDeviceType::Discrete => 0,
410        GpuDeviceType::Integrated => 1,
411        GpuDeviceType::Other => 2,
412        GpuDeviceType::Virtual => 3,
413        GpuDeviceType::Cpu => 4,
414    };
415    let backend_priority: u8 = match adapter.backend {
416        GpuBackend::Vulkan | GpuBackend::Metal | GpuBackend::Dx12 | GpuBackend::Dx11 => 0,
417        _ => 1,
418    };
419    match preference {
420        GpuPreference::Auto => GpuRank::new(0, compositor_match, auto_type, backend_priority),
421        GpuPreference::Device(gpu_id) => GpuRank::new(
422            if gpu_id.matches(adapter) { 0 } else { 1 },
423            compositor_match,
424            auto_type,
425            backend_priority,
426        ),
427        GpuPreference::LowPower => GpuRank::new(
428            0,
429            type_priority(adapter.device_type, true),
430            compositor_match,
431            backend_priority,
432        ),
433        GpuPreference::HighPerformance => GpuRank::new(
434            0,
435            type_priority(adapter.device_type, false),
436            compositor_match,
437            backend_priority,
438        ),
439        // Resolved by `custom_choice` before any sorting; equal keys keep the backend's order.
440        GpuPreference::Custom(_) => GpuRank::new(0, 0, 0, 0),
441    }
442}
443
444/// The device-type order under an explicit power preference. A CPU adapter ranks last in both: it is
445/// a fallback, not a low-power device.
446fn type_priority(device_type: GpuDeviceType, low_power: bool) -> u8 {
447    match (device_type, low_power) {
448        (GpuDeviceType::Integrated, true) => 0,
449        (GpuDeviceType::Discrete, false) => 0,
450        (GpuDeviceType::Other, _) => 1,
451        (GpuDeviceType::Integrated, false) => 2,
452        (GpuDeviceType::Discrete, true) => 2,
453        (GpuDeviceType::Virtual, _) => 3,
454        (GpuDeviceType::Cpu, _) => 4,
455    }
456}
457
458/// Resolve a [`GpuPreference::Custom`] selector against the adapters that were enumerated.
459///
460/// The selector chooses one adapter out of the whole list — it does not rank them. `None`, or an
461/// index outside the list, is an error rather than a fallback: the application and this code saw the
462/// same enumeration, so a disagreement is a bug worth naming instead of silently drawing on a GPU
463/// the application did not choose.
464pub fn custom_choice(
465    adapters: &[GpuAdapterInfo],
466    selector: &dyn Fn(&[GpuAdapterInfo]) -> Option<usize>,
467) -> anyhow::Result<usize> {
468    match selector(adapters) {
469        Some(index) if index < adapters.len() => Ok(index),
470        Some(index) => anyhow::bail!(
471            "The application's custom GPU preference chose adapter {index}, but only {} adapter(s) \
472             were enumerated",
473            adapters.len()
474        ),
475        None => anyhow::bail!("The application's custom GPU preference chose no adapter"),
476    }
477}
478
479#[cfg(test)]
480mod tests {
481    use std::sync::Arc;
482
483    use super::{
484        GpuAdapterInfo, GpuBackend, GpuDeviceType, GpuId, GpuPreference, PermanentGpuId,
485        SessionGpuId, custom_choice, rank,
486    };
487
488    fn adapter(name: &str, device_type: GpuDeviceType, vendor: u32, device: u32) -> GpuAdapterInfo {
489        GpuAdapterInfo::new(
490            GpuId::pci(vendor, device),
491            name,
492            device_type,
493            GpuBackend::Vulkan,
494            false,
495        )
496    }
497
498    fn hint(vendor: u32, device: u32) -> PermanentGpuId {
499        PermanentGpuId::Pci {
500            vendor_id: Some(vendor),
501            device_id: device,
502            subsys_id: None,
503            revision: None,
504        }
505    }
506
507    #[test]
508    fn the_pci_id_is_four_hex_digits_with_an_optional_prefix() {
509        assert_eq!(GpuId::parse_pci_id("0xABCD"), Some(0xabcd));
510        assert_eq!(GpuId::parse_pci_id("ABCD"), Some(0xabcd));
511        assert_eq!(GpuId::parse_pci_id("abcd"), Some(0xabcd));
512        assert_eq!(GpuId::parse_pci_id("1234"), Some(0x1234));
513        assert_eq!(GpuId::parse_pci_id(" 0x1346 "), Some(0x1346));
514        assert_eq!(GpuId::parse_pci_id("123"), None);
515        assert_eq!(GpuId::parse_pci_id("0x12345"), None);
516        assert_eq!(GpuId::parse_pci_id("zzzz"), None);
517    }
518
519    #[test]
520    fn a_device_id_matches_by_device_and_leaves_the_vendor_open() {
521        let integrated = adapter("integrated", GpuDeviceType::Integrated, 0x8086, 0x1916);
522        let discrete = adapter("discrete", GpuDeviceType::Discrete, 0x10de, 0x1346);
523
524        let by_device = GpuId::by_device(0x1346);
525        assert!(by_device.matches(&discrete));
526        assert!(!by_device.matches(&integrated));
527
528        let exact = GpuId {
529            permanent: hint(0x10de, 0x1346),
530            session: None,
531        };
532        assert!(exact.matches(&discrete));
533        assert!(!exact.matches(&integrated));
534    }
535
536    #[test]
537    fn a_permanent_id_survives_a_restart_and_a_session_does_not() {
538        let mut enumerated = adapter("discrete", GpuDeviceType::Discrete, 0x10de, 0x1346);
539
540        let stored = GpuId {
541            permanent: enumerated.id.permanent.clone(),
542            session: None,
543        };
544        assert!(
545            stored.matches(&enumerated),
546            "the permanent part still names the card after the session is gone"
547        );
548
549        enumerated.id.session = Some(SessionGpuId::Luid(1));
550        let stale = GpuId {
551            permanent: enumerated.id.permanent.clone(),
552            session: Some(SessionGpuId::Luid(2)),
553        };
554        assert!(
555            !stale.matches(&enumerated),
556            "a session from another boot is not the same card"
557        );
558    }
559
560    #[test]
561    fn a_gpu_id_serialises_without_its_session() {
562        let enumerated = adapter("discrete", GpuDeviceType::Discrete, 0x10de, 0x1346);
563        let json = serde_json::to_value(&enumerated.id).expect("serialise a GpuId");
564        assert!(
565            json.get("session").is_none(),
566            "the session is per process and must not reach a config: {json}"
567        );
568
569        let restored: GpuId = serde_json::from_value(json).expect("deserialise a GpuId");
570        assert_eq!(restored.session, None);
571        assert!(restored.matches(&enumerated));
572    }
573
574    #[test]
575    fn a_single_gpu_platform_reports_one_identity() {
576        let only = GpuAdapterInfo::single();
577        assert_eq!(only.id.permanent, PermanentGpuId::Single);
578        assert_eq!(only.id.session, Some(SessionGpuId::Default));
579        assert!(GpuId::single().matches(&only));
580        assert!(
581            GpuId {
582                permanent: PermanentGpuId::Single,
583                session: None,
584            }
585            .matches(&only)
586        );
587    }
588
589    #[test]
590    fn the_preference_orders_the_adapters_it_names() {
591        use super::GpuPreference;
592
593        let integrated = adapter("integrated", GpuDeviceType::Integrated, 0x8086, 0x1916);
594        let discrete = adapter("discrete", GpuDeviceType::Discrete, 0x10de, 0x1346);
595        let on_the_discrete = hint(0x10de, 0x1346);
596
597        assert!(
598            rank(
599                &integrated,
600                &GpuPreference::LowPower,
601                Some(&on_the_discrete)
602            ) < rank(&discrete, &GpuPreference::LowPower, Some(&on_the_discrete)),
603            "LowPower takes the integrated adapter even when the compositor is on the discrete one",
604        );
605        assert!(
606            rank(
607                &discrete,
608                &GpuPreference::HighPerformance,
609                Some(&on_the_discrete)
610            ) < rank(
611                &integrated,
612                &GpuPreference::HighPerformance,
613                Some(&on_the_discrete)
614            ),
615            "HighPerformance takes the discrete adapter",
616        );
617        assert!(
618            rank(&discrete, &GpuPreference::Auto, None)
619                < rank(&integrated, &GpuPreference::Auto, None),
620            "Auto keeps the historical discrete-first order",
621        );
622        assert!(
623            rank(
624                &integrated,
625                &GpuPreference::Auto,
626                Some(&hint(0x8086, 0x1916))
627            ) < rank(&discrete, &GpuPreference::Auto, Some(&hint(0x8086, 0x1916))),
628            "Auto still follows the compositor's hint",
629        );
630    }
631
632    #[test]
633    fn a_named_device_ranks_first_and_an_absent_one_falls_back() {
634        let integrated = adapter("integrated", GpuDeviceType::Integrated, 0x8086, 0x1916);
635        let discrete = adapter("discrete", GpuDeviceType::Discrete, 0x10de, 0x1346);
636
637        let named = GpuPreference::Device(GpuId {
638            permanent: hint(0x8086, 0x1916),
639            session: None,
640        });
641        assert!(rank(&integrated, &named, None) < rank(&discrete, &named, None));
642
643        let absent = GpuPreference::Device(GpuId::by_device(0x9999));
644        assert!(
645            rank(&discrete, &absent, None) < rank(&integrated, &absent, None),
646            "an absent device falls back to Auto's discrete-first order",
647        );
648    }
649
650    #[test]
651    fn a_custom_preference_chooses_one_adapter() {
652        let adapters = [
653            adapter("integrated", GpuDeviceType::Integrated, 0x8086, 0x1916),
654            adapter("discrete", GpuDeviceType::Discrete, 0x10de, 0x1346),
655        ];
656
657        let choose_discrete =
658            |gpus: &[GpuAdapterInfo]| gpus.iter().position(|gpu| gpu.device_id() == Some(0x1346));
659        assert_eq!(custom_choice(&adapters, &choose_discrete).unwrap(), 1);
660
661        let chose_nothing = |_: &[GpuAdapterInfo]| None;
662        assert!(
663            custom_choice(&adapters, &chose_nothing).is_err(),
664            "choosing no adapter is an error, not a silent fallback",
665        );
666
667        let out_of_range = |_: &[GpuAdapterInfo]| Some(2);
668        let error =
669            custom_choice(&adapters, &out_of_range).expect_err("an index past the list is refused");
670        assert!(
671            error.to_string().contains("only 2 adapter(s)"),
672            "the refusal names the shortfall: {error}",
673        );
674    }
675
676    #[test]
677    fn the_default_preference_is_low_power() {
678        assert!(matches!(GpuPreference::default(), GpuPreference::LowPower));
679    }
680
681    #[test]
682    fn a_custom_preference_is_named_in_debug() {
683        let preference = GpuPreference::Custom(Arc::new(|_| Some(0)));
684        assert_eq!(format!("{preference:?}"), "Custom(..)");
685    }
686}