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}