Skip to content
Open
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
14 changes: 12 additions & 2 deletions apps/desktop-gpui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ cargo run
`RUST_LOG=cap_gpui=debug cargo run` for logs — note the filter is `cap_gpui`
(the binary), not `cap_desktop_gpui` (the package).

On dual-GPU Macs the renderer and every `cap-rendering` wgpu device follow the
system's GPU choice by default; wgpu matches `MTLCreateSystemDefaultDevice()`
rather than taking its first Metal adapter. Windows and Linux keep each call
site's previous preference unless the variable below names a GPU.
`CAP_GPU_PREFERENCE=high` (or `performance`/`discrete`) forces the
high-performance GPU, and `low` (or `integrated`/`battery`) forces the integrated
GPU.

The first build takes about 11 minutes and 6.4 GB of `target/`: gpui pulls its
own revisions of the wgpu and font stacks. After that, an edit rebuilds only
the app crate, at `opt-level = 0` and incrementally, in about 22s with plain
Expand Down Expand Up @@ -797,8 +805,10 @@ sampling UV, the legacy page flips the canvas with `scaleX(-1)`; neither app
ever mirrors the recorded camera track).

Background blur runs live on preview frames through `src/camera_blur.rs`: a
dedicated `camera-blur` thread owns its own wgpu device (requested the way the
Tauri preview requests one — LowPower, downlevel limits) and runs the exact
dedicated `camera-blur` thread owns its own wgpu device (requested through
`cap_rendering::gpu_power_preference` like every other renderer and encoder —
system GPU by default on macOS, `LowPower` elsewhere, downlevel limits) and runs
the exact
`cap_camera_effects::BlurProcessor` the recording's project config points the
editor at, with the Tauri preview's 640×360 input cap and 150ms inference
interval. The converter downscales to the cap inside the same hardware
Expand Down
46 changes: 46 additions & 0 deletions apps/desktop-gpui/patches/zed-gpu-preference.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
diff --git a/crates/gpui_macos/src/metal_renderer.rs b/crates/gpui_macos/src/metal_renderer.rs
--- a/crates/gpui_macos/src/metal_renderer.rs
+++ b/crates/gpui_macos/src/metal_renderer.rs
@@ -246,14 +246,34 @@
}

fn create_device() -> Result<metal::Device> {
- // Prefer low‐power integrated GPUs on Intel Mac. On Apple
- // Silicon, there is only ever one GPU, so this is equivalent to
- // `metal::Device::system_default()`.
- if let Some(d) = metal::Device::all()
- .into_iter()
- .min_by_key(|d| (d.is_removable(), !d.is_low_power()))
- {
- Ok(d)
+ let preference = std::env::var("CAP_GPU_PREFERENCE")
+ .unwrap_or_default()
+ .to_ascii_lowercase();
+ let prefer_low_power = matches!(preference.as_str(), "low" | "integrated" | "battery");
+ let prefer_high_performance =
+ matches!(preference.as_str(), "high" | "performance" | "discrete");
+ if !prefer_low_power && !prefer_high_performance {
+ if let Some(device) = metal::Device::system_default() {
+ log::info!("selected system Metal device: {}", device.name());
+ return Ok(device);
+ }
+ }
+ if let Some(device) = metal::Device::all().into_iter().min_by_key(|device| {
+ (
+ if prefer_low_power {
+ !device.is_low_power()
+ } else {
+ device.is_low_power()
+ },
+ device.is_removable(),
+ )
+ }) {
+ log::info!(
+ "selected Metal device: {} (low_power={})",
+ device.name(),
+ device.is_low_power()
+ );
+ Ok(device)
} else {
// For some reason `all()` can return an empty list, see https://github.com/zed-industries/zed/issues/37689
// In that case, we fall back to the system default device.
25 changes: 12 additions & 13 deletions apps/desktop-gpui/src/camera_blur.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,25 +176,24 @@ pub fn run(jobs: flume::Receiver<BlurJob>, results: flume::Sender<BlurOutput>) {
/// One device + queue for the app's blur lifetime, latched like Tauri's
/// `blur_processor_init_attempted`: a failed bring-up is not retried on every
/// toggle. The Tauri preview keeps its device for the window's lifetime
/// (`camera.rs:573-583`, `LowPower`, downlevel limits); only the ONNX/blur
/// resources are per-enable.
/// (`camera.rs:573-583`, downlevel limits); only the ONNX/blur resources are
/// per-enable.
fn shared_device(runtime: &tokio::runtime::Runtime) -> Option<(wgpu::Device, wgpu::Queue)> {
static DEVICE: OnceLock<Option<(wgpu::Device, wgpu::Queue)>> = OnceLock::new();
DEVICE
.get_or_init(|| {
runtime.block_on(async {
let instance = cap_rendering::create_wgpu_instance_sync();
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::LowPower,
force_fallback_adapter: cap_rendering::force_software_wgpu_adapter(),
compatible_surface: None,
})
.await
.map_err(|error| {
tracing::warn!("camera blur adapter unavailable: {error}");
})
.ok()?;
let adapter = cap_rendering::request_wgpu_adapter(
&instance,
wgpu::PowerPreference::LowPower,
cap_rendering::force_software_wgpu_adapter(),
)
.await
.map_err(|error| {
tracing::warn!("camera blur adapter unavailable: {error}");
})
.ok()?;
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor {
label: Some("Camera Preview Blur"),
Expand Down
15 changes: 7 additions & 8 deletions apps/desktop-gpui/src/camera_blur_portable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,13 @@ impl PortableCameraBlur {
.context("camera blur runtime")?;
let (device, queue) = runtime.block_on(async {
let instance = cap_rendering::create_wgpu_instance_sync();
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::LowPower,
force_fallback_adapter: cap_rendering::force_software_wgpu_adapter(),
compatible_surface: None,
})
.await
.map_err(|error| anyhow!("camera blur adapter: {error}"))?;
let adapter = cap_rendering::request_wgpu_adapter(
&instance,
wgpu::PowerPreference::LowPower,
cap_rendering::force_software_wgpu_adapter(),
)
.await
.map_err(|error| anyhow!("camera blur adapter: {error}"))?;
adapter
.request_device(&wgpu::DeviceDescriptor {
label: Some("Camera Preview Blur"),
Expand Down
147 changes: 135 additions & 12 deletions crates/rendering/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,80 @@ pub fn force_software_wgpu_adapter() -> bool {
})
}

fn parse_gpu_power_preference(value: &str) -> Option<wgpu::PowerPreference> {
match value.trim().to_ascii_lowercase().as_str() {
"low" | "integrated" | "battery" => Some(wgpu::PowerPreference::LowPower),
"high" | "performance" | "discrete" => Some(wgpu::PowerPreference::HighPerformance),
_ => None,
}
}

fn configured_gpu_power_preference() -> Option<wgpu::PowerPreference> {
let value = std::env::var("CAP_GPU_PREFERENCE").ok()?;
parse_gpu_power_preference(&value)
}

fn resolve_gpu_power_preference(
configured: Option<wgpu::PowerPreference>,
default: wgpu::PowerPreference,
) -> wgpu::PowerPreference {
match configured {
Some(preference) => preference,
None if cfg!(target_os = "macos") => wgpu::PowerPreference::None,
None => default,
}
}

/// `CAP_GPU_PREFERENCE` selects the physical GPU when more than one is present.
/// `low` picks the integrated GPU and `high` picks the high-performance (usually
/// discrete) GPU. When it is unset (or `system`/`auto`), macOS follows the
/// system GPU and every other platform keeps the `default` its call site used
/// before this override existed.
pub fn gpu_power_preference(default: wgpu::PowerPreference) -> wgpu::PowerPreference {
resolve_gpu_power_preference(configured_gpu_power_preference(), default)
}

#[cfg(target_os = "macos")]
fn system_metal_adapter(instance: &wgpu::Instance) -> Option<wgpu::Adapter> {
let name = metal::Device::system_default()?.name().to_string();
let adapter = instance
.enumerate_adapters(wgpu::Backends::METAL)
.into_iter()
.find(|adapter| adapter.get_info().name == name);
if adapter.is_some() {
tracing::info!(adapter_name = %name, "Following the system Metal device");
} else {
tracing::warn!(adapter_name = %name, "System Metal device is not available through wgpu");
}
adapter
}

/// Requests a hardware adapter, or a fallback adapter when
/// `force_fallback_adapter` is set. On macOS with no override, the adapter
/// matching the system Metal device is used so Cap follows the GPU macOS is
/// currently driving.
pub async fn request_wgpu_adapter(
instance: &wgpu::Instance,
default: wgpu::PowerPreference,
force_fallback_adapter: bool,
) -> Result<wgpu::Adapter, wgpu::RequestAdapterError> {
#[cfg(target_os = "macos")]
if !force_fallback_adapter
&& configured_gpu_power_preference().is_none()
&& let Some(adapter) = system_metal_adapter(instance)
{
return Ok(adapter);
}

instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: gpu_power_preference(default),
force_fallback_adapter,
compatible_surface: None,
})
.await
}

pub fn create_wgpu_instance_sync() -> wgpu::Instance {
#[cfg(not(target_os = "windows"))]
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::default());
Expand Down Expand Up @@ -285,12 +359,7 @@ pub async fn probe_software_adapter() -> Option<(bool, String)> {
let hardware_adapter = if force_software_adapter {
None
} else {
instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
force_fallback_adapter: false,
compatible_surface: None,
})
request_wgpu_adapter(&instance, wgpu::PowerPreference::HighPerformance, false)
.await
.ok()
};
Expand Down Expand Up @@ -2497,12 +2566,7 @@ pub async fn create_shared_device() -> Result<SharedWgpuDevice, RenderingError>
let hardware_adapter = if force_software_adapter {
None
} else {
instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
force_fallback_adapter: false,
compatible_surface: None,
})
request_wgpu_adapter(&instance, wgpu::PowerPreference::HighPerformance, false)
.await
.ok()
};
Expand Down Expand Up @@ -5207,6 +5271,65 @@ impl ProjectUniforms {
mod tests {
use super::*;

#[test]
fn gpu_power_preference_defaults_follow_the_platform() {
let high_performance = wgpu::PowerPreference::HighPerformance;
let low_power = wgpu::PowerPreference::LowPower;
let expected = |default| {
if cfg!(target_os = "macos") {
wgpu::PowerPreference::None
} else {
default
}
};
for value in [None, Some(""), Some("system"), Some("auto"), Some("System")] {
let configured = value.and_then(parse_gpu_power_preference);
assert_eq!(
resolve_gpu_power_preference(configured, high_performance),
expected(high_performance),
"{value:?}"
);
assert_eq!(
resolve_gpu_power_preference(configured, low_power),
expected(low_power),
"{value:?}"
);
}
}

#[cfg(target_os = "macos")]
#[test]
fn system_metal_adapter_matches_the_system_default_device() {
let instance = create_wgpu_instance_sync();
let system = metal::Device::system_default().expect("system Metal device");
let adapter = system_metal_adapter(&instance).expect("system Metal adapter");
assert_eq!(adapter.get_info().name, system.name());
}

#[test]
fn gpu_power_preference_honors_overrides() {
for value in ["low", "integrated", "battery", "LOW"] {
assert_eq!(
resolve_gpu_power_preference(
parse_gpu_power_preference(value),
wgpu::PowerPreference::HighPerformance
),
wgpu::PowerPreference::LowPower,
"{value}"
);
}
for value in ["high", "performance", "discrete", "HIGH"] {
assert_eq!(
resolve_gpu_power_preference(
parse_gpu_power_preference(value),
wgpu::PowerPreference::LowPower
),
wgpu::PowerPreference::HighPerformance,
"{value}"
);
}
}

#[test]
fn camera_blur_output_rejects_missing_pending_and_failed_masks() {
use cap_camera_effects::{BlurFailure, BlurMaskStatus, BlurMode, BlurOutputStatus};
Expand Down
1 change: 1 addition & 0 deletions scripts/prepare-gpui-dependency.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ patch_paths=(
"apps/desktop-gpui/patches/zed-windows.patch"
"apps/desktop-gpui/patches/zed-linux.patch"
"apps/desktop-gpui/patches/zed-idle-frames.patch"
"apps/desktop-gpui/patches/zed-gpu-preference.patch"
)
patch_files=("${patch_paths[@]/#/$repo_root/}")
base_revision="5d1f83d9f27a19bec1fb241dc33b42238af9cf8d"
Expand Down
7 changes: 7 additions & 0 deletions scripts/prepare-gpui-dependency.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ test("dependency verification handles overlapping patches without changing the c
writeFileSync(path.join(upstream, "renderer.rs"), "base\n");
writeFileSync(path.join(upstream, "windows.rs"), "base\n");
writeFileSync(path.join(upstream, "frames.rs"), "base\n");
writeFileSync(path.join(upstream, "gpu.rs"), "base\n");
commit();
const base = git(upstream, "rev-parse", "HEAD").trim();
const patch = (name) => {
Expand All @@ -142,6 +143,8 @@ test("dependency verification handles overlapping patches without changing the c
patch("zed-linux.patch");
writeFileSync(path.join(upstream, "frames.rs"), "paced frames\n");
patch("zed-idle-frames.patch");
writeFileSync(path.join(upstream, "gpu.rs"), "high performance GPU\n");
patch("zed-gpu-preference.patch");
const script = path.join(cap, "scripts/prepare-gpui-dependency.sh");
writeFileSync(
script,
Expand All @@ -165,6 +168,10 @@ test("dependency verification handles overlapping patches without changing the c
readFileSync(path.join(zed, "frames.rs"), "utf8"),
"paced frames\n",
);
assert.equal(
readFileSync(path.join(zed, "gpu.rs"), "utf8"),
"high performance GPU\n",
);
});

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