1use crate::time::{Nanos, StepBudget, Timestamp, Timestep};
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20pub struct FrameLoop {
21 timestep: Timestep,
22 budget: StepBudget,
23 last: Timestamp,
25 bank: Nanos,
28 tick: u64,
31}
32
33impl FrameLoop {
34 #[must_use]
41 pub const fn new(timestep: Timestep, budget: StepBudget, start: Timestamp) -> Self {
42 Self {
43 timestep,
44 budget,
45 last: start,
46 bank: Nanos::ZERO,
47 tick: 0,
48 }
49 }
50
51 pub fn begin_frame(&mut self, now: Timestamp) -> FramePlan {
58 let dt = self.timestep.nanos().get();
59 let bank = self
60 .bank
61 .get()
62 .saturating_add(now.saturating_since(self.last).get());
63 let due = bank / dt;
64 let steps = u32::try_from(due)
68 .unwrap_or(u32::MAX)
69 .min(self.budget.get().get());
70 let run = u64::from(steps);
71 let remainder = Nanos::from_nanos(bank % dt);
80 let plan = FramePlan {
82 first_tick: self.tick,
83 steps,
84 dropped: due - run,
85 remainder,
86 dt: self.timestep,
87 };
88 self.bank = remainder;
89 self.tick = self.tick.saturating_add(run);
90 self.last = now;
91 plan
92 }
93
94 pub fn resync(&mut self, now: Timestamp) {
102 self.last = now;
103 }
104
105 #[must_use]
107 pub const fn tick(&self) -> u64 {
108 self.tick
109 }
110
111 #[must_use]
114 pub const fn simulated(&self) -> Nanos {
115 Nanos::from_nanos(self.tick.saturating_mul(self.timestep.nanos().get()))
116 }
117
118 #[must_use]
121 pub const fn remainder(&self) -> Nanos {
122 self.bank
123 }
124
125 #[must_use]
126 pub const fn timestep(&self) -> Timestep {
127 self.timestep
128 }
129
130 #[must_use]
131 pub const fn budget(&self) -> StepBudget {
132 self.budget
133 }
134}
135
136#[must_use = "a frame plan's steps must be executed and its alpha rendered"]
142#[derive(Clone, Copy, Debug, PartialEq, Eq)]
143pub struct FramePlan {
144 first_tick: u64,
145 steps: u32,
146 dropped: u64,
147 remainder: Nanos,
148 dt: Timestep,
149}
150
151impl FramePlan {
152 #[must_use]
155 pub const fn steps(&self) -> Steps {
156 Steps {
157 next: self.first_tick,
158 remaining: self.steps,
159 dt: self.dt,
160 }
161 }
162
163 #[must_use]
164 pub const fn step_count(&self) -> u32 {
165 self.steps
166 }
167
168 #[must_use]
175 pub const fn dt(&self) -> Timestep {
176 self.dt
177 }
178
179 #[must_use]
182 pub const fn first_tick(&self) -> u64 {
183 self.first_tick
184 }
185
186 #[must_use]
189 pub const fn dropped(&self) -> u64 {
190 self.dropped
191 }
192
193 #[must_use]
206 pub const fn remainder(&self) -> Nanos {
207 self.remainder
208 }
209
210 #[must_use]
211 pub const fn timestep(&self) -> Timestep {
212 self.dt
213 }
214}
215
216#[derive(Clone, Copy, Debug, PartialEq, Eq)]
218pub struct Step {
219 pub tick: u64,
222 pub dt: Nanos,
225 pub sim_time: Nanos,
229}
230
231#[derive(Clone, Debug)]
237pub struct Steps {
238 next: u64,
239 remaining: u32,
240 dt: Timestep,
241}
242
243impl Iterator for Steps {
244 type Item = Step;
245
246 fn next(&mut self) -> Option<Step> {
247 if self.remaining == 0 {
248 return None;
249 }
250 self.remaining -= 1;
251 let tick = self.next;
252 self.next = tick.saturating_add(1);
253 let dt = self.dt.nanos().get();
254 Some(Step {
255 tick,
256 dt: Nanos::from_nanos(dt),
257 sim_time: Nanos::from_nanos(tick.saturating_mul(dt)),
258 })
259 }
260
261 fn size_hint(&self) -> (usize, Option<usize>) {
262 let remaining = usize::try_from(self.remaining).unwrap_or(usize::MAX);
263 (remaining, Some(remaining))
264 }
265}
266
267impl ExactSizeIterator for Steps {}
268
269impl core::iter::FusedIterator for Steps {}
270
271#[cfg(test)]
272mod tests {
273 use super::{FrameLoop, StepBudget, Timestamp, Timestep};
274 use crate::time::Nanos;
275 use core::num::{NonZeroU32, NonZeroU64};
276
277 const DT: u64 = 16_666_667;
280
281 fn at(nanos: u64) -> Timestamp {
282 Timestamp::from_nanos(nanos)
283 }
284
285 fn timestep(nanos: u64) -> Timestep {
286 Timestep::from_nanos(NonZeroU64::new(nanos).expect("non-zero"))
287 }
288
289 fn budget(steps: u32) -> StepBudget {
290 StepBudget::new(NonZeroU32::new(steps).expect("non-zero"))
291 }
292
293 fn loop_at_60hz() -> FrameLoop {
294 FrameLoop::new(Timestep::HZ_60, StepBudget::DEFAULT, at(0))
295 }
296
297 #[test]
298 fn a_fresh_loop_reports_its_configuration_and_an_empty_schedule() {
299 let frame = loop_at_60hz();
300 assert_eq!(frame.timestep(), Timestep::HZ_60);
301 assert_eq!(frame.budget(), StepBudget::DEFAULT);
302 assert_eq!(frame.tick(), 0);
303 assert_eq!(frame.remainder(), Nanos::ZERO);
304 assert_eq!(frame.simulated(), Nanos::ZERO);
305 }
306
307 #[test]
308 fn no_elapsed_time_yields_no_steps() {
309 let mut frame = loop_at_60hz();
310 let plan = frame.begin_frame(at(0));
311 assert_eq!(plan.step_count(), 0);
312 assert_eq!(plan.dropped(), 0);
313 assert_eq!(plan.first_tick(), 0);
314 assert_eq!(plan.remainder(), Nanos::ZERO);
315 assert_eq!(plan.timestep(), Timestep::HZ_60);
316 assert_eq!(plan.steps().count(), 0);
317 }
318
319 #[test]
320 fn exactly_one_timestep_yields_one_step_and_an_empty_bank() {
321 let mut frame = loop_at_60hz();
322 let plan = frame.begin_frame(at(DT));
323 assert_eq!(plan.step_count(), 1);
324 assert_eq!(plan.remainder(), Nanos::ZERO);
325 assert_eq!(frame.tick(), 1);
326 assert_eq!(frame.simulated(), Nanos::from_nanos(DT));
327 }
328
329 #[test]
330 fn the_remainder_carries_across_frames() {
331 let mut frame = loop_at_60hz();
332 let first = frame.begin_frame(at(DT + 5));
333 assert_eq!(first.step_count(), 1);
334 assert_eq!(first.remainder(), Nanos::from_nanos(5));
335 let second = frame.begin_frame(at(DT + 5 + DT - 5));
338 assert_eq!(second.step_count(), 1);
339 assert_eq!(second.remainder(), Nanos::ZERO);
340 assert_eq!(frame.tick(), 2);
341 }
342
343 #[test]
344 fn sub_timestep_frames_bank_until_a_step_is_due() {
345 let mut frame = loop_at_60hz();
346 let half = DT / 2;
347 assert_eq!(frame.begin_frame(at(half)).step_count(), 0);
348 assert_eq!(frame.remainder(), Nanos::from_nanos(half));
349 assert_eq!(frame.begin_frame(at(2 * half)).step_count(), 0);
351 assert_eq!(frame.remainder(), Nanos::from_nanos(DT - 1));
352 assert_eq!(frame.begin_frame(at(2 * half + 1)).step_count(), 1);
353 assert_eq!(frame.remainder(), Nanos::ZERO);
354 }
355
356 #[test]
357 fn several_whole_timesteps_run_in_one_frame_while_the_budget_allows() {
358 let mut frame = loop_at_60hz();
359 let plan = frame.begin_frame(at(3 * DT + 7));
360 assert_eq!(plan.step_count(), 3);
361 assert_eq!(plan.dropped(), 0);
362 assert_eq!(plan.remainder(), Nanos::from_nanos(7));
363 }
364
365 #[test]
368 fn a_stall_is_clamped_and_the_refused_steps_are_reported() {
369 let mut frame = loop_at_60hz();
370 let plan = frame.begin_frame(at(200_000_000));
371 assert_eq!(plan.step_count(), 5);
372 assert_eq!(plan.dropped(), 200_000_000 / DT - 5);
373 assert_eq!(plan.remainder(), Nanos::from_nanos(200_000_000 % DT));
377 let recovered = frame.begin_frame(at(200_000_000 + DT));
378 assert_eq!(recovered.step_count(), 1);
379 assert_eq!(recovered.dropped(), 0);
380 }
381
382 #[test]
383 fn a_saturated_bank_drops_billions_of_steps_without_wrapping() {
384 let mut frame = loop_at_60hz();
385 let plan = frame.begin_frame(at(u64::MAX));
386 assert_eq!(plan.step_count(), 5);
387 assert_eq!(plan.dropped(), u64::MAX / DT - 5);
388 assert_eq!(plan.remainder(), Nanos::from_nanos(u64::MAX % DT));
389 assert_eq!(frame.tick(), 5);
390 }
391
392 #[test]
395 fn the_refused_count_exceeds_the_thirty_two_bit_range() {
396 let mut frame = FrameLoop::new(timestep(1), budget(1), at(0));
397 let plan = frame.begin_frame(at(u64::MAX));
398 assert_eq!(plan.step_count(), 1);
399 assert_eq!(plan.dropped(), u64::MAX - 1);
400 assert!(plan.dropped() > u64::from(u32::MAX));
401 }
402
403 #[test]
404 fn a_backwards_clock_advances_nothing_and_leaves_the_bank_alone() {
405 let mut frame = loop_at_60hz();
406 let _ = frame.begin_frame(at(DT + 11));
407 let backwards = frame.begin_frame(at(1));
408 assert_eq!(backwards.step_count(), 0);
409 assert_eq!(backwards.dropped(), 0);
410 assert_eq!(backwards.remainder(), Nanos::from_nanos(11));
411 assert_eq!(frame.tick(), 1);
412 assert_eq!(frame.begin_frame(at(1 + DT)).step_count(), 1);
416 }
417
418 #[test]
419 fn resync_discards_the_gap_but_keeps_the_tick_and_the_remainder() {
420 let mut frame = loop_at_60hz();
421 let _ = frame.begin_frame(at(DT + 11));
422 assert_eq!(frame.tick(), 1);
423 frame.resync(at(10_000_000_000));
425 assert_eq!(frame.tick(), 1);
426 assert_eq!(frame.remainder(), Nanos::from_nanos(11));
427 let plan = frame.begin_frame(at(10_000_000_000 + DT - 11));
428 assert_eq!(plan.step_count(), 1, "the pause was not banked");
429 assert_eq!(plan.dropped(), 0);
430 assert_eq!(plan.remainder(), Nanos::ZERO);
431 }
432
433 #[test]
434 fn the_simulated_clock_is_tick_times_timestep_and_saturates() {
435 let half = u64::MAX / 2;
436 let mut frame = FrameLoop::new(timestep(half), budget(2), at(0));
437 let _ = frame.begin_frame(at(u64::MAX));
438 assert_eq!(frame.tick(), 2);
439 assert_eq!(frame.simulated(), Nanos::from_nanos(2 * half));
440
441 let _ = frame.begin_frame(at(0));
446 let plan = frame.begin_frame(at(u64::MAX));
447 assert_eq!(plan.step_count(), 2);
448 assert_eq!(frame.tick(), 4);
449 assert_eq!(frame.simulated(), Nanos::from_nanos(u64::MAX));
450 let last = plan.steps().last().expect("two steps");
451 assert_eq!(last.tick, 3);
452 assert_eq!(last.sim_time, Nanos::from_nanos(u64::MAX));
453 }
454
455 #[test]
458 fn a_due_count_beyond_the_thirty_two_bit_range_is_still_budget_bounded() {
459 let mut frame = FrameLoop::new(timestep(1), budget(u32::MAX), at(0));
460 let plan = frame.begin_frame(at(u64::MAX));
461 assert_eq!(plan.step_count(), u32::MAX);
462 assert_eq!(plan.dropped(), u64::MAX - u64::from(u32::MAX));
463 assert_eq!(frame.tick(), u64::from(u32::MAX));
464 }
465
466 #[test]
467 fn the_steps_of_a_plan_are_consecutive_ticks_with_exact_simulation_times() {
468 let mut frame = loop_at_60hz();
469 let _ = frame.begin_frame(at(DT));
470 let plan = frame.begin_frame(at(4 * DT));
471 assert_eq!(plan.first_tick(), 1);
472 let steps: Vec<_> = plan.steps().collect();
473 assert_eq!(steps.len(), 3);
474 for (offset, step) in steps.iter().enumerate() {
475 let tick = 1 + offset as u64;
476 assert_eq!(step.tick, tick);
477 assert_eq!(step.dt, Nanos::from_nanos(DT));
478 assert_eq!(step.sim_time, Nanos::from_nanos(tick * DT));
479 }
480 assert_eq!(frame.simulated(), Nanos::from_nanos(4 * DT));
483 }
484
485 #[test]
486 fn the_step_iterator_reports_its_exact_length_and_then_stays_empty() {
487 let mut frame = loop_at_60hz();
488 let plan = frame.begin_frame(at(2 * DT));
489 let mut steps = plan.steps();
490 assert_eq!(steps.len(), 2);
491 assert_eq!(steps.size_hint(), (2, Some(2)));
492 assert!(steps.next().is_some());
493 assert_eq!(steps.len(), 1);
494 assert!(steps.next().is_some());
495 assert_eq!(steps.len(), 0);
496 assert!(steps.next().is_none());
497 assert!(steps.next().is_none(), "fused");
498 assert_eq!(plan.steps().count(), 2);
500 }
501
502 #[test]
512 fn the_remainder_is_the_exact_position_between_two_steps() {
513 let mut frame = loop_at_60hz();
514
515 let plan = frame.begin_frame(at(DT));
517 assert_eq!(plan.remainder(), Nanos::from_nanos(0));
518 assert_eq!(plan.timestep().nanos().get(), DT);
519
520 let plan = frame.begin_frame(at(DT + DT / 2));
523 assert_eq!(plan.remainder(), Nanos::from_nanos(DT / 2));
524
525 for dt in [
529 16_666_667_u64,
530 4_166_667,
531 8_000_000,
532 33_333_333,
533 1_000_000_000,
534 ] {
535 let mut frame = FrameLoop::new(timestep(dt), budget(1), at(0));
536 let plan = frame.begin_frame(at(dt - 1));
537 assert_eq!(plan.remainder(), Nanos::from_nanos(dt - 1));
538 assert!(
539 plan.remainder().get() < plan.timestep().nanos().get(),
540 "the remainder must stay a proper fraction of the step"
541 );
542 }
543 }
544
545 #[test]
558 fn the_absorbed_accumulator_reproduces_hello_engines_committed_output() {
559 let pattern = [15_000_000_u64, 16_666_667, 18_000_000, 33_333_334];
560 let mut frame = loop_at_60hz();
561 let mut now = 0u64;
562 let mut dropped = 0u64;
563 for delta in pattern.iter().copied().cycle().take(60) {
564 now += delta;
565 dropped += frame.begin_frame(at(now)).dropped();
566 }
567 assert_eq!(now, 1_245_000_015, "time submitted");
568 assert_eq!(frame.tick(), 74, "ticks executed");
569 assert_eq!(
570 frame.remainder(),
571 Nanos::from_nanos(11_666_657),
572 "time pending"
573 );
574 assert_eq!(dropped, 0, "the pattern never reaches the budget");
575 assert_eq!(frame.simulated().get() + frame.remainder().get(), now);
578 }
579}