emblema_hal/sync.rs
1//! GPU completion, and handing it to something outside this process.
2
3use crate::error::Result;
4use std::time::Duration;
5
6/// A signal that GPU work has completed.
7///
8/// This is the second of the two requirements that exist in the HAL from day
9/// one for the sake of the DRM path. Explicit sync is the design center of the
10/// frame loop: on the DRM path the render-done primitive is exported as a
11/// `sync_file` fd and attached to the atomic commit as `IN_FENCE_FD`, so the
12/// kernel latches the flip when the fence signals. Neither `glFinish` nor
13/// `vkDeviceWaitIdle` belongs in the frame loop on any path.
14///
15/// Backing primitives differ per backend — Vulkan fences, `GLsync` objects,
16/// sync_file fds — and the fence waiter thread retires all of them uniformly.
17pub trait HalFence: Send + Sync + 'static {
18 /// Whether the fence has signaled, without blocking.
19 fn is_signaled(&self) -> Result<bool>;
20
21 /// Block until the fence signals or the timeout elapses.
22 ///
23 /// Returns `Ok(true)` on signal and `Ok(false)` on timeout, so an expected
24 /// timeout is not an error. A caller that treats timeout as fatal checks
25 /// the returned value.
26 fn wait(&self, timeout: Duration) -> Result<bool>;
27
28 /// Export as a `sync_file` fd for a consumer outside this process.
29 ///
30 /// Returns [`crate::Error::Unsupported`] where the driver cannot export.
31 /// Callers check [`crate::SyncSupport::export_sync_file`] first and fall
32 /// back to a CPU-side wait before commit — correct, slower, and worth
33 /// logging loudly, since on Tier-1 hardware it is a driver bug to chase
34 /// rather than a state to settle into.
35 #[cfg(unix)]
36 fn export_sync_file(&self) -> Result<std::os::fd::OwnedFd> {
37 Err(crate::Error::Unsupported("sync_file export"))
38 }
39}
40
41/// A fence for a backend that cannot submit without waiting.
42///
43/// Uninhabited, so every method is unreachable: no value can exist to call one
44/// on. That is a stronger statement than a type whose methods return errors,
45/// which would be constructible and could reach a caller expecting a real
46/// fence.
47impl HalFence for std::convert::Infallible {
48 fn is_signaled(&self) -> Result<bool> {
49 match *self {}
50 }
51
52 fn wait(&self, _timeout: Duration) -> Result<bool> {
53 match *self {}
54 }
55}
56
57/// How long a frame-loop wait should tolerate before it is considered a hang.
58///
59/// Generous relative to any real frame: a wait reaching this has hit a lost
60/// device or a fence that will never signal, not a slow frame.
61pub const FRAME_WAIT_TIMEOUT: Duration = Duration::from_secs(5);
62
63#[cfg(test)]
64mod tests {
65 use super::*;
66 use crate::Error;
67 use std::sync::atomic::{AtomicBool, Ordering};
68
69 struct TestFence {
70 signaled: AtomicBool,
71 }
72
73 impl HalFence for TestFence {
74 fn is_signaled(&self) -> Result<bool> {
75 Ok(self.signaled.load(Ordering::Acquire))
76 }
77
78 fn wait(&self, _timeout: Duration) -> Result<bool> {
79 self.is_signaled()
80 }
81 }
82
83 #[test]
84 fn fence_export_defaults_to_unsupported_rather_than_panicking() {
85 let fence = TestFence {
86 signaled: AtomicBool::new(false),
87 };
88 // A backend that has not implemented export must degrade to the
89 // CPU-wait fallback, so the default has to be a recoverable error.
90 #[cfg(unix)]
91 assert!(matches!(
92 fence.export_sync_file(),
93 Err(Error::Unsupported(_))
94 ));
95 assert!(!fence.is_signaled().unwrap());
96 }
97
98 #[test]
99 fn timeout_is_reported_as_a_value_not_an_error() {
100 let fence = TestFence {
101 signaled: AtomicBool::new(false),
102 };
103 // Waiting out a frame slot that is still in flight is ordinary, so it
104 // must not surface as Err.
105 assert!(!fence.wait(Duration::from_millis(0)).unwrap());
106 fence.signaled.store(true, Ordering::Release);
107 assert!(fence.wait(Duration::from_millis(0)).unwrap());
108 }
109}