cranpose_runtime_std/
lib.rs

1//! Standard runtime services backed by Rust's `std` library.
2//!
3//! This crate provides concrete implementations of the platform
4//! abstraction traits defined in `cranpose-core`. Applications can
5//! construct a [`StdRuntime`] and pass it to [`cranpose_core::Composition`]
6//! to power the runtime with `std` primitives.
7
8use std::fmt;
9use std::sync::atomic::{AtomicBool, Ordering};
10use std::sync::{Arc, RwLock};
11use std::time::{Duration, Instant};
12
13use cranpose_core::{Clock, FrameClock, Runtime, RuntimeHandle, RuntimeScheduler};
14
15// On WASM, wrap closures to make them Sync since WASM is single-threaded
16#[cfg(target_arch = "wasm32")]
17struct SyncWaker<F>(F);
18
19#[cfg(target_arch = "wasm32")]
20unsafe impl<F> Sync for SyncWaker<F> {}
21
22#[cfg(target_arch = "wasm32")]
23impl<F: Fn()> SyncWaker<F> {
24    fn call(&self) {
25        (self.0)();
26    }
27}
28
29/// Scheduler that delegates work to Rust's threading primitives.
30pub struct StdScheduler {
31    frame_requested: AtomicBool,
32    #[cfg(not(target_arch = "wasm32"))]
33    frame_waker: RwLock<Option<Arc<dyn Fn() + Send + Sync + 'static>>>,
34    #[cfg(target_arch = "wasm32")]
35    frame_waker: RwLock<Option<Arc<SyncWaker<Box<dyn Fn() + Send + 'static>>>>>,
36}
37
38impl StdScheduler {
39    pub fn new() -> Self {
40        Self {
41            frame_requested: AtomicBool::new(false),
42            frame_waker: RwLock::new(None),
43        }
44    }
45
46    /// Returns whether a frame has been requested since the last call.
47    pub fn take_frame_request(&self) -> bool {
48        self.frame_requested.swap(false, Ordering::SeqCst)
49    }
50
51    /// Registers a waker that will be invoked whenever a new frame is scheduled.
52    #[cfg(not(target_arch = "wasm32"))]
53    pub fn set_frame_waker(&self, waker: impl Fn() + Send + Sync + 'static) {
54        *self.frame_waker.write().unwrap() = Some(Arc::new(waker));
55    }
56
57    #[cfg(target_arch = "wasm32")]
58    pub fn set_frame_waker(&self, waker: impl Fn() + Send + 'static) {
59        *self.frame_waker.write().unwrap() = Some(Arc::new(SyncWaker(Box::new(waker))));
60    }
61
62    /// Clears any registered frame waker.
63    pub fn clear_frame_waker(&self) {
64        *self.frame_waker.write().unwrap() = None;
65    }
66
67    #[cfg(not(target_arch = "wasm32"))]
68    fn wake(&self) {
69        let waker = self.frame_waker.read().unwrap().clone();
70        if let Some(waker) = waker {
71            waker();
72        }
73    }
74
75    #[cfg(target_arch = "wasm32")]
76    fn wake(&self) {
77        let waker = self.frame_waker.read().unwrap().clone();
78        if let Some(waker) = waker {
79            waker.call();
80        }
81    }
82}
83
84impl Default for StdScheduler {
85    fn default() -> Self {
86        Self::new()
87    }
88}
89
90impl fmt::Debug for StdScheduler {
91    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92        f.debug_struct("StdScheduler")
93            .field(
94                "frame_requested",
95                &self.frame_requested.load(Ordering::SeqCst),
96            )
97            .finish()
98    }
99}
100
101impl RuntimeScheduler for StdScheduler {
102    fn schedule_frame(&self) {
103        self.frame_requested.store(true, Ordering::SeqCst);
104        self.wake();
105    }
106}
107
108/// Clock implementation backed by [`std::time`].
109#[derive(Debug, Default, Clone)]
110pub struct StdClock;
111
112impl Clock for StdClock {
113    type Instant = Instant;
114
115    fn now(&self) -> Self::Instant {
116        Instant::now()
117    }
118
119    fn elapsed_millis(&self, since: Self::Instant) -> u64 {
120        since.elapsed().as_millis() as u64
121    }
122}
123
124impl StdClock {
125    /// Returns the elapsed time as a [`Duration`] for convenience.
126    pub fn elapsed(&self, since: Instant) -> Duration {
127        since.elapsed()
128    }
129}
130
131/// Convenience container bundling the standard scheduler and clock.
132#[derive(Clone)]
133pub struct StdRuntime {
134    scheduler: Arc<StdScheduler>,
135    clock: Arc<StdClock>,
136    runtime: Runtime,
137}
138
139impl StdRuntime {
140    /// Creates a new standard runtime instance.
141    pub fn new() -> Self {
142        let scheduler = Arc::new(StdScheduler::default());
143        let runtime = Runtime::new(scheduler.clone());
144        Self {
145            scheduler,
146            clock: Arc::new(StdClock),
147            runtime,
148        }
149    }
150
151    /// Returns a [`cranpose_core::Runtime`] configured with the standard scheduler.
152    pub fn runtime(&self) -> Runtime {
153        self.runtime.clone()
154    }
155
156    /// Returns a handle to the runtime.
157    pub fn runtime_handle(&self) -> RuntimeHandle {
158        self.runtime.handle()
159    }
160
161    /// Returns the runtime's frame clock.
162    pub fn frame_clock(&self) -> FrameClock {
163        self.runtime.frame_clock()
164    }
165
166    /// Returns the scheduler implementation.
167    pub fn scheduler(&self) -> Arc<StdScheduler> {
168        Arc::clone(&self.scheduler)
169    }
170
171    /// Returns the clock implementation.
172    pub fn clock(&self) -> Arc<StdClock> {
173        Arc::clone(&self.clock)
174    }
175
176    /// Returns whether a frame was requested since the last poll.
177    pub fn take_frame_request(&self) -> bool {
178        self.scheduler.take_frame_request()
179    }
180
181    /// Registers a waker to be called when the runtime schedules a new frame.
182    #[cfg(not(target_arch = "wasm32"))]
183    pub fn set_frame_waker(&self, waker: impl Fn() + Send + Sync + 'static) {
184        self.scheduler.set_frame_waker(waker);
185    }
186
187    #[cfg(target_arch = "wasm32")]
188    pub fn set_frame_waker(&self, waker: impl Fn() + Send + 'static) {
189        self.scheduler.set_frame_waker(waker);
190    }
191
192    /// Clears any previously registered frame waker.
193    pub fn clear_frame_waker(&self) {
194        self.scheduler.clear_frame_waker();
195    }
196
197    /// Drains pending frame callbacks using the provided frame timestamp in nanoseconds.
198    pub fn drain_frame_callbacks(&self, frame_time_nanos: u64) {
199        self.runtime_handle()
200            .drain_frame_callbacks(frame_time_nanos);
201    }
202}
203
204impl fmt::Debug for StdRuntime {
205    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
206        f.debug_struct("StdRuntime")
207            .field("scheduler", &self.scheduler)
208            .field("clock", &self.clock)
209            .finish()
210    }
211}
212
213impl Default for StdRuntime {
214    fn default() -> Self {
215        Self::new()
216    }
217}
218
219#[cfg(test)]
220#[path = "tests/std_runtime_tests.rs"]
221mod tests;