Defer wgpu context creation until we have a surface (#49926)

Conrad Irwin and Zed Zippy created

Fixes ZED-54X

Release Notes:

- Linux: wait to request a graphics context until we have a window so we
can (ideally) pick a better context or (less ideally) fail more
gracefully.

---------

Co-authored-by: Zed Zippy <234243425+zed-zippy[bot]@users.noreply.github.com>

Change summary

crates/editor/src/editor.rs                   |  4 
crates/gpui_linux/src/linux/platform.rs       | 44 ----------
crates/gpui_linux/src/linux/wayland/client.rs | 18 ++--
crates/gpui_linux/src/linux/wayland/window.rs |  9 +
crates/gpui_linux/src/linux/x11/client.rs     | 33 ++++---
crates/gpui_linux/src/linux/x11/window.rs     |  9 +
crates/gpui_wgpu/src/wgpu_context.rs          | 89 ++++++++++++++++----
crates/gpui_wgpu/src/wgpu_renderer.rs         | 46 ++++++++--
crates/zed/src/zed.rs                         |  2 
crates/zed/src/zed/open_listener.rs           |  2 
10 files changed, 149 insertions(+), 107 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -360,7 +360,7 @@ pub fn init(cx: &mut App) {
                     Editor::new_file(workspace, &Default::default(), window, cx)
                 },
             )
-            .detach();
+            .detach_and_log_err(cx);
         }
     })
     .on_action(move |_: &workspace::NewWindow, cx| {
@@ -375,7 +375,7 @@ pub fn init(cx: &mut App) {
                     Editor::new_file(workspace, &Default::default(), window, cx)
                 },
             )
-            .detach();
+            .detach_and_log_err(cx);
         }
     });
     _ = ui_input::ERASED_EDITOR_FACTORY.set(|window, cx| {

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

@@ -46,50 +46,6 @@ pub(crate) const KEYRING_LABEL: &str = "zed-github-account";
 const FILE_PICKER_PORTAL_MISSING: &str =
     "Couldn't open file picker due to missing xdg-desktop-portal implementation.";
 
-#[cfg(any(feature = "x11", feature = "wayland"))]
-pub trait ResultExt {
-    type Ok;
-
-    fn notify_err(self, msg: &'static str) -> Self::Ok;
-}
-
-#[cfg(any(feature = "x11", feature = "wayland"))]
-impl<T> ResultExt for anyhow::Result<T> {
-    type Ok = T;
-
-    fn notify_err(self, msg: &'static str) -> T {
-        match self {
-            Ok(v) => v,
-            Err(e) => {
-                use ashpd::desktop::notification::{Notification, NotificationProxy, Priority};
-                use futures::executor::block_on;
-
-                let proxy = block_on(NotificationProxy::new()).expect(msg);
-
-                let notification_id = "dev.zed.Oops";
-                block_on(
-                    proxy.add_notification(
-                        notification_id,
-                        Notification::new("Zed failed to launch")
-                            .body(Some(
-                                format!(
-                                    "{e:?}. See https://zed.dev/docs/linux for troubleshooting steps."
-                                )
-                                .as_str(),
-                            ))
-                            .priority(Priority::High)
-                            .icon(ashpd::desktop::Icon::with_names(&[
-                                "dialog-question-symbolic",
-                            ])),
-                    )
-                ).expect(msg);
-
-                panic!("{msg}");
-            }
-        }
-    }
-}
-
 pub(crate) trait LinuxClient {
     fn compositor_name(&self) -> &'static str;
     fn with_common<R>(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R;

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

@@ -74,10 +74,10 @@ use super::{
 };
 
 use crate::linux::{
-    DOUBLE_CLICK_INTERVAL, LinuxClient, LinuxCommon, LinuxKeyboardLayout, ResultExt as _,
-    SCROLL_LINES, capslock_from_xkb, cursor_style_to_icon_names, get_xkb_compose_state,
-    is_within_click_distance, keystroke_from_xkb, keystroke_underlying_dead_key,
-    modifiers_from_xkb, open_uri_internal, read_fd, reveal_path_internal,
+    DOUBLE_CLICK_INTERVAL, LinuxClient, LinuxCommon, LinuxKeyboardLayout, SCROLL_LINES,
+    capslock_from_xkb, cursor_style_to_icon_names, get_xkb_compose_state, is_within_click_distance,
+    keystroke_from_xkb, keystroke_underlying_dead_key, modifiers_from_xkb, open_uri_internal,
+    read_fd, reveal_path_internal,
     wayland::{
         clipboard::{Clipboard, DataOffer, FILE_LIST_MIME_TYPE, TEXT_MIME_TYPES},
         cursor::Cursor,
@@ -201,7 +201,7 @@ pub struct Output {
 pub(crate) struct WaylandClientState {
     serial_tracker: SerialTracker,
     globals: Globals,
-    pub gpu_context: WgpuContext,
+    pub gpu_context: Option<WgpuContext>,
     wl_seat: wl_seat::WlSeat, // TODO: Multi seat support
     wl_pointer: Option<wl_pointer::WlPointer>,
     wl_keyboard: Option<wl_keyboard::WlKeyboard>,
@@ -515,8 +515,7 @@ impl WaylandClient {
             })
             .unwrap();
 
-        // This could be unified with the notification handling in zed/main:fail_to_open_window.
-        let gpu_context = WgpuContext::new().notify_err("Unable to init GPU context");
+        let gpu_context = None;
 
         let seat = seat.unwrap();
         let globals = Globals::new(
@@ -715,13 +714,14 @@ impl LinuxClient for WaylandClient {
 
         let parent = state.keyboard_focused_window.clone();
 
+        let appearance = state.common.appearance;
         let (window, surface_id) = WaylandWindow::new(
             handle,
             state.globals.clone(),
-            &state.gpu_context,
+            &mut state.gpu_context,
             WaylandClientStatePtr(Rc::downgrade(&self.0)),
             params,
-            state.common.appearance,
+            appearance,
             parent,
         )?;
         state.windows.insert(surface_id, window.0.clone());

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

@@ -317,7 +317,7 @@ impl WaylandWindowState {
         viewport: Option<wp_viewport::WpViewport>,
         client: WaylandClientStatePtr,
         globals: Globals,
-        gpu_context: &WgpuContext,
+        gpu_context: &mut Option<WgpuContext>,
         options: WindowParams,
         parent: Option<WaylandWindowStatePtr>,
     ) -> anyhow::Result<Self> {
@@ -481,7 +481,7 @@ impl WaylandWindow {
     pub fn new(
         handle: AnyWindowHandle,
         globals: Globals,
-        gpu_context: &WgpuContext,
+        gpu_context: &mut Option<WgpuContext>,
         client: WaylandClientStatePtr,
         params: WindowParams,
         appearance: WindowAppearance,
@@ -1230,7 +1230,10 @@ impl PlatformWindow for WaylandWindow {
     fn is_subpixel_rendering_supported(&self) -> bool {
         let client = self.borrow().client.get_client();
         let state = client.borrow();
-        state.gpu_context.supports_dual_source_blending()
+        state
+            .gpu_context
+            .as_ref()
+            .is_some_and(|ctx| ctx.supports_dual_source_blending())
     }
 
     fn minimize(&self) {

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

@@ -49,10 +49,9 @@ use super::{
 };
 
 use crate::linux::{
-    DEFAULT_CURSOR_ICON_NAME, LinuxClient, ResultExt as _, capslock_from_xkb,
-    cursor_style_to_icon_names, get_xkb_compose_state, is_within_click_distance,
-    keystroke_from_xkb, keystroke_underlying_dead_key, log_cursor_icon_warning, modifiers_from_xkb,
-    open_uri_internal,
+    DEFAULT_CURSOR_ICON_NAME, LinuxClient, capslock_from_xkb, cursor_style_to_icon_names,
+    get_xkb_compose_state, is_within_click_distance, keystroke_from_xkb,
+    keystroke_underlying_dead_key, log_cursor_icon_warning, modifiers_from_xkb, open_uri_internal,
     platform::{DOUBLE_CLICK_INTERVAL, SCROLL_LINES},
     reveal_path_internal,
     xdg_desktop_portal::{Event as XDPEvent, XDPEventSource},
@@ -178,7 +177,7 @@ pub struct X11ClientState {
     pub(crate) last_location: Point<Pixels>,
     pub(crate) current_count: usize,
 
-    pub(crate) gpu_context: WgpuContext,
+    pub(crate) gpu_context: Option<WgpuContext>,
 
     pub(crate) scale_factor: f32,
 
@@ -421,8 +420,6 @@ impl X11Client {
             .to_string();
         let keyboard_layout = LinuxKeyboardLayout::new(layout_name.into());
 
-        let gpu_context = WgpuContext::new().notify_err("Unable to init GPU context");
-
         let resource_database = x11rb::resource_manager::new_from_default(&xcb_connection)
             .context("Failed to create resource database")?;
         let scale_factor = get_scale_factor(&xcb_connection, &resource_database, x_root_index);
@@ -492,7 +489,7 @@ impl X11Client {
             last_mouse_button: None,
             last_location: Point::new(px(0.0), px(0.0)),
             current_count: 0,
-            gpu_context,
+            gpu_context: None,
             scale_factor,
 
             xkb_context,
@@ -1511,19 +1508,25 @@ impl LinuxClient for X11Client {
             .generate_id()
             .context("X11: Failed to generate window ID")?;
 
+        let xcb_connection = state.xcb_connection.clone();
+        let client_side_decorations_supported = state.client_side_decorations_supported;
+        let x_root_index = state.x_root_index;
+        let atoms = state.atoms;
+        let scale_factor = state.scale_factor;
+        let appearance = state.common.appearance;
         let window = X11Window::new(
             handle,
             X11ClientStatePtr(Rc::downgrade(&self.0)),
             state.common.foreground_executor.clone(),
-            &state.gpu_context,
+            &mut state.gpu_context,
             params,
-            &state.xcb_connection,
-            state.client_side_decorations_supported,
-            state.x_root_index,
+            &xcb_connection,
+            client_side_decorations_supported,
+            x_root_index,
             x_window,
-            &state.atoms,
-            state.scale_factor,
-            state.common.appearance,
+            &atoms,
+            scale_factor,
+            appearance,
             parent_window,
         )?;
         check_reply(

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

@@ -391,7 +391,7 @@ impl X11WindowState {
         handle: AnyWindowHandle,
         client: X11ClientStatePtr,
         executor: ForegroundExecutor,
-        gpu_context: &WgpuContext,
+        gpu_context: &mut Option<WgpuContext>,
         params: WindowParams,
         xcb: &Rc<XCBConnection>,
         client_side_decorations_supported: bool,
@@ -798,7 +798,7 @@ impl X11Window {
         handle: AnyWindowHandle,
         client: X11ClientStatePtr,
         executor: ForegroundExecutor,
-        gpu_context: &WgpuContext,
+        gpu_context: &mut Option<WgpuContext>,
         params: WindowParams,
         xcb: &Rc<XCBConnection>,
         client_side_decorations_supported: bool,
@@ -1465,7 +1465,10 @@ impl PlatformWindow for X11Window {
             .upgrade()
             .map(|ref_cell| {
                 let state = ref_cell.borrow();
-                state.gpu_context.supports_dual_source_blending()
+                state
+                    .gpu_context
+                    .as_ref()
+                    .is_some_and(|ctx| ctx.supports_dual_source_blending())
             })
             .unwrap_or_default()
     }

crates/gpui_wgpu/src/wgpu_context.rs 🔗

@@ -11,7 +11,7 @@ pub struct WgpuContext {
 }
 
 impl WgpuContext {
-    pub fn new() -> anyhow::Result<Self> {
+    pub fn new(instance: wgpu::Instance, surface: &wgpu::Surface<'_>) -> 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,14 +24,24 @@ impl WgpuContext {
             }
         };
 
-        let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
-            backends: wgpu::Backends::VULKAN | wgpu::Backends::GL,
-            flags: wgpu::InstanceFlags::default(),
-            backend_options: wgpu::BackendOptions::default(),
-            memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
-        });
-
-        let adapter = smol::block_on(Self::select_adapter(&instance, device_id_filter))?;
+        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,
+            );
+        }
 
         log::info!(
             "Selected GPU adapter: {:?} ({:?})",
@@ -39,6 +49,42 @@ impl WgpuContext {
             adapter.get_info().backend
         );
 
+        let (device, queue, dual_source_blending) = Self::create_device(&adapter)?;
+
+        Ok(Self {
+            instance,
+            adapter,
+            device: Arc::new(device),
+            queue: Arc::new(queue),
+            dual_source_blending,
+        })
+    }
+
+    pub fn instance() -> wgpu::Instance {
+        wgpu::Instance::new(&wgpu::InstanceDescriptor {
+            backends: wgpu::Backends::VULKAN | wgpu::Backends::GL,
+            flags: wgpu::InstanceFlags::default(),
+            backend_options: wgpu::BackendOptions::default(),
+            memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
+        })
+    }
+
+    pub fn check_compatible_with_surface(&self, surface: &wgpu::Surface<'_>) -> anyhow::Result<()> {
+        let caps = surface.get_capabilities(&self.adapter);
+        if caps.formats.is_empty() {
+            let info = self.adapter.get_info();
+            anyhow::bail!(
+                "Adapter {:?} (backend={:?}, device={:#06x}) is not compatible with the \
+                 display surface for this window.",
+                info.name,
+                info.backend,
+                info.device,
+            );
+        }
+        Ok(())
+    }
+
+    fn create_device(adapter: &wgpu::Adapter) -> anyhow::Result<(wgpu::Device, wgpu::Queue, bool)> {
         let dual_source_blending_available = adapter
             .features()
             .contains(wgpu::Features::DUAL_SOURCE_BLENDING);
@@ -63,18 +109,13 @@ impl WgpuContext {
         }))
         .map_err(|e| anyhow::anyhow!("Failed to create wgpu device: {e}"))?;
 
-        Ok(Self {
-            instance,
-            adapter,
-            device: Arc::new(device),
-            queue: Arc::new(queue),
-            dual_source_blending: dual_source_blending_available,
-        })
+        Ok((device, queue, dual_source_blending_available))
     }
 
     async fn select_adapter(
         instance: &wgpu::Instance,
         device_id_filter: Option<u32>,
+        compatible_surface: Option<&wgpu::Surface<'_>>,
     ) -> anyhow::Result<wgpu::Adapter> {
         if let Some(device_id) = device_id_filter {
             let adapters: Vec<_> = instance.enumerate_adapters(wgpu::Backends::all()).await;
@@ -88,6 +129,18 @@ impl WgpuContext {
             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;
+                        }
+                    }
                     log::info!(
                         "Found GPU matching ZED_DEVICE_ID={:#06x}: {}",
                         device_id,
@@ -100,7 +153,7 @@ impl WgpuContext {
             }
 
             log::warn!(
-                "No GPU found matching ZED_DEVICE_ID={:#06x}. Available devices:",
+                "No compatible GPU found matching ZED_DEVICE_ID={:#06x}. Available devices:",
                 device_id
             );
 
@@ -117,7 +170,7 @@ impl WgpuContext {
         instance
             .request_adapter(&wgpu::RequestAdapterOptions {
                 power_preference: wgpu::PowerPreference::None,
-                compatible_surface: None,
+                compatible_surface,
                 force_fallback_adapter: false,
             })
             .await

crates/gpui_wgpu/src/wgpu_renderer.rs 🔗

@@ -124,7 +124,7 @@ impl WgpuRenderer {
     /// The caller must ensure that the window handle remains valid for the lifetime
     /// of the returned renderer.
     pub fn new<W: HasWindowHandle + HasDisplayHandle>(
-        context: &WgpuContext,
+        gpu_context: &mut Option<WgpuContext>,
         window: &W,
         config: WgpuSurfaceConfig,
     ) -> anyhow::Result<Self> {
@@ -140,20 +140,32 @@ impl WgpuRenderer {
             raw_window_handle: window_handle.as_raw(),
         };
 
+        // Use the existing context's instance if available, otherwise create a new one.
+        // The surface must be created with the same instance that will be used for
+        // adapter selection, otherwise wgpu will panic.
+        let instance = gpu_context
+            .as_ref()
+            .map(|ctx| ctx.instance.clone())
+            .unwrap_or_else(WgpuContext::instance);
+
         // Safety: The caller guarantees that the window handle is valid for the
         // lifetime of this renderer. In practice, the RawWindow struct is created
         // from the native window handles and the surface is dropped before the window.
         let surface = unsafe {
-            context
-                .instance
+            instance
                 .create_surface_unsafe(target)
                 .map_err(|e| anyhow::anyhow!("Failed to create surface: {e}"))?
         };
 
+        let context = match gpu_context {
+            Some(context) => {
+                context.check_compatible_with_surface(&surface)?;
+                context
+            }
+            None => gpu_context.insert(WgpuContext::new(instance, &surface)?),
+        };
+
         let surface_caps = surface.get_capabilities(&context.adapter);
-        // Prefer standard 8-bit non-sRGB formats that don't require special features.
-        // Other formats like Rgba16Unorm require TEXTURE_FORMAT_16BIT_NORM which may
-        // not be available on all devices.
         let preferred_formats = [
             wgpu::TextureFormat::Bgra8Unorm,
             wgpu::TextureFormat::Rgba8Unorm,
@@ -163,26 +175,38 @@ impl WgpuRenderer {
             .find(|f| surface_caps.formats.contains(f))
             .copied()
             .or_else(|| surface_caps.formats.iter().find(|f| !f.is_srgb()).copied())
-            .unwrap_or(surface_caps.formats[0]);
+            .or_else(|| surface_caps.formats.first().copied())
+            .ok_or_else(|| {
+                anyhow::anyhow!(
+                    "Surface reports no supported texture formats for adapter {:?}",
+                    context.adapter.get_info().name
+                )
+            })?;
 
         let pick_alpha_mode =
-            |preferences: &[wgpu::CompositeAlphaMode]| -> wgpu::CompositeAlphaMode {
+            |preferences: &[wgpu::CompositeAlphaMode]| -> anyhow::Result<wgpu::CompositeAlphaMode> {
                 preferences
                     .iter()
                     .find(|p| surface_caps.alpha_modes.contains(p))
                     .copied()
-                    .unwrap_or(surface_caps.alpha_modes[0])
+                    .or_else(|| surface_caps.alpha_modes.first().copied())
+                    .ok_or_else(|| {
+                        anyhow::anyhow!(
+                            "Surface reports no supported alpha modes for adapter {:?}",
+                            context.adapter.get_info().name
+                        )
+                    })
             };
 
         let transparent_alpha_mode = pick_alpha_mode(&[
             wgpu::CompositeAlphaMode::PreMultiplied,
             wgpu::CompositeAlphaMode::Inherit,
-        ]);
+        ])?;
 
         let opaque_alpha_mode = pick_alpha_mode(&[
             wgpu::CompositeAlphaMode::Opaque,
             wgpu::CompositeAlphaMode::Inherit,
-        ]);
+        ])?;
 
         let alpha_mode = if config.transparent {
             transparent_alpha_mode

crates/zed/src/zed.rs 🔗

@@ -1119,7 +1119,7 @@ fn register_actions(
                             Editor::new_file(workspace, &Default::default(), window, cx)
                         },
                     )
-                    .detach();
+                    .detach_and_log_err(cx);
                 }
             }
         })

crates/zed/src/zed/open_listener.rs 🔗

@@ -498,7 +498,7 @@ async fn open_workspaces(
                 workspace::open_new(open_options, app_state, cx, |workspace, window, cx| {
                     Editor::new_file(workspace, &Default::default(), window, cx)
                 })
-                .detach();
+                .detach_and_log_err(cx);
             });
         }
         return Ok(());