diff --git a/release-notes.md b/release-notes.md index ea2793e5..25dc169e 100644 --- a/release-notes.md +++ b/release-notes.md @@ -1,6 +1,10 @@ # Unreleased - Add remaining layer-shell features. +- Add JAY_MAX_RENDER_TIME_NSEC environment variable. + This can be used to delay rendering until shortly before a page flip, reducing input + delay. + This is an unstable feature that might change in the future. # 1.2.0 (2024-05-05) diff --git a/src/backends/metal/video.rs b/src/backends/metal/video.rs index c0752eee..13adee1f 100644 --- a/src/backends/metal/video.rs +++ b/src/backends/metal/video.rs @@ -18,6 +18,7 @@ use { renderer::RenderResult, state::State, theme::Color, + time::now_nsec, tree::OutputNode, udev::UdevDevice, utils::{ @@ -45,6 +46,7 @@ use { indexmap::{indexset, IndexSet}, isnt::std_1::collections::IsntHashMap2Ext, jay_config::video::GfxApi, + once_cell::sync::Lazy, std::{ any::Any, cell::{Cell, RefCell}, @@ -416,6 +418,7 @@ pub struct MetalConnector { pub can_present: Cell, pub has_damage: Cell, pub cursor_changed: Cell, + pub next_flip_nsec: Cell, pub display: RefCell, @@ -578,6 +581,20 @@ impl MetalConnector { async fn present_loop(self: Rc) { loop { self.present_trigger.triggered().await; + static DELTA: Lazy> = Lazy::new(|| { + if let Ok(max_render_time) = std::env::var("JAY_MAX_RENDER_TIME_NSEC") { + if let Ok(max_render_time) = max_render_time.parse() { + return Some(max_render_time); + } + } + None + }); + if let Some(delta) = *DELTA { + let next_present = self.next_flip_nsec.get().saturating_sub(delta); + if now_nsec() < next_present { + self.state.ring.timeout(next_present).await.unwrap(); + } + } match self.present(true) { Ok(_) => self.state.set_backend_idle(false), Err(e) => { @@ -1397,6 +1414,7 @@ fn create_connector( active_framebuffer: Default::default(), next_framebuffer: Default::default(), direct_scanout_active: Cell::new(false), + next_flip_nsec: Cell::new(0), }); let futures = ConnectorFutures { present: backend @@ -2161,6 +2179,9 @@ impl MetalBackend { connector.schedule_present(); } let dd = connector.display.borrow_mut(); + connector + .next_flip_nsec + .set(tv_sec as u64 * 1_000_000_000 + tv_usec as u64 * 1000 + dd.refresh as u64); { let global = self.state.root.outputs.get(&connector.connector_id); let mut rr = connector.render_result.borrow_mut(); diff --git a/src/io_uring.rs b/src/io_uring.rs index 0090ed4b..baf4c934 100644 --- a/src/io_uring.rs +++ b/src/io_uring.rs @@ -6,7 +6,7 @@ use { ops::{ accept::AcceptTask, async_cancel::AsyncCancelTask, connect::ConnectTask, poll::PollTask, read_write::ReadWriteTask, recvmsg::RecvmsgTask, - sendmsg::SendmsgTask, timeout::TimeoutTask, + sendmsg::SendmsgTask, timeout::TimeoutTask, timeout_link::TimeoutLinkTask, }, pending_result::PendingResults, sys::{ @@ -211,6 +211,7 @@ impl IoUring { cached_sendmsg: Default::default(), cached_recvmsg: Default::default(), cached_timeouts: Default::default(), + cached_timeout_links: Default::default(), cached_cmsg_bufs: Default::default(), cached_connects: Default::default(), cached_accepts: Default::default(), @@ -266,6 +267,7 @@ struct IoUringData { cached_sendmsg: Stack>, cached_recvmsg: Stack>, cached_timeouts: Stack>, + cached_timeout_links: Stack>, cached_cmsg_bufs: Stack, cached_connects: Stack>, cached_accepts: Stack>, diff --git a/src/io_uring/ops.rs b/src/io_uring/ops.rs index ba6f79f1..64ea36b7 100644 --- a/src/io_uring/ops.rs +++ b/src/io_uring/ops.rs @@ -8,6 +8,7 @@ pub mod read_write; pub mod recvmsg; pub mod sendmsg; pub mod timeout; +pub mod timeout_link; pub type TaskResult = Result, IoUringError>; diff --git a/src/io_uring/ops/read_write.rs b/src/io_uring/ops/read_write.rs index d8b8ba9f..9ebf2c1d 100644 --- a/src/io_uring/ops/read_write.rs +++ b/src/io_uring/ops/read_write.rs @@ -51,7 +51,7 @@ impl IoUring { }); self.ring.schedule(pw); if let Some(time) = timeout { - self.schedule_timeout(time); + self.schedule_timeout_link(time); } } Ok(pr.await.map(|v| v as usize)).merge() diff --git a/src/io_uring/ops/sendmsg.rs b/src/io_uring/ops/sendmsg.rs index 2ce5d84b..23cb7a8f 100644 --- a/src/io_uring/ops/sendmsg.rs +++ b/src/io_uring/ops/sendmsg.rs @@ -78,7 +78,7 @@ impl IoUring { st.has_timeout = timeout.is_some(); self.ring.schedule(st); if let Some(timeout) = timeout { - self.schedule_timeout(timeout); + self.schedule_timeout_link(timeout); } } Ok(pr.await? as _) diff --git a/src/io_uring/ops/timeout.rs b/src/io_uring/ops/timeout.rs index 09d6a327..c91aceb9 100644 --- a/src/io_uring/ops/timeout.rs +++ b/src/io_uring/ops/timeout.rs @@ -1,10 +1,8 @@ use { - crate::{ - io_uring::{ - sys::{io_uring_sqe, IORING_OP_LINK_TIMEOUT, IORING_TIMEOUT_ABS}, - IoUring, IoUringData, Task, - }, - time::Time, + crate::io_uring::{ + pending_result::PendingResult, + sys::{io_uring_sqe, IORING_OP_TIMEOUT, IORING_TIMEOUT_ABS}, + IoUring, IoUringData, IoUringError, Task, }, uapi::c, }; @@ -12,27 +10,35 @@ use { #[allow(non_camel_case_types)] #[repr(C)] #[derive(Default)] -struct timespec64 { - tv_sec: i64, - tv_nsec: c::c_long, +pub(super) struct timespec64 { + pub tv_sec: i64, + pub tv_nsec: c::c_long, } #[derive(Default)] pub struct TimeoutTask { id: u64, timespec: timespec64, + pr: Option, } impl IoUring { - pub(super) fn schedule_timeout(&self, timeout: Time) { - let id = self.ring.id_raw(); + pub async fn timeout(&self, timeout_nsec: u64) -> Result<(), IoUringError> { + self.ring.check_destroyed()?; + let id = self.ring.id(); + let pr = self.ring.pending_results.acquire(); { - let mut to = self.ring.cached_timeouts.pop().unwrap_or_default(); - to.id = id; - to.timespec.tv_sec = timeout.0.tv_sec as _; - to.timespec.tv_nsec = timeout.0.tv_nsec as _; - self.ring.schedule(to); + let mut pw = self.ring.cached_timeouts.pop().unwrap_or_default(); + pw.id = id.id; + pw.timespec = timespec64 { + tv_sec: (timeout_nsec / 1_000_000_000) as _, + tv_nsec: (timeout_nsec % 1_000_000_000) as _, + }; + pw.pr = Some(pr.clone()); + self.ring.schedule(pw); } + let _ = pr.await; + Ok(()) } } @@ -41,14 +47,18 @@ unsafe impl Task for TimeoutTask { self.id } - fn complete(self: Box, ring: &IoUringData, _res: i32) { + fn complete(mut self: Box, ring: &IoUringData, res: i32) { + if let Some(pr) = self.pr.take() { + pr.complete(res); + } ring.cached_timeouts.push(self); } fn encode(&self, sqe: &mut io_uring_sqe) { - sqe.opcode = IORING_OP_LINK_TIMEOUT; + sqe.opcode = IORING_OP_TIMEOUT; sqe.u2.addr = &self.timespec as *const _ as _; sqe.len = 1; sqe.u3.timeout_flags = IORING_TIMEOUT_ABS; + sqe.u1.off = 0; } } diff --git a/src/io_uring/ops/timeout_link.rs b/src/io_uring/ops/timeout_link.rs new file mode 100644 index 00000000..aabaa421 --- /dev/null +++ b/src/io_uring/ops/timeout_link.rs @@ -0,0 +1,44 @@ +use crate::{ + io_uring::{ + ops::timeout::timespec64, + sys::{io_uring_sqe, IORING_OP_LINK_TIMEOUT, IORING_TIMEOUT_ABS}, + IoUring, IoUringData, Task, + }, + time::Time, +}; + +#[derive(Default)] +pub struct TimeoutLinkTask { + id: u64, + timespec: timespec64, +} + +impl IoUring { + pub(super) fn schedule_timeout_link(&self, timeout: Time) { + let id = self.ring.id_raw(); + { + let mut to = self.ring.cached_timeout_links.pop().unwrap_or_default(); + to.id = id; + to.timespec.tv_sec = timeout.0.tv_sec as _; + to.timespec.tv_nsec = timeout.0.tv_nsec as _; + self.ring.schedule(to); + } + } +} + +unsafe impl Task for TimeoutLinkTask { + fn id(&self) -> u64 { + self.id + } + + fn complete(self: Box, ring: &IoUringData, _res: i32) { + ring.cached_timeout_links.push(self); + } + + fn encode(&self, sqe: &mut io_uring_sqe) { + sqe.opcode = IORING_OP_LINK_TIMEOUT; + sqe.u2.addr = &self.timespec as *const _ as _; + sqe.len = 1; + sqe.u3.timeout_flags = IORING_TIMEOUT_ABS; + } +} diff --git a/src/time.rs b/src/time.rs index 9537c2f0..f60ac160 100644 --- a/src/time.rs +++ b/src/time.rs @@ -62,7 +62,12 @@ impl Time { } } - #[allow(dead_code)] + pub fn nsec(self) -> u64 { + let sec = self.0.tv_sec as u64 * 1_000_000_000; + let nsec = self.0.tv_nsec as u64; + sec + nsec + } + pub fn usec(self) -> u64 { let sec = self.0.tv_sec as u64 * 1_000_000; let nsec = self.0.tv_nsec as u64 / 1_000; @@ -119,6 +124,10 @@ impl Add for Time { } } +pub fn now_nsec() -> u64 { + Time::now_unchecked().nsec() +} + pub fn now_usec() -> u64 { Time::now_unchecked().usec() }