// SPDX-License-Identifier: AGPL-3.0-or-later use napi_derive::napi; pub const SOURCE_METAL: &str = "metal"; pub const SOURCE_DXGI: &str = "dxgi"; pub const SOURCE_LINUX_SYSFS: &str = "linux-sysfs"; #[napi(object)] pub struct PlatformGpuDeviceInfo { pub active: bool, #[napi(js_name = "vendorId")] pub vendor_id: u32, #[napi(js_name = "deviceId")] pub device_id: u32, #[napi(js_name = "vendorName")] pub vendor_name: Option, #[napi(js_name = "deviceString")] pub device_string: Option, #[napi(js_name = "driverVendor")] pub driver_vendor: Option, #[napi(js_name = "driverVersion")] pub driver_version: Option, #[napi(js_name = "dedicatedVideoMemory")] pub dedicated_video_memory: Option, #[napi(js_name = "sharedSystemMemory")] pub shared_system_memory: Option, #[napi(js_name = "subsystemVendorId")] pub subsystem_vendor_id: Option, #[napi(js_name = "subsystemDeviceId")] pub subsystem_device_id: Option, #[napi(js_name = "registryId")] pub registry_id: Option, #[napi(js_name = "adapterLuid")] pub adapter_luid: Option, #[napi(js_name = "pciPath")] pub pci_path: Option, pub integrated: Option, pub removable: Option, pub headless: Option, pub source: String, } #[napi(object)] pub struct PlatformGpuInfo { pub devices: Vec, pub source: String, pub error: Option, } #[napi(js_name = "getGpuInfo")] pub fn get_gpu_info() -> PlatformGpuInfo { platform::gpu_info() } #[allow(dead_code)] fn base_device( active: bool, vendor_id: u32, device_id: u32, source: &str, ) -> PlatformGpuDeviceInfo { PlatformGpuDeviceInfo { active, vendor_id, device_id, vendor_name: fluxer_desktop_native::platform_info::probe_helpers::vendor_name(vendor_id) .map(str::to_owned), device_string: None, driver_vendor: None, driver_version: None, dedicated_video_memory: None, shared_system_memory: None, subsystem_vendor_id: None, subsystem_device_id: None, registry_id: None, adapter_luid: None, pci_path: None, integrated: None, removable: None, headless: None, source: source.to_owned(), } } #[allow(dead_code)] fn empty_info(source: &str) -> PlatformGpuInfo { PlatformGpuInfo { devices: Vec::new(), source: source.to_owned(), error: None, } } #[cfg(target_os = "macos")] mod platform { use fluxer_desktop_native::platform_info::probe_helpers::{ vendor_id_from_name, vendor_name, write_hex_u64, }; use objc2::exception::catch; use objc2::rc::autoreleasepool; use objc2_metal::{MTLCopyAllDevices, MTLCreateSystemDefaultDevice, MTLDevice}; use super::{PlatformGpuDeviceInfo, PlatformGpuInfo, SOURCE_METAL, empty_info}; pub(super) fn gpu_info() -> PlatformGpuInfo { match catch(|| autoreleasepool(|_| gpu_info_inner())) { Ok(info) => info, Err(exception) => { let mut info = empty_info(SOURCE_METAL); info.error = Some(exception.map_or_else( || "macOS GPU probe failed".to_owned(), |exception| exception.to_string(), )); info } } } fn gpu_info_inner() -> PlatformGpuInfo { let default_device = MTLCreateSystemDefaultDevice(); let default_registry_id = default_device.as_deref().map(MTLDevice::registryID); let all_devices = MTLCopyAllDevices(); let mut devices = Vec::new(); for index in 0..all_devices.count() { let device = all_devices.objectAtIndex(index); let active = default_registry_id.is_some_and(|registry_id| registry_id == device.registryID()); devices.push(mac_device(&device, active)); } if devices.is_empty() { if let Some(device) = default_device { devices.push(mac_device(&device, true)); } } else if !devices.iter().any(|device| device.active) { devices[0].active = true; } PlatformGpuInfo { devices, source: SOURCE_METAL.to_owned(), error: None, } } fn mac_device( device: &objc2::runtime::ProtocolObject, active: bool, ) -> PlatformGpuDeviceInfo { let name = device.name().to_string(); let vendor_id = vendor_id_from_name(&name); PlatformGpuDeviceInfo { active, vendor_id, device_id: 0, vendor_name: vendor_name(vendor_id).map(str::to_owned), device_string: Some(if name.is_empty() { "Metal GPU".to_owned() } else { name }), driver_vendor: None, driver_version: None, dedicated_video_memory: None, shared_system_memory: None, subsystem_vendor_id: None, subsystem_device_id: None, registry_id: Some(write_hex_u64(device.registryID())), adapter_luid: None, pci_path: None, integrated: Some(device.isLowPower() || vendor_id == 0x106b), removable: Some(device.isRemovable()), headless: Some(device.isHeadless()), source: SOURCE_METAL.to_owned(), } } } #[allow(dead_code)] mod dxgi_helpers { pub fn utf16_description(raw: &[u16; 128]) -> String { let end = raw.iter().position(|ch| *ch == 0).unwrap_or(raw.len()); let text = String::from_utf16_lossy(&raw[..end]); if text.is_empty() { "DXGI adapter".to_owned() } else { text } } pub fn luid_string(high_part: i32, low_part: u32) -> String { format!("{:08x}:{low_part:08x}", high_part as u32) } #[cfg(test)] mod tests { use super::*; #[test] fn luid_format_matches_legacy_contract() { assert_eq!("ffffffff:1234abcd", luid_string(-1, 0x1234_abcd)); assert_eq!("00000002:00000001", luid_string(2, 1)); } #[test] fn utf16_description_trims_at_first_nul() { let mut raw = [0u16; 128]; raw[0] = 'G' as u16; raw[1] = 'P' as u16; raw[2] = 'U' as u16; raw[3] = 0; raw[4] = 'X' as u16; assert_eq!("GPU", utf16_description(&raw)); } } } #[cfg(target_os = "windows")] mod platform { use super::dxgi_helpers::{luid_string, utf16_description}; use fluxer_desktop_native::platform_info::probe_helpers::vendor_name; use windows::Win32::Graphics::Dxgi::{ CreateDXGIFactory1, DXGI_ADAPTER_DESC1, DXGI_ADAPTER_FLAG_SOFTWARE, DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE, IDXGIAdapter1, IDXGIFactory1, IDXGIFactory6, }; use super::{PlatformGpuDeviceInfo, PlatformGpuInfo, SOURCE_DXGI, empty_info}; pub(super) fn gpu_info() -> PlatformGpuInfo { let mut info = empty_info(SOURCE_DXGI); info.devices = enumerate_dxgi_adapters(); info } fn enumerate_dxgi_adapters() -> Vec { unsafe { if let Ok(factory) = CreateDXGIFactory1::() { return enumerate_factory6(&factory); } if let Ok(factory) = CreateDXGIFactory1::() { return enumerate_factory1(&factory); } } Vec::new() } unsafe fn enumerate_factory6(factory: &IDXGIFactory6) -> Vec { let mut devices = Vec::new(); let mut ordered_index = 0; loop { let Ok(adapter) = (unsafe { factory.EnumAdapterByGpuPreference::( ordered_index, DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE, ) }) else { break; }; if let Ok(desc) = unsafe { adapter.GetDesc1() } { push_dxgi_desc(&mut devices, &desc, ordered_index); } ordered_index += 1; } devices } unsafe fn enumerate_factory1(factory: &IDXGIFactory1) -> Vec { let mut devices = Vec::new(); let mut ordered_index = 0; loop { let Ok(adapter) = (unsafe { factory.EnumAdapters1(ordered_index) }) else { break; }; if let Ok(desc) = unsafe { adapter.GetDesc1() } { push_dxgi_desc(&mut devices, &desc, ordered_index); } ordered_index += 1; } devices } fn push_dxgi_desc( devices: &mut Vec, desc: &DXGI_ADAPTER_DESC1, ordered_index: u32, ) { if (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE.0 as u32) != 0 { return; } let mut device = PlatformGpuDeviceInfo { active: ordered_index == 0, vendor_id: desc.VendorId, device_id: desc.DeviceId, vendor_name: vendor_name(desc.VendorId).map(str::to_owned), device_string: Some(utf16_description(&desc.Description)), driver_vendor: None, driver_version: None, dedicated_video_memory: Some(desc.DedicatedVideoMemory as f64), shared_system_memory: Some(desc.SharedSystemMemory as f64), subsystem_vendor_id: Some(desc.SubSysId >> 16), subsystem_device_id: Some(desc.SubSysId & 0xffff), registry_id: None, adapter_luid: None, pci_path: None, integrated: None, removable: None, headless: None, source: SOURCE_DXGI.to_owned(), }; device.adapter_luid = Some(luid_string( desc.AdapterLuid.HighPart, desc.AdapterLuid.LowPart, )); devices.push(device); } } #[cfg(target_os = "linux")] mod platform { use std::fs; use std::path::Path; use fluxer_desktop_native::platform_info::probe_helpers::{ basename, is_drm_card_name, parse_hex_id, vendor_name, }; use super::{PlatformGpuDeviceInfo, PlatformGpuInfo, SOURCE_LINUX_SYSFS, base_device}; pub(super) fn gpu_info() -> PlatformGpuInfo { linux_gpu_info_from_drm_root(Path::new("/sys/class/drm")) } fn linux_gpu_info_from_drm_root(root: &Path) -> PlatformGpuInfo { let mut info = PlatformGpuInfo { devices: Vec::new(), source: SOURCE_LINUX_SYSFS.to_owned(), error: None, }; let Ok(entries) = fs::read_dir(root) else { return info; }; let mut card_names = entries .filter_map(Result::ok) .filter_map(|entry| entry.file_name().into_string().ok()) .filter(|name| is_drm_card_name(name)) .collect::>(); card_names.sort_unstable_by_key(|name| card_sort_key(name)); for card_name in card_names { if let Some(device) = linux_card(root, &card_name, info.devices.is_empty()) { info.devices.push(device); } } info } fn linux_card(root: &Path, card_name: &str, active: bool) -> Option { let base = root.join(card_name).join("device"); let vendor_id = read_hex_file(base.join("vendor"))?; let device_id = read_hex_file(base.join("device")).unwrap_or(0); let mut device = base_device(active, vendor_id, device_id, SOURCE_LINUX_SYSFS); device.subsystem_vendor_id = Some(read_hex_file(base.join("subsystem_vendor")).unwrap_or(0)); device.subsystem_device_id = Some(read_hex_file(base.join("subsystem_device")).unwrap_or(0)); device.pci_path = read_link_basename(&base); device.driver_vendor = read_link_basename(base.join("driver")); if let Some(uevent) = read_trimmed_file(base.join("uevent")) { if let Some(driver) = uevent_value(&uevent, "DRIVER") { device.driver_vendor = Some(driver.to_owned()); } if let Some(pci_id) = uevent_value(&uevent, "PCI_ID") { device.device_string = Some(match vendor_name(vendor_id) { Some(vendor) => format!("{vendor} GPU ({pci_id})"), None => format!("GPU ({pci_id})"), }); } } if let Some(vram) = read_trimmed_file(base.join("mem_info_vram_total")) && let Ok(bytes) = vram.parse::() { device.dedicated_video_memory = Some(bytes as f64); } Some(device) } fn card_sort_key(name: &str) -> u32 { name.strip_prefix("card") .and_then(|rest| rest.parse::().ok()) .unwrap_or(u32::MAX) } fn read_hex_file(path: impl AsRef) -> Option { let raw = fs::read_to_string(path).ok()?; parse_hex_id(&raw).ok() } fn read_trimmed_file(path: impl AsRef) -> Option { let raw = fs::read_to_string(path).ok()?; Some(raw.trim_matches([' ', '\t', '\r', '\n']).to_owned()) } fn read_link_basename(path: impl AsRef) -> Option { let target = fs::read_link(path).ok()?; Some(path_basename(&target)) } fn path_basename(path: &Path) -> String { path.file_name() .and_then(|name| name.to_str()) .map(str::to_owned) .unwrap_or_else(|| basename(&path.to_string_lossy()).to_owned()) } fn uevent_value<'a>(blob: &'a str, key: &str) -> Option<&'a str> { blob.lines().find_map(|line| { let (line_key, value) = line.split_once('=')?; if line_key == key { Some(value.trim_matches([' ', '\t', '\r', '\n'])) } else { None } }) } #[cfg(test)] mod tests { use std::fs::{create_dir_all, write}; use std::os::unix::fs::symlink; use super::*; #[test] fn uevent_value_matches_exact_keys() { let blob = "PCI_ID=8086:46A6\nDRIVER=i915\nNOT_DRIVER=bad\n"; assert_eq!(Some("i915"), uevent_value(blob, "DRIVER")); assert_eq!(Some("8086:46A6"), uevent_value(blob, "PCI_ID")); assert_eq!(None, uevent_value(blob, "MISSING")); } #[test] fn card_sort_key_keeps_card_order_numeric() { let mut names = ["card10".to_owned(), "card2".to_owned(), "card0".to_owned()]; names.sort_unstable_by_key(|name| card_sort_key(name)); assert_eq!(["card0", "card2", "card10"], names); } #[test] fn linux_sysfs_fixture_ports_legacy_gpu_contract() { let tmp = tempfile::tempdir().unwrap(); let root = tmp.path(); let pci0 = root.join("devices/pci0000:00/0000:00:02.0"); let pci1 = root.join("devices/pci0000:01/0000:01:00.0"); create_dir_all(root.join("card0")).unwrap(); create_dir_all(root.join("card1")).unwrap(); create_dir_all(&pci0).unwrap(); create_dir_all(&pci1).unwrap(); symlink( "../devices/pci0000:00/0000:00:02.0", root.join("card0/device"), ) .unwrap(); symlink( "../devices/pci0000:01/0000:01:00.0", root.join("card1/device"), ) .unwrap(); let card0 = root.join("card0/device"); let card1 = root.join("card1/device"); write(card0.join("vendor"), "0x8086\n").unwrap(); write(card0.join("device"), "0x46a6\n").unwrap(); write(card0.join("subsystem_vendor"), "0x1028\n").unwrap(); write(card0.join("subsystem_device"), "0x0b19\n").unwrap(); write(card0.join("uevent"), "DRIVER=i915\nPCI_ID=8086:46A6\n").unwrap(); write(card0.join("mem_info_vram_total"), "268435456\n").unwrap(); write(card1.join("vendor"), "0x10de\n").unwrap(); write(card1.join("device"), "0x1f99\n").unwrap(); write(card1.join("uevent"), "PCI_ID=10DE:1F99\n").unwrap(); symlink("/sys/bus/pci/drivers/nvidia", card1.join("driver")).unwrap(); let info = linux_gpu_info_from_drm_root(root); assert_eq!(SOURCE_LINUX_SYSFS, info.source); assert_eq!(2, info.devices.len()); assert!(info.devices[0].active); assert_eq!(0x8086, info.devices[0].vendor_id); assert_eq!(Some("Intel".to_owned()), info.devices[0].vendor_name); assert_eq!( Some("Intel GPU (8086:46A6)".to_owned()), info.devices[0].device_string ); assert_eq!(Some("i915".to_owned()), info.devices[0].driver_vendor); assert_eq!(Some("0000:00:02.0".to_owned()), info.devices[0].pci_path); assert_eq!(Some(268_435_456.0), info.devices[0].dedicated_video_memory); assert!(!info.devices[1].active); assert_eq!(Some("nvidia".to_owned()), info.devices[1].driver_vendor); } } } #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))] mod platform { use super::{PlatformGpuInfo, SOURCE_LINUX_SYSFS, empty_info}; pub(super) fn gpu_info() -> PlatformGpuInfo { empty_info(SOURCE_LINUX_SYSFS) } } #[cfg(test)] mod tests { use super::*; #[test] fn base_device_sets_required_contract_fields() { let device = base_device(true, 0x10de, 0x2684, SOURCE_DXGI); assert!(device.active); assert_eq!(0x10de, device.vendor_id); assert_eq!(0x2684, device.device_id); assert_eq!(Some("NVIDIA".to_owned()), device.vendor_name); assert_eq!(SOURCE_DXGI, device.source); } #[test] fn empty_info_preserves_source_and_empty_devices() { let info = empty_info(SOURCE_LINUX_SYSFS); assert_eq!(SOURCE_LINUX_SYSFS, info.source); assert!(info.devices.is_empty()); assert!(info.error.is_none()); } }