use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::fmt;
const fn gcd(mut a: u64, mut b: u64) -> u64 {
while b != 0 {
let t = a % b;
a = b;
b = t;
}
a
}
fn checked_lcm(a: u64, b: u64) -> Option<u64> {
if a == 0 || b == 0 {
return Some(0);
}
(a / gcd(a, b)).checked_mul(b)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct U192([u64; 3]);
impl U192 {
fn mul_u128_u64(a: u128, b: u64) -> U192 {
let (ah, al) = ((a >> 64) as u64, a as u64);
let lo = (al as u128) * (b as u128);
let hi = (ah as u128) * (b as u128) + (lo >> 64);
U192([(hi >> 64) as u64, hi as u64, lo as u64])
}
fn div_u64(self, d: u64) -> (U192, u64) {
debug_assert!(d != 0);
let mut rem: u128 = 0;
let mut q = [0u64; 3];
for (out, limb) in q.iter_mut().zip(self.0) {
let cur = (rem << 64) | (limb as u128);
*out = (cur / (d as u128)) as u64;
rem = cur % (d as u128);
}
(U192(q), rem as u64)
}
fn to_u128(self) -> Option<u128> {
if self.0[0] != 0 {
return None;
}
Some(((self.0[1] as u128) << 64) | (self.0[2] as u128))
}
fn shr64(self) -> U192 {
U192([0, self.0[0], self.0[1]])
}
#[cfg(test)]
fn shl64(self) -> Option<U192> {
if self.0[0] != 0 {
return None;
}
Some(U192([self.0[1], self.0[2], 0]))
}
fn dec(self) -> U192 {
let mut l = self.0;
for i in (0..3).rev() {
match l[i].checked_sub(1) {
Some(v) => {
l[i] = v;
return U192(l);
}
None => l[i] = u64::MAX,
}
}
debug_assert!(false, "decrement of zero");
U192(l)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Rational {
num: u64,
den: u64,
}
impl Rational {
pub const ONE: Rational = Rational { num: 1, den: 1 };
pub fn new(num: u64, den: u64) -> ClockResult<Rational> {
if den == 0 {
return Err(ClockError::ZeroDenominator);
}
if num == 0 {
return Ok(Rational { num: 0, den: 1 });
}
let g = gcd(num, den);
Ok(Rational {
num: num / g,
den: den / g,
})
}
pub const fn integer(n: u64) -> Rational {
Rational { num: n, den: 1 }
}
#[inline]
pub const fn num(self) -> u64 {
self.num
}
#[inline]
pub const fn den(self) -> u64 {
self.den
}
#[inline]
pub const fn is_zero(self) -> bool {
self.num == 0
}
pub fn checked_mul(self, other: Rational) -> Option<Rational> {
if self.is_zero() || other.is_zero() {
return Some(Rational { num: 0, den: 1 });
}
let g1 = gcd(self.num, other.den);
let g2 = gcd(other.num, self.den);
let num = (self.num / g1).checked_mul(other.num / g2)?;
let den = (self.den / g2).checked_mul(other.den / g1)?;
Some(Rational { num, den })
}
pub fn checked_scale(self, mul: u64, div: u64) -> Option<Rational> {
self.checked_mul(Rational::new(mul, div).ok()?)
}
}
impl PartialOrd for Rational {
fn partial_cmp(&self, other: &Rational) -> Option<core::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Rational {
fn cmp(&self, other: &Rational) -> core::cmp::Ordering {
let a = (self.num as u128) * (other.den as u128);
let b = (other.num as u128) * (self.den as u128);
a.cmp(&b)
}
}
impl fmt::Display for Rational {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.den == 1 {
write!(f, "{}", self.num)
} else {
write!(f, "{}/{}", self.num, self.den)
}
}
}
pub const GLOBAL_TIME_FRAC_BITS: u32 = 64;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
pub struct GlobalTime(u128);
impl GlobalTime {
pub const ZERO: GlobalTime = GlobalTime(0);
pub const MAX: GlobalTime = GlobalTime(u128::MAX);
#[inline]
pub const fn from_raw(raw: u128) -> GlobalTime {
GlobalTime(raw)
}
#[inline]
pub const fn raw(self) -> u128 {
self.0
}
pub const fn from_nanos(nanos: u64) -> GlobalTime {
GlobalTime(((nanos as u128) << GLOBAL_TIME_FRAC_BITS) / 1_000_000_000)
}
pub const fn as_nanos(self) -> u64 {
let whole = self.0 >> GLOBAL_TIME_FRAC_BITS;
let frac = self.0 & ((1u128 << GLOBAL_TIME_FRAC_BITS) - 1);
let secs_ns = match whole.checked_mul(1_000_000_000) {
Some(v) => v,
None => return u64::MAX,
};
let frac_ns = (frac * 1_000_000_000) >> GLOBAL_TIME_FRAC_BITS;
let total = secs_ns + frac_ns;
if total > u64::MAX as u128 {
u64::MAX
} else {
total as u64
}
}
#[inline]
pub const fn saturating_add(self, other: GlobalTime) -> GlobalTime {
GlobalTime(self.0.saturating_add(other.0))
}
#[inline]
pub const fn saturating_sub(self, other: GlobalTime) -> GlobalTime {
GlobalTime(self.0.saturating_sub(other.0))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct DomainId(u32);
impl DomainId {
#[inline]
pub const fn index(self) -> usize {
self.0 as usize
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct OscillatorId(u32);
impl OscillatorId {
#[inline]
pub const fn index(self) -> usize {
self.0 as usize
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ClockError {
ZeroDenominator,
ZeroRate(String),
UnknownDomain(DomainId),
UnknownOscillator(OscillatorId),
LcmUnavailable {
domains: Vec<String>,
},
Overflow {
what: &'static str,
domains: Vec<String>,
},
Cycle(String),
NotApplicable(String),
CrossTree(DomainId, DomainId),
Gated(String),
}
impl fmt::Display for ClockError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ClockError::ZeroDenominator => f.write_str("zero denominator"),
ClockError::ZeroRate(w) => write!(f, "`{w}`: rate must be greater than zero"),
ClockError::UnknownDomain(id) => write!(f, "no clock domain #{}", id.0),
ClockError::UnknownOscillator(id) => write!(f, "no oscillator #{}", id.0),
ClockError::LcmUnavailable { domains } => write!(
f,
"no exact common clock unit for domains [{}]: the tree's internal lcm does \
not fit in 64 bits",
domains.join(", ")
),
ClockError::Overflow { what, domains } => {
write!(f, "{what} overflowed for domains [{}]", domains.join(", "))
}
ClockError::Cycle(name) => write!(f, "reparenting `{name}` would create a cycle"),
ClockError::NotApplicable(msg) => f.write_str(msg),
ClockError::CrossTree(a, b) => write!(
f,
"domains #{} and #{} are driven by different oscillators; no exact ratio exists",
a.0, b.0
),
ClockError::Gated(name) => write!(f, "clock domain `{name}` is gated"),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for ClockError {}
impl From<ClockError> for crate::core::Error {
fn from(e: ClockError) -> Self {
crate::core::Error::Config {
at: String::from("clock"),
message: e.to_string(),
}
}
}
pub type ClockResult<T> = core::result::Result<T, ClockError>;
#[derive(Debug, Clone)]
pub struct ClockDomain {
name: String,
parent: Option<DomainId>,
mul: u64,
div: u64,
children: Vec<DomainId>,
root: OscillatorId,
ratio: Rational,
units_per_tick: u64,
base_unit: u64,
base_ticks: u64,
gated: bool,
}
impl ClockDomain {
#[inline]
pub fn name(&self) -> &str {
&self.name
}
#[inline]
pub const fn parent(&self) -> Option<DomainId> {
self.parent
}
#[inline]
pub const fn mul(&self) -> u64 {
self.mul
}
#[inline]
pub const fn div(&self) -> u64 {
self.div
}
#[inline]
pub const fn root(&self) -> OscillatorId {
self.root
}
#[inline]
pub const fn ratio_to_root(&self) -> Rational {
self.ratio
}
#[inline]
pub const fn units_per_tick(&self) -> u64 {
self.units_per_tick
}
#[inline]
pub const fn is_gated(&self) -> bool {
self.gated
}
#[inline]
fn ticks_at(&self, units: u64) -> u64 {
if self.gated {
self.base_ticks
} else {
self.base_ticks + (units - self.base_unit) / self.units_per_tick
}
}
}
#[derive(Debug, Clone)]
struct Oscillator {
name: String,
freq: Rational,
root: DomainId,
unit_mul: u64,
unit_rate: Rational,
units: u64,
base_units: u64,
base_time: u128,
step: u128,
rem: u128,
time: u128,
residual: u128,
active: bool,
}
impl Oscillator {
fn recompute_conversion(&mut self) {
let num = self.unit_rate.num() as u128;
let den = self.unit_rate.den() as u128;
debug_assert!(num != 0);
let scaled = den << GLOBAL_TIME_FRAC_BITS;
self.step = scaled / num;
self.rem = scaled % num;
}
fn overflow(&self, what: &'static str) -> ClockError {
ClockError::Overflow {
what,
domains: alloc::vec![self.name.clone()],
}
}
fn accumulate(&mut self, units: u64) -> ClockResult<()> {
let n = units as u128;
let add = n
.checked_mul(self.step)
.ok_or_else(|| self.overflow("global timeline"))?;
let carry_in = n
.checked_mul(self.rem)
.ok_or_else(|| self.overflow("global timeline"))?;
let mut time = self
.time
.checked_add(add)
.ok_or_else(|| self.overflow("global timeline"))?;
let mut residual = self
.residual
.checked_add(carry_in)
.ok_or_else(|| self.overflow("global timeline"))?;
let num = self.unit_rate.num() as u128;
if residual >= num {
time = time
.checked_add(residual / num)
.ok_or_else(|| self.overflow("global timeline"))?;
residual %= num;
}
self.time = time;
self.residual = residual;
Ok(())
}
fn time_for_units(&self, units: u64) -> ClockResult<u128> {
if units <= self.base_units {
return Ok(self.base_time);
}
let n = (units - self.base_units) as u128;
let num = self.unit_rate.num() as u128;
let whole = n
.checked_mul(self.step)
.ok_or_else(|| self.overflow("global timeline"))?;
let frac = n
.checked_mul(self.rem)
.ok_or_else(|| self.overflow("global timeline"))?
/ num;
self.base_time
.checked_add(whole)
.and_then(|t| t.checked_add(frac))
.ok_or_else(|| self.overflow("global timeline"))
}
fn units_at(&self, t: u128) -> ClockResult<u64> {
if t <= self.base_time {
return Ok(self.base_units);
}
let dt = t - self.base_time;
let numerator = U192::mul_u128_u64(dt.saturating_add(1), self.unit_rate.num()).dec();
let (q, _) = numerator.div_u64(self.unit_rate.den());
let units = q
.shr64()
.to_u128()
.and_then(|v| u64::try_from(v).ok())
.ok_or_else(|| self.overflow("unit position"))?;
self.base_units
.checked_add(units)
.ok_or_else(|| self.overflow("unit position"))
}
}
#[derive(Debug, Clone, Default)]
pub struct ClockForest {
domains: Vec<ClockDomain>,
oscillators: Vec<Oscillator>,
}
impl ClockForest {
pub fn new() -> ClockForest {
ClockForest::default()
}
pub fn add_oscillator(&mut self, name: &str, freq: Rational) -> ClockResult<DomainId> {
if freq.is_zero() {
return Err(ClockError::ZeroRate(String::from(name)));
}
let osc = OscillatorId(self.oscillators.len() as u32);
let root = DomainId(self.domains.len() as u32);
self.domains.push(ClockDomain {
name: String::from(name),
parent: None,
mul: 1,
div: 1,
children: Vec::new(),
root: osc,
ratio: Rational::ONE,
units_per_tick: 1,
base_unit: 0,
base_ticks: 0,
gated: false,
});
let mut o = Oscillator {
name: String::from(name),
freq,
root,
unit_mul: 1,
unit_rate: freq,
units: 0,
base_units: 0,
base_time: 0,
step: 0,
rem: 0,
time: 0,
residual: 0,
active: true,
};
o.recompute_conversion();
self.oscillators.push(o);
Ok(root)
}
pub fn add_domain(
&mut self,
name: &str,
parent: DomainId,
mul: u64,
div: u64,
) -> ClockResult<DomainId> {
self.check_domain(parent)?;
if mul == 0 || div == 0 {
return Err(ClockError::ZeroRate(String::from(name)));
}
let backup = self.clone();
let osc = self.domains[parent.index()].root;
let id = DomainId(self.domains.len() as u32);
let units_now = self.oscillators[osc.index()].units;
self.domains.push(ClockDomain {
name: String::from(name),
parent: Some(parent),
mul,
div,
children: Vec::new(),
root: osc,
ratio: Rational::ONE,
units_per_tick: 1,
base_unit: units_now,
base_ticks: 0,
gated: false,
});
self.domains[parent.index()].children.push(id);
if let Err(e) = self.recompute_tree(osc) {
*self = backup;
return Err(e);
}
Ok(id)
}
pub fn domain(&self, id: DomainId) -> ClockResult<&ClockDomain> {
self.domains
.get(id.index())
.ok_or(ClockError::UnknownDomain(id))
}
#[inline]
pub fn domain_count(&self) -> usize {
self.domains.len()
}
pub fn oscillators(&self) -> impl Iterator<Item = OscillatorId> + '_ {
(0..self.oscillators.len() as u32).map(OscillatorId)
}
pub fn domains(&self) -> impl Iterator<Item = DomainId> + '_ {
(0..self.domains.len() as u32).map(DomainId)
}
pub fn is_active(&self, osc: OscillatorId) -> ClockResult<bool> {
Ok(self.osc(osc)?.active)
}
pub fn frequency(&self, osc: OscillatorId) -> ClockResult<Rational> {
Ok(self.osc(osc)?.freq)
}
pub fn domain_frequency(&self, id: DomainId) -> ClockResult<Rational> {
let d = self.domain(id)?;
let f = self.oscillators[d.root.index()].freq;
f.checked_mul(d.ratio).ok_or_else(|| ClockError::Overflow {
what: "domain frequency",
domains: alloc::vec![d.name.clone()],
})
}
pub fn root_of(&self, id: DomainId) -> ClockResult<OscillatorId> {
Ok(self.domain(id)?.root)
}
pub fn is_gated(&self, id: DomainId) -> ClockResult<bool> {
Ok(self.domain(id)?.gated)
}
pub fn ticks(&self, id: DomainId) -> ClockResult<u64> {
let d = self.domain(id)?;
Ok(d.ticks_at(self.oscillators[d.root.index()].units))
}
pub fn unit_position(&self, osc: OscillatorId) -> ClockResult<u64> {
Ok(self.osc(osc)?.units)
}
pub fn unit_rate(&self, osc: OscillatorId) -> ClockResult<Rational> {
Ok(self.osc(osc)?.unit_rate)
}
pub fn convert_ticks(&self, from: DomainId, to: DomainId, ticks: u64) -> ClockResult<u64> {
let a = self.domain(from)?;
let b = self.domain(to)?;
if a.root != b.root {
return Err(ClockError::CrossTree(from, to));
}
let units = ticks
.checked_mul(a.units_per_tick)
.ok_or_else(|| ClockError::Overflow {
what: "tick conversion",
domains: alloc::vec![a.name.clone(), b.name.clone()],
})?;
Ok(units / b.units_per_tick)
}
pub fn advance_domain(&mut self, id: DomainId, ticks: u64) -> ClockResult<()> {
let d = self.domain(id)?;
if d.gated {
return Err(ClockError::Gated(d.name.clone()));
}
let delta = ticks
.checked_mul(d.units_per_tick)
.ok_or_else(|| ClockError::Overflow {
what: "unit position",
domains: alloc::vec![d.name.clone()],
})?;
let osc = d.root;
self.advance_units(osc, delta)
}
pub fn advance_units(&mut self, osc: OscillatorId, units: u64) -> ClockResult<()> {
let idx = osc.index();
if idx >= self.oscillators.len() {
return Err(ClockError::UnknownOscillator(osc));
}
let o = &mut self.oscillators[idx];
let new_units = o
.units
.checked_add(units)
.ok_or_else(|| o.overflow("unit position"))?;
o.accumulate(units)?;
o.units = new_units;
Ok(())
}
pub fn global_time(&self, osc: OscillatorId) -> ClockResult<GlobalTime> {
Ok(GlobalTime(self.osc(osc)?.time))
}
pub fn global_time_of_units(&self, osc: OscillatorId, units: u64) -> ClockResult<GlobalTime> {
Ok(GlobalTime(self.osc(osc)?.time_for_units(units)?))
}
pub fn global_time_of_tick(&self, id: DomainId, tick: u64) -> ClockResult<GlobalTime> {
let d = self.domain(id)?;
let o = &self.oscillators[d.root.index()];
if tick <= d.base_ticks {
return Ok(GlobalTime(o.time_for_units(d.base_unit)?));
}
let units = (tick - d.base_ticks)
.checked_mul(d.units_per_tick)
.and_then(|u| u.checked_add(d.base_unit))
.ok_or_else(|| ClockError::Overflow {
what: "unit position",
domains: alloc::vec![d.name.clone()],
})?;
Ok(GlobalTime(o.time_for_units(units)?))
}
pub fn units_at_global(&self, osc: OscillatorId, at: GlobalTime) -> ClockResult<u64> {
self.osc(osc)?.units_at(at.0)
}
pub fn advance_to_global(&mut self, osc: OscillatorId, at: GlobalTime) -> ClockResult<u64> {
let target = self.units_at_global(osc, at)?;
let cur = self.osc(osc)?.units;
if target <= cur {
return Ok(0);
}
let delta = target - cur;
self.advance_units(osc, delta)?;
Ok(delta)
}
pub fn set_rating(&mut self, id: DomainId, mul: u64, div: u64) -> ClockResult<()> {
self.check_domain(id)?;
if self.domains[id.index()].parent.is_none() {
return Err(ClockError::NotApplicable(alloc::format!(
"`{}` is an oscillator root; set its frequency instead",
self.domains[id.index()].name
)));
}
if mul == 0 || div == 0 {
return Err(ClockError::ZeroRate(self.domains[id.index()].name.clone()));
}
let backup = self.clone();
let osc = self.domains[id.index()].root;
self.rebase_subtree(id);
self.domains[id.index()].mul = mul;
self.domains[id.index()].div = div;
if let Err(e) = self.recompute_tree(osc) {
*self = backup;
return Err(e);
}
Ok(())
}
pub fn set_frequency(&mut self, osc: OscillatorId, freq: Rational) -> ClockResult<()> {
let idx = osc.index();
if idx >= self.oscillators.len() {
return Err(ClockError::UnknownOscillator(osc));
}
if freq.is_zero() {
return Err(ClockError::ZeroRate(self.oscillators[idx].name.clone()));
}
let backup = self.clone();
self.oscillators[idx].freq = freq;
if let Err(e) = self.recompute_tree(osc) {
*self = backup;
return Err(e);
}
Ok(())
}
pub fn reparent(
&mut self,
id: DomainId,
new_parent: DomainId,
mul: u64,
div: u64,
) -> ClockResult<()> {
self.check_domain(id)?;
self.check_domain(new_parent)?;
if mul == 0 || div == 0 {
return Err(ClockError::ZeroRate(self.domains[id.index()].name.clone()));
}
if id == new_parent || self.is_ancestor(id, new_parent) {
return Err(ClockError::Cycle(self.domains[id.index()].name.clone()));
}
let backup = self.clone();
let old_osc = self.domains[id.index()].root;
let new_osc = self.domains[new_parent.index()].root;
self.rebase_subtree(id);
match self.domains[id.index()].parent {
Some(p) => self.domains[p.index()].children.retain(|c| *c != id),
None => self.oscillators[old_osc.index()].active = false,
}
self.domains[id.index()].parent = Some(new_parent);
self.domains[id.index()].mul = mul;
self.domains[id.index()].div = div;
self.domains[new_parent.index()].children.push(id);
if old_osc != new_osc {
let dest_units = self.oscillators[new_osc.index()].units;
for m in self.subtree(id) {
self.domains[m.index()].root = new_osc;
self.domains[m.index()].base_unit = dest_units;
}
}
let old_root = self.oscillators[old_osc.index()].root;
let old_tree_survives =
old_osc != new_osc && self.domains[old_root.index()].root == old_osc;
let outcome = self.recompute_tree(new_osc).and_then(|()| {
if old_tree_survives {
self.recompute_tree(old_osc)
} else {
Ok(())
}
});
if let Err(e) = outcome {
*self = backup;
return Err(e);
}
Ok(())
}
pub fn lock_oscillator(
&mut self,
osc: OscillatorId,
parent: DomainId,
mul: u64,
div: u64,
) -> ClockResult<()> {
let root = self.osc(osc)?.root;
self.reparent(root, parent, mul, div)
}
pub fn set_gated(&mut self, id: DomainId, gated: bool) -> ClockResult<()> {
self.check_domain(id)?;
let units = self.oscillators[self.domains[id.index()].root.index()].units;
let d = &mut self.domains[id.index()];
if d.gated == gated {
return Ok(());
}
d.base_ticks = d.ticks_at(units);
d.base_unit = units;
d.gated = gated;
Ok(())
}
pub fn restore_unit_position(&mut self, osc: OscillatorId, units: u64) -> ClockResult<()> {
let idx = osc.index();
if idx >= self.oscillators.len() {
return Err(ClockError::UnknownOscillator(osc));
}
let o = &mut self.oscillators[idx];
o.base_units = 0;
o.base_time = 0;
o.units = units;
o.time = o.time_for_units(units)?;
let num = o.unit_rate.num() as u128;
o.residual = (units as u128)
.checked_mul(o.rem)
.ok_or_else(|| o.overflow("global timeline"))?
% num;
Ok(())
}
pub fn restore_ticks(&mut self, id: DomainId, ticks: u64) -> ClockResult<()> {
self.check_domain(id)?;
let units = self.oscillators[self.domains[id.index()].root.index()].units;
let d = &mut self.domains[id.index()];
d.base_ticks = ticks;
d.base_unit = units;
Ok(())
}
fn osc(&self, osc: OscillatorId) -> ClockResult<&Oscillator> {
self.oscillators
.get(osc.index())
.ok_or(ClockError::UnknownOscillator(osc))
}
fn check_domain(&self, id: DomainId) -> ClockResult<()> {
if id.index() >= self.domains.len() {
return Err(ClockError::UnknownDomain(id));
}
Ok(())
}
fn subtree(&self, id: DomainId) -> Vec<DomainId> {
let mut out = alloc::vec![id];
let mut i = 0;
while i < out.len() {
let cur = out[i];
out.extend_from_slice(&self.domains[cur.index()].children);
i += 1;
}
out
}
fn is_ancestor(&self, maybe_ancestor: DomainId, of: DomainId) -> bool {
let mut cur = Some(of);
while let Some(c) = cur {
if c == maybe_ancestor {
return true;
}
cur = self.domains[c.index()].parent;
}
false
}
fn rebase_subtree(&mut self, id: DomainId) {
let units = self.oscillators[self.domains[id.index()].root.index()].units;
for m in self.subtree(id) {
let d = &mut self.domains[m.index()];
d.base_ticks = d.ticks_at(units);
d.base_unit = units;
}
}
fn recompute_tree(&mut self, osc: OscillatorId) -> ClockResult<()> {
let root_domain = self.oscillators[osc.index()].root;
let order = self.subtree(root_domain);
let names = |forest: &ClockForest| -> Vec<String> {
order
.iter()
.map(|d| forest.domains[d.index()].name.clone())
.collect()
};
let mut ratios: Vec<Rational> = Vec::with_capacity(order.len());
for id in &order {
let d = &self.domains[id.index()];
let r = match d.parent {
None => Rational::ONE,
Some(p) => {
let pi = order
.iter()
.position(|x| *x == p)
.expect("BFS order lists a parent before its children");
ratios[pi].checked_scale(d.mul, d.div).ok_or_else(|| {
ClockError::LcmUnavailable {
domains: names(self),
}
})?
}
};
ratios.push(r);
}
let old_a = self.oscillators[osc.index()].unit_mul;
let mut a = old_a;
for r in &ratios {
a = checked_lcm(a, r.num()).ok_or_else(|| ClockError::LcmUnavailable {
domains: names(self),
})?;
}
let mut ks: Vec<u64> = Vec::with_capacity(order.len());
for r in &ratios {
ks.push((a / r.num()).checked_mul(r.den()).ok_or_else(|| {
ClockError::LcmUnavailable {
domains: names(self),
}
})?);
}
let f = a / old_a;
let new_units = self.oscillators[osc.index()]
.units
.checked_mul(f)
.ok_or_else(|| ClockError::Overflow {
what: "unit position rescale",
domains: names(self),
})?;
let mut new_bases: Vec<u64> = Vec::with_capacity(order.len());
for id in &order {
new_bases.push(
self.domains[id.index()]
.base_unit
.checked_mul(f)
.ok_or_else(|| ClockError::Overflow {
what: "unit position rescale",
domains: names(self),
})?,
);
}
let unit_rate = self.oscillators[osc.index()]
.freq
.checked_mul(Rational::integer(a))
.ok_or_else(|| ClockError::LcmUnavailable {
domains: names(self),
})?;
for (i, id) in order.iter().enumerate() {
let d = &mut self.domains[id.index()];
d.ratio = ratios[i];
d.units_per_tick = ks[i];
d.base_unit = new_bases[i];
}
let o = &mut self.oscillators[osc.index()];
o.units = new_units;
o.unit_mul = a;
o.unit_rate = unit_rate;
o.base_units = new_units;
o.base_time = o.time;
o.residual = 0;
o.recompute_conversion();
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::string::ToString;
fn nes() -> (ClockForest, DomainId, DomainId, DomainId) {
let mut f = ClockForest::new();
let master = f
.add_oscillator("master", Rational::new(236_250_000, 11).unwrap())
.unwrap();
let cpu = f.add_domain("cpu", master, 1, 12).unwrap();
let ppu = f.add_domain("ppu", master, 1, 4).unwrap();
(f, master, cpu, ppu)
}
struct Lcg(u64);
impl Lcg {
fn next(&mut self) -> u64 {
self.0 = self
.0
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
self.0
}
}
fn exact_time(units: u64, num: u64, den: u64) -> u128 {
let shifted = U192::mul_u128_u64(units as u128, den)
.shl64()
.expect("test inputs must fit");
let (q, _) = shifted.div_u64(num);
q.to_u128().expect("test inputs must fit")
}
#[test]
fn rationals_reduce_and_compare_exactly() {
let r = Rational::new(236_250_000, 11).unwrap();
assert_eq!(r.num(), 236_250_000);
assert_eq!(r.den(), 11);
assert_eq!(Rational::new(4, 8).unwrap(), Rational::new(1, 2).unwrap());
assert!(Rational::new(1, 3).unwrap() < Rational::new(1, 2).unwrap());
assert_eq!(
Rational::new(1, 0).unwrap_err(),
ClockError::ZeroDenominator
);
assert_eq!(Rational::new(3, 6).unwrap().to_string(), "1/2");
}
#[test]
fn u192_arithmetic_is_exact() {
let p = U192::mul_u128_u64(u128::MAX, u64::MAX);
let (q, r) = p.div_u64(u64::MAX);
assert_eq!(r, 0);
assert_eq!(q, U192([0, (u128::MAX >> 64) as u64, u128::MAX as u64]));
assert_eq!(U192([0, 0, 1]).dec(), U192([0, 0, 0]));
assert_eq!(U192([1, 0, 0]).dec(), U192([0, u64::MAX, u64::MAX]));
assert_eq!(U192([0, 1, 0]).shr64(), U192([0, 0, 1]));
assert_eq!(U192([0, 0, 1]).shl64(), Some(U192([0, 1, 0])));
assert_eq!(U192([1, 0, 0]).shl64(), None);
}
#[test]
fn nes_domains_derive_the_master_tick() {
let (f, master, cpu, ppu) = nes();
let osc = f.root_of(cpu).unwrap();
assert_eq!(f.unit_rate(osc).unwrap(), f.frequency(osc).unwrap());
assert_eq!(f.domain(master).unwrap().units_per_tick(), 1);
assert_eq!(f.domain(cpu).unwrap().units_per_tick(), 12);
assert_eq!(f.domain(ppu).unwrap().units_per_tick(), 4);
assert_eq!(
f.domain_frequency(cpu).unwrap(),
Rational::new(236_250_000, 132).unwrap()
);
}
#[test]
fn nes_cpu_ppu_ratio_is_exactly_three_to_one_forever() {
let (mut f, _master, cpu, ppu) = nes();
for i in 1..=2_000_000u64 {
f.advance_domain(cpu, 1).unwrap();
assert_eq!(f.ticks(cpu).unwrap(), i);
assert_eq!(f.ticks(ppu).unwrap(), 3 * i);
}
let mut rng = Lcg(0x5eed_1234_dead_beef);
let mut cpu_ticks = 2_000_000u64;
while cpu_ticks < 2_000_000_000 {
let n = (rng.next() % 5_000) + 1;
f.advance_domain(cpu, n).unwrap();
cpu_ticks += n;
assert_eq!(f.ticks(cpu).unwrap(), cpu_ticks);
assert_eq!(f.ticks(ppu).unwrap(), 3 * cpu_ticks);
assert_eq!(f.convert_ticks(cpu, ppu, cpu_ticks).unwrap(), 3 * cpu_ticks);
assert_eq!(f.convert_ticks(ppu, cpu, 3 * cpu_ticks).unwrap(), cpu_ticks);
}
assert!(cpu_ticks > 1_000_000_000);
}
#[test]
fn intra_tree_conversion_never_touches_absolute_time() {
let mut f = ClockForest::new();
let root = f
.add_oscillator("odd", Rational::new(3, 7_919).unwrap())
.unwrap();
let a = f.add_domain("a", root, 1, 12).unwrap();
let b = f.add_domain("b", root, 1, 4).unwrap();
assert_eq!(f.convert_ticks(a, b, 1_000_003).unwrap(), 3_000_009);
}
#[test]
fn cross_tree_conversion_is_refused_not_approximated() {
let (mut f, _m, cpu, _ppu) = nes();
let spc = f
.add_oscillator("spc700", Rational::integer(24_576_000))
.unwrap();
let dsp = f.add_domain("dsp", spc, 1, 768).unwrap();
assert_eq!(
f.convert_ticks(cpu, dsp, 1).unwrap_err(),
ClockError::CrossTree(cpu, dsp)
);
}
#[test]
fn cross_tree_drift_is_bounded_and_non_accumulating() {
let mut f = ClockForest::new();
let master = f
.add_oscillator("master", Rational::new(236_250_000, 11).unwrap())
.unwrap();
let rtc = f.add_oscillator("rtc", Rational::integer(32_768)).unwrap();
let cpu = f.add_domain("cpu", master, 1, 12).unwrap();
let _ = f.add_domain("sec", rtc, 1, 32_768).unwrap();
let m_osc = f.root_of(master).unwrap();
let r_osc = f.root_of(rtc).unwrap();
let (m_num, m_den) = {
let r = f.unit_rate(m_osc).unwrap();
(r.num(), r.den())
};
let mut rng = Lcg(0x1234_5678_9abc_def0);
let mut cpu_ticks = 0u64;
while cpu_ticks < 100_000_000_000 {
let n = (rng.next() % 1_000_000) + 1;
f.advance_domain(cpu, n).unwrap();
cpu_ticks += n;
let units = f.unit_position(m_osc).unwrap();
assert_eq!(units, cpu_ticks * 12);
let acc = f.global_time(m_osc).unwrap().raw();
let closed = f.global_time_of_units(m_osc, units).unwrap().raw();
assert_eq!(acc, closed);
assert_eq!(acc, exact_time(units, m_num, m_den));
}
assert!(f.unit_position(m_osc).unwrap() > 1_000_000_000_000);
let now = f.global_time(m_osc).unwrap();
f.advance_to_global(r_osc, now).unwrap();
let back = f
.global_time_of_units(r_osc, f.unit_position(r_osc).unwrap())
.unwrap();
assert!(back <= now);
let one_tick = GlobalTime::from_raw((1u128 << 64) / 32_768);
assert!(now.saturating_sub(back) < one_tick);
}
#[test]
fn timeline_error_stays_below_one_unit_per_step() {
let mut f = ClockForest::new();
let osc_root = f.add_oscillator("awkward", Rational::integer(3)).unwrap();
let osc = f.root_of(osc_root).unwrap();
for n in 1..=3_000u64 {
f.advance_units(osc, 1).unwrap();
let t = f.global_time(osc).unwrap().raw();
let exact = ((n as u128) << 64) / 3;
assert_eq!(t, exact, "at tick {n}");
}
}
#[test]
fn lcm_failure_is_reported_and_names_the_domains() {
const P: u64 = 18_446_744_073_709_551_557; let mut f = ClockForest::new();
let root = f.add_oscillator("xtal", Rational::integer(1_000)).unwrap();
let a = f.add_domain("pll_a", root, 3, 1).unwrap();
let err = f.add_domain("pll_b", root, P, 1).unwrap_err();
match &err {
ClockError::LcmUnavailable { domains } => {
assert!(domains.iter().any(|d| d == "xtal"));
assert!(domains.iter().any(|d| d == "pll_a"));
assert!(domains.iter().any(|d| d == "pll_b"));
}
other => panic!("expected LcmUnavailable, got {other:?}"),
}
let text = err.to_string();
assert!(text.contains("pll_a") && text.contains("pll_b"));
assert_eq!(f.domain_count(), 2);
assert_eq!(f.domain(a).unwrap().units_per_tick(), 1);
let b = f.add_domain("pll_b", root, 5, 1).unwrap();
let before = f.domain(b).unwrap().units_per_tick();
assert!(matches!(
f.set_rating(b, P, 1),
Err(ClockError::LcmUnavailable { .. })
));
assert_eq!(f.domain(b).unwrap().units_per_tick(), before);
assert_eq!(f.domain(b).unwrap().mul(), 5);
}
#[test]
fn re_rating_preserves_history_and_keeps_the_tree_exact() {
let (mut f, master, cpu, ppu) = nes();
f.advance_domain(cpu, 1_000).unwrap();
assert_eq!(f.ticks(cpu).unwrap(), 1_000);
assert_eq!(f.ticks(ppu).unwrap(), 3_000);
f.set_rating(cpu, 1, 6).unwrap();
assert_eq!(f.ticks(cpu).unwrap(), 1_000);
f.advance_domain(master, 60).unwrap();
assert_eq!(f.ticks(cpu).unwrap(), 1_010);
assert_eq!(f.ticks(ppu).unwrap(), 3_015);
}
#[test]
fn a_finer_domain_refines_the_unit_without_disturbing_counters() {
let (mut f, master, cpu, ppu) = nes();
f.advance_domain(cpu, 500).unwrap();
let osc = f.root_of(cpu).unwrap();
let unit_rate_before = f.unit_rate(osc).unwrap();
let pll = f.add_domain("pll", master, 5, 2).unwrap();
assert_eq!(
f.unit_rate(osc).unwrap(),
unit_rate_before.checked_mul(Rational::integer(5)).unwrap()
);
assert_eq!(f.ticks(cpu).unwrap(), 500);
assert_eq!(f.ticks(ppu).unwrap(), 1_500);
assert_eq!(f.domain(cpu).unwrap().units_per_tick(), 60);
assert_eq!(f.domain(ppu).unwrap().units_per_tick(), 20);
assert_eq!(f.domain(pll).unwrap().units_per_tick(), 2);
f.advance_domain(cpu, 10).unwrap();
assert_eq!(f.ticks(ppu).unwrap(), 1_530);
assert_eq!(f.ticks(pll).unwrap(), 300);
}
#[test]
fn gating_stops_a_domain_without_stopping_its_tree() {
let (mut f, _m, cpu, ppu) = nes();
f.advance_domain(cpu, 100).unwrap();
f.set_gated(cpu, true).unwrap();
assert!(f.is_gated(cpu).unwrap());
assert!(matches!(
f.advance_domain(cpu, 1),
Err(ClockError::Gated(_))
));
f.advance_domain(ppu, 300).unwrap();
assert_eq!(f.ticks(cpu).unwrap(), 100);
assert_eq!(f.ticks(ppu).unwrap(), 600);
f.set_gated(cpu, false).unwrap();
f.advance_domain(cpu, 10).unwrap();
assert_eq!(f.ticks(cpu).unwrap(), 110);
}
#[test]
fn locking_an_oscillator_makes_the_relationship_exact() {
let mut f = ClockForest::new();
let master = f
.add_oscillator("master", Rational::new(236_250_000, 11).unwrap())
.unwrap();
let cpu = f.add_domain("cpu", master, 1, 12).unwrap();
let spc_root = f
.add_oscillator("spc700", Rational::integer(24_576_000))
.unwrap();
let spc = f.root_of(spc_root).unwrap();
let dsp = f.add_domain("dsp", spc_root, 1, 2).unwrap();
assert!(f.convert_ticks(cpu, dsp, 1).is_err());
f.lock_oscillator(spc, master, 32, 21).unwrap();
assert!(!f.is_active(spc).unwrap());
assert_eq!(f.root_of(dsp).unwrap(), f.root_of(cpu).unwrap());
let n = f.convert_ticks(cpu, dsp, 21).unwrap();
assert_eq!(n, 21 * 12 * 16 / 21);
}
#[test]
fn reparenting_rejects_cycles() {
let (mut f, master, cpu, _ppu) = nes();
assert!(matches!(
f.reparent(master, cpu, 1, 1),
Err(ClockError::Cycle(_))
));
assert!(matches!(
f.reparent(cpu, cpu, 1, 1),
Err(ClockError::Cycle(_))
));
}
#[test]
fn unknown_handles_are_errors_not_panics() {
let (f, _m, _c, _p) = nes();
let bogus = DomainId(99);
assert_eq!(
f.ticks(bogus).unwrap_err(),
ClockError::UnknownDomain(bogus)
);
let bogus_osc = OscillatorId(99);
assert_eq!(
f.frequency(bogus_osc).unwrap_err(),
ClockError::UnknownOscillator(bogus_osc)
);
}
#[test]
fn clock_errors_convert_into_the_crate_error() {
let e: crate::core::Error = ClockError::LcmUnavailable {
domains: alloc::vec!["cpu".to_string(), "ppu".to_string()],
}
.into();
let text = alloc::format!("{e}");
assert!(text.contains("cpu") && text.contains("ppu"));
}
#[test]
fn a_restored_forest_is_identical_not_merely_close() {
let (mut f, _m, cpu, ppu) = nes();
f.advance_domain(cpu, 1_234_567).unwrap();
let osc = f.root_of(cpu).unwrap();
let saved_units = f.unit_position(osc).unwrap();
let saved_cpu = f.ticks(cpu).unwrap();
let saved_ppu = f.ticks(ppu).unwrap();
let saved_time = f.global_time(osc).unwrap();
let (mut g, _m2, cpu2, ppu2) = nes();
let osc2 = g.root_of(cpu2).unwrap();
g.restore_unit_position(osc2, saved_units).unwrap();
g.restore_ticks(cpu2, saved_cpu).unwrap();
g.restore_ticks(ppu2, saved_ppu).unwrap();
assert_eq!(g.ticks(cpu2).unwrap(), saved_cpu);
assert_eq!(g.ticks(ppu2).unwrap(), saved_ppu);
assert_eq!(g.global_time(osc2).unwrap(), saved_time);
f.advance_domain(cpu, 99_991).unwrap();
g.advance_domain(cpu2, 99_991).unwrap();
assert_eq!(g.global_time(osc2).unwrap(), f.global_time(osc).unwrap());
assert_eq!(g.ticks(ppu2).unwrap(), f.ticks(ppu).unwrap());
}
#[test]
fn global_time_round_trips_through_ticks() {
let (mut f, _m, cpu, _p) = nes();
f.advance_domain(cpu, 1_789_773).unwrap();
let osc = f.root_of(cpu).unwrap();
let t = f.global_time(osc).unwrap();
let ns = t.as_nanos();
assert!((999_000_000..=1_001_000_000).contains(&ns), "{ns}");
assert_eq!(
f.units_at_global(osc, t).unwrap(),
f.unit_position(osc).unwrap()
);
assert_eq!(f.global_time_of_tick(cpu, 1_789_773).unwrap(), t);
}
#[test]
fn nanosecond_conversions_are_integer_and_never_overshoot() {
assert_eq!(GlobalTime::from_nanos(0), GlobalTime::ZERO);
assert_eq!(
GlobalTime::from_nanos(1_000_000_000),
GlobalTime::from_raw(1u128 << 64)
);
for ns in [1u64, 999, 123_456_789, 4_000_000_000] {
let back = GlobalTime::from_nanos(ns).as_nanos();
assert!(back == ns || back == ns - 1, "{ns} -> {back}");
}
assert_eq!(GlobalTime::MAX.as_nanos(), u64::MAX);
}
}