xgpui_gpu_interop/
heavy.rs1use crate::SurfaceStats;
22use std::time::{Duration, Instant};
23
24#[derive(Clone, Copy, Debug, PartialEq)]
26pub struct HeavyEngine {
27 pub max_hz: f32,
29 pub min_hz: f32,
31 pub lag_budget_ms: f32,
33 pub chunk_ms: f32,
35 pub max_chunks: u32,
39}
40
41impl Default for HeavyEngine {
42 fn default() -> Self {
43 Self {
44 max_hz: 60.,
45 min_hz: 0.25,
46 lag_budget_ms: 33.,
47 chunk_ms: 4.,
48 max_chunks: 64,
49 }
50 }
51}
52
53#[cfg_attr(not(feature = "wgpu"), allow(dead_code))] impl HeavyEngine {
55 fn validated(self) -> Self {
56 let max_hz = if self.max_hz.is_finite() {
57 self.max_hz.clamp(0.01, 1000.)
58 } else {
59 60.
60 };
61 let min_hz = if self.min_hz.is_finite() {
62 self.min_hz.clamp(0.01, max_hz)
63 } else {
64 0.25f32.min(max_hz)
65 };
66 Self {
67 max_hz,
68 min_hz,
69 lag_budget_ms: if self.lag_budget_ms.is_finite() && self.lag_budget_ms > 0. {
70 self.lag_budget_ms
71 } else {
72 33.
73 },
74 chunk_ms: if self.chunk_ms.is_finite() && self.chunk_ms > 0. {
75 self.chunk_ms
76 } else {
77 4.
78 },
79 max_chunks: self.max_chunks.clamp(1, 64),
80 }
81 }
82}
83
84#[cfg_attr(not(feature = "wgpu"), allow(dead_code))] #[derive(Debug)]
87pub(crate) struct Heavy {
88 config: HeavyEngine,
89 rate: f32,
90 frame_ms: Option<f32>,
93 pending: Option<Instant>,
94}
95
96#[cfg_attr(not(feature = "wgpu"), allow(dead_code))] impl Heavy {
98 pub(crate) fn new(config: HeavyEngine) -> Self {
99 let config = config.validated();
100 Self {
101 rate: config.max_hz,
102 config,
103 frame_ms: None,
104 pending: None,
105 }
106 }
107
108 #[cfg(test)]
109 pub(crate) fn config(&self) -> HeavyEngine {
110 self.config
111 }
112
113 pub(crate) fn on_present(&mut self, submitted: Instant) {
115 self.pending.get_or_insert(submitted);
116 }
117
118 pub(crate) fn observe(&mut self, reached: bool) {
120 if reached && let Some(submitted) = self.pending.take() {
121 let ms = submitted.elapsed().as_secs_f32() * 1000.;
122 self.frame_ms = Some(match self.frame_ms {
123 Some(previous) => previous * 0.7 + ms * 0.3,
124 None => ms,
125 });
126 }
127 }
128
129 pub(crate) fn pace(&mut self, stats: &SurfaceStats) -> Duration {
133 let lag = stats.composite_latency_ms;
134 let budget = self.config.lag_budget_ms;
135 if lag > budget {
136 self.rate = (self.rate * 0.5).max(self.config.min_hz);
137 } else if lag < budget * (2. / 3.) {
138 self.rate = (self.rate * 1.1).min(self.config.max_hz);
139 }
140 Duration::from_secs_f32(1. / self.rate)
141 }
142
143 #[cfg(test)]
144 pub(crate) fn rate_hz(&self) -> f32 {
145 self.rate
146 }
147
148 #[cfg(test)]
149 pub(crate) fn frame_ms(&self) -> Option<f32> {
150 self.frame_ms
151 }
152
153 pub(crate) fn suggested_chunks(&self) -> u32 {
155 self.frame_ms
156 .map(|ms| {
157 (ms / self.config.chunk_ms)
158 .ceil()
159 .clamp(1., self.config.max_chunks as f32) as u32
160 })
161 .unwrap_or(1)
162 }
163}
164
165#[derive(Clone, Copy, Debug, PartialEq)]
167pub struct HeavyStatus {
168 pub config: HeavyEngine,
169 pub rate_hz: f32,
171 pub frame_ms: Option<f32>,
173 pub suggested_chunks: u32,
175}
176
177#[cfg_attr(not(feature = "wgpu"), allow(dead_code))] impl Heavy {
179 pub(crate) fn status(&self) -> HeavyStatus {
180 HeavyStatus {
181 config: self.config,
182 rate_hz: self.rate,
183 frame_ms: self.frame_ms,
184 suggested_chunks: self.suggested_chunks(),
185 }
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 fn lagging(ms: f32) -> SurfaceStats {
194 SurfaceStats {
195 composite_latency_ms: ms,
196 ..Default::default()
197 }
198 }
199
200 #[test]
201 fn pace_backs_off_while_lagging_and_recovers() {
202 let mut heavy = Heavy::new(HeavyEngine::default());
203 assert_eq!(heavy.pace(&lagging(5.)), Duration::from_secs_f32(1. / 60.));
204 for _ in 0..20 {
205 heavy.pace(&lagging(200.));
206 }
207 assert_eq!(heavy.rate_hz(), 0.25, "floors at min_hz");
208 heavy.pace(&lagging(30.));
210 assert_eq!(heavy.rate_hz(), 0.25);
211 for _ in 0..200 {
212 heavy.pace(&lagging(5.));
213 }
214 assert_eq!(heavy.rate_hz(), 60., "recovers to max_hz");
215 }
216
217 #[test]
218 fn chunks_follow_measured_frame_time() {
219 let mut heavy = Heavy::new(HeavyEngine::default());
220 assert_eq!(heavy.suggested_chunks(), 1, "unmeasured");
221 heavy.on_present(Instant::now() - Duration::from_millis(40));
222 heavy.observe(false);
223 assert_eq!(heavy.frame_ms(), None, "not reached yet");
224 heavy.observe(true);
225 let ms = heavy.frame_ms().unwrap();
226 assert!(ms >= 40., "{ms}");
227 let cap = HeavyEngine::default().max_chunks;
228 assert_eq!(heavy.suggested_chunks(), ((ms / 4.).ceil() as u32).min(cap));
229 heavy.on_present(Instant::now() - Duration::from_secs(2));
231 heavy.observe(true);
232 assert_eq!(heavy.suggested_chunks(), cap);
233 }
234
235 #[test]
236 fn invalid_config_is_sanitised() {
237 let heavy = Heavy::new(HeavyEngine {
238 max_hz: f32::NAN,
239 min_hz: 500.,
240 lag_budget_ms: -1.,
241 chunk_ms: 0.,
242 max_chunks: 0,
243 });
244 let config = heavy.config();
245 assert_eq!(config.max_hz, 60.);
246 assert_eq!(config.min_hz, 60.);
247 assert_eq!(config.lag_budget_ms, 33.);
248 assert_eq!(config.chunk_ms, 4.);
249 assert_eq!(config.max_chunks, 1);
250 }
251}