1#[derive(Default, Debug, serde::Serialize, serde::Deserialize, Clone)]
5pub struct GpuSpecs {
6 pub is_software_emulated: bool,
8 pub device_name: String,
10 pub driver_name: String,
12 pub driver_info: String,
14}
15
16#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
25pub struct GpuId {
26 pub permanent: PermanentGpuId,
28 #[serde(skip)]
30 pub session: Option<SessionGpuId>,
31}
32
33impl GpuId {
34 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 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 pub const fn single() -> Self {
68 Self {
69 permanent: PermanentGpuId::Single,
70 session: Some(SessionGpuId::Default),
71 }
72 }
73
74 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 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#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
106pub enum PermanentGpuId {
107 Pci {
112 vendor_id: Option<u32>,
114 device_id: u32,
116 subsys_id: Option<u32>,
118 revision: Option<u32>,
120 },
121 Pnp(String),
124 Name(String),
127 Single,
129}
130
131impl PermanentGpuId {
132 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 pub fn vendor_id(&self) -> Option<u32> {
166 match self {
167 Self::Pci { vendor_id, .. } => *vendor_id,
168 _ => None,
169 }
170 }
171
172 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#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
187pub enum SessionGpuId {
188 Drm {
190 render_minor: u32,
192 },
193 Luid(u64),
196 Metal {
198 registry_id: u64,
200 },
201 Pci {
203 vendor_id: u32,
205 device_id: u32,
207 },
208 Default,
210}
211
212#[derive(Clone, Default)]
219pub enum GpuPreference {
220 Auto,
223 #[default]
225 LowPower,
226 HighPerformance,
228 Device(GpuId),
231 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
277pub enum GpuDeviceType {
278 Discrete,
280 Integrated,
282 Virtual,
284 Cpu,
286 Other,
288}
289
290#[derive(Clone, Copy, Debug, PartialEq, Eq)]
292pub enum GpuBackend {
293 Vulkan,
295 Metal,
297 Dx12,
299 Dx11,
301 Gl,
303 BrowserWebGpu,
305 Other,
307}
308
309#[derive(Clone, Debug, PartialEq, Eq)]
316pub struct GpuAdapterInfo {
317 pub id: GpuId,
319 pub name: String,
321 pub device_type: GpuDeviceType,
323 pub backend: GpuBackend,
325 pub is_software: bool,
327}
328
329impl GpuAdapterInfo {
330 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 pub fn single() -> Self {
349 Self::new(
350 GpuId::single(),
351 "default",
352 GpuDeviceType::Other,
353 GpuBackend::Other,
354 false,
355 )
356 }
357
358 pub fn vendor_id(&self) -> Option<u32> {
360 self.id.permanent.vendor_id()
361 }
362
363 pub fn device_id(&self) -> Option<u32> {
365 self.id.permanent.device_id()
366 }
367}
368
369#[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
393pub 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 GpuPreference::Custom(_) => GpuRank::new(0, 0, 0, 0),
441 }
442}
443
444fn 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
458pub 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}