pub trait MotionRate {
fn advance(&self, scroll_q8: &mut i32);
fn pixels_per_frame(&self) -> i32;
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct FrameRoundedRate {
pub pixels_per_sec: u32,
pub frame_hz: u32,
}
impl FrameRoundedRate {
#[inline]
pub const fn new(pixels_per_sec: u32, frame_hz: u32) -> Self {
Self {
pixels_per_sec,
frame_hz,
}
}
#[inline]
pub const fn px_per_frame(&self) -> u32 {
if self.frame_hz == 0 {
return 0;
}
(self.pixels_per_sec + self.frame_hz / 2) / self.frame_hz
}
}
impl MotionRate for FrameRoundedRate {
#[inline]
fn advance(&self, scroll_q8: &mut i32) {
let pxf = self.px_per_frame() as i32;
*scroll_q8 = scroll_q8.wrapping_add(pxf << 8);
}
#[inline]
fn pixels_per_frame(&self) -> i32 {
self.px_per_frame() as i32
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct SubPixelRate {
pub pixels_per_sec: u32,
pub frame_hz: u32,
}
impl SubPixelRate {
#[inline]
pub const fn new(pixels_per_sec: u32, frame_hz: u32) -> Self {
Self {
pixels_per_sec,
frame_hz,
}
}
#[inline]
pub const fn q8_per_frame(&self) -> i32 {
if self.frame_hz == 0 {
return 0;
}
((self.pixels_per_sec * 256) / self.frame_hz) as i32
}
}
impl MotionRate for SubPixelRate {
#[inline]
fn advance(&self, scroll_q8: &mut i32) {
*scroll_q8 = scroll_q8.wrapping_add(self.q8_per_frame());
}
#[inline]
fn pixels_per_frame(&self) -> i32 {
self.q8_per_frame() >> 8
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn thirty_pps_at_thirty_hz_is_one_pixel_per_frame() {
let rate = FrameRoundedRate::new(30, 30);
assert_eq!(rate.px_per_frame(), 1);
let mut scroll = 0;
rate.advance(&mut scroll);
assert_eq!(scroll >> 8, 1);
}
#[test]
fn thirty_pps_at_sixty_hz_rounds_up_to_one() {
let rate = FrameRoundedRate::new(30, 60);
assert_eq!(rate.px_per_frame(), 1);
}
#[test]
fn twenty_nine_pps_at_sixty_hz_rounds_down_to_zero() {
let rate = FrameRoundedRate::new(29, 60);
assert_eq!(rate.px_per_frame(), 0);
let mut scroll = 0;
for _ in 0..10 {
rate.advance(&mut scroll);
}
assert_eq!(scroll, 0, "zero-rounded rate should not move");
}
#[test]
fn sixty_pps_at_thirty_hz_is_two_pixels_per_frame() {
let rate = FrameRoundedRate::new(60, 30);
assert_eq!(rate.px_per_frame(), 2);
let mut scroll = 0;
rate.advance(&mut scroll);
assert_eq!(scroll, 2 << 8);
}
#[test]
fn accumulator_advances_linearly() {
let rate = FrameRoundedRate::new(30, 30);
let mut scroll = 0;
for expected in 1..=10 {
rate.advance(&mut scroll);
assert_eq!(scroll >> 8, expected);
}
}
#[test]
fn advance_uses_wrapping_arithmetic() {
let rate = FrameRoundedRate::new(30, 30);
let mut scroll = i32::MAX - 64;
rate.advance(&mut scroll);
let _ = scroll;
}
#[test]
fn zero_frame_hz_is_safe() {
let rate = FrameRoundedRate::new(30, 0);
assert_eq!(rate.px_per_frame(), 0);
let mut scroll = 0;
rate.advance(&mut scroll);
assert_eq!(scroll, 0);
}
#[test]
fn pixels_per_frame_matches_px_per_frame() {
let rate = FrameRoundedRate::new(90, 30);
assert_eq!(rate.pixels_per_frame(), 3);
assert_eq!(rate.px_per_frame() as i32, rate.pixels_per_frame());
}
#[test]
fn subpixel_forty_pps_at_thirty_hz_matches_hardware() {
let rate = SubPixelRate::new(40, 30);
assert_eq!(rate.q8_per_frame(), 341);
let mut scroll = 0;
for _ in 0..30 {
rate.advance(&mut scroll);
}
assert!(
(39..=41).contains(&(scroll >> 8)),
"expected ~40 integer px after 30 frames, got {}",
scroll >> 8
);
}
#[test]
fn subpixel_forty_pps_at_sixty_hz_also_matches_forty() {
let rate = SubPixelRate::new(40, 60);
let mut scroll = 0;
for _ in 0..60 {
rate.advance(&mut scroll);
}
assert!(
(39..=41).contains(&(scroll >> 8)),
"expected ~40 integer px after 60 frames, got {}",
scroll >> 8
);
}
#[test]
fn subpixel_zero_frame_hz_is_safe() {
let rate = SubPixelRate::new(40, 0);
assert_eq!(rate.q8_per_frame(), 0);
let mut scroll = 0;
rate.advance(&mut scroll);
assert_eq!(scroll, 0);
}
#[test]
fn subpixel_pixels_per_frame_is_integer_floor() {
let rate = SubPixelRate::new(60, 30);
assert_eq!(rate.pixels_per_frame(), 2);
let rate = SubPixelRate::new(40, 30);
assert_eq!(rate.pixels_per_frame(), 1);
}
}