Skip to content

fix: follow the system GPU on dual-GPU Macs - #2776

Open
AasishDairelSahayaGrinspan wants to merge 3 commits into
CapSoftware:mainfrom
AasishDairelSahayaGrinspan:fix/gpui-high-performance-gpu
Open

AasishDairelSahayaGrinspan wants to merge 3 commits into
CapSoftware:mainfrom
AasishDairelSahayaGrinspan:fix/gpui-high-performance-gpu

Conversation

@AasishDairelSahayaGrinspan

@AasishDairelSahayaGrinspan AasishDairelSahayaGrinspan commented Oct 20, 2026 •

Copy link
Copy Markdown

Supersedes #1759 for the shipping GPUI app. #1759 only changed the old Tauri Info.plist; the key alone could never fix this because GPUI explicitly requested the integrated GPU.

Problem

On Intel Macs with a discrete AMD GPU, gpui_macos::metal_renderer::create_device sorted metal::Device::all() by (is_removable, !is_low_power). On my 2019 MacBook Pro (i7-9750H, Intel UHD 630 + Radeon Pro 5300M) that always selected the low-power Intel iGPU, so the UI was rendered and composited on the integrated GPU even when macOS itself was using the AMD GPU. This caused the lag and GPU-related instability reported in #1759.

Reproduction with GPUI's exact predicate:

devices=2
name=Intel(R) UHD Graphics 630 lowPower=true removable=false
name=AMD Radeon Pro 5300M lowPower=false removable=false
before=Intel(R) UHD Graphics 630
after=AMD Radeon Pro 5300M
system_default=AMD Radeon Pro 5300M

Fix

  • Adds a macOS-only apps/desktop-gpui/patches/zed-gpu-preference.patch to the existing GPUI patch stack. create_device now follows MTLCreateSystemDefaultDevice() by default; CAP_GPU_PREFERENCE=high|performance|discrete forces the high-performance GPU (including an external GPU over a built-in iGPU), and low|integrated|battery forces the iGPU.
  • Routes Cap's wgpu device creation (shared device, software probe, camera blur and portable camera blur) through cap_rendering::request_wgpu_adapter. On macOS the hardware adapter is matched to MTLCreateSystemDefaultDevice() by name, because wgpu's PowerPreference::None takes the first Metal adapter and wgpu-hal orders discrete devices before low-power ones. Off macOS each call site keeps its previous preference (HighPerformance for the renderer and probe, LowPower for camera blur) when the variable is unset.
  • Leaves the Windows DirectX adapter path and Info.plist unchanged. NSSupportsAutomaticGraphicsSwitching is omitted; I also tested true, and it did not change the system default or mux decision on this machine.
  • Documents the behavior and overrides in apps/desktop-gpui/README.md.

Verification

  • cargo check --workspace and cargo check --workspace --all-targets in apps/desktop-gpui.
  • cargo test -p cap-rendering gpu_power_preference --lib — 2 passed, and system_metal_adapter --lib — 1 passed. The latter asserts the wgpu Metal adapter name equals MTLCreateSystemDefaultDevice()'s name on this machine.
  • node --test scripts/prepare-gpui-dependency.test.mjs — 8/8, including a fresh checkout with the new patch applied.
  • ./scripts/prepare-gpui-dependency.sh validates the patch stack on an already-prepared checkout.
  • Built the app and ran it with RUST_LOG=gpui_macos=info; with no CAP_GPU_PREFERENCE it logs selected system Metal device: AMD Radeon Pro 5300M, with CAP_GPU_PREFERENCE=high it logs selected Metal device: AMD Radeon Pro 5300M (low_power=false), and with low it logs the Intel UHD 630.
  • With the default setting, opening a studio recording logs Following the system Metal device adapter_name=AMD Radeon Pro 5300M and Using hardware GPU adapter adapter_name="AMD Radeon Pro 5300M" adapter_backend=Metal adapter_device_type=DiscreteGpu, so the GPUI renderer and the wgpu renderer/encoders land on the same GPU. CAP_GPU_PREFERENCE=low selects Intel(R) UHD Graphics 630, high selects the 5300M.
  • Rebuilt and re-ran the app after routing camera blur through the same adapter helper; the default and low override logs are unchanged (5300M and UHD 630 respectively).
  • cargo fmt --all --check.

Idle GPU power

Measured on the same 2019 MacBook Pro, on battery, automatic graphics switching on, only the Cap main window open, same debug binary. powermetrics --samplers gpu_power on this Intel Mac only reports the Intel GPU (GPU 0 name IntelIG) and has no AMD power counter, so I sampled the AMD driver's "Total Power(W)" from IOAccelerator at 1 Hz for 30 s, plus whole-system battery current.

Case Selected GPU AMD GPU power Battery draw
CAP_GPU_PREFERENCE=low (old iGPU choice) Intel UHD 630 0.00 W avg; off 30/30 samples 17.1 W
CAP_GPU_PREFERENCE=high (previous default) Radeon Pro 5300M 15.60 W avg, 17 W max 38.4 W (30 s) / 21.2 W (10 s repeat)
default / system (new default) Radeon Pro 5300M 15.40 W avg (10 s) 17.2 W (10 s)

The high-performance path keeps the dGPU powered at ~15.6 W while the mux stays on the iGPU, so both GPUs are active. With system, macOS moves the display to the 5300M (mux GFX0), powers down the iGPU, and system draw returns to the iGPU-only baseline in the 10 s sample. The default is now system; CAP_GPU_PREFERENCE=high remains available for users who need to force the dGPU, and low restores the old battery behavior.

Battery draw is noisier than the AMD counter because other apps were running; the stable result is that the previous high-performance default kept the dGPU at ~15.6 W for as long as the window was open.

RetriggerView in GreptileConfidence Score: 5/5

The PR appears safe to merge; both previous findings are fixed.

Summary

Cap now follows the system GPU on macOS by default, with CAP_GPU_PREFERENCE overrides.

  • Cap's macOS renderers follow the system's GPU by default.

Reviews (2) · Last reviewed commit: "fix: follow the system GPU for camera bl..." · Reviewed by Greptile

@vercel

vercel Bot commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

@AasishDairelSahayaGrinspan is attempting to deploy a commit to the Cap Software Inc Team on Vercel.

A member of the Team first needs to authorize it.

@richiemcilroy

Copy link
Copy Markdown
Member

@AasishDairelSahayaGrinspan thanks for digging into this on real hardware, the device logs make the macOS diagnosis clear. A few things before we can take it: 1) patches/zed-gpu-preference.patch also changes GPUI's Windows DirectX adapter to DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE for everyone. On hybrid (Optimus) laptops that moves the whole UI onto the discrete GPU and keeps it powered while Cap is idle, and it has not been compiled or run on Windows yet. Could you drop the gpui_windows hunk so this stays a macOS fix? 2) On Intel dual-GPU Macs the GPUI renderer will now keep the discrete GPU awake for as long as a Cap window exists. Could you share an idle power comparison on the 2019 MBP, on battery with only the main window open, before and after (e.g. sudo powermetrics --samplers gpu_power -i 1000 -n 30)? If the idle cost is noticeable we may want the default to be system instead. 3) NSSupportsAutomaticGraphicsSwitching set to false behaves the same as leaving the key out, so I think it can be dropped from Info.plist.

@AasishDairelSahayaGrinspan AasishDairelSahayaGrinspan changed the title fix: prefer the high-performance GPU on dual-GPU Macs fix: follow the system GPU on dual-GPU Macs Oct 21, 2026
@AasishDairelSahayaGrinspan

Copy link
Copy Markdown
Author

Updated the default to system and added the idle-power data to the PR body.

The short version on the 2019 MBP, on battery with only the main window open:

  • CAP_GPU_PREFERENCE=low (old behavior): Intel UHD 630, AMD GPU off 30/30 samples, battery ~17 W.
  • CAP_GPU_PREFERENCE=high (previous default): 5300M at 15.6 W average, mux stayed on the iGPU, battery 21-38 W depending on background load.
  • system (new default): macOS selects the 5300M and moves the display to it (mux GFX0), AMD 15.4 W, battery ~17.2 W in a 10 s sample.

powermetrics --samplers gpu_power only reports GPU 0 name IntelIG on this Intel Mac and has no AMD power counter, so I used the driver's IOAccelerator "Total Power(W)" at 1 Hz plus battery current.

Head is d9deb92. The patch is still macOS-only and the plist key is still omitted. Could you take another look when you have a minute?

@richiemcilroy

Copy link
Copy Markdown
Member

Thanks @AasishDairelSahayaGrinspan, this is a big improvement: the patch is macOS-only now, the Info.plist key is gone, and the IOAccelerator plus battery measurements make the trade-off really clear, so switching the default to system makes sense for Intel Macs. One thing the new default changes outside macOS: with CAP_GPU_PREFERENCE unset, gpu_power_preference() (crates/rendering/src/lib.rs) returns wgpu::PowerPreference::None on every platform, so on Windows and Linux create_shared_device and probe_software_adapter no longer ask for HighPerformance. wgpu then takes the first adapter in enumeration order (DXGI EnumAdapters1 on Windows, which on most Optimus laptops is the integrated GPU), so editor preview and export would move off the discrete GPU there, and camera_blur.rs / camera_blur_portable.rs stop asking for LowPower. Could you keep the previous per-site defaults when the env var is unset on non-macOS (for example pass the old default into gpu_power_preference(default) and only use None on macOS), so this stays a macOS-only change? Also, since wgpu's None picks the first Metal device rather than MTLCreateSystemDefaultDevice(), could you add one line to the description with the wgpu adapter name Cap logs on the 2019 MBP with the default setting, to confirm the renderer and the wgpu device really land on the same GPU? Thanks!

@AasishDairelSahayaGrinspan

AasishDairelSahayaGrinspan commented Oct 21, 2026 •

Copy link
Copy Markdown
Author

Addressed in 44daf4d.

  1. gpu_power_preference(default) keeps each call site's previous preference when the variable is unset off macOS: HighPerformance for create_shared_device and probe_software_adapter, LowPower for camera_blur.rs and camera_blur_portable.rs. Only macOS resolves to the system default. system/auto behave the same as unset.
  2. While adding the adapter line I confirmed your suspicion about PowerPreference::None: wgpu-hal sorts Metal devices with low-power first, so it was selecting the Intel iGPU. create_shared_device and probe_software_adapter now match the Metal adapter to MTLCreateSystemDefaultDevice() by name when the variable is unset, and only fall back to request_adapter otherwise. On the 2019 MBP that is the 5300M for both GPUI and wgpu.
  3. Captured from the app with the default setting (opening a studio recording prewarms the shared device): Following the system Metal device adapter_name=AMD Radeon Pro 5300M and Using hardware GPU adapter adapter_name="AMD Radeon Pro 5300M" adapter_backend=Metal adapter_device_type=DiscreteGpu. With CAP_GPU_PREFERENCE=low the wgpu path selects the Intel UHD 630, with high it selects the 5300M.

Checks: cargo test -p cap-rendering gpu_power_preference --lib (2 passed), system_metal_adapter --lib (1 passed, asserts the adapter name equals MTLCreateSystemDefaultDevice()'s name), node --test scripts/prepare-gpui-dependency.test.mjs (8/8), cargo check in both workspaces, cargo fmt --all --check.

One unrelated note: macOS Engine verify is red on this PR and on main at d16fd199 with the same crash/recorder-kill/*/static-screen fragment-span and AVFoundation encoder errors (main run 39544976143). The audio/timing_repair failure passed on retry. Not caused by this diff.

@AasishDairelSahayaGrinspan

Copy link
Copy Markdown
Author

Captured from the built app at 44daf4d on the 2019 MacBook Pro, CAP_GPU_PREFERENCE unset, opening a studio recording (which prewarms the shared wgpu device):

INFO gpui_macos::metal_renderer: selected system Metal device: AMD Radeon Pro 5300M
INFO cap_rendering: Following the system Metal device adapter_name=AMD Radeon Pro 5300M
INFO cap_rendering: Using hardware GPU adapter adapter_name="AMD Radeon Pro 5300M" adapter_backend=Metal adapter_device_type=DiscreteGpu
INFO cap_gpui::app_windows: shared wgpu device ready elapsed_ms=161.59 software=false

Both the GPUI renderer and the wgpu renderer/encoders are on the 5300M. For comparison, CAP_GPU_PREFERENCE=low makes the wgpu path select Intel(R) UHD Graphics 630, and high selects the 5300M.

@richiemcilroy

Copy link
Copy Markdown
Member

@greptileai review

Comment thread apps/desktop-gpui/src/camera_blur.rs Outdated
Comment on lines +189 to +191
power_preference: cap_rendering::gpu_power_preference(
wgpu::PowerPreference::LowPower,
),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Blur skips system GPU choice

With CAP_GPU_PREFERENCE unset on macOS, gpu_power_preference(LowPower) returns PowerPreference::None. This direct request_adapter call never uses system_metal_adapter. The vendored Metal backend sorts discrete GPUs first, so blur can wake the discrete GPU even when macOS and GPUI use integrated graphics, increasing battery use.

Route this request through the same system-device matching used by create_shared_device, while preserving forced software rendering.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop-gpui/src/camera_blur.rs
Line: 189-191

Comment:
**Blur skips system GPU choice**

With `CAP_GPU_PREFERENCE` unset on macOS, `gpu_power_preference(LowPower)` returns `PowerPreference::None`. This direct `request_adapter` call never uses `system_metal_adapter`. The vendored Metal backend sorts discrete GPUs first, so blur can wake the discrete GPU even when macOS and GPUI use integrated graphics, increasing battery use.

Route this request through the same system-device matching used by `create_shared_device`, while preserving forced software rendering.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in c09abaf. camera_blur.rs and camera_blur_portable.rs now go through cap_rendering::request_wgpu_adapter, which uses the same MTLCreateSystemDefaultDevice() match as create_shared_device when CAP_GPU_PREFERENCE is unset, and still forces the fallback adapter when software rendering is requested. The vendored wgpu-hal orders discrete devices first, so the previous direct None request could indeed wake the dGPU; that path is gone.

Comment on lines +30 to +34
+ device.is_removable(),
+ if prefer_low_power {
+ !device.is_low_power()
+ } else {
+ device.is_low_power()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 High ignores external GPUs

CAP_GPU_PREFERENCE=high still prefers any built-in GPU over an external GPU because is_removable() is the first sort key. If an Intel Mac has integrated graphics and an external discrete GPU, integrated graphics wins despite the explicit high-performance request.

Rank the requested power class first and use removability only to break ties.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop-gpui/patches/zed-gpu-preference.patch
Line: 30-34

Comment:
**High ignores external GPUs**

`CAP_GPU_PREFERENCE=high` still prefers any built-in GPU over an external GPU because `is_removable()` is the first sort key. If an Intel Mac has integrated graphics and an external discrete GPU, integrated graphics wins despite the explicit high-performance request.

Rank the requested power class first and use removability only to break ties.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in c09abaf. The sort key now ranks the requested power class first and uses is_removable() only as a tie-breaker, so CAP_GPU_PREFERENCE=high can select an external discrete GPU over a built-in iGPU.

@AasishDairelSahayaGrinspan

Copy link
Copy Markdown
Author

Both Greptile findings are fixed in c09abaf:

  • camera_blur.rs and camera_blur_portable.rs now go through cap_rendering::request_wgpu_adapter, so on macOS with no override they use the same MTLCreateSystemDefaultDevice() match as the shared device and probe. low/high still override, and forced software rendering still bypasses the system match.
  • The GPUI patch now ranks the requested power class first and uses is_removable() only as a tie-breaker, so CAP_GPU_PREFERENCE=high can select an external discrete GPU over a built-in iGPU.

The new workflows are waiting for approval. Could you approve them when you get a chance?

@richiemcilroy

Copy link
Copy Markdown
Member

@greptileai review

This branch has not been deployed

No deployments
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.

2 participants