Skip to main content

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
8#![deny(unsafe_code)]
9
10#[cfg(target_arch = "wasm32")]
11use std::cell::RefCell;
12use std::fmt;
13use std::sync::atomic::{AtomicBool, Ordering};
14use std::sync::Arc;
15#[cfg(not(target_arch = "wasm32"))]
16use std::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
17use std::time::Duration;
18use web_time::Instant;
19
20#[cfg(feature = "internal")]
21use cranpose_core::internal::FrameClock;
22use cranpose_core::{Clock, Runtime, RuntimeHandle, RuntimeScheduler};
23
24#[cfg(not(target_arch = "wasm32"))]
25type NativeFrameWaker = Arc<dyn Fn() + Send + Sync + 'static>;
26
27/// Scheduler that delegates work to Rust's threading primitives.
28pub struct StdScheduler {
29    frame_requested: AtomicBool,
30    #[cfg(not(target_arch = "wasm32"))]
31    frame_waker: RwLock<Option<NativeFrameWaker>>,
32    #[cfg(target_arch = "wasm32")]
33    frame_waker: RefCell<Option<Box<dyn Fn() + 'static>>>,
34}
35
36impl StdScheduler {
37    pub fn new() -> Self {
38        Self {
39            frame_requested: AtomicBool::new(false),
40            frame_waker: Default::default(),
41        }
42    }
43
44    /// Returns whether a frame has been requested since the last call.
45    pub fn take_frame_request(&self) -> bool {
46        self.frame_requested.swap(false, Ordering::SeqCst)
47    }
48
49    /// Returns whether a frame is currently pending without consuming the request.
50    pub fn has_frame_request(&self) -> bool {
51        self.frame_requested.load(Ordering::SeqCst)
52    }
53
54    /// Registers a waker that will be invoked whenever a new frame is scheduled.
55    #[cfg(not(target_arch = "wasm32"))]
56    pub fn set_frame_waker(&self, waker: impl Fn() + Send + Sync + 'static) {
57        let old_waker = {
58            let mut frame_waker = self.frame_waker_write();
59            frame_waker.replace(Arc::new(waker))
60        };
61        drop(old_waker);
62    }
63
64    #[cfg(target_arch = "wasm32")]
65    pub fn set_frame_waker(&self, waker: impl Fn() + 'static) {
66        *self.frame_waker.borrow_mut() = Some(Box::new(waker));
67    }
68
69    /// Clears any registered frame waker.
70    #[cfg(not(target_arch = "wasm32"))]
71    pub fn clear_frame_waker(&self) {
72        let old_waker = {
73            let mut frame_waker = self.frame_waker_write();
74            frame_waker.take()
75        };
76        drop(old_waker);
77    }
78
79    /// Clears any registered frame waker.
80    #[cfg(target_arch = "wasm32")]
81    pub fn clear_frame_waker(&self) {
82        *self.frame_waker.borrow_mut() = None;
83    }
84
85    #[cfg(not(target_arch = "wasm32"))]
86    fn wake(&self) {
87        let waker = self.frame_waker_read().clone();
88        if let Some(waker) = waker {
89            waker();
90        }
91    }
92
93    #[cfg(target_arch = "wasm32")]
94    fn wake(&self) {
95        if let Some(waker) = self.frame_waker.borrow().as_ref() {
96            waker();
97        }
98    }
99
100    #[cfg(not(target_arch = "wasm32"))]
101    fn frame_waker_read(&self) -> RwLockReadGuard<'_, Option<NativeFrameWaker>> {
102        match self.frame_waker.read() {
103            Ok(guard) => guard,
104            Err(poisoned) => poisoned.into_inner(),
105        }
106    }
107
108    #[cfg(not(target_arch = "wasm32"))]
109    fn frame_waker_write(&self) -> RwLockWriteGuard<'_, Option<NativeFrameWaker>> {
110        match self.frame_waker.write() {
111            Ok(guard) => guard,
112            Err(poisoned) => poisoned.into_inner(),
113        }
114    }
115}
116
117impl Default for StdScheduler {
118    fn default() -> Self {
119        Self::new()
120    }
121}
122
123impl fmt::Debug for StdScheduler {
124    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125        f.debug_struct("StdScheduler")
126            .field(
127                "frame_requested",
128                &self.frame_requested.load(Ordering::SeqCst),
129            )
130            .finish()
131    }
132}
133
134impl RuntimeScheduler for StdScheduler {
135    fn schedule_frame(&self) {
136        self.frame_requested.store(true, Ordering::SeqCst);
137        self.wake();
138    }
139}
140
141/// Clock implementation backed by a cross-platform monotonic timer.
142#[derive(Debug, Default, Clone)]
143pub struct StdClock;
144
145impl Clock for StdClock {
146    type Instant = Instant;
147
148    fn now(&self) -> Self::Instant {
149        Instant::now()
150    }
151
152    fn elapsed_millis(&self, since: Self::Instant) -> u64 {
153        since.elapsed().as_millis() as u64
154    }
155}
156
157impl StdClock {
158    /// Returns the elapsed time as a [`Duration`] for convenience.
159    pub fn elapsed(&self, since: Instant) -> Duration {
160        since.elapsed()
161    }
162}
163
164/// Convenience container bundling the standard scheduler and clock.
165#[derive(Clone)]
166pub struct StdRuntime {
167    scheduler: Arc<StdScheduler>,
168    clock: Arc<StdClock>,
169    runtime: Runtime,
170}
171
172impl StdRuntime {
173    /// Creates a new standard runtime instance.
174    pub fn new() -> Self {
175        let scheduler = Arc::new(StdScheduler::default());
176        let runtime = Runtime::new(scheduler.clone());
177        Self {
178            scheduler,
179            clock: Arc::new(StdClock),
180            runtime,
181        }
182    }
183
184    /// Returns a [`cranpose_core::Runtime`] configured with the standard scheduler.
185    pub fn runtime(&self) -> Runtime {
186        self.runtime.clone()
187    }
188
189    /// Returns a handle to the runtime.
190    pub fn runtime_handle(&self) -> RuntimeHandle {
191        self.runtime.handle()
192    }
193
194    /// Returns the runtime's frame clock.
195    #[cfg(feature = "internal")]
196    pub fn frame_clock(&self) -> FrameClock {
197        self.runtime.frame_clock()
198    }
199
200    /// Returns the scheduler implementation.
201    pub fn scheduler(&self) -> Arc<StdScheduler> {
202        Arc::clone(&self.scheduler)
203    }
204
205    /// Returns the clock implementation.
206    pub fn clock(&self) -> Arc<StdClock> {
207        Arc::clone(&self.clock)
208    }
209
210    /// Returns whether a frame was requested since the last poll.
211    pub fn take_frame_request(&self) -> bool {
212        self.scheduler.take_frame_request()
213    }
214
215    pub fn has_frame_request(&self) -> bool {
216        self.scheduler.has_frame_request()
217    }
218
219    /// Registers a waker to be called when the runtime schedules a new frame.
220    #[cfg(not(target_arch = "wasm32"))]
221    pub fn set_frame_waker(&self, waker: impl Fn() + Send + Sync + 'static) {
222        self.scheduler.set_frame_waker(waker);
223    }
224
225    #[cfg(target_arch = "wasm32")]
226    pub fn set_frame_waker(&self, waker: impl Fn() + 'static) {
227        self.scheduler.set_frame_waker(waker);
228    }
229
230    /// Clears any previously registered frame waker.
231    pub fn clear_frame_waker(&self) {
232        self.scheduler.clear_frame_waker();
233    }
234
235    /// Drains pending frame callbacks using the provided frame timestamp in nanoseconds.
236    pub fn drain_frame_callbacks(&self, frame_time_nanos: u64) {
237        self.runtime_handle()
238            .drain_frame_callbacks(frame_time_nanos);
239    }
240}
241
242impl fmt::Debug for StdRuntime {
243    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
244        f.debug_struct("StdRuntime")
245            .field("scheduler", &self.scheduler)
246            .field("clock", &self.clock)
247            .finish()
248    }
249}
250
251impl Default for StdRuntime {
252    fn default() -> Self {
253        Self::new()
254    }
255}
256
257#[cfg(test)]
258#[path = "tests/std_runtime_tests.rs"]
259mod tests;