Skip to main content

ferroui_browser/rendering/
browser_shared_render_loop.rs

1use super::{BrowserRenderTimer, RenderWorker};
2use ferroui_base::rendering::{IRenderLoop, IRenderTimer, RenderLoop};
3use std::sync::{Arc, OnceLock};
4
5/// The one render loop of the page. Every view renders in the animation
6/// frames of one thread: the thread of the user interface, or the render
7/// thread when the platform started one
8/// ([`start_render_thread`](Self::start_render_thread)).
9pub struct BrowserSharedRenderLoop;
10
11static BROWSER_UI_RENDER_TIMER: OnceLock<Arc<BrowserRenderTimer>> = OnceLock::new();
12static RENDER_LOOP: OnceLock<Arc<dyn IRenderLoop>> = OnceLock::new();
13
14impl BrowserSharedRenderLoop {
15    /// The render timer of the page. Upstream creates it with
16    /// `RunsInBackground` false in both of its modes; here it runs in the
17    /// background when it was created for the render thread
18    /// ([`start_render_thread`](Self::start_render_thread)).
19    pub fn render_timer() -> Arc<BrowserRenderTimer> {
20        BROWSER_UI_RENDER_TIMER.get_or_init(|| BrowserRenderTimer::new(false)).clone()
21    }
22
23    /// Starts the render thread of the page and makes the render timer one
24    /// that ticks there, in the background. Returns whether the views of
25    /// the page are rendered by the render thread from here on.
26    ///
27    /// The platform calls it once, when it is registered, in a module built
28    /// with threads: before the first view is created and before anything
29    /// asks for the render timer or the render loop. The thread of the user
30    /// interface does not wait for the thread
31    /// ([`RenderWorker::start`]); a view that is created before the thread
32    /// has reported itself is not ready until it has.
33    ///
34    /// The answer is `false`, and the page renders on the thread of the
35    /// user interface as it does without threads, when the module was built
36    /// without threads, when the thread cannot be created, when the render
37    /// timer exists already (the order above was not kept) and when a
38    /// render thread was started by somebody else, with a frame loop of
39    /// their own.
40    pub fn start_render_thread() -> bool {
41        if !cfg!(all(target_os = "emscripten", target_feature = "atomics")) {
42            return false;
43        }
44        if BROWSER_UI_RENDER_TIMER.get().is_some() {
45            return Self::renders_on_render_thread();
46        }
47        // Created in its final state before the first compositor is: a
48        // compositor reads where its loop runs once, when it is created.
49        let timer = BrowserRenderTimer::new(true);
50        match RenderWorker::start(Some(timer.clone()), None) {
51            Ok(true) => BROWSER_UI_RENDER_TIMER.set(timer).is_ok(),
52            // Started before, with another frame loop or none; or not
53            // started at all. The timer made above is dropped unused.
54            Ok(false) | Err(_) => false,
55        }
56    }
57
58    /// Whether the render loop of the page ticks on the render thread: the
59    /// compositor of a view is then confined to that thread, and the canvas
60    /// of a view is created for it.
61    pub fn renders_on_render_thread() -> bool {
62        BROWSER_UI_RENDER_TIMER.get().is_some_and(|timer| timer.runs_in_background())
63    }
64
65    /// The render loop driven by [`render_timer`](Self::render_timer).
66    pub fn render_loop() -> Arc<dyn IRenderLoop> {
67        RENDER_LOOP
68            .get_or_init(|| {
69                let timer: Arc<dyn IRenderTimer> = Self::render_timer();
70                RenderLoop::from_timer(timer)
71            })
72            .clone()
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    #[test]
81    fn without_threads_no_render_thread_is_started_and_the_loop_stays_on_the_page() {
82        assert!(!BrowserSharedRenderLoop::start_render_thread());
83        assert!(!RenderWorker::exists());
84        assert!(!BrowserSharedRenderLoop::renders_on_render_thread());
85
86        // The timer and the loop of the page, as before there were threads.
87        assert!(!BrowserSharedRenderLoop::render_timer().runs_in_background());
88        assert!(!BrowserSharedRenderLoop::render_loop().runs_in_background());
89
90        // Asked again, with the timer in place, the answer is the same.
91        assert!(!BrowserSharedRenderLoop::start_render_thread());
92        assert!(!BrowserSharedRenderLoop::renders_on_render_thread());
93    }
94}