use core::ops::{Div, DivAssign, Mul, MulAssign};
use rinia::FloatScalar;
#[cfg(feature = "zerocopy")]
use zerocopy::*;
use crate::{
Duration, HasDuration,
macros::{impl_approx_forwarding, impl_bytemuck_transparent, impl_min_max_forwarding},
};
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
#[cfg_attr(feature = "zerocopy", derive(FromBytes, Immutable, IntoBytes, KnownLayout))]
#[repr(transparent)]
pub struct FrameRate<T: FloatScalar>(T);
impl<T: FloatScalar> FrameRate<T> {
#[inline]
pub const fn from_fps_unchecked(fps: T) -> Self {
Self(fps)
}
#[inline]
pub fn from_fps(fps: T) -> Self {
assert!(fps.is_finite(), "FrameRate must be finite");
assert!(fps > T::ZERO, "FrameRate must be positive");
Self(fps)
}
#[inline]
pub fn from_seconds_per_frame_unchecked(spf: T) -> Self {
Self::from_fps_unchecked(T::ONE / spf)
}
#[inline]
pub fn from_seconds_per_frame(spf: T) -> Self {
assert!(spf.is_finite(), "seconds_per_frame must be finite");
assert!(spf > T::ZERO, "seconds_per_frame must be positive");
Self::from_fps(T::ONE / spf)
}
#[inline]
pub fn from_duration_unchecked(duration: Duration<T>) -> Self {
Self::from_seconds_per_frame_unchecked(duration.seconds())
}
#[inline]
pub fn from_duration(duration: Duration<T>) -> Self {
assert!(duration > Duration::zero(), "Duration must be positive");
Self::from_seconds_per_frame(duration.seconds())
}
#[inline]
pub fn fps(&self) -> T {
self.0
}
#[inline]
pub fn seconds_per_frame(&self) -> T {
T::ONE / self.0
}
#[inline]
pub fn duration(&self) -> Duration<T> {
Duration::from_seconds(self.seconds_per_frame())
}
#[inline]
pub fn ratio(&self, other: FrameRate<T>) -> T {
*self / other
}
impl_min_max_forwarding!(T, 0);
}
impl<T: FloatScalar> From<T> for FrameRate<T> {
#[inline]
fn from(value: T) -> Self {
Self::from_fps(value)
}
}
impl From<FrameRate<f32>> for f32 {
#[inline]
fn from(frame_rate: FrameRate<f32>) -> Self {
frame_rate.fps()
}
}
impl From<FrameRate<f64>> for f64 {
#[inline]
fn from(frame_rate: FrameRate<f64>) -> Self {
frame_rate.fps()
}
}
impl<T: FloatScalar> From<Duration<T>> for FrameRate<T> {
#[inline]
fn from(value: Duration<T>) -> Self {
Self::from_duration(value)
}
}
impl<T: FloatScalar> From<FrameRate<T>> for Duration<T> {
#[inline]
fn from(value: FrameRate<T>) -> Self {
value.duration()
}
}
impl_approx_forwarding!(FrameRate<T>, 0);
impl<T: FloatScalar> HasDuration<T> for FrameRate<T> {
#[inline]
fn duration(&self) -> Duration<T> {
FrameRate::duration(self)
}
}
impl<T: FloatScalar> Div for FrameRate<T> {
type Output = T;
#[inline]
fn div(self, rhs: Self) -> T {
self.fps() / rhs.fps()
}
}
impl<T: FloatScalar> Mul<T> for FrameRate<T> {
type Output = Self;
#[inline]
fn mul(self, rhs: T) -> Self::Output {
Self::from_fps(self.fps() * rhs)
}
}
impl<T: FloatScalar> MulAssign<T> for FrameRate<T> {
#[inline]
fn mul_assign(&mut self, rhs: T) {
*self = Self::from_fps(self.fps() * rhs);
}
}
impl<T: FloatScalar> Div<T> for FrameRate<T> {
type Output = Self;
#[inline]
fn div(self, rhs: T) -> Self::Output {
Self::from_fps(self.fps() / rhs)
}
}
impl<T: FloatScalar> DivAssign<T> for FrameRate<T> {
#[inline]
fn div_assign(&mut self, rhs: T) {
*self = Self::from_fps(self.fps() / rhs);
}
}
macro_rules! impl_common_fps_defaults {
($scalar:ty) => {
impl FrameRate<$scalar> {
pub const FPS_24: Self = Self::from_fps_unchecked(24.0);
pub const FPS_30: Self = Self::from_fps_unchecked(30.0);
pub const FPS_60: Self = Self::from_fps_unchecked(60.0);
pub const FPS_120: Self = Self::from_fps_unchecked(120.0);
pub const FPS_240: Self = Self::from_fps_unchecked(240.0);
}
};
}
impl_common_fps_defaults!(f32);
impl_common_fps_defaults!(f64);
impl_bytemuck_transparent!(FrameRate<T>, T);
#[cfg(test)]
mod tests {
use approx::assert_abs_diff_eq;
use super::*;
#[test]
fn from_fps_and_getters_work() {
let r = FrameRate::from_fps(50.0_f64);
assert_abs_diff_eq!(r.fps(), 50.0, epsilon = 1e-12);
assert_abs_diff_eq!(r.seconds_per_frame(), 0.02, epsilon = 1e-12);
}
#[test]
fn from_seconds_per_frame_and_duration_roundtrip() {
let r = FrameRate::from_seconds_per_frame(0.25_f64);
let d = r.duration();
let r2 = FrameRate::from_duration(d);
assert_abs_diff_eq!(r.fps(), 4.0, epsilon = 1e-12);
assert_abs_diff_eq!(d.seconds(), 0.25, epsilon = 1e-12);
assert_abs_diff_eq!(r2.fps(), 4.0, epsilon = 1e-12);
}
#[test]
#[should_panic(expected = "FrameRate must be positive")]
fn from_fps_panics_on_non_positive() {
let _ = FrameRate::from_fps(0.0_f64);
}
#[test]
#[should_panic(expected = "seconds_per_frame must be positive")]
fn from_seconds_per_frame_panics_on_non_positive() {
let _ = FrameRate::from_seconds_per_frame(-1.0_f64);
}
#[test]
#[should_panic(expected = "Duration must be positive")]
fn from_duration_panics_on_non_positive_duration() {
let _ = FrameRate::<f64>::from_duration(Duration::zero());
}
#[test]
fn ratio_and_arithmetic_ops_work() {
let mut r = FrameRate::from_fps(30.0_f64);
let base = FrameRate::from_fps(10.0_f64);
assert_abs_diff_eq!(r.ratio(base), 3.0, epsilon = 1e-12);
assert_abs_diff_eq!((r * 2.0).fps(), 60.0, epsilon = 1e-12);
assert_abs_diff_eq!((r / 3.0).fps(), 10.0, epsilon = 1e-12);
r *= 2.0;
assert_abs_diff_eq!(r.fps(), 60.0, epsilon = 1e-12);
r /= 4.0;
assert_abs_diff_eq!(r.fps(), 15.0, epsilon = 1e-12);
}
#[test]
fn min_max_choose_expected_values() {
let a = FrameRate::from_fps(24.0_f64);
let b = FrameRate::from_fps(60.0_f64);
assert_eq!(a.min(b), a);
assert_eq!(a.max(b), b);
}
#[test]
fn from_into_conversions_work() {
let r: FrameRate<f64> = 48.0_f64.into();
let raw: f64 = r.into();
assert_abs_diff_eq!(raw, 48.0, epsilon = 1e-12);
let d: Duration<f64> = r.into();
assert_abs_diff_eq!(d.seconds(), 1.0 / 48.0, epsilon = 1e-12);
let r2: FrameRate<f64> = d.into();
assert_abs_diff_eq!(r2.fps(), 48.0, epsilon = 1e-12);
}
#[test]
fn has_duration_and_common_defaults_are_valid() {
let d = HasDuration::duration(&FrameRate::<f64>::FPS_24);
assert_abs_diff_eq!(d.seconds(), 1.0 / 24.0, epsilon = 1e-12);
assert_abs_diff_eq!(FrameRate::<f64>::FPS_30.fps(), 30.0, epsilon = 1e-12);
assert_abs_diff_eq!(FrameRate::<f64>::FPS_60.fps(), 60.0, epsilon = 1e-12);
assert_abs_diff_eq!(FrameRate::<f64>::FPS_120.fps(), 120.0, epsilon = 1e-12);
assert_abs_diff_eq!(FrameRate::<f64>::FPS_240.fps(), 240.0, epsilon = 1e-12);
}
#[cfg(feature = "bytemuck")]
#[test]
fn bytemuck_traits_are_implemented() {
fn assert_impl<
T: bytemuck::Pod + bytemuck::Zeroable + bytemuck::TransparentWrapper<f32>,
>() {
}
assert_impl::<FrameRate<f32>>();
}
#[cfg(feature = "bytemuck")]
#[test]
fn bytemuck_roundtrip_works() {
let value = FrameRate::from_fps(48.0_f32);
let bytes = bytemuck::bytes_of(&value);
let decoded = *bytemuck::from_bytes::<FrameRate<f32>>(bytes);
assert_eq!(decoded, value);
}
#[cfg(feature = "zerocopy")]
#[test]
fn zerocopy_traits_are_implemented() {
fn assert_impl<
T: zerocopy::FromBytes + zerocopy::IntoBytes + zerocopy::KnownLayout + zerocopy::Immutable,
>() {
}
assert_impl::<FrameRate<f32>>();
}
#[cfg(feature = "zerocopy")]
#[test]
fn zerocopy_roundtrip_works() {
let value = FrameRate::from_fps(120.0_f32);
let bytes = <FrameRate<f32> as zerocopy::IntoBytes>::as_bytes(&value);
let decoded = <FrameRate<f32> as zerocopy::FromBytes>::ref_from_bytes(bytes)
.expect("FrameRate bytes should decode");
assert_eq!(*decoded, value);
}
}