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;