Skip to main content

game_toolkit_dosbus/
clock.rs

1use core::ops::{Add, AddAssign, Sub, SubAssign};
2use core::time::Duration;
3
4/// An instant or duration in cycles of the PC's 315/22 MHz crystal.
5///
6/// One cycle is exactly 22/315 microseconds. PIT input cycles span 12 ticks;
7/// an OPL native sample spans 288 ticks. Integer conversions round down to whole ticks.
8/// For repeated charges, use [`TickAccumulator`] to retain the fractional part.
9///
10/// Arithmetic panics on overflow or subtraction below zero in all build profiles.
11#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
12#[repr(transparent)]
13pub struct Tick(pub u64);
14
15impl Tick {
16    /// Power-on time, or a zero duration.
17    pub const ZERO: Self = Self(0);
18    /// Largest representable timestamp or duration.
19    pub const MAX: Self = Self(u64::MAX);
20    /// Numerator of the crystal frequency in hertz.
21    pub const HZ_NUMERATOR: u64 = 315_000_000;
22    /// Denominator of the crystal frequency in hertz.
23    pub const HZ_DENOMINATOR: u64 = 22;
24
25    /// Convert integer microseconds, rounding down.
26    ///
27    /// # Panics
28    /// Panics if the result exceeds [`Tick::MAX`].
29    pub const fn from_micros(micros: u64) -> Self {
30        Self::from_duration(Duration::from_micros(micros))
31    }
32
33    /// Convert integer nanoseconds, rounding down.
34    pub const fn from_nanos(nanos: u64) -> Self {
35        Self::from_duration(Duration::from_nanos(nanos))
36    }
37
38    /// Convert a duration, rounding down.
39    ///
40    /// # Panics
41    /// Panics if the result exceeds [`Tick::MAX`].
42    pub const fn from_duration(duration: Duration) -> Self {
43        match Self::checked_from_duration(duration) {
44            Some(tick) => tick,
45            None => panic!("duration exceeds Tick::MAX"),
46        }
47    }
48
49    /// Convert a duration, returning `None` on overflow.
50    pub const fn checked_from_duration(duration: Duration) -> Option<Self> {
51        // Even Duration::MAX multiplied by the crystal numerator fits u128.
52        let ticks = duration.as_nanos() * Self::HZ_NUMERATOR as u128
53            / (Self::HZ_DENOMINATOR as u128 * 1_000_000_000);
54        if ticks > u64::MAX as u128 {
55            None
56        } else {
57            Some(Self(ticks as u64))
58        }
59    }
60
61    /// Convert approximate floating-point seconds, rounding to the nearest tick.
62    ///
63    /// Rounding makes `Tick::from_secs_f64(t.as_secs_f64()) == t` despite float error.
64    /// Prefer [`Self::from_duration`] when exact integer conversion matters.
65    ///
66    /// # Panics
67    /// Panics for negative, non-finite or out-of-range input.
68    pub fn from_secs_f64(seconds: f64) -> Self {
69        assert!(seconds.is_finite() && seconds >= 0.0, "invalid seconds");
70        // `f64::round` needs std; adding 0.5 before truncation rounds nonnegative values.
71        let ticks = seconds * Self::HZ_NUMERATOR as f64 / Self::HZ_DENOMINATOR as f64 + 0.5;
72        // u64::MAX as f64 rounds up to 2^64, the exclusive upper bound.
73        assert!(ticks < u64::MAX as f64, "seconds exceed Tick::MAX");
74        Self(ticks as u64)
75    }
76
77    /// Convert to a duration, rounding down to whole nanoseconds.
78    pub const fn as_duration(self) -> Duration {
79        let nanos = self.0 as u128 * Self::HZ_DENOMINATOR as u128 * 1_000_000_000
80            / Self::HZ_NUMERATOR as u128;
81        Duration::new(
82            (nanos / 1_000_000_000) as u64,
83            (nanos % 1_000_000_000) as u32,
84        )
85    }
86
87    /// Approximate seconds, for display or compatibility with older clocks.
88    pub fn as_secs_f64(self) -> f64 {
89        self.0 as f64 * Self::HZ_DENOMINATOR as f64 / Self::HZ_NUMERATOR as f64
90    }
91
92    /// Approximate nanoseconds, for compatibility with older device interfaces.
93    pub fn as_nanos_f64(self) -> f64 {
94        self.0 as f64 * (Self::HZ_DENOMINATOR * 1_000_000_000) as f64 / Self::HZ_NUMERATOR as f64
95    }
96
97    /// Add a duration, returning `None` on overflow.
98    pub const fn checked_add(self, rhs: Self) -> Option<Self> {
99        match self.0.checked_add(rhs.0) {
100            Some(value) => Some(Self(value)),
101            None => None,
102        }
103    }
104
105    /// Subtract an instant or duration, returning `None` if `rhs` is greater.
106    pub const fn checked_sub(self, rhs: Self) -> Option<Self> {
107        match self.0.checked_sub(rhs.0) {
108            Some(value) => Some(Self(value)),
109            None => None,
110        }
111    }
112}
113
114impl Add for Tick {
115    type Output = Self;
116
117    fn add(self, rhs: Self) -> Self {
118        self.checked_add(rhs).expect("tick addition overflow")
119    }
120}
121
122impl AddAssign for Tick {
123    fn add_assign(&mut self, rhs: Self) {
124        *self = *self + rhs;
125    }
126}
127
128impl Sub for Tick {
129    type Output = Self;
130
131    fn sub(self, rhs: Self) -> Self {
132        self.checked_sub(rhs).expect("tick subtraction below zero")
133    }
134}
135
136impl SubAssign for Tick {
137    fn sub_assign(&mut self, rhs: Self) {
138        *self = *self - rhs;
139    }
140}
141
142/// Converts cycles at a fixed integer frequency to crystal ticks without drift.
143///
144/// Use one accumulator per source clock: a 33 MHz CPU, a 48 kHz audio stream,
145/// or nanosecond bus charges (1 GHz). Each call returns the whole ticks elapsed
146/// since the previous call and retains the fractional remainder internally.
147/// Splitting a charge across any number of calls produces the same total ticks.
148/// Creating a new accumulator resets the source clock's fractional phase.
149#[derive(Clone, Debug, Eq, PartialEq)]
150pub struct TickAccumulator {
151    source_hz: u64,
152    remainder: u128,
153}
154
155impl TickAccumulator {
156    /// Create a converter for a source clock at `source_hz` hertz.
157    ///
158    /// # Panics
159    /// Panics if the frequency is zero.
160    pub const fn new(source_hz: u64) -> Self {
161        assert!(source_hz > 0, "source frequency must be nonzero");
162        Self {
163            source_hz,
164            remainder: 0,
165        }
166    }
167
168    /// The source clock's frequency in hertz.
169    pub const fn source_hz(&self) -> u64 {
170        self.source_hz
171    }
172
173    /// Charge source cycles and return the elapsed whole crystal ticks.
174    ///
175    /// # Panics
176    /// Panics if this call's result exceeds [`Tick::MAX`]. On overflow the
177    /// fractional phase remains unchanged.
178    pub fn advance(&mut self, cycles: u64) -> Tick {
179        self.checked_advance(cycles)
180            .expect("cycle charge exceeds Tick::MAX")
181    }
182
183    /// Charge source cycles, returning `None` on overflow without changing state.
184    pub fn checked_advance(&mut self, cycles: u64) -> Option<Tick> {
185        let denominator = self.source_hz as u128 * Tick::HZ_DENOMINATOR as u128;
186        let numerator = cycles as u128 * Tick::HZ_NUMERATOR as u128 + self.remainder;
187        let ticks = u64::try_from(numerator / denominator).ok()?;
188        self.remainder = numerator % denominator;
189        Some(Tick(ticks))
190    }
191}