From a35a741b076430f1b5584537b5cdfe575f1b4fb0 Mon Sep 17 00:00:00 2001 From: Bendik Aagaard Lynghaug Date: Mon, 8 Jun 2026 19:19:56 +0200 Subject: [PATCH] use vulkan first and fallback to gl if driver errors --- src/renderer.rs | 94 +++++++++++++++++++++++++++++++++++++++---------- src/wayland.rs | 64 ++++++++++++++++++++++++++------- 2 files changed, 126 insertions(+), 32 deletions(-) diff --git a/src/renderer.rs b/src/renderer.rs index 418c393..03456b3 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -117,7 +117,7 @@ impl Renderer { )); let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { - backends: wgpu::Backends::GL, + backends: wgpu::Backends::VULKAN | wgpu::Backends::GL, flags: wgpu::InstanceFlags::default(), memory_budget_thresholds: Default::default(), backend_options: Default::default(), @@ -131,27 +131,83 @@ impl Renderer { }) .context("failed to create wgpu surface")?; - let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { - power_preference: wgpu::PowerPreference::default(), - compatible_surface: Some(&wgpu_surface), - force_fallback_adapter: false, - })) - .context("no suitable wgpu adapter found")?; + // Build a priority-ordered candidate list: Vulkan discrete first, then + // Vulkan integrated, then GL. We try each in turn and fall back on + // failure so that Vulkan is used wherever it works (NVIDIA, AMD, etc.) + // while broken Vulkan drivers (e.g. Mesa V3DV on Raspberry Pi, which + // advertises features it can't actually enable) are skipped silently. + let mut candidates: Vec = { + let all: Vec = pollster::block_on( + instance.enumerate_adapters(wgpu::Backends::all()), + ) + .into_iter() + .filter(|a| a.is_surface_supported(&wgpu_surface)) + .collect(); + + let mut vulkan: Vec<_> = all + .iter() + .filter(|a| a.get_info().backend == wgpu::Backend::Vulkan) + .cloned() + .collect(); + // Discrete GPUs first within the Vulkan set. + vulkan.sort_by_key(|a| { + (a.get_info().device_type != wgpu::DeviceType::DiscreteGpu) as u8 + }); + + let gl: Vec<_> = all + .iter() + .filter(|a| a.get_info().backend == wgpu::Backend::Gl) + .cloned() + .collect(); + + vulkan.into_iter().chain(gl).collect() + }; + + if candidates.is_empty() { + anyhow::bail!("no suitable wgpu adapter found"); + } + + let device_desc = wgpu::DeviceDescriptor { + label: Some("gpupaper"), + required_features: wgpu::Features::empty(), + // downlevel_defaults is safe on both GL/GLES and Vulkan. + required_limits: wgpu::Limits::downlevel_defaults(), + memory_hints: wgpu::MemoryHints::default(), + experimental_features: Default::default(), + trace: Default::default(), + }; + + // Try each candidate. Some Vulkan drivers (e.g. Mesa V3DV) panic + // inside vkCreateDevice with VK_ERROR_FEATURE_NOT_PRESENT despite + // advertising the feature — catch that and move to the next adapter. + let (adapter, device, queue) = candidates + .drain(..) + .find_map(|adapter| { + let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + pollster::block_on(adapter.request_device(&device_desc)) + })); + match outcome { + Ok(Ok((device, queue))) => Some((adapter, device, queue)), + Ok(Err(e)) => { + log::warn!( + "skipping adapter {:?}: request_device failed: {e}", + adapter.get_info().name + ); + None + } + Err(_) => { + log::warn!( + "skipping adapter {:?}: Vulkan driver panicked during device creation", + adapter.get_info().name + ); + None + } + } + }) + .context("no adapter could create a wgpu device")?; log::info!("wgpu adapter: {:?}", adapter.get_info()); - let (device, queue) = pollster::block_on(adapter.request_device( - &wgpu::DeviceDescriptor { - label: Some("gpupaper"), - required_features: wgpu::Features::empty(), - required_limits: adapter.limits(), - memory_hints: wgpu::MemoryHints::default(), - experimental_features: Default::default(), - trace: Default::default(), - }, - )) - .context("failed to create wgpu device")?; - // Surface configuration let caps = wgpu_surface.get_capabilities(&adapter); let format = caps diff --git a/src/wayland.rs b/src/wayland.rs index 153ce1a..448d4f6 100644 --- a/src/wayland.rs +++ b/src/wayland.rs @@ -49,6 +49,10 @@ pub struct AppState { width: u32, height: u32, pub exit: bool, + + // Frame callback: set true when compositor says it's ready for next frame + draw_ready: bool, + last_frame: Instant, } // Safety: we only use the raw pointers in single-threaded context @@ -98,6 +102,8 @@ pub fn run( width: 0, height: 0, exit: false, + draw_ready: false, + last_frame: Instant::now(), }; // One roundtrip to discover outputs. @@ -147,36 +153,66 @@ pub fn run( .context("failed to create renderer")? }); - // Render loop. + // Render loop driven by wl_surface.frame callbacks. + // The compositor stops sending callbacks when the surface is fully occluded, + // so the loop naturally idles to zero CPU when another app is fullscreen. let frame_duration = if fps > 0 { Some(Duration::from_nanos(1_000_000_000 / fps as u64)) } else { None }; + // Register the first frame callback then render immediately. On Wayland + // the compositor only fires frame callbacks for commits that contain a + // buffer; an empty commit would stall the loop forever on many compositors. + // Rendering here attaches the first buffer and commits it together with the + // frame callback so the compositor has something to display right away. + { + let wl_surface = state.layer.as_ref().unwrap().wl_surface(); + wl_surface.frame(&qh, wl_surface.clone()); + } + if let Some(r) = state.renderer.as_mut() { + r.render(); + } + conn.flush().ok(); + loop { - // Non-blocking Wayland event dispatch. - event_queue.dispatch_pending(&mut state).context("event dispatch")?; - // Flush outgoing messages. + // Block the thread until the compositor sends us any event (including + // the frame callback). Zero CPU while occluded. + event_queue.blocking_dispatch(&mut state).context("event dispatch")?; conn.flush().ok(); if state.exit { break; } - let frame_start = Instant::now(); + if !state.draw_ready { + continue; + } + state.draw_ready = false; + + // Optional fps cap: sleep the remainder of the frame budget. + if let Some(dur) = frame_duration { + let elapsed = state.last_frame.elapsed(); + if elapsed < dur { + std::thread::sleep(dur - elapsed); + } + } + state.last_frame = Instant::now(); + + // Register the NEXT frame callback before present() so that the + // request is included in the same wl_surface.commit that wgpu + // issues inside present(). + { + let wl_surface = state.layer.as_ref().unwrap().wl_surface(); + wl_surface.frame(&qh, wl_surface.clone()); + } if let Some(r) = state.renderer.as_mut() { r.render(); } - // Cap frame rate if requested. - if let Some(dur) = frame_duration { - let elapsed = frame_start.elapsed(); - if elapsed < dur { - std::thread::sleep(dur - elapsed); - } - } + conn.flush().ok(); } Ok(()) @@ -220,7 +256,9 @@ impl CompositorHandler for AppState { fn frame( &mut self, _: &Connection, _: &QueueHandle, _: &wl_surface::WlSurface, _: u32, - ) {} + ) { + self.draw_ready = true; + } fn surface_enter( &mut self, _: &Connection, _: &QueueHandle,