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