coreshift_core/drm.rs
1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Direct Rendering Manager (DRM) vblank primitives.
6//!
7//! The public API is intentionally narrow: open a card node, then block on
8//! `DRM_IOCTL_WAIT_VBLANK` until the next vertical blank, reporting the
9//! monotonic instant the wait returned. Android's `msm` display stack zeroes
10//! the ioctl reply timestamps (`t_sec`/`t_usec`), so callers must use a
11//! monotonic clock taken at the moment the wait unblocks — this module does
12//! exactly that and never exposes the (unreliable) reply fields.
13
14use std::os::unix::io::AsRawFd;
15use std::time::Instant;
16
17use crate::CoreError;
18use crate::error::syscall_ret;
19use crate::fd::Fd;
20
21const O_RDWR: i32 = libc::O_RDWR;
22const O_CLOEXEC: i32 = libc::O_CLOEXEC;
23
24// _IOC(dir, type, nr, size) = (dir << 30) | (type << 8) | (nr << 0) | (size << 16)
25// DRM_IOCTL_BASE is 'd' (0x64); DRM_IOCTL_WAIT_VBLANK is _IOWR(0x3a, drm_wait_vblank).
26const DRM_IOCTL_BASE: u32 = 0x64; // 'd'
27const DRM_IOCTL_WAIT_VBLANK_NR: u32 = 0x3a;
28
29// DRM_VBLANK_RELATIVE (wait relative to current counter), as used for the
30// common "wake me at the next vblank" blocking call.
31const DRM_VBLANK_RELATIVE: u32 = 0x1;
32
33#[repr(C)]
34#[derive(Clone, Copy)]
35struct DrmWaitVBlankRequest {
36 // enum drm_vblank_seq_type
37 r#type: u32,
38 sequence: u32,
39 // __aligned_u64 signal (unsigned long on LP64)
40 signal: u64,
41}
42
43#[repr(C)]
44#[derive(Clone, Copy)]
45struct DrmWaitVBlankReply {
46 r#type: u32,
47 sequence: u32,
48 tval_sec: i64,
49 tval_usec: i64,
50}
51
52/// `union drm_wait_vblank` from `<drm/drm.h>`. The request and reply overlay
53/// each other; only the request is ever written by this module (the reply
54/// timestamps are unreliable on `msm` stacks and deliberately ignored).
55#[repr(C)]
56#[derive(Clone, Copy)]
57union DrmWaitVBlank {
58 request: DrmWaitVBlankRequest,
59 reply: DrmWaitVBlankReply,
60}
61
62// LP64 (aarch64/x86_64) kernel ABI: request=16 bytes, reply=24 bytes, so the
63// union is 24 bytes. The previous hand-rolled 32-byte struct encoded the wrong
64// `_IOC_SIZE` (0xC020643A vs the real 0xc018643a), which copy_to_user would
65// round with 8 bytes of kernel-stack garbage. Fail loudly on any layout that
66// does not match the LP64 header rather than silently send a wrong size.
67const _: () = assert!(std::mem::size_of::<DrmWaitVBlank>() == 24);
68const _: () = assert!(std::mem::size_of::<DrmWaitVBlankRequest>() == 16);
69
70/// An open DRM card node (e.g. `/dev/dri/card0`).
71///
72/// The fd is `O_CLOEXEC` and owned by this wrapper; it is closed on drop.
73pub struct DrmCard {
74 fd: Fd,
75}
76
77impl DrmCard {
78 /// Open a DRM primary node by device path.
79 ///
80 /// ### Errors
81 /// - `ENOENT`/`ENODEV`: no such card node.
82 /// - `EACCES`/`EPERM`: insufficient privilege for the device node.
83 pub fn open(path: &str) -> Result<Self, CoreError> {
84 let c_path = std::ffi::CString::new(path)
85 .map_err(|_| CoreError::sys(libc::EINVAL, "drm open path contains NUL"))?;
86 let fd = unsafe { libc::open(c_path.as_ptr(), O_RDWR | O_CLOEXEC) };
87 syscall_ret(fd, "open(drm)")?;
88 // SAFETY: `fd` is a freshly opened owned descriptor from `open`.
89 let fd = unsafe { Fd::from_owned_raw_fd(fd, "drm") }?;
90 Ok(Self { fd })
91 }
92
93 /// Block until the next vblank and return the monotonic instant the wait
94 /// returned.
95 ///
96 /// The reply `t_sec`/`t_usec` fields are deliberately ignored: they are
97 /// zeroed on `msm` display stacks. Only the wait-unblock instant is
98 /// trustworthy, so callers diff consecutive return values.
99 ///
100 /// ### Errors
101 /// - `EINTR`: interrupted by a signal (safe to retry).
102 /// - `EINVAL`: the card does not support vblank waits.
103 /// - `EBADF`: the card fd was closed.
104 pub fn wait_vblank(&self) -> Result<Instant, CoreError> {
105 let mut vblank = DrmWaitVBlank {
106 request: DrmWaitVBlankRequest {
107 r#type: DRM_VBLANK_RELATIVE,
108 sequence: 1,
109 signal: 0,
110 },
111 };
112
113 let nr = DRM_IOCTL_WAIT_VBLANK_NR;
114 let size = std::mem::size_of::<DrmWaitVBlank>() as u32;
115 let ioctl_nr: libc::Ioctl =
116 ((3u32 << 30) | (DRM_IOCTL_BASE << 8) | nr | (size << 16)) as libc::Ioctl;
117
118 // The vblank wait is a blocking ioctl; a signal (e.g. the per-thread
119 // shutdown interrupt) surfaces as EINTR and is propagated to the caller.
120 let ret = unsafe { libc::ioctl(self.fd.as_raw_fd(), ioctl_nr, &mut vblank) };
121 syscall_ret(ret, "DRM_IOCTL_WAIT_VBLANK")?;
122 Ok(Instant::now())
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 #[test]
131 fn wait_vblank_ioctl_matches_known_constant() {
132 // (3 << 30) | ('d' << 8) | (0x3a) | (size(24) << 16) == 0xC018643A,
133 // matching `DRM_IOCTL_WAIT_VBLANK` (`_IOWR(0x3a, union drm_wait_vblank)`)
134 // on the LP64 kernel ABI where the union is 24 bytes.
135 let size = std::mem::size_of::<DrmWaitVBlank>() as u32;
136 let computed: libc::Ioctl =
137 ((3u32 << 30) | (DRM_IOCTL_BASE << 8) | (0x3au32) | (size << 16)) as libc::Ioctl;
138 assert_eq!(computed, 0xC018643Au32 as libc::Ioctl);
139 assert_eq!(size, 24);
140 }
141
142 #[test]
143 fn vblank_request_union_layout_is_24_bytes() {
144 assert_eq!(std::mem::size_of::<DrmWaitVBlank>(), 24);
145 assert_eq!(std::mem::size_of::<DrmWaitVBlankRequest>(), 16);
146 }
147}