use nix::ioctl_write_ptr;
use std::os::fd::{AsRawFd, BorrowedFd, FromRawFd, OwnedFd};
use std::sync::OnceLock;
const DMA_BUF_BASE: u8 = b'b';
const DMA_BUF_IOCTL_SYNC: u8 = 0;
const DMA_BUF_SYNC_READ: u64 = 1;
const DMA_BUF_SYNC_WRITE: u64 = 2;
const DMA_BUF_SYNC_RW: u64 = 3;
const DMA_BUF_SYNC_START: u64 = 0;
const DMA_BUF_SYNC_END: u64 = 4;
#[derive(Default)]
#[repr(C)]
struct DmaBufSync {
flags: u64,
}
ioctl_write_ptr!(
ioctl_dma_buf_sync,
DMA_BUF_BASE,
DMA_BUF_IOCTL_SYNC,
DmaBufSync
);
fn sync(fd: &OwnedFd, flags: u64) -> nix::Result<()> {
let sync = DmaBufSync { flags };
unsafe { ioctl_dma_buf_sync(fd.as_raw_fd(), &sync) }?;
Ok(())
}
fn sync_flags(start: bool, access: crate::CpuAccess) -> u64 {
let mut flags = 0;
if access.reads() {
flags |= DMA_BUF_SYNC_READ;
}
if access.writes() {
flags |= DMA_BUF_SYNC_WRITE;
}
if flags == 0 {
flags = DMA_BUF_SYNC_RW;
}
flags |= if start {
DMA_BUF_SYNC_START
} else {
DMA_BUF_SYNC_END
};
flags
}
#[cfg(test)]
mod sync_counter {
thread_local! {
pub(crate) static SYNC_CALLS: std::cell::Cell<(usize, usize)> =
const { std::cell::Cell::new((0, 0)) };
}
}
#[cfg(test)]
pub(crate) use sync_counter::SYNC_CALLS;
pub(crate) fn sync_access(fd: &OwnedFd, start: bool, access: crate::CpuAccess) -> nix::Result<()> {
#[cfg(test)]
SYNC_CALLS.with(|c| {
let (s, e) = c.get();
c.set(if start { (s + 1, e) } else { (s, e + 1) });
});
sync(fd, sync_flags(start, access))
}
const DRM_IOCTL_BASE: u8 = b'd';
#[repr(C)]
struct DrmPrimeHandle {
handle: u32,
flags: u32,
fd: i32,
}
const DRM_IOCTL_PRIME_FD_TO_HANDLE: nix::sys::ioctl::ioctl_num_type =
nix::request_code_readwrite!(DRM_IOCTL_BASE, 0x2e, std::mem::size_of::<DrmPrimeHandle>());
#[repr(C)]
struct DrmGemClose {
handle: u32,
pad: u32,
}
const DRM_IOCTL_GEM_CLOSE: nix::sys::ioctl::ioctl_num_type =
nix::request_code_write!(DRM_IOCTL_BASE, 0x09, std::mem::size_of::<DrmGemClose>());
static SHARED_DRM_FD: OnceLock<Option<OwnedFd>> = OnceLock::new();
fn shared_drm_fd() -> Option<BorrowedFd<'static>> {
SHARED_DRM_FD
.get_or_init(|| {
let path = b"/dev/dri/renderD128\0";
let raw_fd = unsafe {
libc::open(
path.as_ptr() as *const libc::c_char,
libc::O_RDWR | libc::O_CLOEXEC,
)
};
if raw_fd < 0 {
log::debug!(
"DrmAttachment: /dev/dri/renderD128 not available: {}",
std::io::Error::last_os_error()
);
None
} else {
log::debug!("DrmAttachment: opened shared /dev/dri/renderD128");
Some(unsafe { OwnedFd::from_raw_fd(raw_fd) })
}
})
.as_ref()
.map(|fd| unsafe { BorrowedFd::borrow_raw(fd.as_raw_fd()) })
}
#[derive(Debug)]
pub(crate) struct DrmAttachment {
gem_handle: u32,
}
impl DrmAttachment {
pub(crate) fn new(dma_buf_fd: &OwnedFd, imported: bool) -> Option<Self> {
let drm_fd = shared_drm_fd()?;
let mut prime = DrmPrimeHandle {
handle: 0,
flags: 0,
fd: dma_buf_fd.as_raw_fd(),
};
let ret =
unsafe { libc::ioctl(drm_fd.as_raw_fd(), DRM_IOCTL_PRIME_FD_TO_HANDLE, &mut prime) };
if ret == -1 {
let err = std::io::Error::last_os_error();
if imported {
log::debug!("DrmAttachment: PRIME_FD_TO_HANDLE failed (imported fd): {err}");
} else {
log::warn!("DrmAttachment: PRIME_FD_TO_HANDLE failed: {err}");
}
return None;
}
log::trace!("DrmAttachment: imported as GEM handle {}", prime.handle);
Some(Self {
gem_handle: prime.handle,
})
}
}
impl Drop for DrmAttachment {
fn drop(&mut self) {
if let Some(drm_fd) = shared_drm_fd() {
let close = DrmGemClose {
handle: self.gem_handle,
pad: 0,
};
unsafe { libc::ioctl(drm_fd.as_raw_fd(), DRM_IOCTL_GEM_CLOSE, &close) };
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::CpuAccess;
fn dev_null() -> OwnedFd {
OwnedFd::from(std::fs::File::open("/dev/null").expect("open /dev/null"))
}
#[test]
fn sync_flags_match_the_dma_buf_uapi_for_every_direction() {
let table = [
(true, CpuAccess::Read, 1),
(true, CpuAccess::Write, 2),
(true, CpuAccess::ReadWrite, 3),
(true, CpuAccess::None, 3),
(false, CpuAccess::Read, 5),
(false, CpuAccess::Write, 6),
(false, CpuAccess::ReadWrite, 7),
(false, CpuAccess::None, 7),
];
for (start, access, want) in table {
assert_eq!(
sync_flags(start, access),
want,
"start={start} access={access:?}"
);
}
}
#[test]
#[cfg(any(
target_arch = "x86_64",
target_arch = "x86",
target_arch = "aarch64",
target_arch = "arm",
target_arch = "riscv64"
))]
fn drm_ioctl_request_codes_match_the_kernel_uapi() {
assert_eq!(DRM_IOCTL_PRIME_FD_TO_HANDLE as u32, 0xC00C_642E);
assert_eq!(DRM_IOCTL_GEM_CLOSE as u32, 0x4008_6409);
}
#[test]
fn sync_on_a_fd_that_is_not_a_dma_buf_reports_enotty() {
let fd = dev_null();
for start in [true, false] {
assert_eq!(
sync_access(&fd, start, CpuAccess::ReadWrite),
Err(nix::errno::Errno::ENOTTY),
"start={start}"
);
}
}
#[test]
fn sync_calls_are_counted_per_bracket_half() {
let fd = dev_null();
let before = SYNC_CALLS.with(|c| c.get());
let _ = sync_access(&fd, true, CpuAccess::Read);
let _ = sync_access(&fd, false, CpuAccess::Read);
let _ = sync_access(&fd, false, CpuAccess::Read);
let after = SYNC_CALLS.with(|c| c.get());
assert_eq!((after.0 - before.0, after.1 - before.1), (1, 2));
}
#[test]
fn shared_drm_fd_opens_the_render_node_read_write_close_on_exec() {
let openable = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open("/dev/dri/renderD128")
.is_ok();
let shared = shared_drm_fd();
assert_eq!(shared.is_some(), openable);
if let Some(fd) = shared {
let flags = nix::fcntl::fcntl(fd, nix::fcntl::FcntlArg::F_GETFD).expect("F_GETFD");
assert_ne!(
flags & libc::FD_CLOEXEC,
0,
"shared DRM fd lacks FD_CLOEXEC"
);
let fl = nix::fcntl::fcntl(fd, nix::fcntl::FcntlArg::F_GETFL).expect("F_GETFL");
assert_eq!(
fl & libc::O_ACCMODE,
libc::O_RDWR,
"shared DRM fd is not O_RDWR"
);
}
}
#[test]
#[cfg(not(miri))]
fn dropping_a_drm_attachment_closes_its_gem_handle() {
const NAME: &str = "dropping_a_drm_attachment_closes_its_gem_handle";
if !crate::test_support::dma_or_skip(NAME) {
return;
}
let heap = dma_heap::Heap::new(dma_heap::HeapKind::Cma)
.or_else(|_| dma_heap::Heap::new(dma_heap::HeapKind::System))
.expect("dma heap");
let buf = heap.allocate(4096).expect("allocate");
let Some(attachment) = DrmAttachment::new(&buf, true) else {
crate::test_support::report_skip(&format!(
"{NAME} - the render node does not accept a PRIME import of a heap buffer"
));
return;
};
let handle = attachment.gem_handle;
drop(attachment);
let drm_fd = shared_drm_fd().expect("an attachment implies a render node");
let close = DrmGemClose { handle, pad: 0 };
let ret = unsafe { libc::ioctl(drm_fd.as_raw_fd(), DRM_IOCTL_GEM_CLOSE, &close) };
let err = std::io::Error::last_os_error();
assert_eq!(ret, -1, "GEM handle {handle} was still open after drop");
assert_eq!(err.raw_os_error(), Some(libc::EINVAL));
}
}