Skip to main content

drmkit_sync/
lib.rs

1// SPDX-FileCopyrightText: (c) 2026 Joel Winarske
2// SPDX-License-Identifier: MIT
3
4//! `sync_file` fences for explicit synchronization.
5//!
6//! Port of `src/sync/` from drm-cxx @ `dc2915b`.
7//!
8//! A [`SyncFence`] wraps a kernel `sync_file` descriptor — the objects that
9//! flow through a plane's `IN_FENCE_FD` property and come back out of a commit
10//! through `OUT_FENCE_PTR`. The type owns its descriptor and closes it on drop.
11//!
12//! # Ownership
13//!
14//! The kernel does **not** close an fd passed as `IN_FENCE_FD`. The fence
15//! therefore keeps ownership across a commit and still closes the descriptor
16//! when dropped — handing [`SyncFence::as_fd`] to a property write does not
17//! transfer anything. This is the same contract the C++ `fd()` accessor
18//! documents, made structural by returning a [`BorrowedFd`].
19
20use std::io;
21use std::os::fd::{AsFd, BorrowedFd, OwnedFd};
22use std::time::Duration;
23
24use rustix::io::Errno;
25
26/// Errors from fence operations.
27///
28/// The variants mirror the C++ failure cases one for one, and converting to
29/// [`io::Error`] gives the errno the C++ `std::error_code` carries for each --
30/// `EBADF`, `ETIMEDOUT`, `EBADF`, or the syscall's own -- so a consumer
31/// bridging both implementations sees the same classification. In particular
32/// `EAGAIN`-style flow control never hides inside a generic I/O error.
33#[derive(Debug, thiserror::Error)]
34#[non_exhaustive]
35pub enum SyncError {
36    /// The fence holds no descriptor.
37    ///
38    /// The C++ reports `errc::bad_file_descriptor` for this, and for a
39    /// negative fd handed to `import_fd`. The latter case cannot arise here:
40    /// [`SyncFence::import`] takes a [`BorrowedFd`], so an invalid descriptor
41    /// is a type error rather than a runtime one.
42    #[error("fence holds no descriptor")]
43    Empty,
44
45    /// The wait expired before the fence signaled. `errc::timed_out`.
46    #[error("fence wait timed out")]
47    TimedOut,
48
49    /// The merge ioctl succeeded but returned no descriptor.
50    /// `errc::bad_file_descriptor`, as in the C++.
51    #[error("SYNC_IOC_MERGE returned no fence")]
52    MergeReturnedNothing,
53
54    /// An underlying syscall failed.
55    #[error("fence syscall failed: {0}")]
56    Io(#[from] Errno),
57}
58
59impl From<SyncError> for io::Error {
60    fn from(e: SyncError) -> Self {
61        match e {
62            SyncError::Empty | SyncError::MergeReturnedNothing => {
63                Self::from_raw_os_error(Errno::BADF.raw_os_error())
64            }
65            SyncError::TimedOut => Self::from_raw_os_error(Errno::TIMEDOUT.raw_os_error()),
66            SyncError::Io(errno) => Self::from_raw_os_error(errno.raw_os_error()),
67        }
68    }
69}
70
71/// `struct sync_merge_data` from `linux/sync_file.h`.
72#[repr(C)]
73#[derive(Debug, Clone, Copy)]
74struct SyncMergeData {
75    /// Name of the merged fence. The kernel copies it verbatim; unused here.
76    name: [u8; 32],
77    /// Descriptor of the fence to merge in.
78    fd2: i32,
79    /// Out: descriptor of the merged fence.
80    fence: i32,
81    flags: u32,
82    pad: u32,
83}
84
85/// `SYNC_IOC_MERGE`, i.e. `_IOWR(SYNC_IOC_MAGIC, 3, struct sync_merge_data)`
86/// with `SYNC_IOC_MAGIC == '>'`.
87const SYNC_IOC_MERGE: rustix::ioctl::Opcode =
88    rustix::ioctl::opcode::read_write::<SyncMergeData>(b'>', 3);
89
90/// A kernel `sync_file` fence.
91///
92/// Default-constructs empty, as the C++ does. Here a commit's `OUT_FENCE_PTR`
93/// comes back as an `OwnedFd`, which [`SyncFence::from_owned`] adopts.
94#[derive(Debug, Default)]
95pub struct SyncFence {
96    fd: Option<OwnedFd>,
97}
98
99impl SyncFence {
100    /// An empty fence holding no descriptor.
101    #[must_use]
102    pub const fn new() -> Self {
103        Self { fd: None }
104    }
105
106    /// Import a fence by **duplicating** `fd`.
107    ///
108    /// The caller keeps ownership of `fd` and may close it immediately; this
109    /// fence owns the duplicate. Matches the C++ `import_fd`, which dups for
110    /// the same reason.
111    ///
112    /// # Errors
113    ///
114    /// [`SyncError::Io`] if `dup` fails — in practice `EMFILE`.
115    pub fn import(fd: BorrowedFd<'_>) -> Result<Self, SyncError> {
116        let duped = fd.try_clone_to_owned().map_err(|e| {
117            // io::Error from a failed dup always carries an errno.
118            Errno::from_io_error(&e).unwrap_or(Errno::MFILE)
119        })?;
120        Ok(Self { fd: Some(duped) })
121    }
122
123    /// Take ownership of an existing descriptor without duplicating it.
124    ///
125    /// The natural constructor when a commit has just produced an
126    /// `OUT_FENCE_PTR` descriptor that nothing else holds. The C++ has no
127    /// equivalent: it cannot express "this fd is yours now" in a type, so it
128    /// dups unconditionally and the caller closes its copy.
129    #[must_use]
130    pub const fn from_owned(fd: OwnedFd) -> Self {
131        Self { fd: Some(fd) }
132    }
133
134    /// Whether this holds a real `sync_file` descriptor.
135    #[must_use]
136    pub const fn is_valid(&self) -> bool {
137        self.fd.is_some()
138    }
139
140    /// The underlying descriptor, for a plane's `IN_FENCE_FD` property.
141    ///
142    /// Borrowed, never transferred: the kernel does not close `IN_FENCE_FD`,
143    /// so this fence still closes it on drop.
144    #[must_use]
145    pub fn as_fd(&self) -> Option<BorrowedFd<'_>> {
146        self.fd.as_ref().map(AsFd::as_fd)
147    }
148
149    /// Release the descriptor to the caller, consuming the fence; `None` if it
150    /// held none.
151    #[must_use]
152    pub fn into_owned_fd(self) -> Option<OwnedFd> {
153        self.fd
154    }
155
156    /// Wait for the fence to signal, up to `timeout`.
157    ///
158    /// # Interruption
159    ///
160    /// A signal landing mid-wait surfaces as `SyncError::Io(Errno::INTR)`,
161    /// matching the C++, which returns `EINTR` to its caller. This is
162    /// deliberately **not** the `PageFlip::dispatch` contract (invariant 3),
163    /// which retries internally: a fence wait is a caller-owned wait, and
164    /// swallowing `EINTR` here would make the timeout unobservable to a caller
165    /// that wants to act on the signal.
166    ///
167    /// # Errors
168    ///
169    /// - [`SyncError::Empty`] if the fence holds no descriptor.
170    /// - [`SyncError::TimedOut`] if `timeout` elapsed first.
171    /// - [`SyncError::Io`] if `poll` failed.
172    pub fn wait(&self, timeout: Duration) -> Result<(), SyncError> {
173        let fd = self.as_fd().ok_or(SyncError::Empty)?;
174
175        let mut fds = [rustix::event::PollFd::new(
176            &fd,
177            rustix::event::PollFlags::IN,
178        )];
179        // rustix takes a Timespec, so unlike the C++ -- which clamps
180        // milliseconds into an int -- there is nothing to saturate.
181        let spec = rustix::event::Timespec {
182            tv_sec: timeout.as_secs().try_into().unwrap_or(i64::MAX),
183            tv_nsec: timeout.subsec_nanos().into(),
184        };
185
186        match rustix::event::poll(&mut fds, Some(&spec))? {
187            0 => Err(SyncError::TimedOut),
188            _ => Ok(()),
189        }
190    }
191
192    /// Merge `other` into this fence.
193    ///
194    /// The merged fence signals when both inputs have. `other` is consumed;
195    /// this fence's descriptor is replaced by the merged one.
196    ///
197    /// # Errors
198    ///
199    /// - [`SyncError::Empty`] if either fence holds no descriptor.
200    /// - [`SyncError::MergeReturnedNothing`] if the ioctl reported success but
201    ///   produced no descriptor.
202    /// - [`SyncError::Io`] if the ioctl failed.
203    pub fn merge(&mut self, other: Self) -> Result<(), SyncError> {
204        let ours = self.as_fd().ok_or(SyncError::Empty)?;
205        let theirs = other.as_fd().ok_or(SyncError::Empty)?;
206
207        let mut data = SyncMergeData {
208            name: [0; 32],
209            fd2: rustix::fd::AsRawFd::as_raw_fd(&theirs),
210            fence: -1,
211            flags: 0,
212            pad: 0,
213        };
214
215        // SAFETY: SYNC_IOC_MERGE is _IOWR(SYNC_IOC_MAGIC, 3, struct
216        // sync_merge_data) and `SyncMergeData` is a #[repr(C)] definition of
217        // exactly that struct, so the opcode and the value type agree. `ours`
218        // is a live sync_file descriptor for the duration of the call, and
219        // `data.fd2` borrows `theirs`, which outlives the call because `other`
220        // is still owned here.
221        unsafe {
222            rustix::ioctl::ioctl(
223                ours,
224                rustix::ioctl::Updater::<SYNC_IOC_MERGE, SyncMergeData>::new(&mut data),
225            )?;
226        }
227
228        // Explicit: `other` is consumed, and dropping it closes its descriptor.
229        // The C++ relies on the destructor at end of scope for the same effect
230        // ("other's destructor will close its fd"). Doing it here, after the
231        // ioctl has read `fd2`, makes the ordering visible rather than implied.
232        drop(other);
233
234        if data.fence < 0 {
235            return Err(SyncError::MergeReturnedNothing);
236        }
237
238        // SAFETY: the ioctl succeeded and reported a non-negative descriptor,
239        // which the kernel has just created and handed to this process. Taking
240        // ownership here is what closes it; nothing else holds a copy.
241        let merged = unsafe { <OwnedFd as rustix::fd::FromRawFd>::from_raw_fd(data.fence) };
242
243        // Replaces -- and so closes -- our previous descriptor.
244        self.fd = Some(merged);
245        Ok(())
246    }
247}
248
249#[cfg(test)]
250mod tests;