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: diff --git a/Cargo.lock b/Cargo.lock index 5b6feabf..9c1638a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -772,7 +772,9 @@ dependencies = [ "freedesktop-desktop-entry", "inotify", "khronos-egl", + "libdrm_amdgpu_sys", "log", + "nvml-wrapper", "rusqlite", "serde", "serde_json", @@ -1177,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" @@ -1349,7 +1386,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]] @@ -1475,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" @@ -2197,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" @@ -2511,6 +2560,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" @@ -3013,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" @@ -4096,7 +4178,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4595,10 +4677,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 +5574,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]] @@ -5899,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 8e4368e6..19296195 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,8 +35,10 @@ 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"]} +nvml-wrapper = "0.13.0" # EBPF aya = "0.14.0" diff --git a/assets/cardwired.service b/assets/cardwired.service index 1fa6d314..91e207d5 100644 --- a/assets/cardwired.service +++ b/assets/cardwired.service @@ -1,6 +1,10 @@ [Unit] Description=Cardwire Daemon -After=dbus.service +Before=graphical.target display-manager.service +# drm-module-load.target is from cachyos +# +After=drm-module-load.target +Wants=drm-module-load.target [Service] 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 + let available_gpus: Vec<(usize, bool, GpuType)> = 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.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 diff --git a/crates/cardwire-daemon/Cargo.toml b/crates/cardwire-daemon/Cargo.toml index 22f0751b..ac353edf 100644 --- a/crates/cardwire-daemon/Cargo.toml +++ b/crates/cardwire-daemon/Cargo.toml @@ -28,6 +28,8 @@ vulkano.workspace = true khronos-egl.workspace = true rusqlite.workspace = true inotify.workspace = true +libdrm_amdgpu_sys.workspace = true +nvml-wrapper.workspace = true [[bin]] name = "cardwired" @@ -41,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, libdrm-amdgpu1" assets = [ ["target/release/cardwired", "usr/bin/", "755"], diff --git a/crates/cardwire-daemon/src/core/env.rs b/crates/cardwire-daemon/src/core/env.rs index 4194a8e6..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); } diff --git a/crates/cardwire-daemon/src/core/errors.rs b/crates/cardwire-daemon/src/core/errors.rs index 94ca97ed..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; @@ -44,6 +45,12 @@ pub enum CardwireError { #[error("Error with state_file {0}: {1}")] CardwireStateError(String, serde_json::Error), + #[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/device_info.rs b/crates/cardwire-daemon/src/core/gpu/device_info.rs deleted file mode 100644 index b287f4b4..00000000 --- a/crates/cardwire-daemon/src/core/gpu/device_info.rs +++ /dev/null @@ -1,74 +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 -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 -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/display.rs b/crates/cardwire-daemon/src/core/gpu/display.rs deleted file mode 100644 index 6369bcb6..00000000 --- a/crates/cardwire-daemon/src/core/gpu/display.rs +++ /dev/null @@ -1,152 +0,0 @@ -//! DRM display connector detection and node resolution. - -use log::{info, warn}; -use std::{fs, io, path::Path, time::Duration}; -use udev::{Device, Enumerator}; - -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")? { - let entry = entry?; - let name = entry.file_name(); - let name = name.to_string_lossy(); - let Some(connector) = name.strip_prefix(&card_prefix) else { - continue; - }; - if connector.is_empty() - || NON_PHYSICAL - .iter() - .any(|prefix| connector.starts_with(prefix)) - || INTERNAL_PANELS - .iter() - .any(|prefix| connector.starts_with(prefix)) - { - continue; - } - - 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) => { - status_error.get_or_insert(err); - } - } - } - - // Fail safely instead of allowing incomplete topology information to block a display GPU. - match status_error { - Some(err) => Err(err), - None => Ok(false), - } -} - -/// 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` -pub async fn is_gpu_active(card: u32) -> Option { - let prefix = format!("card{card}-"); - let mut entries = tokio::fs::read_dir("/sys/class/drm").await.ok()?; - let mut status_error = None; - while let Some(entry) = entries.next_entry().await.ok()? { - let name = entry.file_name(); - let name = name.to_string_lossy(); - let Some(connector) = name.strip_prefix(&prefix) else { - continue; - }; - if connector.is_empty() - || NON_PHYSICAL - .iter() - .any(|prefix| connector.starts_with(prefix)) - { - continue; - } - match tokio::fs::read_to_string(entry.path().join("status")).await { - Ok(status) if status.trim() == "connected" => return Some(true), - Ok(_) => {} - Err(err) => { - status_error.get_or_insert(err); - } - } - } - match status_error { - Some(_) => None, - None => Some(false), - } -} diff --git a/crates/cardwire-daemon/src/core/gpu/enumerator.rs b/crates/cardwire-daemon/src/core/gpu/enumerator.rs index 8259ec48..1bd521b6 100644 --- a/crates/cardwire-daemon/src/core/gpu/enumerator.rs +++ b/crates/cardwire-daemon/src/core/gpu/enumerator.rs @@ -1,33 +1,34 @@ -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 log::{debug, error, info, warn}; 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, 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_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 }; 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 } } + /// Enumerate the GPUS on the host system pub fn enumerate(&self, pci_list: &BTreeMap) -> BTreeMap { 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() @@ -52,9 +53,25 @@ 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 } + /* + 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() { @@ -62,116 +79,249 @@ 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, 15); - // 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 { - GpuVendor::Nvidia => nvidia_get_device_model(device.pci_address()), - 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() - }); - - if let Some(driver) = device.driver() - && driver.contains("vfio-") - { - info!("Device: {} is bound to: {}", device_name, driver); - return Ok(GpuDevice::new( - device_name, - device.clone(), - u32::MAX, - u32::MAX, - None, - gpu_vendor, - None, - false, - true, - false, - false, - )); - } - - let nvidia_minor = match gpu_vendor { - GpuVendor::Nvidia => nvidia_get_minor(device.pci_address()), - _ => None, - }; + // 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) { + let gpu_type = self.vulkan.get_gpu_type(pci_id); + let gpu_name = self.vulkan.get_gpu_name(pci_id); - // 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: {}", device_name, err); - (u32::MAX, u32::MAX, false) + if let (Some(card), Some(render)) = ( + self.vulkan.get_gpu_card(pci_id), + self.vulkan.get_gpu_render(pci_id), + ) { + 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); } - }; + } - // 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 + // else, fallback to sysfs GPU building + + // For now only support the popular GPU vendors, more can be added inthe future + match gpu_vendor { + // 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 + */ + // 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); + // 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(|| { + device + .device_name() + .clone() + .unwrap_or_else(|| "Unknown Device".to_string()) + }); + + // return a working GPU if drm available, else return a non-available GPU + // 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 we support (eg. nova), will be marked as not available + error!( + "{}: driver {:?} is not supported by Cardwire, please request it on Github", + device.pci_address(), + device.driver() + ); + } + } + GpuVendor::Intel => { + // I think i915 and Xe should work the same + let gpu_name = device + .device_name() + .clone() + .unwrap_or_else(|| "Unknown Device".to_string()); + 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(), + render, + card, + None, + gpu_vendor, + None, + gpu_type, + ); + info!("{}: Used AMDGPU to build", gpu_device.name()); + debug!("{:?}", gpu_device); + return Ok(gpu_device); + } + // 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 it on Github", + device.pci_address(), + device.driver() + ); + } + } + // Just set the type to Virtual + GpuVendor::Virtio => { + let gpu_name = device + .device_name() + .clone() + .unwrap_or_else(|| "Unknown Device".to_string()); + 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 + // 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()); + 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( - device_name, + gpu_name, device.clone(), - render, - card, + u32::MAX, + u32::MAX, None, gpu_vendor, - nvidia_minor, - discrete, - false, - available, - self.is_virtual_gpu(device), + None, + GpuType::Unavailable, )) } - fn is_discrete_vulkan(&self, pci_id: &str) -> bool { - 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; - } - - device - .vendor_id() - .as_deref() - .is_some_and(|id| id == VIRTIO_VENDOR_ID) - } } 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 92% rename from crates/cardwire-daemon/src/core/gpu/default_gpu.rs rename to crates/cardwire-daemon/src/core/gpu/generic/default_gpu.rs index a935f70f..ea1317fb 100644 --- a/crates/cardwire-daemon/src/core/gpu/default_gpu.rs +++ b/crates/cardwire-daemon/src/core/gpu/generic/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/generic/display.rs b/crates/cardwire-daemon/src/core/gpu/generic/display.rs new file mode 100644 index 00000000..23326492 --- /dev/null +++ b/crates/cardwire-daemon/src/core/gpu/generic/display.rs @@ -0,0 +1,79 @@ +//! DRM display connector detection and node resolution. + +use std::{fs, io}; + +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. +#[allow(dead_code)] +pub fn external_display_connected(card: u32) -> io::Result { + let card_prefix = format!("card{card}-"); + let mut status_error = None; + + for entry in fs::read_dir("/sys/class/drm")? { + let entry = entry?; + let name = entry.file_name(); + let name = name.to_string_lossy(); + let Some(connector) = name.strip_prefix(&card_prefix) else { + continue; + }; + if connector.is_empty() + || NON_PHYSICAL + .iter() + .any(|prefix| connector.starts_with(prefix)) + || INTERNAL_PANELS + .iter() + .any(|prefix| connector.starts_with(prefix)) + { + continue; + } + + match fs::read_to_string(entry.path().join("status")) { + Ok(status) if status.trim() == "connected" => return Ok(true), + Ok(_) => {} + Err(err) => { + status_error.get_or_insert(err); + } + } + } + + match status_error { + Some(err) => Err(err), + None => Ok(false), + } +} + +/// Check whether the given DRM card currently has any connected display. +/// +/// Reads `/sys/class/drm/card{card}-*/status` +pub async fn is_gpu_active(card: u32) -> Option { + let prefix = format!("card{card}-"); + let mut entries = tokio::fs::read_dir("/sys/class/drm").await.ok()?; + let mut status_error = None; + while let Some(entry) = entries.next_entry().await.ok()? { + let name = entry.file_name(); + let name = name.to_string_lossy(); + let Some(connector) = name.strip_prefix(&prefix) else { + continue; + }; + if connector.is_empty() + || NON_PHYSICAL + .iter() + .any(|prefix| connector.starts_with(prefix)) + { + continue; + } + match tokio::fs::read_to_string(entry.path().join("status")).await { + Ok(status) if status.trim() == "connected" => return Some(true), + Ok(_) => {} + Err(err) => { + status_error.get_or_insert(err); + } + } + } + match status_error { + Some(_) => None, + None => Some(false), + } +} diff --git a/crates/cardwire-daemon/src/core/gpu/egl.rs b/crates/cardwire-daemon/src/core/gpu/generic/egl.rs similarity index 98% rename from crates/cardwire-daemon/src/core/gpu/egl.rs rename to crates/cardwire-daemon/src/core/gpu/generic/egl.rs index 8ebf6e4a..51231f4e 100644 --- a/crates/cardwire-daemon/src/core/gpu/egl.rs +++ b/crates/cardwire-daemon/src/core/gpu/generic/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/generic/mod.rs b/crates/cardwire-daemon/src/core/gpu/generic/mod.rs new file mode 100644 index 00000000..040ff0ef --- /dev/null +++ b/crates/cardwire-daemon/src/core/gpu/generic/mod.rs @@ -0,0 +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..e515f584 --- /dev/null +++ b/crates/cardwire-daemon/src/core/gpu/generic/udev.rs @@ -0,0 +1,57 @@ +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 { + warn!( + "[{}/{}] waiting for {} DRM subsystem to init...", + attempt, retries, pci_id + ); + if drm.read_dir().is_ok_and(|mut d| d.next().is_some()) { + info!("{} DRM subsystem is ready", pci_id); + return true; + } + 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()?; + + let mut render: Option = None; + let mut card: Option = None; + + 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::() + { + card = Some(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::() + { + render = Some(minor_int); + continue; + } + } + } + + Some((card?, render?)) +} diff --git a/crates/cardwire-daemon/src/core/gpu/generic/vulkan.rs b/crates/cardwire-daemon/src/core/gpu/generic/vulkan.rs new file mode 100644 index 00000000..19a10197 --- /dev/null +++ b/crates/cardwire-daemon/src/core/gpu/generic/vulkan.rs @@ -0,0 +1,134 @@ +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 { + self.vlk_physical_devices + .as_ref() + .is_some_and(|map| map.contains_key(pci_id)) + } + + /// 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::DiscreteGpu => GpuType::Discrete, + PhysicalDeviceType::IntegratedGpu => GpuType::Integrated, + PhysicalDeviceType::VirtualGpu => GpuType::Virtual, + PhysicalDeviceType::Other | PhysicalDeviceType::Cpu => 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.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 { + 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/mod.rs b/crates/cardwire-daemon/src/core/gpu/mod.rs index 7b9964d0..161bd4d4 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; -mod vulkan; +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 models::{DbusGpuDevice, GpuDevice, GpuVendor, PowerState}; -pub use nvidia::{start_nvidia_powerd, stop_nvidia_powerd}; +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, GpuType, GpuVendor, PowerState}; +pub use vendor_specific::nvidia::{start_nvidia_powerd, stop_nvidia_powerd}; diff --git a/crates/cardwire-daemon/src/core/gpu/models.rs b/crates/cardwire-daemon/src/core/gpu/models.rs index 7b9adf63..a6902599 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)] @@ -52,6 +54,7 @@ pub enum GpuVendor { Amd, Nvidia, Intel, + Virtio, #[default] Other, } @@ -64,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, } @@ -76,12 +80,28 @@ 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"), } } } -#[derive(Clone, serde::Serialize, serde::Deserialize, zbus::zvariant::Type, PartialEq)] +#[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, + External = 6, +} + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, zbus::zvariant::Type, PartialEq)] pub struct GpuDevice { name: String, pub pci: PciDevice, @@ -90,10 +110,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 +143,28 @@ impl GpuDevice { &self.nvidia_minor } + pub fn device_type(&self) -> &GpuType { + &self.device_type + } + pub fn is_discrete(&self) -> bool { - self.discrete + self.device_type == GpuType::Discrete } - pub fn set_discrete(&mut self, discrete: bool) { - self.discrete = discrete; + pub fn is_integrated(&self) -> bool { + self.device_type == GpuType::Integrated } - pub fn is_available(&self) -> bool { - self.available + pub fn is_virtual(&self) -> bool { + self.device_type == GpuType::Virtual } - pub fn _vfio(&self) -> bool { - self.vfio + pub fn is_available(&self) -> bool { + self.device_type != GpuType::Unavailable } - /// 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 + pub fn set_type(&mut self, gpu_type: GpuType) { + self.device_type = gpu_type } #[allow(clippy::too_many_arguments)] @@ -156,10 +176,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 +186,7 @@ impl GpuDevice { default, gpu_vendor, nvidia_minor, - discrete, - vfio, - available, - virtual_gpu, + device_type, } } @@ -188,12 +202,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, } @@ -205,12 +216,9 @@ 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, nvidia_minor: if let Some(minor) = gpu.nvidia_minor() { minor.to_string() } else { @@ -329,10 +337,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 +359,7 @@ mod tests { Some(true), GpuVendor::Amd, None, - true, - false, - true, - false, + GpuType::Other, ); assert!(gpu.is_default()); } @@ -372,10 +374,7 @@ mod tests { Some(false), GpuVendor::Amd, None, - true, - false, - true, - false, + GpuType::Other, ); assert!(!gpu.is_default()); } @@ -390,10 +389,7 @@ mod tests { None, GpuVendor::Amd, None, - false, - false, - true, - false, + GpuType::Other, ); assert!(!gpu.is_default()); assert!(!gpu.is_discrete()); @@ -409,10 +405,7 @@ mod tests { None, GpuVendor::Amd, None, - true, - false, - true, - false, + GpuType::Other, ); assert!(!gpu.is_default()); gpu.set_default(Some(true)); @@ -429,10 +422,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)); diff --git a/crates/cardwire-daemon/src/core/gpu/nvidia.rs b/crates/cardwire-daemon/src/core/gpu/nvidia.rs deleted file mode 100644 index d1305a1d..00000000 --- a/crates/cardwire-daemon/src/core/gpu/nvidia.rs +++ /dev/null @@ -1,96 +0,0 @@ -use std::time::Duration; - -use log::{error, info, warn}; -use tokio::{process::Command, time::timeout}; - -const SERVICE: &str = "nvidia-powerd.service"; - -/// run a systemctl command against the nvidia-powerd service and log the result -async fn run_systemctl(action: &str, extra_args: &[&str]) { - let output_cmd = Command::new("systemctl") - .arg(action) - .arg(SERVICE) - .args(extra_args) - .kill_on_drop(true) - .output(); - - let output = match timeout(Duration::from_secs(10), output_cmd).await { - Ok(Ok(output)) => output, - Ok(Err(err)) => { - error!("error while trying to {action} nvidia-powerd: {err}"); - return; - } - Err(_) => { - error!("timed out after 10s while trying to {action} nvidia-powerd"); - return; - } - }; - - let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); - if output.status.success() { - info!("successfully sent {action} on nvidia-powerd.service"); - } else { - let code = output.status.code(); - let detail = match code { - Some(code) => { - if stderr.is_empty() { - format!("systemctl exited with code {code}") - } else { - format!("systemctl exited with code {code}: {stderr}") - } - } - None => { - if stderr.is_empty() { - "systemctl was terminated".to_string() - } else { - format!("systemctl was terminated: {stderr}") - } - } - }; - warn!("error while trying to {action} nvidia-powerd: {detail}"); - } -} - -/// stop the nvidia-powerd service using systemctl -pub async fn stop_nvidia_powerd() { - if nvidia_powerd_enabled().await { - run_systemctl("stop", &[]).await; - } -} - -/// start the nvidia-powerd service using systemctl, resetting its failed state first -pub async fn start_nvidia_powerd() { - if nvidia_powerd_enabled().await { - run_systemctl("reset-failed", &[]).await; - run_systemctl("start", &[]).await; - } -} - -/// check whether the nvidia-powerd service is enabled -async fn nvidia_powerd_enabled() -> bool { - let output = match timeout( - Duration::from_secs(10), - Command::new("systemctl") - .arg("is-enabled") - .arg(SERVICE) - .kill_on_drop(true) - .output(), - ) - .await - { - Ok(Ok(output)) => output, - Ok(Err(err)) => { - error!("error while trying to detect nvidia-powerd: {err}"); - return false; - } - Err(_) => { - error!("timed out after 10s while trying to detect nvidia-powerd"); - return false; - } - }; - if let Ok(output_str) = str::from_utf8(&output.stdout) { - output_str.contains("enabled") - } else { - false - } -} 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..744c7b08 --- /dev/null +++ b/crates/cardwire-daemon/src/core/gpu/vendor_specific/amd.rs @@ -0,0 +1,47 @@ +use libdrm_amdgpu_sys::{ + AMDGPU::{DeviceHandle, GPU_INFO, amdgpu_gpu_info}, LibDrmAmdgpu +}; + +use crate::{ + Result, core::{errors::CardwireError::CardwireAmdGpuError, gpu::models::GpuType} +}; + +pub struct AmdGpuDev { + #[allow(unused)] + amdgpu_dev: DeviceHandle, + amdgpu_gpu_info: amdgpu_gpu_info, +} +impl AmdGpuDev { + pub fn new(render: u32) -> Result { + 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)?; + + libdrm_amdgpu + .init_device_handle_with_fd(f) + .map_err(CardwireAmdGpuError)? + }; + 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 fusion = self.amdgpu_gpu_info.ids_flags & AMDGPU_IDS_FLAGS_FUSION; + if fusion == 0 { + GpuType::Discrete + } else { + GpuType::Integrated + } + } + + pub fn amd_get_device_name(&self) -> String { + self.amdgpu_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 new file mode 100644 index 00000000..29209870 --- /dev/null +++ b/crates/cardwire-daemon/src/core/gpu/vendor_specific/intel.rs @@ -0,0 +1,12 @@ +use crate::core::gpu::models::GpuType; + +/// Get the gpu type for an intel GPU +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 { + GpuType::Discrete + } +} diff --git a/crates/cardwire-daemon/src/core/gpu/vendor_specific/mod.rs b/crates/cardwire-daemon/src/core/gpu/vendor_specific/mod.rs new file mode 100644 index 00000000..c5d0ef16 --- /dev/null +++ b/crates/cardwire-daemon/src/core/gpu/vendor_specific/mod.rs @@ -0,0 +1,4 @@ +pub mod amd; +pub mod intel; +pub mod nvidia; +pub mod virtio; 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 diff --git a/crates/cardwire-daemon/src/core/gpu/vendor_specific/nvidia.rs b/crates/cardwire-daemon/src/core/gpu/vendor_specific/nvidia.rs new file mode 100644 index 00000000..907b2964 --- /dev/null +++ b/crates/cardwire-daemon/src/core/gpu/vendor_specific/nvidia.rs @@ -0,0 +1,252 @@ +use crate::core::gpu::models::GpuType; + +use std::{fs, path::Path, thread, time::Duration}; + +use log::{debug, error, info, warn}; +use nvml_wrapper::{ + Nvml, enum_wrappers::device::{Brand, GpuVirtualizationMode}, enums::device::DeviceArchitecture, error::NvmlError +}; +use tokio::{process::Command, time::timeout}; + +/// Get nvidia minor id +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"); + 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 +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"); + 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, + } +} + +/// 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") | name.contains("GTX") { + GpuType::Discrete + } else { + GpuType::Unknown + } +} + +const SERVICE: &str = "nvidia-powerd.service"; + +/// run a systemctl command against the nvidia-powerd service and log the result +async fn run_systemctl(action: &str, extra_args: &[&str]) { + let output_cmd = Command::new("systemctl") + .arg(action) + .arg(SERVICE) + .args(extra_args) + .kill_on_drop(true) + .output(); + + let output = match timeout(Duration::from_secs(10), output_cmd).await { + Ok(Ok(output)) => output, + Ok(Err(err)) => { + error!("error while trying to {action} nvidia-powerd: {err}"); + return; + } + Err(_) => { + error!("timed out after 10s while trying to {action} nvidia-powerd"); + return; + } + }; + + let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); + if output.status.success() { + info!("successfully sent {action} on nvidia-powerd.service"); + } else { + let code = output.status.code(); + let detail = match code { + Some(code) => { + if stderr.is_empty() { + format!("systemctl exited with code {code}") + } else { + format!("systemctl exited with code {code}: {stderr}") + } + } + None => { + if stderr.is_empty() { + "systemctl was terminated".to_string() + } else { + format!("systemctl was terminated: {stderr}") + } + } + }; + warn!("error while trying to {action} nvidia-powerd: {detail}"); + } +} + +/// stop the nvidia-powerd service using systemctl +pub async fn stop_nvidia_powerd() { + if nvidia_powerd_enabled().await { + run_systemctl("stop", &[]).await; + } +} + +/// start the nvidia-powerd service using systemctl, resetting its failed state first +pub async fn start_nvidia_powerd() { + if nvidia_powerd_enabled().await { + run_systemctl("reset-failed", &[]).await; + run_systemctl("start", &[]).await; + } +} + +/// check whether the nvidia-powerd service is enabled +async fn nvidia_powerd_enabled() -> bool { + let output = match timeout( + Duration::from_secs(10), + Command::new("systemctl") + .arg("is-enabled") + .arg(SERVICE) + .kill_on_drop(true) + .output(), + ) + .await + { + Ok(Ok(output)) => output, + Ok(Err(err)) => { + error!("error while trying to detect nvidia-powerd: {err}"); + return false; + } + Err(_) => { + error!("timed out after 10s while trying to detect nvidia-powerd"); + return false; + } + }; + if let Ok(output_str) = str::from_utf8(&output.stdout) { + output_str.contains("enabled") + } else { + 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() { + debug!("[{}]: nvml virt_mode: {:?}", pci_id, virt_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, + ) => { + debug!("[{}]: nvml arch: {:?}", pci_id, nvidia_dev.architecture()); + 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)) => { + 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 + | 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; + } + } + } + } + debug!("[{}]: nvml fallback", pci_id); + GpuType::Unknown +} diff --git a/crates/cardwire-daemon/src/core/gpu/vendor_specific/virtio.rs b/crates/cardwire-daemon/src/core/gpu/vendor_specific/virtio.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/crates/cardwire-daemon/src/core/gpu/vendor_specific/virtio.rs @@ -0,0 +1 @@ + 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) -} 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, 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 } diff --git a/crates/cardwire-daemon/src/interface/mode.rs b/crates/cardwire-daemon/src/interface/mode.rs index 9d382128..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); @@ -163,9 +163,9 @@ 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", + "Couldn't set mode to {}, Manual mode is only available laptops with a offload discrete GPU", mode ); error!("{}", error_message); @@ -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 } } 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) 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"; diff --git a/nix/default.nix b/nix/default.nix index 3a243c07..2d8d5674 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -13,35 +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 - ]; - doCheck = false; doInstallCheck = true; @@ -64,9 +49,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 = { @@ -76,39 +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 - ] - } + --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) ''; } diff --git a/nix/nixos-module.nix b/nix/nixos-module.nix index f9d103ec..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 = [ "systemd-udev-settle.service" ]; - After = [ - "dbus.service" - "systemd-udev-settle.service" + Before = [ + "graphical.target" + "display-manager.service" ]; }; serviceConfig = { 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 = { 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")