use std::collections::HashMap;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct AdapterLuid {
pub high: i32,
pub low: u32,
}
impl AdapterLuid {
#[cfg_attr(not(test), allow(dead_code))]
pub fn new(high: i32, low: u32) -> Self {
Self { high, low }
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct PciIds {
pub vendor: u32,
pub device: u32,
}
#[derive(Clone, Debug, PartialEq)]
pub struct GpuEngineInstance {
pub pid: u32,
pub luid: AdapterLuid,
pub phys: u32,
pub eng: u32,
pub engtype: String,
}
#[derive(Clone, Debug, PartialEq)]
pub struct GpuProcessMemoryInstance {
pub pid: u32,
pub luid: AdapterLuid,
pub phys: u32,
}
#[derive(Clone, Debug, PartialEq)]
pub struct GpuAdapterMemoryInstance {
pub luid: AdapterLuid,
pub phys: u32,
}
#[derive(Clone, Debug, PartialEq)]
pub struct AdapterIdentity {
pub luid: AdapterLuid,
pub vendor_id: u32,
pub device_id: u32,
pub description: String,
}
fn expect<'t, 's>(tokens: &'t [&'s str], expected: &str) -> Option<&'t [&'s str]> {
match tokens.split_first() {
Some((first, rest)) if first.eq_ignore_ascii_case(expected) => Some(rest),
_ => None,
}
}
fn take<'t, 's>(tokens: &'t [&'s str]) -> Option<(&'s str, &'t [&'s str])> {
tokens.split_first().map(|(first, rest)| (*first, rest))
}
fn parse_hex_u32(text: &str) -> Option<u32> {
let body = text
.strip_prefix("0x")
.or_else(|| text.strip_prefix("0X"))
.unwrap_or(text);
if body.is_empty() {
return None;
}
u32::from_str_radix(body, 16).ok()
}
fn parse_luid_pair(high: &str, low: &str) -> Option<AdapterLuid> {
Some(AdapterLuid {
high: parse_hex_u32(high)? as i32,
low: parse_hex_u32(low)?,
})
}
pub fn parse_gpu_engine_instance(instance: &str) -> Option<GpuEngineInstance> {
let tokens: Vec<&str> = instance.split('_').collect();
let rest = expect(&tokens, "pid")?;
let (pid, rest) = take(rest)?;
let rest = expect(rest, "luid")?;
let (high, rest) = take(rest)?;
let (low, rest) = take(rest)?;
let rest = expect(rest, "phys")?;
let (phys, rest) = take(rest)?;
let rest = expect(rest, "eng")?;
let (eng, rest) = take(rest)?;
let rest = expect(rest, "engtype")?;
let engtype = rest.join("_");
if engtype.is_empty() {
return None;
}
Some(GpuEngineInstance {
pid: pid.parse().ok()?,
luid: parse_luid_pair(high, low)?,
phys: phys.parse().ok()?,
eng: eng.parse().ok()?,
engtype,
})
}
pub fn parse_gpu_process_memory_instance(instance: &str) -> Option<GpuProcessMemoryInstance> {
let tokens: Vec<&str> = instance.split('_').collect();
let rest = expect(&tokens, "pid")?;
let (pid, rest) = take(rest)?;
let rest = expect(rest, "luid")?;
let (high, rest) = take(rest)?;
let (low, rest) = take(rest)?;
let rest = expect(rest, "phys")?;
let (phys, _rest) = take(rest)?;
Some(GpuProcessMemoryInstance {
pid: pid.parse().ok()?,
luid: parse_luid_pair(high, low)?,
phys: phys.parse().ok()?,
})
}
pub fn parse_gpu_adapter_memory_instance(instance: &str) -> Option<GpuAdapterMemoryInstance> {
let tokens: Vec<&str> = instance.split('_').collect();
let rest = expect(&tokens, "luid")?;
let (high, rest) = take(rest)?;
let (low, rest) = take(rest)?;
let rest = expect(rest, "phys")?;
let (phys, _rest) = take(rest)?;
Some(GpuAdapterMemoryInstance {
luid: parse_luid_pair(high, low)?,
phys: phys.parse().ok()?,
})
}
pub fn parse_pnp_device_id(pnp: &str) -> Option<PciIds> {
let upper = pnp.to_ascii_uppercase();
Some(PciIds {
vendor: extract_hex_after(&upper, "VEN_")?,
device: extract_hex_after(&upper, "DEV_")?,
})
}
fn extract_hex_after(haystack: &str, marker: &str) -> Option<u32> {
let start = haystack.find(marker)? + marker.len();
let digits: String = haystack[start..]
.chars()
.take_while(|c| c.is_ascii_hexdigit())
.collect();
if digits.is_empty() {
return None;
}
u32::from_str_radix(&digits, 16).ok()
}
pub const UTILIZATION_ENGINE_TYPES: &[&str] = &["3D", "Compute"];
pub fn is_utilization_engine_type(engtype: &str) -> bool {
UTILIZATION_ENGINE_TYPES
.iter()
.any(|candidate| engtype.eq_ignore_ascii_case(candidate))
}
pub fn aggregate_engine_utilization(
samples: impl IntoIterator<Item = (GpuEngineInstance, f64)>,
) -> HashMap<AdapterLuid, f64> {
let mut per_engine: HashMap<(AdapterLuid, u32, u32, String), f64> = HashMap::new();
for (instance, value) in samples {
if !is_utilization_engine_type(&instance.engtype) {
continue;
}
if !value.is_finite() {
continue;
}
let key = (
instance.luid,
instance.phys,
instance.eng,
instance.engtype.to_ascii_uppercase(),
);
*per_engine.entry(key).or_insert(0.0) += value;
}
let mut per_adapter: HashMap<AdapterLuid, f64> = HashMap::new();
for ((luid, _, _, _), busy) in per_engine {
let slot = per_adapter.entry(luid).or_insert(0.0);
if busy > *slot {
*slot = busy;
}
}
for value in per_adapter.values_mut() {
*value = value.clamp(0.0, 100.0);
}
per_adapter
}
pub fn aggregate_adapter_memory(
samples: impl IntoIterator<Item = (GpuAdapterMemoryInstance, f64)>,
) -> HashMap<AdapterLuid, u64> {
let mut per_adapter: HashMap<AdapterLuid, f64> = HashMap::new();
for (instance, value) in samples {
if !value.is_finite() || value < 0.0 {
continue;
}
*per_adapter.entry(instance.luid).or_insert(0.0) += value;
}
per_adapter
.into_iter()
.map(|(luid, bytes)| (luid, bytes as u64))
.collect()
}
pub fn match_adapter<'a>(
adapters: &'a [AdapterIdentity],
pnp_device_id: Option<&str>,
name: &str,
ordinal: usize,
) -> Option<&'a AdapterIdentity> {
if adapters.is_empty() {
return None;
}
let ids = pnp_device_id.and_then(parse_pnp_device_id);
if let Some(ids) = ids {
let exact: Vec<&AdapterIdentity> = adapters
.iter()
.filter(|a| a.vendor_id == ids.vendor && a.device_id == ids.device)
.collect();
match exact.len() {
0 => {}
1 => return Some(exact[0]),
_ => return Some(exact.get(ordinal).copied().unwrap_or(exact[0])),
}
}
let ids = ids?;
let same_vendor: Vec<&AdapterIdentity> = adapters
.iter()
.filter(|a| a.vendor_id == ids.vendor)
.collect();
if same_vendor.is_empty() {
return None;
}
let trimmed = name.trim();
if !trimmed.is_empty() {
if let Some(hit) = same_vendor
.iter()
.find(|a| a.description.trim().eq_ignore_ascii_case(trimmed))
{
return Some(hit);
}
let lowered = trimmed.to_lowercase();
if let Some(hit) = same_vendor.iter().find(|a| {
let description = a.description.trim().to_lowercase();
!description.is_empty()
&& (description.contains(&lowered) || lowered.contains(&description))
}) {
return Some(hit);
}
}
same_vendor.get(ordinal).copied()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_a_real_gpu_engine_instance_name() {
let parsed = parse_gpu_engine_instance(
"pid_9540_luid_0x00000000_0x0000D3F5_phys_0_eng_3_engtype_3D",
)
.expect("well-formed instance name should parse");
assert_eq!(parsed.pid, 9540);
assert_eq!(parsed.luid, AdapterLuid::new(0, 0xD3F5));
assert_eq!(parsed.phys, 0);
assert_eq!(parsed.eng, 3);
assert_eq!(parsed.engtype, "3D");
}
#[test]
fn parses_multiword_engine_types() {
let parsed = parse_gpu_engine_instance(
"pid_1_luid_0x00000000_0x00001234_phys_0_eng_1_engtype_VideoDecode",
)
.unwrap();
assert_eq!(parsed.engtype, "VideoDecode");
assert!(!is_utilization_engine_type(&parsed.engtype));
}
#[test]
fn preserves_a_negative_luid_high_part() {
let parsed = parse_gpu_engine_instance(
"pid_7_luid_0xFFFFFFFF_0x0000ABCD_phys_0_eng_0_engtype_Compute",
)
.unwrap();
assert_eq!(parsed.luid, AdapterLuid::new(-1, 0xABCD));
}
#[test]
fn rejects_malformed_engine_instance_names() {
for bad in [
"",
"pid_9540",
"pid_abc_luid_0x0_0x1_phys_0_eng_0_engtype_3D",
"luid_0x00000000_0x0000D3F5_phys_0",
"pid_1_luid_0x0_0x1_phys_0_eng_0_engtype_",
"pid_1_luid_0xZZ_0x1_phys_0_eng_0_engtype_3D",
] {
assert!(
parse_gpu_engine_instance(bad).is_none(),
"expected {bad:?} to be rejected"
);
}
}
#[test]
fn parses_process_and_adapter_memory_instances() {
let process =
parse_gpu_process_memory_instance("pid_4242_luid_0x00000000_0x0000D3F5_phys_0")
.unwrap();
assert_eq!(process.pid, 4242);
assert_eq!(process.luid, AdapterLuid::new(0, 0xD3F5));
let adapter =
parse_gpu_adapter_memory_instance("luid_0x00000000_0x0000D3F5_phys_0").unwrap();
assert_eq!(adapter.luid, AdapterLuid::new(0, 0xD3F5));
assert_eq!(adapter.phys, 0);
assert!(parse_gpu_adapter_memory_instance("pid_1_luid_0x0_0x1_phys_0").is_none());
assert!(parse_gpu_process_memory_instance("luid_0x0_0x1_phys_0").is_none());
}
#[test]
fn parses_pnp_device_ids() {
let ids =
parse_pnp_device_id(r"PCI\VEN_1002&DEV_744C&SUBSYS_00000000&REV_C8\6&1a2b&0&00000019")
.unwrap();
assert_eq!(ids.vendor, 0x1002);
assert_eq!(ids.device, 0x744C);
let intel = parse_pnp_device_id(r"pci\ven_8086&dev_56a0&subsys_00000000").unwrap();
assert_eq!(intel.vendor, 0x8086);
assert_eq!(intel.device, 0x56A0);
assert!(parse_pnp_device_id(r"ROOT\BasicDisplay\0000").is_none());
assert!(parse_pnp_device_id("").is_none());
}
fn engine(pid: u32, luid: u32, eng: u32, engtype: &str) -> GpuEngineInstance {
GpuEngineInstance {
pid,
luid: AdapterLuid::new(0, luid),
phys: 0,
eng,
engtype: engtype.to_string(),
}
}
#[test]
fn sums_processes_within_an_engine_and_maxes_across_engines() {
let samples = vec![
(engine(100, 0xAAAA, 0, "3D"), 30.0),
(engine(200, 0xAAAA, 0, "3D"), 25.0),
(engine(100, 0xAAAA, 1, "Compute"), 40.0),
];
let aggregated = aggregate_engine_utilization(samples);
assert_eq!(aggregated.len(), 1);
let value = aggregated[&AdapterLuid::new(0, 0xAAAA)];
assert!((value - 55.0).abs() < f64::EPSILON, "got {value}");
}
#[test]
fn keeps_adapters_separate_and_ignores_non_compute_engines() {
let samples = vec![
(engine(1, 0xAAAA, 0, "3D"), 70.0),
(engine(1, 0xBBBB, 0, "3D"), 10.0),
(engine(1, 0xBBBB, 1, "VideoDecode"), 95.0),
(engine(1, 0xBBBB, 2, "Copy"), 88.0),
];
let aggregated = aggregate_engine_utilization(samples);
assert_eq!(aggregated[&AdapterLuid::new(0, 0xAAAA)], 70.0);
assert_eq!(aggregated[&AdapterLuid::new(0, 0xBBBB)], 10.0);
}
#[test]
fn clamps_out_of_range_and_drops_non_finite_samples() {
let samples = vec![
(engine(1, 0xAAAA, 0, "3D"), 80.0),
(engine(2, 0xAAAA, 0, "3D"), 80.0), (engine(1, 0xBBBB, 0, "3D"), -5.0), (engine(1, 0xCCCC, 0, "3D"), f64::NAN),
(engine(1, 0xCCCC, 0, "3D"), f64::INFINITY),
];
let aggregated = aggregate_engine_utilization(samples);
assert_eq!(aggregated[&AdapterLuid::new(0, 0xAAAA)], 100.0);
assert_eq!(aggregated[&AdapterLuid::new(0, 0xBBBB)], 0.0);
assert!(!aggregated.contains_key(&AdapterLuid::new(0, 0xCCCC)));
}
#[test]
fn sums_adapter_memory_across_segments() {
let samples = vec![
(
GpuAdapterMemoryInstance {
luid: AdapterLuid::new(0, 0xAAAA),
phys: 0,
},
1024.0,
),
(
GpuAdapterMemoryInstance {
luid: AdapterLuid::new(0, 0xAAAA),
phys: 1,
},
2048.0,
),
];
let aggregated = aggregate_adapter_memory(samples);
assert_eq!(aggregated[&AdapterLuid::new(0, 0xAAAA)], 3072);
}
fn identity(luid: u32, vendor: u32, device: u32, description: &str) -> AdapterIdentity {
AdapterIdentity {
luid: AdapterLuid::new(0, luid),
vendor_id: vendor,
device_id: device,
description: description.to_string(),
}
}
#[test]
fn matches_on_pci_ids_before_anything_else() {
let adapters = vec![
identity(1, 0x8086, 0x56A0, "Intel(R) Arc(TM) A770 Graphics"),
identity(2, 0x1002, 0x744C, "AMD Radeon RX 7900 XTX"),
];
let hit = match_adapter(
&adapters,
Some(r"PCI\VEN_1002&DEV_744C&SUBSYS_0&REV_C8"),
"Intel(R) Arc(TM) A770 Graphics",
0,
)
.unwrap();
assert_eq!(hit.luid, AdapterLuid::new(0, 2));
}
#[test]
fn disambiguates_identical_cards_by_ordinal() {
let adapters = vec![
identity(1, 0x1002, 0x744C, "AMD Radeon RX 7900 XTX"),
identity(2, 0x1002, 0x744C, "AMD Radeon RX 7900 XTX"),
];
let pnp = Some(r"PCI\VEN_1002&DEV_744C");
assert_eq!(
match_adapter(&adapters, pnp, "AMD Radeon RX 7900 XTX", 0)
.unwrap()
.luid,
AdapterLuid::new(0, 1)
);
assert_eq!(
match_adapter(&adapters, pnp, "AMD Radeon RX 7900 XTX", 1)
.unwrap()
.luid,
AdapterLuid::new(0, 2)
);
}
#[test]
fn falls_back_to_name_within_the_same_vendor() {
let adapters = vec![
identity(1, 0x8086, 0x56A0, "Intel(R) Arc(TM) A770 Graphics"),
identity(2, 0x1002, 0x1234, "AMD Radeon RX 7900 XTX"),
];
let hit = match_adapter(
&adapters,
Some(r"PCI\VEN_1002&DEV_744C"),
"AMD Radeon RX 7900 XTX",
0,
)
.unwrap();
assert_eq!(hit.luid, AdapterLuid::new(0, 2));
}
#[test]
fn never_pairs_across_vendors() {
let adapters = vec![
identity(1, 0x8086, 0x56A0, "Intel(R) Arc(TM) A770 Graphics"),
identity(2, 0x10DE, 0x2684, "NVIDIA GeForce RTX 4090"),
];
assert!(
match_adapter(
&adapters,
Some(r"PCI\VEN_1002&DEV_744C"),
"AMD Radeon RX 7900 XTX",
0
)
.is_none()
);
}
#[test]
fn declines_to_guess_without_a_vendor() {
let adapters = vec![identity(1, 0x1002, 0x744C, "AMD Radeon RX 7900 XTX")];
assert!(match_adapter(&adapters, None, "AMD Radeon RX 7900 XTX", 0).is_none());
assert!(match_adapter(&adapters, Some("AMD-GPU-0"), "AMD Radeon RX 7900 XTX", 0).is_none());
}
#[test]
fn an_empty_adapter_description_does_not_swallow_every_row() {
let adapters = vec![
identity(1, 0x1002, 0x1111, ""),
identity(2, 0x1002, 0x2222, "AMD Radeon RX 7900 XTX"),
];
let hit = match_adapter(
&adapters,
Some(r"PCI\VEN_1002&DEV_744C"),
"AMD Radeon RX 7900 XTX",
9,
)
.unwrap();
assert_eq!(hit.luid, AdapterLuid::new(0, 2));
}
#[test]
fn out_of_range_and_empty_inputs_yield_nothing() {
let adapters = vec![identity(1, 0x1002, 0x744C, "AMD Radeon RX 7900 XTX")];
assert!(match_adapter(&adapters, Some(r"PCI\VEN_1002&DEV_9999"), "Unknown", 9).is_none());
assert!(match_adapter(&[], None, "anything", 0).is_none());
}
}