Skip to main content

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;