From d6c0fac7a02f436135a77793032d3a2f8a677016 Mon Sep 17 00:00:00 2001 From: FILIPE RIBEIRO DOS SANTOS <45768814+frds3445@users.noreply.github.com> Date: Sat, 7 Nov 2026 12:50:49 -0300 Subject: [PATCH 1/3] fix(timestamp): eliminate float precision drift in long-running recordings Replace f64 timebase division and u64-to-f64 epoch casts in MachAbsoluteTimestamp and PerformanceCounterTimestamp with 128-bit integer rational arithmetic so cursor and video timelines remain synchronized across multi-hour recordings. Fixes #2144 Signed-off-by: Lucas Oliveira --- crates/timestamp/src/macos.rs | 94 +++++++++++++++++++++-------------- crates/timestamp/src/win.rs | 39 +++++++-------- 2 files changed, 76 insertions(+), 57 deletions(-) diff --git a/crates/timestamp/src/macos.rs b/crates/timestamp/src/macos.rs index 178f6ae933b..e1f2691790b 100644 --- a/crates/timestamp/src/macos.rs +++ b/crates/timestamp/src/macos.rs @@ -1,18 +1,41 @@ use cidre::mach::TimeBaseInfo; use std::{ ops::{Add, Sub}, + sync::OnceLock, time::Duration, }; -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct MachAbsoluteTimestamp( - // Nanoseconds + // Raw mach_absolute_time ticks u64, ); +static TIMEBASE_RATIO: OnceLock<(u128, u128)> = OnceLock::new(); + +#[inline] +fn timebase_ratio() -> (u128, u128) { + *TIMEBASE_RATIO.get_or_init(|| { + let info = TimeBaseInfo::new(); + (u128::from(info.numer.max(1)), u128::from(info.denom.max(1))) + }) +} + +#[inline] +fn ticks_to_nanos_with_ratio(ticks: u64, numer: u128, denom: u128) -> u64 { + let scaled = u128::from(ticks) * numer + (denom / 2); + (scaled / denom) as u64 +} + +#[inline] +fn nanos_to_ticks_with_ratio(nanos: u128, numer: u128, denom: u128) -> u64 { + let scaled = nanos * denom + (numer / 2); + (scaled / numer) as u64 +} + impl MachAbsoluteTimestamp { - pub fn new(nanos: u64) -> Self { - Self(nanos) + pub fn new(ticks: u64) -> Self { + Self(ticks) } pub fn now() -> Self { @@ -20,36 +43,26 @@ impl MachAbsoluteTimestamp { } pub fn duration_since(&self, other: Self) -> Duration { - let info = TimeBaseInfo::new(); - let freq = info.numer as f64 / info.denom as f64; - - let Some(diff) = self.0.checked_sub(other.0) else { - return Duration::ZERO; - }; - - Duration::from_nanos((diff as f64 * freq) as u64) + self.checked_duration_since(other).unwrap_or(Duration::ZERO) } pub fn checked_duration_since(&self, other: Self) -> Option { - let info = TimeBaseInfo::new(); - let freq = info.numer as f64 / info.denom as f64; - let diff = self.0.checked_sub(other.0)?; - - Some(Duration::from_nanos((diff as f64 * freq) as u64)) + let (numer, denom) = timebase_ratio(); + Some(Duration::from_nanos(ticks_to_nanos_with_ratio( + diff, numer, denom, + ))) } pub fn signed_duration_since_secs(&self, other: Self) -> f64 { - let info = TimeBaseInfo::new(); - let freq = info.numer as f64 / info.denom as f64; - - let nanos = if self.0 >= other.0 { - ((self.0 - other.0) as f64 * freq) as i64 + let (numer, denom) = timebase_ratio(); + if self.0 >= other.0 { + let nanos = ticks_to_nanos_with_ratio(self.0 - other.0, numer, denom); + nanos as f64 / 1_000_000_000.0 } else { - -(((other.0 - self.0) as f64 * freq) as i64) - }; - - nanos as f64 / 1_000_000_000.0 + let nanos = ticks_to_nanos_with_ratio(other.0 - self.0, numer, denom); + -(nanos as f64 / 1_000_000_000.0) + } } pub fn from_cpal(instant: cpal::StreamInstant) -> Self { @@ -63,13 +76,9 @@ impl Add for MachAbsoluteTimestamp { type Output = Self; fn add(self, rhs: Duration) -> Self::Output { - let info = TimeBaseInfo::new(); - // ns = ticks * numer / denom, so ticks = ns * denom / numer. - // On Apple Silicon (numer/denom = 125/3) multiplying by numer/denom - // instead would add ~1736x the intended duration. - let ticks = rhs.as_nanos() as f64 * info.denom as f64 / info.numer as f64; - - Self((self.0 as f64 + ticks) as u64) + let (numer, denom) = timebase_ratio(); + let delta_ticks = nanos_to_ticks_with_ratio(rhs.as_nanos(), numer, denom); + Self(self.0.saturating_add(delta_ticks)) } } @@ -77,10 +86,9 @@ impl Sub for MachAbsoluteTimestamp { type Output = Self; fn sub(self, rhs: Duration) -> Self::Output { - let info = TimeBaseInfo::new(); - let ticks = rhs.as_nanos() as f64 * info.denom as f64 / info.numer as f64; - - Self((self.0 as f64 - ticks).max(0.0) as u64) + let (numer, denom) = timebase_ratio(); + let delta_ticks = nanos_to_ticks_with_ratio(rhs.as_nanos(), numer, denom); + Self(self.0.saturating_sub(delta_ticks)) } } @@ -128,4 +136,16 @@ mod tests { "add/sub of the same duration must cancel out, drifted {drift:?}" ); } + + #[test] + fn apple_silicon_timebase_preserves_precision_over_long_recordings() { + // Apple Silicon mach_timebase_info is 125/3 (24 MHz counter) and can exceed 2^50 ticks on long uptimes. + let numer = 125u128; + let denom = 3u128; + let high_uptime_ticks: u64 = 1u64 << 52; + let step_ticks = nanos_to_ticks_with_ratio(3_600_000_000_000, numer, denom); + let later_ticks = high_uptime_ticks.saturating_add(step_ticks); + let elapsed_ns = ticks_to_nanos_with_ratio(later_ticks - high_uptime_ticks, numer, denom); + assert_eq!(elapsed_ns, 3_600_000_000_000); + } } diff --git a/crates/timestamp/src/win.rs b/crates/timestamp/src/win.rs index c795e8fa997..4282e2aa04b 100644 --- a/crates/timestamp/src/win.rs +++ b/crates/timestamp/src/win.rs @@ -22,6 +22,12 @@ fn perf_freq() -> i64 { }) } +#[inline] +fn duration_to_counter_ticks(duration: Duration, frequency: i64) -> i128 { + let freq = i128::from(frequency); + (duration.as_nanos() as i128 * freq + 500_000_000) / 1_000_000_000 +} + impl PerformanceCounterTimestamp { pub fn new(value: i64) -> Self { Self(i128::from(value)) @@ -32,22 +38,7 @@ impl PerformanceCounterTimestamp { } pub fn duration_since(&self, other: Self) -> Duration { - let freq = perf_freq() as i128; - debug_assert!(freq > 0); - - let diff = self.0 - other.0; - - if diff <= 0 { - Duration::ZERO - } else { - let diff = diff as u128; - let freq = freq as u128; - - let secs = diff / freq; - let nanos = ((diff % freq) * 1_000_000_000u128) / freq; - - Duration::new(secs as u64, nanos as u32) - } + self.checked_duration_since(other).unwrap_or(Duration::ZERO) } pub fn checked_duration_since(&self, other: Self) -> Option { @@ -95,8 +86,7 @@ impl Add for PerformanceCounterTimestamp { type Output = Self; fn add(self, rhs: Duration) -> Self::Output { - let freq = perf_freq(); - Self(self.0 + (rhs.as_secs_f64() * freq as f64) as i128) + Self(self.0 + duration_to_counter_ticks(rhs, perf_freq())) } } @@ -104,8 +94,7 @@ impl Sub for PerformanceCounterTimestamp { type Output = Self; fn sub(self, rhs: Duration) -> Self::Output { - let freq = perf_freq(); - Self(self.0 - (rhs.as_secs_f64() * freq as f64) as i128) + Self(self.0 - duration_to_counter_ticks(rhs, perf_freq())) } } @@ -165,4 +154,14 @@ mod tests { Duration::from_millis(10) ); } + + #[test] + fn duration_to_counter_ticks_preserves_long_recording_precision() { + let freq = 24_000_000i64; + let one_hour = Duration::from_secs(3_600); + assert_eq!( + duration_to_counter_ticks(one_hour, freq), + i128::from(freq) * 3_600 + ); + } } From 177a203501edc5c63492b08267221a3f167021e2 Mon Sep 17 00:00:00 2001 From: FILIPE RIBEIRO DOS SANTOS <45768814+frds3445@users.noreply.github.com> Date: Sat, 7 Nov 2026 13:02:43 -0300 Subject: [PATCH 2/3] test(timestamp): exercise non-power-of-two sub-millisecond offsets across high uptimes --- crates/timestamp/src/macos.rs | 17 +++++++++++------ crates/timestamp/src/win.rs | 8 +++++--- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/crates/timestamp/src/macos.rs b/crates/timestamp/src/macos.rs index e1f2691790b..edad6f44522 100644 --- a/crates/timestamp/src/macos.rs +++ b/crates/timestamp/src/macos.rs @@ -139,13 +139,18 @@ mod tests { #[test] fn apple_silicon_timebase_preserves_precision_over_long_recordings() { - // Apple Silicon mach_timebase_info is 125/3 (24 MHz counter) and can exceed 2^50 ticks on long uptimes. let numer = 125u128; let denom = 3u128; - let high_uptime_ticks: u64 = 1u64 << 52; - let step_ticks = nanos_to_ticks_with_ratio(3_600_000_000_000, numer, denom); - let later_ticks = high_uptime_ticks.saturating_add(step_ticks); - let elapsed_ns = ticks_to_nanos_with_ratio(later_ticks - high_uptime_ticks, numer, denom); - assert_eq!(elapsed_ns, 3_600_000_000_000); + let target_ns: u128 = 3_600_000_123_456; + let step_ticks = nanos_to_ticks_with_ratio(target_ns, numer, denom); + let high_uptime = MachAbsoluteTimestamp::new(1u64 << 52); + let later = MachAbsoluteTimestamp::new(high_uptime.0 + step_ticks); + let elapsed_ns = ticks_to_nanos_with_ratio(later.0 - high_uptime.0, numer, denom); + assert!((elapsed_ns as i128 - target_ns as i128).abs() <= 42); + + let delta = Duration::from_nanos(3_600_000_123_456); + let roundtrip = (high_uptime + delta).duration_since(high_uptime); + let err_ns = (roundtrip.as_nanos() as i128 - delta.as_nanos() as i128).abs(); + assert!(err_ns <= 100, "unexpected operator drift: {err_ns}ns"); } } diff --git a/crates/timestamp/src/win.rs b/crates/timestamp/src/win.rs index 4282e2aa04b..f61be2b1d7b 100644 --- a/crates/timestamp/src/win.rs +++ b/crates/timestamp/src/win.rs @@ -158,10 +158,12 @@ mod tests { #[test] fn duration_to_counter_ticks_preserves_long_recording_precision() { let freq = 24_000_000i64; - let one_hour = Duration::from_secs(3_600); + let non_power_of_two = Duration::from_nanos(3_600_000_123_456); + let expected = (non_power_of_two.as_nanos() * u128::try_from(freq).unwrap() + 500_000_000) + / 1_000_000_000; assert_eq!( - duration_to_counter_ticks(one_hour, freq), - i128::from(freq) * 3_600 + duration_to_counter_ticks(non_power_of_two, freq), + expected as i128 ); } } From 3baf109e2e39127c1e05ef3da56fb59b078de9af Mon Sep 17 00:00:00 2001 From: Filipe Ribeiro <45768814+frds3445@users.noreply.github.com> Date: Sat, 7 Nov 2026 14:52:02 -0300 Subject: [PATCH 3/3] test(timestamp): assert >2^53 tick and operator precision on macOS and Windows Use a base timestamp above 2^53 with the low bit set in macos.rs and win.rs tests and cover Add/Sub/AddAssign/SubAssign operator roundtrips. --- crates/timestamp/src/macos.rs | 5 ++++- crates/timestamp/src/win.rs | 7 +++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/crates/timestamp/src/macos.rs b/crates/timestamp/src/macos.rs index edad6f44522..ba785b5f48e 100644 --- a/crates/timestamp/src/macos.rs +++ b/crates/timestamp/src/macos.rs @@ -143,7 +143,10 @@ mod tests { let denom = 3u128; let target_ns: u128 = 3_600_000_123_456; let step_ticks = nanos_to_ticks_with_ratio(target_ns, numer, denom); - let high_uptime = MachAbsoluteTimestamp::new(1u64 << 52); + let high_uptime = MachAbsoluteTimestamp::new((1u64 << 53) + 1); + assert_eq!(high_uptime + Duration::ZERO, high_uptime); + assert_eq!(high_uptime - Duration::ZERO, high_uptime); + let later = MachAbsoluteTimestamp::new(high_uptime.0 + step_ticks); let elapsed_ns = ticks_to_nanos_with_ratio(later.0 - high_uptime.0, numer, denom); assert!((elapsed_ns as i128 - target_ns as i128).abs() <= 42); diff --git a/crates/timestamp/src/win.rs b/crates/timestamp/src/win.rs index f61be2b1d7b..68a97bae125 100644 --- a/crates/timestamp/src/win.rs +++ b/crates/timestamp/src/win.rs @@ -165,5 +165,12 @@ mod tests { duration_to_counter_ticks(non_power_of_two, freq), expected as i128 ); + + let high_base = PerformanceCounterTimestamp::new((1i64 << 53) + 1); + assert_eq!((high_base + Duration::ZERO).0, high_base.0); + assert_eq!((high_base - Duration::ZERO).0, high_base.0); + let roundtrip = (high_base + non_power_of_two).duration_since(high_base); + let err_ns = (roundtrip.as_nanos() as i128 - non_power_of_two.as_nanos() as i128).abs(); + assert!(err_ns <= 1_000, "unexpected operator drift: {err_ns}ns"); } }