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;