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