Skip to main content

cranpose_runtime_std/
lib.rs

1#![deny(missing_docs)]
2#![doc = include_str!("../README.md")]
3
4#[cfg(target_arch = "wasm32")]
5use std::cell::RefCell;
6#[cfg(not(target_arch = "wasm32"))]
7use std::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
8use std::{
9    fmt,
10    sync::{
11        Arc,
12        atomic::{AtomicBool, Ordering},
13    },
14    time::Duration,
15};
16
17#[cfg(feature = "internal")]
18use cranpose_core::internal::FrameClock;
19use cranpose_core::{Clock, Runtime, RuntimeHandle, RuntimeScheduler};
20use web_time::Instant;
21
22#[cfg(not(target_arch = "wasm32"))]
23type NativeFrameWaker = Arc<dyn Fn() + Send + Sync + 'static>;
24
25/// Scheduler that delegates work to Rust's threading primitives.
26pub struct StdScheduler {
27    frame_requested: AtomicBool,
28    #[cfg(not(target_arch = "wasm32"))]
29    frame_waker: RwLock<Option<NativeFrameWaker>>,
30    #[cfg(target_arch = "wasm32")]
31    frame_waker: RefCell<Option<Box<dyn Fn() + 'static>>>,
32}
33
34impl StdScheduler {
35    /// Creates a scheduler with no pending frame request and no frame waker.
36    pub fn new() -> Self {
37        Self {
38            frame_requested: AtomicBool::new(false),
39            frame_waker: Default::default(),
40        }
41    }
42
43    /// Returns whether a frame has been requested since the last call.
44    pub fn take_frame_request(&self) -> bool {
45        self.frame_requested.swap(false, Ordering::SeqCst)
46    }
47
48    /// Returns whether a frame is currently pending without consuming the request.
49    pub fn has_frame_request(&self) -> bool {
50        self.frame_requested.load(Ordering::SeqCst)
51    }
52
53    /// Registers a waker that will be invoked whenever a new frame is scheduled.
54    #[cfg(not(target_arch = "wasm32"))]
55    pub fn set_frame_waker(&self, waker: impl Fn() + Send + Sync + 'static) {
56        let old_waker = {
57            let mut frame_waker = self.frame_waker_write();
58            frame_waker.replace(Arc::new(waker))
59        };
60        drop(old_waker);
61    }
62
63    /// Registers a waker that will be invoked whenever a new frame is scheduled.
64    ///
65    /// This single-threaded variant is used on wasm, so the waker does not need to be `Send`.
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    /// Returns whether a frame has been requested, without consuming the request.
233    pub fn has_frame_request(&self) -> bool {
234        self.scheduler.has_frame_request()
235    }
236
237    /// Registers a waker to be called when the runtime schedules a new frame.
238    #[cfg(not(target_arch = "wasm32"))]
239    pub fn set_frame_waker(&self, waker: impl Fn() + Send + Sync + 'static) {
240        self.scheduler.set_frame_waker(waker);
241    }
242
243    /// Registers a waker to be called when the runtime schedules a new frame.
244    ///
245    /// This single-threaded variant is used on wasm, so the waker does not need to be `Send`.
246    #[cfg(target_arch = "wasm32")]
247    pub fn set_frame_waker(&self, waker: impl Fn() + 'static) {
248        self.scheduler.set_frame_waker(waker);
249    }
250
251    /// Clears any previously registered frame waker.
252    pub fn clear_frame_waker(&self) {
253        self.scheduler.clear_frame_waker();
254    }
255
256    /// Drains pending frame callbacks using the provided frame timestamp in nanoseconds.
257    pub fn drain_frame_callbacks(&self, frame_time_nanos: u64) {
258        self.runtime_handle()
259            .drain_frame_callbacks(frame_time_nanos);
260    }
261}
262
263impl fmt::Debug for StdRuntime {
264    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
265        f.debug_struct("StdRuntime")
266            .field("scheduler", &self.scheduler)
267            .field("clock", &self.clock)
268            .finish()
269    }
270}
271
272impl Default for StdRuntime {
273    fn default() -> Self {
274        Self::new()
275    }
276}
277
278#[cfg(test)]
279#[path = "tests/std_runtime_tests.rs"]
280mod tests;