use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ControlFlowIntent {
Wait,
WaitUntil(Instant),
Unchanged,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PacedDecision {
pub paced_wake: Option<Instant>,
pub request_redraw: bool,
pub control_flow: ControlFlowIntent,
}
pub fn next_paced_wake(
needs_frame: bool,
needs_frame_paced_only: bool,
anim_pacing: bool,
now: Instant,
cosmetic_loop_hz: f32,
requested_interval: Option<Duration>,
) -> PacedDecision {
if !needs_frame {
return PacedDecision {
paced_wake: None,
request_redraw: false,
control_flow: ControlFlowIntent::Wait,
};
}
if anim_pacing && needs_frame_paced_only {
let cap = Duration::from_secs_f32(1.0 / cosmetic_loop_hz);
let interval = match requested_interval {
Some(requested) => cap.max(requested),
None => cap,
};
PacedDecision {
paced_wake: Some(now + interval),
request_redraw: false,
control_flow: ControlFlowIntent::Unchanged,
}
} else {
PacedDecision {
paced_wake: None,
request_redraw: true,
control_flow: ControlFlowIntent::Wait,
}
}
}
pub fn paced_wake_action(paced_wake: Option<Instant>, now: Instant) -> PacedDecision {
match paced_wake {
None => PacedDecision {
paced_wake: None,
request_redraw: false,
control_flow: ControlFlowIntent::Wait,
},
Some(deadline) if now >= deadline => PacedDecision {
paced_wake: None,
request_redraw: true,
control_flow: ControlFlowIntent::Wait,
},
Some(deadline) => PacedDecision {
paced_wake: Some(deadline),
request_redraw: false,
control_flow: ControlFlowIntent::WaitUntil(deadline),
},
}
}
pub const PRESENT_RETRY_BASE: Duration = Duration::from_millis(33);
pub const PRESENT_RETRY_MAX: Duration = Duration::from_secs(1);
pub fn present_retry_delay(streak: u32) -> Duration {
PRESENT_RETRY_BASE
.saturating_mul(1u32 << streak.min(16))
.min(PRESENT_RETRY_MAX)
}
pub fn schedule_present_retry(paced_wake: Option<Instant>, now: Instant, streak: u32) -> Instant {
let retry = now + present_retry_delay(streak);
match paced_wake {
Some(deadline) => deadline.min(retry),
None => retry,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn base() -> Instant {
Instant::now()
}
fn interval_30hz() -> Duration {
Duration::from_secs_f32(1.0 / 30.0)
}
#[test]
fn settling_clears_to_idle_and_returns_to_wait() {
let now = base();
for paced_only in [true, false] {
for anim_pacing in [true, false] {
for requested in [None, Some(Duration::from_millis(500))] {
assert_eq!(
next_paced_wake(false, paced_only, anim_pacing, now, 30.0, requested),
PacedDecision {
paced_wake: None,
request_redraw: false,
control_flow: ControlFlowIntent::Wait,
}
);
}
}
}
}
#[test]
fn paced_only_with_pacing_enabled_schedules_and_leaves_control_flow_unchanged() {
let now = base();
assert_eq!(
next_paced_wake(true, true, true, now, 30.0, None),
PacedDecision {
paced_wake: Some(now + interval_30hz()),
request_redraw: false,
control_flow: ControlFlowIntent::Unchanged,
}
);
}
#[test]
fn paced_only_with_pacing_disabled_fires_now() {
let now = base();
assert_eq!(
next_paced_wake(true, true, false, now, 30.0, None),
PacedDecision {
paced_wake: None,
request_redraw: true,
control_flow: ControlFlowIntent::Wait,
}
);
}
#[test]
fn a_real_transition_fires_now_even_with_pacing_enabled() {
let now = base();
assert_eq!(
next_paced_wake(true, false, true, now, 30.0, None),
PacedDecision {
paced_wake: None,
request_redraw: true,
control_flow: ControlFlowIntent::Wait,
}
);
}
#[test]
fn no_requested_interval_falls_back_to_the_cosmetic_loop_cap() {
let now = base();
assert_eq!(
next_paced_wake(true, true, true, now, 30.0, None),
next_paced_wake(true, true, true, now, 30.0, Some(Duration::ZERO)),
"None and Some(ZERO) both mean \"at the theme's own rate\""
);
assert_eq!(
next_paced_wake(true, true, true, now, 30.0, None),
PacedDecision {
paced_wake: Some(now + interval_30hz()),
request_redraw: false,
control_flow: ControlFlowIntent::Unchanged,
}
);
}
#[test]
fn a_500ms_caret_request_paces_slower_than_the_30hz_cap() {
let now = base();
let requested = Duration::from_millis(500);
assert_eq!(
next_paced_wake(true, true, true, now, 30.0, Some(requested)),
PacedDecision {
paced_wake: Some(now + requested),
request_redraw: false,
control_flow: ControlFlowIntent::Unchanged,
}
);
}
#[test]
fn a_tighter_requested_interval_is_clamped_up_to_the_theme_cap() {
let now = base();
let too_fast = Duration::from_millis(5);
assert_eq!(
next_paced_wake(true, true, true, now, 30.0, Some(too_fast)),
PacedDecision {
paced_wake: Some(now + interval_30hz()),
request_redraw: false,
control_flow: ControlFlowIntent::Unchanged,
}
);
}
#[test]
fn requested_interval_is_ignored_while_pacing_is_disabled() {
let now = base();
assert_eq!(
next_paced_wake(
true,
true,
false,
now,
30.0,
Some(Duration::from_millis(500))
),
PacedDecision {
paced_wake: None,
request_redraw: true,
control_flow: ControlFlowIntent::Wait,
}
);
}
#[test]
fn interval_change_between_paints_re_derives_the_next_deadline() {
let t0 = base();
let d0 = next_paced_wake(true, true, true, t0, 30.0, None);
assert_eq!(d0.paced_wake, Some(t0 + interval_30hz()));
let t1 = t0 + Duration::from_millis(1);
let d1 = next_paced_wake(true, true, true, t1, 30.0, Some(Duration::from_millis(500)));
assert_eq!(
d1.paced_wake,
Some(t1 + Duration::from_millis(500)),
"the deadline must be re-derived from this paint's own requested \
interval, not carried over from the prior 30Hz decision"
);
assert_ne!(
d1.paced_wake, d0.paced_wake,
"the new deadline must differ from the stale 30Hz-derived one"
);
}
#[test]
fn no_pending_wake_returns_to_wait() {
assert_eq!(
paced_wake_action(None, base()),
PacedDecision {
paced_wake: None,
request_redraw: false,
control_flow: ControlFlowIntent::Wait,
}
);
}
#[test]
fn future_deadline_parks() {
let now = base();
let deadline = now + Duration::from_millis(10);
assert_eq!(
paced_wake_action(Some(deadline), now),
PacedDecision {
paced_wake: Some(deadline),
request_redraw: false,
control_flow: ControlFlowIntent::WaitUntil(deadline),
}
);
}
#[test]
fn elapsed_deadline_fires() {
let now = base();
let deadline = now - Duration::from_millis(1);
assert_eq!(
paced_wake_action(Some(deadline), now),
PacedDecision {
paced_wake: None,
request_redraw: true,
control_flow: ControlFlowIntent::Wait,
}
);
}
#[test]
fn exactly_at_deadline_fires() {
let now = base();
assert_eq!(
paced_wake_action(Some(now), now),
PacedDecision {
paced_wake: None,
request_redraw: true,
control_flow: ControlFlowIntent::Wait,
}
);
}
#[test]
fn present_retry_doubles_from_the_base_and_caps() {
assert_eq!(present_retry_delay(0), PRESENT_RETRY_BASE);
assert_eq!(present_retry_delay(1), PRESENT_RETRY_BASE * 2);
assert_eq!(present_retry_delay(4), PRESENT_RETRY_BASE * 16);
assert_eq!(present_retry_delay(5), PRESENT_RETRY_MAX);
assert_eq!(present_retry_delay(u32::MAX), PRESENT_RETRY_MAX);
}
#[test]
fn present_retry_schedules_from_now_when_nothing_is_pending() {
let now = base();
assert_eq!(
schedule_present_retry(None, now, 0),
now + PRESENT_RETRY_BASE
);
}
#[test]
fn present_retry_never_delays_an_earlier_paced_deadline() {
let now = base();
let paced = now + Duration::from_millis(5);
assert_eq!(schedule_present_retry(Some(paced), now, 0), paced);
let later = now + Duration::from_secs(5);
assert_eq!(
schedule_present_retry(Some(later), now, 2),
now + PRESENT_RETRY_BASE * 4
);
}
#[test]
fn a_scheduled_retry_parks_then_fires_through_the_paced_wake() {
let now = base();
let deadline = schedule_present_retry(None, now, 0);
assert_eq!(
paced_wake_action(Some(deadline), now).control_flow,
ControlFlowIntent::WaitUntil(deadline)
);
assert!(paced_wake_action(Some(deadline), deadline).request_redraw);
}
}