use std::ffi::{CString, c_void};
use core_foundation::base::{CFType, TCFType, ToVoid};
use core_foundation::data::CFData;
use core_foundation::dictionary::CFDictionary;
use core_foundation::number::CFNumber;
use core_foundation::string::CFString;
use core_foundation_sys::base::kCFAllocatorDefault;
use core_foundation_sys::dictionary::{CFDictionaryRef, CFMutableDictionaryRef};
type IoObject = u32;
type KernReturn = i32;
const IO_MAIN_PORT_DEFAULT: u32 = 0;
#[link(name = "IOKit", kind = "framework")]
unsafe extern "C" {
fn IOServiceMatching(name: *const std::ffi::c_char) -> CFMutableDictionaryRef;
fn IOServiceNameMatching(name: *const std::ffi::c_char) -> CFMutableDictionaryRef;
fn IOServiceGetMatchingServices(
main_port: u32,
matching: CFDictionaryRef,
existing: *mut IoObject,
) -> KernReturn;
fn IOIteratorNext(iterator: IoObject) -> IoObject;
fn IORegistryEntryCreateCFProperties(
entry: IoObject,
properties: *mut CFMutableDictionaryRef,
allocator: *const c_void,
options: u32,
) -> KernReturn;
fn IOObjectRelease(object: IoObject) -> KernReturn;
}
type Properties = CFDictionary<CFString, CFType>;
#[derive(Debug, Clone, Default)]
pub struct AcceleratorInfo {
pub model: Option<String>,
pub class: Option<String>,
pub gpu_core_count: Option<u32>,
pub driver_version: Option<String>,
pub utilization_pct: Option<i64>,
pub in_use_memory_bytes: Option<u64>,
}
impl AcceleratorInfo {
pub fn is_apple_gpu(&self) -> bool {
self.class.as_deref().is_some_and(|c| c.starts_with("AGX"))
}
}
pub fn accelerators() -> Vec<AcceleratorInfo> {
matching_properties(MatchBy::Class("IOAccelerator"))
.iter()
.map(decode_accelerator)
.collect()
}
pub fn gpu_dvfs_blob() -> Option<Vec<u8>> {
for props in matching_properties(MatchBy::Name("pmgr")) {
if let Some(blob) = data_value(&props, "voltage-states9") {
return Some(blob);
}
}
None
}
pub fn unified_memory_bytes() -> Option<u64> {
let mut value: u64 = 0;
let mut len = std::mem::size_of::<u64>();
let name = c"hw.memsize";
let rc = unsafe {
libc::sysctlbyname(
name.as_ptr(),
(&raw mut value).cast::<c_void>(),
&raw mut len,
std::ptr::null_mut(),
0,
)
};
(rc == 0 && len == std::mem::size_of::<u64>()).then_some(value)
}
enum MatchBy {
Class(&'static str),
Name(&'static str),
}
fn matching_properties(by: MatchBy) -> Vec<Properties> {
let mut out = Vec::new();
let (name, matcher): (
&str,
unsafe extern "C" fn(*const i8) -> CFMutableDictionaryRef,
) = match by {
MatchBy::Class(c) => (c, IOServiceMatching),
MatchBy::Name(n) => (n, IOServiceNameMatching),
};
let Ok(c_name) = CString::new(name) else {
return out;
};
unsafe {
let matching = matcher(c_name.as_ptr());
if matching.is_null() {
return out;
}
let mut iterator: IoObject = 0;
if IOServiceGetMatchingServices(IO_MAIN_PORT_DEFAULT, matching, &mut iterator) != 0 {
return out;
}
loop {
let entry = IOIteratorNext(iterator);
if entry == 0 {
break;
}
let mut properties: CFMutableDictionaryRef = std::ptr::null_mut();
let rc =
IORegistryEntryCreateCFProperties(entry, &mut properties, kCFAllocatorDefault, 0);
if rc == 0 && !properties.is_null() {
out.push(CFDictionary::wrap_under_create_rule(
properties as CFDictionaryRef,
));
}
IOObjectRelease(entry);
}
IOObjectRelease(iterator);
}
out
}
fn decode_accelerator(props: &Properties) -> AcceleratorInfo {
let stats = props
.find(CFString::new("PerformanceStatistics"))
.and_then(|v| v.downcast::<CFDictionary>());
AcceleratorInfo {
model: string_value(props, "model"),
class: string_value(props, "IOClass"),
gpu_core_count: number_value(props, "gpu-core-count").and_then(|n| u32::try_from(n).ok()),
driver_version: string_value(props, "IOSourceVersion"),
utilization_pct: stats
.as_ref()
.and_then(|s| untyped_number(s, "Device Utilization %")),
in_use_memory_bytes: stats
.as_ref()
.and_then(|s| untyped_number(s, "In use system memory"))
.and_then(|n| u64::try_from(n).ok()),
}
}
fn string_value(props: &Properties, key: &str) -> Option<String> {
let value = props.find(CFString::new(key))?;
if let Some(s) = value.downcast::<CFString>() {
return Some(s.to_string());
}
let bytes = value.downcast::<CFData>()?;
let text = String::from_utf8_lossy(bytes.bytes())
.trim_end_matches('\0')
.trim()
.to_string();
(!text.is_empty()).then_some(text)
}
fn number_value(props: &Properties, key: &str) -> Option<i64> {
props
.find(CFString::new(key))?
.downcast::<CFNumber>()?
.to_i64()
}
fn data_value(props: &Properties, key: &str) -> Option<Vec<u8>> {
Some(
props
.find(CFString::new(key))?
.downcast::<CFData>()?
.bytes()
.to_vec(),
)
}
fn untyped_number(dict: &CFDictionary, key: &str) -> Option<i64> {
let cf_key = CFString::new(key);
unsafe {
let value = dict.find(cf_key.to_void())?;
CFType::wrap_under_get_rule(*value as _)
.downcast::<CFNumber>()?
.to_i64()
}
}