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)]
61#[must_use = "a runtime frame must be consumed by update/render work"]
62pub struct RuntimeFrame<'runtime> {
63 input: &'runtime mut InputSnapshot,
64 plan: FramePlan,
65 schedule: FrameSchedule,
66}
67
68impl RuntimeFrame<'_> {
69 #[must_use]
71 pub const fn plan(&self) -> FramePlan {
72 self.plan
73 }
74
75 pub const fn schedule(&self) -> FrameSchedule {
78 self.schedule
79 }
80
81 #[must_use]
83 pub const fn input(&self) -> &InputSnapshot {
84 self.input
85 }
86}
87
88impl Drop for RuntimeFrame<'_> {
89 fn drop(&mut self) {
90 self.input.end_frame();
91 }
92}
93
94impl Runtime {
95 #[must_use]
97 pub fn new(config: RuntimeConfig, started_at: Instant) -> Self {
98 let mut pacer = FramePacer::new();
99 pacer.resume(started_at);
100 Self {
101 input: InputSnapshot::default(),
102 clock: timing::FrameClock::new(config),
103 pacer,
104 }
105 }
106
107 pub fn record_presented(&mut self, presented_at: Instant) {
114 self.pacer.record_presented(presented_at);
115 }
116
117 pub fn record_untimed_presented(&mut self) {
120 self.pacer.record_untimed_presented();
121 }
122
123 #[must_use]
125 pub fn pacing_report(&self) -> PacingReport {
126 self.pacer.report()
127 }
128
129 pub fn handle_input(&mut self, event: InputEvent) {
131 self.input.handle_event(event);
132 }
133
134 pub fn handle_window_event(&mut self, event: WindowEvent) {
139 match event {
140 WindowEvent::Input(input) => self.handle_input(input),
141 WindowEvent::RenderingSuspended => self.suspend(),
142 WindowEvent::RenderingResumed(_) => self.resume(Instant::now()),
143 _ => {}
144 }
145 }
146
147 #[must_use]
149 pub const fn input(&self) -> &InputSnapshot {
150 &self.input
151 }
152
153 pub fn begin_frame(&mut self, now: Instant) -> RuntimeFrame<'_> {
163 let (plan, schedule) = if self.clock.suspended() {
164 (self.clock.idle_plan(), FrameSchedule::idle())
165 } else {
166 let schedule = self.pacer.schedule(now);
167 (self.clock.advance_by(schedule.frame_delta()), schedule)
168 };
169 RuntimeFrame {
170 input: &mut self.input,
171 plan,
172 schedule,
173 }
174 }
175
176 pub fn suspend(&mut self) {
181 self.clock.suspend();
182 self.input.release_all();
183 }
184
185 pub fn resume(&mut self, now: Instant) {
187 self.clock.resume();
188 self.pacer.resume(now);
189 }
190
191 #[must_use]
193 pub const fn suspended(&self) -> bool {
194 self.clock.suspended()
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use std::time::{Duration, Instant};
201
202 use mulciber_platform::{ButtonState, InputEvent, KeyCode, Modifiers, WindowEvent};
203
204 use super::{Runtime, RuntimeConfig};
205
206 const STEP: Duration = Duration::from_micros(16_667);
207
208 fn runtime_after_steady_presents(count: u32) -> (Runtime, Instant) {
210 let mut at = Instant::now();
211 let mut runtime = Runtime::new(RuntimeConfig::fixed_hz(60).unwrap(), at);
212 for _ in 1..count {
213 runtime.record_presented(at);
214 at += STEP;
215 }
216 runtime.record_presented(at);
217 (runtime, at)
218 }
219
220 #[test]
221 fn recorded_feedback_paces_jittered_frame_starts_onto_the_display_cadence() {
222 let (mut runtime, mut presented) = runtime_after_steady_presents(30);
223 let mut now = presented + Duration::from_millis(1);
224 drop(runtime.begin_frame(now));
225 for jitter_ms in [3_u64, 18, 2, 17] {
226 now += Duration::from_millis(jitter_ms);
227 let frame = runtime.begin_frame(now);
228 assert!(frame.schedule().paced(), "jitter {jitter_ms} ms");
229 assert_eq!(frame.plan().frame_delta(), STEP, "jitter {jitter_ms} ms");
230 drop(frame);
231 presented += STEP;
232 runtime.record_presented(presented);
233 }
234 assert_eq!(runtime.pacing_report().estimated_cadence, Some(STEP));
235 }
236
237 #[test]
238 fn without_feedback_frames_observably_fall_back_to_wall_clock_gaps() {
239 let start = Instant::now();
240 let mut runtime = Runtime::new(RuntimeConfig::fixed_hz(10).unwrap(), start);
241 let frame = runtime.begin_frame(start + Duration::from_millis(40));
242 assert!(!frame.schedule().paced());
243 assert_eq!(frame.plan().fixed_steps(), 0);
244 assert!((frame.plan().interpolation() - 0.4).abs() < f64::EPSILON);
245 drop(frame);
246 let frame = runtime.begin_frame(start + Duration::from_millis(125));
247 assert!(!frame.schedule().paced());
248 assert_eq!(frame.plan().fixed_steps(), 1);
249 assert!((frame.plan().interpolation() - 0.25).abs() < f64::EPSILON);
250 }
251
252 #[test]
253 fn suspension_advances_nothing_and_resume_discards_the_suspended_interval() {
254 let (mut runtime, presented) = runtime_after_steady_presents(30);
255 let now = presented + Duration::from_millis(1);
256 drop(runtime.begin_frame(now));
257
258 runtime.suspend();
259 let idle = runtime.begin_frame(now + Duration::from_millis(120));
260 assert!(!idle.schedule().paced());
261 assert_eq!(idle.plan().frame_delta(), Duration::ZERO);
262 assert_eq!(idle.plan().fixed_steps(), 0);
263 drop(idle);
264
265 let resumed_at = now + Duration::from_millis(120);
268 runtime.record_presented(presented + Duration::from_millis(120));
269 runtime.resume(resumed_at);
270 let frame = runtime.begin_frame(resumed_at + Duration::from_millis(2));
271 assert!(frame.schedule().paced());
272 assert_eq!(frame.plan().frame_delta(), STEP);
273 }
274
275 #[test]
276 fn scoped_frame_cleanup_and_window_suspension_release_input() {
277 let mut runtime = Runtime::new(RuntimeConfig::fixed_hz(60).unwrap(), Instant::now());
278 runtime.handle_window_event(WindowEvent::Input(InputEvent::Keyboard {
279 key: KeyCode::KeyW,
280 state: ButtonState::Pressed,
281 repeat: false,
282 modifiers: Modifiers::default(),
283 }));
284 let frame = runtime.begin_frame(Instant::now());
285 assert!(frame.input().key_pressed(KeyCode::KeyW));
286 drop(frame);
287 assert!(!runtime.input().key_pressed(KeyCode::KeyW));
288 assert!(runtime.input().key_held(KeyCode::KeyW));
289
290 runtime.handle_window_event(WindowEvent::RenderingSuspended);
291 assert!(runtime.suspended());
292 assert!(!runtime.input().key_held(KeyCode::KeyW));
293 assert!(runtime.input().key_released(KeyCode::KeyW));
294 }
295}