From 564424b31f82bdc488bd839292816cbbd59e8df0 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 23 Feb 2026 23:27:31 -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_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(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 0acc4ce142891109d6888b5f637b472133a5eaa5..54e20d00cafebc209cec2bd10eb0cbb0007e3af8 100644 --- a/crates/editor/src/editor.rs +++ b/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| { diff --git a/crates/gpui_linux/src/linux/platform.rs b/crates/gpui_linux/src/linux/platform.rs index b3a08310ea419e55c91cd361a032e51163d1b2f3..5929533951738a474cdb76f3047162451de5ce1e 100644 --- a/crates/gpui_linux/src/linux/platform.rs +++ b/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 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(crate) trait LinuxClient { fn compositor_name(&self) -> &'static str; fn with_common(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R; diff --git a/crates/gpui_linux/src/linux/wayland/client.rs b/crates/gpui_linux/src/linux/wayland/client.rs index 2378b822c53dce527d622b24da7cb602b4fc7060..a810a00af642c3a252a9a144b884837f82eac7e7 100644 --- a/crates/gpui_linux/src/linux/wayland/client.rs +++ b/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, wl_seat: wl_seat::WlSeat, // TODO: Multi seat support wl_pointer: Option, wl_keyboard: Option, @@ -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()); diff --git a/crates/gpui_linux/src/linux/wayland/window.rs b/crates/gpui_linux/src/linux/wayland/window.rs index 54e868683696b6b6bee08b6ab09fdae15b9cbaf4..c1006a816a3844db22ea8932177b0f0b2ff1c99f 100644 --- a/crates/gpui_linux/src/linux/wayland/window.rs +++ b/crates/gpui_linux/src/linux/wayland/window.rs @@ -317,7 +317,7 @@ impl WaylandWindowState { viewport: Option, client: WaylandClientStatePtr, globals: Globals, - gpu_context: &WgpuContext, + gpu_context: &mut Option, options: WindowParams, parent: Option, ) -> anyhow::Result { @@ -481,7 +481,7 @@ impl WaylandWindow { pub fn new( handle: AnyWindowHandle, globals: Globals, - gpu_context: &WgpuContext, + gpu_context: &mut Option, 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) { diff --git a/crates/gpui_linux/src/linux/x11/client.rs b/crates/gpui_linux/src/linux/x11/client.rs index 7766f23095fccf1a1c8c002314afdf012d3494ea..7e3f67c9bf5fe3176f3badd9b33375ffdeb9dc19 100644 --- a/crates/gpui_linux/src/linux/x11/client.rs +++ b/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, pub(crate) current_count: usize, - pub(crate) gpu_context: WgpuContext, + pub(crate) gpu_context: Option, 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( diff --git a/crates/gpui_linux/src/linux/x11/window.rs b/crates/gpui_linux/src/linux/x11/window.rs index cc48a86b0c33890d58880360848fa6336cd95a75..8060e4c4457c6ef4575d86c4d975e3ead901f693 100644 --- a/crates/gpui_linux/src/linux/x11/window.rs +++ b/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, params: WindowParams, xcb: &Rc, client_side_decorations_supported: bool, @@ -798,7 +798,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, @@ -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() } diff --git a/crates/gpui_wgpu/src/wgpu_context.rs b/crates/gpui_wgpu/src/wgpu_context.rs index b0de623f0e9d611863825f2aa446d1e120a7091e..270201183c8afd33534c184b7dc597ed6ab7d9d5 100644 --- a/crates/gpui_wgpu/src/wgpu_context.rs +++ b/crates/gpui_wgpu/src/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_wgpu/src/wgpu_renderer.rs b/crates/gpui_wgpu/src/wgpu_renderer.rs index f443f12dd54e599ad97d583b8ebf7f70b0c8f7ea..95d64d952373f303c1015669ee90a93b5d179dd5 100644 --- a/crates/gpui_wgpu/src/wgpu_renderer.rs +++ b/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( - 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 1e57334be5997585ecaca517f52134a210b364fe..83d504ea8f1cfbb13b5f0ea97cea6508a04126aa 100644 --- a/crates/zed/src/zed.rs +++ b/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); } } }) 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(());