use std::time::{Duration, Instant};
pub(crate) const RETRY: Duration = Duration::from_millis(16);
pub(crate) const RETRIES: u32 = 60;
pub(crate) const REST: Duration = Duration::from_millis(500);
#[derive(Debug)]
pub(crate) struct Retry {
owed: Option<(u32, Instant)>,
hidden: bool,
presented: bool,
last_skip: Option<Instant>,
}
impl Retry {
pub(crate) fn new(now: Instant) -> Self {
Retry {
owed: Some((RETRIES, now)),
hidden: false,
presented: false,
last_skip: None,
}
}
pub(crate) fn presented_once(&self) -> bool {
self.presented
}
pub(crate) fn presented(&mut self) {
self.owed = None;
self.presented = true;
self.last_skip = None;
}
pub(crate) fn animation_waits(&self, now: Instant) -> Option<Instant> {
let at = self.last_skip? + RETRY;
(now < at).then_some(at)
}
pub(crate) fn skipped(&mut self, now: Instant) {
let rested = self
.last_skip
.is_none_or(|last| now.saturating_duration_since(last) >= REST);
self.last_skip = Some(now);
if self.hidden {
return;
}
match self.owed {
None => self.owed = Some((RETRIES, now + RETRY)),
Some((0, _)) if rested => self.owed = Some((RETRIES, now + RETRY)),
Some(_) => {}
}
}
pub(crate) fn occluded(&mut self, covered: bool, now: Instant) {
self.hidden = covered;
self.owed = (!covered).then_some((RETRIES, now));
}
pub(crate) fn poll(&mut self, now: Instant) -> (bool, Option<Instant>) {
let Some((left, at)) = &mut self.owed else {
return (false, None);
};
if *left == 0 {
return (false, None);
}
let ask = now >= *at;
if ask {
*left -= 1;
*at = now + RETRY;
}
(ask, Some(*at))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn asks(r: &mut Retry, now: Instant) -> bool {
r.poll(now).0
}
#[test]
fn a_new_windows_first_frame_is_asked_for_until_it_lands() {
let t = Instant::now();
let mut r = Retry::new(t);
assert!(asks(&mut r, t), "at once");
r.skipped(t);
assert!(!asks(&mut r, t + RETRY / 2), "not before the retry");
assert!(asks(&mut r, t + RETRY), "a retry later");
r.presented();
assert!(r.presented_once());
assert_eq!(r.poll(t + RETRY * 3), (false, None), "nothing owed");
}
#[test]
fn a_frame_skipped_after_the_first_is_asked_for_again() {
let t = Instant::now();
let mut r = Retry::new(t);
r.presented();
r.skipped(t);
let (ask, wake) = r.poll(t);
assert!(!ask);
assert_eq!(wake, Some(t + RETRY), "the loop wakes for it");
assert!(asks(&mut r, t + RETRY));
r.presented();
assert_eq!(r.poll(t + RETRY * 2), (false, None));
}
#[test]
fn the_tries_run_out_and_stay_out() {
let t = Instant::now();
let mut r = Retry::new(t);
r.presented();
r.skipped(t);
let mut now = t;
let mut n = 0;
for _ in 0..RETRIES * 2 {
now += RETRY;
if asks(&mut r, now) {
n += 1;
}
r.skipped(now);
}
assert_eq!(n, RETRIES);
r.skipped(now);
assert_eq!(r.poll(now + RETRY), (false, None), "spent");
for i in 1..100u32 {
r.skipped(now + RETRY * i);
}
assert_eq!(r.poll(now + RETRY * 100), (false, None), "still spent");
}
#[test]
fn a_skip_after_a_rest_is_owed_tries_of_its_own() {
let t = Instant::now();
let mut r = Retry::new(t);
r.presented();
r.skipped(t);
let mut now = t;
for _ in 0..RETRIES {
now += RETRY;
assert!(asks(&mut r, now));
r.skipped(now);
}
assert_eq!(r.poll(now + RETRY), (false, None), "spent");
let back = now + Duration::from_secs(10);
r.skipped(back);
assert!(!asks(&mut r, back + RETRY / 2));
assert!(asks(&mut r, back + RETRY), "asked for again");
r.presented();
assert_eq!(r.poll(back + RETRY * 2), (false, None));
}
#[test]
fn an_animation_waits_a_retry_after_a_skip() {
let t = Instant::now();
let mut r = Retry::new(t);
r.presented();
assert_eq!(r.animation_waits(t), None, "presenting: no wait");
let mut now = t;
let mut asked = 0;
while now < t + Duration::from_secs(3) {
match r.animation_waits(now) {
Some(at) => {
assert!(at > now && at <= now + RETRY);
now = at;
}
None => {
asked += 1;
r.skipped(now);
now += Duration::from_micros(50);
}
}
}
let most = (Duration::from_secs(3).as_millis() / RETRY.as_millis()) as u32 + 1;
assert!(asked <= most, "{asked} tries in 3 s, at most {most}");
r.presented();
assert_eq!(r.animation_waits(now), None, "landed: the display's rate");
}
#[test]
fn covered_owes_nothing_and_uncovered_owes_a_frame_at_once() {
let t = Instant::now();
let mut r = Retry::new(t);
r.presented();
r.occluded(true, t);
r.skipped(t);
assert_eq!(r.poll(t + RETRY), (false, None), "hidden: no retry");
let back = t + Duration::from_secs(5);
r.occluded(false, back);
assert!(asks(&mut r, back), "uncovered: at once");
r.skipped(back);
assert!(asks(&mut r, back + RETRY), "and again while it skips");
}
}