Merge bugfix/hyprland-wgpu-surface-timeout into pre-release

cc
This commit is contained in:
vicrodh committed 2026-07-07 09:36:08 -06:00
commit 39304dbef5
4 files changed
+70 -2672

No files matched your search

File diff suppressed because it is too large. Load diff
@@ -140,3 +140,6 @@ default-features = false
level = "warn"
priority = 0
check-cfg = ["cfg(slint_nightly_test)"]
# Standalone when checked directly; the app consumes it via [patch.crates-io].
[workspace]
+23 -1
View File
@@ -48,6 +48,22 @@ pub trait WindowSurface<R: femtovg::Renderer> {
fn render_output(&self) -> impl Into<R::RenderOutput>;
}
/// QBZ vendor patch (issue #540): sentinel error a graphics backend returns
/// from `begin_surface_rendering` when the current frame should be silently
/// skipped (e.g. a transient wgpu surface-acquire timeout while the
/// compositor is parked by VRR). The renderer treats it as "nothing to
/// render" instead of a fatal platform error.
#[derive(Debug)]
pub struct FrameSkipped;
impl core::fmt::Display for FrameSkipped {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str("frame skipped: surface temporarily unavailable")
}
}
impl std::error::Error for FrameSkipped {}
pub trait GraphicsBackend {
type Renderer: femtovg::Renderer + TextureImporter;
type WindowSurface: WindowSurface<Self::Renderer>;
@@ -125,7 +141,13 @@ impl<B: GraphicsBackend> FemtoVGRenderer<B> {
surface_size: i_slint_core::api::PhysicalSize,
post_render_cb: Option<&dyn Fn(&mut dyn ItemRenderer)>,
) -> Result<(), i_slint_core::platform::PlatformError> {
let surface = self.graphics_backend.begin_surface_rendering()?;
// QBZ vendor patch (issue #540): a FrameSkipped from the backend is
// "nothing to render this frame", not a fatal error — the next
// redraw request will try again once the compositor resumes.
let surface = match self.graphics_backend.begin_surface_rendering() {
Err(e) if e.is::<FrameSkipped>() => return Ok(()),
other => other?,
};
if self.rendering_first_time.take() {
*self.rendering_metrics_collector.borrow_mut() = RenderingMetricsCollector::new(
+44 -1
View File
@@ -65,6 +65,14 @@ pub struct WGPUBackend {
/// construction — this backend's window keeps the same alpha preference
/// across every surface re-creation.
prefers_transparent: bool,
/// QBZ vendor patch (issue #540): when the compositor stops delivering
/// frames (Hyprland VFR with a static screen / hidden workspace), every
/// `get_current_texture()` BLOCKS the UI thread until it times out.
/// After a timeout we skip frames without touching the surface until
/// this deadline, doubling the cooldown up to ~2 s while the compositor
/// stays parked. Reset on the first successful acquire.
acquire_skip_until: std::cell::Cell<Option<std::time::Instant>>,
acquire_backoff_ms: std::cell::Cell<u64>,
}
pub struct WGPUWindowSurface {
@@ -91,6 +99,8 @@ impl GraphicsBackend for WGPUBackend {
surface: Default::default(),
prefers_transparent: SURFACE_PREFERS_TRANSPARENT
.load(core::sync::atomic::Ordering::Relaxed),
acquire_skip_until: Default::default(),
acquire_backoff_ms: Default::default(),
}
}
@@ -104,11 +114,42 @@ impl GraphicsBackend for WGPUBackend {
fn begin_surface_rendering(
&self,
) -> Result<Self::WindowSurface, Box<dyn std::error::Error + Send + Sync>> {
// QBZ vendor patch (issue #540): during a timeout cooldown, skip the
// frame WITHOUT touching the surface — each blocked acquire wedges
// the UI thread for seconds while the compositor is parked.
if let Some(until) = self.acquire_skip_until.get() {
if std::time::Instant::now() < until {
return Err(Box::new(crate::FrameSkipped));
}
}
let surface = self.surface.borrow();
let surface = surface.as_ref().unwrap();
let frame = match surface.get_current_texture() {
Ok(texture) => texture,
Err(wgpu::SurfaceError::Timeout) => surface.get_current_texture()?,
// QBZ vendor patch (issue #540): a surface-acquire timeout is
// TRANSIENT (Hyprland VFR parks the display when nothing moves;
// no frame callback will arrive until it resumes). Upstream
// retried once and propagated the second failure, which killed
// the event loop and the whole app. Skip the frame instead and
// back off exponentially (250 ms → 2 s) so repeated redraw
// requests don't keep blocking the UI thread. First successful
// acquire resets the cooldown.
Err(wgpu::SurfaceError::Timeout) => {
let backoff = (self.acquire_backoff_ms.get().max(125) * 2).min(2000);
self.acquire_backoff_ms.set(backoff);
self.acquire_skip_until.set(Some(
std::time::Instant::now() + std::time::Duration::from_millis(backoff),
));
static WARNED: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(false);
if !WARNED.swap(true, std::sync::atomic::Ordering::Relaxed) {
eprintln!(
"qbz(femtovg-wgpu): surface acquire timed out — compositor is not \
delivering frames (VRR/hidden surface?); skipping frames until it resumes"
);
}
return Err(Box::new(crate::FrameSkipped));
}
// Outdated or lost: re-configure and try again
Err(_) => {
let mut device = self.device.borrow_mut();
@@ -118,6 +159,8 @@ impl GraphicsBackend for WGPUBackend {
surface.get_current_texture()?
}
};
self.acquire_backoff_ms.set(0);
self.acquire_skip_until.set(None);
Ok(WGPUWindowSurface { surface_texture: frame })
}