Skip to main content

mulciber_runtime/
lib.rs

1//! Game-loop timing and input snapshots for Mulciber.
2//!
3//! The first runtime slice decouples a fixed-rate simulation from variable-rate rendering. It owns
4//! the accumulator and bounded catch-up policy while leaving previous/current game state and its
5//! interpolation with the application.
6
7mod input;
8mod timing;
9
10use std::time::Instant;
11
12pub use input::{InputSnapshot, ScrollSample};
13use mulciber_platform::{InputEvent, WindowEvent};
14pub use timing::{FramePlan, RuntimeConfig, RuntimeConfigError};
15
16/// Coordinates frame-scoped input with a fixed-rate simulation clock.
17#[derive(Debug)]
18pub struct Runtime {
19    input: InputSnapshot,
20    clock: timing::FrameClock,
21}
22
23/// One scoped runtime frame containing its timing plan and immutable input snapshot.
24///
25/// Dropping the frame clears pressed/released transitions and scroll samples while preserving held
26/// controls. This includes early returns from surface acquisition or rendering errors.
27#[derive(Debug)]
28#[must_use = "a runtime frame must be consumed by update/render work"]
29pub struct RuntimeFrame<'runtime> {
30    input: &'runtime mut InputSnapshot,
31    plan: FramePlan,
32}
33
34impl RuntimeFrame<'_> {
35    /// Returns the fixed/variable timing work and render interpolation for this frame.
36    #[must_use]
37    pub const fn plan(&self) -> FramePlan {
38        self.plan
39    }
40
41    /// Returns the held state and transitions accumulated for this frame.
42    #[must_use]
43    pub const fn input(&self) -> &InputSnapshot {
44        self.input
45    }
46}
47
48impl Drop for RuntimeFrame<'_> {
49    fn drop(&mut self) {
50        self.input.end_frame();
51    }
52}
53
54impl Runtime {
55    /// Starts a runtime clock at `started_at` with no accumulated simulation debt.
56    #[must_use]
57    pub fn new(config: RuntimeConfig, started_at: Instant) -> Self {
58        Self {
59            input: InputSnapshot::default(),
60            clock: timing::FrameClock::new(config, started_at),
61        }
62    }
63
64    /// Adds one ordered native input transition to the current snapshot.
65    pub fn handle_input(&mut self, event: InputEvent) {
66        self.input.handle_event(event);
67    }
68
69    /// Applies the input and rendering-lifecycle parts of one platform window event.
70    ///
71    /// Redraw, metrics, and close policy remain with the application. Lower-level input, suspend,
72    /// and resume methods remain available when an application uses a different coordination shape.
73    pub fn handle_window_event(&mut self, event: WindowEvent) {
74        match event {
75            WindowEvent::Input(input) => self.handle_input(input),
76            WindowEvent::RenderingSuspended => self.suspend(),
77            WindowEvent::RenderingResumed(_) => self.resume(Instant::now()),
78            _ => {}
79        }
80    }
81
82    /// Returns the held state and transitions accumulated for the current frame.
83    #[must_use]
84    pub const fn input(&self) -> &InputSnapshot {
85        &self.input
86    }
87
88    /// Begins a scoped frame with fixed simulation work, input, and render interpolation.
89    ///
90    /// Dropping the returned frame automatically ends the input snapshot, including on early return.
91    pub fn begin_frame(&mut self, now: Instant) -> RuntimeFrame<'_> {
92        RuntimeFrame {
93            input: &mut self.input,
94            plan: self.clock.advance(now),
95        }
96    }
97
98    /// Pauses frame timing and releases every held input control.
99    ///
100    /// The fractional fixed-step accumulator is preserved so rendering can resume without a small
101    /// interpolation jump. Calls to [`Self::begin_frame`] while suspended schedule no updates.
102    pub fn suspend(&mut self) {
103        self.clock.suspend();
104        self.input.release_all();
105    }
106
107    /// Resumes frame timing from `now` without treating the suspended interval as elapsed game time.
108    pub fn resume(&mut self, now: Instant) {
109        self.clock.resume(now);
110    }
111
112    /// Returns whether frame timing is currently suspended.
113    #[must_use]
114    pub const fn suspended(&self) -> bool {
115        self.clock.suspended()
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use std::time::Instant;
122
123    use mulciber_platform::{ButtonState, InputEvent, KeyCode, Modifiers, WindowEvent};
124
125    use super::{Runtime, RuntimeConfig};
126
127    #[test]
128    fn scoped_frame_cleanup_and_window_suspension_release_input() {
129        let mut runtime = Runtime::new(RuntimeConfig::fixed_hz(60).unwrap(), Instant::now());
130        runtime.handle_window_event(WindowEvent::Input(InputEvent::Keyboard {
131            key: KeyCode::KeyW,
132            state: ButtonState::Pressed,
133            repeat: false,
134            modifiers: Modifiers::default(),
135        }));
136        let frame = runtime.begin_frame(Instant::now());
137        assert!(frame.input().key_pressed(KeyCode::KeyW));
138        drop(frame);
139        assert!(!runtime.input().key_pressed(KeyCode::KeyW));
140        assert!(runtime.input().key_held(KeyCode::KeyW));
141
142        runtime.handle_window_event(WindowEvent::RenderingSuspended);
143        assert!(runtime.suspended());
144        assert!(!runtime.input().key_held(KeyCode::KeyW));
145        assert!(runtime.input().key_released(KeyCode::KeyW));
146    }
147}