From 2c5fe8fe579aac98ea8cb75962bc9177fce68379 Mon Sep 17 00:00:00 2001 From: Gustavo Lima Chaves Date: Sat, 21 Feb 2026 01:45:16 -0800 Subject: [PATCH 1/3] fix(wgpu): trust DMA-BUF device preference without probing surface On Wayland with hybrid GPU setups (e.g. NVIDIA + Intel), the surface may not be fully committed when wgpu probes adapter compatibility during compositor initialization. This causes VK_ERROR_SURFACE_LOST_KHR in vkGetPhysicalDeviceSurfaceCapabilitiesKHR, which falsely rejects the correct GPU and forces a fallback to llvmpipe. The DMA-BUF feedback from the Wayland compositor already guarantees which device should be used for rendering. When vendor/device IDs from DMA-BUF match an enumerated adapter, select it directly without the redundant (and race-prone) surface compatibility probe. The surface will be (re-)created on the chosen adapter later anyway. --- wgpu/src/window/compositor.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index 833a642e6e..17486b1543 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -153,20 +153,21 @@ impl Compositor { not(target_os = "redox") ))] if let Some((vendor_id, device_id)) = ids { + // Trust the compositor's DMA-BUF device preference + // without probing the surface. On Wayland the surface + // may not be fully committed at this point, causing + // VK_ERROR_SURFACE_LOST_KHR in the capability query + // which falsely rejects the correct GPU. The DMA-BUF + // feedback already guarantees this device can render for + // the compositor, and the surface will be (re-)created + // on the chosen adapter later anyway. adapter = instance .enumerate_adapters(settings.backends) .into_iter() - .filter(|adapter| { + .find(|adapter| { let info = adapter.get_info(); info.device == device_id as u32 && info.vendor == vendor_id as u32 - }) - .find(|adapter| { - if let Some(surface) = compatible_surface.as_ref() { - adapter.is_surface_supported(surface) - } else { - true - } }); } } else if let Ok(name) = std::env::var("WGPU_ADAPTER_NAME") { From 508d542890eb1d6ac0d3166d58d26b47f55c91cb Mon Sep 17 00:00:00 2001 From: Gustavo Lima Chaves Date: Sat, 21 Feb 2026 01:46:15 -0800 Subject: [PATCH 2/3] fix(wgpu): retry adapter selection without surface when only CPU adapter found When request_adapter() returns only a CPU software rasterizer (e.g. llvmpipe), it is likely that real GPU adapters were falsely rejected by a stale Wayland surface. This commonly happens with layer-shell panel applets, where the surface is not yet fully committed when the compositor initialises the wgpu backend. The Vulkan surface capability query fails with VK_ERROR_SURFACE_LOST_KHR for real GPUs, but software rasterizers survive because they do not perform the same surface validation. Retry adapter selection without the compatible_surface constraint so that wgpu's power-preference ranking can pick a real GPU (discrete or integrated). If the surface-less retry also yields nothing, fall back to whatever the original surface-constrained attempt returned (possibly the CPU adapter), so behaviour is never worse than before. On a system with only a CPU adapter (e.g. a VM with no GPU passthrough) the retry produces the same result and the CPU adapter is used as expected. --- wgpu/src/window/compositor.rs | 92 ++++++++++++++++++++++++++--------- 1 file changed, 69 insertions(+), 23 deletions(-) diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index 17486b1543..5d136f55a8 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -190,30 +190,76 @@ impl Compositor { let adapter = match adapter { Some(adapter) => adapter, None => { - // fall back to allowing GL backend if enabled - instance = wgpu::util::new_instance_with_webgpu_detection( - &wgpu::InstanceDescriptor { - backends: settings.backends, - flags: if cfg!(feature = "strict-assertions") { - wgpu::InstanceFlags::debugging() - } else { - wgpu::InstanceFlags::empty() + let surface_pick = + instance.request_adapter(&adapter_options).await; + let is_cpu = surface_pick + .as_ref() + .is_ok_and(|a| a.get_info().device_type == wgpu::DeviceType::Cpu); + + if surface_pick.is_err() || is_cpu { + // Either no adapter was found, or only a CPU software + // rasterizer (e.g. llvmpipe) survived the surface + // compatibility filter. + // + // On Wayland the surface may not be fully committed + // yet (common for layer-shell panel applets), causing + // real GPUs to fail VK_ERROR_SURFACE_LOST_KHR while + // software rasterizers pass. Retry without the + // surface constraint so a real GPU is preferred. + // + // If the surface-less retry also yields nothing, fall + // back to the GL backend (upstream fallback) or to + // whatever the original attempt returned (possibly + // the CPU adapter) so we are never worse off. + log::warn!( + "adapter selection: surface-compatible pick is \ + {pick}; retrying without surface constraint", + pick = match surface_pick.as_ref() { + Ok(a) => a.get_info().name, + Err(e) => format!("Err({e})"), }, - ..Default::default() - }, - ) - .await; - compatible_surface = compatible_window - .and_then(|window| instance.create_surface(window).ok()); - adapter_options = wgpu::RequestAdapterOptions { - power_preference: wgpu::PowerPreference::from_env() - .unwrap_or(wgpu::PowerPreference::HighPerformance), - compatible_surface: compatible_surface.as_ref(), - force_fallback_adapter: false, - }; - instance.request_adapter(&adapter_options).await.map_err( - |_| Error::NoAdapterFound(format!("{:?}", adapter_options)), - )? + ); + let fallback_options = wgpu::RequestAdapterOptions { + compatible_surface: None, + ..adapter_options + }; + match instance + .request_adapter(&fallback_options) + .await + .or(surface_pick) + { + Ok(a) => a, + Err(_) => { + // Last resort: fall back to allowing GL backend + instance = wgpu::util::new_instance_with_webgpu_detection( + &wgpu::InstanceDescriptor { + backends: settings.backends, + flags: if cfg!(feature = "strict-assertions") { + wgpu::InstanceFlags::debugging() + } else { + wgpu::InstanceFlags::empty() + }, + ..Default::default() + }, + ) + .await; + compatible_surface = compatible_window + .and_then(|window| instance.create_surface(window).ok()); + adapter_options = wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::from_env() + .unwrap_or(wgpu::PowerPreference::HighPerformance), + compatible_surface: compatible_surface.as_ref(), + force_fallback_adapter: false, + }; + instance.request_adapter(&adapter_options).await.map_err( + |_| Error::NoAdapterFound(format!("{:?}", adapter_options)), + )? + } + } + } else { + // A real (non-CPU) GPU was found -- use it. + surface_pick.expect("guaranteed Ok: is_err() was false") + } } }; log::info!("Selected: {:#?}", adapter.get_info()); From 94f845fd61e3d7402b2a11e7e1b1683e1ab9701a Mon Sep 17 00:00:00 2001 From: Gustavo Lima Chaves Date: Fri, 10 Apr 2026 16:32:58 -0700 Subject: [PATCH 3/3] fix(list): repair event dispatch and horizontal sizing in virtual List widget MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three bugs prevented the List widget from working correctly: 1. Lazy iterator silently dropped all events (.map → .for_each) The event dispatch loop used `.map()`, which produces a lazy iterator that was never consumed. No click, keyboard, or any other event was ever forwarded to child widgets. 2. Visible elements empty during input events (hoist repopulation) `visible_elements` lives on the `List` struct, which is recreated every `view()` call (always starts empty). The repopulation from `state.visible_layouts` only ran inside the `RedrawRequested` branch. In winit's event loop, input events fire *before* `RedrawRequested`, so clicks arriving in the first event batch after a view rebuild found an empty element list and were lost. Fix: repopulate `visible_elements` from persisted state at the top of `update()`, before the dispatch loop. 3. Width reported as Shrink instead of Fill Both `size()` and `layout()` used `Length::Shrink` for width, causing the List to collapse to intrinsic content width rather than filling available horizontal space. Parent containers (scrollables, etc.) would not allocate full width to it. --- widget/src/list.rs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/widget/src/list.rs b/widget/src/list.rs index de9fe9b8f7..5dbcbd1a06 100644 --- a/widget/src/list.rs +++ b/widget/src/list.rs @@ -108,7 +108,7 @@ where fn size(&self) -> Size { Size { - width: Length::Shrink, + width: Length::Fill, height: Length::Shrink, } } @@ -344,7 +344,7 @@ where ); let size = - limits.resolve(Length::Shrink, Length::Shrink, intrinsic_size); + limits.resolve(Length::Fill, Length::Shrink, intrinsic_size); layout::Node::new(size) } @@ -363,10 +363,23 @@ where let state = tree.state.downcast_mut::(); let offset = layout.position() - Point::ORIGIN; + // Repopulate visible elements after a view() rebuild so that + // input events (which arrive before RedrawRequested in winit's + // event loop) are not silently lost. + if self.visible_elements.is_empty() { + self.visible_elements = state + .visible_layouts + .iter() + .map(|(i, _, _)| { + (self.view_item)(*i, &self.content.items[*i]) + }) + .collect(); + } + self.visible_elements .iter_mut() .zip(&mut state.visible_layouts) - .map(|(element, (index, layout, tree))| { + .for_each(|(element, (index, layout, tree))| { element.as_widget_mut().update( tree, event,