From 178779c38509efb1a61bbb7c84f3ed2c49392aa0 Mon Sep 17 00:00:00 2001 From: luytan Date: Tue, 8 Sep 2026 19:05:18 +0200 Subject: [PATCH 01/44] refactor(cardwired): correct gpu types --- .../src/core/gpu/default_gpu.rs | 6 -- crates/cardwire-daemon/src/core/gpu/egl.rs | 2 + .../src/core/gpu/enumerator.rs | 72 +++++++-------- crates/cardwire-daemon/src/core/gpu/models.rs | 92 ++++++++----------- 4 files changed, 74 insertions(+), 98 deletions(-) diff --git a/crates/cardwire-daemon/src/core/gpu/default_gpu.rs b/crates/cardwire-daemon/src/core/gpu/default_gpu.rs index a935f70f..ea1317fb 100644 --- a/crates/cardwire-daemon/src/core/gpu/default_gpu.rs +++ b/crates/cardwire-daemon/src/core/gpu/default_gpu.rs @@ -129,12 +129,6 @@ pub fn check_default_drm_class(gpu_list: &mut BTreeMap) -> io: gpu.set_default(Some(true)); } else { gpu.set_default(Some(false)); - // Virtual GPUs (e.g. virtio-gpu in qemu) are reported as VirtualGpu by Vulkan and - // don't count as discrete. Keep the historical behavior of treating a non-default - // virtual GPU as a dGPU. - if gpu.is_virtual() && !gpu.is_discrete() { - gpu.set_discrete(true); - } } } } diff --git a/crates/cardwire-daemon/src/core/gpu/egl.rs b/crates/cardwire-daemon/src/core/gpu/egl.rs index 8ebf6e4a..51231f4e 100644 --- a/crates/cardwire-daemon/src/core/gpu/egl.rs +++ b/crates/cardwire-daemon/src/core/gpu/egl.rs @@ -3,6 +3,8 @@ use std::ffi::{c_char, c_int, c_void}; use khronos_egl::DynamicInstance; // For legacy device, use egl EXT to check if it's discrete or not +#[deprecated] +#[allow(dead_code, deprecated)] pub fn is_discrete_egl(render: u32) -> Result { // Unsafe is required, khronos_egl doesnt include EGL EXT // reference: diff --git a/crates/cardwire-daemon/src/core/gpu/enumerator.rs b/crates/cardwire-daemon/src/core/gpu/enumerator.rs index 8259ec48..8fd44136 100644 --- a/crates/cardwire-daemon/src/core/gpu/enumerator.rs +++ b/crates/cardwire-daemon/src/core/gpu/enumerator.rs @@ -7,7 +7,7 @@ use vulkano::device::physical::{PhysicalDevice, PhysicalDeviceType}; use crate::core::{ gpu::{ - GpuDevice, GpuVendor, check_default_drm_class, device_info::{amd_get_device_model, nvidia_get_device_model, nvidia_get_minor}, display::drm_node_ids, egl::is_discrete_egl, vulkan::vlk_enumerate + GpuDevice, GpuVendor, check_default_drm_class, device_info::{amd_get_device_model, nvidia_get_device_model, nvidia_get_minor}, display::drm_node_ids, models::GpuType, vulkan::vlk_enumerate }, pci::PciDevice }; @@ -72,7 +72,9 @@ impl GpuEnumerator { .map(|name| name.split('(').next().unwrap_or(&name).trim().to_string()) // Fallback to vendor-specific lookup .or_else(|| match gpu_vendor { + // Use the driver info GpuVendor::Nvidia => nvidia_get_device_model(device.pci_address()), + // use amdgpu.ids GpuVendor::Amd => device .device_id() .as_ref() @@ -90,6 +92,7 @@ impl GpuEnumerator { "Unknown Device".to_string() }); + // If the GPU is bound to vfio, mark it as unavailable if let Some(driver) = device.driver() && driver.contains("vfio-") { @@ -102,10 +105,7 @@ impl GpuEnumerator { None, gpu_vendor, None, - false, - true, - false, - false, + GpuType::Unavailable, )); } @@ -123,17 +123,12 @@ impl GpuEnumerator { } }; - // Skip the EGL probe for unavailable GPUs: the render node is unknown (u32::MAX) and the - // lookup would always fail on a phantom /dev/dri/renderD4294967295 path - let discrete = self.is_discrete_vulkan(device.pci_address()) - || (available - && match is_discrete_egl(render) { - Ok(discrete) => discrete, - Err(err) => { - warn!("{}: EGL discrete check failed: {}", device_name, err); - false - } - }); + // Get the device type using vulkan + let mut device_type = self.get_gpu_type_vulkan(device.pci_address()); + // Mark non-available device + if !available { + device_type = GpuType::Unavailable + }; Ok(GpuDevice::new( device_name, @@ -143,35 +138,32 @@ impl GpuEnumerator { None, gpu_vendor, nvidia_minor, - discrete, - false, - available, - self.is_virtual_gpu(device), + device_type, )) } - fn is_discrete_vulkan(&self, pci_id: &str) -> bool { + /// get the gpu type using vulkan + fn get_gpu_type_vulkan(&self, pci_id: &str) -> GpuType { if let Some(vlk_map) = &self.vlk_physical_devices && let Some(vlk_dev) = vlk_map.get(pci_id) { - return vlk_dev.properties().device_type == PhysicalDeviceType::DiscreteGpu; - } - - false - } - /// Detect virtual GPUs (e.g. virtio-gpu in qemu) through Vulkan when available, falling - /// back to the virtio PCI vendor id. - fn is_virtual_gpu(&self, device: &PciDevice) -> bool { - const VIRTIO_VENDOR_ID: &str = "0x1af4"; - - if let Some(vlk_map) = &self.vlk_physical_devices - && let Some(vlk_dev) = vlk_map.get(device.pci_address()) - { - return vlk_dev.properties().device_type == PhysicalDeviceType::VirtualGpu; + match vlk_dev.properties().device_type { + PhysicalDeviceType::Cpu => GpuType::Cpu, + PhysicalDeviceType::DiscreteGpu => GpuType::Discrete, + PhysicalDeviceType::IntegratedGpu => GpuType::Integrated, + PhysicalDeviceType::VirtualGpu => GpuType::Virtual, + PhysicalDeviceType::Other => GpuType::Other, + _ => { + // List is non-exhaustive, warn and give it the unknown type + warn!( + "{} Unknown GPU type: {:?}", + pci_id, + vlk_dev.properties().device_type + ); + GpuType::Unknown + } + } + } else { + GpuType::Unknown } - - device - .vendor_id() - .as_deref() - .is_some_and(|id| id == VIRTIO_VENDOR_ID) } } diff --git a/crates/cardwire-daemon/src/core/gpu/models.rs b/crates/cardwire-daemon/src/core/gpu/models.rs index 7b9adf63..3f14ea90 100644 --- a/crates/cardwire-daemon/src/core/gpu/models.rs +++ b/crates/cardwire-daemon/src/core/gpu/models.rs @@ -1,5 +1,7 @@ use std::{fmt::Display, str::FromStr}; +use zbus::zvariant; + use crate::core::pci::PciDevice; #[derive(Default, Debug, Clone, Copy, PartialEq)] @@ -81,6 +83,20 @@ impl Display for GpuVendor { } } +#[derive( + Clone, Debug, serde::Serialize, serde::Deserialize, Default, PartialEq, zvariant::Type, +)] +pub enum GpuType { + Integrated, + Discrete, + Virtual, + Cpu, + Other, + Unavailable, + #[default] + Unknown, +} + #[derive(Clone, serde::Serialize, serde::Deserialize, zbus::zvariant::Type, PartialEq)] pub struct GpuDevice { name: String, @@ -90,10 +106,7 @@ pub struct GpuDevice { default: Option, gpu_vendor: GpuVendor, nvidia_minor: Option, - discrete: bool, - vfio: bool, - available: bool, - virtual_gpu: bool, + device_type: GpuType, } impl GpuDevice { pub fn pci(&self) -> &PciDevice { @@ -126,25 +139,28 @@ impl GpuDevice { &self.nvidia_minor } - pub fn is_discrete(&self) -> bool { - self.discrete + pub fn device_type(&self) -> &GpuType { + &self.device_type } - pub fn set_discrete(&mut self, discrete: bool) { - self.discrete = discrete; + pub fn is_discrete(&self) -> bool { + self.device_type == GpuType::Discrete } - pub fn is_available(&self) -> bool { - self.available + pub fn is_cpu(&self) -> bool { + self.device_type == GpuType::Cpu } - pub fn _vfio(&self) -> bool { - self.vfio + pub fn is_integrated(&self) -> bool { + self.device_type == GpuType::Integrated } - /// True for virtual GPUs (e.g. virtio-gpu in qemu) that expose no PCI display controller. pub fn is_virtual(&self) -> bool { - self.virtual_gpu + self.device_type == GpuType::Virtual + } + + pub fn is_available(&self) -> bool { + self.device_type != GpuType::Unavailable } #[allow(clippy::too_many_arguments)] @@ -156,10 +172,7 @@ impl GpuDevice { default: Option, gpu_vendor: GpuVendor, nvidia_minor: Option, - discrete: bool, - vfio: bool, - available: bool, - virtual_gpu: bool, + device_type: GpuType, ) -> GpuDevice { GpuDevice { name, @@ -169,10 +182,7 @@ impl GpuDevice { default, gpu_vendor, nvidia_minor, - discrete, - vfio, - available, - virtual_gpu, + device_type, } } @@ -188,9 +198,7 @@ pub struct DbusGpuDevice { pub render: u32, pub card: u32, pub default: bool, - pub discrete: bool, - pub virtual_gpu: bool, - pub available: bool, + pub device_type: GpuType, pub vendor: String, pub driver: String, pub nvidia: bool, @@ -205,9 +213,7 @@ impl From<&GpuDevice> for DbusGpuDevice { name: gpu.name().to_string(), card: *gpu.card(), default: gpu.is_default(), - discrete: gpu.is_discrete(), - virtual_gpu: gpu.is_virtual(), - available: gpu.is_available(), + device_type: gpu.device_type.clone(), vendor: gpu.gpu_vendor().to_string(), driver: gpu.pci.driver().clone().unwrap_or("none".to_string()), nvidia: gpu.gpu_vendor() == GpuVendor::Nvidia, @@ -329,10 +335,7 @@ mod tests { Some(true), GpuVendor::Amd, None, - true, - false, - true, - false, + GpuType::Discrete, ); assert_eq!(gpu.name(), "RX 7900 XTX"); assert_eq!(*gpu.render(), 128); @@ -354,10 +357,7 @@ mod tests { Some(true), GpuVendor::Amd, None, - true, - false, - true, - false, + GpuType::Other, ); assert!(gpu.is_default()); } @@ -372,10 +372,7 @@ mod tests { Some(false), GpuVendor::Amd, None, - true, - false, - true, - false, + GpuType::Other, ); assert!(!gpu.is_default()); } @@ -390,10 +387,7 @@ mod tests { None, GpuVendor::Amd, None, - false, - false, - true, - false, + GpuType::Other, ); assert!(!gpu.is_default()); assert!(!gpu.is_discrete()); @@ -409,10 +403,7 @@ mod tests { None, GpuVendor::Amd, None, - true, - false, - true, - false, + GpuType::Other, ); assert!(!gpu.is_default()); gpu.set_default(Some(true)); @@ -429,10 +420,7 @@ mod tests { Some(false), GpuVendor::Nvidia, Some(0), - true, - false, - true, - false, + GpuType::Discrete, ); assert_eq!(gpu.gpu_vendor(), GpuVendor::Nvidia); assert_eq!(*gpu.nvidia_minor(), Some(0)); From f510572713f18d87c92f5e6a23ed311b3102e0fb Mon Sep 17 00:00:00 2001 From: luytan Date: Sun, 13 Sep 2026 09:05:13 +0200 Subject: [PATCH 02/44] feat(cardwired): working vulkan --- .../src/core/gpu/device_info.rs | 2 + .../src/core/gpu/enumerator.rs | 133 +++++++---------- crates/cardwire-daemon/src/core/gpu/mod.rs | 6 +- .../core/gpu/{nvidia.rs => nvidia_powerd.rs} | 0 .../src/core/gpu/type_detection/amd.rs | 1 + .../src/core/gpu/type_detection/intel.rs | 12 ++ .../src/core/gpu/type_detection/mod.rs | 5 + .../src/core/gpu/type_detection/nvidia.rs | 6 + .../src/core/gpu/type_detection/virtio.rs | 1 + .../src/core/gpu/type_detection/vulkan.rs | 140 ++++++++++++++++++ crates/cardwire-daemon/src/core/gpu/vulkan.rs | 61 -------- 11 files changed, 227 insertions(+), 140 deletions(-) rename crates/cardwire-daemon/src/core/gpu/{nvidia.rs => nvidia_powerd.rs} (100%) create mode 100644 crates/cardwire-daemon/src/core/gpu/type_detection/amd.rs create mode 100644 crates/cardwire-daemon/src/core/gpu/type_detection/intel.rs create mode 100644 crates/cardwire-daemon/src/core/gpu/type_detection/mod.rs create mode 100644 crates/cardwire-daemon/src/core/gpu/type_detection/nvidia.rs create mode 100644 crates/cardwire-daemon/src/core/gpu/type_detection/virtio.rs create mode 100644 crates/cardwire-daemon/src/core/gpu/type_detection/vulkan.rs delete mode 100644 crates/cardwire-daemon/src/core/gpu/vulkan.rs diff --git a/crates/cardwire-daemon/src/core/gpu/device_info.rs b/crates/cardwire-daemon/src/core/gpu/device_info.rs index b287f4b4..2d112026 100644 --- a/crates/cardwire-daemon/src/core/gpu/device_info.rs +++ b/crates/cardwire-daemon/src/core/gpu/device_info.rs @@ -18,6 +18,7 @@ pub fn nvidia_get_minor(pci_address: &str) -> Option { } /// find the nvidia model using the device information file +#[allow(unused, dead_code)] pub fn nvidia_get_device_model(pci_address: &str) -> Option { let nvidia_driver_proc = Path::new("/proc/driver/nvidia/gpus/") .join(pci_address) @@ -37,6 +38,7 @@ pub fn nvidia_get_device_model(pci_address: &str) -> Option { } /// Find the amd model using amdgpu.ids, require the device id and the revision for precise matching +#[allow(unused, dead_code)] pub fn amd_get_device_model(device_id: &str, pci: &str) -> Option { let path = "/usr/share/libdrm/amdgpu.ids"; let device_id = device_id.to_string().replace("0x", "").to_ascii_uppercase(); diff --git a/crates/cardwire-daemon/src/core/gpu/enumerator.rs b/crates/cardwire-daemon/src/core/gpu/enumerator.rs index 8fd44136..0525d7c5 100644 --- a/crates/cardwire-daemon/src/core/gpu/enumerator.rs +++ b/crates/cardwire-daemon/src/core/gpu/enumerator.rs @@ -1,28 +1,21 @@ -use std::{ - collections::{BTreeMap, HashMap}, io, sync::Arc -}; +use std::{collections::BTreeMap, io}; use log::{error, info, warn}; -use vulkano::device::physical::{PhysicalDevice, PhysicalDeviceType}; use crate::core::{ gpu::{ - GpuDevice, GpuVendor, check_default_drm_class, device_info::{amd_get_device_model, nvidia_get_device_model, nvidia_get_minor}, display::drm_node_ids, models::GpuType, vulkan::vlk_enumerate + GpuDevice, GpuVendor, check_default_drm_class, device_info::nvidia_get_minor, display::drm_node_ids, models::GpuType, type_detection::vulkan::Vulkan }, pci::PciDevice }; pub struct GpuEnumerator { - vlk_physical_devices: Option>>, + vulkan: Vulkan, } impl GpuEnumerator { pub fn build() -> Self { - // Store the vulkan list to prevent calling vulkan everytime we look into it - let vlk_physical_devices = vlk_enumerate(); - - Self { - vlk_physical_devices, - } + let vulkan = Vulkan::build(); + Self { vulkan } } pub fn enumerate(&self, pci_list: &BTreeMap) -> BTreeMap { let mut gpu_list: BTreeMap = BTreeMap::new(); @@ -63,42 +56,61 @@ impl GpuEnumerator { None => GpuVendor::default(), }; + // Check if the gpu info can be fetched using vulkan, if so use vulkan to build the GPU + if self.vulkan.vulkan_compatible(device.pci_address()) { + let pci_id = device.pci_address(); + let gpu_type = self.vulkan.get_gpu_type(pci_id); + let gpu_name = self.vulkan.get_gpu_name(pci_id); + + let gpu_card = self.vulkan.get_gpu_card(pci_id).unwrap_or_default(); + let gpu_render = self.vulkan.get_gpu_render(pci_id).unwrap_or_default(); + + let gpu_device = GpuDevice::new( + gpu_name, + device.clone(), + gpu_render as u32, + gpu_card as u32, + None, + gpu_vendor, + None, + gpu_type, + ); + return Ok(gpu_device); + } + + // else, fallback to sysfs GPU building + // Try with vulkan first - let device_name = self - .vlk_physical_devices - .as_ref() - .and_then(|map| map.get(device.pci_address())) - .map(|vlk_dev| vlk_dev.properties().device_name.clone()) - .map(|name| name.split('(').next().unwrap_or(&name).trim().to_string()) - // Fallback to vendor-specific lookup - .or_else(|| match gpu_vendor { - // Use the driver info - GpuVendor::Nvidia => nvidia_get_device_model(device.pci_address()), - // use amdgpu.ids - GpuVendor::Amd => device - .device_id() - .as_ref() - .and_then(|id| amd_get_device_model(id, device.pci_address())), - _ => None, - }) - // Fallback to hwdata - .or_else(|| { - warn!("Couldn't get device_name, falling back to hwdata"); - device.device_name().clone() - }) - // fallback default - .unwrap_or_else(|| { - warn!("Couldn't get name using hwdata, falling back to default"); - "Unknown Device".to_string() - }); + //let device_name = (|| match gpu_vendor { + // // Use the driver info + // GpuVendor::Nvidia => nvidia_get_device_model(device.pci_address()), + // // use amdgpu.ids + // GpuVendor::Amd => device + // .device_id() + // .as_ref() + // .and_then(|id| amd_get_device_model(id, device.pci_address())), + // _ => None, + // }) + // // Fallback to hwdata + // .or_else(|| { + // warn!("Couldn't get device_name, falling back to hwdata"); + // device.device_name().clone() + // }) + // // fallback default + // .unwrap_or_else(|| { + // warn!("Couldn't get name using hwdata, falling back to default"); + // "Unknown Device".to_string() + // }); + + let gpu_name = String::new(); // If the GPU is bound to vfio, mark it as unavailable if let Some(driver) = device.driver() && driver.contains("vfio-") { - info!("Device: {} is bound to: {}", device_name, driver); + info!("Device: {} is bound to: {}", gpu_name, driver); return Ok(GpuDevice::new( - device_name, + gpu_name, device.clone(), u32::MAX, u32::MAX, @@ -115,23 +127,17 @@ impl GpuEnumerator { }; // Available is used to know if the device should be used by cardwire or not - let (card, render, available) = match drm_node_ids(device.pci_address()) { + let (card, render, _available) = match drm_node_ids(device.pci_address()) { Ok((c, r)) => (c, r, true), Err(err) => { - error!("{}: Couldn't get drm node IDs: {}", device_name, err); + error!("{}: Couldn't get drm node IDs: {}", gpu_name, err); (u32::MAX, u32::MAX, false) } }; - - // Get the device type using vulkan - let mut device_type = self.get_gpu_type_vulkan(device.pci_address()); - // Mark non-available device - if !available { - device_type = GpuType::Unavailable - }; + let device_type = GpuType::Unknown; Ok(GpuDevice::new( - device_name, + gpu_name, device.clone(), render, card, @@ -141,29 +147,4 @@ impl GpuEnumerator { device_type, )) } - /// get the gpu type using vulkan - fn get_gpu_type_vulkan(&self, pci_id: &str) -> GpuType { - if let Some(vlk_map) = &self.vlk_physical_devices - && let Some(vlk_dev) = vlk_map.get(pci_id) - { - match vlk_dev.properties().device_type { - PhysicalDeviceType::Cpu => GpuType::Cpu, - PhysicalDeviceType::DiscreteGpu => GpuType::Discrete, - PhysicalDeviceType::IntegratedGpu => GpuType::Integrated, - PhysicalDeviceType::VirtualGpu => GpuType::Virtual, - PhysicalDeviceType::Other => GpuType::Other, - _ => { - // List is non-exhaustive, warn and give it the unknown type - warn!( - "{} Unknown GPU type: {:?}", - pci_id, - vlk_dev.properties().device_type - ); - GpuType::Unknown - } - } - } else { - GpuType::Unknown - } - } } diff --git a/crates/cardwire-daemon/src/core/gpu/mod.rs b/crates/cardwire-daemon/src/core/gpu/mod.rs index 7b9964d0..f6fe8192 100644 --- a/crates/cardwire-daemon/src/core/gpu/mod.rs +++ b/crates/cardwire-daemon/src/core/gpu/mod.rs @@ -4,12 +4,12 @@ mod display; mod egl; mod enumerator; mod models; -mod nvidia; -mod vulkan; +mod nvidia_powerd; +mod type_detection; pub use default_gpu::check_default_drm_class; #[expect(unused_imports)] pub use display::{external_display_connected, is_gpu_active}; pub use enumerator::GpuEnumerator; pub use models::{DbusGpuDevice, GpuDevice, GpuVendor, PowerState}; -pub use nvidia::{start_nvidia_powerd, stop_nvidia_powerd}; +pub use nvidia_powerd::{start_nvidia_powerd, stop_nvidia_powerd}; diff --git a/crates/cardwire-daemon/src/core/gpu/nvidia.rs b/crates/cardwire-daemon/src/core/gpu/nvidia_powerd.rs similarity index 100% rename from crates/cardwire-daemon/src/core/gpu/nvidia.rs rename to crates/cardwire-daemon/src/core/gpu/nvidia_powerd.rs diff --git a/crates/cardwire-daemon/src/core/gpu/type_detection/amd.rs b/crates/cardwire-daemon/src/core/gpu/type_detection/amd.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/crates/cardwire-daemon/src/core/gpu/type_detection/amd.rs @@ -0,0 +1 @@ + diff --git a/crates/cardwire-daemon/src/core/gpu/type_detection/intel.rs b/crates/cardwire-daemon/src/core/gpu/type_detection/intel.rs new file mode 100644 index 00000000..f2d95c75 --- /dev/null +++ b/crates/cardwire-daemon/src/core/gpu/type_detection/intel.rs @@ -0,0 +1,12 @@ +use crate::core::gpu::models::GpuType; + +/// Get the gpu type for an intel GPU +#[allow(unused, dead_code)] +pub fn get_intel_type(pci_id: &str) -> GpuType { + // PCI id reserved for iGPUs + if pci_id == "0000:00:02.0" { + GpuType::Integrated + } else { + GpuType::Discrete + } +} diff --git a/crates/cardwire-daemon/src/core/gpu/type_detection/mod.rs b/crates/cardwire-daemon/src/core/gpu/type_detection/mod.rs new file mode 100644 index 00000000..02ae56be --- /dev/null +++ b/crates/cardwire-daemon/src/core/gpu/type_detection/mod.rs @@ -0,0 +1,5 @@ +pub mod amd; +pub mod intel; +pub mod nvidia; +pub mod virtio; +pub mod vulkan; diff --git a/crates/cardwire-daemon/src/core/gpu/type_detection/nvidia.rs b/crates/cardwire-daemon/src/core/gpu/type_detection/nvidia.rs new file mode 100644 index 00000000..de66f55a --- /dev/null +++ b/crates/cardwire-daemon/src/core/gpu/type_detection/nvidia.rs @@ -0,0 +1,6 @@ +use crate::core::gpu::models::GpuType; + +#[allow(unused, dead_code)] +pub fn get_nvidia_type(pci_id: &str, gpu_name: &str) -> GpuType { + GpuType::Unknown +} diff --git a/crates/cardwire-daemon/src/core/gpu/type_detection/virtio.rs b/crates/cardwire-daemon/src/core/gpu/type_detection/virtio.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/crates/cardwire-daemon/src/core/gpu/type_detection/virtio.rs @@ -0,0 +1 @@ + diff --git a/crates/cardwire-daemon/src/core/gpu/type_detection/vulkan.rs b/crates/cardwire-daemon/src/core/gpu/type_detection/vulkan.rs new file mode 100644 index 00000000..70ff79a8 --- /dev/null +++ b/crates/cardwire-daemon/src/core/gpu/type_detection/vulkan.rs @@ -0,0 +1,140 @@ +use std::{collections::HashMap, sync::Arc}; + +use log::warn; +use vulkano::{ + VulkanLibrary, device::physical::{PhysicalDevice, PhysicalDeviceType}, instance::{Instance, InstanceCreateFlags, InstanceCreateInfo} +}; + +use crate::core::gpu::models::GpuType; + +pub struct Vulkan { + vlk_physical_devices: Option>>, +} + +impl Vulkan { + pub fn build() -> Self { + Self { + vlk_physical_devices: vlk_enumerate(), + } + } + + /// Verify if the device pci id is in the vulkan enum map + pub fn vulkan_compatible(&self, pci_id: &str) -> bool { + if let Some(vlk_map) = &self.vlk_physical_devices + && vlk_map.contains_key(pci_id) + { + true + } else { + false + } + } + + /// get the gpu type using vulkan + pub fn get_gpu_type(&self, pci_id: &str) -> GpuType { + if let Some(vlk_map) = &self.vlk_physical_devices + && let Some(vlk_dev) = vlk_map.get(pci_id) + { + match vlk_dev.properties().device_type { + PhysicalDeviceType::Cpu => GpuType::Cpu, + PhysicalDeviceType::DiscreteGpu => GpuType::Discrete, + PhysicalDeviceType::IntegratedGpu => GpuType::Integrated, + PhysicalDeviceType::VirtualGpu => GpuType::Virtual, + PhysicalDeviceType::Other => GpuType::Other, + _ => { + // List is non-exhaustive, warn and give it the unknown type + warn!( + "{} Unknown GPU type: {:?}", + pci_id, + vlk_dev.properties().device_type + ); + GpuType::Unknown + } + } + } else { + GpuType::Unknown + } + } + /// Get the gpu name using vulkan + pub fn get_gpu_name(&self, pci_id: &str) -> String { + let mut name = String::new(); + if let Some(vlk_map) = &self.vlk_physical_devices + && let Some(vlk_dev) = vlk_map.get(pci_id) + { + name = vlk_dev.properties().device_name.clone(); + } + + name + } + /// Get the gpu render node using vulkan + pub fn get_gpu_render(&self, pci_id: &str) -> Option { + if let Some(vlk_map) = &self.vlk_physical_devices + && let Some(vlk_dev) = vlk_map.get(pci_id) + { + return vlk_dev.properties().render_minor; + } + None + } + /// Get the gpu card node using vulkan + pub fn get_gpu_card(&self, pci_id: &str) -> Option { + if let Some(vlk_map) = &self.vlk_physical_devices + && let Some(vlk_dev) = vlk_map.get(pci_id) + { + return vlk_dev.properties().primary_minor; + } + None + } +} +/// enumerate vulkan physical devices, return None if an error happened +fn vlk_enumerate() -> Option>> { + let library = match VulkanLibrary::new() { + Ok(lib) => lib, + Err(err) => { + warn!("Couldn't find Vulkan library/DLL: {}", err); + return None; + } + }; + let instance = match Instance::new( + library, + InstanceCreateInfo { + flags: InstanceCreateFlags::ENUMERATE_PORTABILITY, + ..Default::default() + }, + ) { + Ok(inst) => inst, + Err(err) => { + warn!("Could not create Vulkan Instance: {}", err); + return None; + } + }; + + let physical_devices_enum = match instance.enumerate_physical_devices() { + Ok(vlk_enum) => vlk_enum, + Err(err) => { + warn!("Could not enumerate vulkan physical devices: {}", err); + return None; + } + }; + let mut vlk_physical_devices: HashMap> = HashMap::new(); + + for vlk_device in physical_devices_enum { + match ( + vlk_device.properties().pci_domain, + vlk_device.properties().pci_bus, + vlk_device.properties().pci_device, + vlk_device.properties().pci_function, + ) { + (Some(domain), Some(bus), Some(device), Some(function)) => { + let pci_id = format!("{:04x}:{:02x}:{:02x}.{:x}", domain, bus, device, function); + vlk_physical_devices.insert(pci_id, Arc::clone(&vlk_device)); + } + _ => { + warn!( + "{}: Not available (VK_EXT_pci_bus_info not supported)", + vlk_device.properties().device_name + ); + continue; + } + } + } + Some(vlk_physical_devices) +} diff --git a/crates/cardwire-daemon/src/core/gpu/vulkan.rs b/crates/cardwire-daemon/src/core/gpu/vulkan.rs deleted file mode 100644 index 67634417..00000000 --- a/crates/cardwire-daemon/src/core/gpu/vulkan.rs +++ /dev/null @@ -1,61 +0,0 @@ -use std::{collections::HashMap, sync::Arc}; - -use log::warn; -use vulkano::{ - VulkanLibrary, device::physical::PhysicalDevice, instance::{Instance, InstanceCreateFlags, InstanceCreateInfo} -}; - -/// enumerate vulkan physical devices, return None if an error happened -pub fn vlk_enumerate() -> Option>> { - let library = match VulkanLibrary::new() { - Ok(lib) => lib, - Err(err) => { - warn!("Couldn't find Vulkan library/DLL: {}", err); - return None; - } - }; - let instance = match Instance::new( - library, - InstanceCreateInfo { - flags: InstanceCreateFlags::ENUMERATE_PORTABILITY, - ..Default::default() - }, - ) { - Ok(inst) => inst, - Err(err) => { - warn!("Could not create Vulkan Instance: {}", err); - return None; - } - }; - - let physical_devices_enum = match instance.enumerate_physical_devices() { - Ok(vlk_enum) => vlk_enum, - Err(err) => { - warn!("Could not enumerate vulkan physical devices: {}", err); - return None; - } - }; - let mut vlk_physical_devices: HashMap> = HashMap::new(); - - for vlk_device in physical_devices_enum { - match ( - vlk_device.properties().pci_domain, - vlk_device.properties().pci_bus, - vlk_device.properties().pci_device, - vlk_device.properties().pci_function, - ) { - (Some(domain), Some(bus), Some(device), Some(function)) => { - let pci_id = format!("{:04x}:{:02x}:{:02x}.{:x}", domain, bus, device, function); - vlk_physical_devices.insert(pci_id, Arc::clone(&vlk_device)); - } - _ => { - warn!( - "{}: Not available (VK_EXT_pci_bus_info not supported)", - vlk_device.properties().device_name - ); - continue; - } - } - } - Some(vlk_physical_devices) -} From 151dab7e9bf2b36139fb3a12e2dd09438efa91ed Mon Sep 17 00:00:00 2001 From: luytan Date: Sun, 13 Sep 2026 09:54:51 +0200 Subject: [PATCH 03/44] feat: bind systemd service to graphical --- assets/cardwired.service | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/assets/cardwired.service b/assets/cardwired.service index 1fa6d314..c3b1534e 100644 --- a/assets/cardwired.service +++ b/assets/cardwired.service @@ -1,6 +1,10 @@ [Unit] Description=Cardwire Daemon -After=dbus.service +Before=graphical.target +# drm-module-load.target is from cachyos +# +After=multi-user.target drm-module-load.target +Wants=multi-user.target drm-module-load.target [Service] @@ -38,4 +42,4 @@ SystemCallFilter=~@cpu-emulation @module @obsolete @raw-io @reboot @swap [Install] -WantedBy=multi-user.target +WantedBy=graphical.target From bc95f917b25c92d347b04f29c7e573f46cdc4443 Mon Sep 17 00:00:00 2001 From: luytan Date: Sun, 13 Sep 2026 10:08:57 +0200 Subject: [PATCH 04/44] refactor(cardwired): re-organise folder structure for gpu --- .../src/core/gpu/device_info.rs | 76 ------------------- .../src/core/gpu/enumerator.rs | 9 +-- .../src/core/gpu/{ => generic}/default_gpu.rs | 0 .../src/core/gpu/{ => generic}/display.rs | 7 -- .../src/core/gpu/{ => generic}/egl.rs | 0 .../src/core/gpu/generic/mod.rs | 4 + .../gpu/{type_detection => generic}/vulkan.rs | 0 crates/cardwire-daemon/src/core/gpu/mod.rs | 16 ++-- .../src/core/gpu/type_detection/amd.rs | 1 - .../src/core/gpu/type_detection/nvidia.rs | 6 -- .../src/core/gpu/vendor_specific/amd.rs | 39 ++++++++++ .../intel.rs | 0 .../mod.rs | 1 - .../nvidia.rs} | 46 ++++++++++- .../virtio.rs | 0 15 files changed, 96 insertions(+), 109 deletions(-) delete mode 100644 crates/cardwire-daemon/src/core/gpu/device_info.rs rename crates/cardwire-daemon/src/core/gpu/{ => generic}/default_gpu.rs (100%) rename crates/cardwire-daemon/src/core/gpu/{ => generic}/display.rs (89%) rename crates/cardwire-daemon/src/core/gpu/{ => generic}/egl.rs (100%) create mode 100644 crates/cardwire-daemon/src/core/gpu/generic/mod.rs rename crates/cardwire-daemon/src/core/gpu/{type_detection => generic}/vulkan.rs (100%) delete mode 100644 crates/cardwire-daemon/src/core/gpu/type_detection/amd.rs delete mode 100644 crates/cardwire-daemon/src/core/gpu/type_detection/nvidia.rs create mode 100644 crates/cardwire-daemon/src/core/gpu/vendor_specific/amd.rs rename crates/cardwire-daemon/src/core/gpu/{type_detection => vendor_specific}/intel.rs (100%) rename crates/cardwire-daemon/src/core/gpu/{type_detection => vendor_specific}/mod.rs (78%) rename crates/cardwire-daemon/src/core/gpu/{nvidia_powerd.rs => vendor_specific/nvidia.rs} (69%) rename crates/cardwire-daemon/src/core/gpu/{type_detection => vendor_specific}/virtio.rs (100%) diff --git a/crates/cardwire-daemon/src/core/gpu/device_info.rs b/crates/cardwire-daemon/src/core/gpu/device_info.rs deleted file mode 100644 index 2d112026..00000000 --- a/crates/cardwire-daemon/src/core/gpu/device_info.rs +++ /dev/null @@ -1,76 +0,0 @@ -//! GPU vendor model lookup from /proc and libdrm data files. - -use std::{fs, path::Path}; - -pub fn nvidia_get_minor(pci_address: &str) -> Option { - let nvidia_driver_proc = Path::new("/proc/driver/nvidia/gpus/") - .join(pci_address) - .join("information"); - let information = fs::read_to_string(nvidia_driver_proc).ok()?; - information - .lines() - .find(|line| line.starts_with("Device Minor:"))? - .split_once(':')? - .1 - .trim() - .parse::() - .ok() -} - -/// find the nvidia model using the device information file -#[allow(unused, dead_code)] -pub fn nvidia_get_device_model(pci_address: &str) -> Option { - let nvidia_driver_proc = Path::new("/proc/driver/nvidia/gpus/") - .join(pci_address) - .join("information"); - let information = fs::read_to_string(nvidia_driver_proc).ok()?; - let model = information - .lines() - .find(|line| line.starts_with("Model:"))? - .split_once(':')? - .1 - .trim() - .to_string(); - match !model.is_empty() { - true => Some(model), - false => None, - } -} - -/// Find the amd model using amdgpu.ids, require the device id and the revision for precise matching -#[allow(unused, dead_code)] -pub fn amd_get_device_model(device_id: &str, pci: &str) -> Option { - let path = "/usr/share/libdrm/amdgpu.ids"; - let device_id = device_id.to_string().replace("0x", "").to_ascii_uppercase(); - - let revision = fs::read_to_string(format!("/sys/bus/pci/devices/{}/revision", pci)) - .ok()? - .trim() - .replace("0x", "") - .to_ascii_uppercase(); - - let content = fs::read_to_string(path).ok()?; - - for line in content.lines() { - if line.starts_with('#') { - continue; - } - - let mut parts = line.split('\t'); - let Some(id) = parts.next() else { - continue; - }; - let Some(rev) = parts.next() else { - continue; - }; - let Some(name) = parts.next() else { - continue; - }; - - if id.trim_end_matches(',') == device_id && rev.trim_end_matches(',') == revision { - return Some(name.to_string()); - } - } - - None -} diff --git a/crates/cardwire-daemon/src/core/gpu/enumerator.rs b/crates/cardwire-daemon/src/core/gpu/enumerator.rs index 0525d7c5..a74442d7 100644 --- a/crates/cardwire-daemon/src/core/gpu/enumerator.rs +++ b/crates/cardwire-daemon/src/core/gpu/enumerator.rs @@ -4,7 +4,7 @@ use log::{error, info, warn}; use crate::core::{ gpu::{ - GpuDevice, GpuVendor, check_default_drm_class, device_info::nvidia_get_minor, display::drm_node_ids, models::GpuType, type_detection::vulkan::Vulkan + GpuDevice, GpuVendor, check_default_drm_class, generic::{display::drm_node_ids, vulkan::Vulkan}, models::GpuType }, pci::PciDevice }; @@ -121,11 +121,6 @@ impl GpuEnumerator { )); } - let nvidia_minor = match gpu_vendor { - GpuVendor::Nvidia => nvidia_get_minor(device.pci_address()), - _ => None, - }; - // Available is used to know if the device should be used by cardwire or not let (card, render, _available) = match drm_node_ids(device.pci_address()) { Ok((c, r)) => (c, r, true), @@ -143,7 +138,7 @@ impl GpuEnumerator { card, None, gpu_vendor, - nvidia_minor, + None, device_type, )) } diff --git a/crates/cardwire-daemon/src/core/gpu/default_gpu.rs b/crates/cardwire-daemon/src/core/gpu/generic/default_gpu.rs similarity index 100% rename from crates/cardwire-daemon/src/core/gpu/default_gpu.rs rename to crates/cardwire-daemon/src/core/gpu/generic/default_gpu.rs diff --git a/crates/cardwire-daemon/src/core/gpu/display.rs b/crates/cardwire-daemon/src/core/gpu/generic/display.rs similarity index 89% rename from crates/cardwire-daemon/src/core/gpu/display.rs rename to crates/cardwire-daemon/src/core/gpu/generic/display.rs index 6369bcb6..2a732dad 100644 --- a/crates/cardwire-daemon/src/core/gpu/display.rs +++ b/crates/cardwire-daemon/src/core/gpu/generic/display.rs @@ -8,14 +8,9 @@ const NON_PHYSICAL: &[&str] = &["Virtual-", "Unknown-", "Writeback-"]; const INTERNAL_PANELS: &[&str] = &["eDP-", "LVDS-", "DSI-", "DPI-", "SPI-"]; /// Return whether a DRM card currently owns a connected physical external display. -/// -/// Connector ownership is encoded in sysfs names such as `card1-HDMI-A-1`. Internal panels and -/// virtual connectors are excluded so only physical external outputs keep the card available. #[allow(dead_code)] pub fn external_display_connected(card: u32) -> io::Result { let card_prefix = format!("card{card}-"); - // An unreadable status is not proof of a disconnect. Keep the first error while checking - // whether another connector can still confirm that the card is in use. let mut status_error = None; for entry in fs::read_dir("/sys/class/drm")? { @@ -37,7 +32,6 @@ pub fn external_display_connected(card: u32) -> io::Result { } match fs::read_to_string(entry.path().join("status")) { - // A confirmed connection takes precedence over errors from other connectors. Ok(status) if status.trim() == "connected" => return Ok(true), Ok(_) => {} Err(err) => { @@ -46,7 +40,6 @@ pub fn external_display_connected(card: u32) -> io::Result { } } - // Fail safely instead of allowing incomplete topology information to block a display GPU. match status_error { Some(err) => Err(err), None => Ok(false), diff --git a/crates/cardwire-daemon/src/core/gpu/egl.rs b/crates/cardwire-daemon/src/core/gpu/generic/egl.rs similarity index 100% rename from crates/cardwire-daemon/src/core/gpu/egl.rs rename to crates/cardwire-daemon/src/core/gpu/generic/egl.rs diff --git a/crates/cardwire-daemon/src/core/gpu/generic/mod.rs b/crates/cardwire-daemon/src/core/gpu/generic/mod.rs new file mode 100644 index 00000000..61f24bf3 --- /dev/null +++ b/crates/cardwire-daemon/src/core/gpu/generic/mod.rs @@ -0,0 +1,4 @@ +//! Generic functions that should work on all GPUs +pub mod default_gpu; +pub mod display; +pub mod vulkan; diff --git a/crates/cardwire-daemon/src/core/gpu/type_detection/vulkan.rs b/crates/cardwire-daemon/src/core/gpu/generic/vulkan.rs similarity index 100% rename from crates/cardwire-daemon/src/core/gpu/type_detection/vulkan.rs rename to crates/cardwire-daemon/src/core/gpu/generic/vulkan.rs diff --git a/crates/cardwire-daemon/src/core/gpu/mod.rs b/crates/cardwire-daemon/src/core/gpu/mod.rs index f6fe8192..0a099659 100644 --- a/crates/cardwire-daemon/src/core/gpu/mod.rs +++ b/crates/cardwire-daemon/src/core/gpu/mod.rs @@ -1,15 +1,11 @@ -mod default_gpu; -mod device_info; -mod display; -mod egl; mod enumerator; +mod generic; mod models; -mod nvidia_powerd; -mod type_detection; +mod vendor_specific; -pub use default_gpu::check_default_drm_class; -#[expect(unused_imports)] -pub use display::{external_display_connected, is_gpu_active}; pub use enumerator::GpuEnumerator; +pub use generic::default_gpu::check_default_drm_class; +#[expect(unused_imports)] +pub use generic::display::{external_display_connected, is_gpu_active}; pub use models::{DbusGpuDevice, GpuDevice, GpuVendor, PowerState}; -pub use nvidia_powerd::{start_nvidia_powerd, stop_nvidia_powerd}; +pub use vendor_specific::nvidia::{start_nvidia_powerd, stop_nvidia_powerd}; diff --git a/crates/cardwire-daemon/src/core/gpu/type_detection/amd.rs b/crates/cardwire-daemon/src/core/gpu/type_detection/amd.rs deleted file mode 100644 index 8b137891..00000000 --- a/crates/cardwire-daemon/src/core/gpu/type_detection/amd.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/crates/cardwire-daemon/src/core/gpu/type_detection/nvidia.rs b/crates/cardwire-daemon/src/core/gpu/type_detection/nvidia.rs deleted file mode 100644 index de66f55a..00000000 --- a/crates/cardwire-daemon/src/core/gpu/type_detection/nvidia.rs +++ /dev/null @@ -1,6 +0,0 @@ -use crate::core::gpu::models::GpuType; - -#[allow(unused, dead_code)] -pub fn get_nvidia_type(pci_id: &str, gpu_name: &str) -> GpuType { - GpuType::Unknown -} diff --git a/crates/cardwire-daemon/src/core/gpu/vendor_specific/amd.rs b/crates/cardwire-daemon/src/core/gpu/vendor_specific/amd.rs new file mode 100644 index 00000000..7574adca --- /dev/null +++ b/crates/cardwire-daemon/src/core/gpu/vendor_specific/amd.rs @@ -0,0 +1,39 @@ +use std::fs; + +/// Find the amd model using amdgpu.ids, require the device id and the revision for precise matching +#[allow(unused, dead_code)] +pub fn amd_get_device_model(device_id: &str, pci: &str) -> Option { + let path = "/usr/share/libdrm/amdgpu.ids"; + let device_id = device_id.to_string().replace("0x", "").to_ascii_uppercase(); + + let revision = fs::read_to_string(format!("/sys/bus/pci/devices/{}/revision", pci)) + .ok()? + .trim() + .replace("0x", "") + .to_ascii_uppercase(); + + let content = fs::read_to_string(path).ok()?; + + for line in content.lines() { + if line.starts_with('#') { + continue; + } + + let mut parts = line.split('\t'); + let Some(id) = parts.next() else { + continue; + }; + let Some(rev) = parts.next() else { + continue; + }; + let Some(name) = parts.next() else { + continue; + }; + + if id.trim_end_matches(',') == device_id && rev.trim_end_matches(',') == revision { + return Some(name.to_string()); + } + } + + None +} diff --git a/crates/cardwire-daemon/src/core/gpu/type_detection/intel.rs b/crates/cardwire-daemon/src/core/gpu/vendor_specific/intel.rs similarity index 100% rename from crates/cardwire-daemon/src/core/gpu/type_detection/intel.rs rename to crates/cardwire-daemon/src/core/gpu/vendor_specific/intel.rs diff --git a/crates/cardwire-daemon/src/core/gpu/type_detection/mod.rs b/crates/cardwire-daemon/src/core/gpu/vendor_specific/mod.rs similarity index 78% rename from crates/cardwire-daemon/src/core/gpu/type_detection/mod.rs rename to crates/cardwire-daemon/src/core/gpu/vendor_specific/mod.rs index 02ae56be..c5d0ef16 100644 --- a/crates/cardwire-daemon/src/core/gpu/type_detection/mod.rs +++ b/crates/cardwire-daemon/src/core/gpu/vendor_specific/mod.rs @@ -2,4 +2,3 @@ pub mod amd; pub mod intel; pub mod nvidia; pub mod virtio; -pub mod vulkan; diff --git a/crates/cardwire-daemon/src/core/gpu/nvidia_powerd.rs b/crates/cardwire-daemon/src/core/gpu/vendor_specific/nvidia.rs similarity index 69% rename from crates/cardwire-daemon/src/core/gpu/nvidia_powerd.rs rename to crates/cardwire-daemon/src/core/gpu/vendor_specific/nvidia.rs index d1305a1d..b2881552 100644 --- a/crates/cardwire-daemon/src/core/gpu/nvidia_powerd.rs +++ b/crates/cardwire-daemon/src/core/gpu/vendor_specific/nvidia.rs @@ -1,8 +1,52 @@ -use std::time::Duration; +use crate::core::gpu::models::GpuType; + +use std::{fs, path::Path, time::Duration}; use log::{error, info, warn}; use tokio::{process::Command, time::timeout}; +#[allow(unused, dead_code)] +pub fn get_nvidia_type(pci_id: &str, gpu_name: &str) -> GpuType { + GpuType::Unknown +} + +/// Get nvidia minor id +#[allow(unused, dead_code)] +pub fn nvidia_get_minor(pci_address: &str) -> Option { + let nvidia_driver_proc = Path::new("/proc/driver/nvidia/gpus/") + .join(pci_address) + .join("information"); + let information = fs::read_to_string(nvidia_driver_proc).ok()?; + information + .lines() + .find(|line| line.starts_with("Device Minor:"))? + .split_once(':')? + .1 + .trim() + .parse::() + .ok() +} + +/// find the nvidia model using the device information file +#[allow(unused, dead_code)] +pub fn nvidia_get_device_model(pci_address: &str) -> Option { + let nvidia_driver_proc = Path::new("/proc/driver/nvidia/gpus/") + .join(pci_address) + .join("information"); + let information = fs::read_to_string(nvidia_driver_proc).ok()?; + let model = information + .lines() + .find(|line| line.starts_with("Model:"))? + .split_once(':')? + .1 + .trim() + .to_string(); + match !model.is_empty() { + true => Some(model), + false => None, + } +} + const SERVICE: &str = "nvidia-powerd.service"; /// run a systemctl command against the nvidia-powerd service and log the result diff --git a/crates/cardwire-daemon/src/core/gpu/type_detection/virtio.rs b/crates/cardwire-daemon/src/core/gpu/vendor_specific/virtio.rs similarity index 100% rename from crates/cardwire-daemon/src/core/gpu/type_detection/virtio.rs rename to crates/cardwire-daemon/src/core/gpu/vendor_specific/virtio.rs From 9f20798f10b8bbfcaeb8c97cdf6b627bd22edce0 Mon Sep 17 00:00:00 2001 From: luytan Date: Sun, 13 Sep 2026 14:46:57 +0200 Subject: [PATCH 05/44] feat(cardwired): working amd and intel detection --- Cargo.lock | 21 ++- Cargo.toml | 3 +- crates/cardwire-daemon/Cargo.toml | 1 + .../src/core/gpu/enumerator.rs | 152 +++++++++++++++++- .../src/core/gpu/generic/mod.rs | 1 + .../src/core/gpu/generic/udev.rs | 55 +++++++ .../src/core/gpu/vendor_specific/amd.rs | 47 +++++- .../src/core/gpu/vendor_specific/intel.rs | 2 +- .../src/core/gpu/vendor_specific/nvidia.rs | 8 +- flake.nix | 2 + 10 files changed, 275 insertions(+), 17 deletions(-) create mode 100644 crates/cardwire-daemon/src/core/gpu/generic/udev.rs diff --git a/Cargo.lock b/Cargo.lock index 5b6feabf..6e2cb26f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -772,6 +772,7 @@ dependencies = [ "freedesktop-desktop-entry", "inotify", "khronos-egl", + "libdrm_amdgpu_sys", "log", "rusqlite", "serde", @@ -1349,7 +1350,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2511,6 +2512,16 @@ version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +[[package]] +name = "libdrm_amdgpu_sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1906d62e4f68e367feddc504e3171e649c4015c4a88a0bfbde29e317d9c2bd8f" +dependencies = [ + "libc", + "libloading", +] + [[package]] name = "libfuzzer-sys" version = "0.4.13" @@ -4096,7 +4107,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4595,10 +4606,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.3", "once_cell", "rustix 1.1.4", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -5492,7 +5503,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 8e4368e6..8da7be48 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,8 +35,9 @@ anyhow = "1.0.102" env_logger = "0.11" toml = "1.0.3" vulkano = "0.35.2" -khronos-egl = { version= "6.0.0", features = ["dynamic", "1_5"] } +khronos-egl = { version = "6.0.0", features = ["dynamic", "1_5"] } rusqlite = { version = "0.40.1", features = ["bundled"] } +libdrm_amdgpu_sys = { version = "0.9.0", default-features = false, features = ["dynamic_loading"]} # EBPF aya = "0.14.0" diff --git a/crates/cardwire-daemon/Cargo.toml b/crates/cardwire-daemon/Cargo.toml index 22f0751b..06f73407 100644 --- a/crates/cardwire-daemon/Cargo.toml +++ b/crates/cardwire-daemon/Cargo.toml @@ -28,6 +28,7 @@ vulkano.workspace = true khronos-egl.workspace = true rusqlite.workspace = true inotify.workspace = true +libdrm_amdgpu_sys.workspace = true [[bin]] name = "cardwired" diff --git a/crates/cardwire-daemon/src/core/gpu/enumerator.rs b/crates/cardwire-daemon/src/core/gpu/enumerator.rs index a74442d7..45bfe5fd 100644 --- a/crates/cardwire-daemon/src/core/gpu/enumerator.rs +++ b/crates/cardwire-daemon/src/core/gpu/enumerator.rs @@ -4,7 +4,11 @@ use log::{error, info, warn}; use crate::core::{ gpu::{ - GpuDevice, GpuVendor, check_default_drm_class, generic::{display::drm_node_ids, vulkan::Vulkan}, models::GpuType + GpuDevice, GpuVendor, check_default_drm_class, generic::{ + display::drm_node_ids, udev::{sysfs_get_device_drm, wait_for_drm}, vulkan::Vulkan + }, models::GpuType, vendor_specific::{ + amd::AmdGpuDev, intel::intel_get_device_type, nvidia::{nvidia_get_device_minor, nvidia_get_device_name, nvidia_get_device_type} + } }, pci::PciDevice }; @@ -48,6 +52,12 @@ impl GpuEnumerator { gpu_list } + /* + Gpu building: + first attempt is to use vulkan, this is easier and more precise for type detection, if not available + use per-vendor + generic methods + */ + /// Take a pci device and build a GpuDevice fn build_gpu(&self, device: &PciDevice) -> io::Result { let gpu_vendor = match device.vendor_id() { @@ -55,10 +65,12 @@ impl GpuEnumerator { // Default to "Other" None => GpuVendor::default(), }; + let pci_id = device.pci_address(); + // Wait for DRM to be ready, each attempt take 250ms + let _ = wait_for_drm(pci_id, 5); // Check if the gpu info can be fetched using vulkan, if so use vulkan to build the GPU - if self.vulkan.vulkan_compatible(device.pci_address()) { - let pci_id = device.pci_address(); + if !self.vulkan.vulkan_compatible(device.pci_address()) { let gpu_type = self.vulkan.get_gpu_type(pci_id); let gpu_name = self.vulkan.get_gpu_name(pci_id); @@ -80,6 +92,140 @@ impl GpuEnumerator { // else, fallback to sysfs GPU building + // For now only support the popular GPU vendors, more can be added inthe future + match gpu_vendor { + // We use /proc/driver/nvidia/gpus for nvidia devices + // TODO: add another match for nova driver + GpuVendor::Nvidia => { + // Try to get the device name using nvidia driver, if fail, use hwdata, then + // fallback to unknown + let gpu_name = nvidia_get_device_name(pci_id).unwrap_or_else(|| { + device + .device_name() + .clone() + .unwrap_or_else(|| "Unknown Device".to_string()) + }); + let drm_res = sysfs_get_device_drm(pci_id); + // return a working GPU if drm available, else return a non-available GPU + match drm_res { + Some((card, render)) => { + let gpu_type = nvidia_get_device_type(pci_id, &gpu_name); + let nvidia_minor = nvidia_get_device_minor(pci_id); + let gpu_device = GpuDevice::new( + gpu_name, + device.clone(), + render, + card, + None, + gpu_vendor, + nvidia_minor, + gpu_type, + ); + return Ok(gpu_device); + } + None => { + // Couldn't get DRM, mark GPU as not available + let gpu_type = GpuType::Unavailable; + let gpu_device = GpuDevice::new( + gpu_name, + device.clone(), + u32::MAX, + u32::MAX, + None, + gpu_vendor, + None, + gpu_type, + ); + return Ok(gpu_device); + } + } + } + GpuVendor::Intel => { + // Use Hwdata for intel + let gpu_name = device + .device_name() + .clone() + .unwrap_or_else(|| "Unknown Device".to_string()); + let drm_res = sysfs_get_device_drm(pci_id); + match drm_res { + Some((card, render)) => { + let gpu_type = intel_get_device_type(pci_id); + let gpu_device = GpuDevice::new( + gpu_name, + device.clone(), + render, + card, + None, + gpu_vendor, + None, + gpu_type, + ); + return Ok(gpu_device); + } + None => { + // Couldn't get DRM, mark GPU as not available + let gpu_type = GpuType::Unavailable; + let gpu_device = GpuDevice::new( + gpu_name, + device.clone(), + u32::MAX, + u32::MAX, + None, + gpu_vendor, + None, + gpu_type, + ); + return Ok(gpu_device); + } + } + } + GpuVendor::Amd => { + // For AMD, we fetch infos using libdrm_amdgpu if DRM nodes are availables + let gpu_name = device + .device_name() + .clone() + .unwrap_or_else(|| "Unknown Device".to_string()); + let drm_res = sysfs_get_device_drm(pci_id); + match drm_res { + Some((card, render)) => { + let amdgpu = AmdGpuDev::new(render); + + let gpu_type = amdgpu.amd_get_device_type(); + // amdgpu is more precise + let gpu_name = amdgpu.amd_get_device_name(); + let gpu_device = GpuDevice::new( + gpu_name, + device.clone(), + render, + card, + None, + gpu_vendor, + None, + gpu_type, + ); + println!("{}: {:?}", gpu_device.name(), gpu_device.device_type()); + return Ok(gpu_device); + } + None => { + // Couldn't get DRM, mark GPU as not available + let gpu_type = GpuType::Unavailable; + let gpu_device = GpuDevice::new( + gpu_name, + device.clone(), + u32::MAX, + u32::MAX, + None, + gpu_vendor, + None, + gpu_type, + ); + return Ok(gpu_device); + } + } + } + _ => todo!(), + }; + // Try with vulkan first //let device_name = (|| match gpu_vendor { // // Use the driver info diff --git a/crates/cardwire-daemon/src/core/gpu/generic/mod.rs b/crates/cardwire-daemon/src/core/gpu/generic/mod.rs index 61f24bf3..040ff0ef 100644 --- a/crates/cardwire-daemon/src/core/gpu/generic/mod.rs +++ b/crates/cardwire-daemon/src/core/gpu/generic/mod.rs @@ -1,4 +1,5 @@ //! Generic functions that should work on all GPUs pub mod default_gpu; pub mod display; +pub mod udev; pub mod vulkan; diff --git a/crates/cardwire-daemon/src/core/gpu/generic/udev.rs b/crates/cardwire-daemon/src/core/gpu/generic/udev.rs new file mode 100644 index 00000000..3d87316f --- /dev/null +++ b/crates/cardwire-daemon/src/core/gpu/generic/udev.rs @@ -0,0 +1,55 @@ +use std::{path::Path, thread, time::Duration}; + +/// Wait for the device to be initialized +pub fn wait_for_drm(pci_id: &str, retries: usize) -> bool { + let drm = Path::new("/sys/bus/pci/devices").join(pci_id).join("drm"); + + for _attempt in 0..retries { + if drm.read_dir().is_ok_and(|mut d| d.next().is_some()) { + return false; + } + thread::sleep(Duration::from_millis(250)); + } + false +} + +/// Get the drm nodes using sysfs +pub fn sysfs_get_device_drm(pci_id: &str) -> Option<(u32, u32)> { + let syspath = Path::new("/sys/bus/pci/devices").join(pci_id).join("drm"); + let drm = syspath.read_dir().ok()?; + + // index 0 = card + // index 1 = render + let mut drm_nodes: Vec = vec![0; 2]; + + for entry in drm.flatten() { + if let Some(str) = entry.file_name().to_str() + && str.contains("card") + { + let minor_s_opt = str.strip_prefix("card"); + if let Some(minor_s) = minor_s_opt + && let Ok(minor_int) = minor_s.parse::() + { + drm_nodes[0] = minor_int; + continue; + } + } + if let Some(str) = entry.file_name().to_str() + && str.contains("renderD") + { + let minor_s_opt = str.strip_prefix("renderD"); + if let Some(minor_s) = minor_s_opt + && let Ok(minor_int) = minor_s.parse::() + { + drm_nodes[1] = minor_int; + continue; + } + } + } + + if drm_nodes.is_empty() { + None + } else { + Some((drm_nodes[0] as u32, drm_nodes[1] as u32)) + } +} diff --git a/crates/cardwire-daemon/src/core/gpu/vendor_specific/amd.rs b/crates/cardwire-daemon/src/core/gpu/vendor_specific/amd.rs index 7574adca..e7bbec53 100644 --- a/crates/cardwire-daemon/src/core/gpu/vendor_specific/amd.rs +++ b/crates/cardwire-daemon/src/core/gpu/vendor_specific/amd.rs @@ -1,8 +1,13 @@ -use std::fs; +use libdrm_amdgpu_sys::{ + AMDGPU::{DeviceHandle, GPU_INFO}, LibDrmAmdgpu +}; + +use crate::core::gpu::models::GpuType; +use std::{fs, path::Path}; /// Find the amd model using amdgpu.ids, require the device id and the revision for precise matching #[allow(unused, dead_code)] -pub fn amd_get_device_model(device_id: &str, pci: &str) -> Option { +pub fn amd_get_device_name(device_id: &str, pci: &str) -> Option { let path = "/usr/share/libdrm/amdgpu.ids"; let device_id = device_id.to_string().replace("0x", "").to_ascii_uppercase(); @@ -37,3 +42,41 @@ pub fn amd_get_device_model(device_id: &str, pci: &str) -> Option { None } + +pub struct AmdGpuDev { + amdgpu_dev: DeviceHandle, +} +impl AmdGpuDev { + pub fn new(render: u32) -> Self { + let libdrm_amdgpu = LibDrmAmdgpu::new().unwrap(); + let (amdgpu_dev, _drm_major, _drm_minor) = { + use std::fs::OpenOptions; + let path = format!("/dev/dri/renderD{}", render); + let f = OpenOptions::new() + .read(true) + .write(true) + .open(path) + .unwrap(); + + libdrm_amdgpu.init_device_handle_with_fd(f).unwrap() + }; + Self { amdgpu_dev } + } + + /// Get the AMD gpu type using amdgpu_gpu_info + pub fn amd_get_device_type(&self) -> GpuType { + const AMDGPU_IDS_FLAGS_FUSION: u64 = 0x01; + let gpu_info = self.amdgpu_dev.query_gpu_info().unwrap(); + let fusion = gpu_info.ids_flags & AMDGPU_IDS_FLAGS_FUSION; + if fusion == 0 { + GpuType::Discrete + } else { + GpuType::Integrated + } + } + + pub fn amd_get_device_name(&self) -> String { + let gpu_info = self.amdgpu_dev.device_info().unwrap(); + gpu_info.find_device_name_or_default() + } +} diff --git a/crates/cardwire-daemon/src/core/gpu/vendor_specific/intel.rs b/crates/cardwire-daemon/src/core/gpu/vendor_specific/intel.rs index f2d95c75..c9adeeae 100644 --- a/crates/cardwire-daemon/src/core/gpu/vendor_specific/intel.rs +++ b/crates/cardwire-daemon/src/core/gpu/vendor_specific/intel.rs @@ -2,7 +2,7 @@ use crate::core::gpu::models::GpuType; /// Get the gpu type for an intel GPU #[allow(unused, dead_code)] -pub fn get_intel_type(pci_id: &str) -> GpuType { +pub fn intel_get_device_type(pci_id: &str) -> GpuType { // PCI id reserved for iGPUs if pci_id == "0000:00:02.0" { GpuType::Integrated diff --git a/crates/cardwire-daemon/src/core/gpu/vendor_specific/nvidia.rs b/crates/cardwire-daemon/src/core/gpu/vendor_specific/nvidia.rs index b2881552..84b10570 100644 --- a/crates/cardwire-daemon/src/core/gpu/vendor_specific/nvidia.rs +++ b/crates/cardwire-daemon/src/core/gpu/vendor_specific/nvidia.rs @@ -6,13 +6,12 @@ use log::{error, info, warn}; use tokio::{process::Command, time::timeout}; #[allow(unused, dead_code)] -pub fn get_nvidia_type(pci_id: &str, gpu_name: &str) -> GpuType { +pub fn nvidia_get_device_type(pci_id: &str, gpu_name: &str) -> GpuType { GpuType::Unknown } /// Get nvidia minor id -#[allow(unused, dead_code)] -pub fn nvidia_get_minor(pci_address: &str) -> Option { +pub fn nvidia_get_device_minor(pci_address: &str) -> Option { let nvidia_driver_proc = Path::new("/proc/driver/nvidia/gpus/") .join(pci_address) .join("information"); @@ -28,8 +27,7 @@ pub fn nvidia_get_minor(pci_address: &str) -> Option { } /// find the nvidia model using the device information file -#[allow(unused, dead_code)] -pub fn nvidia_get_device_model(pci_address: &str) -> Option { +pub fn nvidia_get_device_name(pci_address: &str) -> Option { let nvidia_driver_proc = Path::new("/proc/driver/nvidia/gpus/") .join(pci_address) .join("information"); diff --git a/flake.nix b/flake.nix index ecf014d7..fdfdf318 100644 --- a/flake.nix +++ b/flake.nix @@ -76,6 +76,7 @@ (pkgs system).egl-wayland (pkgs system).egl-x11 (pkgs system).libglvnd + (pkgs system).libdrm ] ++ self.checks.${system}.pre-commit-check.enabledPackages; LD_LIBRARY_PATH = (pkgs system).lib.makeLibraryPath [ @@ -89,6 +90,7 @@ (pkgs system).egl-wayland (pkgs system).egl-x11 (pkgs system).libglvnd + (pkgs system).libdrm ]; LIBCLANG_PATH = "${(pkgs system).llvmPackages.libclang.lib}/lib"; RUST_SRC_PATH = "${toolchainFor system}/lib/rustlib/src/rust/library"; From acbf25acf14c42280adde2b70c9fedeeb66ff0b6 Mon Sep 17 00:00:00 2001 From: luytan Date: Sun, 13 Sep 2026 15:28:34 +0200 Subject: [PATCH 06/44] refactor(cardwired): move amdgpu into a struct, caching the query --- crates/cardwire-daemon/src/core/errors.rs | 3 +++ .../src/core/gpu/vendor_specific/amd.rs | 25 ++++++++++++------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/crates/cardwire-daemon/src/core/errors.rs b/crates/cardwire-daemon/src/core/errors.rs index 94ca97ed..dca9c43d 100644 --- a/crates/cardwire-daemon/src/core/errors.rs +++ b/crates/cardwire-daemon/src/core/errors.rs @@ -44,6 +44,9 @@ pub enum CardwireError { #[error("Error with state_file {0}: {1}")] CardwireStateError(String, serde_json::Error), + #[error("Failed to query amdgpu info {0}")] + CardwireAmdGpuError(i32), + // Mode errors #[error("unknown mode: {0}")] UnknownMode(u32), diff --git a/crates/cardwire-daemon/src/core/gpu/vendor_specific/amd.rs b/crates/cardwire-daemon/src/core/gpu/vendor_specific/amd.rs index e7bbec53..0a0b3471 100644 --- a/crates/cardwire-daemon/src/core/gpu/vendor_specific/amd.rs +++ b/crates/cardwire-daemon/src/core/gpu/vendor_specific/amd.rs @@ -1,12 +1,15 @@ use libdrm_amdgpu_sys::{ - AMDGPU::{DeviceHandle, GPU_INFO}, LibDrmAmdgpu + AMDGPU::{DeviceHandle, GPU_INFO, amdgpu_gpu_info}, LibDrmAmdgpu }; -use crate::core::gpu::models::GpuType; -use std::{fs, path::Path}; +use crate::{ + Result, core::{errors::CardwireError::CardwireAmdGpuError, gpu::models::GpuType} +}; +use std::fs; /// Find the amd model using amdgpu.ids, require the device id and the revision for precise matching #[allow(unused, dead_code)] +#[deprecated] pub fn amd_get_device_name(device_id: &str, pci: &str) -> Option { let path = "/usr/share/libdrm/amdgpu.ids"; let device_id = device_id.to_string().replace("0x", "").to_ascii_uppercase(); @@ -44,10 +47,12 @@ pub fn amd_get_device_name(device_id: &str, pci: &str) -> Option { } pub struct AmdGpuDev { + #[allow(unused)] amdgpu_dev: DeviceHandle, + amdgpu_gpu_info: amdgpu_gpu_info, } impl AmdGpuDev { - pub fn new(render: u32) -> Self { + pub fn new(render: u32) -> Result { let libdrm_amdgpu = LibDrmAmdgpu::new().unwrap(); let (amdgpu_dev, _drm_major, _drm_minor) = { use std::fs::OpenOptions; @@ -60,14 +65,17 @@ impl AmdGpuDev { libdrm_amdgpu.init_device_handle_with_fd(f).unwrap() }; - Self { amdgpu_dev } + let gpu_info = amdgpu_dev.query_gpu_info().map_err(CardwireAmdGpuError)?; + Ok(Self { + amdgpu_dev, + amdgpu_gpu_info: gpu_info, + }) } /// Get the AMD gpu type using amdgpu_gpu_info pub fn amd_get_device_type(&self) -> GpuType { const AMDGPU_IDS_FLAGS_FUSION: u64 = 0x01; - let gpu_info = self.amdgpu_dev.query_gpu_info().unwrap(); - let fusion = gpu_info.ids_flags & AMDGPU_IDS_FLAGS_FUSION; + let fusion = self.amdgpu_gpu_info.ids_flags & AMDGPU_IDS_FLAGS_FUSION; if fusion == 0 { GpuType::Discrete } else { @@ -76,7 +84,6 @@ impl AmdGpuDev { } pub fn amd_get_device_name(&self) -> String { - let gpu_info = self.amdgpu_dev.device_info().unwrap(); - gpu_info.find_device_name_or_default() + self.amdgpu_gpu_info.find_device_name_or_default() } } From f2f5c9c355b2e28ccd9aac308f448b4c49d4828b Mon Sep 17 00:00:00 2001 From: luytan Date: Sun, 13 Sep 2026 15:29:11 +0200 Subject: [PATCH 07/44] feat(cardwired): also check the driver and act corresponding to it --- .../src/core/gpu/enumerator.rs | 181 ++++++++---------- 1 file changed, 78 insertions(+), 103 deletions(-) diff --git a/crates/cardwire-daemon/src/core/gpu/enumerator.rs b/crates/cardwire-daemon/src/core/gpu/enumerator.rs index 45bfe5fd..df0cf70b 100644 --- a/crates/cardwire-daemon/src/core/gpu/enumerator.rs +++ b/crates/cardwire-daemon/src/core/gpu/enumerator.rs @@ -5,7 +5,7 @@ use log::{error, info, warn}; use crate::core::{ gpu::{ GpuDevice, GpuVendor, check_default_drm_class, generic::{ - display::drm_node_ids, udev::{sysfs_get_device_drm, wait_for_drm}, vulkan::Vulkan + udev::{sysfs_get_device_drm, wait_for_drm}, vulkan::Vulkan }, models::GpuType, vendor_specific::{ amd::AmdGpuDev, intel::intel_get_device_type, nvidia::{nvidia_get_device_minor, nvidia_get_device_name, nvidia_get_device_type} } @@ -21,6 +21,7 @@ impl GpuEnumerator { let vulkan = Vulkan::build(); Self { vulkan } } + /// Enumerate the GPUS on the host system pub fn enumerate(&self, pci_list: &BTreeMap) -> BTreeMap { let mut gpu_list: BTreeMap = BTreeMap::new(); @@ -180,112 +181,86 @@ impl GpuEnumerator { } } GpuVendor::Amd => { - // For AMD, we fetch infos using libdrm_amdgpu if DRM nodes are availables - let gpu_name = device - .device_name() - .clone() - .unwrap_or_else(|| "Unknown Device".to_string()); - let drm_res = sysfs_get_device_drm(pci_id); - match drm_res { - Some((card, render)) => { - let amdgpu = AmdGpuDev::new(render); - - let gpu_type = amdgpu.amd_get_device_type(); - // amdgpu is more precise - let gpu_name = amdgpu.amd_get_device_name(); - let gpu_device = GpuDevice::new( - gpu_name, - device.clone(), - render, - card, - None, - gpu_vendor, - None, - gpu_type, - ); - println!("{}: {:?}", gpu_device.name(), gpu_device.device_type()); - return Ok(gpu_device); - } - None => { - // Couldn't get DRM, mark GPU as not available - let gpu_type = GpuType::Unavailable; - let gpu_device = GpuDevice::new( - gpu_name, - device.clone(), - u32::MAX, - u32::MAX, - None, - gpu_vendor, - None, - gpu_type, - ); - return Ok(gpu_device); + // Only support for amdgpu will be added, radeon will be considered on user demand + if device.driver().clone().is_some_and(|d| d == "amdgpu") { + // For AMD, we fetch infos using libdrm_amdgpu if DRM nodes are availables + let gpu_name = device + .device_name() + .clone() + .unwrap_or_else(|| "Unknown Device".to_string()); + let drm_res = sysfs_get_device_drm(pci_id); + match drm_res { + Some((card, render)) => { + let gpu_device = if let Ok(amdgpu) = AmdGpuDev::new(render) { + let gpu_type = amdgpu.amd_get_device_type(); + // amdgpu is more precise + let gpu_name = amdgpu.amd_get_device_name(); + GpuDevice::new( + gpu_name, + device.clone(), + render, + card, + None, + gpu_vendor, + None, + gpu_type, + ) + } else { + // If we cannot use AMDGPU, mark device is unknown until i find a + // reliable way to detect discrete using sysfs + let gpu_type = GpuType::Unknown; + GpuDevice::new( + gpu_name, + device.clone(), + render, + card, + None, + gpu_vendor, + None, + gpu_type, + ) + }; + println!("{}: {:?}", gpu_device.name(), gpu_device.device_type()); + return Ok(gpu_device); + } + None => { + // Couldn't get DRM, mark GPU as not available + let gpu_type = GpuType::Unavailable; + let gpu_device = GpuDevice::new( + gpu_name, + device.clone(), + u32::MAX, + u32::MAX, + None, + gpu_vendor, + None, + gpu_type, + ); + return Ok(gpu_device); + } } + } else { + println!("not amdgpu"); + // Couldn't get DRM, mark GPU as not available + let gpu_type = GpuType::Unavailable; + let gpu_name = device + .device_name() + .clone() + .unwrap_or_else(|| "Unknown Device".to_string()); + let gpu_device = GpuDevice::new( + gpu_name, + device.clone(), + u32::MAX, + u32::MAX, + None, + gpu_vendor, + None, + gpu_type, + ); + return Ok(gpu_device); } } _ => todo!(), }; - - // Try with vulkan first - //let device_name = (|| match gpu_vendor { - // // Use the driver info - // GpuVendor::Nvidia => nvidia_get_device_model(device.pci_address()), - // // use amdgpu.ids - // GpuVendor::Amd => device - // .device_id() - // .as_ref() - // .and_then(|id| amd_get_device_model(id, device.pci_address())), - // _ => None, - // }) - // // Fallback to hwdata - // .or_else(|| { - // warn!("Couldn't get device_name, falling back to hwdata"); - // device.device_name().clone() - // }) - // // fallback default - // .unwrap_or_else(|| { - // warn!("Couldn't get name using hwdata, falling back to default"); - // "Unknown Device".to_string() - // }); - - let gpu_name = String::new(); - - // If the GPU is bound to vfio, mark it as unavailable - if let Some(driver) = device.driver() - && driver.contains("vfio-") - { - info!("Device: {} is bound to: {}", gpu_name, driver); - return Ok(GpuDevice::new( - gpu_name, - device.clone(), - u32::MAX, - u32::MAX, - None, - gpu_vendor, - None, - GpuType::Unavailable, - )); - } - - // Available is used to know if the device should be used by cardwire or not - let (card, render, _available) = match drm_node_ids(device.pci_address()) { - Ok((c, r)) => (c, r, true), - Err(err) => { - error!("{}: Couldn't get drm node IDs: {}", gpu_name, err); - (u32::MAX, u32::MAX, false) - } - }; - let device_type = GpuType::Unknown; - - Ok(GpuDevice::new( - gpu_name, - device.clone(), - render, - card, - None, - gpu_vendor, - None, - device_type, - )) } } From 630c64f9d8a98b7acd8d57454ed9f79e1e111064 Mon Sep 17 00:00:00 2001 From: luytan Date: Sun, 13 Sep 2026 16:03:03 +0200 Subject: [PATCH 08/44] feat(cardwired): handle specific drivers during enum and logs errors --- .../src/core/gpu/enumerator.rs | 198 +++++++++++++----- crates/cardwire-daemon/src/core/gpu/models.rs | 2 +- crates/cardwire-daemon/src/core/pci/models.rs | 2 +- 3 files changed, 153 insertions(+), 49 deletions(-) diff --git a/crates/cardwire-daemon/src/core/gpu/enumerator.rs b/crates/cardwire-daemon/src/core/gpu/enumerator.rs index df0cf70b..3756b193 100644 --- a/crates/cardwire-daemon/src/core/gpu/enumerator.rs +++ b/crates/cardwire-daemon/src/core/gpu/enumerator.rs @@ -1,6 +1,6 @@ use std::{collections::BTreeMap, io}; -use log::{error, info, warn}; +use log::{debug, error, info, warn}; use crate::core::{ gpu::{ @@ -71,7 +71,7 @@ impl GpuEnumerator { let _ = wait_for_drm(pci_id, 5); // Check if the gpu info can be fetched using vulkan, if so use vulkan to build the GPU - if !self.vulkan.vulkan_compatible(device.pci_address()) { + if self.vulkan.vulkan_compatible(device.pci_address()) { let gpu_type = self.vulkan.get_gpu_type(pci_id); let gpu_name = self.vulkan.get_gpu_name(pci_id); @@ -88,6 +88,8 @@ impl GpuEnumerator { None, gpu_type, ); + info!("{}: Used Vulkan to build", gpu_device.name()); + debug!("{:?}", gpu_device); return Ok(gpu_device); } @@ -98,51 +100,85 @@ impl GpuEnumerator { // We use /proc/driver/nvidia/gpus for nvidia devices // TODO: add another match for nova driver GpuVendor::Nvidia => { - // Try to get the device name using nvidia driver, if fail, use hwdata, then - // fallback to unknown - let gpu_name = nvidia_get_device_name(pci_id).unwrap_or_else(|| { - device + // Proprietary nvidia driver, supported by cardwire + if device.driver().clone().is_some_and(|d| d == "nvidia") { + // Try to get the device name using nvidia driver, if fail, use hwdata, then + // fallback to unknown + let gpu_name = nvidia_get_device_name(pci_id).unwrap_or_else(|| { + device + .device_name() + .clone() + .unwrap_or_else(|| "Unknown Device".to_string()) + }); + + let drm_res = sysfs_get_device_drm(pci_id); + // return a working GPU if drm available, else return a non-available GPU + match drm_res { + Some((card, render)) => { + let gpu_type = nvidia_get_device_type(pci_id, &gpu_name); + let nvidia_minor = nvidia_get_device_minor(pci_id); + let gpu_device = GpuDevice::new( + gpu_name, + device.clone(), + render, + card, + None, + gpu_vendor, + nvidia_minor, + gpu_type, + ); + info!("{}: Used Nvidia to build", gpu_device.name()); + debug!("{:?}", gpu_device); + return Ok(gpu_device); + } + None => { + // Couldn't get DRM, mark GPU as not available + let gpu_type = GpuType::Unavailable; + let gpu_device = GpuDevice::new( + gpu_name, + device.clone(), + u32::MAX, + u32::MAX, + None, + gpu_vendor, + None, + gpu_type, + ); + error!( + "{}: Cannot fetch DRM nodes, marking as un-available", + gpu_device.name() + ); + return Ok(gpu_device); + } + } + } else { + // Not a driver cardwire supports (eg nova), mark GPU as not available until + // support is added + let gpu_type = GpuType::Unavailable; + let gpu_name = device .device_name() .clone() - .unwrap_or_else(|| "Unknown Device".to_string()) - }); - let drm_res = sysfs_get_device_drm(pci_id); - // return a working GPU if drm available, else return a non-available GPU - match drm_res { - Some((card, render)) => { - let gpu_type = nvidia_get_device_type(pci_id, &gpu_name); - let nvidia_minor = nvidia_get_device_minor(pci_id); - let gpu_device = GpuDevice::new( - gpu_name, - device.clone(), - render, - card, - None, - gpu_vendor, - nvidia_minor, - gpu_type, - ); - return Ok(gpu_device); - } - None => { - // Couldn't get DRM, mark GPU as not available - let gpu_type = GpuType::Unavailable; - let gpu_device = GpuDevice::new( - gpu_name, - device.clone(), - u32::MAX, - u32::MAX, - None, - gpu_vendor, - None, - gpu_type, - ); - return Ok(gpu_device); - } + .unwrap_or_else(|| "Unknown Device".to_string()); + let gpu_device = GpuDevice::new( + gpu_name, + device.clone(), + u32::MAX, + u32::MAX, + None, + gpu_vendor, + None, + gpu_type, + ); + error!( + "{}: driver {:?} is not supported by Cardwire, please request it on Github", + gpu_device.name(), + device.driver() + ); + return Ok(gpu_device); } } GpuVendor::Intel => { - // Use Hwdata for intel + // I think i915 and Xe should work the same let gpu_name = device .device_name() .clone() @@ -161,6 +197,8 @@ impl GpuEnumerator { None, gpu_type, ); + info!("{}: Used Intel to build", gpu_device.name()); + debug!("{:?}", gpu_device); return Ok(gpu_device); } None => { @@ -176,6 +214,10 @@ impl GpuEnumerator { None, gpu_type, ); + error!( + "{}: Cannot fetch DRM nodes, marking as un-available", + gpu_device.name() + ); return Ok(gpu_device); } } @@ -195,6 +237,7 @@ impl GpuEnumerator { let gpu_type = amdgpu.amd_get_device_type(); // amdgpu is more precise let gpu_name = amdgpu.amd_get_device_name(); + info!("{}: Used AMDGPU to build", gpu_name); GpuDevice::new( gpu_name, device.clone(), @@ -206,9 +249,13 @@ impl GpuEnumerator { gpu_type, ) } else { - // If we cannot use AMDGPU, mark device is unknown until i find a + // If we cannot use AMDGPU, mark device as unknown until i find a // reliable way to detect discrete using sysfs let gpu_type = GpuType::Unknown; + error!( + "{}: cannot use amdgpu for type detection, please report on Github", + gpu_name + ); GpuDevice::new( gpu_name, device.clone(), @@ -220,7 +267,7 @@ impl GpuEnumerator { gpu_type, ) }; - println!("{}: {:?}", gpu_device.name(), gpu_device.device_type()); + debug!("{:?}", gpu_device); return Ok(gpu_device); } None => { @@ -236,12 +283,14 @@ impl GpuEnumerator { None, gpu_type, ); + error!( + "{}: cannot fetch DRM nodes, marking as un-available", + gpu_device.name() + ); return Ok(gpu_device); } } } else { - println!("not amdgpu"); - // Couldn't get DRM, mark GPU as not available let gpu_type = GpuType::Unavailable; let gpu_name = device .device_name() @@ -257,10 +306,65 @@ impl GpuEnumerator { None, gpu_type, ); + error!( + "{}: driver {:?} is not supported by Cardwire, please request support for it on Github", + gpu_device.name(), + device.driver() + ); return Ok(gpu_device); } } - _ => todo!(), + // Cardwire depends on knowing the GPU type for the modes, mark Other devices as + // unknown, leaving only hybrid and manual available until support added + GpuVendor::Other => { + let gpu_name = device + .device_name() + .clone() + .unwrap_or_else(|| "Unknown Device".to_string()); + let drm_res = sysfs_get_device_drm(pci_id); + match drm_res { + Some((card, render)) => { + let gpu_type = GpuType::Unknown; + let gpu_device = GpuDevice::new( + gpu_name, + device.clone(), + render, + card, + None, + gpu_vendor, + None, + gpu_type, + ); + warn!( + "{}: unknown device vendor ({:?}/{:?}), please request support for it on Github", + gpu_device.name(), + device.vendor_id(), + device.vendor_name() + ); + debug!("{:?}", gpu_device); + return Ok(gpu_device); + } + None => { + // Couldn't get DRM, mark GPU as not available + let gpu_type = GpuType::Unavailable; + let gpu_device = GpuDevice::new( + gpu_name, + device.clone(), + u32::MAX, + u32::MAX, + None, + gpu_vendor, + None, + gpu_type, + ); + error!( + "{}: cannot fetch DRM nodes, marking as un-available", + gpu_device.name() + ); + return Ok(gpu_device); + } + } + } }; } } diff --git a/crates/cardwire-daemon/src/core/gpu/models.rs b/crates/cardwire-daemon/src/core/gpu/models.rs index 3f14ea90..22440253 100644 --- a/crates/cardwire-daemon/src/core/gpu/models.rs +++ b/crates/cardwire-daemon/src/core/gpu/models.rs @@ -97,7 +97,7 @@ pub enum GpuType { Unknown, } -#[derive(Clone, serde::Serialize, serde::Deserialize, zbus::zvariant::Type, PartialEq)] +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, zbus::zvariant::Type, PartialEq)] pub struct GpuDevice { name: String, pub pci: PciDevice, diff --git a/crates/cardwire-daemon/src/core/pci/models.rs b/crates/cardwire-daemon/src/core/pci/models.rs index 4cceca13..c689296d 100644 --- a/crates/cardwire-daemon/src/core/pci/models.rs +++ b/crates/cardwire-daemon/src/core/pci/models.rs @@ -1,4 +1,4 @@ -#[derive(Clone, serde::Serialize, serde::Deserialize, zbus::zvariant::Type, PartialEq)] +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, zbus::zvariant::Type, PartialEq)] pub struct PciDevice { pci_address: String, iommu_group: Option, From a03f23fb9a464080726c2a6e5233735c6428f2da Mon Sep 17 00:00:00 2001 From: luytan Date: Sun, 13 Sep 2026 16:03:17 +0200 Subject: [PATCH 09/44] feat(cardwired): clean Vulkan device name --- crates/cardwire-daemon/src/core/gpu/generic/vulkan.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/cardwire-daemon/src/core/gpu/generic/vulkan.rs b/crates/cardwire-daemon/src/core/gpu/generic/vulkan.rs index 70ff79a8..0506b409 100644 --- a/crates/cardwire-daemon/src/core/gpu/generic/vulkan.rs +++ b/crates/cardwire-daemon/src/core/gpu/generic/vulkan.rs @@ -62,8 +62,8 @@ impl Vulkan { { name = vlk_dev.properties().device_name.clone(); } - - name + let cleaned_name = name.split('(').next().unwrap_or(&name).trim().to_string(); + cleaned_name } /// Get the gpu render node using vulkan pub fn get_gpu_render(&self, pci_id: &str) -> Option { From e6567d4cedfcfc0c2ea916e07504afdce498cc90 Mon Sep 17 00:00:00 2001 From: luytan Date: Sun, 13 Sep 2026 16:03:36 +0200 Subject: [PATCH 10/44] feat(cardwired): empty nova file --- crates/cardwire-daemon/src/core/gpu/vendor_specific/nova.rs | 1 + 1 file changed, 1 insertion(+) create mode 100644 crates/cardwire-daemon/src/core/gpu/vendor_specific/nova.rs diff --git a/crates/cardwire-daemon/src/core/gpu/vendor_specific/nova.rs b/crates/cardwire-daemon/src/core/gpu/vendor_specific/nova.rs new file mode 100644 index 00000000..ac87873d --- /dev/null +++ b/crates/cardwire-daemon/src/core/gpu/vendor_specific/nova.rs @@ -0,0 +1 @@ +//! For the future nova driver \ No newline at end of file From 1b6315ae986ccdb31b9108dadbf68e1993b49714 Mon Sep 17 00:00:00 2001 From: luytan Date: Sun, 13 Sep 2026 17:52:35 +0200 Subject: [PATCH 11/44] feat(cardwired): nvidia detection --- Cargo.lock | 83 ++++++++++++++ Cargo.toml | 1 + crates/cardwire-daemon/Cargo.toml | 1 + crates/cardwire-daemon/src/core/errors.rs | 4 + .../src/core/gpu/enumerator.rs | 70 +++++++++++- .../src/core/gpu/vendor_specific/nvidia.rs | 107 +++++++++++++++++- 6 files changed, 261 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6e2cb26f..9c1638a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -774,6 +774,7 @@ dependencies = [ "khronos-egl", "libdrm_amdgpu_sys", "log", + "nvml-wrapper", "rusqlite", "serde", "serde_json", @@ -1178,6 +1179,41 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.119", +] + [[package]] name = "data-url" version = "0.3.2" @@ -1476,6 +1512,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8" +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.1.5" @@ -2198,6 +2240,12 @@ dependencies = [ "winit", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "image" version = "0.25.10" @@ -3024,6 +3072,29 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "nvml-wrapper" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d164abbde0b3c03edb9edb9cb8d31a7f5b79015c692b7c771f6e0840e9106b9f" +dependencies = [ + "bitflags 2.13.1", + "libloading", + "nvml-wrapper-sys", + "static_assertions", + "thiserror 1.0.69", + "wrapcenum-derive", +] + +[[package]] +name = "nvml-wrapper-sys" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d2079f4c9b6d2170bfb71c6355734ead6c47da75c179847395c31f9f2f66ede" +dependencies = [ + "libloading", +] + [[package]] name = "objc" version = "0.2.7" @@ -5910,6 +5981,18 @@ version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +[[package]] +name = "wrapcenum-derive" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a76ff259533532054cfbaefb115c613203c73707017459206380f03b3b3f266e" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "x11-dl" version = "2.21.0" diff --git a/Cargo.toml b/Cargo.toml index 8da7be48..19296195 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ vulkano = "0.35.2" khronos-egl = { version = "6.0.0", features = ["dynamic", "1_5"] } rusqlite = { version = "0.40.1", features = ["bundled"] } libdrm_amdgpu_sys = { version = "0.9.0", default-features = false, features = ["dynamic_loading"]} +nvml-wrapper = "0.13.0" # EBPF aya = "0.14.0" diff --git a/crates/cardwire-daemon/Cargo.toml b/crates/cardwire-daemon/Cargo.toml index 06f73407..a7be62ee 100644 --- a/crates/cardwire-daemon/Cargo.toml +++ b/crates/cardwire-daemon/Cargo.toml @@ -29,6 +29,7 @@ khronos-egl.workspace = true rusqlite.workspace = true inotify.workspace = true libdrm_amdgpu_sys.workspace = true +nvml-wrapper.workspace = true [[bin]] name = "cardwired" diff --git a/crates/cardwire-daemon/src/core/errors.rs b/crates/cardwire-daemon/src/core/errors.rs index dca9c43d..f2c03af2 100644 --- a/crates/cardwire-daemon/src/core/errors.rs +++ b/crates/cardwire-daemon/src/core/errors.rs @@ -1,3 +1,4 @@ +use nvml_wrapper::error::NvmlError; use std::{io, path}; use thiserror::Error; @@ -47,6 +48,9 @@ pub enum CardwireError { #[error("Failed to query amdgpu info {0}")] CardwireAmdGpuError(i32), + #[error("Failed to init Nvml {0}")] + CardwireNvmlError(NvmlError), + // Mode errors #[error("unknown mode: {0}")] UnknownMode(u32), diff --git a/crates/cardwire-daemon/src/core/gpu/enumerator.rs b/crates/cardwire-daemon/src/core/gpu/enumerator.rs index 3756b193..643ad847 100644 --- a/crates/cardwire-daemon/src/core/gpu/enumerator.rs +++ b/crates/cardwire-daemon/src/core/gpu/enumerator.rs @@ -7,7 +7,9 @@ use crate::core::{ GpuDevice, GpuVendor, check_default_drm_class, generic::{ udev::{sysfs_get_device_drm, wait_for_drm}, vulkan::Vulkan }, models::GpuType, vendor_specific::{ - amd::AmdGpuDev, intel::intel_get_device_type, nvidia::{nvidia_get_device_minor, nvidia_get_device_name, nvidia_get_device_type} + amd::AmdGpuDev, intel::intel_get_device_type, nvidia::{ + nvidia_get_device_minor, nvidia_get_device_minor_nvml, nvidia_get_device_name, nvidia_get_device_name_nvml, nvidia_get_device_type, nvidia_get_device_type_nvml, wait_for_nvidia + } } }, pci::PciDevice }; @@ -26,6 +28,7 @@ impl GpuEnumerator { let mut gpu_list: BTreeMap = BTreeMap::new(); let mut id = 0; + for pci_device in pci_list.values().filter(|dev| { // Check if the class is tied to graphics dev.class() @@ -97,11 +100,72 @@ impl GpuEnumerator { // For now only support the popular GPU vendors, more can be added inthe future match gpu_vendor { - // We use /proc/driver/nvidia/gpus for nvidia devices // TODO: add another match for nova driver GpuVendor::Nvidia => { // Proprietary nvidia driver, supported by cardwire if device.driver().clone().is_some_and(|d| d == "nvidia") { + /* + This part may sound confusing + We first try to use NVML to build the GPU, using NVML allows us to have a good discrete detection + If NVML fails/GPU wasnt ready after 5 retries, fallback to the manual method that reads /proc/driver/nvidia + */ + let pci_id = device.pci_address(); + // Wait for the driver to be ready using NVML + if let Some(nvml) = wait_for_nvidia(pci_id, 5) { + // The device is ready and nvml is available, use it for GPU construction + let gpu_name = + nvidia_get_device_name_nvml(&nvml, pci_id).unwrap_or_else(|| { + device + .device_name() + .clone() + .unwrap_or_else(|| "Unknown Device".to_string()) + }); + let gpu_type = nvidia_get_device_type_nvml(&nvml, pci_id); + let nvidia_minor = nvidia_get_device_minor_nvml(&nvml, pci_id); + let drm_res = sysfs_get_device_drm(pci_id); + // return a working GPU if drm available, else return a non-available GPU + match drm_res { + Some((card, render)) => { + let gpu_device = GpuDevice::new( + gpu_name, + device.clone(), + render, + card, + None, + gpu_vendor, + nvidia_minor, + gpu_type, + ); + info!("{}: Used Nvidia+NVML to build", gpu_device.name()); + debug!("{:?}", gpu_device); + return Ok(gpu_device); + } + None => { + // Couldn't get DRM, mark GPU as not available + // this shouldn't happen unless nvidia-drm is not loaded? + let gpu_type = GpuType::Unavailable; + let gpu_device = GpuDevice::new( + gpu_name, + device.clone(), + u32::MAX, + u32::MAX, + None, + gpu_vendor, + None, + gpu_type, + ); + error!( + "{}: Cannot fetch DRM nodes, marking as un-available", + gpu_device.name() + ); + return Ok(gpu_device); + } + } + } + + // If we are here, it means the NVML failed, this is odd, but will still try + // using the good old sysfs + /proc + // Try to get the device name using nvidia driver, if fail, use hwdata, then // fallback to unknown let gpu_name = nvidia_get_device_name(pci_id).unwrap_or_else(|| { @@ -127,7 +191,7 @@ impl GpuEnumerator { nvidia_minor, gpu_type, ); - info!("{}: Used Nvidia to build", gpu_device.name()); + info!("{}: Used Nvidia+SysFS to build", gpu_device.name()); debug!("{:?}", gpu_device); return Ok(gpu_device); } diff --git a/crates/cardwire-daemon/src/core/gpu/vendor_specific/nvidia.rs b/crates/cardwire-daemon/src/core/gpu/vendor_specific/nvidia.rs index 84b10570..21184ef0 100644 --- a/crates/cardwire-daemon/src/core/gpu/vendor_specific/nvidia.rs +++ b/crates/cardwire-daemon/src/core/gpu/vendor_specific/nvidia.rs @@ -1,8 +1,13 @@ -use crate::core::gpu::models::GpuType; +use crate::{ + Result, core::{errors::CardwireError::CardwireNvmlError, gpu::models::GpuType} +}; -use std::{fs, path::Path, time::Duration}; +use std::{fs, path::Path, thread, time::Duration}; use log::{error, info, warn}; +use nvml_wrapper::{ + Device, Nvml, enum_wrappers::device::{Brand, GpuVirtualizationMode}, enums::device::DeviceArchitecture, error::NvmlError +}; use tokio::{process::Command, time::timeout}; #[allow(unused, dead_code)] @@ -136,3 +141,101 @@ async fn nvidia_powerd_enabled() -> bool { false } } + +/// Wait for the nvidia device is be initialized and return the lib +pub fn wait_for_nvidia(pci_id: &str, retries: usize) -> Option { + for _attempt in 0..retries { + if let Ok(nvml) = Nvml::init() + && let Ok(nvidia_dev) = nvml.device_by_pci_bus_id(pci_id) + && let Ok(_) = nvidia_dev.architecture() + { + return Some(nvml); + } else { + thread::sleep(Duration::from_millis(250)); + } + } + None +} +/// Get the device name using NVML +pub fn nvidia_get_device_name_nvml(nvml: &Nvml, pci_id: &str) -> Option { + if let Ok(nvidia_dev) = nvml.device_by_pci_bus_id(pci_id) + && let Ok(name) = nvidia_dev.name() + { + return Some(name); + } + None +} +/// Get the device minor using NVML +pub fn nvidia_get_device_minor_nvml(nvml: &Nvml, pci_id: &str) -> Option { + if let Ok(nvidia_dev) = nvml.device_by_pci_bus_id(pci_id) + && let Ok(minor) = nvidia_dev.minor_number() + { + return Some(minor); + } + None +} +/// Get the device type using NVML +pub fn nvidia_get_device_type_nvml(nvml: &Nvml, pci_id: &str) -> GpuType { + if let Ok(nvidia_dev) = nvml.device_by_pci_bus_id(pci_id) { + if let Ok(virt_mode) = nvidia_dev.virtualization_mode() { + match virt_mode { + GpuVirtualizationMode::Vgpu => return GpuType::Virtual, + // Do not want to assume this one + GpuVirtualizationMode::Bare => {} + // Others SHOULD be discrete + _ => return GpuType::Discrete, + } + } + match nvidia_dev.architecture() { + Ok( + DeviceArchitecture::Kepler + | DeviceArchitecture::Maxwell + | DeviceArchitecture::Pascal + | DeviceArchitecture::Turing + | DeviceArchitecture::Volta + | DeviceArchitecture::Ampere + | DeviceArchitecture::Ada + | DeviceArchitecture::Hopper + | DeviceArchitecture::Blackwell, + ) => { + return GpuType::Discrete; + } + // Architecture not implemented by nvml_wrapper yet + // 11 is DLA + // 12 is DLA2 + // 15 is NPU3 + // 13 is RUBIN + // + Err(NvmlError::UnexpectedVariant(raw)) => match raw { + 11 | 12 | 15 => return GpuType::Integrated, + 13 => return GpuType::Discrete, + _ => {} + }, + _ => {} + } + // Pretty much a fallback, i hope it doesnt get used + if let Ok(brand) = nvidia_dev.brand() { + match brand { + Brand::Quadro + | Brand::Tesla + | Brand::GeForce + | Brand::Titan + | Brand::QuadroRTX + | Brand::NvidiaRTX + | Brand::GeForceRTX + | Brand::NVS + | Brand::TitanRTX => return GpuType::Discrete, + Brand::GRID + | Brand::VApps + | Brand::VPC + | Brand::VCS + | Brand::VWS + | Brand::CloudGaming => return GpuType::Virtual, + _ => { + return GpuType::Unknown; + } + } + } + } + GpuType::Unknown +} From 4c77e26a12f6ea5d0384cfa6fa280da096692766 Mon Sep 17 00:00:00 2001 From: luytan Date: Sun, 13 Sep 2026 17:56:04 +0200 Subject: [PATCH 12/44] chore(cardwired): run clippy --- .../src/core/gpu/enumerator.rs | 22 +++--- .../src/core/gpu/generic/display.rs | 68 +------------------ .../src/core/gpu/generic/vulkan.rs | 3 +- .../src/core/gpu/vendor_specific/nvidia.rs | 6 +- 4 files changed, 15 insertions(+), 84 deletions(-) diff --git a/crates/cardwire-daemon/src/core/gpu/enumerator.rs b/crates/cardwire-daemon/src/core/gpu/enumerator.rs index 643ad847..80b14e45 100644 --- a/crates/cardwire-daemon/src/core/gpu/enumerator.rs +++ b/crates/cardwire-daemon/src/core/gpu/enumerator.rs @@ -193,7 +193,7 @@ impl GpuEnumerator { ); info!("{}: Used Nvidia+SysFS to build", gpu_device.name()); debug!("{:?}", gpu_device); - return Ok(gpu_device); + Ok(gpu_device) } None => { // Couldn't get DRM, mark GPU as not available @@ -212,7 +212,7 @@ impl GpuEnumerator { "{}: Cannot fetch DRM nodes, marking as un-available", gpu_device.name() ); - return Ok(gpu_device); + Ok(gpu_device) } } } else { @@ -238,7 +238,7 @@ impl GpuEnumerator { gpu_device.name(), device.driver() ); - return Ok(gpu_device); + Ok(gpu_device) } } GpuVendor::Intel => { @@ -263,7 +263,7 @@ impl GpuEnumerator { ); info!("{}: Used Intel to build", gpu_device.name()); debug!("{:?}", gpu_device); - return Ok(gpu_device); + Ok(gpu_device) } None => { // Couldn't get DRM, mark GPU as not available @@ -282,7 +282,7 @@ impl GpuEnumerator { "{}: Cannot fetch DRM nodes, marking as un-available", gpu_device.name() ); - return Ok(gpu_device); + Ok(gpu_device) } } } @@ -332,7 +332,7 @@ impl GpuEnumerator { ) }; debug!("{:?}", gpu_device); - return Ok(gpu_device); + Ok(gpu_device) } None => { // Couldn't get DRM, mark GPU as not available @@ -351,7 +351,7 @@ impl GpuEnumerator { "{}: cannot fetch DRM nodes, marking as un-available", gpu_device.name() ); - return Ok(gpu_device); + Ok(gpu_device) } } } else { @@ -375,7 +375,7 @@ impl GpuEnumerator { gpu_device.name(), device.driver() ); - return Ok(gpu_device); + Ok(gpu_device) } } // Cardwire depends on knowing the GPU type for the modes, mark Other devices as @@ -406,7 +406,7 @@ impl GpuEnumerator { device.vendor_name() ); debug!("{:?}", gpu_device); - return Ok(gpu_device); + Ok(gpu_device) } None => { // Couldn't get DRM, mark GPU as not available @@ -425,10 +425,10 @@ impl GpuEnumerator { "{}: cannot fetch DRM nodes, marking as un-available", gpu_device.name() ); - return Ok(gpu_device); + Ok(gpu_device) } } } - }; + } } } diff --git a/crates/cardwire-daemon/src/core/gpu/generic/display.rs b/crates/cardwire-daemon/src/core/gpu/generic/display.rs index 2a732dad..23326492 100644 --- a/crates/cardwire-daemon/src/core/gpu/generic/display.rs +++ b/crates/cardwire-daemon/src/core/gpu/generic/display.rs @@ -1,8 +1,6 @@ //! DRM display connector detection and node resolution. -use log::{info, warn}; -use std::{fs, io, path::Path, time::Duration}; -use udev::{Device, Enumerator}; +use std::{fs, io}; const NON_PHYSICAL: &[&str] = &["Virtual-", "Unknown-", "Writeback-"]; const INTERNAL_PANELS: &[&str] = &["eDP-", "LVDS-", "DSI-", "DPI-", "SPI-"]; @@ -46,70 +44,6 @@ pub fn external_display_connected(card: u32) -> io::Result { } } -/// Reads both the card and render node IDs (e.g., (1, 128)) for a given PCI address. -/// Retries until both DRM nodes are spawned by the kernel and initialized by udev -pub fn drm_node_ids(pci_address: &str) -> io::Result<(u32, u32)> { - const MAX_RETRIES: u32 = 10; - const RETRY_INTERVAL: Duration = Duration::from_millis(500); - - let pci_syspath = Path::new("/sys/bus/pci/devices").join(pci_address); - - for attempt in 1..=MAX_RETRIES { - let mut card_id = None; - let mut render_id = None; - - if let Ok(parent) = Device::from_syspath(&pci_syspath) - && let Ok(mut enumerator) = Enumerator::new() - { - let _ = enumerator.match_parent(&parent); - let _ = enumerator.match_subsystem("drm"); - - if let Ok(devices) = enumerator.scan_devices() { - for dev in devices { - // Skip if uninitialized - if !dev.is_initialized() { - continue; - } - - let sysname = dev.sysname().to_string_lossy(); - - if let Some(num) = dev.sysnum() { - if sysname == format!("card{num}") { - card_id = Some(num as u32); - } else if sysname == format!("renderD{num}") { - render_id = Some(num as u32); - } - } - } - } - } - - if let (Some(card), Some(render)) = (card_id, render_id) { - info!( - "Successfully resolved card{} and renderD{} for PCI {}", - card, render, pci_address - ); - return Ok((card, render)); - } - - if attempt < MAX_RETRIES { - warn!( - "DRM nodes (card/render) for PCI {} not fully ready, attempt {}/{MAX_RETRIES}, retrying in 500ms...", - pci_address, attempt - ); - std::thread::sleep(RETRY_INTERVAL); - } - } - - Err(io::Error::new( - io::ErrorKind::NotFound, - format!( - "Failed to find both initialized card and render DRM nodes for PCI {}", - pci_address - ), - )) -} - /// Check whether the given DRM card currently has any connected display. /// /// Reads `/sys/class/drm/card{card}-*/status` diff --git a/crates/cardwire-daemon/src/core/gpu/generic/vulkan.rs b/crates/cardwire-daemon/src/core/gpu/generic/vulkan.rs index 0506b409..170f3dc1 100644 --- a/crates/cardwire-daemon/src/core/gpu/generic/vulkan.rs +++ b/crates/cardwire-daemon/src/core/gpu/generic/vulkan.rs @@ -62,8 +62,7 @@ impl Vulkan { { name = vlk_dev.properties().device_name.clone(); } - let cleaned_name = name.split('(').next().unwrap_or(&name).trim().to_string(); - cleaned_name + name.split('(').next().unwrap_or(&name).trim().to_string() } /// Get the gpu render node using vulkan pub fn get_gpu_render(&self, pci_id: &str) -> Option { diff --git a/crates/cardwire-daemon/src/core/gpu/vendor_specific/nvidia.rs b/crates/cardwire-daemon/src/core/gpu/vendor_specific/nvidia.rs index 21184ef0..89ce03b4 100644 --- a/crates/cardwire-daemon/src/core/gpu/vendor_specific/nvidia.rs +++ b/crates/cardwire-daemon/src/core/gpu/vendor_specific/nvidia.rs @@ -1,12 +1,10 @@ -use crate::{ - Result, core::{errors::CardwireError::CardwireNvmlError, gpu::models::GpuType} -}; +use crate::core::gpu::models::GpuType; use std::{fs, path::Path, thread, time::Duration}; use log::{error, info, warn}; use nvml_wrapper::{ - Device, Nvml, enum_wrappers::device::{Brand, GpuVirtualizationMode}, enums::device::DeviceArchitecture, error::NvmlError + Nvml, enum_wrappers::device::{Brand, GpuVirtualizationMode}, enums::device::DeviceArchitecture, error::NvmlError }; use tokio::{process::Command, time::timeout}; From b26accec1de61340d520e664648a183f87b1ba79 Mon Sep 17 00:00:00 2001 From: luytan Date: Sun, 13 Sep 2026 18:42:43 +0200 Subject: [PATCH 13/44] refactor(cardwired): add catch-all for env --- crates/cardwire-daemon/src/core/env.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/cardwire-daemon/src/core/env.rs b/crates/cardwire-daemon/src/core/env.rs index 4194a8e6..a415e62b 100644 --- a/crates/cardwire-daemon/src/core/env.rs +++ b/crates/cardwire-daemon/src/core/env.rs @@ -65,7 +65,7 @@ pub fn compute_switcheroo_env( env.push("VK_LOADER_DRIVERS_SELECT".to_string()); env.push("*intel*".to_string()); } - GpuVendor::Other => { + GpuVendor::Other | _ => { env.push("DRI_PRIME".to_string()); env.push(dri_prime_val); } From 47d1226826dbf2ac727ea3b142e1d2865ba40890 Mon Sep 17 00:00:00 2001 From: luytan Date: Sun, 13 Sep 2026 18:43:02 +0200 Subject: [PATCH 14/44] refactor(cardwired): use pci_id --- crates/cardwire-daemon/src/core/gpu/enumerator.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/cardwire-daemon/src/core/gpu/enumerator.rs b/crates/cardwire-daemon/src/core/gpu/enumerator.rs index 80b14e45..6705c9ea 100644 --- a/crates/cardwire-daemon/src/core/gpu/enumerator.rs +++ b/crates/cardwire-daemon/src/core/gpu/enumerator.rs @@ -74,7 +74,7 @@ impl GpuEnumerator { let _ = wait_for_drm(pci_id, 5); // Check if the gpu info can be fetched using vulkan, if so use vulkan to build the GPU - if self.vulkan.vulkan_compatible(device.pci_address()) { + if self.vulkan.vulkan_compatible(pci_id) { let gpu_type = self.vulkan.get_gpu_type(pci_id); let gpu_name = self.vulkan.get_gpu_name(pci_id); @@ -109,7 +109,6 @@ impl GpuEnumerator { We first try to use NVML to build the GPU, using NVML allows us to have a good discrete detection If NVML fails/GPU wasnt ready after 5 retries, fallback to the manual method that reads /proc/driver/nvidia */ - let pci_id = device.pci_address(); // Wait for the driver to be ready using NVML if let Some(nvml) = wait_for_nvidia(pci_id, 5) { // The device is ready and nvml is available, use it for GPU construction From a19a09311a84a225f29485878ee50b480eeb132b Mon Sep 17 00:00:00 2001 From: luytan Date: Sun, 13 Sep 2026 18:43:18 +0200 Subject: [PATCH 15/44] feat(cardwired): add logs to drm loop --- crates/cardwire-daemon/src/core/gpu/generic/udev.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/crates/cardwire-daemon/src/core/gpu/generic/udev.rs b/crates/cardwire-daemon/src/core/gpu/generic/udev.rs index 3d87316f..8a968ea8 100644 --- a/crates/cardwire-daemon/src/core/gpu/generic/udev.rs +++ b/crates/cardwire-daemon/src/core/gpu/generic/udev.rs @@ -1,12 +1,19 @@ use std::{path::Path, thread, time::Duration}; +use log::{info, warn}; + /// Wait for the device to be initialized pub fn wait_for_drm(pci_id: &str, retries: usize) -> bool { let drm = Path::new("/sys/bus/pci/devices").join(pci_id).join("drm"); - for _attempt in 0..retries { + for attempt in 0..retries { + warn!( + "[{}/{}] waiting for {} DRM subsystem to init...", + attempt, retries, pci_id + ); if drm.read_dir().is_ok_and(|mut d| d.next().is_some()) { - return false; + info!("{} DRM subsystem is ready", pci_id); + return true; } thread::sleep(Duration::from_millis(250)); } From df401df81b2f11bc8e8cd489b924c35808b3b057 Mon Sep 17 00:00:00 2001 From: luytan Date: Sun, 13 Sep 2026 18:43:45 +0200 Subject: [PATCH 16/44] feat(cardwired): initial support for virtio --- .../src/core/gpu/enumerator.rs | 45 +++++++++++++++++++ crates/cardwire-daemon/src/core/gpu/models.rs | 3 ++ 2 files changed, 48 insertions(+) diff --git a/crates/cardwire-daemon/src/core/gpu/enumerator.rs b/crates/cardwire-daemon/src/core/gpu/enumerator.rs index 6705c9ea..d92445c2 100644 --- a/crates/cardwire-daemon/src/core/gpu/enumerator.rs +++ b/crates/cardwire-daemon/src/core/gpu/enumerator.rs @@ -377,6 +377,51 @@ impl GpuEnumerator { Ok(gpu_device) } } + // Just set the type to Virtual + GpuVendor::Virtio => { + let gpu_name = device + .device_name() + .clone() + .unwrap_or_else(|| "Unknown Device".to_string()); + let drm_res = sysfs_get_device_drm(pci_id); + match drm_res { + Some((card, render)) => { + let gpu_type = GpuType::Virtual; + let gpu_device = GpuDevice::new( + gpu_name, + device.clone(), + render, + card, + None, + gpu_vendor, + None, + gpu_type, + ); + info!("{}: Used Virtio to build", gpu_device.name()); + debug!("{:?}", gpu_device); + Ok(gpu_device) + } + None => { + // Couldn't get DRM, mark GPU as not available + let gpu_type = GpuType::Unavailable; + let gpu_device = GpuDevice::new( + gpu_name, + device.clone(), + u32::MAX, + u32::MAX, + None, + gpu_vendor, + None, + gpu_type, + ); + error!( + "{}: cannot fetch DRM nodes, marking as un-available", + gpu_device.name() + ); + Ok(gpu_device) + } + } + } // Cardwire depends on knowing the GPU type for the modes, mark Other devices as // unknown, leaving only hybrid and manual available until support added GpuVendor::Other => { diff --git a/crates/cardwire-daemon/src/core/gpu/models.rs b/crates/cardwire-daemon/src/core/gpu/models.rs index 22440253..b518b8af 100644 --- a/crates/cardwire-daemon/src/core/gpu/models.rs +++ b/crates/cardwire-daemon/src/core/gpu/models.rs @@ -54,6 +54,7 @@ pub enum GpuVendor { Amd, Nvidia, Intel, + Virtio, #[default] Other, } @@ -66,6 +67,7 @@ impl> From for GpuVendor { "0x1002" => GpuVendor::Amd, "0x10de" | "0x104a" | "0x12d2" => GpuVendor::Nvidia, "0x8086" => GpuVendor::Intel, + "0x1AF4" => GpuVendor::Virtio, // Unknown id _ => GpuVendor::Other, } @@ -78,6 +80,7 @@ impl Display for GpuVendor { GpuVendor::Amd => write!(f, "AMD"), GpuVendor::Nvidia => write!(f, "Nvidia"), GpuVendor::Intel => write!(f, "Intel"), + GpuVendor::Virtio => write!(f, "Virtio"), GpuVendor::Other => write!(f, "Unknown Vendor"), } } From 65ab693ee06b1d8779f0527e369d6d04dcf9fb57 Mon Sep 17 00:00:00 2001 From: luytan Date: Sun, 13 Sep 2026 18:50:19 +0200 Subject: [PATCH 17/44] chore(cardwired): deprecated amggpu.ids --- .../src/core/gpu/vendor_specific/amd.rs | 40 ------------------- nix/default.nix | 5 +-- 2 files changed, 2 insertions(+), 43 deletions(-) diff --git a/crates/cardwire-daemon/src/core/gpu/vendor_specific/amd.rs b/crates/cardwire-daemon/src/core/gpu/vendor_specific/amd.rs index 0a0b3471..6619a938 100644 --- a/crates/cardwire-daemon/src/core/gpu/vendor_specific/amd.rs +++ b/crates/cardwire-daemon/src/core/gpu/vendor_specific/amd.rs @@ -5,46 +5,6 @@ use libdrm_amdgpu_sys::{ use crate::{ Result, core::{errors::CardwireError::CardwireAmdGpuError, gpu::models::GpuType} }; -use std::fs; - -/// Find the amd model using amdgpu.ids, require the device id and the revision for precise matching -#[allow(unused, dead_code)] -#[deprecated] -pub fn amd_get_device_name(device_id: &str, pci: &str) -> Option { - let path = "/usr/share/libdrm/amdgpu.ids"; - let device_id = device_id.to_string().replace("0x", "").to_ascii_uppercase(); - - let revision = fs::read_to_string(format!("/sys/bus/pci/devices/{}/revision", pci)) - .ok()? - .trim() - .replace("0x", "") - .to_ascii_uppercase(); - - let content = fs::read_to_string(path).ok()?; - - for line in content.lines() { - if line.starts_with('#') { - continue; - } - - let mut parts = line.split('\t'); - let Some(id) = parts.next() else { - continue; - }; - let Some(rev) = parts.next() else { - continue; - }; - let Some(name) = parts.next() else { - continue; - }; - - if id.trim_end_matches(',') == device_id && rev.trim_end_matches(',') == revision { - return Some(name.to_string()); - } - } - - None -} pub struct AmdGpuDev { #[allow(unused)] diff --git a/nix/default.nix b/nix/default.nix index 3a243c07..547fe66d 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -40,6 +40,7 @@ pkgs.rustPlatform.buildRustPackage { pkgs.libxkbcommon pkgs.vulkan-loader pkgs.libglvnd + pkgs.libdrm ]; doCheck = false; @@ -64,9 +65,6 @@ pkgs.rustPlatform.buildRustPackage { # Point to the correct hwdata location substituteInPlace crates/cardwire-daemon/src/core/pci/pci_device.rs \ --replace-fail "/usr/share/hwdata/pci.ids" "${pkgs.hwdata}/share/hwdata/pci.ids" - - substituteInPlace crates/cardwire-daemon/src/core/gpu/device_info.rs \ - --replace-fail "/usr/share/libdrm/amdgpu.ids" "${pkgs.libdrm}/share/libdrm/amdgpu.ids" ''; env = { @@ -98,6 +96,7 @@ pkgs.rustPlatform.buildRustPackage { pkgs.upower pkgs.vulkan-loader pkgs.libglvnd + pkgs.libdrm ] } From d34fc3811f99405dd13a69730ce0ab60c4166c7f Mon Sep 17 00:00:00 2001 From: luytan Date: Sun, 13 Sep 2026 19:21:14 +0200 Subject: [PATCH 18/44] chore(cardwired): add repr for GpuType and remove CPU type --- .../src/core/gpu/generic/vulkan.rs | 3 +-- crates/cardwire-daemon/src/core/gpu/models.rs | 20 +++++++------------ 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/crates/cardwire-daemon/src/core/gpu/generic/vulkan.rs b/crates/cardwire-daemon/src/core/gpu/generic/vulkan.rs index 170f3dc1..179ebaad 100644 --- a/crates/cardwire-daemon/src/core/gpu/generic/vulkan.rs +++ b/crates/cardwire-daemon/src/core/gpu/generic/vulkan.rs @@ -35,11 +35,10 @@ impl Vulkan { && let Some(vlk_dev) = vlk_map.get(pci_id) { match vlk_dev.properties().device_type { - PhysicalDeviceType::Cpu => GpuType::Cpu, PhysicalDeviceType::DiscreteGpu => GpuType::Discrete, PhysicalDeviceType::IntegratedGpu => GpuType::Integrated, PhysicalDeviceType::VirtualGpu => GpuType::Virtual, - PhysicalDeviceType::Other => GpuType::Other, + PhysicalDeviceType::Other | PhysicalDeviceType::Cpu => GpuType::Other, _ => { // List is non-exhaustive, warn and give it the unknown type warn!( diff --git a/crates/cardwire-daemon/src/core/gpu/models.rs b/crates/cardwire-daemon/src/core/gpu/models.rs index b518b8af..253d0ef6 100644 --- a/crates/cardwire-daemon/src/core/gpu/models.rs +++ b/crates/cardwire-daemon/src/core/gpu/models.rs @@ -89,15 +89,15 @@ impl Display for GpuVendor { #[derive( Clone, Debug, serde::Serialize, serde::Deserialize, Default, PartialEq, zvariant::Type, )] +#[repr(u32)] pub enum GpuType { - Integrated, - Discrete, - Virtual, - Cpu, - Other, - Unavailable, + Integrated = 0, + Discrete = 1, + Virtual = 2, + Other = 3, + Unavailable = 4, #[default] - Unknown, + Unknown = 5, } #[derive(Clone, Debug, serde::Serialize, serde::Deserialize, zbus::zvariant::Type, PartialEq)] @@ -150,10 +150,6 @@ impl GpuDevice { self.device_type == GpuType::Discrete } - pub fn is_cpu(&self) -> bool { - self.device_type == GpuType::Cpu - } - pub fn is_integrated(&self) -> bool { self.device_type == GpuType::Integrated } @@ -204,7 +200,6 @@ pub struct DbusGpuDevice { pub device_type: GpuType, pub vendor: String, pub driver: String, - pub nvidia: bool, pub nvidia_minor: String, } @@ -219,7 +214,6 @@ impl From<&GpuDevice> for DbusGpuDevice { device_type: gpu.device_type.clone(), vendor: gpu.gpu_vendor().to_string(), driver: gpu.pci.driver().clone().unwrap_or("none".to_string()), - nvidia: gpu.gpu_vendor() == GpuVendor::Nvidia, nvidia_minor: if let Some(minor) = gpu.nvidia_minor() { minor.to_string() } else { From 9d100e993201e66c155decc97600d527c32dc2fe Mon Sep 17 00:00:00 2001 From: luytan Date: Sun, 13 Sep 2026 19:21:45 +0200 Subject: [PATCH 19/44] feat(cardwire-cli): implement new DBUS to cli --- crates/cardwire-cli/src/dbus.rs | 20 ++++++++++++---- crates/cardwire-cli/src/display.rs | 38 ++++++++++++------------------ crates/cardwire-cli/src/main.rs | 22 +++++++++-------- crates/cardwire-cli/src/types.rs | 6 ++--- 4 files changed, 45 insertions(+), 41 deletions(-) diff --git a/crates/cardwire-cli/src/dbus.rs b/crates/cardwire-cli/src/dbus.rs index 2b0a46aa..a6595a5a 100644 --- a/crates/cardwire-cli/src/dbus.rs +++ b/crates/cardwire-cli/src/dbus.rs @@ -1,6 +1,6 @@ use std::collections::BTreeMap; -use zbus::{Proxy, connection::Connection}; +use zbus::{Proxy, connection::Connection, zvariant}; use crate::display::PciDevice; @@ -12,14 +12,24 @@ pub struct DbusGpuDevice { pub render: u32, pub card: u32, pub default: bool, - pub discrete: bool, - pub virtual_gpu: bool, - pub available: bool, + pub device_type: GpuType, pub vendor: String, pub driver: String, - pub nvidia: bool, pub nvidia_minor: String, } +#[derive( + Clone, Debug, serde::Serialize, serde::Deserialize, Default, PartialEq, zvariant::Type, +)] +#[repr(u32)] +pub enum GpuType { + Integrated = 0, + Discrete = 1, + Virtual = 2, + Other = 3, + Unavailable = 4, + #[default] + Unknown = 5, +} pub struct DaemonClient<'a> { proxy: Proxy<'a>, diff --git a/crates/cardwire-cli/src/display.rs b/crates/cardwire-cli/src/display.rs index 034ddae5..4406f4e6 100644 --- a/crates/cardwire-cli/src/display.rs +++ b/crates/cardwire-cli/src/display.rs @@ -4,6 +4,8 @@ use std::collections::BTreeMap; use anyhow::{Ok, Result}; + +use crate::dbus::GpuType; // Define the struct here instead of importing from cardwire_core, // I want cardwire-cli to be independent of the rest of cardwire // This allow other dev to make their own client for cardwire @@ -16,14 +18,11 @@ pub struct GpuDevice { pub render: u32, pub card: u32, pub default: bool, - pub discrete: bool, - pub virtual_gpu: bool, - pub available: bool, + pub device_type: GpuType, pub vendor: String, pub driver: String, pub blocked: bool, pub launchable: bool, - pub nvidia: bool, pub nvidia_minor: String, } #[derive(serde::Deserialize, serde::Serialize, zbus::zvariant::Type)] @@ -119,7 +118,11 @@ fn pretty_print_gpu(gpu_list: BTreeMap) { render_full, card_full, if gpu.default { "(*)" } else { "( )" }, - if gpu.discrete { "(*)" } else { "( )" }, + if gpu.device_type == GpuType::Discrete { + "(*)" + } else { + "( )" + }, gpu.blocked, id_w = id_w, name_w = name_w, @@ -144,9 +147,7 @@ mod tests { name: &str, pci: &str, default: bool, - discrete: bool, - virtual_gpu: bool, - available: bool, + device_type: GpuType, vendor: &str, driver: &str, blocked: bool, @@ -158,14 +159,11 @@ mod tests { render: 128, card: 0, default, - discrete, - virtual_gpu, - available, + device_type: device_type.clone(), vendor: vendor.to_string(), driver: driver.to_string(), blocked, - launchable: !blocked && available, - nvidia: false, + launchable: !blocked && device_type != GpuType::Unavailable, nvidia_minor: String::new(), } } @@ -180,9 +178,7 @@ mod tests { "Intel UHD", "0000:00:02.0", true, - false, - false, - true, + GpuType::Integrated, "Intel", "xe", false, @@ -195,9 +191,7 @@ mod tests { "RTX 4060", "0000:01:00.0", false, - true, - false, - true, + GpuType::Discrete, "Nvidia", "nouveau", true, @@ -226,9 +220,7 @@ mod tests { "RX 7900 XTX", "0000:03:00.0", false, - true, - false, - true, + GpuType::Discrete, "AMD", "amdgpu", false, @@ -242,6 +234,6 @@ mod tests { assert_eq!(parsed.card, 0); assert!(!parsed.default); assert!(!parsed.blocked); - assert!(!parsed.nvidia); + assert!(parsed.vendor != "Nvidia"); } } diff --git a/crates/cardwire-cli/src/main.rs b/crates/cardwire-cli/src/main.rs index 8018c31a..a6994205 100644 --- a/crates/cardwire-cli/src/main.rs +++ b/crates/cardwire-cli/src/main.rs @@ -10,7 +10,7 @@ use args::{Args, CliMode, Commands, ConfigAction, DebugAction, ManagerAction}; use clap::{CommandFactory, Parser}; use dbus::DaemonClient; -use crate::{display::print_devices_pci, types::SystemType}; +use crate::{dbus::GpuType, display::print_devices_pci, types::SystemType}; const BIN_NAME: &str = "cardwire"; @@ -281,7 +281,7 @@ async fn main() -> anyhow::Result<()> { Commands::Launch { gpu, program } => { let mut available_gpu = get_gpu_list(&client).await; - available_gpu.retain(|_, gpu| gpu.available); + available_gpu.retain(|_, gpu| gpu.device_type != GpuType::Unavailable); let target_gpu = if let Some(gpu_id) = gpu { let target = available_gpu.get(&(gpu_id as usize)); @@ -302,23 +302,28 @@ async fn main() -> anyhow::Result<()> { target // No gpu specified } else { - available_gpu.retain(|_, gpu| gpu.available && gpu.launchable); + available_gpu + .retain(|_, gpu| gpu.device_type != GpuType::Unavailable && gpu.launchable); let system_type = SystemType::from_gpulist(&available_gpu); match system_type { // 2 GPUs, one iGPU and one dGPU SystemType::Laptop => available_gpu .iter() - .find(|(_, gpu)| !gpu.default && gpu.discrete), + .find(|(_, gpu)| !gpu.default && gpu.device_type == GpuType::Discrete), // 2 GPUs, use default discrete GPU SystemType::Desktop => available_gpu .iter() - .find(|(_, gpu)| gpu.default && gpu.discrete), + .find(|(_, gpu)| gpu.default && gpu.device_type == GpuType::Discrete), // 1 GPU or 3+ GPUs, get in this priority: // 0. Default Discrete GPU // 1. non-Default discrete GPU // 2. Others SystemType::Manual => available_gpu.iter().max_by_key(|(_, gpu)| { - (gpu.default && gpu.discrete, gpu.discrete, gpu.default) + ( + gpu.default && gpu.device_type == GpuType::Discrete, + gpu.device_type == GpuType::Discrete, + gpu.default, + ) }), } .map(|(_, gpu)| gpu) @@ -447,14 +452,11 @@ async fn get_gpu_list(client: &'_ DaemonClient<'_>) -> BTreeMap) -> Self { let available_gpus: Vec<(usize, bool, bool)> = gpu_list .iter() - .filter(|(_, gpu)| gpu.available) - .map(|(id, gpu)| (*id, gpu.default, gpu.discrete)) + .filter(|(_, gpu)| gpu.device_type != GpuType::Unavailable) + .map(|(id, gpu)| (*id, gpu.default, gpu.device_type == GpuType::Discrete)) .collect(); if available_gpus.len() != 2 { From f1cc6cebfe4df6a41f7c6b066c8c789177dc1235 Mon Sep 17 00:00:00 2001 From: luytan Date: Mon, 14 Sep 2026 20:14:08 +0200 Subject: [PATCH 20/44] chore(cardwired): wildcard pattern cover Other too --- crates/cardwire-daemon/src/core/env.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/cardwire-daemon/src/core/env.rs b/crates/cardwire-daemon/src/core/env.rs index a415e62b..3cecfc60 100644 --- a/crates/cardwire-daemon/src/core/env.rs +++ b/crates/cardwire-daemon/src/core/env.rs @@ -65,7 +65,7 @@ pub fn compute_switcheroo_env( env.push("VK_LOADER_DRIVERS_SELECT".to_string()); env.push("*intel*".to_string()); } - GpuVendor::Other | _ => { + _ => { env.push("DRI_PRIME".to_string()); env.push(dri_prime_val); } From 9570b2a2d8254b572275f05d7e08a718c57cf184 Mon Sep 17 00:00:00 2001 From: luytan Date: Mon, 14 Sep 2026 21:58:02 +0200 Subject: [PATCH 21/44] fix(cardwired): correct Virtio vendor --- crates/cardwire-daemon/src/core/gpu/models.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/cardwire-daemon/src/core/gpu/models.rs b/crates/cardwire-daemon/src/core/gpu/models.rs index 253d0ef6..9e2d48f6 100644 --- a/crates/cardwire-daemon/src/core/gpu/models.rs +++ b/crates/cardwire-daemon/src/core/gpu/models.rs @@ -67,7 +67,7 @@ impl> From for GpuVendor { "0x1002" => GpuVendor::Amd, "0x10de" | "0x104a" | "0x12d2" => GpuVendor::Nvidia, "0x8086" => GpuVendor::Intel, - "0x1AF4" => GpuVendor::Virtio, + "0x1af4" => GpuVendor::Virtio, // Unknown id _ => GpuVendor::Other, } @@ -162,6 +162,10 @@ impl GpuDevice { self.device_type != GpuType::Unavailable } + pub fn set_type(&mut self, gpu_type: GpuType) { + self.device_type = gpu_type + } + #[allow(clippy::too_many_arguments)] pub fn new( name: String, From 5525292a76b5545315634e7ceb029e2bd60e6e30 Mon Sep 17 00:00:00 2001 From: luytan Date: Mon, 14 Sep 2026 21:58:21 +0200 Subject: [PATCH 22/44] fix(cardwired): manual gpu typing for ci --- crates/cardwire-daemon/src/core/gpu/enumerator.rs | 10 ++++++++++ nix/vm-configuration.nix | 1 + 2 files changed, 11 insertions(+) diff --git a/crates/cardwire-daemon/src/core/gpu/enumerator.rs b/crates/cardwire-daemon/src/core/gpu/enumerator.rs index d92445c2..66691d47 100644 --- a/crates/cardwire-daemon/src/core/gpu/enumerator.rs +++ b/crates/cardwire-daemon/src/core/gpu/enumerator.rs @@ -53,6 +53,16 @@ impl GpuEnumerator { // Check which device is the default let _ = check_default_drm_class(&mut gpu_list); + // For cardwire CI, make GPU 0 integrated, and GPU 1 discrete + if std::env::var_os("CARDWIRE_TESTING").is_some() { + info!("CARDWIRE TESTING DETECTED"); + // panic if any of them is missing + let gpu0 = gpu_list.get_mut(&0).unwrap(); + gpu0.set_type(GpuType::Integrated); + let gpu1 = gpu_list.get_mut(&1).unwrap(); + gpu1.set_type(GpuType::Discrete); + } + gpu_list } diff --git a/nix/vm-configuration.nix b/nix/vm-configuration.nix index 99cef09f..c9f13f70 100644 --- a/nix/vm-configuration.nix +++ b/nix/vm-configuration.nix @@ -53,6 +53,7 @@ ]; }; }; + systemd.services.cardwired.environment.CARDWIRE_TESTING = "1"; programs.bash = { enable = true; shellAliases = { From 1e7efa709818d0b3c03e2457e8ea432aa3fcda3d Mon Sep 17 00:00:00 2001 From: luytan Date: Tue, 15 Sep 2026 21:25:01 +0200 Subject: [PATCH 23/44] refactor(cardwired): simplify drm error handling in gpu building --- .../src/core/gpu/enumerator.rs | 413 ++++++------------ .../src/core/gpu/vendor_specific/nvidia.rs | 16 +- 2 files changed, 136 insertions(+), 293 deletions(-) diff --git a/crates/cardwire-daemon/src/core/gpu/enumerator.rs b/crates/cardwire-daemon/src/core/gpu/enumerator.rs index 66691d47..31664251 100644 --- a/crates/cardwire-daemon/src/core/gpu/enumerator.rs +++ b/crates/cardwire-daemon/src/core/gpu/enumerator.rs @@ -131,50 +131,26 @@ impl GpuEnumerator { }); let gpu_type = nvidia_get_device_type_nvml(&nvml, pci_id); let nvidia_minor = nvidia_get_device_minor_nvml(&nvml, pci_id); - let drm_res = sysfs_get_device_drm(pci_id); - // return a working GPU if drm available, else return a non-available GPU - match drm_res { - Some((card, render)) => { - let gpu_device = GpuDevice::new( - gpu_name, - device.clone(), - render, - card, - None, - gpu_vendor, - nvidia_minor, - gpu_type, - ); - info!("{}: Used Nvidia+NVML to build", gpu_device.name()); - debug!("{:?}", gpu_device); - return Ok(gpu_device); - } - None => { - // Couldn't get DRM, mark GPU as not available - // this shouldn't happen unless nvidia-drm is not loaded? - let gpu_type = GpuType::Unavailable; - let gpu_device = GpuDevice::new( - gpu_name, - device.clone(), - u32::MAX, - u32::MAX, - None, - gpu_vendor, - None, - gpu_type, - ); - error!( - "{}: Cannot fetch DRM nodes, marking as un-available", - gpu_device.name() - ); - return Ok(gpu_device); - } + // return a working GPU if drm available + if let Some((card, render)) = sysfs_get_device_drm(pci_id) { + let gpu_device = GpuDevice::new( + gpu_name, + device.clone(), + render, + card, + None, + gpu_vendor, + nvidia_minor, + gpu_type, + ); + info!("{}: Used Nvidia+NVML to build", gpu_device.name()); + debug!("{:?}", gpu_device); + return Ok(gpu_device); } } // If we are here, it means the NVML failed, this is odd, but will still try // using the good old sysfs + /proc - // Try to get the device name using nvidia driver, if fail, use hwdata, then // fallback to unknown let gpu_name = nvidia_get_device_name(pci_id).unwrap_or_else(|| { @@ -184,70 +160,32 @@ impl GpuEnumerator { .unwrap_or_else(|| "Unknown Device".to_string()) }); - let drm_res = sysfs_get_device_drm(pci_id); // return a working GPU if drm available, else return a non-available GPU - match drm_res { - Some((card, render)) => { - let gpu_type = nvidia_get_device_type(pci_id, &gpu_name); - let nvidia_minor = nvidia_get_device_minor(pci_id); - let gpu_device = GpuDevice::new( - gpu_name, - device.clone(), - render, - card, - None, - gpu_vendor, - nvidia_minor, - gpu_type, - ); - info!("{}: Used Nvidia+SysFS to build", gpu_device.name()); - debug!("{:?}", gpu_device); - Ok(gpu_device) - } - None => { - // Couldn't get DRM, mark GPU as not available - let gpu_type = GpuType::Unavailable; - let gpu_device = GpuDevice::new( - gpu_name, - device.clone(), - u32::MAX, - u32::MAX, - None, - gpu_vendor, - None, - gpu_type, - ); - error!( - "{}: Cannot fetch DRM nodes, marking as un-available", - gpu_device.name() - ); - Ok(gpu_device) - } + // The type detection for this one is kinda dirty, TODO: find a better way + if let Some((card, render)) = sysfs_get_device_drm(pci_id) { + let gpu_type = nvidia_get_device_type(&gpu_name); + let nvidia_minor = nvidia_get_device_minor(pci_id); + let gpu_device = GpuDevice::new( + gpu_name, + device.clone(), + render, + card, + None, + gpu_vendor, + nvidia_minor, + gpu_type, + ); + info!("{}: Used Nvidia+SysFS to build", gpu_device.name()); + debug!("{:?}", gpu_device); + return Ok(gpu_device); } } else { - // Not a driver cardwire supports (eg nova), mark GPU as not available until - // support is added - let gpu_type = GpuType::Unavailable; - let gpu_name = device - .device_name() - .clone() - .unwrap_or_else(|| "Unknown Device".to_string()); - let gpu_device = GpuDevice::new( - gpu_name, - device.clone(), - u32::MAX, - u32::MAX, - None, - gpu_vendor, - None, - gpu_type, - ); + // Not a driver we support (eg. nova), will be marked as not available error!( "{}: driver {:?} is not supported by Cardwire, please request it on Github", - gpu_device.name(), + device.pci_address(), device.driver() ); - Ok(gpu_device) } } GpuVendor::Intel => { @@ -256,10 +194,37 @@ impl GpuEnumerator { .device_name() .clone() .unwrap_or_else(|| "Unknown Device".to_string()); - let drm_res = sysfs_get_device_drm(pci_id); - match drm_res { - Some((card, render)) => { - let gpu_type = intel_get_device_type(pci_id); + if let Some((card, render)) = sysfs_get_device_drm(pci_id) { + let gpu_type = intel_get_device_type(pci_id); + let gpu_device = GpuDevice::new( + gpu_name, + device.clone(), + render, + card, + None, + gpu_vendor, + None, + gpu_type, + ); + info!("{}: Used Intel to build", gpu_device.name()); + debug!("{:?}", gpu_device); + return Ok(gpu_device); + } + // DRM couldn't be fetched + error!( + "{}: Cannot fetch DRM nodes, marking as un-available", + device.pci_address() + ); + } + GpuVendor::Amd => { + // Only support for amdgpu will be added, radeon will be considered on user demand + if device.driver().clone().is_some_and(|d| d == "amdgpu") { + // For AMD, we fetch infos using libdrm_amdgpu if DRM nodes are availables + if let Some((card, render)) = sysfs_get_device_drm(pci_id) + && let Ok(amdgpu) = AmdGpuDev::new(render) + { + let gpu_type = amdgpu.amd_get_device_type(); + let gpu_name = amdgpu.amd_get_device_name(); let gpu_device = GpuDevice::new( gpu_name, device.clone(), @@ -270,121 +235,22 @@ impl GpuEnumerator { None, gpu_type, ); - info!("{}: Used Intel to build", gpu_device.name()); + info!("{}: Used AMDGPU to build", gpu_device.name()); debug!("{:?}", gpu_device); - Ok(gpu_device) - } - None => { - // Couldn't get DRM, mark GPU as not available - let gpu_type = GpuType::Unavailable; - let gpu_device = GpuDevice::new( - gpu_name, - device.clone(), - u32::MAX, - u32::MAX, - None, - gpu_vendor, - None, - gpu_type, - ); - error!( - "{}: Cannot fetch DRM nodes, marking as un-available", - gpu_device.name() - ); - Ok(gpu_device) - } - } - } - GpuVendor::Amd => { - // Only support for amdgpu will be added, radeon will be considered on user demand - if device.driver().clone().is_some_and(|d| d == "amdgpu") { - // For AMD, we fetch infos using libdrm_amdgpu if DRM nodes are availables - let gpu_name = device - .device_name() - .clone() - .unwrap_or_else(|| "Unknown Device".to_string()); - let drm_res = sysfs_get_device_drm(pci_id); - match drm_res { - Some((card, render)) => { - let gpu_device = if let Ok(amdgpu) = AmdGpuDev::new(render) { - let gpu_type = amdgpu.amd_get_device_type(); - // amdgpu is more precise - let gpu_name = amdgpu.amd_get_device_name(); - info!("{}: Used AMDGPU to build", gpu_name); - GpuDevice::new( - gpu_name, - device.clone(), - render, - card, - None, - gpu_vendor, - None, - gpu_type, - ) - } else { - // If we cannot use AMDGPU, mark device as unknown until i find a - // reliable way to detect discrete using sysfs - let gpu_type = GpuType::Unknown; - error!( - "{}: cannot use amdgpu for type detection, please report on Github", - gpu_name - ); - GpuDevice::new( - gpu_name, - device.clone(), - render, - card, - None, - gpu_vendor, - None, - gpu_type, - ) - }; - debug!("{:?}", gpu_device); - Ok(gpu_device) - } - None => { - // Couldn't get DRM, mark GPU as not available - let gpu_type = GpuType::Unavailable; - let gpu_device = GpuDevice::new( - gpu_name, - device.clone(), - u32::MAX, - u32::MAX, - None, - gpu_vendor, - None, - gpu_type, - ); - error!( - "{}: cannot fetch DRM nodes, marking as un-available", - gpu_device.name() - ); - Ok(gpu_device) - } + return Ok(gpu_device); } - } else { - let gpu_type = GpuType::Unavailable; - let gpu_name = device - .device_name() - .clone() - .unwrap_or_else(|| "Unknown Device".to_string()); - let gpu_device = GpuDevice::new( - gpu_name, - device.clone(), - u32::MAX, - u32::MAX, - None, - gpu_vendor, - None, - gpu_type, + // DRM couldn't be fetched or AMDGPU ioctl error + error!( + "{}: Cannot fetch DRM nodes or amdgpu ioctl error, marking as un-available", + device.pci_address() ); + } else { + // Not a driver we support (eg. radeon), will be marked as not available error!( - "{}: driver {:?} is not supported by Cardwire, please request support for it on Github", - gpu_device.name(), + "{}: driver {:?} is not supported by Cardwire, please request it on Github", + device.pci_address(), device.driver() ); - Ok(gpu_device) } } // Just set the type to Virtual @@ -393,43 +259,21 @@ impl GpuEnumerator { .device_name() .clone() .unwrap_or_else(|| "Unknown Device".to_string()); - let drm_res = sysfs_get_device_drm(pci_id); - match drm_res { - Some((card, render)) => { - let gpu_type = GpuType::Virtual; - let gpu_device = GpuDevice::new( - gpu_name, - device.clone(), - render, - card, - None, - gpu_vendor, - None, - gpu_type, - ); - info!("{}: Used Virtio to build", gpu_device.name()); - debug!("{:?}", gpu_device); - Ok(gpu_device) - } - None => { - // Couldn't get DRM, mark GPU as not available - let gpu_type = GpuType::Unavailable; - let gpu_device = GpuDevice::new( - gpu_name, - device.clone(), - u32::MAX, - u32::MAX, - None, - gpu_vendor, - None, - gpu_type, - ); - error!( - "{}: cannot fetch DRM nodes, marking as un-available", - gpu_device.name() - ); - Ok(gpu_device) - } + if let Some((card, render)) = sysfs_get_device_drm(pci_id) { + let gpu_type = GpuType::Virtual; + let gpu_device = GpuDevice::new( + gpu_name, + device.clone(), + render, + card, + None, + gpu_vendor, + None, + gpu_type, + ); + info!("{}: Used Virtio to build", gpu_device.name()); + debug!("{:?}", gpu_device); + return Ok(gpu_device); } } // Cardwire depends on knowing the GPU type for the modes, mark Other devices as @@ -439,50 +283,43 @@ impl GpuEnumerator { .device_name() .clone() .unwrap_or_else(|| "Unknown Device".to_string()); - let drm_res = sysfs_get_device_drm(pci_id); - match drm_res { - Some((card, render)) => { - let gpu_type = GpuType::Unknown; - let gpu_device = GpuDevice::new( - gpu_name, - device.clone(), - render, - card, - None, - gpu_vendor, - None, - gpu_type, - ); - warn!( - "{}: unknown device vendor ({:?}/{:?}), please request support for it on Github", - gpu_device.name(), - device.vendor_id(), - device.vendor_name() - ); - debug!("{:?}", gpu_device); - Ok(gpu_device) - } - None => { - // Couldn't get DRM, mark GPU as not available - let gpu_type = GpuType::Unavailable; - let gpu_device = GpuDevice::new( - gpu_name, - device.clone(), - u32::MAX, - u32::MAX, - None, - gpu_vendor, - None, - gpu_type, - ); - error!( - "{}: cannot fetch DRM nodes, marking as un-available", - gpu_device.name() - ); - Ok(gpu_device) - } + if let Some((card, render)) = sysfs_get_device_drm(pci_id) { + let gpu_type = GpuType::Unknown; + let gpu_device = GpuDevice::new( + gpu_name, + device.clone(), + render, + card, + None, + gpu_vendor, + None, + gpu_type, + ); + warn!( + "{}: unknown device vendor ({:?}/{:?}), please request support for it on Github", + gpu_device.name(), + device.vendor_id(), + device.vendor_name() + ); + debug!("{:?}", gpu_device); + return Ok(gpu_device); } } } + // If we are here, an error happend (mostly DRM or libraries), build an un-available GPU + let gpu_name = device + .device_name() + .clone() + .unwrap_or_else(|| "Unknown Device".to_string()); + Ok(GpuDevice::new( + gpu_name, + device.clone(), + u32::MAX, + u32::MAX, + None, + gpu_vendor, + None, + GpuType::Unavailable, + )) } } diff --git a/crates/cardwire-daemon/src/core/gpu/vendor_specific/nvidia.rs b/crates/cardwire-daemon/src/core/gpu/vendor_specific/nvidia.rs index 89ce03b4..9650fa88 100644 --- a/crates/cardwire-daemon/src/core/gpu/vendor_specific/nvidia.rs +++ b/crates/cardwire-daemon/src/core/gpu/vendor_specific/nvidia.rs @@ -8,11 +8,6 @@ use nvml_wrapper::{ }; use tokio::{process::Command, time::timeout}; -#[allow(unused, dead_code)] -pub fn nvidia_get_device_type(pci_id: &str, gpu_name: &str) -> GpuType { - GpuType::Unknown -} - /// Get nvidia minor id pub fn nvidia_get_device_minor(pci_address: &str) -> Option { let nvidia_driver_proc = Path::new("/proc/driver/nvidia/gpus/") @@ -48,6 +43,17 @@ pub fn nvidia_get_device_name(pci_address: &str) -> Option { } } +/// Get the gpu type by using its name +pub fn nvidia_get_device_type(name: &str) -> GpuType { + // I hate this + // This is probably temporary until i come up with a more reliable way to detect without nvml + if name.contains("Geforce") | name.contains("RTX") { + GpuType::Discrete + } else { + GpuType::Unknown + } +} + const SERVICE: &str = "nvidia-powerd.service"; /// run a systemctl command against the nvidia-powerd service and log the result From e55b9056a8ce599eb0a338dd09a7da02553a7d32 Mon Sep 17 00:00:00 2001 From: luytan Date: Tue, 15 Sep 2026 21:29:17 +0200 Subject: [PATCH 24/44] fix(cardwired): use gpu type for systemtype check, and todo --- crates/cardwire-cli/src/types.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/crates/cardwire-cli/src/types.rs b/crates/cardwire-cli/src/types.rs index c03b05f9..52606c81 100644 --- a/crates/cardwire-cli/src/types.rs +++ b/crates/cardwire-cli/src/types.rs @@ -10,29 +10,30 @@ pub enum SystemType { } impl SystemType { pub fn from_gpulist(gpu_list: &BTreeMap) -> Self { - let available_gpus: Vec<(usize, bool, bool)> = gpu_list + let available_gpus: Vec<(usize, bool, GpuType)> = gpu_list .iter() .filter(|(_, gpu)| gpu.device_type != GpuType::Unavailable) - .map(|(id, gpu)| (*id, gpu.default, gpu.device_type == GpuType::Discrete)) + .map(|(id, gpu)| (*id, gpu.default, gpu.device_type.clone())) .collect(); if available_gpus.len() != 2 { Self::Manual } else if available_gpus .iter() - .any(|(_, default, discrete)| *default && *discrete) + .any(|(_, default, device_type)| *default && *device_type == GpuType::Discrete) && available_gpus .iter() - .any(|(_, default, discrete)| !*discrete && !*default) + .any(|(_, default, device_type)| *device_type != GpuType::Discrete && !*default) { - // Has a default discrete GPU and a non-default non-discrete GPU + // Has a default discrete GPU and a non-default non-discrete GPU, desktop and manual are + // pretty much the same, TODO Self::Desktop } else if available_gpus .iter() - .any(|(_, default, discrete)| *discrete && !*default) + .any(|(_, default, device_type)| *device_type == GpuType::Discrete && !*default) && available_gpus .iter() - .any(|(_, default, discrete)| !*discrete && *default) + .any(|(_, default, device_type)| *device_type != GpuType::Discrete && *default) { // Has a non-default discrete GPU and a default non-discrete GPU Self::Laptop From d5136d4c7139f9e024c8b841bed352673e8900cd Mon Sep 17 00:00:00 2001 From: luytan Date: Tue, 15 Sep 2026 21:40:01 +0200 Subject: [PATCH 25/44] feat(cardwired): bump drm retry --- crates/cardwire-daemon/src/core/gpu/enumerator.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/cardwire-daemon/src/core/gpu/enumerator.rs b/crates/cardwire-daemon/src/core/gpu/enumerator.rs index 31664251..4d9c07f6 100644 --- a/crates/cardwire-daemon/src/core/gpu/enumerator.rs +++ b/crates/cardwire-daemon/src/core/gpu/enumerator.rs @@ -81,7 +81,7 @@ impl GpuEnumerator { }; let pci_id = device.pci_address(); // Wait for DRM to be ready, each attempt take 250ms - let _ = wait_for_drm(pci_id, 5); + let _ = wait_for_drm(pci_id, 15); // Check if the gpu info can be fetched using vulkan, if so use vulkan to build the GPU if self.vulkan.vulkan_compatible(pci_id) { From 53745fff849665395f997adc8d9820b0fcbcc3ce Mon Sep 17 00:00:00 2001 From: luytan Date: Tue, 15 Sep 2026 21:40:14 +0200 Subject: [PATCH 26/44] docs(cardwired): add todo for xe ioctl in the future, if intel fails --- crates/cardwire-daemon/src/core/gpu/vendor_specific/intel.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/cardwire-daemon/src/core/gpu/vendor_specific/intel.rs b/crates/cardwire-daemon/src/core/gpu/vendor_specific/intel.rs index c9adeeae..29209870 100644 --- a/crates/cardwire-daemon/src/core/gpu/vendor_specific/intel.rs +++ b/crates/cardwire-daemon/src/core/gpu/vendor_specific/intel.rs @@ -1,9 +1,9 @@ use crate::core::gpu::models::GpuType; /// Get the gpu type for an intel GPU -#[allow(unused, dead_code)] pub fn intel_get_device_type(pci_id: &str) -> GpuType { // PCI id reserved for iGPUs + // TODO: try with xe ioctl if pci_id == "0000:00:02.0" { GpuType::Integrated } else { From 59347b5d7c7829b87a9d4da8d543e598518d376f Mon Sep 17 00:00:00 2001 From: luytan Date: Wed, 16 Sep 2026 20:16:34 +0200 Subject: [PATCH 27/44] fix(cardwired): actually return none if drm devices couldn't be read --- .../cardwire-daemon/src/core/gpu/generic/udev.rs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/crates/cardwire-daemon/src/core/gpu/generic/udev.rs b/crates/cardwire-daemon/src/core/gpu/generic/udev.rs index 8a968ea8..e515f584 100644 --- a/crates/cardwire-daemon/src/core/gpu/generic/udev.rs +++ b/crates/cardwire-daemon/src/core/gpu/generic/udev.rs @@ -25,9 +25,8 @@ pub fn sysfs_get_device_drm(pci_id: &str) -> Option<(u32, u32)> { let syspath = Path::new("/sys/bus/pci/devices").join(pci_id).join("drm"); let drm = syspath.read_dir().ok()?; - // index 0 = card - // index 1 = render - let mut drm_nodes: Vec = vec![0; 2]; + let mut render: Option = None; + let mut card: Option = None; for entry in drm.flatten() { if let Some(str) = entry.file_name().to_str() @@ -37,7 +36,7 @@ pub fn sysfs_get_device_drm(pci_id: &str) -> Option<(u32, u32)> { if let Some(minor_s) = minor_s_opt && let Ok(minor_int) = minor_s.parse::() { - drm_nodes[0] = minor_int; + card = Some(minor_int); continue; } } @@ -48,15 +47,11 @@ pub fn sysfs_get_device_drm(pci_id: &str) -> Option<(u32, u32)> { if let Some(minor_s) = minor_s_opt && let Ok(minor_int) = minor_s.parse::() { - drm_nodes[1] = minor_int; + render = Some(minor_int); continue; } } } - if drm_nodes.is_empty() { - None - } else { - Some((drm_nodes[0] as u32, drm_nodes[1] as u32)) - } + Some((card?, render?)) } From 2c0697953cb881af6548dead0703af98fe6059c7 Mon Sep 17 00:00:00 2001 From: luytan Date: Wed, 16 Sep 2026 20:22:28 +0200 Subject: [PATCH 28/44] fix(cardwired): skip vulkan drm if it fails --- .../src/core/gpu/enumerator.rs | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/crates/cardwire-daemon/src/core/gpu/enumerator.rs b/crates/cardwire-daemon/src/core/gpu/enumerator.rs index 4d9c07f6..05774585 100644 --- a/crates/cardwire-daemon/src/core/gpu/enumerator.rs +++ b/crates/cardwire-daemon/src/core/gpu/enumerator.rs @@ -88,22 +88,28 @@ impl GpuEnumerator { let gpu_type = self.vulkan.get_gpu_type(pci_id); let gpu_name = self.vulkan.get_gpu_name(pci_id); - let gpu_card = self.vulkan.get_gpu_card(pci_id).unwrap_or_default(); - let gpu_render = self.vulkan.get_gpu_render(pci_id).unwrap_or_default(); - - let gpu_device = GpuDevice::new( - gpu_name, - device.clone(), - gpu_render as u32, - gpu_card as u32, - None, - gpu_vendor, - None, - gpu_type, - ); - info!("{}: Used Vulkan to build", gpu_device.name()); - debug!("{:?}", gpu_device); - return Ok(gpu_device); + match ( + self.vulkan.get_gpu_card(pci_id), + self.vulkan.get_gpu_render(pci_id), + ) { + (Some(card), Some(render)) => { + let gpu_device = GpuDevice::new( + gpu_name, + device.clone(), + render as u32, + card as u32, + None, + gpu_vendor, + None, + gpu_type, + ); + info!("{}: Used Vulkan to build", gpu_device.name()); + debug!("{:?}", gpu_device); + return Ok(gpu_device); + } + // Fallback to vendor match + _ => {} + } } // else, fallback to sysfs GPU building From c6bec0b20151ed8dcfc0619ba315a82ef16e93dd Mon Sep 17 00:00:00 2001 From: luytan Date: Wed, 16 Sep 2026 20:27:55 +0200 Subject: [PATCH 29/44] fix(cardwired): remove unwrap in amd.rs --- .../src/core/gpu/vendor_specific/amd.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/crates/cardwire-daemon/src/core/gpu/vendor_specific/amd.rs b/crates/cardwire-daemon/src/core/gpu/vendor_specific/amd.rs index 6619a938..744c7b08 100644 --- a/crates/cardwire-daemon/src/core/gpu/vendor_specific/amd.rs +++ b/crates/cardwire-daemon/src/core/gpu/vendor_specific/amd.rs @@ -17,13 +17,11 @@ impl AmdGpuDev { let (amdgpu_dev, _drm_major, _drm_minor) = { use std::fs::OpenOptions; let path = format!("/dev/dri/renderD{}", render); - let f = OpenOptions::new() - .read(true) - .write(true) - .open(path) - .unwrap(); + let f = OpenOptions::new().read(true).write(true).open(path)?; - libdrm_amdgpu.init_device_handle_with_fd(f).unwrap() + libdrm_amdgpu + .init_device_handle_with_fd(f) + .map_err(CardwireAmdGpuError)? }; let gpu_info = amdgpu_dev.query_gpu_info().map_err(CardwireAmdGpuError)?; Ok(Self { From a01b5c8eae6f1edf448b2f8c8b12a52a6d448d16 Mon Sep 17 00:00:00 2001 From: luytan Date: Wed, 16 Sep 2026 20:32:48 +0200 Subject: [PATCH 30/44] fix(cardwired): prevent un-available gpu inodes from getting searched --- crates/cardwire-daemon/src/core/gpu/mod.rs | 2 +- crates/cardwire-daemon/src/interface/gpu.rs | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/cardwire-daemon/src/core/gpu/mod.rs b/crates/cardwire-daemon/src/core/gpu/mod.rs index 0a099659..161bd4d4 100644 --- a/crates/cardwire-daemon/src/core/gpu/mod.rs +++ b/crates/cardwire-daemon/src/core/gpu/mod.rs @@ -7,5 +7,5 @@ pub use enumerator::GpuEnumerator; pub use generic::default_gpu::check_default_drm_class; #[expect(unused_imports)] pub use generic::display::{external_display_connected, is_gpu_active}; -pub use models::{DbusGpuDevice, GpuDevice, GpuVendor, PowerState}; +pub use models::{DbusGpuDevice, GpuDevice, GpuType, GpuVendor, PowerState}; pub use vendor_specific::nvidia::{start_nvidia_powerd, stop_nvidia_powerd}; diff --git a/crates/cardwire-daemon/src/interface/gpu.rs b/crates/cardwire-daemon/src/interface/gpu.rs index c6db0d88..0b82dbc8 100644 --- a/crates/cardwire-daemon/src/interface/gpu.rs +++ b/crates/cardwire-daemon/src/interface/gpu.rs @@ -6,7 +6,7 @@ use std::{ use crate::{ Result, core::{ - env::is_gpu_launchable, gpu::{DbusGpuDevice, GpuDevice, is_gpu_active}, inode::{card_to_inode, get_inodes, nvidia_to_inode, render_to_inode, single_pci_to_inode}, pci::PciDevice, procfs + env::is_gpu_launchable, gpu::{DbusGpuDevice, GpuDevice, GpuType, is_gpu_active}, inode::{card_to_inode, get_inodes, nvidia_to_inode, render_to_inode, single_pci_to_inode}, pci::PciDevice, procfs }, file::{CardwireGpuState, CardwireModeState}, interface::{Modes, SwitcherooInterface} }; use cardwire_ebpf_userspace::{EbpfBlocker, InodeKey}; @@ -251,6 +251,10 @@ impl GpuInterface { #[zbus(property)] /// Check if the GPU is blocked pub async fn block(&self) -> fdo::Result { + // Directly return for non-available GPUs + if *self.device.device_type() == GpuType::Unavailable { + return Ok(false); + } self.gpu_blocked().await } From 7893ceba03aca0bd7d2697113c7c9f814a053fed Mon Sep 17 00:00:00 2001 From: luytan Date: Wed, 16 Sep 2026 20:34:36 +0200 Subject: [PATCH 31/44] refactor(cardwired): replace match with if let in vulkan drm nodes --- .../src/core/gpu/enumerator.rs | 32 ++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/crates/cardwire-daemon/src/core/gpu/enumerator.rs b/crates/cardwire-daemon/src/core/gpu/enumerator.rs index 05774585..1bd521b6 100644 --- a/crates/cardwire-daemon/src/core/gpu/enumerator.rs +++ b/crates/cardwire-daemon/src/core/gpu/enumerator.rs @@ -88,27 +88,23 @@ impl GpuEnumerator { let gpu_type = self.vulkan.get_gpu_type(pci_id); let gpu_name = self.vulkan.get_gpu_name(pci_id); - match ( + if let (Some(card), Some(render)) = ( self.vulkan.get_gpu_card(pci_id), self.vulkan.get_gpu_render(pci_id), ) { - (Some(card), Some(render)) => { - let gpu_device = GpuDevice::new( - gpu_name, - device.clone(), - render as u32, - card as u32, - None, - gpu_vendor, - None, - gpu_type, - ); - info!("{}: Used Vulkan to build", gpu_device.name()); - debug!("{:?}", gpu_device); - return Ok(gpu_device); - } - // Fallback to vendor match - _ => {} + let gpu_device = GpuDevice::new( + gpu_name, + device.clone(), + render as u32, + card as u32, + None, + gpu_vendor, + None, + gpu_type, + ); + info!("{}: Used Vulkan to build", gpu_device.name()); + debug!("{:?}", gpu_device); + return Ok(gpu_device); } } From 6b0edc5351194fcb8ad414fe3696b6f1303ffe49 Mon Sep 17 00:00:00 2001 From: luytan Date: Wed, 16 Sep 2026 20:35:59 +0200 Subject: [PATCH 32/44] fix(cardwired): typo in geforce and add GTX --- crates/cardwire-daemon/src/core/gpu/vendor_specific/nvidia.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/cardwire-daemon/src/core/gpu/vendor_specific/nvidia.rs b/crates/cardwire-daemon/src/core/gpu/vendor_specific/nvidia.rs index 9650fa88..5bc6fa83 100644 --- a/crates/cardwire-daemon/src/core/gpu/vendor_specific/nvidia.rs +++ b/crates/cardwire-daemon/src/core/gpu/vendor_specific/nvidia.rs @@ -47,7 +47,7 @@ pub fn nvidia_get_device_name(pci_address: &str) -> Option { pub fn nvidia_get_device_type(name: &str) -> GpuType { // I hate this // This is probably temporary until i come up with a more reliable way to detect without nvml - if name.contains("Geforce") | name.contains("RTX") { + if name.contains("GeForce") | name.contains("RTX") | name.contains("GTX") { GpuType::Discrete } else { GpuType::Unknown From a10738ea780833fc99ab8ddaa10ec30ded7f0f2b Mon Sep 17 00:00:00 2001 From: luytan Date: Wed, 16 Sep 2026 22:25:54 +0200 Subject: [PATCH 33/44] ci: fix bpf-linker to 0.11.0 --- .github/workflows/cicd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index a2a4dd01..c4f6a226 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -85,7 +85,7 @@ jobs: with: version: "1.21.1" - name: Install bpf-linker - run: cargo binstall bpf-linker + run: cargo binstall --version 0.11.0 --locked --no-confirm bpf-linker - name: Clippy Workspace uses: auguwu/clippy-action@9817d076b82df0194935be9db6154c56ac07b317 # 1.5.0 with: From b32a3cd35b943aaa98be5665b5bbc57f95fee750 Mon Sep 17 00:00:00 2001 From: luytan Date: Sat, 19 Sep 2026 17:10:00 +0200 Subject: [PATCH 34/44] feat: update nix systemd service --- nix/nixos-module.nix | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/nixos-module.nix b/nix/nixos-module.nix index f9d103ec..55a03252 100644 --- a/nix/nixos-module.nix +++ b/nix/nixos-module.nix @@ -63,10 +63,10 @@ in systemd.services.cardwired = { unitConfig = { Description = "Cardwire Daemon"; - Wants = [ "systemd-udev-settle.service" ]; + Wants = [ "multi-user.target" ]; + Before = [ "graphical.target" ]; After = [ - "dbus.service" - "systemd-udev-settle.service" + "multi-user.target" ]; }; serviceConfig = { @@ -111,7 +111,7 @@ in "~`@cpu-emulation` `@module` `@obsolete` `@raw-io` `@reboot` `@swap`" ]; }; - wantedBy = [ "multi-user.target" ]; + wantedBy = [ "graphical.target" ]; }; }; } From 066f2d0884a2eebae0d8c7b9f503b1b1db47985f Mon Sep 17 00:00:00 2001 From: luytan Date: Sat, 19 Sep 2026 18:09:54 +0200 Subject: [PATCH 35/44] fix: load before multi-user --- assets/cardwired.service | 8 ++++---- nix/nixos-module.nix | 9 ++++----- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/assets/cardwired.service b/assets/cardwired.service index c3b1534e..91e207d5 100644 --- a/assets/cardwired.service +++ b/assets/cardwired.service @@ -1,10 +1,10 @@ [Unit] Description=Cardwire Daemon -Before=graphical.target +Before=graphical.target display-manager.service # drm-module-load.target is from cachyos # -After=multi-user.target drm-module-load.target -Wants=multi-user.target drm-module-load.target +After=drm-module-load.target +Wants=drm-module-load.target [Service] @@ -42,4 +42,4 @@ SystemCallFilter=~@cpu-emulation @module @obsolete @raw-io @reboot @swap [Install] -WantedBy=graphical.target +WantedBy=multi-user.target diff --git a/nix/nixos-module.nix b/nix/nixos-module.nix index 55a03252..a9d2c036 100644 --- a/nix/nixos-module.nix +++ b/nix/nixos-module.nix @@ -63,10 +63,9 @@ in systemd.services.cardwired = { unitConfig = { Description = "Cardwire Daemon"; - Wants = [ "multi-user.target" ]; - Before = [ "graphical.target" ]; - After = [ - "multi-user.target" + Before = [ + "graphical.target" + "display-manager.service" ]; }; serviceConfig = { @@ -111,7 +110,7 @@ in "~`@cpu-emulation` `@module` `@obsolete` `@raw-io` `@reboot` `@swap`" ]; }; - wantedBy = [ "graphical.target" ]; + wantedBy = [ "multi-user.target" ]; }; }; } From c1093b016880b8b8f7d1b7dc008bbe81aac1c39c Mon Sep 17 00:00:00 2001 From: luytan Date: Sun, 20 Sep 2026 08:45:58 +0200 Subject: [PATCH 36/44] feat(cardwired): add debug logs to nvml --- .../src/core/gpu/vendor_specific/nvidia.rs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/crates/cardwire-daemon/src/core/gpu/vendor_specific/nvidia.rs b/crates/cardwire-daemon/src/core/gpu/vendor_specific/nvidia.rs index 5bc6fa83..907b2964 100644 --- a/crates/cardwire-daemon/src/core/gpu/vendor_specific/nvidia.rs +++ b/crates/cardwire-daemon/src/core/gpu/vendor_specific/nvidia.rs @@ -2,7 +2,7 @@ use crate::core::gpu::models::GpuType; use std::{fs, path::Path, thread, time::Duration}; -use log::{error, info, warn}; +use log::{debug, error, info, warn}; use nvml_wrapper::{ Nvml, enum_wrappers::device::{Brand, GpuVirtualizationMode}, enums::device::DeviceArchitecture, error::NvmlError }; @@ -182,6 +182,7 @@ pub fn nvidia_get_device_minor_nvml(nvml: &Nvml, pci_id: &str) -> Option { pub fn nvidia_get_device_type_nvml(nvml: &Nvml, pci_id: &str) -> GpuType { if let Ok(nvidia_dev) = nvml.device_by_pci_bus_id(pci_id) { if let Ok(virt_mode) = nvidia_dev.virtualization_mode() { + debug!("[{}]: nvml virt_mode: {:?}", pci_id, virt_mode); match virt_mode { GpuVirtualizationMode::Vgpu => return GpuType::Virtual, // Do not want to assume this one @@ -202,6 +203,7 @@ pub fn nvidia_get_device_type_nvml(nvml: &Nvml, pci_id: &str) -> GpuType { | DeviceArchitecture::Hopper | DeviceArchitecture::Blackwell, ) => { + debug!("[{}]: nvml arch: {:?}", pci_id, nvidia_dev.architecture()); return GpuType::Discrete; } // Architecture not implemented by nvml_wrapper yet @@ -210,15 +212,19 @@ pub fn nvidia_get_device_type_nvml(nvml: &Nvml, pci_id: &str) -> GpuType { // 15 is NPU3 // 13 is RUBIN // - Err(NvmlError::UnexpectedVariant(raw)) => match raw { - 11 | 12 | 15 => return GpuType::Integrated, - 13 => return GpuType::Discrete, - _ => {} - }, + Err(NvmlError::UnexpectedVariant(raw)) => { + debug!("[{}]: nvml arch raw: {:?}", pci_id, raw); + match raw { + 11 | 12 | 15 => return GpuType::Integrated, + 13 => return GpuType::Discrete, + _ => {} + } + } _ => {} } // Pretty much a fallback, i hope it doesnt get used if let Ok(brand) = nvidia_dev.brand() { + debug!("[{}]: nvml brand: {:?}", pci_id, brand); match brand { Brand::Quadro | Brand::Tesla @@ -241,5 +247,6 @@ pub fn nvidia_get_device_type_nvml(nvml: &Nvml, pci_id: &str) -> GpuType { } } } + debug!("[{}]: nvml fallback", pci_id); GpuType::Unknown } From 0aaa1e6ac866d818ea0b770edf100bdb92ef90c5 Mon Sep 17 00:00:00 2001 From: luytan Date: Sun, 20 Sep 2026 10:20:19 +0200 Subject: [PATCH 37/44] feat(cardwired): add external Gpu Type --- crates/cardwire-daemon/src/core/gpu/models.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/cardwire-daemon/src/core/gpu/models.rs b/crates/cardwire-daemon/src/core/gpu/models.rs index 9e2d48f6..a6902599 100644 --- a/crates/cardwire-daemon/src/core/gpu/models.rs +++ b/crates/cardwire-daemon/src/core/gpu/models.rs @@ -98,6 +98,7 @@ pub enum GpuType { Unavailable = 4, #[default] Unknown = 5, + External = 6, } #[derive(Clone, Debug, serde::Serialize, serde::Deserialize, zbus::zvariant::Type, PartialEq)] From f1fd2c3c8a8cf9136e8ca21e74467036b05c3abc Mon Sep 17 00:00:00 2001 From: luytan Date: Sun, 20 Sep 2026 10:20:56 +0200 Subject: [PATCH 38/44] feat(cardwired): add External type to the systemtype impl, remove Desktop --- crates/cardwire-daemon/src/interface/mode.rs | 4 +- crates/cardwire-daemon/src/types.rs | 43 +++++++++++--------- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/crates/cardwire-daemon/src/interface/mode.rs b/crates/cardwire-daemon/src/interface/mode.rs index 9d382128..f8cc56e8 100644 --- a/crates/cardwire-daemon/src/interface/mode.rs +++ b/crates/cardwire-daemon/src/interface/mode.rs @@ -163,7 +163,7 @@ impl ModeInterface { // Else apply the gpu_state but still unblock other gpus Modes::Manual => { // Manual is only allowed on Desktop or Manual - if system_type != SystemType::Manual && system_type != SystemType::Desktop { + if system_type != SystemType::Manual { let error_message = format!( "Couldn't set mode to {}, Manual mode is only available on Desktop or system with either 1 GPU or 3+ GPUs", mode @@ -240,7 +240,7 @@ impl ModeInterface { SystemType::Laptop => { vec![Modes::Integrated, Modes::Hybrid, Modes::Smart] } - SystemType::Desktop | SystemType::Manual => { + SystemType::Manual => { vec![Modes::Hybrid, Modes::Manual] } }) diff --git a/crates/cardwire-daemon/src/types.rs b/crates/cardwire-daemon/src/types.rs index ea5c84ee..6c1e0d99 100644 --- a/crates/cardwire-daemon/src/types.rs +++ b/crates/cardwire-daemon/src/types.rs @@ -3,7 +3,9 @@ use serde::{Deserialize, Serialize}; use std::{collections::BTreeMap, fmt, sync::Arc}; -use crate::{Result, core::errors::CardwireError, interface::GpuInterface}; +use crate::{ + Result, core::{errors::CardwireError, gpu::GpuType}, interface::GpuInterface +}; #[derive(Deserialize, Serialize, PartialEq, zbus::zvariant::Type, Clone, Copy, Default, Debug)] #[serde(rename_all = "snake_case")] @@ -56,42 +58,43 @@ impl From for u32 { } } +/* + Laptop = 1 integrated default + 1 discrete/eGPU non default + Manual = Others +*/ + #[derive(Clone, Debug, PartialEq)] pub enum SystemType { Laptop, - Desktop, Manual, } impl SystemType { pub fn from_gpulist(gpu_list: &BTreeMap>) -> Self { - let available_gpus: Vec<(usize, bool, bool)> = gpu_list + // Sort to keep available GPUs, and only keep id, default and gpu type + let available_gpus: Vec<(usize, bool, GpuType)> = gpu_list .iter() .filter(|(_, gpu)| gpu.device.is_available()) - .map(|(id, gpu)| (*id, gpu.device.is_default(), gpu.device.is_discrete())) + .map(|(id, gpu)| { + ( + *id, + gpu.device.is_default(), + gpu.device.device_type().clone(), + ) + }) .collect(); + // Directly assign system with less or more than 2 GPUs if available_gpus.len() != 2 { Self::Manual - } else if available_gpus - .iter() - .any(|(_, default, discrete)| *default && *discrete) - && available_gpus - .iter() - .any(|(_, default, discrete)| !*discrete && !*default) - { - // Has a default discrete GPU and a non-default non-discrete GPU - Self::Desktop - } else if available_gpus + } else if available_gpus.iter().any(|(_, default, gpu_type)| { + *gpu_type == GpuType::Discrete || *gpu_type == GpuType::External && !*default + }) && available_gpus .iter() - .any(|(_, default, discrete)| *discrete && !*default) - && available_gpus - .iter() - .any(|(_, default, discrete)| !*discrete && *default) + .any(|(_, default, gpu_type)| *gpu_type == GpuType::Integrated && *default) { - // Has a non-default discrete GPU and a default non-discrete GPU + // Has a non-default discrete/external GPU and a default integrated GPU Self::Laptop } else { - // Even if it's a desktop, we treat it as a Manual if it doesn't have the iGPU Self::Manual } } From 75c2d27d5ae89fd0a223b80be2fcf36862ad509f Mon Sep 17 00:00:00 2001 From: luytan Date: Sun, 20 Sep 2026 13:38:42 +0200 Subject: [PATCH 39/44] refactor(cardwired): simplify vulkan compatible with a one liner --- crates/cardwire-daemon/src/core/gpu/generic/vulkan.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/crates/cardwire-daemon/src/core/gpu/generic/vulkan.rs b/crates/cardwire-daemon/src/core/gpu/generic/vulkan.rs index 179ebaad..19a10197 100644 --- a/crates/cardwire-daemon/src/core/gpu/generic/vulkan.rs +++ b/crates/cardwire-daemon/src/core/gpu/generic/vulkan.rs @@ -20,13 +20,9 @@ impl Vulkan { /// Verify if the device pci id is in the vulkan enum map pub fn vulkan_compatible(&self, pci_id: &str) -> bool { - if let Some(vlk_map) = &self.vlk_physical_devices - && vlk_map.contains_key(pci_id) - { - true - } else { - false - } + self.vlk_physical_devices + .as_ref() + .is_some_and(|map| map.contains_key(pci_id)) } /// get the gpu type using vulkan From 980e990fdef7db1444a91d4d31b9078287b61505 Mon Sep 17 00:00:00 2001 From: luytan Date: Sun, 20 Sep 2026 13:48:16 +0200 Subject: [PATCH 40/44] feat(cardwire-gui): update for the new Gpu API --- crates/cardwire-gui/src/helpers/dbus.rs | 37 +++++++++++++++++-------- crates/cardwire-gui/src/helpers/mod.rs | 2 +- crates/cardwire-gui/src/tray.rs | 32 ++++++++++++--------- crates/cardwire-gui/src/ui.rs | 16 +++++------ 4 files changed, 52 insertions(+), 35 deletions(-) diff --git a/crates/cardwire-gui/src/helpers/dbus.rs b/crates/cardwire-gui/src/helpers/dbus.rs index b9314fc5..cfddcc97 100644 --- a/crates/cardwire-gui/src/helpers/dbus.rs +++ b/crates/cardwire-gui/src/helpers/dbus.rs @@ -6,6 +6,28 @@ use zbus::{ use crate::models::{DaemonSettings, DbusAppMetadata, LsofData, Mode}; +#[derive( + Clone, + Debug, + serde::Serialize, + serde::Deserialize, + Default, + PartialEq, + zbus::zvariant::Type, + Copy, +)] +#[repr(u32)] +pub enum GpuType { + Integrated = 0, + Discrete = 1, + Virtual = 2, + Other = 3, + Unavailable = 4, + #[default] + Unknown = 5, + External = 6, +} + #[derive(serde::Deserialize, serde::Serialize, Debug, Clone)] pub struct GpuDevice { pub id: u32, @@ -14,13 +36,10 @@ pub struct GpuDevice { pub render: u32, pub card: u32, pub default: bool, - pub discrete: bool, - pub virtual_gpu: bool, - pub available: bool, + pub device_type: GpuType, pub vendor: String, pub driver: String, pub blocked: bool, - pub nvidia: bool, pub nvidia_minor: String, pub power_state: Option, } @@ -32,12 +51,9 @@ pub struct DbusGpuDevice { pub render: u32, pub card: u32, pub default: bool, - pub discrete: bool, - pub virtual_gpu: bool, - pub available: bool, + pub device_type: GpuType, pub vendor: String, pub driver: String, - pub nvidia: bool, pub nvidia_minor: String, } @@ -98,13 +114,10 @@ impl CardwireDbus { render: dbus_dev.render, card: dbus_dev.card, default: dbus_dev.default, - discrete: dbus_dev.discrete, - virtual_gpu: dbus_dev.virtual_gpu, - available: dbus_dev.available, + device_type: dbus_dev.device_type, vendor: dbus_dev.vendor, driver: dbus_dev.driver, blocked, - nvidia: dbus_dev.nvidia, nvidia_minor: dbus_dev.nvidia_minor, power_state: None, }; diff --git a/crates/cardwire-gui/src/helpers/mod.rs b/crates/cardwire-gui/src/helpers/mod.rs index 2274fc98..d2919fec 100644 --- a/crates/cardwire-gui/src/helpers/mod.rs +++ b/crates/cardwire-gui/src/helpers/mod.rs @@ -2,4 +2,4 @@ pub mod app_resolver; mod dbus; pub use app_resolver::resolve_app_metadata; -pub use dbus::{CardwireDbus, GpuDevice}; +pub use dbus::{CardwireDbus, GpuDevice, GpuType}; diff --git a/crates/cardwire-gui/src/tray.rs b/crates/cardwire-gui/src/tray.rs index d7760efe..a164e711 100644 --- a/crates/cardwire-gui/src/tray.rs +++ b/crates/cardwire-gui/src/tray.rs @@ -259,13 +259,14 @@ pub async fn notify(message: String) { #[cfg(test)] mod tests { use super::*; + use crate::helpers::GpuType; #[allow(clippy::too_many_arguments)] fn gpu( name: &str, default: bool, blocked: bool, - discrete: bool, + device_type: GpuType, power_state: &str, ) -> GpuDevice { GpuDevice { @@ -275,13 +276,10 @@ mod tests { render: 0, card: 0, default, - discrete, - virtual_gpu: false, - available: true, + device_type, vendor: String::new(), driver: String::new(), blocked, - nvidia: false, nvidia_minor: String::new(), power_state: Some(power_state.to_string()), } @@ -315,10 +313,14 @@ mod tests { #[test] fn manual_menu_only_lists_non_default_gpus() { let (mut tray, _) = tray(Some(Mode::Manual)); - tray.gpus - .insert(0, gpu("Integrated", true, false, false, "active")); - tray.gpus - .insert(1, gpu("Discrete", false, true, true, "suspended")); + tray.gpus.insert( + 0, + gpu("Integrated", true, false, GpuType::Integrated, "active"), + ); + tray.gpus.insert( + 1, + gpu("Discrete", false, true, GpuType::Discrete, "suspended"), + ); let submenu = tray.menu().into_iter().find_map(|item| match item { MenuItem::SubMenu(item) => Some(item), _ => None, @@ -329,8 +331,10 @@ mod tests { #[test] fn blocked_gpu_checkmark_requests_unblock() { let (mut tray, mut actions) = tray(Some(Mode::Manual)); - tray.gpus - .insert(1, gpu("Discrete", false, true, true, "suspended")); + tray.gpus.insert( + 1, + gpu("Discrete", false, true, GpuType::Discrete, "suspended"), + ); let checkmark = tray.menu().into_iter().find_map(|item| match item { MenuItem::SubMenu(submenu) => submenu.submenu.into_iter().find_map(|item| match item { MenuItem::Checkmark(checkmark) => Some(checkmark), @@ -352,8 +356,10 @@ mod tests { #[test] fn tooltip_reports_gpu_state() { let (mut tray, _) = tray(Some(Mode::Hybrid)); - tray.gpus - .insert(0, gpu("Integrated", true, false, false, "active\n")); + tray.gpus.insert( + 0, + gpu("Integrated", true, false, GpuType::Integrated, "active\n"), + ); assert!( tray.tool_tip() .description diff --git a/crates/cardwire-gui/src/ui.rs b/crates/cardwire-gui/src/ui.rs index 1b98794b..3e483435 100644 --- a/crates/cardwire-gui/src/ui.rs +++ b/crates/cardwire-gui/src/ui.rs @@ -9,7 +9,7 @@ use std::collections::BTreeMap; use strum::{IntoEnumIterator, VariantArray}; use crate::{ - gui_config::{GuiConfig, PrimaryClickAction}, helpers::GpuDevice, message::Message, models::{ + gui_config::{GuiConfig, PrimaryClickAction}, helpers::{GpuDevice, GpuType}, message::Message, models::{ LogEntry, LogState, LsofData, MainState, Mode, Page, PciDevice, ResolvedApp, SettingState, SmartState } }; @@ -358,7 +358,7 @@ fn gpu_cards( let gpu_id = *id; let is_blocked = gpu.blocked; - let is_available = gpu.available; + let is_available = gpu.device_type != GpuType::Unavailable; // Build dropdown menu items let mut dropdown_col = column![]; @@ -428,7 +428,7 @@ fn gpu_cards( .size(15) .color(Color::from_rgb(0.72, 0.72, 0.75)) .width(width), - text(gpu.discrete) + text(gpu.device_type == GpuType::Discrete) .size(15) .color(Color::from_rgb(0.92, 0.92, 0.92)) ], @@ -473,7 +473,7 @@ fn gpu_cards( .size(15) .color(Color::from_rgb(0.72, 0.72, 0.75)) .width(width), - text(gpu.virtual_gpu) + text(gpu.device_type == GpuType::Virtual) .size(15) .color(Color::from_rgb(0.92, 0.92, 0.92)) ], @@ -525,11 +525,9 @@ fn gpu_cards( .size(15) .color(Color::from_rgb(0.72, 0.72, 0.75)) .width(width), - text(gpu.available).size(15).color(Color::from_rgb( - 239.0 / 255.0, - 68.0 / 255.0, - 68.0 / 255.0 - )) + text(gpu.device_type != GpuType::Unavailable) + .size(15) + .color(Color::from_rgb(239.0 / 255.0, 68.0 / 255.0, 68.0 / 255.0)) ] ] .spacing(8) From 9093ef73f893a668c3296c2d275e260d43fc0b8f Mon Sep 17 00:00:00 2001 From: luytan Date: Sun, 20 Sep 2026 13:57:15 +0200 Subject: [PATCH 41/44] feat(packaging): add libdrm --- crates/cardwire-daemon/Cargo.toml | 2 +- packages/arch-linux/cardwire-PKGBUILD | 2 +- packages/arch-linux/cardwire-git-PKGBUILD | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/cardwire-daemon/Cargo.toml b/crates/cardwire-daemon/Cargo.toml index a7be62ee..26fb01f2 100644 --- a/crates/cardwire-daemon/Cargo.toml +++ b/crates/cardwire-daemon/Cargo.toml @@ -43,7 +43,7 @@ section = "utils" priority = "optional" extended-description = """\ GPU manager for Linux using eBPF LSM hooks.""" -depends = "hwdata, dbus, systemd, upower, udev, libgcc-s1, libc6, libudev1" +depends = "hwdata, dbus, systemd, upower, udev, libgcc-s1, libc6, libudev1, libdrm2" assets = [ ["target/release/cardwired", "usr/bin/", "755"], diff --git a/packages/arch-linux/cardwire-PKGBUILD b/packages/arch-linux/cardwire-PKGBUILD index 9184279c..0e087fc3 100644 --- a/packages/arch-linux/cardwire-PKGBUILD +++ b/packages/arch-linux/cardwire-PKGBUILD @@ -8,7 +8,7 @@ pkgdesc='GPU manager for Linux using eBPF LSM hooks' arch=('x86_64') url='https://github.com/OpenGamingCollective/cardwire' license=('GPL3') -depends=('hwdata' 'dbus' 'sqlite' 'systemd' 'upower') +depends=('hwdata' 'dbus' 'sqlite' 'systemd' 'upower' 'libdrm') makedepends=('rust' 'rust-src' 'cargo-binstall' 'libxcb') source=("https://github.com/OpenGamingCollective/cardwire/archive/refs/tags/v$pkgver.tar.gz") sha256sums=('eba92c952f002767abead1391d9d2d229e3637a756ac4786759a0012b7e96649') diff --git a/packages/arch-linux/cardwire-git-PKGBUILD b/packages/arch-linux/cardwire-git-PKGBUILD index b976d6a7..ea29b08c 100644 --- a/packages/arch-linux/cardwire-git-PKGBUILD +++ b/packages/arch-linux/cardwire-git-PKGBUILD @@ -8,7 +8,7 @@ pkgdesc='GPU manager for Linux using eBPF LSM hooks' arch=('x86_64') url='https://github.com/OpenGamingCollective/cardwire' license=('GPL-3.0-only') -depends=('hwdata' 'dbus' 'sqlite' 'systemd' 'upower') +depends=('hwdata' 'dbus' 'sqlite' 'systemd' 'upower' 'libdrm') makedepends=('git' 'rust' 'rust-src' 'libxcb') provides=("$_pkgname") conflicts=("$_pkgname") From 857a932fd3d0b9cd63c6ffe8d337d53fee760465 Mon Sep 17 00:00:00 2001 From: luytan Date: Sun, 20 Sep 2026 14:16:24 +0200 Subject: [PATCH 42/44] feat(packaging): align nix with nixpkgs upstream, and add libdrm to the path --- nix/default.nix | 76 ++++++++++++++++++++----------------------------- 1 file changed, 31 insertions(+), 45 deletions(-) diff --git a/nix/default.nix b/nix/default.nix index 547fe66d..2d8d5674 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -13,36 +13,20 @@ pkgs.rustPlatform.buildRustPackage { src = ./..; cargoLock.lockFile = ../Cargo.lock; + __structuredAttrs = true; + nativeBuildInputs = [ - pkgs.clang - pkgs.installShellFiles - pkgs.makeWrapper pkgs.pkg-config pkgs.bpf-linker + pkgs.makeBinaryWrapper + pkgs.installShellFiles ]; buildInputs = [ - pkgs.hwdata - pkgs.libbpf pkgs.udev - pkgs.vulkan-headers - pkgs.libglvnd - pkgs.egl-wayland - pkgs.egl-x11 pkgs.libxcb ]; - runtimeDeps = [ - pkgs.hwdata - pkgs.upower - pkgs.udev - pkgs.wayland - pkgs.libxkbcommon - pkgs.vulkan-loader - pkgs.libglvnd - pkgs.libdrm - ]; - doCheck = false; doInstallCheck = true; @@ -74,40 +58,42 @@ pkgs.rustPlatform.buildRustPackage { postInstall = '' install -Dm444 ./assets/org.opengamingcollective.cardwire.conf \ - $out/share/dbus-1/system.d/org.opengamingcollective.cardwire.conf - - install -Dm444 ./assets/cardwire-gui.desktop \ - $out/share/applications/cardwire-gui.desktop + $out/share/dbus-1/system.d/org.opengamingcollective.cardwire.conf install -Dm444 ./assets/org.opengamingcollective.cardwire.metainfo.xml \ - $out/share/metainfo/org.opengamingcollective.cardwire.metainfo.xml + $out/share/metainfo/org.opengamingcollective.cardwire.metainfo.xml + + install -Dm444 ./assets/cardwire-gui.desktop \ + $out/share/applications/cardwire-gui.desktop for icon in ./assets/icons/*.svg; do install -Dm444 "$icon" "$out/share/icons/hicolor/scalable/apps/$(basename "$icon")" done - installShellCompletion --cmd cardwire \ - --fish <($out/bin/cardwire completion fish) - wrapProgram $out/bin/cardwired \ - --prefix LD_LIBRARY_PATH : ${ - lib.makeLibraryPath [ - pkgs.udev - pkgs.upower - pkgs.vulkan-loader - pkgs.libglvnd - pkgs.libdrm - ] - } + --prefix LD_LIBRARY_PATH : ${ + lib.makeLibraryPath [ + pkgs.vulkan-loader + pkgs.libglvnd + pkgs.libdrm + ] + } wrapProgram $out/bin/cardwire-gui \ - --prefix LD_LIBRARY_PATH : ${ - lib.makeLibraryPath [ - pkgs.wayland - pkgs.libxkbcommon - pkgs.vulkan-loader - pkgs.libGL - ] - } + --prefix LD_LIBRARY_PATH : ${ + lib.makeLibraryPath [ + pkgs.wayland + pkgs.libxkbcommon + pkgs.vulkan-loader + pkgs.libGL + ] + } + + '' + + lib.optionalString (pkgs.stdenv.buildPlatform.canExecute pkgs.stdenv.hostPlatform) '' + installShellCompletion --cmd cardwire \ + --fish <($out/bin/cardwire completion fish) \ + --bash <($out/bin/cardwire completion bash) \ + --zsh <($out/bin/cardwire completion zsh) ''; } From 0ab7982cb1fe6bfe8316773025debcafd97799c4 Mon Sep 17 00:00:00 2001 From: luytan Date: Sun, 20 Sep 2026 14:29:01 +0200 Subject: [PATCH 43/44] feat(cardwired): update mode error message --- crates/cardwire-daemon/src/interface/mode.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/cardwire-daemon/src/interface/mode.rs b/crates/cardwire-daemon/src/interface/mode.rs index f8cc56e8..e094e21f 100644 --- a/crates/cardwire-daemon/src/interface/mode.rs +++ b/crates/cardwire-daemon/src/interface/mode.rs @@ -131,7 +131,7 @@ impl ModeInterface { // Check if there is an offload discrete GPU (discrete and not the default display) if system_type != SystemType::Laptop { let error_message = format!( - "Couldn't set mode to {}, Integrated and Smart modes require a offload discrete GPU (not supported on desktops where the discrete GPU is the primary display)", + "Couldn't set mode to {}, Integrated and Smart modes are only available on laptops with a offload discrete GPU", mode ); error!("{}", error_message); @@ -165,7 +165,7 @@ impl ModeInterface { // Manual is only allowed on Desktop or Manual if system_type != SystemType::Manual { let error_message = format!( - "Couldn't set mode to {}, Manual mode is only available on Desktop or system with either 1 GPU or 3+ GPUs", + "Couldn't set mode to {}, Manual mode is only available laptops with a offload discrete GPU", mode ); error!("{}", error_message); From 5d99197a6edc9ada110bfcdfdaa686f7657d11bf Mon Sep 17 00:00:00 2001 From: luytan Date: Sun, 20 Sep 2026 14:29:35 +0200 Subject: [PATCH 44/44] feat(packaging): add libdrm-amdgpu1 to debian --- crates/cardwire-daemon/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/cardwire-daemon/Cargo.toml b/crates/cardwire-daemon/Cargo.toml index 26fb01f2..ac353edf 100644 --- a/crates/cardwire-daemon/Cargo.toml +++ b/crates/cardwire-daemon/Cargo.toml @@ -43,7 +43,7 @@ section = "utils" priority = "optional" extended-description = """\ GPU manager for Linux using eBPF LSM hooks.""" -depends = "hwdata, dbus, systemd, upower, udev, libgcc-s1, libc6, libudev1, libdrm2" +depends = "hwdata, dbus, systemd, upower, udev, libgcc-s1, libc6, libudev1, libdrm2, libdrm-amdgpu1" assets = [ ["target/release/cardwired", "usr/bin/", "755"],