atagen

monolith

public
Star0
Watch0

Commit 6dfb54e27e7f

tarball

atagen <boss@atagen.co>2026-10-19 07:56 UTC

6 files changed+155-31
render-vk: report a lost or hung GPU instead of waiting on it forever

VK_ERROR_DEVICE_LOST became a generic Vulkan error or Interrupted, reap treated a failed fence query as signalled, and fence waits used u64::MAX or an unbounded poll, so a hung GPU froze the main thread. A From<vk::Result> now yields Error::DeviceLost and Error::Timeout, waits give up after three seconds, and device_lost() turns true after a lost device or three timeouts in a row.
Modified file: crates/render-vk/src/error.rs+89 additions−1 deletions
@@ -1,12 +1,18 @@
11 use ash::vk;
22 use smithay::backend::allocator::Fourcc;
3+use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
4+use std::time::Duration;
35
46 #[derive(Debug, thiserror::Error)]
57 pub enum Error {
68 #[error("failed to load the Vulkan loader: {0}")]
79 Loading(#[from] ash::LoadingError),
810 #[error("Vulkan call failed: {0}")]
9- Vulkan(#[from] vk::Result),
11+ Vulkan(vk::Result),
12+ #[error("the GPU was lost")]
13+ DeviceLost,
14+ #[error("the GPU did not finish in time")]
15+ Timeout,
1016 #[error("GPU memory allocation failed: {0}")]
1117 Allocation(#[from] gpu_allocator::AllocationError),
1218 #[error("no Vulkan 1.3 device with dynamic rendering, synchronization2 and push descriptors")]
@@ -30,3 +36,85 @@ pub enum Error {
3036 #[error("invalid size {0}x{1}")]
3137 InvalidSize(i32, i32),
3238 }
39+
40+/// How long any wait for the GPU may last.
41+pub(crate) const WAIT: Duration = Duration::from_secs(3);
42+
43+/// Consecutive timed out waits after which the GPU counts as hung.
44+const HANG_AFTER: u32 = 3;
45+
46+/// Set once a call reports the device lost, and never cleared. A compositor
47+/// has one device, so the state is process wide, which lets every `?` on a
48+/// `vk::Result` record it without threading a handle through.
49+static LOST: AtomicBool = AtomicBool::new(false);
50+static HUNG: AtomicBool = AtomicBool::new(false);
51+static TIMEOUTS: AtomicU32 = AtomicU32::new(0);
52+
53+/// Whether the GPU is gone or stopped answering, so rendering cannot go on.
54+pub fn device_lost() -> bool {
55+ LOST.load(Ordering::Relaxed) || HUNG.load(Ordering::Relaxed)
56+}
57+
58+/// Whether waiting for the GPU timed out repeatedly. Tearing the device down
59+/// then would wait for work that never ends.
60+pub(crate) fn hung() -> bool {
61+ HUNG.load(Ordering::Relaxed)
62+}
63+
64+/// Records what a failed call says about the device.
65+pub(crate) fn observe(result: vk::Result) {
66+ match result {
67+ vk::Result::ERROR_DEVICE_LOST => LOST.store(true, Ordering::Relaxed),
68+ vk::Result::TIMEOUT => {
69+ let timeouts = TIMEOUTS.fetch_add(1, Ordering::Relaxed) + 1;
70+ HUNG.fetch_or(timeouts >= HANG_AFTER, Ordering::Relaxed);
71+ }
72+ _ => {}
73+ }
74+}
75+
76+/// Records that a wait for the GPU finished.
77+pub(crate) fn waited() {
78+ TIMEOUTS.store(0, Ordering::Relaxed);
79+}
80+
81+impl From<vk::Result> for Error {
82+ fn from(result: vk::Result) -> Self {
83+ observe(result);
84+ match result {
85+ vk::Result::ERROR_DEVICE_LOST => Self::DeviceLost,
86+ vk::Result::TIMEOUT => Self::Timeout,
87+ other => Self::Vulkan(other),
88+ }
89+ }
90+}
91+
92+/// What a failed `SyncPoint::wait`, which says nothing itself, meant.
93+pub(crate) fn wait_error() -> Error {
94+ if device_lost() {
95+ Error::DeviceLost
96+ } else {
97+ Error::Interrupted
98+ }
99+}
100+
101+#[cfg(test)]
102+mod tests {
103+ use super::*;
104+
105+ #[test]
106+ fn lost_device_and_repeated_timeouts_are_fatal() {
107+ assert!(!device_lost());
108+ assert!(matches!(Error::from(vk::Result::TIMEOUT), Error::Timeout));
109+ waited();
110+ assert!(matches!(Error::from(vk::Result::TIMEOUT), Error::Timeout));
111+ assert!(matches!(Error::from(vk::Result::TIMEOUT), Error::Timeout));
112+ assert!(!device_lost());
113+ assert!(matches!(Error::from(vk::Result::TIMEOUT), Error::Timeout));
114+ assert!(hung() && device_lost());
115+ assert!(matches!(
116+ Error::from(vk::Result::ERROR_DEVICE_LOST),
117+ Error::DeviceLost
118+ ));
119+ }
120+}
Modified file: crates/render-vk/src/frame.rs+1 additions−1 deletions
@@ -302,7 +302,7 @@ impl Frame for VulkanFrame<'_, '_> {
302302 }
303303
304304 fn wait(&mut self, sync: &SyncPoint) -> Result<(), Error> {
305- sync.wait().map_err(|_| Error::Interrupted)
305+ sync.wait().map_err(|_| crate::error::wait_error())
306306 }
307307
308308 fn finish(mut self) -> Result<SyncPoint, Error> {
Modified file: crates/render-vk/src/lib.rs+1 additions−1 deletions
@@ -23,7 +23,7 @@ mod sync;
2323 mod transform;
2424
2525 pub use dmabuf::VulkanAllocator;
26-pub use error::Error;
26+pub use error::{Error, device_lost};
2727 pub use frame::{Clip, Effect, Transition, VulkanFrame};
2828 pub use image::{VulkanImage, VulkanMapping, VulkanTarget, VulkanTexture};
2929 pub use renderer::{DmabufImporter, ShmStager, StagedShm, VulkanRenderer};
Modified file: crates/render-vk/src/renderer/mod.rs+23 additions−13 deletions
@@ -209,19 +209,27 @@ impl VulkanRenderer {
209209 self.samplers[(idx(self.min_filter) << 1) | idx(self.mag_filter)]
210210 }
211211
212- fn reap(&mut self) {
212+ /// Recycles finished work. A fence that cannot be queried stays
213+ /// pending and fails the call, since its work may never have finished.
214+ fn reap(&mut self) -> Result<(), Error> {
213215 let spare = &mut self.spare_cmds;
214- self.pending.retain(|p| {
215- if !p.fence.signaled() {
216- return true;
216+ let mut failed = None;
217+ self.pending.retain(|p| match p.fence.status() {
218+ Ok(false) => true,
219+ Ok(true) => {
220+ spare.push(p.cmd);
221+ false
222+ }
223+ Err(e) => {
224+ failed.get_or_insert(e);
225+ true
217226 }
218- spare.push(p.cmd);
219- false
220227 });
221228 self.dmabuf_textures.retain(|k, _| !k.is_gone());
222229 self.dmabuf_targets.retain(|k, _| !k.is_gone());
223230 self.staged.retain(|_, (b, _)| b.is_alive());
224231 self.staging_pool.trim();
232+ failed.map_or(Ok(()), |e| Err(e.into()))
225233 }
226234
227235 fn begin(&mut self) -> Result<vk::CommandBuffer, Error> {
@@ -414,7 +422,7 @@ impl VulkanRenderer {
414422 target: &mut VulkanTarget<'_>,
415423 ops: Vec<Op>,
416424 ) -> Result<SyncPoint, Error> {
417- self.reap();
425+ self.reap()?;
418426 let image = target.0.clone();
419427 let mut sampled: Vec<Arc<ImageInner>> = Vec::new();
420428 for op in &ops {
@@ -605,7 +613,7 @@ impl VulkanRenderer {
605613 if image.external {
606614 return Err(Error::ForeignTexture);
607615 }
608- self.reap();
616+ self.reap()?;
609617 if region.size.w <= 0 || region.size.h <= 0 {
610618 return Ok(());
611619 }
@@ -749,7 +757,9 @@ impl Drop for VulkanRenderer {
749757 fn drop(&mut self) {
750758 let d = &self.shared.device.device;
751759 unsafe {
752- let _ = d.device_wait_idle();
760+ if !crate::error::hung() {
761+ let _ = d.device_wait_idle();
762+ }
753763 for p in self.pending.drain(..) {
754764 d.free_command_buffers(self.pool, &[p.cmd]);
755765 }
@@ -823,7 +833,7 @@ impl Renderer for VulkanRenderer {
823833 'buffer: 'frame,
824834 {
825835 self.own(&framebuffer.0)?;
826- self.reap();
836+ self.reap()?;
827837 Ok(VulkanFrame::new(
828838 self,
829839 framebuffer,
@@ -833,16 +843,16 @@ impl Renderer for VulkanRenderer {
833843 }
834844
835845 fn wait(&mut self, sync: &SyncPoint) -> Result<(), Error> {
836- sync.wait().map_err(|_| Error::Interrupted)
846+ sync.wait().map_err(|_| crate::error::wait_error())
837847 }
838848
839849 fn cleanup_texture_cache(&mut self) -> Result<(), Error> {
840- self.reap();
850+ self.reap()?;
841851 Ok(())
842852 }
843853
844854 fn invalidate_caches(&mut self) -> Result<(), Error> {
845- self.reap();
855+ self.reap()?;
846856 self.dmabuf_textures.clear();
847857 self.dmabuf_targets.clear();
848858 Ok(())
Modified file: crates/render-vk/src/renderer/stall.rs+1 additions−1 deletions
@@ -49,7 +49,7 @@ impl VulkanRenderer {
4949 /// right after a flip. NVIDIA stalls for milliseconds on every 1024th
5050 /// submission, see research/nvidia.md.
5151 pub fn absorb_submit_stall(&mut self) -> Result<(), Error> {
52- self.reap();
52+ self.reap()?;
5353 let Some(phase) = self.stall.get().phase else {
5454 return Ok(());
5555 };
Modified file: crates/render-vk/src/sync.rs+40 additions−14 deletions
@@ -1,4 +1,5 @@
11 use crate::device::Shared;
2+use crate::error::{WAIT, observe, waited};
23 use ash::vk;
34 use rustix::event::{PollFd, PollFlags, poll};
45 use smithay::backend::renderer::sync::{Fence, Interrupted};
@@ -55,36 +56,58 @@ impl FenceInner {
5556 self.fence
5657 }
5758
58- pub fn signaled(&self) -> bool {
59+ /// Whether the GPU has signalled, or the error the query failed with.
60+ pub fn status(&self) -> Result<bool, vk::Result> {
5961 match &*self.state.lock().unwrap_or_else(|e| e.into_inner()) {
60- State::Pending => {
61- unsafe { self.shared.device.device.get_fence_status(self.fence) }.unwrap_or(true)
62- }
63- State::Exported(None) => true,
64- State::Exported(Some(fd)) => readable(fd, Some(&Default::default())),
62+ State::Pending => unsafe { self.shared.device.device.get_fence_status(self.fence) },
63+ State::Exported(None) => Ok(true),
64+ State::Exported(Some(fd)) => Ok(readable(fd, Some(&Default::default()))),
6565 }
6666 }
6767
68+ /// Like `status` for callers that cannot report a failure. A failed query
69+ /// counts as signalled so nobody waits on a fence that cannot signal.
70+ pub fn signaled(&self) -> bool {
71+ self.status().unwrap_or_else(|e| {
72+ observe(e);
73+ true
74+ })
75+ }
76+
6877 /// Blocks until the GPU signals. A pending fence is first turned into its
6978 /// sync_file and polled, because waiting on the Vulkan fence itself sleeps
7079 /// in 10 ms steps on NVIDIA even when the work is done in microseconds.
80+ /// Gives up with `TIMEOUT` after [`WAIT`], so a hung GPU cannot freeze the
81+ /// caller.
7182 pub fn wait(&self) -> Result<(), vk::Result> {
7283 if self.exportable {
7384 drop(self.export());
7485 }
75- match &*self.state.lock().unwrap_or_else(|e| e.into_inner()) {
86+ let waited_for = match &*self.state.lock().unwrap_or_else(|e| e.into_inner()) {
7687 State::Pending => unsafe {
77- self.shared
78- .device
79- .device
80- .wait_for_fences(&[self.fence], true, u64::MAX)
88+ self.shared.device.device.wait_for_fences(
89+ &[self.fence],
90+ true,
91+ WAIT.as_nanos() as u64,
92+ )
8193 },
8294 State::Exported(None) => Ok(()),
8395 State::Exported(Some(fd)) => {
84- readable(fd, None);
85- Ok(())
96+ let timeout = rustix::event::Timespec {
97+ tv_sec: WAIT.as_secs() as _,
98+ tv_nsec: 0,
99+ };
100+ if readable(fd, Some(&timeout)) {
101+ Ok(())
102+ } else {
103+ Err(vk::Result::TIMEOUT)
104+ }
86105 }
106+ };
107+ if waited_for.is_ok() {
108+ waited();
87109 }
110+ waited_for
88111 }
89112
90113 #[profiling::function]
@@ -157,7 +180,10 @@ impl Fence for VkFence {
157180 }
158181
159182 fn wait(&self) -> Result<(), Interrupted> {
160- self.0.wait().map_err(|_| Interrupted)
183+ self.0.wait().map_err(|e| {
184+ observe(e);
185+ Interrupted
186+ })
161187 }
162188
163189 fn is_exportable(&self) -> bool {