Merge bugfix/hyprland-wgpu-surface-timeout into pre-release
cc
This commit is contained in:
4 files changed
+70
-2672
No files matched your search
-2670
File diff suppressed because it is too large.
Load diff
@@ -140,3 +140,6 @@ default-features = false
|
|||||||
level = "warn"
|
level = "warn"
|
||||||
priority = 0
|
priority = 0
|
||||||
check-cfg = ["cfg(slint_nightly_test)"]
|
check-cfg = ["cfg(slint_nightly_test)"]
|
||||||
|
|
||||||
|
# Standalone when checked directly; the app consumes it via [patch.crates-io].
|
||||||
|
[workspace]
|
||||||
+23
-1
@@ -48,6 +48,22 @@ pub trait WindowSurface<R: femtovg::Renderer> {
|
|||||||
fn render_output(&self) -> impl Into<R::RenderOutput>;
|
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 {
|
pub trait GraphicsBackend {
|
||||||
type Renderer: femtovg::Renderer + TextureImporter;
|
type Renderer: femtovg::Renderer + TextureImporter;
|
||||||
type WindowSurface: WindowSurface<Self::Renderer>;
|
type WindowSurface: WindowSurface<Self::Renderer>;
|
||||||
@@ -125,7 +141,13 @@ impl<B: GraphicsBackend> FemtoVGRenderer<B> {
|
|||||||
surface_size: i_slint_core::api::PhysicalSize,
|
surface_size: i_slint_core::api::PhysicalSize,
|
||||||
post_render_cb: Option<&dyn Fn(&mut dyn ItemRenderer)>,
|
post_render_cb: Option<&dyn Fn(&mut dyn ItemRenderer)>,
|
||||||
) -> Result<(), i_slint_core::platform::PlatformError> {
|
) -> 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() {
|
if self.rendering_first_time.take() {
|
||||||
*self.rendering_metrics_collector.borrow_mut() = RenderingMetricsCollector::new(
|
*self.rendering_metrics_collector.borrow_mut() = RenderingMetricsCollector::new(
|
||||||
|
|||||||
+44
-1
@@ -65,6 +65,14 @@ pub struct WGPUBackend {
|
|||||||
/// construction — this backend's window keeps the same alpha preference
|
/// construction — this backend's window keeps the same alpha preference
|
||||||
/// across every surface re-creation.
|
/// across every surface re-creation.
|
||||||
prefers_transparent: bool,
|
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 {
|
pub struct WGPUWindowSurface {
|
||||||
@@ -91,6 +99,8 @@ impl GraphicsBackend for WGPUBackend {
|
|||||||
surface: Default::default(),
|
surface: Default::default(),
|
||||||
prefers_transparent: SURFACE_PREFERS_TRANSPARENT
|
prefers_transparent: SURFACE_PREFERS_TRANSPARENT
|
||||||
.load(core::sync::atomic::Ordering::Relaxed),
|
.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(
|
fn begin_surface_rendering(
|
||||||
&self,
|
&self,
|
||||||
) -> Result<Self::WindowSurface, Box<dyn std::error::Error + Send + Sync>> {
|
) -> 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 = self.surface.borrow();
|
||||||
let surface = surface.as_ref().unwrap();
|
let surface = surface.as_ref().unwrap();
|
||||||
let frame = match surface.get_current_texture() {
|
let frame = match surface.get_current_texture() {
|
||||||
Ok(texture) => 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
|
// Outdated or lost: re-configure and try again
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
let mut device = self.device.borrow_mut();
|
let mut device = self.device.borrow_mut();
|
||||||
@@ -118,6 +159,8 @@ impl GraphicsBackend for WGPUBackend {
|
|||||||
surface.get_current_texture()?
|
surface.get_current_texture()?
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
self.acquire_backoff_ms.set(0);
|
||||||
|
self.acquire_skip_until.set(None);
|
||||||
Ok(WGPUWindowSurface { surface_texture: frame })
|
Ok(WGPUWindowSurface { surface_texture: frame })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user