From e40b0d806c09c9d4bcd4c448ae0d32a20afa16ce Mon Sep 17 00:00:00 2001 From: luytan Date: Sun, 9 Aug 2026 11:38:53 +0200 Subject: [PATCH 1/7] feat(cardwire-ebpf): send event on manual mode and make force_gpu works --- crates/cardwire-ebpf/src/helpers.rs | 33 +++++++++++++++++++++++++++-- crates/cardwire-ebpf/src/main.rs | 14 ++++++------ 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/crates/cardwire-ebpf/src/helpers.rs b/crates/cardwire-ebpf/src/helpers.rs index 15106f3b..d18ef9e9 100644 --- a/crates/cardwire-ebpf/src/helpers.rs +++ b/crates/cardwire-ebpf/src/helpers.rs @@ -51,8 +51,31 @@ pub unsafe fn is_inode_blocked(inode: u64) -> bool { let comm = bpf_get_current_comm().unwrap_or([0u8; 16]); - if *mode == INTEGRATED || *mode == MANUAL { - // if integrated/manual, just report the event and block + if *mode == INTEGRATED { + // if integrated, just report the event and block + report_event(pid, ino_gpu_id, comm); + return true; + } + + if *mode == MANUAL { + let ppid = get_task_ppid().unwrap_or(u32::MAX); + + // Check if the PID or PPID is in the forced map + let forced_gpu_id = + unsafe { CW_FORCED_PID.get(pid).or_else(|| CW_FORCED_PID.get(ppid)) }; + + if let Some(pid_gpu_id) = forced_gpu_id { + // If forced GPU ID matches the inode's GPU ID, allow access + match *pid_gpu_id == ino_gpu_id { + true => break 'end, + false => { + report_event(pid, ino_gpu_id, comm); + return true; + } + } + } + + // Default manual mode behavior: block access to blocked inodes report_event(pid, ino_gpu_id, comm); return true; } @@ -178,6 +201,12 @@ pub unsafe fn is_smart() -> Option { CW_MODE.get(MODE_INDEX).map(|mode| *mode == SMART) } +/// Verify if the current device mode is manual, returns None if the map fails +#[inline(always)] +pub unsafe fn is_manual() -> Option { + CW_MODE.get(MODE_INDEX).map(|mode| *mode == MANUAL) +} + #[inline(always)] pub unsafe fn is_nvidia_setting_enabled() -> bool { match unsafe { CW_SETTINGS.get(CardwiredSetting::EXP_NVIDIA) } { diff --git a/crates/cardwire-ebpf/src/main.rs b/crates/cardwire-ebpf/src/main.rs index c590a4ea..be7623ab 100644 --- a/crates/cardwire-ebpf/src/main.rs +++ b/crates/cardwire-ebpf/src/main.rs @@ -7,7 +7,9 @@ use aya_ebpf::{ use aya_log_ebpf::{error, warn}; use crate::{ - helpers::{is_cardwired, is_comm_whitelisted, is_hybrid, is_inode_blocked, is_smart}, maps::{CW_ALLOWED_PID, CW_DIRENT, CW_EXEC_EVENTS, CW_FORCED_PID, ExecEvent}, vmlinux::{dentry, file, inode, linux_dirent64, path} + helpers::{ + is_cardwired, is_comm_whitelisted, is_hybrid, is_inode_blocked, is_manual, is_smart + }, maps::{CW_ALLOWED_PID, CW_DIRENT, CW_EXEC_EVENTS, CW_FORCED_PID, ExecEvent}, vmlinux::{dentry, file, inode, linux_dirent64, path} }; #[allow( @@ -449,11 +451,11 @@ unsafe fn try_tracepoint_sched_process_exec(ctx: TracePointContext) -> Result ring_buf, From 7e59ca90310e531a0ce186f0f0b7ee2eebe7864b Mon Sep 17 00:00:00 2001 From: luytan Date: Sun, 9 Aug 2026 11:59:02 +0200 Subject: [PATCH 2/7] fix(cardwired): skip lookup_name if manual mode --- crates/cardwire-daemon/src/analyzer/models.rs | 12 ++++++++++-- crates/cardwire-ebpf/src/main.rs | 5 ++++- crates/cardwire-ebpf/src/maps.rs | 1 + 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/crates/cardwire-daemon/src/analyzer/models.rs b/crates/cardwire-daemon/src/analyzer/models.rs index 51051205..f8e9a6ec 100644 --- a/crates/cardwire-daemon/src/analyzer/models.rs +++ b/crates/cardwire-daemon/src/analyzer/models.rs @@ -21,6 +21,7 @@ use crate::{ #[derive(Debug, Copy, Clone)] pub struct ExecEvent { pub pid: u32, + pub mode: u8, } #[repr(C)] @@ -176,7 +177,9 @@ impl CardwireAnalyzer { Some(name) => name, None => return, }; - if let Some(result) = self.evaluate_app(event.pid, &real_app_name).await + if let Some(result) = self + .evaluate_app(event.pid, &real_app_name, event.mode) + .await && result.0 { match result.1 { @@ -286,7 +289,7 @@ impl CardwireAnalyzer { /// Default app are blocked, try to find if it's a game or a gpu intensive app, the u8 is the /// gpu id - async fn evaluate_app(&self, pid: u32, comm: &str) -> Option<(bool, PidType, u32)> { + async fn evaluate_app(&self, pid: u32, comm: &str, mode: u8) -> Option<(bool, PidType, u32)> { let path = format!("/proc/{}/environ", pid); let environ = match fs::read(path) { Ok(content) => content, @@ -303,6 +306,11 @@ impl CardwireAnalyzer { return Some((true, PidType::Forced, value)); } + // If manual mode, do not process app discovery or database policies + if mode == 2 { + return None; + } + // Check the database now, we can take our time since if we reached it, the app would've // been blocked let mut lookup_name = comm.to_lowercase(); diff --git a/crates/cardwire-ebpf/src/main.rs b/crates/cardwire-ebpf/src/main.rs index be7623ab..f4cea0f5 100644 --- a/crates/cardwire-ebpf/src/main.rs +++ b/crates/cardwire-ebpf/src/main.rs @@ -466,8 +466,11 @@ unsafe fn try_tracepoint_sched_process_exec(ctx: TracePointContext) -> Result = HashMap::::with_max_entries( #[allow(dead_code)] pub struct ExecEvent { pub pid: u32, + pub mode: u8, } #[btf_map] From b8049d6631d2bc1749c4bb8990a3656097dcce0b Mon Sep 17 00:00:00 2001 From: luytan Date: Sun, 9 Aug 2026 12:00:59 +0200 Subject: [PATCH 3/7] fix(cardwired): update tests --- crates/cardwire-daemon/src/analyzer/models.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/cardwire-daemon/src/analyzer/models.rs b/crates/cardwire-daemon/src/analyzer/models.rs index f8e9a6ec..1433a4f2 100644 --- a/crates/cardwire-daemon/src/analyzer/models.rs +++ b/crates/cardwire-daemon/src/analyzer/models.rs @@ -476,24 +476,29 @@ mod tests { fn test_event_deserialization_from_valid_bytes() { let item: Vec = vec![ 0x01, 0x00, 0x00, 0x00, // pid = 1 + 0x03, 0x00, 0x00, 0x00, // mode = 3 (Smart) ]; assert!(item.len() >= std::mem::size_of::()); let event = unsafe { ptr::read_unaligned(item.as_ptr() as *const ExecEvent) }; assert_eq!(event.pid, 1); + assert_eq!(event.mode, 3); } #[test] fn test_event_deserialization_rejects_undersized_buffer() { - let item: Vec = vec![0x01, 0x00, 0x00]; // 3 bytes, Event needs 4 + let item: Vec = vec![0x01, 0x00, 0x00]; // 3 bytes, Event needs 8 assert!(item.len() < std::mem::size_of::()); } #[test] fn test_event_deserialization_with_large_pid() { // pid = 0xFFFFFFFF (u32::MAX) - let item: Vec = vec![0xFF, 0xFF, 0xFF, 0xFF]; + let item: Vec = vec![ + 0xFF, 0xFF, 0xFF, 0xFF, 0x02, 0x00, 0x00, 0x00, // mode = 2 (Manual) + ]; let event = unsafe { ptr::read_unaligned(item.as_ptr() as *const ExecEvent) }; assert_eq!(event.pid, u32::MAX); + assert_eq!(event.mode, 2); } // ── ReportEvent ────────────────────────────────────────────────── From f14e27bffc7fe9edea666a837380aec6eb8249e0 Mon Sep 17 00:00:00 2001 From: luytan Date: Sun, 9 Aug 2026 12:44:56 +0200 Subject: [PATCH 4/7] feat: improve BLOCKED_INO tracking and make it work with manual mode --- crates/cardwire-daemon/src/analyzer/models.rs | 1 + crates/cardwire-daemon/src/interface/gpu.rs | 6 +- crates/cardwire-daemon/src/interface/mode.rs | 2 +- crates/cardwire-ebpf-userspace/src/lib.rs | 59 +++++++++++++------ crates/cardwire-ebpf/src/helpers.rs | 28 +++++---- crates/cardwire-ebpf/src/main.rs | 6 +- crates/cardwire-ebpf/src/maps.rs | 16 ++++- 7 files changed, 79 insertions(+), 39 deletions(-) diff --git a/crates/cardwire-daemon/src/analyzer/models.rs b/crates/cardwire-daemon/src/analyzer/models.rs index 1433a4f2..b9292c4f 100644 --- a/crates/cardwire-daemon/src/analyzer/models.rs +++ b/crates/cardwire-daemon/src/analyzer/models.rs @@ -22,6 +22,7 @@ use crate::{ pub struct ExecEvent { pub pid: u32, pub mode: u8, + pub _padding: [u8; 3], } #[repr(C)] diff --git a/crates/cardwire-daemon/src/interface/gpu.rs b/crates/cardwire-daemon/src/interface/gpu.rs index bd58701e..63ad34fa 100644 --- a/crates/cardwire-daemon/src/interface/gpu.rs +++ b/crates/cardwire-daemon/src/interface/gpu.rs @@ -131,7 +131,7 @@ impl GpuInterface { let mut blocker = self.blocker.write().await; for inode in inodes.iter() { - blocker.unblock_inode(*inode).into_fdo()?; + blocker.unblock_inode(*inode, self.id).into_fdo()?; } Ok(()) } @@ -251,10 +251,6 @@ impl GpuInterface { #[zbus(property)] pub async fn block(&self) -> fdo::Result { - let mode = self.mode_state.read().await.mode(); - if mode == Modes::Smart && (self.device.is_default() && !self.device.is_discrete()) { - 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 df0e3d47..18ba0abd 100644 --- a/crates/cardwire-daemon/src/interface/mode.rs +++ b/crates/cardwire-daemon/src/interface/mode.rs @@ -122,7 +122,7 @@ impl ModeInterface { && !gpu.device.is_discrete() { // push default gpu (iGPU) into the blocked inode map for tracking only - gpu.block_gpu(*id as u32).await?; + gpu.unblock_gpu().await?; } } } diff --git a/crates/cardwire-ebpf-userspace/src/lib.rs b/crates/cardwire-ebpf-userspace/src/lib.rs index d7eb6b99..031f2ab6 100644 --- a/crates/cardwire-ebpf-userspace/src/lib.rs +++ b/crates/cardwire-ebpf-userspace/src/lib.rs @@ -23,6 +23,15 @@ pub struct EbpfBlocker { pub forced_map: Arc>>, } +#[repr(C)] +#[derive(Copy, Clone)] +pub struct InodeState { + pub gpu_id: u32, + pub blocked: u8, + pub _padding: [u8; 3], // 8-byte alignment +} +unsafe impl aya::Pod for InodeState {} + impl EbpfBlocker { pub fn new() -> CardwireEbpfResult { // quit if bpf is not enabled @@ -173,47 +182,59 @@ impl EbpfBlocker { } /// Block an inode, value is the associated GPU id - pub fn block_inode(&mut self, inode: u64, value: u32) -> CardwireEbpfResult<()> { - // Also insert hardcoded values for now - let mut inode_map: HashMap<_, u64, u32> = HashMap::try_from( + pub fn block_inode(&mut self, inode: u64, gpu_id: u32) -> CardwireEbpfResult<()> { + let mut inode_map: HashMap<_, u64, InodeState> = HashMap::try_from( self.ebpf .map_mut("CW_BLOCKED_INO") .ok_or_else(|| CardwireEbpfError::missing_map("CW_BLOCKED_INO"))?, ) .map_err(CardwireEbpfError::aya)?; inode_map - .insert(inode, value, 0) + .insert( + inode, + InodeState { + gpu_id, + blocked: 1, + _padding: [0; 3], + }, + 0, + ) .map_err(CardwireEbpfError::aya)?; Ok(()) } - pub fn unblock_inode(&mut self, inode: u64) -> CardwireEbpfResult<()> { - // Also insert hardcoded values for now - let mut inode_map: HashMap<_, u64, u32> = HashMap::try_from( + + pub fn unblock_inode(&mut self, inode: u64, gpu_id: u32) -> CardwireEbpfResult<()> { + let mut inode_map: HashMap<_, u64, InodeState> = HashMap::try_from( self.ebpf .map_mut("CW_BLOCKED_INO") .ok_or_else(|| CardwireEbpfError::missing_map("CW_BLOCKED_INO"))?, ) .map_err(CardwireEbpfError::aya)?; - match inode_map.get(&inode, 0) { - // Ok = key found, remove - Ok(_) => inode_map.remove(&inode).map_err(CardwireEbpfError::aya), - // key not found, skip - Err(MapError::KeyNotFound) => Ok(()), - Err(err) => Err(CardwireEbpfError::aya(err)), - } + // Keep the inode in the map for tracking, but set blocked to 0 + inode_map + .insert( + inode, + InodeState { + gpu_id, + blocked: 0, + _padding: [0; 3], + }, + 0, + ) + .map_err(CardwireEbpfError::aya)?; + Ok(()) } - pub fn is_inode_blocked(&self, inode: u64, value: u32) -> CardwireEbpfResult { - // Also insert hardcoded values for now - let inode_map: HashMap<_, u64, u32> = HashMap::try_from( + pub fn is_inode_blocked(&self, inode: u64, gpu_id: u32) -> CardwireEbpfResult { + let inode_map: HashMap<_, u64, InodeState> = HashMap::try_from( self.ebpf .map("CW_BLOCKED_INO") .ok_or_else(|| CardwireEbpfError::missing_map("CW_BLOCKED_INO"))?, ) .map_err(CardwireEbpfError::aya)?; + match inode_map.get(&inode, 0) { - // if value (gpu key associed to inode) = our func value - Ok(map_value) => Ok(value == map_value), + Ok(state) => Ok(state.gpu_id == gpu_id && state.blocked == 1), Err(MapError::KeyNotFound) => Ok(false), Err(err) => Err(CardwireEbpfError::aya(err)), } diff --git a/crates/cardwire-ebpf/src/helpers.rs b/crates/cardwire-ebpf/src/helpers.rs index d18ef9e9..ffcb61ef 100644 --- a/crates/cardwire-ebpf/src/helpers.rs +++ b/crates/cardwire-ebpf/src/helpers.rs @@ -13,28 +13,32 @@ use crate::vmlinux::task_struct; /// Verify if the inode is inside CW_BLOCKED_INO or not #[inline(always)] pub unsafe fn is_inode_blocked(inode: u64) -> bool { - let mut blocked: bool = false; + let mut tracked: bool = false; let mut ino_gpu_id: u32 = 0; + let mut blocked: bool = false; 'inode_check: { // Check if the inode is in the blocked list if let Some(v) = unsafe { CW_BLOCKED_INO.get(inode) } { - blocked = true; - ino_gpu_id = *v; + tracked = true; + ino_gpu_id = v.gpu_id; + blocked = v.blocked == 1; break 'inode_check; } // We didn't match any inode, try with nvidia inodes if unsafe { is_nvidia_setting_enabled() } && let Some(v) = unsafe { CW_EXP_BLK_INO.get(inode) } { - blocked = true; + tracked = true; ino_gpu_id = *v; + // Nvidia experimental inodes are considered globally blocked for now if in map + blocked = true; break 'inode_check; } } 'end: { - if !blocked { + if !tracked { // exit and return success break 'end; } @@ -51,8 +55,8 @@ pub unsafe fn is_inode_blocked(inode: u64) -> bool { let comm = bpf_get_current_comm().unwrap_or([0u8; 16]); - if *mode == INTEGRATED { - // if integrated, just report the event and block + if *mode == INTEGRATED && blocked { + // if integrated, block and report report_event(pid, ino_gpu_id, comm); return true; } @@ -75,9 +79,13 @@ pub unsafe fn is_inode_blocked(inode: u64) -> bool { } } - // Default manual mode behavior: block access to blocked inodes - report_event(pid, ino_gpu_id, comm); - return true; + // Normal process behavior: block access if its blocked + if blocked { + report_event(pid, ino_gpu_id, comm); + return true; + } else { + break 'end; + } } // 0 = iGPU diff --git a/crates/cardwire-ebpf/src/main.rs b/crates/cardwire-ebpf/src/main.rs index f4cea0f5..8a948f87 100644 --- a/crates/cardwire-ebpf/src/main.rs +++ b/crates/cardwire-ebpf/src/main.rs @@ -470,7 +470,11 @@ unsafe fn try_tracepoint_sched_process_exec(ctx: TracePointContext) -> Result = Array::::with_max_entries(1, 0); #[map] pub static CW_SETTINGS: HashMap = HashMap::::with_max_entries(255, 0); +#[repr(C, align(8))] +#[derive(Copy, Clone)] +pub struct InodeState { + pub gpu_id: u32, + pub blocked: u8, + pub _padding: [u8; 3], // 8-byte alignment +} + /* Map used to store blocked inodes sent from userspace Key = Inode - Value = associated GPU + Value = associated GPU and block state */ #[map] -pub static CW_BLOCKED_INO: HashMap = HashMap::::with_max_entries(4096, 0); +pub static CW_BLOCKED_INO: HashMap = + HashMap::::with_max_entries(4096, 0); /* Map used to store blocked inodes from exp_nvidia @@ -71,11 +80,12 @@ pub static CW_ALLOWED_COMM: HashMap<[u8; 16], u8> = #[map] pub static CW_DIRENT: HashMap = HashMap::::with_max_entries(1024, 0); -#[repr(align(8))] +#[repr(C, align(8))] #[allow(dead_code)] pub struct ExecEvent { pub pid: u32, pub mode: u8, + pub _padding: [u8; 3], } #[btf_map] From f8b0812cd6451558a883949a79d063f47e424687 Mon Sep 17 00:00:00 2001 From: luytan Date: Sun, 9 Aug 2026 12:47:14 +0200 Subject: [PATCH 5/7] ci: run vms in matrix and also add 3 and 15 gpu vm back --- .github/workflows/cicd.yml | 9 +++++++-- flake.nix | 3 +++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 10333cd2..4157337b 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -130,13 +130,18 @@ jobs: with: tool_name: rustfmt vm-test: + name: VM Test (${{ matrix.vm }}) runs-on: ubuntu-latest - needs: [prepare, rust-lint, rust-test, rust-format] + needs: [prepare] if: ${{ !failure() && !cancelled() && needs.prepare.outputs.run_nix_vm == 'true' }} + strategy: + fail-fast: false + matrix: + vm: [vm-ci-2gpu, vm-ci-3gpu, vm-ci-15gpu] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: determinateSystems/nix-installer-action@ef8a148080ab6020fd15196c2084a2eea5ff2d25 # v22 - - run: nix build .#vm-test + - run: nix build .#checks.x86_64-linux.${{ matrix.vm }} mdbook-test: runs-on: ubuntu-latest needs: [prepare] diff --git a/flake.nix b/flake.nix index 1b01d56e..2485c394 100644 --- a/flake.nix +++ b/flake.nix @@ -42,6 +42,9 @@ packages = forAllSystems (system: { default = (pkgs system).callPackage ./nix { toolchain = toolchainFor system; }; vm-test = self.checks.${system}.vm-ci-2gpu; + vm-test-2gpu = self.checks.${system}.vm-ci-2gpu; + vm-test-3gpu = self.checks.${system}.vm-ci-3gpu; + vm-test-15gpu = self.checks.${system}.vm-ci-15gpu; }); formatter = forAllSystems ( system: From 54f72588b90fd3ba7b298bb72bf8485d95f2cd94 Mon Sep 17 00:00:00 2001 From: luytan Date: Sun, 9 Aug 2026 13:37:16 +0200 Subject: [PATCH 6/7] ci: fix ci and bring back old vm --- .../cardwire-daemon/src/core/gpu/display.rs | 25 ++++++++-------- nix/ci-15gpu.nix | 7 +++-- nix/ci-2gpu.nix | 30 +++++++++++++++++++ nix/ci-3gpu.nix | 16 ++++++++-- 4 files changed, 60 insertions(+), 18 deletions(-) diff --git a/crates/cardwire-daemon/src/core/gpu/display.rs b/crates/cardwire-daemon/src/core/gpu/display.rs index b2bc820c..b1589dd5 100644 --- a/crates/cardwire-daemon/src/core/gpu/display.rs +++ b/crates/cardwire-daemon/src/core/gpu/display.rs @@ -4,23 +4,15 @@ 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 { - // These connector types are internal panels or do not represent a physical display output. - const NON_EXTERNAL: &[&str] = &[ - "eDP-", - "LVDS-", - "DSI-", - "DPI-", - "SPI-", - "Virtual-", - "Unknown-", - "Writeback-", - ]; 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. @@ -34,7 +26,10 @@ pub fn external_display_connected(card: u32) -> io::Result { continue; }; if connector.is_empty() - || NON_EXTERNAL + || NON_PHYSICAL + .iter() + .any(|prefix| connector.starts_with(prefix)) + || INTERNAL_PANELS .iter() .any(|prefix| connector.starts_with(prefix)) { @@ -135,7 +130,11 @@ pub async fn is_gpu_active(card: u32) -> Option { let Some(connector) = name.strip_prefix(&prefix) else { continue; }; - if connector.is_empty() { + 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 { diff --git a/nix/ci-15gpu.nix b/nix/ci-15gpu.nix index b0e30bbc..ac642384 100644 --- a/nix/ci-15gpu.nix +++ b/nix/ci-15gpu.nix @@ -70,8 +70,9 @@ with subtest("Check if cardwire found all gpus"): t.assertIn("17", machine.succeed("cardwire list | wc -l"), "Must be 17 (15 GPUs + 2 headers)") - with subtest("Try to switch to integrated and hybrid"): - t.assertIn("Couldn't set mode to Integrated, the mode requires exactly 2 GPUs", machine.fail("cardwire set integrated 2>&1"), "Mode has been switched to integrated") + with subtest("Try to switch to integrated, smart and hybrid"): + t.assertIn("Couldn't set mode to Integrated", machine.fail("cardwire set integrated 2>&1"), "Mode has been switched to integrated") + t.assertIn("Couldn't set mode to Smart", machine.fail("cardwire set smart 2>&1"), "Mode has been switched to smart") t.assertIn("Mode has been set to Hybrid", machine.succeed("cardwire set hybrid"), "Mode has been switched to hybrid") with subtest("Set to manual, and block 14 gpus"): @@ -93,7 +94,7 @@ machine.succeed(": < /dev/dri/renderD128") machine.succeed(": < /dev/dri/card0") - with subtest("Check gpu_state.json to see if two gpus got blocked"): + with subtest("Check gpu_state.json to see if 14 gpus got blocked"): t.assertIn("14", machine.succeed("cat /var/lib/cardwire/gpu_state.json|grep true|wc -l"), "Only 13 or less got blocked") diff --git a/nix/ci-2gpu.nix b/nix/ci-2gpu.nix index 97836e2b..a8ddacb1 100644 --- a/nix/ci-2gpu.nix +++ b/nix/ci-2gpu.nix @@ -72,5 +72,35 @@ with subtest("Try to block default gpu"): t.assertIn("Per GPU block is only available on manual mode", machine.fail("cardwire gpu 0 --block 2>&1"), "Default gpu got blocked") + + with subtest("Smart Mode Base Test"): + machine.succeed("cardwire set smart") + t.assertIn("smart", machine.succeed("cat /var/lib/cardwire/mode.json")) + # In Smart mode, dGPU is blocked by default + machine.fail(": < /dev/dri/renderD129") + + with subtest("Test Dynamic Analysis ENV Flags"): + # CARDWIRE_ALLOW + machine.succeed("CARDWIRE_ALLOW=1 : < /dev/dri/renderD129") + machine.fail("CARDWIRE_ALLOW=0 : < /dev/dri/renderD129") + # CARDWIRE_FORCE_DGPU + machine.succeed("CARDWIRE_FORCE_DGPU=1 : < /dev/dri/renderD129") + machine.fail("CARDWIRE_FORCE_DGPU=0 : < /dev/dri/renderD129") + + with subtest("Test cardwire launch Environment Injection for GPU 0"): + env_out = machine.succeed("cardwire launch --gpu 0 env") + t.assertIn("CARDWIRE_ALLOW=0", env_out, "Missing CARDWIRE_ALLOW=0 for iGPU default") + + with subtest("Test cardwire launch Environment Injection for GPU 1"): + env_out = machine.succeed("cardwire launch --gpu 1 env") + t.assertIn("CARDWIRE_FORCE_DGPU=1", env_out, "Missing CARDWIRE_FORCE_DGPU=1 for dGPU") + t.assertIn("DRI_PRIME=pci", env_out, "Missing DRI_PRIME for dGPU") + + with subtest("Test cardwire launch Default GPU"): + # Without --gpu, it should default to the unblocked discrete GPU (GPU 1) + env_out = machine.succeed("cardwire launch env") + t.assertIn("CARDWIRE_FORCE_DGPU=1", env_out, "Default launch didn't target dGPU (GPU 1)") + t.assertIn("DRI_PRIME=pci", env_out, "Default launch didn't set DRI_PRIME") + ''; } diff --git a/nix/ci-3gpu.nix b/nix/ci-3gpu.nix index bcd025b3..8674a38b 100644 --- a/nix/ci-3gpu.nix +++ b/nix/ci-3gpu.nix @@ -55,8 +55,9 @@ with subtest("Ensure cardwire is started and dbus works"): machine.wait_until_succeeds("su - john -c 'cardwire help'") - with subtest("Try to switch to integrated and hybrid"): - t.assertIn("Couldn't set mode to Integrated, the mode requires exactly 2 GPUs", machine.fail("cardwire set integrated 2>&1"), "Mode has been switched to integrated") + with subtest("Try to switch to integrated, smart and hybrid"): + t.assertIn("Couldn't set mode to Integrated", machine.fail("cardwire set integrated 2>&1"), "Mode has been switched to integrated") + t.assertIn("Couldn't set mode to Smart", machine.fail("cardwire set smart 2>&1"), "Mode has been switched to smart") t.assertIn("Mode has been set to Hybrid", machine.succeed("cardwire set hybrid"), "Mode has been switched to hybrid") with subtest("Set to manual, and block two gpus"): @@ -92,6 +93,17 @@ machine.fail(": < /dev/dri/renderD130") machine.fail(": < /dev/dri/card1") machine.fail(": < /dev/dri/card2") + + with subtest("Test Multi-GPU Launch and ENV Injection"): + # For GPU 1 + env_out_1 = machine.succeed("cardwire launch --gpu 1 env") + t.assertIn("CARDWIRE_FORCE_GPU=1", env_out_1, "Missing CARDWIRE_FORCE_GPU=1") + t.assertIn("DRI_PRIME=pci", env_out_1, "Missing DRI_PRIME") + + # For GPU 2 + env_out_2 = machine.succeed("cardwire launch --gpu 2 env") + t.assertIn("CARDWIRE_FORCE_GPU=2", env_out_2, "Missing CARDWIRE_FORCE_GPU=2") + t.assertIn("DRI_PRIME=pci", env_out_2, "Missing DRI_PRIME") ''; } From c4f55190335dd0a8d4417292705b1b806c108080 Mon Sep 17 00:00:00 2001 From: luytan Date: Sun, 9 Aug 2026 13:51:00 +0200 Subject: [PATCH 7/7] ci : fix cat and cred --- .github/workflows/cicd.yml | 2 ++ nix/ci-2gpu.nix | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 4157337b..7a596b3b 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -140,6 +140,8 @@ jobs: vm: [vm-ci-2gpu, vm-ci-3gpu, vm-ci-15gpu] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - uses: determinateSystems/nix-installer-action@ef8a148080ab6020fd15196c2084a2eea5ff2d25 # v22 - run: nix build .#checks.x86_64-linux.${{ matrix.vm }} mdbook-test: diff --git a/nix/ci-2gpu.nix b/nix/ci-2gpu.nix index a8ddacb1..87fea072 100644 --- a/nix/ci-2gpu.nix +++ b/nix/ci-2gpu.nix @@ -81,11 +81,11 @@ with subtest("Test Dynamic Analysis ENV Flags"): # CARDWIRE_ALLOW - machine.succeed("CARDWIRE_ALLOW=1 : < /dev/dri/renderD129") - machine.fail("CARDWIRE_ALLOW=0 : < /dev/dri/renderD129") + machine.succeed("CARDWIRE_ALLOW=1 sh -c 'sleep 0.5 && exec 3< /dev/dri/renderD129'") + machine.fail("CARDWIRE_ALLOW=0 sh -c 'sleep 0.5 && exec 3< /dev/dri/renderD129'") # CARDWIRE_FORCE_DGPU - machine.succeed("CARDWIRE_FORCE_DGPU=1 : < /dev/dri/renderD129") - machine.fail("CARDWIRE_FORCE_DGPU=0 : < /dev/dri/renderD129") + machine.succeed("CARDWIRE_FORCE_DGPU=1 sh -c 'sleep 0.5 && exec 3< /dev/dri/renderD129'") + machine.fail("CARDWIRE_FORCE_DGPU=0 sh -c 'sleep 0.5 && exec 3< /dev/dri/renderD129'") with subtest("Test cardwire launch Environment Injection for GPU 0"): env_out = machine.succeed("cardwire launch --gpu 0 env")