use core::sync::atomic::{AtomicBool, AtomicU64, Ordering};
#[derive(Copy, Clone, PartialEq, Debug)]
pub struct ZeroTimestamp {
pub sample_time: f64,
pub host_time: u64,
pub seed: u64,
}
#[derive(Debug)]
pub struct DeviceClock {
anchor_host_time: AtomicU64,
seed: AtomicU64,
running: AtomicBool,
}
impl DeviceClock {
#[must_use]
pub const fn new() -> Self {
Self {
anchor_host_time: AtomicU64::new(0),
seed: AtomicU64::new(0),
running: AtomicBool::new(false),
}
}
pub fn start(&self, now_host_time: u64) {
self.anchor_host_time
.store(now_host_time, Ordering::Release);
self.seed.fetch_add(1, Ordering::AcqRel);
self.running.store(true, Ordering::Release);
}
pub fn stop(&self) {
self.running.store(false, Ordering::Release);
}
#[must_use]
pub fn is_running(&self) -> bool {
self.running.load(Ordering::Acquire)
}
#[must_use]
pub fn seed(&self) -> u64 {
self.seed.load(Ordering::Acquire)
}
#[must_use]
pub fn zero_timestamp(
&self,
now_host_time: u64,
host_ticks_per_period: f64,
period_frames: f64,
) -> Option<ZeroTimestamp> {
if !self.is_running() {
return None;
}
let anchor = self.anchor_host_time.load(Ordering::Acquire);
let elapsed = now_host_time.saturating_sub(anchor);
let periods = if host_ticks_per_period > 0.0 {
(elapsed as f64 / host_ticks_per_period).floor()
} else {
0.0
};
Some(ZeroTimestamp {
sample_time: periods * period_frames,
host_time: anchor + (periods * host_ticks_per_period) as u64,
seed: self.seed(),
})
}
}
impl Default for DeviceClock {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use static_assertions::assert_impl_all;
assert_impl_all!(DeviceClock: Send, Sync);
#[test]
fn new_clock_is_stopped() {
let clock = DeviceClock::new();
assert!(!clock.is_running());
assert_eq!(clock.seed(), 0);
assert_eq!(clock.zero_timestamp(1000, 100.0, 480.0), None);
}
#[test]
fn default_matches_new() {
assert!(!DeviceClock::default().is_running());
}
#[test]
fn start_anchors_running_and_bumps_the_seed() {
let clock = DeviceClock::new();
clock.start(5_000);
assert!(clock.is_running());
assert_eq!(clock.seed(), 1);
clock.stop();
clock.start(9_000);
assert_eq!(clock.seed(), 2, "each start bumps the seed");
}
#[test]
fn stop_halts_the_clock() {
let clock = DeviceClock::new();
clock.start(0);
clock.stop();
assert!(!clock.is_running());
assert_eq!(clock.zero_timestamp(10_000, 100.0, 480.0), None);
}
#[test]
fn zero_timestamp_reports_period_boundaries() {
let clock = DeviceClock::new();
clock.start(1_000);
assert_eq!(
clock.zero_timestamp(1_000, 100.0, 480.0),
Some(ZeroTimestamp {
sample_time: 0.0,
host_time: 1_000,
seed: 1,
})
);
assert_eq!(
clock.zero_timestamp(1_250, 100.0, 480.0),
Some(ZeroTimestamp {
sample_time: 960.0,
host_time: 1_200,
seed: 1,
})
);
assert_eq!(
clock.zero_timestamp(2_000, 100.0, 480.0),
Some(ZeroTimestamp {
sample_time: 4_800.0,
host_time: 2_000,
seed: 1,
})
);
}
#[test]
fn zero_timestamp_floors_within_a_period() {
let clock = DeviceClock::new();
clock.start(0);
let ts = clock.zero_timestamp(99, 100.0, 480.0).unwrap();
assert_eq!(ts.sample_time, 0.0);
assert_eq!(ts.host_time, 0);
let ts = clock.zero_timestamp(199, 100.0, 480.0).unwrap();
assert_eq!(ts.sample_time, 480.0);
assert_eq!(ts.host_time, 100);
}
#[test]
fn zero_timestamp_clamps_a_backwards_clock_to_the_anchor() {
let clock = DeviceClock::new();
clock.start(10_000);
assert_eq!(
clock.zero_timestamp(9_000, 100.0, 480.0),
Some(ZeroTimestamp {
sample_time: 0.0,
host_time: 10_000,
seed: 1,
})
);
}
#[test]
fn zero_timestamp_tolerates_a_zero_tick_rate() {
let clock = DeviceClock::new();
clock.start(0);
let ts = clock.zero_timestamp(123_456, 0.0, 480.0).unwrap();
assert_eq!(ts.sample_time, 0.0);
assert_eq!(ts.host_time, 0);
}
#[test]
fn seed_is_carried_into_the_timestamp() {
let clock = DeviceClock::new();
clock.start(0);
clock.stop();
clock.start(0);
assert_eq!(clock.zero_timestamp(0, 1.0, 1.0).unwrap().seed, 2);
}
}