From 4e34c73531403dfd07dbfe62a091ebf1d32a335f Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 24 Feb 2026 11:43:09 -0700 Subject: [PATCH] Defer wgpu context creation until we have a surface (#49926) 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> --- crates/editor/src/editor.rs | 4 +- crates/gpui/src/platform/linux/platform.rs | 44 --------- .../gpui/src/platform/linux/wayland/client.rs | 14 +-- .../gpui/src/platform/linux/wayland/window.rs | 9 +- crates/gpui/src/platform/linux/x11/client.rs | 28 +++--- crates/gpui/src/platform/linux/x11/window.rs | 9 +- crates/gpui/src/platform/wgpu/wgpu_context.rs | 89 +++++++++++++++---- .../gpui/src/platform/wgpu/wgpu_renderer.rs | 46 +++++++--- crates/zed/src/zed.rs | 2 +- crates/zed/src/zed/open_listener.rs | 2 +- 10 files changed, 145 insertions(+), 102 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2de66b833af265e2c6fac369b23ac23cd02afdb0..26dfb67cd53266d9584e8a5ddb01557b75433ab2 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -359,7 +359,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| { @@ -374,7 +374,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| { diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index f891aa35adbc62d4d384970fb9d997e60cbfc848..6bdd641ce43af90687f765c89d2517e628ecdf75 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/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 ResultExt for anyhow::Result { - 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 trait LinuxClient { fn compositor_name(&self) -> &'static str; fn with_common(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R; diff --git a/crates/gpui/src/platform/linux/wayland/client.rs b/crates/gpui/src/platform/linux/wayland/client.rs index 41f12916b971d173181225dce185872f4dba6c72..e187d8e169defad1da3464570d5437e4bad691cd 100644 --- a/crates/gpui/src/platform/linux/wayland/client.rs +++ b/crates/gpui/src/platform/linux/wayland/client.rs @@ -78,8 +78,8 @@ use crate::{ FileDropEvent, ForegroundExecutor, KeyDownEvent, KeyUpEvent, Keystroke, LinuxCommon, LinuxKeyboardLayout, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels, PlatformDisplay, - PlatformInput, PlatformKeyboardLayout, Point, ResultExt as _, SCROLL_LINES, ScrollDelta, - ScrollWheelEvent, Size, TouchPhase, WindowParams, point, profiler, px, size, + PlatformInput, PlatformKeyboardLayout, Point, SCROLL_LINES, ScrollDelta, ScrollWheelEvent, + Size, TouchPhase, WindowParams, point, profiler, px, size, }; use crate::{ SharedString, @@ -204,7 +204,7 @@ pub struct Output { pub(crate) struct WaylandClientState { serial_tracker: SerialTracker, globals: Globals, - pub gpu_context: WgpuContext, + pub gpu_context: Option, wl_seat: wl_seat::WlSeat, // TODO: Multi seat support wl_pointer: Option, wl_keyboard: Option, @@ -519,8 +519,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( @@ -719,13 +718,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()); diff --git a/crates/gpui/src/platform/linux/wayland/window.rs b/crates/gpui/src/platform/linux/wayland/window.rs index 7642b93ffe1b8fc7ee9d227fe3711704a370ce87..4bc9172b94c4fa96ccd48977d33b1bcc576223c9 100644 --- a/crates/gpui/src/platform/linux/wayland/window.rs +++ b/crates/gpui/src/platform/linux/wayland/window.rs @@ -319,7 +319,7 @@ impl WaylandWindowState { viewport: Option, client: WaylandClientStatePtr, globals: Globals, - gpu_context: &WgpuContext, + gpu_context: &mut Option, options: WindowParams, parent: Option, ) -> anyhow::Result { @@ -483,7 +483,7 @@ impl WaylandWindow { pub fn new( handle: AnyWindowHandle, globals: Globals, - gpu_context: &WgpuContext, + gpu_context: &mut Option, client: WaylandClientStatePtr, params: WindowParams, appearance: WindowAppearance, @@ -1231,7 +1231,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) { diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index 08d756d3620e0ec63ba562646f78c2d0f059e78d..40c6b71a7aa34341f26c7dcfd4e13d1e702aaf3b 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -1,4 +1,4 @@ -use crate::{Capslock, ResultExt as _, TaskTiming, profiler, xcb_flush}; +use crate::{Capslock, TaskTiming, profiler, xcb_flush}; use anyhow::{Context as _, anyhow}; use ashpd::WindowIdentifier; use calloop::{ @@ -177,7 +177,7 @@ pub struct X11ClientState { pub(crate) last_location: Point, pub(crate) current_count: usize, - pub(crate) gpu_context: WgpuContext, + pub(crate) gpu_context: Option, pub(crate) scale_factor: f32, @@ -420,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); @@ -491,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, @@ -1507,19 +1505,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( diff --git a/crates/gpui/src/platform/linux/x11/window.rs b/crates/gpui/src/platform/linux/x11/window.rs index acfdcb068d42e6b342a8cde69e54421b972e3a4a..ff0a6052da8b7e30d424484c4d2b7deb7733d24a 100644 --- a/crates/gpui/src/platform/linux/x11/window.rs +++ b/crates/gpui/src/platform/linux/x11/window.rs @@ -392,7 +392,7 @@ impl X11WindowState { handle: AnyWindowHandle, client: X11ClientStatePtr, executor: ForegroundExecutor, - gpu_context: &WgpuContext, + gpu_context: &mut Option, params: WindowParams, xcb: &Rc, client_side_decorations_supported: bool, @@ -799,7 +799,7 @@ impl X11Window { handle: AnyWindowHandle, client: X11ClientStatePtr, executor: ForegroundExecutor, - gpu_context: &WgpuContext, + gpu_context: &mut Option, params: WindowParams, xcb: &Rc, client_side_decorations_supported: bool, @@ -1466,7 +1466,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() } diff --git a/crates/gpui/src/platform/wgpu/wgpu_context.rs b/crates/gpui/src/platform/wgpu/wgpu_context.rs index b0de623f0e9d611863825f2aa446d1e120a7091e..270201183c8afd33534c184b7dc597ed6ab7d9d5 100644 --- a/crates/gpui/src/platform/wgpu/wgpu_context.rs +++ b/crates/gpui/src/platform/wgpu/wgpu_context.rs @@ -11,7 +11,7 @@ pub struct WgpuContext { } impl WgpuContext { - pub fn new() -> anyhow::Result { + pub fn new(instance: wgpu::Instance, surface: &wgpu::Surface<'_>) -> anyhow::Result { 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, + compatible_surface: Option<&wgpu::Surface<'_>>, ) -> anyhow::Result { 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 diff --git a/crates/gpui/src/platform/wgpu/wgpu_renderer.rs b/crates/gpui/src/platform/wgpu/wgpu_renderer.rs index 972d6f586341985e53327e3c7588e4b362f8dfba..25b13ce1100e25522ffa8295b47540fc6f158a5f 100644 --- a/crates/gpui/src/platform/wgpu/wgpu_renderer.rs +++ b/crates/gpui/src/platform/wgpu/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( - context: &WgpuContext, + gpu_context: &mut Option, window: &W, config: WgpuSurfaceConfig, ) -> anyhow::Result { @@ -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 { 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 diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index ed94725511b7fb5eaf31af0829979f2f9a05807a..420901b51f319b97d5ee4e35808dd31d93dc0346 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1122,7 +1122,7 @@ fn register_actions( Editor::new_file(workspace, &Default::default(), window, cx) }, ) - .detach(); + .detach_and_log_err(cx); } } }) diff --git a/crates/zed/src/zed/open_listener.rs b/crates/zed/src/zed/open_listener.rs index 94e125ab98c44282c037704158c48e69d7b3a785..a7d1da663b3da6848d3552707f261fe02beba56b 100644 --- a/crates/zed/src/zed/open_listener.rs +++ b/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(());