Skip to main content

cranpose_runtime_std/
lib.rs

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