Skip to content

Commit 9751dbb

Browse files
fix: prefer the high-performance GPU on dual-GPU Macs
GPUI's Metal renderer sorted devices for low power and therefore always picked the Intel integrated GPU on Intel Macs with a discrete AMD GPU, even though the app declares NSSupportsAutomaticGraphicsSwitching=false. The UI was rendered and composited on the iGPU. Carry a GPUI patch that selects the high-performance device by default, with CAP_GPU_PREFERENCE=low|integrated|battery and system|auto overrides. Apply the same preference to the Windows DXGI adapter and to Cap's wgpu devices (including camera blur) so the renderer, preview and encoders agree. Add NSSupportsAutomaticGraphicsSwitching=false to the GPUI Info.plist, the change originally proposed for the Tauri app in #1759.
1 parent 57369c4 commit 9751dbb

8 files changed

Lines changed: 153 additions & 8 deletions

File tree

‎apps/desktop-gpui/Info.plist‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,7 @@
2929
<string>Cap captures your screen as part of recording your Caps</string>
3030
<key>NSCameraUseContinuityCameraDeviceType</key>
3131
<true/>
32+
<key>NSSupportsAutomaticGraphicsSwitching</key>
33+
<false/>
3234
</dict>
3335
</plist>

‎apps/desktop-gpui/README.md‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ cargo run
2020
`RUST_LOG=cap_gpui=debug cargo run` for logs — note the filter is `cap_gpui`
2121
(the binary), not `cap_desktop_gpui` (the package).
2222

23+
On dual-GPU Macs the renderer and every `cap-rendering` wgpu device pick the
24+
high-performance GPU. `CAP_GPU_PREFERENCE=low` (or `integrated`/`battery`)
25+
selects the integrated GPU instead, and `system` lets the platform choose.
26+
2327
The first build takes about 11 minutes and 6.4 GB of `target/`: gpui pulls its
2428
own revisions of the wgpu and font stacks. After that, an edit rebuilds only
2529
the app crate, at `opt-level = 0` and incrementally, in about 22s with plain
@@ -797,8 +801,9 @@ sampling UV, the legacy page flips the canvas with `scaleX(-1)`; neither app
797801
ever mirrors the recorded camera track).
798802

799803
Background blur runs live on preview frames through `src/camera_blur.rs`: a
800-
dedicated `camera-blur` thread owns its own wgpu device (requested the way the
801-
Tauri preview requests one — LowPower, downlevel limits) and runs the exact
804+
dedicated `camera-blur` thread owns its own wgpu device (requested through
805+
`cap_rendering::gpu_power_preference` like every other renderer and encoder —
806+
high-performance by default, downlevel limits) and runs the exact
802807
`cap_camera_effects::BlurProcessor` the recording's project config points the
803808
editor at, with the Tauri preview's 640×360 input cap and 150ms inference
804809
interval. The converter downscales to the cap inside the same hardware
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
diff --git a/crates/gpui_macos/src/metal_renderer.rs b/crates/gpui_macos/src/metal_renderer.rs
2+
--- a/crates/gpui_macos/src/metal_renderer.rs
3+
+++ b/crates/gpui_macos/src/metal_renderer.rs
4+
@@ -246,14 +246,32 @@
5+
}
6+
7+
fn create_device() -> Result<metal::Device> {
8+
- // Prefer low‐power integrated GPUs on Intel Mac. On Apple
9+
- // Silicon, there is only ever one GPU, so this is equivalent to
10+
- // `metal::Device::system_default()`.
11+
- if let Some(d) = metal::Device::all()
12+
- .into_iter()
13+
- .min_by_key(|d| (d.is_removable(), !d.is_low_power()))
14+
- {
15+
- Ok(d)
16+
+ let preference = std::env::var("CAP_GPU_PREFERENCE")
17+
+ .unwrap_or_default()
18+
+ .to_ascii_lowercase();
19+
+ if matches!(preference.as_str(), "system" | "auto") {
20+
+ if let Some(device) = metal::Device::system_default() {
21+
+ log::info!("selected system Metal device: {}", device.name());
22+
+ return Ok(device);
23+
+ }
24+
+ }
25+
+ let prefer_low_power = matches!(preference.as_str(), "low" | "integrated" | "battery");
26+
+ if let Some(device) = metal::Device::all().into_iter().min_by_key(|device| {
27+
+ (
28+
+ device.is_removable(),
29+
+ if prefer_low_power {
30+
+ !device.is_low_power()
31+
+ } else {
32+
+ device.is_low_power()
33+
+ },
34+
+ )
35+
+ }) {
36+
+ log::info!(
37+
+ "selected Metal device: {} (low_power={})",
38+
+ device.name(),
39+
+ device.is_low_power()
40+
+ );
41+
+ Ok(device)
42+
} else {
43+
// For some reason `all()` can return an empty list, see https://github.com/zed-industries/zed/issues/37689
44+
// In that case, we fall back to the system default device.
45+
diff --git a/crates/gpui_windows/src/directx_devices.rs b/crates/gpui_windows/src/directx_devices.rs
46+
--- a/crates/gpui_windows/src/directx_devices.rs
47+
+++ b/crates/gpui_windows/src/directx_devices.rs
48+
@@ -15,11 +15,11 @@
49+
},
50+
Dxgi::{
51+
CreateDXGIFactory2, DXGI_CREATE_FACTORY_DEBUG, DXGI_CREATE_FACTORY_FLAGS,
52+
- IDXGIAdapter1, IDXGIFactory6,
53+
+ DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE, DXGI_GPU_PREFERENCE_MINIMUM_POWER,
54+
+ DXGI_GPU_PREFERENCE_UNSPECIFIED, IDXGIAdapter1, IDXGIFactory6,
55+
},
56+
},
57+
};
58+
-use windows::core::Interface;
59+
60+
pub(crate) fn try_to_recover_from_device_lost<T>(mut f: impl FnMut() -> Result<T>) -> Result<T> {
61+
(0..5)
62+
@@ -112,8 +112,18 @@
63+
ID3D11DeviceContext,
64+
D3D_FEATURE_LEVEL,
65+
)> {
66+
+ let gpu_preference = match std::env::var("CAP_GPU_PREFERENCE")
67+
+ .unwrap_or_default()
68+
+ .to_ascii_lowercase()
69+
+ .as_str()
70+
+ {
71+
+ "low" | "integrated" | "battery" => DXGI_GPU_PREFERENCE_MINIMUM_POWER,
72+
+ "system" | "auto" => DXGI_GPU_PREFERENCE_UNSPECIFIED,
73+
+ _ => DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE,
74+
+ };
75+
for adapter_index in 0.. {
76+
- let adapter: IDXGIAdapter1 = unsafe { dxgi_factory.EnumAdapters(adapter_index)?.cast()? };
77+
+ let adapter: IDXGIAdapter1 =
78+
+ unsafe { dxgi_factory.EnumAdapterByGpuPreference(adapter_index, gpu_preference)? };
79+
if let Ok(desc) = unsafe { adapter.GetDesc1() } {
80+
let gpu_name = String::from_utf16_lossy(&desc.Description)
81+
.trim_matches(char::from(0))

‎apps/desktop-gpui/src/camera_blur.rs‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -176,8 +176,8 @@ pub fn run(jobs: flume::Receiver<BlurJob>, results: flume::Sender<BlurOutput>) {
176176
/// One device + queue for the app's blur lifetime, latched like Tauri's
177177
/// `blur_processor_init_attempted`: a failed bring-up is not retried on every
178178
/// toggle. The Tauri preview keeps its device for the window's lifetime
179-
/// (`camera.rs:573-583`, `LowPower`, downlevel limits); only the ONNX/blur
180-
/// resources are per-enable.
179+
/// (`camera.rs:573-583`, downlevel limits); only the ONNX/blur resources are
180+
/// per-enable.
181181
fn shared_device(runtime: &tokio::runtime::Runtime) -> Option<(wgpu::Device, wgpu::Queue)> {
182182
static DEVICE: OnceLock<Option<(wgpu::Device, wgpu::Queue)>> = OnceLock::new();
183183
DEVICE
@@ -186,7 +186,7 @@ fn shared_device(runtime: &tokio::runtime::Runtime) -> Option<(wgpu::Device, wgp
186186
let instance = cap_rendering::create_wgpu_instance_sync();
187187
let adapter = instance
188188
.request_adapter(&wgpu::RequestAdapterOptions {
189-
power_preference: wgpu::PowerPreference::LowPower,
189+
power_preference: cap_rendering::gpu_power_preference(),
190190
force_fallback_adapter: cap_rendering::force_software_wgpu_adapter(),
191191
compatible_surface: None,
192192
})

‎apps/desktop-gpui/src/camera_blur_portable.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ impl PortableCameraBlur {
6464
let instance = cap_rendering::create_wgpu_instance_sync();
6565
let adapter = instance
6666
.request_adapter(&wgpu::RequestAdapterOptions {
67-
power_preference: wgpu::PowerPreference::LowPower,
67+
power_preference: cap_rendering::gpu_power_preference(),
6868
force_fallback_adapter: cap_rendering::force_software_wgpu_adapter(),
6969
compatible_surface: None,
7070
})

‎crates/rendering/src/lib.rs‎

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,21 @@ pub fn force_software_wgpu_adapter() -> bool {
218218
})
219219
}
220220

221+
fn parse_gpu_power_preference(value: &str) -> wgpu::PowerPreference {
222+
match value.to_ascii_lowercase().as_str() {
223+
"low" | "integrated" | "battery" => wgpu::PowerPreference::LowPower,
224+
"system" | "auto" => wgpu::PowerPreference::None,
225+
_ => wgpu::PowerPreference::HighPerformance,
226+
}
227+
}
228+
229+
/// `CAP_GPU_PREFERENCE` selects the physical GPU when more than one is present.
230+
/// `low` picks the integrated GPU, `system` lets the platform choose, and the
231+
/// default picks the high-performance (usually discrete) GPU.
232+
pub fn gpu_power_preference() -> wgpu::PowerPreference {
233+
parse_gpu_power_preference(&std::env::var("CAP_GPU_PREFERENCE").unwrap_or_default())
234+
}
235+
221236
pub fn create_wgpu_instance_sync() -> wgpu::Instance {
222237
#[cfg(not(target_os = "windows"))]
223238
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::default());
@@ -287,7 +302,7 @@ pub async fn probe_software_adapter() -> Option<(bool, String)> {
287302
} else {
288303
instance
289304
.request_adapter(&wgpu::RequestAdapterOptions {
290-
power_preference: wgpu::PowerPreference::HighPerformance,
305+
power_preference: gpu_power_preference(),
291306
force_fallback_adapter: false,
292307
compatible_surface: None,
293308
})
@@ -2499,7 +2514,7 @@ pub async fn create_shared_device() -> Result<SharedWgpuDevice, RenderingError>
24992514
} else {
25002515
instance
25012516
.request_adapter(&wgpu::RequestAdapterOptions {
2502-
power_preference: wgpu::PowerPreference::HighPerformance,
2517+
power_preference: gpu_power_preference(),
25032518
force_fallback_adapter: false,
25042519
compatible_surface: None,
25052520
})
@@ -5207,6 +5222,40 @@ impl ProjectUniforms {
52075222
mod tests {
52085223
use super::*;
52095224

5225+
#[test]
5226+
fn gpu_power_preference_defaults_to_high_performance() {
5227+
assert_eq!(
5228+
parse_gpu_power_preference(""),
5229+
wgpu::PowerPreference::HighPerformance
5230+
);
5231+
assert_eq!(
5232+
parse_gpu_power_preference("high"),
5233+
wgpu::PowerPreference::HighPerformance
5234+
);
5235+
assert_eq!(
5236+
parse_gpu_power_preference("discrete"),
5237+
wgpu::PowerPreference::HighPerformance
5238+
);
5239+
}
5240+
5241+
#[test]
5242+
fn gpu_power_preference_honors_overrides() {
5243+
for value in ["low", "integrated", "battery", "LOW"] {
5244+
assert_eq!(
5245+
parse_gpu_power_preference(value),
5246+
wgpu::PowerPreference::LowPower,
5247+
"{value}"
5248+
);
5249+
}
5250+
for value in ["system", "auto", "System"] {
5251+
assert_eq!(
5252+
parse_gpu_power_preference(value),
5253+
wgpu::PowerPreference::None,
5254+
"{value}"
5255+
);
5256+
}
5257+
}
5258+
52105259
#[test]
52115260
fn camera_blur_output_rejects_missing_pending_and_failed_masks() {
52125261
use cap_camera_effects::{BlurFailure, BlurMaskStatus, BlurMode, BlurOutputStatus};

‎scripts/prepare-gpui-dependency.sh‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ patch_paths=(
88
"apps/desktop-gpui/patches/zed-windows.patch"
99
"apps/desktop-gpui/patches/zed-linux.patch"
1010
"apps/desktop-gpui/patches/zed-idle-frames.patch"
11+
"apps/desktop-gpui/patches/zed-gpu-preference.patch"
1112
)
1213
patch_files=("${patch_paths[@]/#/$repo_root/}")
1314
base_revision="5d1f83d9f27a19bec1fb241dc33b42238af9cf8d"

‎scripts/prepare-gpui-dependency.test.mjs‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ test("dependency verification handles overlapping patches without changing the c
122122
writeFileSync(path.join(upstream, "renderer.rs"), "base\n");
123123
writeFileSync(path.join(upstream, "windows.rs"), "base\n");
124124
writeFileSync(path.join(upstream, "frames.rs"), "base\n");
125+
writeFileSync(path.join(upstream, "gpu.rs"), "base\n");
125126
commit();
126127
const base = git(upstream, "rev-parse", "HEAD").trim();
127128
const patch = (name) => {
@@ -142,6 +143,8 @@ test("dependency verification handles overlapping patches without changing the c
142143
patch("zed-linux.patch");
143144
writeFileSync(path.join(upstream, "frames.rs"), "paced frames\n");
144145
patch("zed-idle-frames.patch");
146+
writeFileSync(path.join(upstream, "gpu.rs"), "high performance GPU\n");
147+
patch("zed-gpu-preference.patch");
145148
const script = path.join(cap, "scripts/prepare-gpui-dependency.sh");
146149
writeFileSync(
147150
script,
@@ -165,6 +168,10 @@ test("dependency verification handles overlapping patches without changing the c
165168
readFileSync(path.join(zed, "frames.rs"), "utf8"),
166169
"paced frames\n",
167170
);
171+
assert.equal(
172+
readFileSync(path.join(zed, "gpu.rs"), "utf8"),
173+
"high performance GPU\n",
174+
);
168175
});
169176

170177
writeFileSync(path.join(zed, "unrelated.txt"), "preserve staged work\n");

0 commit comments

Comments
 (0)