Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/cicd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ jobs:
curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf -y | sh
rustup update
cargo install --version ${MDBOOK_VERSION} mdbook
cargo install mdbook-mermaid

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Pin mdbook-mermaid for reproducible builds.

The workflow pins mdbook but installs an unconstrained Mermaid preprocessor, so future releases can change CI behavior without any repository change. Pin the version tested with mdbook 0.5.2 and use --locked; the upstream project publishes versioned releases and supports Cargo installation. (github.com)

🪄 Proposed fix
-          cargo install mdbook-mermaid
+          cargo install --version <tested-version> --locked mdbook-mermaid
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/cicd.yml at line 149, Update the mdbook-mermaid
installation step to pin the version tested with mdbook 0.5.2 and pass Cargo’s
--locked option, replacing the unconstrained install command while preserving
the existing workflow setup.

- name: Test Pages
id: pages
uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
Expand Down
6 changes: 6 additions & 0 deletions book.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
[book]
src = "docs"

[preprocessor.mermaid]
command = "mdbook-mermaid"

[output.html]
additional-js = ["./docs/lib/mermaid/mermaid.min.js", "./docs/lib/mermaid/mermaid-init.js"]
2 changes: 2 additions & 0 deletions docs/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,7 @@
# Development

- [DBus](development/dbus.md)
- [SwitcherooShim](development/switcheroo.md)
- [SmartMode](development/smart.md)
- [BPF](development/bpf.md)
- [Build&Dev](development/build-dev.md)
88 changes: 25 additions & 63 deletions docs/development/bpf.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,80 +2,42 @@

## Introduction

Cardwire use the Kernel eBPF + LSM features to block syscall to the dGPU
Cardwire uses Linux eBPF along with Linux Security Modules (LSM) and Syscall tracepoints to intercept and block applications. By intercepting these operations directly in the kernel, Cardwire provides a fast and seamless blocking without needing to unload drivers or modify user applications/files.

## List of used LSM
## eBPF Hooks

- lsm/file_open
- lsm/inode_permission
- lsm/inode_getattr
Cardwire utilizes two main types of eBPF hooks:

## List of used MAPS
### 1. LSM Hooks

**BLOCKED_RENDERID**
LSM hooks are used to intercept and block permission checks or file openings on device files (like `/dev/dri/*`). This stops applications from accessing a GPU simply by checking file stats.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

- Used for the renderD minor
- `lsm/file_open`: Intercepts the actual opening of blocked device files.
- `lsm/inode_permission`: Prevents permissions checks on blocked devices.
- `lsm/inode_getattr`: Prevents `stat()` calls on blocked devices.

**BLOCKED_CARDID**
### 2. Syscall Tracepoints

- For the card minor
Tracepoints are used to monitor process lifecycle and manipulate the directory listings applications see.

**BLOCKED_PCI**
- `tracepoint/sched/sched_process_exec`: In Smart mode, this signals the Cardwire daemon that a new process is starting so it can be analyzed.
- `tracepoint/sched/sched_process_exit`: Signals when a process dies, cleaning up its entries in the allowed process maps.
- `tp/syscalls/sys_enter_getdents64` and `sys_exit_getdents64`: Intercepts directory listings. This is the core magic behind dynamically hiding device files from applications.

- For the PCI address
## eBPF Maps

**BLOCKED_PCI_FILES**
The eBPF programs communicate with the Cardwire userspace daemon using several BPF maps:

- For the list of blocked PCI files
- **`cw_mode`**: Stores the current Cardwire mode (0=Integrated, 1=Hybrid, 2=Manual, 3=Smart).
- **`cw_blocked_ino`**: A hash map containing the inodes of blocked DRM devices (`/dev/dri/cardX`, `/dev/dri/renderDX`). The value indicates the GPU ID (0 for iGPU, 1 for dGPU).
- **`cw_exp_blk_ino`**: Contains inodes of blocked NVIDIA-specific files when `experimental_nvidia_block` is enabled.
- **`cw_allowed_pid`**: Used in Smart mode. Contains the PIDs of applications that have been analyzed and allowed to use the dGPU. The stored value (`__u8`) is used to identify if PID is meant for iGPU(0) or dGPU(1)
- **`cw_allowed_comm`**: A whitelist of process names (like `udev` or `pacman`) that bypass blocking entirely.
- **`cw_daemon_pid`**: Cardwire's own PID so it doesn't block itself.
- **`cw_exec_events`**, **`cw_close_events`**, **`cw_report_events`**: Ring buffers used to send process and block events back to userspace.
Comment on lines +31 to +37

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Unify the Smart-mode state machine across the development guides.

The environment flags, allowed-PID values, and GPU visibility states currently describe incompatible mappings. Define one canonical table and apply it at every affected site.

  • docs/development/bpf.md#L29-L35: document the canonical cw_allowed_pid value mapping.
  • docs/development/smart.md#L21-L23: align analyzer and environment-variable semantics.
  • docs/development/smart.md#L55-L61: align the Mermaid branches with that mapping.
  • docs/development/switcheroo.md#L41-L53: align CARDWIRE_FORCE_DGPU and CARDWIRE_ALLOW behavior.
📍 Affects 3 files
  • docs/development/bpf.md#L29-L35 (this comment)
  • docs/development/smart.md#L21-L23
  • docs/development/smart.md#L55-L61
  • docs/development/switcheroo.md#L41-L53
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/development/bpf.md` around lines 29 - 35, Unify the Smart-mode state
machine by defining one canonical mapping for environment flags, analyzer
outcomes, cw_allowed_pid values, and GPU visibility, then apply it consistently
at docs/development/bpf.md:29-35, docs/development/smart.md:21-23,
docs/development/smart.md:55-61, and docs/development/switcheroo.md:41-53.
Update the cw_allowed_pid documentation, analyzer and environment-variable
semantics, Mermaid branches, and CARDWIRE_FORCE_DGPU/CARDWIRE_ALLOW behavior to
match that single mapping.


**BLOCKED_NVIDIA_FILES**
## Directory Hiding (`getdents64`)

- For the list of blocked NVIDIA files
Across all blocking modes (Integrated, Manual, Smart), Cardwire uses the `getdents64` syscall hooks to manipulate the contents of directories (like `/dev/dri/`) on the fly.

**SETTINGS**

- For experimental_nvidia_block

## Block list

### PCI files

Files that get blocked when a gpu's PCI address is blocked:

- config
- current_link_speed
- current_link_width
- max_link_speed
- max_link_width

### NVIDIA files

These files are only blocked when the `experimental_nvidia_block` setting is enabled

- libGLX_nvidia.so.0
- nvidia_icd.json
- nvidia_icd.x86_64.json
- nvidiactl

/dev/nvidia? using the minor

Example:

```bash
/dev/nvidia0
```

Will be blocked using the major `195` and the minor `0`

### DRM

DRM node (card + renderD) are blocked using their major + minor ID

Example:

```bash
/dev/dri/card1
/dev/dri/renderD128
```

Will be blocked using the major `226` and the minor `1` || `128`
When an application calls `getdents64` to list available GPUs, the eBPF program `patch_dirent_if_found` loops through the directory entries in memory. If it spots an inode belonging to a blocked GPU, it overwrites the previous entry's length field, effectively "jumping over" the blocked device. To the application, the blocked GPU simply does not exist and is omitted from directory listings rather than causing an error.
18 changes: 10 additions & 8 deletions docs/development/dbus.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@

- **Bus Name:** `com.github.opengamingcollective.cardwire`

> [!NOTE]
> Cardwire also implements the SwitcherooControl interface for desktop environment integration. See [switcheroo.md](switcheroo.md) for details.

---

## Object Path

`/com/github/opengamingcollective/cardwire`

### Manager

`com.github.opengamingcollective.cardwire.Manager`

**Methods:**
Expand All @@ -25,6 +30,7 @@
- **Outputs:** None

### Mode

`com.github.opengamingcollective.cardwire.Mode`

**Properties:**
Expand All @@ -38,9 +44,10 @@
- `0` Integrated: Block the dGPU. Requires exactly 2 GPUs
- `1` Hybrid: Unblock the dGPU. Requires exactly 2 GPUs
- `2` Manual: Allow per-GPU blocking via individual GPU objects. Applies saved GPU state on mode change if `auto_apply_gpu_state` is enabled
- `3` Smart
- `3` Smart: Block the dGPU by default but dynamically allow access per-application using eBPF. Requires exactly 2 GPUs

### Config

`com.github.opengamingcollective.cardwire.Config`

**Properties:**
Expand All @@ -65,14 +72,8 @@
- **Type:** `b`
- **Access:** Read/Write

**Methods:**

- **`SaveToFile`**
Save the current daemon configuration (properties above) to the `cardwire.toml` config file
- **Inputs:** None
- **Outputs:** None

### Debug

`com.github.opengamingcollective.cardwire.Debug`

**Methods:**
Expand All @@ -93,6 +94,7 @@
- `child_pci`: `s` - Child PCI address (empty string if unknown)

### Gpu

`/com/github/opengamingcollective/cardwire/Gpu/{id}`

Represents a single GPU device, where `{id}` is the numeric identifier of the GPU (0 is always the default one). These objects can be dynamically discovered by calling `GetManagedObjects` on the standard `org.freedesktop.DBus.ObjectManager` interface located at the root path (`/com/github/opengamingcollective/cardwire`)
Expand Down
69 changes: 69 additions & 0 deletions docs/development/smart.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Smart

## 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 `cw_exec_events` RING_BUF, once the process is received by `cardwired`, it will be analyzed in real-time and if it's a process that should be allowed, its pid will be inserted into the `cw_allowed_pid` map

When a process exits, a notification is sent to `cardwired`, cardwired will remove the PID from its map to prevent the map from overflowing

If you want to dive deeper into the kernel code, take a look at [BPF](bpf.md)

### Userspace

The userspace of Smart mode acts as the brain. 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 the `cw_exec_events` and `cw_close_events` ring buffers. When it receives a new PID from the kernel, it invokes the analysis helpers. If the application passes, it populates the `cw_allowed_pid` map with a value of `1` (normal) or `0` (`iGPU`).
- **`dynamic_analysis.rs`**: A set of helper functions used to analyze a process in real-time. By reading `/proc/<pid>/environ` and `/proc/<pid>/cmdline`, it checks for explicitly requested GPUs (like `CARDWIRE_ALLOW=1`, `CARDWIRE_FORCE_DGPU=1`, `DRI_PRIME=1`) or implicit signs like Steam games (`SteamAppId`) and Flatpak wrappers.
- **`static_analysis.rs`**: A set of helper functions that analyze system data when the daemon starts. Specifically, it scans the XDG data directories for `.desktop` files containing `PrefersNonDefaultGPU=true` or `X-KDE-RunOnDiscreteGpu=true`, building a whitelist of application names that should automatically be granted dGPU access when they launch.

## Complete Execution Flow

Here is a comprehensive breakdown of how the Kernel and Userspace interact in real-time when an application launches:

```mermaid
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 env vars, Steam, Flatpak, XDG lists

alt Is Allowed?
Daemon->>Map: Insert PID into cw_allowed_pid
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

alt PID not in cw_allowed_pid
Kernel-->>Proc: hide GPU (Return -ENOENT)
Kernel->>Daemon: Send block event (cw_report_events)
else PID in cw_allowed_pid (Value 1 = Normal)
Kernel-->>Proc: Allow dGPU and iGPU
else PID in cw_allowed_pid (Value 0 = FORCE_DGPU)
Kernel-->>Proc: Allow dGPU, Hide iGPU (-ENOENT)
end

Note over Proc,Daemon: 4. Application Exit
Proc->>Kernel: sched_process_exit
Kernel->>Map: Send PID via cw_close_events (RingBuf)
Map->Daemon: Listen to cw_close_events and wait for new events
Daemon->>Map: Remove PID from cw_allowed_pid
```
58 changes: 58 additions & 0 deletions docs/development/switcheroo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Switcheroo Shim

Cardwire implements a compatibility shim for the `net.hadess.SwitcherooControl` D-Bus interface. This allows desktop environments (like GNOME(gio-launch-desktop) and KDE) to natively offer "Launch using Discrete Graphics Card" options in their application menus without needing any Cardwire-specific plugins.

_(Having our own integration would've been better tbh)_

## Service

- **Interface:** `net.hadess.SwitcherooControl`

---

## Properties

### `HasDualGpu`

Indicates whether the system has exactly two GPUs.
Comment on lines +15 to +17

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Complete the markdown formatting incantations.

MD022 flags missing blank lines after the subsection headings, and MD047 reports that the file lacks its final newline. Add the required spacing and exactly one trailing newline.

Also applies to: 20-21, 25-26, 41-42, 50-51, 53-53

🧰 Tools
🪛 markdownlint-cli2 (0.23.1)

[warning] 15-15: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/development/switcheroo.md` around lines 15 - 16, Add blank lines after
every affected subsection heading in docs/development/switcheroo.md, including
HasDualGpu and the referenced sections, and ensure the file ends with exactly
one trailing newline.

Source: Linters/SAST tools

- **Type:** `b` (boolean)
- **Access:** Read

### `NumGPUs`

The number of GPUs detected on the system.
- **Type:** `u` (uint32)
- **Access:** Read

### `GPUs`

A list of all available GPUs and their configurations.
- **Type:** `aa{sv}` (Array of dictionaries mapping strings to variants)
- **Access:** Read
- **Dictionary Keys:**
- `Name`: `s` - The name of the GPU.
- `Environment`: `as` - An array of environment variable key-value pairs to set when launching an application on this GPU (e.g., `["CARDWIRE_FORCE_DGPU", "1"]`).
- `Default`: `b` - Whether this is the default display GPU (usually the iGPU).
- `Discrete`: `b` - Whether this is a discrete GPU.

---

## Environment Variables Explained

The `Environment` property provides the exact environment variables the desktop environment should inject into the application when the user selects a specific GPU.

### `CARDWIRE_FORCE_DGPU=1`

This is provided when the user selects the **Discrete GPU**.

When Cardwire detects this environment variable during the application's launch in Smart Mode, it does two things:
1. **Unblocks the dGPU**: The eBPF hooks allow the application to access the discrete GPU's device files.
2. **Hides the iGPU**: It actively intercepts and blocks the application from seeing the integrated GPU.

Hiding the iGPU ensures that the application is forced to use the discrete GPU, preventing issues where an application might get confused by seeing two GPUs and accidentally select the weaker one.

### `CARDWIRE_ALLOW=0`

This is provided when the user selects the **Default/Integrated GPU**.

It explicitly tells Cardwire's Smart Mode to keep the dGPU blocked for this application, ensuring it runs solely on the integrated graphics to save power.
10 changes: 10 additions & 0 deletions docs/diagnostics/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,13 @@ and
```bash
cat /sys/class/drm/*/status
```

## nvidia-powerd failure after switching modes

When switching to integrated mode on NVIDIA hardware, you may see errors or failures related to the `nvidia-powerd` service. This is a known quirk caused by the GPU entering `D3Cold` (a deep sleep state) which prevents `nvidia-powerd` from communicating with it.

Service must be restarted:

```bash
sudo systemctl restart nvidia-powerd.service
```
Comment thread
luytan marked this conversation as resolved.
Loading
Loading