Skip to content

feat: add CARDWIRE_FORCE_DGPU to cardwire - #116

Merged
luytan merged 7 commits into
mainfrom
force_dgpu_env
Jul 29, 2026
Merged

feat: add CARDWIRE_FORCE_DGPU to cardwire#116
luytan merged 7 commits into
mainfrom
force_dgpu_env

Conversation

@luytan

@luytan luytan commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Description

Add a new ENV var named CARWIRE_FORCE_DGPU, setting this env to 1 before launching a process would hide the iGPU from the process, if the program only sees the dGPU, it should always select it

This is only for smart mode, porting to manual mode would be a task for >0.12.0 and would require rewriting some parts of the ebpf code, i'm also considering rewriting it to rust.

Checklist:

  • My code follows the style guidelines of this project (cargo fmt)
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the mdBook documentation
  • My changes generate no new warnings (clippy/clang)
  • New and existing unit tests pass locally with my changes (either use nix flake check or wait for the ci)

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Added environment-variable support to detect a “force dGPU” preference.
    • Improved smart-mode GPU behavior to make visibility/blocking decisions per process.
  • Bug Fixes

    • Updated GPU blocking/unblocking to use explicit GPU identifiers and propagate the selected mode consistently.
    • Refined smart-mode short-circuit and allow-list handling so skips/hiding only occur when process permission data is present and nonzero.

@luytan luytan added this to the 0.11.0 milestone Jul 28, 2026
@luytan luytan linked an issue Jul 28, 2026 that may be closed by this pull request
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@luytan, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 12 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e24e0388-923a-4223-af3f-6f48b1d6db2e

📥 Commits

Reviewing files that changed from the base of the PR and between 34bdad1 and 8c2f6f1.

📒 Files selected for processing (2)
  • crates/cardwire-daemon/src/analyzer/dynamic_analysis.rs
  • crates/cardwire-daemon/src/analyzer/models.rs
📝 Walkthrough

Walkthrough

By wizardry of explicit GPU mode values, application analysis now selects iGPU or dGPU behavior, daemon blocking APIs store those values, and smart-mode eBPF filtering uses process and inode maps to control GPU visibility.

Changes

GPU mode visibility

Layer / File(s) Summary
Value-aware inode blocking contract
crates/cardwire-ebpf/src/lib.rs, crates/cardwire-daemon/src/interface/...
GPU blocking APIs accept explicit mode values and store them for associated device inodes. Mode handling passes index 0 or 1 through blocking and refresh paths.
Application GPU mode evaluation
crates/cardwire-daemon/src/analyzer/...
Application evaluation recognizes CARDWIRE_FORCE_DGPU, returns an allow decision with a mode value, and writes that mode to pid_map.
Smart-mode syscall and directory filtering
crates/cardwire-ebpf/src/c/...
Smart-mode getdents and inode handling use blocked-inode values and pid/ppid allow-map values to decide early returns and device entry visibility.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Application
  participant spawn_exec_analyzer
  participant pid_map
  participant eBPF
  participant cw_blocked_ino
  Application->>spawn_exec_analyzer: start with environment
  spawn_exec_analyzer->>pid_map: store selected GPU mode
  eBPF->>cw_blocked_ino: read inode GPU mode
  eBPF->>pid_map: read process GPU mode
  eBPF-->>Application: allow, hide, or expose GPU directory entries
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately names the new CARDWIRE_FORCE_DGPU feature.
Description check ✅ Passed The description gives the feature summary and motivation, though it omits the related issue and several template sections.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch force_dgpu_env

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
crates/cardwire-daemon/src/interface/mode.rs (1)

124-140: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

A stale enchantment lingers and blinds the iGPU.

The default GPU only gets touched here when mode == Modes::Smart (block_gpu(0)). Nothing ever calls unblock_gpu() on the default GPU for the other branches (Integrated/Hybrid). Per is_blocked_device (helpers.h) and is_inode_blocked (lib.rs), Integrated/Manual mode blocks purely on presence in cw_blocked_ino, ignoring the stored value. So: switch to Smart mode (tags default GPU with value 0), then switch to Integrated mode — the stale 0 entry is never cleared, and the default GPU (often the only usable one) gets fully denied.

🔮 Banish the stale tracking entry on mode change
                 for gpu in gpu_list.values_mut() {
                     if !gpu.device.is_default() {
                         if mode == Modes::Integrated || mode == Modes::Smart {
                             // Here we block the dGPU
                             gpu.block_gpu(1).await?;
                         } else {
                             gpu.unblock_gpu().await?;
                         }
-                    };
-
-                    // If we in smart mode and is the default gpu, push it to the blocked inode map
-                    // but it wont get blocked, trust
-                    if mode == Modes::Smart && gpu.device.is_default() {
-                        gpu.block_gpu(0).await?;
+                    } else if mode == Modes::Smart {
+                        // push default gpu (iGPU) into the blocked inode map for tracking only
+                        gpu.block_gpu(0).await?;
+                    } else {
+                        // clear any stale smart-mode tracking entry left from a prior session
+                        gpu.unblock_gpu().await?;
                     }
                 }
🤖 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 `@crates/cardwire-daemon/src/interface/mode.rs` around lines 124 - 140, Update
the default-GPU handling in the mode loop so switching away from Smart removes
its stale blocked-inode tracking entry by calling the appropriate unblock
operation for non-Smart modes. Preserve the existing Smart behavior that records
the default GPU with block value 0, and keep non-default GPU handling unchanged.
crates/cardwire-daemon/src/interface/debug.rs (1)

134-157: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

The dGPU-forcing charm fades after a hotplug refresh.

should_block for Modes::Smart only covers the non-default GPU (!gpu.device.is_default()), so refresh_gpu never re-casts block_gpu(0) on the default GPU here. If a PCI list change (hotplug/eGPU) rebuilds GpuInterfaces with new inodes while in Smart mode, the default GPU's tracking entry from set_mode is stale/missing, silently breaking CARDWIRE_FORCE_DGPU's iGPU-hiding logic until the mode is manually re-applied.

🧙 Suggested reapplication of the tracking rune
                 if should_block {
                     info!(
                         "GPU {} should be blocked, re-applying block on hotplug",
                         gpu.device.name()
                     );
                     if let Err(e) = gpu.block_gpu(1).await {
                         warn!(
                             "failed to automatically re-block {}: {}",
                             gpu.device.name(),
                             e
                         );
                     }
+                } else if mode == Modes::Smart && gpu.device.is_default() {
+                    if let Err(e) = gpu.block_gpu(0).await {
+                        warn!(
+                            "failed to re-track default gpu {}: {}",
+                            gpu.device.name(),
+                            e
+                        );
+                    }
                 }
🤖 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 `@crates/cardwire-daemon/src/interface/debug.rs` around lines 134 - 157, Update
the refresh logic around `should_block` and `gpu.block_gpu` so `Modes::Smart`
also reapplies the default GPU’s required `block_gpu(0)` tracking state after
hotplug rebuilds, while preserving non-default GPU blocking and existing
Integrated, Hybrid, and Manual behavior. Ensure the Smart-mode path restores the
force-dGPU/iGPU-hiding state for newly recreated interfaces.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@crates/cardwire-daemon/src/analyzer/dynamic_analysis.rs`:
- Around line 93-105: Refactor check_cardwire_force_dgpu and
check_cardwire_allow to use a shared environment-variable parsing helper
parameterized by the variable prefix, deriving the value offset from that prefix
instead of hardcoding 20. Preserve the existing Option<bool> behavior, and add
focused unit tests covering enabled, disabled, missing, and offset-sensitive
values.

In `@crates/cardwire-ebpf/src/c/bpf.c`:
- Around line 152-161: Introduce a shared always-inline helper, such as
should_skip_getdents_patch, that combines the is_pid_allowed check with the
cw_allowed_pid lookups and returns whether patching should be skipped. Replace
the duplicated allow_val logic in both getdents64 hook call sites with this
helper while preserving the existing is_smart guard and return behavior.

In `@crates/cardwire-ebpf/src/c/helpers.h`:
- Around line 216-260: Update the iGPU/dGPU visibility logic in the shown
getdents handling block to look up cw_allowed_pid by pid and, when absent, retry
with ppid. Use the resulting allow_map_value for the existing ALLOW/FORCE_DGPU
branches, preserving their current visibility behavior for both direct processes
and forked children.

---

Outside diff comments:
In `@crates/cardwire-daemon/src/interface/debug.rs`:
- Around line 134-157: Update the refresh logic around `should_block` and
`gpu.block_gpu` so `Modes::Smart` also reapplies the default GPU’s required
`block_gpu(0)` tracking state after hotplug rebuilds, while preserving
non-default GPU blocking and existing Integrated, Hybrid, and Manual behavior.
Ensure the Smart-mode path restores the force-dGPU/iGPU-hiding state for newly
recreated interfaces.

In `@crates/cardwire-daemon/src/interface/mode.rs`:
- Around line 124-140: Update the default-GPU handling in the mode loop so
switching away from Smart removes its stale blocked-inode tracking entry by
calling the appropriate unblock operation for non-Smart modes. Preserve the
existing Smart behavior that records the default GPU with block value 0, and
keep non-default GPU handling unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d919546d-9719-4d45-b0f7-15902bf7e842

📥 Commits

Reviewing files that changed from the base of the PR and between 79236f3 and 738ce56.

📒 Files selected for processing (8)
  • crates/cardwire-daemon/src/analyzer/dynamic_analysis.rs
  • crates/cardwire-daemon/src/analyzer/models.rs
  • crates/cardwire-daemon/src/interface/debug.rs
  • crates/cardwire-daemon/src/interface/gpu.rs
  • crates/cardwire-daemon/src/interface/mode.rs
  • crates/cardwire-ebpf/src/c/bpf.c
  • crates/cardwire-ebpf/src/c/helpers.h
  • crates/cardwire-ebpf/src/lib.rs

Comment thread crates/cardwire-daemon/src/analyzer/dynamic_analysis.rs Outdated
Comment on lines +152 to +161
__u8 *allow_val =
bpf_map_lookup_elem(&cw_allowed_pid, &pid);
if (!allow_val) {
allow_val = bpf_map_lookup_elem(&cw_allowed_pid,
&ppid);
}
// If force_dgpu is NOT enabled, we can safely skip patching
if (allow_val && *allow_val != 0) {
return 0;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Casting the same lookup spell twice, in two scrolls.

Both getdents64 hooks repeat: is_pid_allowed(pid,ppid) (which itself looks up cw_allowed_pid by pid/ppid) followed by an independent re-lookup of the same map to fetch the value. On a syscall hot path (getdents64 fires on every directory read), that's redundant map work performed twice per call site, copy-pasted verbatim across both functions.

♻️ Suggested shared helper
static __always_inline int should_skip_getdents_patch(__u32 pid, __u32 ppid)
{
	if (!is_pid_allowed(pid, ppid))
		return 0;

	__u8 *allow_val = bpf_map_lookup_elem(&cw_allowed_pid, &pid);
	if (!allow_val)
		allow_val = bpf_map_lookup_elem(&cw_allowed_pid, &ppid);

	// If force_dgpu is NOT enabled, we can safely skip patching
	return allow_val && *allow_val != 0;
}

Then both call sites collapse to:

if (is_smart() && should_skip_getdents_patch(pid, ppid)) {
	return 0;
}

Also applies to: 196-205

🤖 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 `@crates/cardwire-ebpf/src/c/bpf.c` around lines 152 - 161, Introduce a shared
always-inline helper, such as should_skip_getdents_patch, that combines the
is_pid_allowed check with the cw_allowed_pid lookups and returns whether
patching should be skipped. Replace the duplicated allow_val logic in both
getdents64 hook call sites with this helper while preserving the existing
is_smart guard and return behavior.

Comment thread crates/cardwire-ebpf/src/c/helpers.h
@luytan

luytan commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 16 minutes.

@luytan
luytan merged commit c19b81f into main Jul 29, 2026
8 checks passed
@luytan
luytan deleted the force_dgpu_env branch July 29, 2026 09:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cardwired: Force app to use a specific GPU

1 participant