1mod input;
38mod pacing;
39mod timing;
40
41use std::time::Instant;
42
43pub use input::{InputSnapshot, ScrollSample};
44use mulciber_platform::{InputEvent, WindowEvent};
45pub use pacing::{FramePacer, FrameSchedule, IntervalSummary, PacingDiagnostics, PacingReport};
46pub use timing::{FramePlan, RuntimeConfig, RuntimeConfigError};
47
48#[derive(Debug)]
50pub struct Runtime {
51 input: InputSnapshot,
52 clock: timing::FrameClock,
53 pacer: FramePacer,
54}
55
56#[derive(Debug)]
62#[must_use = "a runtime frame must be consumed by update/render work"]
63pub struct RuntimeFrame<'runtime> {
64 input: &'runtime mut InputSnapshot,
65 plan: FramePlan,
66 schedule: FrameSchedule,
67}
68
69impl RuntimeFrame<'_> {
70 #[must_use]
72 pub const fn plan(&self) -> FramePlan {
73 self.plan
74 }
75
76 pub const fn schedule(&self) -> FrameSchedule {
79 self.schedule
80 }
81
82 #[must_use]
84 pub const fn input(&self) -> &InputSnapshot {
85 self.input
86 }
87}
88
89impl Drop for RuntimeFrame<'_> {
90 fn drop(&mut self) {
91 if self.plan.fixed_steps() != 0 {
92 self.input.end_frame();
93 }
94 }
95}
96
97impl Runtime {
98 #[must_use]
100 pub fn new(config: RuntimeConfig, started_at: Instant) -> Self {
101 let mut pacer = FramePacer::new();
102 pacer.resume(started_at);
103 Self {
104 input: InputSnapshot::default(),
105 clock: timing::FrameClock::new(config),
106 pacer,
107 }
108 }
109
110 pub fn record_presented(&mut self, presented_at: Instant) {
117 self.pacer.record_presented(presented_at);
118 }
119
120 pub fn record_untimed_presented(&mut self) {
123 self.pacer.record_untimed_presented();
124 }
125
126 #[must_use]
128 pub fn pacing_report(&self) -> PacingReport {
129 self.pacer.report()
130 }
131
132 pub fn handle_input(&mut self, event: InputEvent) {
134 self.input.handle_event(event);
135 }
136
137 pub fn handle_window_event(&mut self, event: WindowEvent) {
142 match event {
143 WindowEvent::Input(input) => self.handle_input(input),
144 WindowEvent::RenderingSuspended => self.suspend(),
145 WindowEvent::RenderingResumed(_) => self.resume(Instant::now()),
146 _ => {}
147 }
148 }
149
150 #[must_use]
152 pub const fn input(&self) -> &InputSnapshot {
153 &self.input
154 }
155
156 pub fn begin_frame(&mut self, now: Instant) -> RuntimeFrame<'_> {
167 let (plan, schedule) = if self.clock.suspended() {
168 (self.clock.idle_plan(), FrameSchedule::idle())
169 } else {
170 let schedule = self.pacer.schedule(now);
171 (self.clock.advance_by(schedule.frame_delta()), schedule)
172 };
173 RuntimeFrame {
174 input: &mut self.input,
175 plan,
176 schedule,
177 }
178 }
179
180 pub fn suspend(&mut self) {
185 self.clock.suspend();
186 self.input.release_all();
187 }
188
189 pub fn resume(&mut self, now: Instant) {
191 self.clock.resume();
192 self.pacer.resume(now);
193 }
194
195 #[must_use]
197 pub const fn suspended(&self) -> bool {
198 self.clock.suspended()
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use std::time::{Duration, Instant};
205
206 use mulciber_platform::{ButtonState, InputEvent, KeyCode, Modifiers, WindowEvent};
207
208 use super::{Runtime, RuntimeConfig};
209
210 const STEP: Duration = Duration::from_micros(16_667);
211
212 fn runtime_after_steady_presents(count: u32) -> (Runtime, Instant) {
214 let mut at = Instant::now();
215 let mut runtime = Runtime::new(RuntimeConfig::fixed_hz(60).unwrap(), at);
216 for _ in 1..count {
217 runtime.record_presented(at);
218 at += STEP;
219 }
220 runtime.record_presented(at);
221 (runtime, at)
222 }
223
224 #[test]
225 fn recorded_feedback_paces_jittered_frame_starts_onto_the_display_cadence() {
226 let (mut runtime, mut presented) = runtime_after_steady_presents(30);
227 let mut now = presented + Duration::from_millis(1);
228 drop(runtime.begin_frame(now));
229 for jitter_ms in [3_u64, 18, 2, 17] {
230 now += Duration::from_millis(jitter_ms);
231 let frame = runtime.begin_frame(now);
232 assert!(frame.schedule().paced(), "jitter {jitter_ms} ms");
233 assert_eq!(frame.plan().frame_delta(), STEP, "jitter {jitter_ms} ms");
234 drop(frame);
235 presented += STEP;
236 runtime.record_presented(presented);
237 }
238 assert_eq!(runtime.pacing_report().estimated_cadence, Some(STEP));
239 }
240
241 #[test]
242 fn without_feedback_frames_observably_fall_back_to_wall_clock_gaps() {
243 let start = Instant::now();
244 let mut runtime = Runtime::new(RuntimeConfig::fixed_hz(10).unwrap(), start);
245 let frame = runtime.begin_frame(start + Duration::from_millis(40));
246 assert!(!frame.schedule().paced());
247 assert_eq!(frame.plan().fixed_steps(), 0);
248 assert!((frame.plan().interpolation() - 0.4).abs() < f64::EPSILON);
249 drop(frame);
250 let frame = runtime.begin_frame(start + Duration::from_millis(125));
251 assert!(!frame.schedule().paced());
252 assert_eq!(frame.plan().fixed_steps(), 1);
253 assert!((frame.plan().interpolation() - 0.25).abs() < f64::EPSILON);
254 }
255
256 #[test]
257 fn suspension_advances_nothing_and_resume_discards_the_suspended_interval() {
258 let (mut runtime, presented) = runtime_after_steady_presents(30);
259 let now = presented + Duration::from_millis(1);
260 drop(runtime.begin_frame(now));
261
262 runtime.suspend();
263 let idle = runtime.begin_frame(now + Duration::from_millis(120));
264 assert!(!idle.schedule().paced());
265 assert_eq!(idle.plan().frame_delta(), Duration::ZERO);
266 assert_eq!(idle.plan().fixed_steps(), 0);
267 drop(idle);
268
269 let resumed_at = now + Duration::from_millis(120);
272 runtime.record_presented(presented + Duration::from_millis(120));
273 runtime.resume(resumed_at);
274 let frame = runtime.begin_frame(resumed_at + Duration::from_millis(2));
275 assert!(frame.schedule().paced());
276 assert_eq!(frame.plan().frame_delta(), STEP);
277 }
278
279 #[test]
280 fn scoped_frame_cleanup_and_window_suspension_release_input() {
281 let started = Instant::now();
282 let mut runtime = Runtime::new(RuntimeConfig::fixed_hz(60).unwrap(), started);
283 runtime.handle_window_event(WindowEvent::Input(InputEvent::Keyboard {
284 key: KeyCode::KeyW,
285 state: ButtonState::Pressed,
286 repeat: false,
287 modifiers: Modifiers::default(),
288 }));
289 let frame = runtime.begin_frame(started + STEP);
290 assert_eq!(frame.plan().fixed_steps(), 1);
291 assert!(frame.input().key_pressed(KeyCode::KeyW));
292 drop(frame);
293 assert!(!runtime.input().key_pressed(KeyCode::KeyW));
294 assert!(runtime.input().key_held(KeyCode::KeyW));
295
296 runtime.handle_window_event(WindowEvent::RenderingSuspended);
297 assert!(runtime.suspended());
298 assert!(!runtime.input().key_held(KeyCode::KeyW));
299 assert!(runtime.input().key_released(KeyCode::KeyW));
300 }
301
302 #[test]
303 fn zero_step_frame_preserves_transitions_until_simulation_can_consume_them() {
304 const DISPLAY_75_HZ_FRAME: Duration = Duration::from_nanos(13_333_333);
305
306 let started = Instant::now();
307 let mut runtime = Runtime::new(RuntimeConfig::fixed_hz(60).unwrap(), started);
308 runtime.handle_input(InputEvent::Keyboard {
309 key: KeyCode::Space,
310 state: ButtonState::Pressed,
311 repeat: false,
312 modifiers: Modifiers::default(),
313 });
314
315 let render_only = runtime.begin_frame(started + DISPLAY_75_HZ_FRAME);
316 assert_eq!(render_only.plan().fixed_steps(), 0);
317 assert!(render_only.input().key_pressed(KeyCode::Space));
318 drop(render_only);
319 assert!(runtime.input().key_pressed(KeyCode::Space));
320
321 let simulation_frame = runtime.begin_frame(started + DISPLAY_75_HZ_FRAME * 2);
322 assert_eq!(simulation_frame.plan().fixed_steps(), 1);
323 assert!(simulation_frame.input().key_pressed(KeyCode::Space));
324 drop(simulation_frame);
325 assert!(!runtime.input().key_pressed(KeyCode::Space));
326 assert!(runtime.input().key_held(KeyCode::Space));
327 }
328}