drmkit_time/lib.rs
1// SPDX-FileCopyrightText: (c) 2026 Joel Winarske
2// SPDX-License-Identifier: MIT
3
4//! drmkit's library-wide monotonic clock abstraction.
5//!
6//! Port of `src/time/clock.{hpp,cpp}` from drm-cxx @ `dc2915b`.
7//!
8//! A [`Clock`] lets a caller:
9//!
10//! - sync animation with an external presentation clock — a compositor that
11//! already tracks vblank/present timestamps hands the same time point to
12//! drmkit and to its own renderers;
13//! - freeze or step time in tests without sleeping (see [`ManualClock`]).
14//!
15//! In this tree, page-flip dispatch samples [`default_clock`] for its timeout
16//! budget, and the cursor renderer takes elapsed time from its caller rather
17//! than a clock. Callers who do not care get [`default_clock`] and never
18//! construct a clock.
19//!
20//! # Why a raw nanosecond count and not `std::time::Instant`
21//!
22//! [`Timestamp`] is a bare `CLOCK_MONOTONIC` nanosecond count, not an
23//! `Instant`. `Instant` is opaque: it cannot be constructed from a value, so
24//! it can express neither of the bullets above. The kernel hands
25//! back page-flip and vblank timestamps as `CLOCK_MONOTONIC` — the whole point
26//! of the abstraction is that a compositor can pass one of *those* straight in.
27//!
28//! This is a deliberate deviation from the C++, which lifts
29//! `std::chrono::steady_clock`'s `time_since_epoch()` onto its own tag.
30//! `steady_clock` is `CLOCK_MONOTONIC` on libstdc++ and libc++ in practice, so the values
31//! agree; naming `CLOCK_MONOTONIC` outright makes the contract explicit rather
32//! than incidental.
33
34use core::fmt;
35use core::ops::{Add, AddAssign, Sub, SubAssign};
36use core::time::Duration;
37
38/// A point on the monotonic timeline, in nanoseconds since an unspecified
39/// epoch (`CLOCK_MONOTONIC`, i.e. system boot on Linux).
40///
41/// Only intervals are meaningful; the absolute value carries no wall-clock
42/// meaning and must never be formatted as a date. The counter is monotonic and
43/// does not jump when the wall clock is stepped by NTP or by the user.
44///
45/// A `u64` nanosecond counter covers roughly 584 years of uptime, so overflow
46/// is not a practical concern — but arithmetic here saturates rather than
47/// wrapping, because a clock that silently runs backwards would turn a pacing
48/// bug into a torn frame.
49#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
50pub struct Timestamp(u64);
51
52impl Timestamp {
53 /// The zero point of the monotonic timeline. Useful as a "never" sentinel
54 /// and as the starting value for a [`ManualClock`].
55 pub const ZERO: Self = Self(0);
56
57 /// Build a timestamp from a raw `CLOCK_MONOTONIC` nanosecond count — for
58 /// example one the kernel reported with a page-flip event.
59 #[must_use]
60 pub const fn from_nanos(nanos: u64) -> Self {
61 Self(nanos)
62 }
63
64 /// Build a timestamp from a `CLOCK_MONOTONIC` `timespec` pair, the shape
65 /// DRM event structs and `libinput` report.
66 ///
67 /// Negative components clamp to zero: `CLOCK_MONOTONIC` never yields them,
68 /// and clamping keeps a malformed event from producing a timestamp that
69 /// sorts before [`Timestamp::ZERO`].
70 #[must_use]
71 pub const fn from_timespec(secs: i64, nanos: i64) -> Self {
72 let secs = if secs < 0 { 0 } else { secs.cast_unsigned() };
73 let nanos = if nanos < 0 { 0 } else { nanos.cast_unsigned() };
74 Self(secs.saturating_mul(1_000_000_000).saturating_add(nanos))
75 }
76
77 /// Build a timestamp from a `CLOCK_MONOTONIC` `timeval` pair (microsecond
78 /// resolution), the shape `drmEventVBlank` reports.
79 #[must_use]
80 pub const fn from_timeval(secs: i64, micros: i64) -> Self {
81 let secs = if secs < 0 { 0 } else { secs.cast_unsigned() };
82 let micros = if micros < 0 {
83 0
84 } else {
85 micros.cast_unsigned()
86 };
87 Self(
88 secs.saturating_mul(1_000_000_000)
89 .saturating_add(micros.saturating_mul(1_000)),
90 )
91 }
92
93 /// The raw nanosecond count.
94 #[must_use]
95 pub const fn as_nanos(self) -> u64 {
96 self.0
97 }
98
99 /// Interval from `earlier` to `self`, saturating at zero if `earlier` is
100 /// in fact later.
101 ///
102 /// Saturating is the right failure mode for a display pipeline: a
103 /// negative interval would otherwise wrap to ~584 years and stall pacing
104 /// until reboot.
105 ///
106 /// ```
107 /// use core::time::Duration;
108 /// use drmkit_time::Timestamp;
109 ///
110 /// let t0 = Timestamp::from_nanos(1_000);
111 /// let t1 = Timestamp::from_nanos(17_667_000);
112 /// assert_eq!(t1.saturating_duration_since(t0), Duration::from_nanos(17_666_000));
113 /// assert_eq!(t0.saturating_duration_since(t1), Duration::ZERO);
114 /// ```
115 #[must_use]
116 pub const fn saturating_duration_since(self, earlier: Self) -> Duration {
117 Duration::from_nanos(self.0.saturating_sub(earlier.0))
118 }
119
120 /// Interval from `earlier` to `self`, or `None` if `earlier` is later.
121 ///
122 /// Use this where going backwards indicates a bug worth surfacing rather
123 /// than absorbing.
124 #[must_use]
125 pub const fn checked_duration_since(self, earlier: Self) -> Option<Duration> {
126 match self.0.checked_sub(earlier.0) {
127 Some(delta) => Some(Duration::from_nanos(delta)),
128 None => None,
129 }
130 }
131}
132
133impl Add<Duration> for Timestamp {
134 type Output = Self;
135
136 fn add(self, rhs: Duration) -> Self {
137 Self(self.0.saturating_add(saturating_nanos(rhs)))
138 }
139}
140
141impl AddAssign<Duration> for Timestamp {
142 fn add_assign(&mut self, rhs: Duration) {
143 *self = *self + rhs;
144 }
145}
146
147impl Sub<Duration> for Timestamp {
148 type Output = Self;
149
150 fn sub(self, rhs: Duration) -> Self {
151 Self(self.0.saturating_sub(saturating_nanos(rhs)))
152 }
153}
154
155impl SubAssign<Duration> for Timestamp {
156 fn sub_assign(&mut self, rhs: Duration) {
157 *self = *self - rhs;
158 }
159}
160
161impl Sub for Timestamp {
162 type Output = Duration;
163
164 /// Equivalent to [`Timestamp::saturating_duration_since`].
165 fn sub(self, rhs: Self) -> Duration {
166 self.saturating_duration_since(rhs)
167 }
168}
169
170impl fmt::Display for Timestamp {
171 /// Renders as seconds with nanosecond precision — readable in a trace
172 /// without implying a wall-clock date.
173 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
174 write!(
175 f,
176 "{}.{:09}s",
177 self.0 / 1_000_000_000,
178 self.0 % 1_000_000_000
179 )
180 }
181}
182
183/// `Duration::as_nanos` is a `u128`; clamp it into the `u64` timeline instead
184/// of truncating. A duration past `u64::MAX` nanoseconds is nonsense in a
185/// display pipeline, and clamping keeps the saturating contract intact.
186fn saturating_nanos(d: Duration) -> u64 {
187 u64::try_from(d.as_nanos()).unwrap_or(u64::MAX)
188}
189
190/// A monotonic clock.
191///
192/// One dynamic call per sample — negligible next to a KMS ioctl. Implementors
193/// must be monotonic: successive [`now`](Clock::now) values must never
194/// decrease, or pacing arithmetic downstream saturates to zero and stalls.
195///
196/// `Send + Sync` because a scene and its sources may sample the clock from
197/// different threads; the C++ `Clock&` carries the same expectation implicitly.
198pub trait Clock: Send + Sync + fmt::Debug {
199 /// Sample the clock.
200 fn now(&self) -> Timestamp;
201}
202
203/// Default implementation backed by `CLOCK_MONOTONIC`.
204///
205/// Holds no state, so it is safe to instantiate per caller or share via
206/// [`default_clock`].
207#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
208#[non_exhaustive]
209pub struct SteadyClock;
210
211impl SteadyClock {
212 /// Construct the clock. Equivalent to [`SteadyClock::default`].
213 #[must_use]
214 pub const fn new() -> Self {
215 Self
216 }
217}
218
219impl Clock for SteadyClock {
220 /// # Panics
221 ///
222 /// Never in practice. `clock_gettime(CLOCK_MONOTONIC)` cannot fail on a
223 /// Linux kernel that supports it, and `rustix` models it as infallible.
224 fn now(&self) -> Timestamp {
225 let ts = rustix::time::clock_gettime(rustix::time::ClockId::Monotonic);
226 Timestamp::from_timespec(ts.tv_sec, ts.tv_nsec as i64)
227 }
228}
229
230/// A process-wide [`SteadyClock`]. The returned reference is valid for the
231/// lifetime of the program.
232///
233/// ```
234/// use drmkit_time::{Clock, default_clock};
235///
236/// let t0 = default_clock().now();
237/// let t1 = default_clock().now();
238/// assert!(t1 >= t0, "CLOCK_MONOTONIC must not run backwards");
239/// ```
240#[must_use]
241pub fn default_clock() -> &'static SteadyClock {
242 static CLOCK: SteadyClock = SteadyClock::new();
243 &CLOCK
244}
245
246/// A clock whose time only moves when the test moves it.
247///
248/// The C++ header names "freeze or step time in tests without sleeping" as a
249/// reason the abstraction exists, but leaves each test to build its own stub.
250/// Providing one here keeps the scene, cursor, and pacing suites from
251/// reinventing it — and keeps them from reinventing it *differently*.
252///
253/// Interior mutability is deliberate: [`Clock::now`] takes `&self`, and a test
254/// holding a `&dyn Clock` still needs to advance it.
255///
256/// ```
257/// use core::time::Duration;
258/// use drmkit_time::{Clock, ManualClock, Timestamp};
259///
260/// let clock = ManualClock::new();
261/// assert_eq!(clock.now(), Timestamp::ZERO);
262///
263/// clock.advance(Duration::from_millis(16));
264/// assert_eq!(clock.now(), Timestamp::from_nanos(16_000_000));
265///
266/// clock.set(Timestamp::from_nanos(1_000));
267/// assert_eq!(clock.now().as_nanos(), 1_000);
268/// ```
269#[derive(Debug, Default)]
270pub struct ManualClock {
271 nanos: core::sync::atomic::AtomicU64,
272}
273
274impl ManualClock {
275 /// A clock frozen at [`Timestamp::ZERO`].
276 #[must_use]
277 pub const fn new() -> Self {
278 Self {
279 nanos: core::sync::atomic::AtomicU64::new(0),
280 }
281 }
282
283 /// A clock frozen at `start`.
284 #[must_use]
285 pub const fn starting_at(start: Timestamp) -> Self {
286 Self {
287 nanos: core::sync::atomic::AtomicU64::new(start.as_nanos()),
288 }
289 }
290
291 /// Move the clock forward by `delta`. Saturates at `u64::MAX` nanoseconds.
292 ///
293 /// Saturating rather than wrapping is the whole point: a wrapped counter
294 /// runs *backwards*, and every interval taken against it collapses to
295 /// [`Duration::ZERO`] (see [`Timestamp::saturating_duration_since`]) —
296 /// a test clock that silently stalls pacing instead of failing loudly.
297 pub fn advance(&self, delta: Duration) {
298 let step = saturating_nanos(delta);
299 // `fetch_add` wraps on overflow, so clamp inside a compare-exchange
300 // loop instead. Hand-rolled rather than `fetch_update`, which nightly
301 // has deprecated in favor of `try_update` -- naming the primitives
302 // directly keeps this compiling on stable and nightly alike, which the
303 // miri lane needs.
304 let mut current = self.nanos.load(core::sync::atomic::Ordering::Relaxed);
305 loop {
306 let clamped = current.saturating_add(step);
307 match self.nanos.compare_exchange_weak(
308 current,
309 clamped,
310 core::sync::atomic::Ordering::Relaxed,
311 core::sync::atomic::Ordering::Relaxed,
312 ) {
313 Ok(_) => break,
314 Err(observed) => current = observed,
315 }
316 }
317 }
318
319 /// Jump the clock to `at`.
320 ///
321 /// Nothing stops a test from moving it backwards — that is the point of a
322 /// manual clock — but production code must never observe a clock that
323 /// does, so keep such tests scoped to the code under test.
324 pub fn set(&self, at: Timestamp) {
325 self.nanos
326 .store(at.as_nanos(), core::sync::atomic::Ordering::Relaxed);
327 }
328}
329
330impl Clock for ManualClock {
331 fn now(&self) -> Timestamp {
332 Timestamp::from_nanos(self.nanos.load(core::sync::atomic::Ordering::Relaxed))
333 }
334}
335
336#[cfg(test)]
337mod tests;