Smart
Goal and integration
Cardwire owns its per-application policy. It does not depend on desktop environment heuristics like PrefersNonDefaultGPU or on DRI_PRIME, __NV_PRIME_RENDER_OFFLOAD or SteamAppId being present in the app environment. Those auto-approval inputs were dropped in 0.12.0 and replaced by the internal application policy (Smart Policy), which makes cardwire self-sufficient while staying compatible with desktop environments through the Switcheroo shim.
Third parties that want to integrate with cardwire get three methods:
- Env: the
CARDWIRE_environment variables is a way to route a process to a GPU, The per-GPU environment can be fetched from theEnvproperty of the GPU API. The env will always have priority over the other methods. - PID via API:
RequestProcessAccessdirectly insert the PID in theCW_ALLOWED_PIDeBPF HashMap. call it with the pid right after spawning it, or apply it to a process that is already running (Caution, App often scan for GPUs at launch). - Smart Policy: As of 0.12.0, cardwire has its own Application Policy, the SmartPolicy interface lists known applications (
GetAppPolicies), changes their persistent policy (SetAppPolicy) and announces discoveries (NewAppAdded).
Introduction
Having an integrated and hybrid mode is good, but what if we could have the best of both worlds?
This is what cardwire’s smart mode was made for. Cardwire uses a mix of kernel-space + userspace to directly allow processes on the fly.
Kernel-Space
Using the eBPF program and the tracepoint/sched/sched_process_exec hooks, the kernel program notifies cardwired when a new process is executed, sending its pid using the CW_EXEC_EVENTS RING_BUF (in Smart and Manual modes). Once the process is received by cardwired, it will be analyzed in real-time and its pid will be inserted into the CW_ALLOWED_PID map (value always 0) or the CW_FORCED_PID map (value is the GPU id)
When a process exits, the kernel’s tracepoint/sched/sched_process_exit removes the pid from both maps directly, preventing the maps from overflowing.
Userspace
It is responsible for making the actual decisions about whether a process is allowed to use a GPU. It is divided into three main components:
CardwireAnalyzer: A dedicated background task that listens to theCW_EXEC_EVENTSring buffer (and theCW_REPORT_EVENTSring for blocked-access logging). When it receives a new PID from the kernel eBPF, it invokes the analysis helpers. If the application passes, it populates theCW_ALLOWED_PIDmap (value always0) or theCW_FORCED_PIDmap (value is the GPU id).dynamic_analysis.rs: A set of helper functions used to analyze a process in real-time. By reading/proc/<pid>/environand/proc/<pid>/cmdline, it checks for explicitly requested GPUs (likeCARDWIRE_ALLOW=1,CARDWIRE_FORCE_DGPU=1,CARDWIRE_FORCE_GPU=<gpu_id>) or Steam games (SteamAppId), more to be added.static_analysis.rs: A set of helper functions that analyze system data when the daemon starts. It scans the XDG data directories and watches them with inotify so new apps are picked up at install time. Every discovered app is blocked by default until the user allows it (will be changed in 0.13.0, with a toggleable setting).
Notes
Technically, it’s a pure race condition between the cardwire analyzer and the process, cardwire scans and allow a process in ~60-100 microseconds, from my testing, no process initialized its render before cardwire allowed it
Complete Execution Flow
Here is a comprehensive breakdown of how the Kernel and Userspace interact in real-time when an application launches: (Please zoom on it)
sequenceDiagram
participant Proc as Process
participant Kernel as eBPF Kernel Hooks
participant Map as BPF Maps
participant Daemon as CardwireAnalyzer (Userspace)
Note over Proc,Daemon: 1. Process Launch
Proc->>Kernel: sched_process_exec
Kernel->>Map: Send PID via cw_exec_events (RingBuf)
Map->Daemon: Listen to cw_exec_events and wait for new events
Note over Daemon: 2. Real-time Analysis
Daemon->>Daemon: Read /proc/<pid>/environ & cmdline
Daemon->>Daemon: Check CARDWIRE_* env vars, Steam, XDG lists, SQLite policies
alt Is Allowed?
Daemon->>Map: Insert PID into cw_allowed_pid
else Is Forced?
Daemon->>Map: Insert PID into cw_forced_pid with the GPU id
else Not Allowed
Daemon->>Daemon: Do nothing
end
Note over Proc,Kernel: 3. GPU Access & Directory Listing
Proc->>Kernel: getdents64 / file_open (/dev/dri/)
Kernel->>Map: Check cw_allowed_pid and cw_forced_pid
alt PID not in any map
Kernel-->>Proc: hide GPU (Return -ENOENT)
Kernel->>Daemon: Send block event (cw_report_events)
else PID in cw_allowed_pid
Kernel-->>Proc: Allow dGPU and iGPU
else PID in cw_forced_pid (value = GPU id)
Kernel-->>Proc: Allow the forced GPU, hide the others (-ENOENT)
end
Note over Proc,Daemon: 4. Application Exit
Proc->>Kernel: sched_process_exit
Kernel->>Kernel: Remove PID from cw_allowed_pid and cw_forced_pid
Application policies
Smart mode is only available on laptops (SystemType::Laptop). Per-application policies are stored in the app_policies table of the daemon’s SQLite database, with two values: Blocked and Allowed. Known apps are blocked by default until the user allows them, and newly discovered apps are announced through the NewAppAdded D-Bus signal.
The Forced policy will be added in 0.13.0
The policy for a process can be overridden at runtime through the org.opengamingcollective.cardwire.SmartPolicy D-Bus interface (RequestProcessAccess, GetProcessStatus, GetAppPolicies, SetAppPolicy). Note that GetProcessStatus returns an empty string (not "Default") for unclassified processes.
Force_GPU can be used on all systems with the Manual mode.