1#![no_std]
29
30#[derive(Copy, Clone, Debug, PartialEq, Eq)]
32pub struct TickRate(u16);
33
34impl TickRate {
35 pub const HZ60: TickRate = TickRate(1);
37 pub const HZ30: TickRate = TickRate(2);
39 pub const HZ20: TickRate = TickRate(3);
41
42 pub const fn every_vblanks(vblanks: u16) -> TickRate {
44 TickRate(if vblanks == 0 { 1 } else { vblanks })
45 }
46
47 pub const fn vblanks(self) -> u16 {
49 self.0
50 }
51}
52
53#[derive(Copy, Clone, Debug, PartialEq, Eq)]
55pub enum CatchUp {
56 Unbounded,
58 Interleave(u16),
62 Cap(u16),
66}
67
68#[derive(Copy, Clone, Debug, PartialEq, Eq)]
70pub struct TickConfig {
71 pub rate: TickRate,
73 pub catch_up: CatchUp,
75}
76
77impl TickConfig {
78 pub const fn new(rate: TickRate) -> TickConfig {
80 TickConfig {
81 rate,
82 catch_up: CatchUp::Unbounded,
83 }
84 }
85
86 pub const fn with_interleave(self, max_ticks: u16) -> TickConfig {
88 TickConfig {
89 rate: self.rate,
90 catch_up: CatchUp::Interleave(if max_ticks == 0 { 1 } else { max_ticks }),
91 }
92 }
93
94 pub const fn with_cap(self, max_ticks: u16) -> TickConfig {
96 TickConfig {
97 rate: self.rate,
98 catch_up: CatchUp::Cap(if max_ticks == 0 { 1 } else { max_ticks }),
99 }
100 }
101}
102
103pub const TICK_HISTOGRAM_BUCKETS: usize = 9;
106
107#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
109pub struct TickStats {
110 pub frames: u32,
112 pub ticks: u32,
114 pub dropped: u32,
116 pub max_frame_ticks: u16,
118 pub ticks_per_frame: [u32; TICK_HISTOGRAM_BUCKETS],
121}
122
123#[derive(Copy, Clone, Debug, PartialEq, Eq)]
125pub struct FixedClock {
126 rate: u16,
127 cap: u16,
128 drop_excess: bool,
129 next_due: u32,
130 tick: u32,
131 frame_ticks: u16,
132 stats: TickStats,
133}
134
135#[inline(always)]
136fn reached(now: u32, target: u32) -> bool {
137 now.wrapping_sub(target) < 0x8000_0000
138}
139
140impl FixedClock {
141 pub const fn new(config: TickConfig, first_due: u32) -> FixedClock {
143 FixedClock {
144 rate: config.rate.vblanks(),
145 cap: match config.catch_up {
146 CatchUp::Unbounded => 0,
147 CatchUp::Interleave(n) | CatchUp::Cap(n) => n,
148 },
149 drop_excess: matches!(config.catch_up, CatchUp::Cap(_)),
150 next_due: first_due,
151 tick: 0,
152 frame_ticks: 0,
153 stats: TickStats {
154 frames: 0,
155 ticks: 0,
156 dropped: 0,
157 max_frame_ticks: 0,
158 ticks_per_frame: [0; TICK_HISTOGRAM_BUCKETS],
159 },
160 }
161 }
162
163 pub fn realign(&mut self, first_due: u32) {
167 self.next_due = first_due;
168 }
169
170 pub fn due(&mut self, now: u32) -> bool {
176 if !reached(now, self.next_due) {
177 return false;
178 }
179 if self.cap != 0 && self.frame_ticks >= self.cap {
180 if !self.drop_excess {
181 return false;
182 }
183 let behind = now.wrapping_sub(self.next_due) / u32::from(self.rate) + 1;
184 self.stats.dropped = self.stats.dropped.saturating_add(behind);
185 self.next_due = self
186 .next_due
187 .wrapping_add(behind.wrapping_mul(u32::from(self.rate)));
188 return false;
189 }
190 self.consume();
191 true
192 }
193
194 pub fn is_due(&self, now: u32) -> bool {
199 reached(now, self.next_due)
200 }
201
202 pub fn consume(&mut self) {
205 self.next_due = self.next_due.wrapping_add(u32::from(self.rate));
206 self.tick = self.tick.wrapping_add(1);
207 self.frame_ticks = self.frame_ticks.saturating_add(1);
208 self.stats.ticks = self.stats.ticks.saturating_add(1);
209 }
210
211 pub fn end_frame(&mut self) {
214 let k = usize::from(self.frame_ticks).min(TICK_HISTOGRAM_BUCKETS - 1);
215 self.stats.ticks_per_frame[k] = self.stats.ticks_per_frame[k].saturating_add(1);
216 self.stats.frames = self.stats.frames.saturating_add(1);
217 self.stats.max_frame_ticks = self.stats.max_frame_ticks.max(self.frame_ticks);
218 self.frame_ticks = 0;
219 }
220
221 pub const fn tick(&self) -> u32 {
223 self.tick
224 }
225
226 pub const fn frame_ticks(&self) -> u16 {
228 self.frame_ticks
229 }
230
231 pub const fn rate(&self) -> TickRate {
233 TickRate(self.rate)
234 }
235
236 pub const fn next_due(&self) -> u32 {
238 self.next_due
239 }
240
241 pub fn phase_q12(&self, now: u32) -> u16 {
248 let ahead = self.next_due.wrapping_sub(now) as i32;
249 let rate = i32::from(self.rate);
250 if ahead <= 0 {
251 4096
252 } else {
253 (((rate - ahead.min(rate)) << 12) / rate) as u16
254 }
255 }
256
257 pub const fn stats(&self) -> TickStats {
259 self.stats
260 }
261
262 pub fn reset_stats(&mut self) {
264 self.stats = TickStats::default();
265 }
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271
272 fn run(clock: &mut FixedClock, now: u32) -> u32 {
273 let mut n = 0;
274 while clock.due(now) {
275 n += 1;
276 }
277 clock.end_frame();
278 n
279 }
280
281 #[test]
282 fn sixty_hz_runs_one_tick_per_vblank_and_catches_up_a_slow_frame() {
283 let mut c = FixedClock::new(TickConfig::new(TickRate::HZ60), 10);
284 assert_eq!(run(&mut c, 9), 0);
285 assert_eq!(run(&mut c, 10), 1);
286 assert_eq!(run(&mut c, 11), 1);
287 assert_eq!(run(&mut c, 15), 4);
288 assert_eq!(c.tick(), 6);
289 assert_eq!(c.stats().ticks_per_frame[..5], [1, 2, 0, 0, 1]);
290 assert_eq!(c.stats().max_frame_ticks, 4);
291 assert_eq!(c.stats().dropped, 0);
292 }
293
294 #[test]
295 fn twenty_hz_ticks_every_third_vblank() {
296 let mut c = FixedClock::new(TickConfig::new(TickRate::HZ20), 3);
297 let ticks: u32 = (0..=30).map(|now| run(&mut c, now)).sum();
298 assert_eq!(ticks, 10); assert_eq!(c.next_due(), 33);
300 }
301
302 #[test]
303 fn logic_time_matches_real_time_whatever_the_frame_lengths() {
304 let mut c = FixedClock::new(TickConfig::new(TickRate::HZ30), 0);
307 let mut now = 0u32;
308 let mut k = 0u32;
309 while now < 1000 {
310 run(&mut c, now);
311 now += 1 + (k % 7);
312 k += 1;
313 }
314 run(&mut c, 1000);
315 assert_eq!(c.tick(), 501); assert_eq!(c.stats().dropped, 0);
317 }
318
319 #[test]
320 fn cap_drops_the_excess_and_counts_it() {
321 let mut c = FixedClock::new(TickConfig::new(TickRate::HZ60).with_cap(4), 0);
322 assert_eq!(run(&mut c, 0), 1);
323 assert_eq!(run(&mut c, 10), 4);
325 assert_eq!(c.stats().dropped, 6);
326 assert_eq!(c.next_due(), 11);
327 assert_eq!(run(&mut c, 11), 1);
328 }
329
330 #[test]
331 fn interleave_renders_between_bursts_and_loses_no_time() {
332 let mut c = FixedClock::new(TickConfig::new(TickRate::HZ60).with_interleave(4), 0);
333 assert_eq!(run(&mut c, 0), 1);
334 assert_eq!(run(&mut c, 10), 4); assert_eq!(run(&mut c, 10), 4); assert_eq!(run(&mut c, 11), 3); assert_eq!(c.tick(), 12);
338 assert_eq!(c.stats().dropped, 0);
339 }
340
341 #[test]
342 fn is_due_and_consume_split_the_decision_from_the_commit() {
343 let mut c = FixedClock::new(TickConfig::new(TickRate::HZ60), 5);
344 assert!(!c.is_due(4));
345 assert!(c.is_due(5));
346 assert!(c.is_due(5)); c.consume();
348 assert!(!c.is_due(5));
349 assert_eq!((c.tick(), c.frame_ticks(), c.next_due()), (1, 1, 6));
350 }
351
352 #[test]
353 fn realign_forgets_debt_without_counting_it_dropped() {
354 let mut c = FixedClock::new(TickConfig::new(TickRate::HZ60), 0);
355 run(&mut c, 0);
356 c.realign(501); assert_eq!(run(&mut c, 501), 1);
358 assert_eq!(c.stats().dropped, 0);
359 }
360
361 #[test]
362 fn phase_sweeps_from_last_tick_to_next() {
363 let mut c = FixedClock::new(TickConfig::new(TickRate::HZ20), 0);
364 run(&mut c, 0); assert_eq!(c.phase_q12(0), 0);
366 assert_eq!(c.phase_q12(1), 1365);
367 assert_eq!(c.phase_q12(2), 2730);
368 assert_eq!(c.phase_q12(3), 4096);
369 let mut s = FixedClock::new(TickConfig::new(TickRate::HZ60), 0);
370 run(&mut s, 0);
371 assert_eq!(s.phase_q12(0), 0);
372 assert_eq!(s.phase_q12(1), 4096);
373 }
374
375 #[test]
376 fn survives_vblank_counter_wraparound() {
377 let start = u32::MAX - 2;
378 let mut c = FixedClock::new(TickConfig::new(TickRate::HZ60), start);
379 assert_eq!(run(&mut c, start.wrapping_add(5)), 6);
380 assert_eq!(c.next_due(), start.wrapping_add(6));
381 }
382}