mod input;
mod pacing;
mod timing;
use std::time::Instant;
pub use input::{InputSnapshot, ScrollSample};
use mulciber_platform::{InputEvent, WindowEvent};
pub use pacing::{FramePacer, FrameSchedule, IntervalSummary, PacingDiagnostics, PacingReport};
pub use timing::{FramePlan, RuntimeConfig, RuntimeConfigError};
#[derive(Debug)]
pub struct Runtime {
input: InputSnapshot,
clock: timing::FrameClock,
pacer: FramePacer,
}
#[derive(Debug)]
#[must_use = "a runtime frame must be consumed by update/render work"]
pub struct RuntimeFrame<'runtime> {
input: &'runtime mut InputSnapshot,
plan: FramePlan,
schedule: FrameSchedule,
}
impl RuntimeFrame<'_> {
#[must_use]
pub const fn plan(&self) -> FramePlan {
self.plan
}
pub const fn schedule(&self) -> FrameSchedule {
self.schedule
}
#[must_use]
pub const fn input(&self) -> &InputSnapshot {
self.input
}
}
impl Drop for RuntimeFrame<'_> {
fn drop(&mut self) {
if self.plan.fixed_steps() != 0 {
self.input.end_frame();
}
}
}
impl Runtime {
#[must_use]
pub fn new(config: RuntimeConfig, started_at: Instant) -> Self {
let mut pacer = FramePacer::new();
pacer.resume(started_at);
Self {
input: InputSnapshot::default(),
clock: timing::FrameClock::new(config),
pacer,
}
}
pub fn record_presented(&mut self, presented_at: Instant) {
self.pacer.record_presented(presented_at);
}
pub fn record_untimed_presented(&mut self) {
self.pacer.record_untimed_presented();
}
#[must_use]
pub fn pacing_report(&self) -> PacingReport {
self.pacer.report()
}
pub fn handle_input(&mut self, event: InputEvent) {
self.input.handle_event(event);
}
pub fn handle_window_event(&mut self, event: WindowEvent) {
match event {
WindowEvent::Input(input) => self.handle_input(input),
WindowEvent::RenderingSuspended => self.suspend(),
WindowEvent::RenderingResumed(_) => self.resume(Instant::now()),
_ => {}
}
}
#[must_use]
pub const fn input(&self) -> &InputSnapshot {
&self.input
}
pub fn begin_frame(&mut self, now: Instant) -> RuntimeFrame<'_> {
let (plan, schedule) = if self.clock.suspended() {
(self.clock.idle_plan(), FrameSchedule::idle())
} else {
let schedule = self.pacer.schedule(now);
(self.clock.advance_by(schedule.frame_delta()), schedule)
};
RuntimeFrame {
input: &mut self.input,
plan,
schedule,
}
}
pub fn suspend(&mut self) {
self.clock.suspend();
self.input.release_all();
}
pub fn resume(&mut self, now: Instant) {
self.clock.resume();
self.pacer.resume(now);
}
#[must_use]
pub const fn suspended(&self) -> bool {
self.clock.suspended()
}
}
#[cfg(test)]
mod tests {
use std::time::{Duration, Instant};
use mulciber_platform::{ButtonState, InputEvent, KeyCode, Modifiers, WindowEvent};
use super::{Runtime, RuntimeConfig};
const STEP: Duration = Duration::from_micros(16_667);
fn runtime_after_steady_presents(count: u32) -> (Runtime, Instant) {
let mut at = Instant::now();
let mut runtime = Runtime::new(RuntimeConfig::fixed_hz(60).unwrap(), at);
for _ in 1..count {
runtime.record_presented(at);
at += STEP;
}
runtime.record_presented(at);
(runtime, at)
}
#[test]
fn recorded_feedback_paces_jittered_frame_starts_onto_the_display_cadence() {
let (mut runtime, mut presented) = runtime_after_steady_presents(30);
let mut now = presented + Duration::from_millis(1);
drop(runtime.begin_frame(now));
for jitter_ms in [3_u64, 18, 2, 17] {
now += Duration::from_millis(jitter_ms);
let frame = runtime.begin_frame(now);
assert!(frame.schedule().paced(), "jitter {jitter_ms} ms");
assert_eq!(frame.plan().frame_delta(), STEP, "jitter {jitter_ms} ms");
drop(frame);
presented += STEP;
runtime.record_presented(presented);
}
assert_eq!(runtime.pacing_report().estimated_cadence, Some(STEP));
}
#[test]
fn without_feedback_frames_observably_fall_back_to_wall_clock_gaps() {
let start = Instant::now();
let mut runtime = Runtime::new(RuntimeConfig::fixed_hz(10).unwrap(), start);
let frame = runtime.begin_frame(start + Duration::from_millis(40));
assert!(!frame.schedule().paced());
assert_eq!(frame.plan().fixed_steps(), 0);
assert!((frame.plan().interpolation() - 0.4).abs() < f64::EPSILON);
drop(frame);
let frame = runtime.begin_frame(start + Duration::from_millis(125));
assert!(!frame.schedule().paced());
assert_eq!(frame.plan().fixed_steps(), 1);
assert!((frame.plan().interpolation() - 0.25).abs() < f64::EPSILON);
}
#[test]
fn suspension_advances_nothing_and_resume_discards_the_suspended_interval() {
let (mut runtime, presented) = runtime_after_steady_presents(30);
let now = presented + Duration::from_millis(1);
drop(runtime.begin_frame(now));
runtime.suspend();
let idle = runtime.begin_frame(now + Duration::from_millis(120));
assert!(!idle.schedule().paced());
assert_eq!(idle.plan().frame_delta(), Duration::ZERO);
assert_eq!(idle.plan().fixed_steps(), 0);
drop(idle);
let resumed_at = now + Duration::from_millis(120);
runtime.record_presented(presented + Duration::from_millis(120));
runtime.resume(resumed_at);
let frame = runtime.begin_frame(resumed_at + Duration::from_millis(2));
assert!(frame.schedule().paced());
assert_eq!(frame.plan().frame_delta(), STEP);
}
#[test]
fn scoped_frame_cleanup_and_window_suspension_release_input() {
let started = Instant::now();
let mut runtime = Runtime::new(RuntimeConfig::fixed_hz(60).unwrap(), started);
runtime.handle_window_event(WindowEvent::Input(InputEvent::Keyboard {
key: KeyCode::KeyW,
state: ButtonState::Pressed,
repeat: false,
modifiers: Modifiers::default(),
}));
let frame = runtime.begin_frame(started + STEP);
assert_eq!(frame.plan().fixed_steps(), 1);
assert!(frame.input().key_pressed(KeyCode::KeyW));
drop(frame);
assert!(!runtime.input().key_pressed(KeyCode::KeyW));
assert!(runtime.input().key_held(KeyCode::KeyW));
runtime.handle_window_event(WindowEvent::RenderingSuspended);
assert!(runtime.suspended());
assert!(!runtime.input().key_held(KeyCode::KeyW));
assert!(runtime.input().key_released(KeyCode::KeyW));
}
#[test]
fn zero_step_frame_preserves_transitions_until_simulation_can_consume_them() {
const DISPLAY_75_HZ_FRAME: Duration = Duration::from_nanos(13_333_333);
let started = Instant::now();
let mut runtime = Runtime::new(RuntimeConfig::fixed_hz(60).unwrap(), started);
runtime.handle_input(InputEvent::Keyboard {
key: KeyCode::Space,
state: ButtonState::Pressed,
repeat: false,
modifiers: Modifiers::default(),
});
let render_only = runtime.begin_frame(started + DISPLAY_75_HZ_FRAME);
assert_eq!(render_only.plan().fixed_steps(), 0);
assert!(render_only.input().key_pressed(KeyCode::Space));
drop(render_only);
assert!(runtime.input().key_pressed(KeyCode::Space));
let simulation_frame = runtime.begin_frame(started + DISPLAY_75_HZ_FRAME * 2);
assert_eq!(simulation_frame.plan().fixed_steps(), 1);
assert!(simulation_frame.input().key_pressed(KeyCode::Space));
drop(simulation_frame);
assert!(!runtime.input().key_pressed(KeyCode::Space));
assert!(runtime.input().key_held(KeyCode::Space));
}
}