#[derive(Default, Debug, serde::Serialize, serde::Deserialize, Clone)]
pub struct GpuSpecs {
pub is_software_emulated: bool,
pub device_name: String,
pub driver_name: String,
pub driver_info: String,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct GpuId {
pub permanent: PermanentGpuId,
#[serde(skip)]
pub session: Option<SessionGpuId>,
}
impl GpuId {
pub const fn by_device(device_id: u32) -> Self {
Self {
permanent: PermanentGpuId::Pci {
vendor_id: None,
device_id,
subsys_id: None,
revision: None,
},
session: None,
}
}
pub const fn pci(vendor_id: u32, device_id: u32) -> Self {
Self {
permanent: PermanentGpuId::Pci {
vendor_id: Some(vendor_id),
device_id,
subsys_id: None,
revision: None,
},
session: Some(SessionGpuId::Pci {
vendor_id,
device_id,
}),
}
}
pub const fn single() -> Self {
Self {
permanent: PermanentGpuId::Single,
session: Some(SessionGpuId::Default),
}
}
pub fn matches(&self, adapter: &GpuAdapterInfo) -> bool {
match &self.session {
Some(session) => adapter.id.session.as_ref() == Some(session),
None => self.permanent.matches(&adapter.id.permanent),
}
}
pub fn parse_pci_id(value: &str) -> Option<u32> {
let id = value.trim();
let id = id
.strip_prefix("0x")
.or_else(|| id.strip_prefix("0X"))
.unwrap_or(id);
if id.len() == 4 && id.chars().all(|character| character.is_ascii_hexdigit()) {
u32::from_str_radix(id, 16).ok()
} else {
None
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
pub enum PermanentGpuId {
Pci {
vendor_id: Option<u32>,
device_id: u32,
subsys_id: Option<u32>,
revision: Option<u32>,
},
Pnp(String),
Name(String),
Single,
}
impl PermanentGpuId {
pub fn matches(&self, other: &Self) -> bool {
match (self, other) {
(
Self::Pci {
vendor_id,
device_id,
subsys_id,
revision,
},
Self::Pci {
vendor_id: other_vendor,
device_id: other_device,
subsys_id: other_subsys,
revision: other_revision,
},
) => {
wildcard(*vendor_id, *other_vendor)
&& device_id == other_device
&& wildcard(*subsys_id, *other_subsys)
&& wildcard(*revision, *other_revision)
}
(Self::Pnp(want), Self::Pnp(have)) => want == have,
(Self::Name(want), Self::Name(have)) => want == have,
(Self::Single, Self::Single) => true,
_ => false,
}
}
pub fn vendor_id(&self) -> Option<u32> {
match self {
Self::Pci { vendor_id, .. } => *vendor_id,
_ => None,
}
}
pub fn device_id(&self) -> Option<u32> {
match self {
Self::Pci { device_id, .. } => Some(*device_id),
_ => None,
}
}
}
fn wildcard(want: Option<u32>, have: Option<u32>) -> bool {
want.is_none_or(|want| have == Some(want))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum SessionGpuId {
Drm {
render_minor: u32,
},
Luid(u64),
Metal {
registry_id: u64,
},
Pci {
vendor_id: u32,
device_id: u32,
},
Default,
}
#[derive(Clone, Default)]
pub enum GpuPreference {
Auto,
#[default]
LowPower,
HighPerformance,
Device(GpuId),
Custom(std::sync::Arc<dyn Fn(&[GpuAdapterInfo]) -> Option<usize> + Send + Sync>),
}
impl std::fmt::Debug for GpuPreference {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Auto => f.write_str("Auto"),
Self::LowPower => f.write_str("LowPower"),
Self::HighPerformance => f.write_str("HighPerformance"),
Self::Device(id) => f.debug_tuple("Device").field(id).finish(),
Self::Custom(_) => f.write_str("Custom(..)"),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum GpuDeviceType {
Discrete,
Integrated,
Virtual,
Cpu,
Other,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum GpuBackend {
Vulkan,
Metal,
Dx12,
Dx11,
Gl,
BrowserWebGpu,
Other,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GpuAdapterInfo {
pub id: GpuId,
pub name: String,
pub device_type: GpuDeviceType,
pub backend: GpuBackend,
pub is_software: bool,
}
impl GpuAdapterInfo {
pub fn new(
id: GpuId,
name: impl Into<String>,
device_type: GpuDeviceType,
backend: GpuBackend,
is_software: bool,
) -> Self {
Self {
id,
name: name.into(),
device_type,
backend,
is_software,
}
}
pub fn single() -> Self {
Self::new(
GpuId::single(),
"default",
GpuDeviceType::Other,
GpuBackend::Other,
false,
)
}
pub fn vendor_id(&self) -> Option<u32> {
self.id.permanent.vendor_id()
}
pub fn device_id(&self) -> Option<u32> {
self.id.permanent.device_id()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct GpuRank {
matched: u8,
preference: u8,
compositor: u8,
backend: u8,
}
impl GpuRank {
fn new(matched: u8, preference: u8, compositor: u8, backend: u8) -> Self {
Self {
matched,
preference,
compositor,
backend,
}
}
}
pub fn rank(
adapter: &GpuAdapterInfo,
preference: &GpuPreference,
compositor: Option<&PermanentGpuId>,
) -> GpuRank {
let compositor_match: u8 = match compositor {
Some(hint) if hint.matches(&adapter.id.permanent) => 0,
_ => 1,
};
let auto_type: u8 = match adapter.device_type {
GpuDeviceType::Discrete => 0,
GpuDeviceType::Integrated => 1,
GpuDeviceType::Other => 2,
GpuDeviceType::Virtual => 3,
GpuDeviceType::Cpu => 4,
};
let backend_priority: u8 = match adapter.backend {
GpuBackend::Vulkan | GpuBackend::Metal | GpuBackend::Dx12 | GpuBackend::Dx11 => 0,
_ => 1,
};
match preference {
GpuPreference::Auto => GpuRank::new(0, compositor_match, auto_type, backend_priority),
GpuPreference::Device(gpu_id) => GpuRank::new(
if gpu_id.matches(adapter) { 0 } else { 1 },
compositor_match,
auto_type,
backend_priority,
),
GpuPreference::LowPower => GpuRank::new(
0,
type_priority(adapter.device_type, true),
compositor_match,
backend_priority,
),
GpuPreference::HighPerformance => GpuRank::new(
0,
type_priority(adapter.device_type, false),
compositor_match,
backend_priority,
),
GpuPreference::Custom(_) => GpuRank::new(0, 0, 0, 0),
}
}
fn type_priority(device_type: GpuDeviceType, low_power: bool) -> u8 {
match (device_type, low_power) {
(GpuDeviceType::Integrated, true) => 0,
(GpuDeviceType::Discrete, false) => 0,
(GpuDeviceType::Other, _) => 1,
(GpuDeviceType::Integrated, false) => 2,
(GpuDeviceType::Discrete, true) => 2,
(GpuDeviceType::Virtual, _) => 3,
(GpuDeviceType::Cpu, _) => 4,
}
}
pub fn custom_choice(
adapters: &[GpuAdapterInfo],
selector: &dyn Fn(&[GpuAdapterInfo]) -> Option<usize>,
) -> anyhow::Result<usize> {
match selector(adapters) {
Some(index) if index < adapters.len() => Ok(index),
Some(index) => anyhow::bail!(
"The application's custom GPU preference chose adapter {index}, but only {} adapter(s) \
were enumerated",
adapters.len()
),
None => anyhow::bail!("The application's custom GPU preference chose no adapter"),
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::{
GpuAdapterInfo, GpuBackend, GpuDeviceType, GpuId, GpuPreference, PermanentGpuId,
SessionGpuId, custom_choice, rank,
};
fn adapter(name: &str, device_type: GpuDeviceType, vendor: u32, device: u32) -> GpuAdapterInfo {
GpuAdapterInfo::new(
GpuId::pci(vendor, device),
name,
device_type,
GpuBackend::Vulkan,
false,
)
}
fn hint(vendor: u32, device: u32) -> PermanentGpuId {
PermanentGpuId::Pci {
vendor_id: Some(vendor),
device_id: device,
subsys_id: None,
revision: None,
}
}
#[test]
fn the_pci_id_is_four_hex_digits_with_an_optional_prefix() {
assert_eq!(GpuId::parse_pci_id("0xABCD"), Some(0xabcd));
assert_eq!(GpuId::parse_pci_id("ABCD"), Some(0xabcd));
assert_eq!(GpuId::parse_pci_id("abcd"), Some(0xabcd));
assert_eq!(GpuId::parse_pci_id("1234"), Some(0x1234));
assert_eq!(GpuId::parse_pci_id(" 0x1346 "), Some(0x1346));
assert_eq!(GpuId::parse_pci_id("123"), None);
assert_eq!(GpuId::parse_pci_id("0x12345"), None);
assert_eq!(GpuId::parse_pci_id("zzzz"), None);
}
#[test]
fn a_device_id_matches_by_device_and_leaves_the_vendor_open() {
let integrated = adapter("integrated", GpuDeviceType::Integrated, 0x8086, 0x1916);
let discrete = adapter("discrete", GpuDeviceType::Discrete, 0x10de, 0x1346);
let by_device = GpuId::by_device(0x1346);
assert!(by_device.matches(&discrete));
assert!(!by_device.matches(&integrated));
let exact = GpuId {
permanent: hint(0x10de, 0x1346),
session: None,
};
assert!(exact.matches(&discrete));
assert!(!exact.matches(&integrated));
}
#[test]
fn a_permanent_id_survives_a_restart_and_a_session_does_not() {
let mut enumerated = adapter("discrete", GpuDeviceType::Discrete, 0x10de, 0x1346);
let stored = GpuId {
permanent: enumerated.id.permanent.clone(),
session: None,
};
assert!(
stored.matches(&enumerated),
"the permanent part still names the card after the session is gone"
);
enumerated.id.session = Some(SessionGpuId::Luid(1));
let stale = GpuId {
permanent: enumerated.id.permanent.clone(),
session: Some(SessionGpuId::Luid(2)),
};
assert!(
!stale.matches(&enumerated),
"a session from another boot is not the same card"
);
}
#[test]
fn a_gpu_id_serialises_without_its_session() {
let enumerated = adapter("discrete", GpuDeviceType::Discrete, 0x10de, 0x1346);
let json = serde_json::to_value(&enumerated.id).expect("serialise a GpuId");
assert!(
json.get("session").is_none(),
"the session is per process and must not reach a config: {json}"
);
let restored: GpuId = serde_json::from_value(json).expect("deserialise a GpuId");
assert_eq!(restored.session, None);
assert!(restored.matches(&enumerated));
}
#[test]
fn a_single_gpu_platform_reports_one_identity() {
let only = GpuAdapterInfo::single();
assert_eq!(only.id.permanent, PermanentGpuId::Single);
assert_eq!(only.id.session, Some(SessionGpuId::Default));
assert!(GpuId::single().matches(&only));
assert!(
GpuId {
permanent: PermanentGpuId::Single,
session: None,
}
.matches(&only)
);
}
#[test]
fn the_preference_orders_the_adapters_it_names() {
use super::GpuPreference;
let integrated = adapter("integrated", GpuDeviceType::Integrated, 0x8086, 0x1916);
let discrete = adapter("discrete", GpuDeviceType::Discrete, 0x10de, 0x1346);
let on_the_discrete = hint(0x10de, 0x1346);
assert!(
rank(
&integrated,
&GpuPreference::LowPower,
Some(&on_the_discrete)
) < rank(&discrete, &GpuPreference::LowPower, Some(&on_the_discrete)),
"LowPower takes the integrated adapter even when the compositor is on the discrete one",
);
assert!(
rank(
&discrete,
&GpuPreference::HighPerformance,
Some(&on_the_discrete)
) < rank(
&integrated,
&GpuPreference::HighPerformance,
Some(&on_the_discrete)
),
"HighPerformance takes the discrete adapter",
);
assert!(
rank(&discrete, &GpuPreference::Auto, None)
< rank(&integrated, &GpuPreference::Auto, None),
"Auto keeps the historical discrete-first order",
);
assert!(
rank(
&integrated,
&GpuPreference::Auto,
Some(&hint(0x8086, 0x1916))
) < rank(&discrete, &GpuPreference::Auto, Some(&hint(0x8086, 0x1916))),
"Auto still follows the compositor's hint",
);
}
#[test]
fn a_named_device_ranks_first_and_an_absent_one_falls_back() {
let integrated = adapter("integrated", GpuDeviceType::Integrated, 0x8086, 0x1916);
let discrete = adapter("discrete", GpuDeviceType::Discrete, 0x10de, 0x1346);
let named = GpuPreference::Device(GpuId {
permanent: hint(0x8086, 0x1916),
session: None,
});
assert!(rank(&integrated, &named, None) < rank(&discrete, &named, None));
let absent = GpuPreference::Device(GpuId::by_device(0x9999));
assert!(
rank(&discrete, &absent, None) < rank(&integrated, &absent, None),
"an absent device falls back to Auto's discrete-first order",
);
}
#[test]
fn a_custom_preference_chooses_one_adapter() {
let adapters = [
adapter("integrated", GpuDeviceType::Integrated, 0x8086, 0x1916),
adapter("discrete", GpuDeviceType::Discrete, 0x10de, 0x1346),
];
let choose_discrete =
|gpus: &[GpuAdapterInfo]| gpus.iter().position(|gpu| gpu.device_id() == Some(0x1346));
assert_eq!(custom_choice(&adapters, &choose_discrete).unwrap(), 1);
let chose_nothing = |_: &[GpuAdapterInfo]| None;
assert!(
custom_choice(&adapters, &chose_nothing).is_err(),
"choosing no adapter is an error, not a silent fallback",
);
let out_of_range = |_: &[GpuAdapterInfo]| Some(2);
let error =
custom_choice(&adapters, &out_of_range).expect_err("an index past the list is refused");
assert!(
error.to_string().contains("only 2 adapter(s)"),
"the refusal names the shortfall: {error}",
);
}
#[test]
fn the_default_preference_is_low_power() {
assert!(matches!(GpuPreference::default(), GpuPreference::LowPower));
}
#[test]
fn a_custom_preference_is_named_in_debug() {
let preference = GpuPreference::Custom(Arc::new(|_| Some(0)));
assert_eq!(format!("{preference:?}"), "Custom(..)");
}
}