use crate::SurfaceStats;
use std::time::{Duration, Instant};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct HeavyEngine {
pub max_hz: f32,
pub min_hz: f32,
pub lag_budget_ms: f32,
pub chunk_ms: f32,
pub max_chunks: u32,
}
impl Default for HeavyEngine {
fn default() -> Self {
Self {
max_hz: 60.,
min_hz: 0.25,
lag_budget_ms: 33.,
chunk_ms: 4.,
max_chunks: 64,
}
}
}
#[cfg_attr(not(feature = "wgpu"), allow(dead_code))] impl HeavyEngine {
fn validated(self) -> Self {
let max_hz = if self.max_hz.is_finite() {
self.max_hz.clamp(0.01, 1000.)
} else {
60.
};
let min_hz = if self.min_hz.is_finite() {
self.min_hz.clamp(0.01, max_hz)
} else {
0.25f32.min(max_hz)
};
Self {
max_hz,
min_hz,
lag_budget_ms: if self.lag_budget_ms.is_finite() && self.lag_budget_ms > 0. {
self.lag_budget_ms
} else {
33.
},
chunk_ms: if self.chunk_ms.is_finite() && self.chunk_ms > 0. {
self.chunk_ms
} else {
4.
},
max_chunks: self.max_chunks.clamp(1, 64),
}
}
}
#[cfg_attr(not(feature = "wgpu"), allow(dead_code))] #[derive(Debug)]
pub(crate) struct Heavy {
config: HeavyEngine,
rate: f32,
frame_ms: Option<f32>,
pending: Option<Instant>,
}
#[cfg_attr(not(feature = "wgpu"), allow(dead_code))] impl Heavy {
pub(crate) fn new(config: HeavyEngine) -> Self {
let config = config.validated();
Self {
rate: config.max_hz,
config,
frame_ms: None,
pending: None,
}
}
#[cfg(test)]
pub(crate) fn config(&self) -> HeavyEngine {
self.config
}
pub(crate) fn on_present(&mut self, submitted: Instant) {
self.pending.get_or_insert(submitted);
}
pub(crate) fn observe(&mut self, reached: bool) {
if reached && let Some(submitted) = self.pending.take() {
let ms = submitted.elapsed().as_secs_f32() * 1000.;
self.frame_ms = Some(match self.frame_ms {
Some(previous) => previous * 0.7 + ms * 0.3,
None => ms,
});
}
}
pub(crate) fn pace(&mut self, stats: &SurfaceStats) -> Duration {
let lag = stats.composite_latency_ms;
let budget = self.config.lag_budget_ms;
if lag > budget {
self.rate = (self.rate * 0.5).max(self.config.min_hz);
} else if lag < budget * (2. / 3.) {
self.rate = (self.rate * 1.1).min(self.config.max_hz);
}
Duration::from_secs_f32(1. / self.rate)
}
#[cfg(test)]
pub(crate) fn rate_hz(&self) -> f32 {
self.rate
}
#[cfg(test)]
pub(crate) fn frame_ms(&self) -> Option<f32> {
self.frame_ms
}
pub(crate) fn suggested_chunks(&self) -> u32 {
self.frame_ms
.map(|ms| {
(ms / self.config.chunk_ms)
.ceil()
.clamp(1., self.config.max_chunks as f32) as u32
})
.unwrap_or(1)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct HeavyStatus {
pub config: HeavyEngine,
pub rate_hz: f32,
pub frame_ms: Option<f32>,
pub suggested_chunks: u32,
}
#[cfg_attr(not(feature = "wgpu"), allow(dead_code))] impl Heavy {
pub(crate) fn status(&self) -> HeavyStatus {
HeavyStatus {
config: self.config,
rate_hz: self.rate,
frame_ms: self.frame_ms,
suggested_chunks: self.suggested_chunks(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn lagging(ms: f32) -> SurfaceStats {
SurfaceStats {
composite_latency_ms: ms,
..Default::default()
}
}
#[test]
fn pace_backs_off_while_lagging_and_recovers() {
let mut heavy = Heavy::new(HeavyEngine::default());
assert_eq!(heavy.pace(&lagging(5.)), Duration::from_secs_f32(1. / 60.));
for _ in 0..20 {
heavy.pace(&lagging(200.));
}
assert_eq!(heavy.rate_hz(), 0.25, "floors at min_hz");
heavy.pace(&lagging(30.));
assert_eq!(heavy.rate_hz(), 0.25);
for _ in 0..200 {
heavy.pace(&lagging(5.));
}
assert_eq!(heavy.rate_hz(), 60., "recovers to max_hz");
}
#[test]
fn chunks_follow_measured_frame_time() {
let mut heavy = Heavy::new(HeavyEngine::default());
assert_eq!(heavy.suggested_chunks(), 1, "unmeasured");
heavy.on_present(Instant::now() - Duration::from_millis(40));
heavy.observe(false);
assert_eq!(heavy.frame_ms(), None, "not reached yet");
heavy.observe(true);
let ms = heavy.frame_ms().unwrap();
assert!(ms >= 40., "{ms}");
let cap = HeavyEngine::default().max_chunks;
assert_eq!(heavy.suggested_chunks(), ((ms / 4.).ceil() as u32).min(cap));
heavy.on_present(Instant::now() - Duration::from_secs(2));
heavy.observe(true);
assert_eq!(heavy.suggested_chunks(), cap);
}
#[test]
fn invalid_config_is_sanitised() {
let heavy = Heavy::new(HeavyEngine {
max_hz: f32::NAN,
min_hz: 500.,
lag_budget_ms: -1.,
chunk_ms: 0.,
max_chunks: 0,
});
let config = heavy.config();
assert_eq!(config.max_hz, 60.);
assert_eq!(config.min_hz, 60.);
assert_eq!(config.lag_budget_ms, 33.);
assert_eq!(config.chunk_ms, 4.);
assert_eq!(config.max_chunks, 1);
}
}