use std::collections::HashMap;
use std::os::fd::{AsFd as _, BorrowedFd, OwnedFd, RawFd};
use std::time::Duration;
use drm::control::Device as _;
use drmkit_core::Device;
use drmkit_time::{Clock, SteadyClock, Timestamp};
use rustix::event::epoll;
use crate::error::{ModesetError, Result};
const MAX_DISPATCH_EVENTS: usize = 16;
pub type Handler = Box<dyn FnMut(PageFlipEvent) + Send>;
pub type SourceCallback = Box<dyn FnMut() + Send>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct PageFlipEvent {
pub crtc_id: u32,
pub sequence: u64,
pub timestamp: Timestamp,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum Timeout {
#[default]
Infinite,
Poll,
Bounded(Duration),
}
pub struct PageFlip {
epoll: OwnedFd,
drm_fd: Option<RawFd>,
handler: Option<Handler>,
sources: HashMap<RawFd, SourceCallback>,
clock: &'static SteadyClock,
}
impl core::fmt::Debug for PageFlip {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("PageFlip")
.field("drm_fd", &self.drm_fd)
.field("has_handler", &self.handler.is_some())
.field("sources", &self.sources.len())
.finish_non_exhaustive()
}
}
impl PageFlip {
pub fn new(device: &Device) -> Result<Self> {
let mut flip = Self::without_device()?;
flip.register_drm_fd(device.raw_fd())?;
Ok(flip)
}
pub fn without_device() -> Result<Self> {
Ok(Self {
epoll: epoll::create(epoll::CreateFlags::CLOEXEC)?,
drm_fd: None,
handler: None,
sources: HashMap::new(),
clock: drmkit_time::default_clock(),
})
}
pub fn set_handler(&mut self, handler: Handler) {
self.handler = Some(handler);
}
#[must_use]
pub fn epoll_fd(&self) -> BorrowedFd<'_> {
self.epoll.as_fd()
}
fn register_drm_fd(&mut self, fd: RawFd) -> Result<()> {
let borrowed = unsafe { BorrowedFd::borrow_raw(fd) };
epoll::add(
&self.epoll,
borrowed,
epoll::EventData::new_u64(fd_key(fd)),
epoll::EventFlags::IN,
)?;
self.drm_fd = Some(fd);
Ok(())
}
pub fn add_source(&mut self, fd: RawFd, callback: SourceCallback) -> Result<()> {
if fd < 0 || self.sources.contains_key(&fd) || self.drm_fd == Some(fd) {
return Err(ModesetError::InvalidSource);
}
let borrowed = unsafe { BorrowedFd::borrow_raw(fd) };
epoll::add(
&self.epoll,
borrowed,
epoll::EventData::new_u64(fd_key(fd)),
epoll::EventFlags::IN,
)?;
self.sources.insert(fd, callback);
Ok(())
}
pub fn remove_source(&mut self, fd: RawFd) {
if self.sources.remove(&fd).is_some() {
let borrowed = unsafe { BorrowedFd::borrow_raw(fd) };
let _ = epoll::delete(&self.epoll, borrowed);
}
}
pub fn rebind(&mut self, device: &Device) -> Result<()> {
if let Some(old) = self.drm_fd.take() {
let borrowed = unsafe { BorrowedFd::borrow_raw(old) };
let _ = epoll::delete(&self.epoll, borrowed);
}
self.register_drm_fd(device.raw_fd())
}
pub fn dispatch(&mut self, timeout: Timeout) -> Result<()> {
if self.drm_fd.is_none() && self.sources.is_empty() {
return Err(ModesetError::NothingToWaitOn);
}
let ready = self.wait_retrying_eintr(timeout)?;
if ready.is_empty() {
return Err(ModesetError::TimedOut);
}
self.route(&ready);
Ok(())
}
fn wait_retrying_eintr(&self, timeout: Timeout) -> Result<Vec<(RawFd, epoll::EventFlags)>> {
let mut events: Vec<epoll::Event> = Vec::with_capacity(MAX_DISPATCH_EVENTS);
let deadline = match timeout {
Timeout::Bounded(budget) => Some(self.clock.now() + budget),
Timeout::Infinite | Timeout::Poll => None,
};
loop {
let remaining = remaining_budget(timeout, deadline, self.clock.now());
let spec = remaining.map(|d| rustix::event::Timespec {
tv_sec: d.as_secs().try_into().unwrap_or(i64::MAX),
tv_nsec: d.subsec_nanos().into(),
});
events.clear();
match epoll::wait(
&self.epoll,
rustix::buffer::spare_capacity(&mut events),
spec.as_ref(),
) {
Ok(_ready) => {
return Ok(events
.iter()
.map(|event| (key_to_fd(event.data.u64()), event.flags))
.collect());
}
Err(rustix::io::Errno::INTR) => {}
Err(e) => return Err(ModesetError::Io(e)),
}
}
}
fn route(&mut self, ready: &[(RawFd, epoll::EventFlags)]) {
for (fd, _flags) in ready {
if Some(*fd) == self.drm_fd {
self.handle_drm_events(*fd);
continue;
}
if let Some(callback) = self.sources.get_mut(fd) {
callback();
}
}
}
fn handle_drm_events(&mut self, fd: RawFd) {
let Some(handler) = self.handler.as_mut() else {
drain_drm_events(fd);
return;
};
let borrowed = unsafe { BorrowedFd::borrow_raw(fd) };
let reader = DrmEventReader { fd: borrowed };
let Ok(events) = reader.receive_events() else {
return;
};
for event in events {
if let drm::control::Event::PageFlip(flip) = event {
handler(PageFlipEvent {
crtc_id: flip.crtc.into(),
sequence: u64::from(flip.frame),
timestamp: Timestamp::from_nanos(
u64::try_from(flip.duration.as_nanos()).unwrap_or(u64::MAX),
),
});
}
}
}
}
#[expect(
clippy::match_same_arms,
reason = "a poll and an unmeasurable bounded wait both yield zero for \
different reasons; merging them would erase why"
)]
pub(crate) fn remaining_budget(
timeout: Timeout,
deadline: Option<Timestamp>,
now: Timestamp,
) -> Option<Duration> {
match (timeout, deadline) {
(Timeout::Infinite, _) => None,
(Timeout::Poll, _) => Some(Duration::ZERO),
(Timeout::Bounded(_), Some(deadline)) => Some(deadline.saturating_duration_since(now)),
(Timeout::Bounded(_), None) => Some(Duration::ZERO),
}
}
fn drain_drm_events(fd: RawFd) {
let borrowed = unsafe { BorrowedFd::borrow_raw(fd) };
let reader = DrmEventReader { fd: borrowed };
let _ = reader.receive_events();
}
struct DrmEventReader<'a> {
fd: BorrowedFd<'a>,
}
impl std::os::fd::AsFd for DrmEventReader<'_> {
fn as_fd(&self) -> BorrowedFd<'_> {
self.fd
}
}
impl drm::Device for DrmEventReader<'_> {}
impl drm::control::Device for DrmEventReader<'_> {}
const fn fd_key(fd: RawFd) -> u64 {
fd.cast_unsigned() as u64
}
const fn key_to_fd(key: u64) -> RawFd {
#[expect(
clippy::cast_possible_truncation,
reason = "the key was produced by fd_key from a 32-bit descriptor, so \
the high half is always zero"
)]
let narrowed = key as u32;
narrowed.cast_signed()
}
impl Drop for PageFlip {
fn drop(&mut self) {
}
}