wGPU: Select more specifically (#50528)

Conrad Irwin and John Tur created

This uses the compositor hints if available to pick the best GPU. If
none is
available, it tries each GPU in turn, and the first that actually works
is chosen

Release Notes:

- Linux: Select a more appropriate GPU

---------

Co-authored-by: John Tur <john-tur@outlook.com>

Change summary

crates/gpui_linux/Cargo.toml                  |   1 
crates/gpui_linux/src/linux/platform.rs       |  40 +++
crates/gpui_linux/src/linux/wayland/client.rs |  74 +++++
crates/gpui_linux/src/linux/wayland/window.rs |   7 
crates/gpui_linux/src/linux/x11/client.rs     |  36 ++
crates/gpui_linux/src/linux/x11/window.rs     |   7 
crates/gpui_wgpu/src/wgpu_context.rs          | 256 ++++++++++++++------
crates/gpui_wgpu/src/wgpu_renderer.rs         |  27 +
crates/zed/src/main.rs                        |   2 
9 files changed, 357 insertions(+), 93 deletions(-)

Detailed changes

crates/gpui_linux/Cargo.toml 🔗

@@ -127,6 +127,7 @@ x11rb = { version = "0.13.1", features = [
     "cursor",
     "resource_manager",
     "sync",
+    "dri3",
 ], optional = true }
 # WARNING: If you change this, you must also publish a new version of zed-xim to crates.io
 xim = { git = "https://github.com/zed-industries/xim-rs.git", rev = "16f35a2c881b815a2b6cdfd6687988e84f8447d8", features = [

crates/gpui_linux/src/linux/platform.rs 🔗

@@ -1036,6 +1036,46 @@ pub(super) fn capslock_from_xkb(keymap_state: &State) -> gpui::Capslock {
     gpui::Capslock { on }
 }
 
+/// Resolve a Linux `dev_t` to PCI vendor/device IDs via sysfs, returning a
+/// [`CompositorGpuHint`] that the GPU adapter selection code can use to
+/// prioritize the compositor's rendering device.
+#[cfg(any(feature = "wayland", feature = "x11"))]
+pub(super) fn compositor_gpu_hint_from_dev_t(dev: u64) -> Option<gpui_wgpu::CompositorGpuHint> {
+    fn dev_major(dev: u64) -> u32 {
+        ((dev >> 8) & 0xfff) as u32 | (((dev >> 32) & !0xfff) as u32)
+    }
+
+    fn dev_minor(dev: u64) -> u32 {
+        (dev & 0xff) as u32 | (((dev >> 12) & !0xff) as u32)
+    }
+
+    fn read_sysfs_hex_id(path: &str) -> Option<u32> {
+        let content = std::fs::read_to_string(path).ok()?;
+        let trimmed = content.trim().strip_prefix("0x").unwrap_or(content.trim());
+        u32::from_str_radix(trimmed, 16).ok()
+    }
+
+    let major = dev_major(dev);
+    let minor = dev_minor(dev);
+
+    let vendor_path = format!("/sys/dev/char/{major}:{minor}/device/vendor");
+    let device_path = format!("/sys/dev/char/{major}:{minor}/device/device");
+
+    let vendor_id = read_sysfs_hex_id(&vendor_path)?;
+    let device_id = read_sysfs_hex_id(&device_path)?;
+
+    log::info!(
+        "Compositor GPU hint: vendor={:#06x}, device={:#06x} (from dev {major}:{minor})",
+        vendor_id,
+        device_id,
+    );
+
+    Some(gpui_wgpu::CompositorGpuHint {
+        vendor_id,
+        device_id,
+    })
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;

crates/gpui_linux/src/linux/wayland/client.rs 🔗

@@ -95,7 +95,10 @@ use gpui::{
     ScrollDelta, ScrollWheelEvent, SharedString, Size, TaskTiming, TouchPhase, WindowParams, point,
     profiler, px, size,
 };
-use gpui_wgpu::WgpuContext;
+use gpui_wgpu::{CompositorGpuHint, WgpuContext};
+use wayland_protocols::wp::linux_dmabuf::zv1::client::{
+    zwp_linux_dmabuf_feedback_v1, zwp_linux_dmabuf_v1,
+};
 
 /// Used to convert evdev scancode to xkb scancode
 const MIN_KEYCODE: u32 = 8;
@@ -202,6 +205,7 @@ pub(crate) struct WaylandClientState {
     serial_tracker: SerialTracker,
     globals: Globals,
     pub gpu_context: Option<WgpuContext>,
+    pub compositor_gpu: Option<CompositorGpuHint>,
     wl_seat: wl_seat::WlSeat, // TODO: Multi seat support
     wl_pointer: Option<wl_pointer::WlPointer>,
     wl_keyboard: Option<wl_keyboard::WlKeyboard>,
@@ -515,6 +519,7 @@ impl WaylandClient {
             })
             .unwrap();
 
+        let compositor_gpu = detect_compositor_gpu();
         let gpu_context = None;
 
         let seat = seat.unwrap();
@@ -571,6 +576,7 @@ impl WaylandClient {
             serial_tracker: SerialTracker::new(),
             globals,
             gpu_context,
+            compositor_gpu,
             wl_seat: seat,
             wl_pointer: None,
             wl_keyboard: None,
@@ -715,10 +721,12 @@ impl LinuxClient for WaylandClient {
         let parent = state.keyboard_focused_window.clone();
 
         let appearance = state.common.appearance;
+        let compositor_gpu = state.compositor_gpu.take();
         let (window, surface_id) = WaylandWindow::new(
             handle,
             state.globals.clone(),
             &mut state.gpu_context,
+            compositor_gpu,
             WaylandClientStatePtr(Rc::downgrade(&self.0)),
             params,
             appearance,
@@ -904,6 +912,70 @@ impl LinuxClient for WaylandClient {
     }
 }
 
+struct DmabufProbeState {
+    device: Option<u64>,
+}
+
+impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for DmabufProbeState {
+    fn event(
+        _: &mut Self,
+        _: &wl_registry::WlRegistry,
+        _: wl_registry::Event,
+        _: &GlobalListContents,
+        _: &Connection,
+        _: &QueueHandle<Self>,
+    ) {
+    }
+}
+
+impl Dispatch<zwp_linux_dmabuf_v1::ZwpLinuxDmabufV1, ()> for DmabufProbeState {
+    fn event(
+        _: &mut Self,
+        _: &zwp_linux_dmabuf_v1::ZwpLinuxDmabufV1,
+        _: zwp_linux_dmabuf_v1::Event,
+        _: &(),
+        _: &Connection,
+        _: &QueueHandle<Self>,
+    ) {
+    }
+}
+
+impl Dispatch<zwp_linux_dmabuf_feedback_v1::ZwpLinuxDmabufFeedbackV1, ()> for DmabufProbeState {
+    fn event(
+        state: &mut Self,
+        _: &zwp_linux_dmabuf_feedback_v1::ZwpLinuxDmabufFeedbackV1,
+        event: zwp_linux_dmabuf_feedback_v1::Event,
+        _: &(),
+        _: &Connection,
+        _: &QueueHandle<Self>,
+    ) {
+        if let zwp_linux_dmabuf_feedback_v1::Event::MainDevice { device } = event {
+            if let Ok(bytes) = <[u8; 8]>::try_from(device.as_slice()) {
+                state.device = Some(u64::from_ne_bytes(bytes));
+            }
+        }
+    }
+}
+
+fn detect_compositor_gpu() -> Option<CompositorGpuHint> {
+    let connection = Connection::connect_to_env().ok()?;
+    let (globals, mut event_queue) = registry_queue_init::<DmabufProbeState>(&connection).ok()?;
+    let queue_handle = event_queue.handle();
+
+    let dmabuf: zwp_linux_dmabuf_v1::ZwpLinuxDmabufV1 =
+        globals.bind(&queue_handle, 4..=4, ()).ok()?;
+    let feedback = dmabuf.get_default_feedback(&queue_handle, ());
+
+    let mut state = DmabufProbeState { device: None };
+
+    event_queue.roundtrip(&mut state).ok()?;
+
+    feedback.destroy();
+    dmabuf.destroy();
+
+    crate::linux::compositor_gpu_hint_from_dev_t(state.device?)
+}
+
 impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for WaylandClientStatePtr {
     fn event(
         this: &mut Self,

crates/gpui_linux/src/linux/wayland/window.rs 🔗

@@ -34,7 +34,7 @@ use gpui::{
     WindowDecorations, WindowKind, WindowParams, layer_shell::LayerShellNotSupportedError, px,
     size,
 };
-use gpui_wgpu::{WgpuContext, WgpuRenderer, WgpuSurfaceConfig};
+use gpui_wgpu::{CompositorGpuHint, WgpuContext, WgpuRenderer, WgpuSurfaceConfig};
 
 #[derive(Default)]
 pub(crate) struct Callbacks {
@@ -318,6 +318,7 @@ impl WaylandWindowState {
         client: WaylandClientStatePtr,
         globals: Globals,
         gpu_context: &mut Option<WgpuContext>,
+        compositor_gpu: Option<CompositorGpuHint>,
         options: WindowParams,
         parent: Option<WaylandWindowStatePtr>,
     ) -> anyhow::Result<Self> {
@@ -338,7 +339,7 @@ impl WaylandWindowState {
                 },
                 transparent: true,
             };
-            WgpuRenderer::new(gpu_context, &raw_window, config)?
+            WgpuRenderer::new(gpu_context, &raw_window, config, compositor_gpu)?
         };
 
         if let WaylandSurfaceState::Xdg(ref xdg_state) = surface_state {
@@ -488,6 +489,7 @@ impl WaylandWindow {
         handle: AnyWindowHandle,
         globals: Globals,
         gpu_context: &mut Option<WgpuContext>,
+        compositor_gpu: Option<CompositorGpuHint>,
         client: WaylandClientStatePtr,
         params: WindowParams,
         appearance: WindowAppearance,
@@ -515,6 +517,7 @@ impl WaylandWindow {
                 client,
                 globals,
                 gpu_context,
+                compositor_gpu,
                 params,
                 parent,
             )?)),

crates/gpui_linux/src/linux/x11/client.rs 🔗

@@ -31,7 +31,7 @@ use x11rb::{
         AtomEnum, ChangeWindowAttributesAux, ClientMessageData, ClientMessageEvent,
         ConnectionExt as _, EventMask, ModMask, Visibility,
     },
-    protocol::{Event, randr, render, xinput, xkb, xproto},
+    protocol::{Event, dri3, randr, render, xinput, xkb, xproto},
     resource_manager::Database,
     wrapper::ConnectionExt as _,
     xcb_ffi::XCBConnection,
@@ -64,7 +64,7 @@ use gpui::{
     PlatformKeyboardLayout, PlatformWindow, Point, RequestFrameOptions, ScrollDelta, Size,
     TouchPhase, WindowParams, point, px,
 };
-use gpui_wgpu::WgpuContext;
+use gpui_wgpu::{CompositorGpuHint, WgpuContext};
 
 /// Value for DeviceId parameters which selects all devices.
 pub(crate) const XINPUT_ALL_DEVICES: xinput::DeviceId = 0;
@@ -178,6 +178,7 @@ pub struct X11ClientState {
     pub(crate) current_count: usize,
 
     pub(crate) gpu_context: Option<WgpuContext>,
+    pub(crate) compositor_gpu: Option<CompositorGpuHint>,
 
     pub(crate) scale_factor: f32,
 
@@ -430,6 +431,9 @@ impl X11Client {
 
         let clipboard = Clipboard::new().context("Failed to initialize clipboard")?;
 
+        let screen = &xcb_connection.setup().roots[x_root_index];
+        let compositor_gpu = detect_compositor_gpu(&xcb_connection, screen);
+
         let xcb_connection = Rc::new(xcb_connection);
 
         let ximc = X11rbClient::init(Rc::clone(&xcb_connection), x_root_index, None).ok();
@@ -490,6 +494,7 @@ impl X11Client {
             last_location: Point::new(px(0.0), px(0.0)),
             current_count: 0,
             gpu_context: None,
+            compositor_gpu,
             scale_factor,
 
             xkb_context,
@@ -1514,11 +1519,13 @@ impl LinuxClient for X11Client {
         let atoms = state.atoms;
         let scale_factor = state.scale_factor;
         let appearance = state.common.appearance;
+        let compositor_gpu = state.compositor_gpu.take();
         let window = X11Window::new(
             handle,
             X11ClientStatePtr(Rc::downgrade(&self.0)),
             state.common.foreground_executor.clone(),
             &mut state.gpu_context,
+            compositor_gpu,
             params,
             &xcb_connection,
             client_side_decorations_supported,
@@ -1976,7 +1983,30 @@ fn fp3232_to_f32(value: xinput::Fp3232) -> f32 {
     value.integral as f32 + value.frac as f32 / u32::MAX as f32
 }
 
-fn check_compositor_present(xcb_connection: &XCBConnection, root: u32) -> bool {
+fn detect_compositor_gpu(
+    xcb_connection: &XCBConnection,
+    screen: &xproto::Screen,
+) -> Option<CompositorGpuHint> {
+    use std::os::fd::AsRawFd;
+    use std::os::unix::fs::MetadataExt;
+
+    xcb_connection
+        .extension_information(dri3::X11_EXTENSION_NAME)
+        .ok()??;
+
+    let reply = dri3::open(xcb_connection, screen.root, 0)
+        .ok()?
+        .reply()
+        .ok()?;
+    let fd = reply.device_fd;
+
+    let path = format!("/proc/self/fd/{}", fd.as_raw_fd());
+    let metadata = std::fs::metadata(&path).ok()?;
+
+    crate::linux::compositor_gpu_hint_from_dev_t(metadata.rdev())
+}
+
+fn check_compositor_present(xcb_connection: &XCBConnection, root: xproto::Window) -> bool {
     // Method 1: Check for _NET_WM_CM_S{root}
     let atom_name = format!("_NET_WM_CM_S{}", root);
     let atom1 = get_reply(

crates/gpui_linux/src/linux/x11/window.rs 🔗

@@ -9,7 +9,7 @@ use gpui::{
     Tiling, WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControlArea,
     WindowDecorations, WindowKind, WindowParams, px,
 };
-use gpui_wgpu::{WgpuContext, WgpuRenderer, WgpuSurfaceConfig};
+use gpui_wgpu::{CompositorGpuHint, WgpuContext, WgpuRenderer, WgpuSurfaceConfig};
 
 use collections::FxHashSet;
 use raw_window_handle as rwh;
@@ -392,6 +392,7 @@ impl X11WindowState {
         client: X11ClientStatePtr,
         executor: ForegroundExecutor,
         gpu_context: &mut Option<WgpuContext>,
+        compositor_gpu: Option<CompositorGpuHint>,
         params: WindowParams,
         xcb: &Rc<XCBConnection>,
         client_side_decorations_supported: bool,
@@ -679,7 +680,7 @@ impl X11WindowState {
                     // too
                     transparent: false,
                 };
-                WgpuRenderer::new(gpu_context, &raw_window, config)?
+                WgpuRenderer::new(gpu_context, &raw_window, config, compositor_gpu)?
             };
 
             // Set max window size hints based on the GPU's maximum texture dimension.
@@ -803,6 +804,7 @@ impl X11Window {
         client: X11ClientStatePtr,
         executor: ForegroundExecutor,
         gpu_context: &mut Option<WgpuContext>,
+        compositor_gpu: Option<CompositorGpuHint>,
         params: WindowParams,
         xcb: &Rc<XCBConnection>,
         client_side_decorations_supported: bool,
@@ -819,6 +821,7 @@ impl X11Window {
                 client,
                 executor,
                 gpu_context,
+                compositor_gpu,
                 params,
                 xcb,
                 client_side_decorations_supported,

crates/gpui_wgpu/src/wgpu_context.rs 🔗

@@ -10,8 +10,18 @@ pub struct WgpuContext {
     dual_source_blending: bool,
 }
 
+#[cfg(not(target_family = "wasm"))]
+pub struct CompositorGpuHint {
+    pub vendor_id: u32,
+    pub device_id: u32,
+}
+
 impl WgpuContext {
-    pub fn new(instance: wgpu::Instance, surface: &wgpu::Surface<'_>) -> anyhow::Result<Self> {
+    pub fn new(
+        instance: wgpu::Instance,
+        surface: &wgpu::Surface<'_>,
+        compositor_gpu: Option<CompositorGpuHint>,
+    ) -> anyhow::Result<Self> {
         let device_id_filter = match std::env::var("ZED_DEVICE_ID") {
             Ok(val) => parse_pci_id(&val)
                 .context("Failed to parse device ID from `ZED_DEVICE_ID` environment variable")
@@ -24,24 +34,15 @@ impl WgpuContext {
             }
         };
 
-        let adapter = smol::block_on(Self::select_adapter(
-            &instance,
-            device_id_filter,
-            Some(surface),
-        ))?;
-
-        let caps = surface.get_capabilities(&adapter);
-        if caps.formats.is_empty() {
-            let info = adapter.get_info();
-            anyhow::bail!(
-                "No adapter compatible with the display surface could be found. \
-                 Best candidate {:?} (backend={:?}, device={:#06x}) reports no \
-                 supported surface formats.",
-                info.name,
-                info.backend,
-                info.device,
-            );
-        }
+        // Select an adapter by actually testing surface configuration with the real device.
+        // This is the only reliable way to determine compatibility on hybrid GPU systems.
+        let (adapter, device, queue, dual_source_blending) =
+            smol::block_on(Self::select_adapter_and_device(
+                &instance,
+                device_id_filter,
+                surface,
+                compositor_gpu.as_ref(),
+            ))?;
 
         log::info!(
             "Selected GPU adapter: {:?} ({:?})",
@@ -49,8 +50,6 @@ impl WgpuContext {
             adapter.get_info().backend
         );
 
-        let (device, queue, dual_source_blending) = Self::create_device(&adapter)?;
-
         Ok(Self {
             instance,
             adapter,
@@ -84,13 +83,15 @@ impl WgpuContext {
         Ok(())
     }
 
-    fn create_device(adapter: &wgpu::Adapter) -> anyhow::Result<(wgpu::Device, wgpu::Queue, bool)> {
-        let dual_source_blending_available = adapter
+    async fn create_device(
+        adapter: &wgpu::Adapter,
+    ) -> anyhow::Result<(wgpu::Device, wgpu::Queue, bool)> {
+        let dual_source_blending = adapter
             .features()
             .contains(wgpu::Features::DUAL_SOURCE_BLENDING);
 
         let mut required_features = wgpu::Features::empty();
-        if dual_source_blending_available {
+        if dual_source_blending {
             required_features |= wgpu::Features::DUAL_SOURCE_BLENDING;
         } else {
             log::warn!(
@@ -99,8 +100,8 @@ impl WgpuContext {
             );
         }
 
-        let (device, queue) = smol::block_on(
-            adapter.request_device(&wgpu::DeviceDescriptor {
+        let (device, queue) = adapter
+            .request_device(&wgpu::DeviceDescriptor {
                 label: Some("gpui_device"),
                 required_features,
                 required_limits: wgpu::Limits::downlevel_defaults()
@@ -109,76 +110,171 @@ impl WgpuContext {
                 memory_hints: wgpu::MemoryHints::MemoryUsage,
                 trace: wgpu::Trace::Off,
                 experimental_features: wgpu::ExperimentalFeatures::disabled(),
-            }),
-        )
-        .map_err(|e| anyhow::anyhow!("Failed to create wgpu device: {e}"))?;
+            })
+            .await
+            .map_err(|e| anyhow::anyhow!("Failed to create wgpu device: {e}"))?;
 
-        Ok((device, queue, dual_source_blending_available))
+        Ok((device, queue, dual_source_blending))
     }
 
-    async fn select_adapter(
+    /// Select an adapter and create a device, testing that the surface can actually be configured.
+    /// This is the only reliable way to determine compatibility on hybrid GPU systems, where
+    /// adapters may report surface compatibility via get_capabilities() but fail when actually
+    /// configuring (e.g., NVIDIA reporting Vulkan Wayland support but failing because the
+    /// Wayland compositor runs on the Intel GPU).
+    async fn select_adapter_and_device(
         instance: &wgpu::Instance,
         device_id_filter: Option<u32>,
-        compatible_surface: Option<&wgpu::Surface<'_>>,
-    ) -> anyhow::Result<wgpu::Adapter> {
+        surface: &wgpu::Surface<'_>,
+        compositor_gpu: Option<&CompositorGpuHint>,
+    ) -> anyhow::Result<(wgpu::Adapter, wgpu::Device, wgpu::Queue, bool)> {
+        let mut adapters: Vec<_> = instance.enumerate_adapters(wgpu::Backends::all()).await;
+
+        if adapters.is_empty() {
+            anyhow::bail!("No GPU adapters found");
+        }
+
         if let Some(device_id) = device_id_filter {
-            let adapters: Vec<_> = instance.enumerate_adapters(wgpu::Backends::all()).await;
+            log::info!("ZED_DEVICE_ID filter: {:#06x}", device_id);
+        }
 
-            if adapters.is_empty() {
-                anyhow::bail!("No GPU adapters found");
-            }
+        // Sort adapters into a single priority order. Tiers (from highest to lowest):
+        //
+        // 1. ZED_DEVICE_ID match — explicit user override
+        // 2. Compositor GPU match — the GPU the display server is rendering on
+        // 3. Device type — WGPU HighPerformance order (Discrete > Integrated >
+        //    Other > Virtual > Cpu). "Other" ranks above "Virtual" because
+        //    backends like OpenGL may report real hardware as "Other".
+        // 4. Backend — prefer Vulkan/Metal/Dx12 over GL/etc.
+        adapters.sort_by_key(|adapter| {
+            let info = adapter.get_info();
+
+            // Backends like OpenGL report device=0 for all adapters, so
+            // device-based matching is only meaningful when non-zero.
+            let device_known = info.device != 0;
 
-            let mut non_matching_adapter_infos: Vec<wgpu::AdapterInfo> = Vec::new();
-
-            for adapter in adapters.into_iter() {
-                let info = adapter.get_info();
-                if info.device == device_id {
-                    if let Some(surface) = compatible_surface {
-                        let caps = surface.get_capabilities(&adapter);
-                        if caps.formats.is_empty() {
-                            log::warn!(
-                                "GPU matching ZED_DEVICE_ID={:#06x} ({}) is not compatible \
-                                 with the display surface. Falling back to auto-selection.",
-                                device_id,
-                                info.name,
-                            );
-                            break;
-                        }
-                    }
+            let user_override: u8 = match device_id_filter {
+                Some(id) if device_known && info.device == id => 0,
+                _ => 1,
+            };
+
+            let compositor_match: u8 = match compositor_gpu {
+                Some(hint)
+                    if device_known
+                        && info.vendor == hint.vendor_id
+                        && info.device == hint.device_id =>
+                {
+                    0
+                }
+                _ => 1,
+            };
+
+            let type_priority: u8 = match info.device_type {
+                wgpu::DeviceType::DiscreteGpu => 0,
+                wgpu::DeviceType::IntegratedGpu => 1,
+                wgpu::DeviceType::Other => 2,
+                wgpu::DeviceType::VirtualGpu => 3,
+                wgpu::DeviceType::Cpu => 4,
+            };
+
+            let backend_priority: u8 = match info.backend {
+                wgpu::Backend::Vulkan => 0,
+                wgpu::Backend::Metal => 0,
+                wgpu::Backend::Dx12 => 0,
+                _ => 1,
+            };
+
+            (
+                user_override,
+                compositor_match,
+                type_priority,
+                backend_priority,
+            )
+        });
+
+        // Log all available adapters (in sorted order)
+        log::info!("Found {} GPU adapter(s):", adapters.len());
+        for adapter in &adapters {
+            let info = adapter.get_info();
+            log::info!(
+                "  - {} (vendor={:#06x}, device={:#06x}, backend={:?}, type={:?})",
+                info.name,
+                info.vendor,
+                info.device,
+                info.backend,
+                info.device_type,
+            );
+        }
+
+        // Test each adapter by creating a device and configuring the surface
+        for adapter in adapters {
+            let info = adapter.get_info();
+            log::info!("Testing adapter: {} ({:?})...", info.name, info.backend);
+
+            match Self::try_adapter_with_surface(&adapter, surface).await {
+                Ok((device, queue, dual_source_blending)) => {
+                    log::info!(
+                        "Selected GPU (passed configuration test): {} ({:?})",
+                        info.name,
+                        info.backend
+                    );
+                    return Ok((adapter, device, queue, dual_source_blending));
+                }
+                Err(e) => {
                     log::info!(
-                        "Found GPU matching ZED_DEVICE_ID={:#06x}: {}",
-                        device_id,
-                        info.name
+                        "  Adapter {} ({:?}) failed: {}, trying next...",
+                        info.name,
+                        info.backend,
+                        e
                     );
-                    return Ok(adapter);
-                } else {
-                    non_matching_adapter_infos.push(info);
                 }
             }
+        }
 
-            log::warn!(
-                "No compatible GPU found matching ZED_DEVICE_ID={:#06x}. Available devices:",
-                device_id
-            );
+        anyhow::bail!("No GPU adapter found that can configure the display surface")
+    }
 
-            for info in &non_matching_adapter_infos {
-                log::warn!(
-                    "  - {} (device_id={:#06x}, backend={})",
-                    info.name,
-                    info.device,
-                    info.backend
-                );
-            }
+    /// Try to use an adapter with a surface by creating a device and testing configuration.
+    /// Returns the device and queue if successful, allowing them to be reused.
+    #[cfg(not(target_family = "wasm"))]
+    async fn try_adapter_with_surface(
+        adapter: &wgpu::Adapter,
+        surface: &wgpu::Surface<'_>,
+    ) -> anyhow::Result<(wgpu::Device, wgpu::Queue, bool)> {
+        let caps = surface.get_capabilities(adapter);
+        if caps.formats.is_empty() {
+            anyhow::bail!("no compatible surface formats");
+        }
+        if caps.alpha_modes.is_empty() {
+            anyhow::bail!("no compatible alpha modes");
         }
 
-        instance
-            .request_adapter(&wgpu::RequestAdapterOptions {
-                power_preference: wgpu::PowerPreference::HighPerformance,
-                compatible_surface,
-                force_fallback_adapter: false,
-            })
-            .await
-            .map_err(|e| anyhow::anyhow!("Failed to request GPU adapter: {e}"))
+        // Create the real device with full features
+        let (device, queue, dual_source_blending) = Self::create_device(adapter).await?;
+
+        // Use an error scope to capture any validation errors during configure
+        let error_scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
+
+        let test_config = wgpu::SurfaceConfiguration {
+            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
+            format: caps.formats[0],
+            width: 64,
+            height: 64,
+            present_mode: wgpu::PresentMode::Fifo,
+            desired_maximum_frame_latency: 2,
+            alpha_mode: caps.alpha_modes[0],
+            view_formats: vec![],
+        };
+
+        surface.configure(&device, &test_config);
+
+        // Check if there was a validation error
+        let error = error_scope.pop().await;
+        if let Some(e) = error {
+            anyhow::bail!("surface configuration failed: {e}");
+        }
+
+        Ok((device, queue, dual_source_blending))
     }
 
     pub fn supports_dual_source_blending(&self) -> bool {

crates/gpui_wgpu/src/wgpu_renderer.rs 🔗

@@ -1,3 +1,5 @@
+#[cfg(not(target_family = "wasm"))]
+use crate::CompositorGpuHint;
 use crate::{WgpuAtlas, WgpuContext};
 use bytemuck::{Pod, Zeroable};
 use gpui::{
@@ -95,6 +97,7 @@ pub struct WgpuRenderer {
     queue: Arc<wgpu::Queue>,
     surface: wgpu::Surface<'static>,
     surface_config: wgpu::SurfaceConfiguration,
+    surface_configured: bool,
     pipelines: WgpuPipelines,
     bind_group_layouts: WgpuBindGroupLayouts,
     atlas: Arc<WgpuAtlas>,
@@ -131,6 +134,7 @@ impl WgpuRenderer {
         gpu_context: &mut Option<WgpuContext>,
         window: &W,
         config: WgpuSurfaceConfig,
+        compositor_gpu: Option<CompositorGpuHint>,
     ) -> anyhow::Result<Self> {
         let window_handle = window
             .window_handle()
@@ -166,9 +170,17 @@ impl WgpuRenderer {
                 context.check_compatible_with_surface(&surface)?;
                 context
             }
-            None => gpu_context.insert(WgpuContext::new(instance, &surface)?),
+            None => gpu_context.insert(WgpuContext::new(instance, &surface, compositor_gpu)?),
         };
 
+        Self::new_with_surface(context, surface, config)
+    }
+
+    fn new_with_surface(
+        context: &WgpuContext,
+        surface: wgpu::Surface<'static>,
+        config: WgpuSurfaceConfig,
+    ) -> anyhow::Result<Self> {
         let surface_caps = surface.get_capabilities(&context.adapter);
         let preferred_formats = [
             wgpu::TextureFormat::Bgra8Unorm,
@@ -244,6 +256,8 @@ impl WgpuRenderer {
             alpha_mode,
             view_formats: vec![],
         };
+        // Configure the surface immediately. The adapter selection process already validated
+        // that this adapter can successfully configure this surface.
         surface.configure(&context.device, &surface_config);
 
         let queue = Arc::clone(&context.queue);
@@ -350,6 +364,7 @@ impl WgpuRenderer {
             queue,
             surface,
             surface_config,
+            surface_configured: true,
             pipelines,
             bind_group_layouts,
             atlas,
@@ -819,7 +834,9 @@ impl WgpuRenderer {
 
             self.surface_config.width = clamped_width.max(1);
             self.surface_config.height = clamped_height.max(1);
-            self.surface.configure(&self.device, &self.surface_config);
+            if self.surface_configured {
+                self.surface.configure(&self.device, &self.surface_config);
+            }
 
             // Invalidate intermediate textures - they will be lazily recreated
             // in draw() after we confirm the surface is healthy. This avoids
@@ -870,7 +887,9 @@ impl WgpuRenderer {
 
         if new_alpha_mode != self.surface_config.alpha_mode {
             self.surface_config.alpha_mode = new_alpha_mode;
-            self.surface.configure(&self.device, &self.surface_config);
+            if self.surface_configured {
+                self.surface.configure(&self.device, &self.surface_config);
+            }
             self.pipelines = Self::create_pipelines(
                 &self.device,
                 &self.bind_group_layouts,
@@ -927,7 +946,7 @@ impl WgpuRenderer {
         let frame = match self.surface.get_current_texture() {
             Ok(frame) => frame,
             Err(wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated) => {
-                self.surface.configure(&self.device, &self.surface_config);
+                self.surface_configured = false;
                 return;
             }
             Err(e) => {

crates/zed/src/main.rs 🔗

@@ -276,7 +276,7 @@ fn main() {
 
     zlog::init();
 
-    if stdout_is_a_pty() {
+    if true {
         zlog::init_output_stdout();
     } else {
         let result = zlog::init_output_file(paths::log_file(), Some(paths::old_log_file()));