Skip to main content

ferroui_browser/rendering/
render_worker.rs

1use super::web_render_target::{worker_started, PENDING_RENDER_THREAD};
2use super::{BrowserRenderTimer, BrowserSurfaceShared, RenderStatistics};
3use crate::interop::thread_proxy;
4use std::any::Any;
5use std::panic::{Location, PanicHookInfo};
6use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
7use std::sync::{Arc, Once};
8
9/// The render thread of the page: a thread of the module whose worker takes
10/// the canvases of the views and draws to them.
11///
12/// Upstream starts a worker of its runtime, waits for it and only then goes
13/// on with the start of the application. A start function of this port
14/// cannot wait (the thread of the page may not block on a thread that needs
15/// the page to start), so [`start`](Self::start) returns at once and the
16/// thread reports itself when it runs:
17///
18/// - the thread installs the handler of the registry of its script
19///   ([`initialize_worker`](super::initialize_worker)), follows the reports
20///   of that registry ([`on_render_target_registered`](Self::on_render_target_registered)),
21///   keeps itself alive past the return of its start function, starts the
22///   frame loop it was given, and publishes its id;
23/// - a canvas that is created before that
24///   ([`canvas_thread_id`](Self::canvas_thread_id) answers
25///   [`PENDING_RENDER_THREAD`]) is transferred by the script at once and
26///   posted to the worker when the thread of the page hears of the thread;
27///   its surface is simply not ready until the worker has reported the
28///   target.
29///
30/// There is one render thread for the life of the page; it never ends.
31///
32/// A panic of the thread does not end it either. A frame that panics is
33/// caught by the render loop (`DefaultRenderLoop`, as upstream's loop
34/// catches the exception of a frame and logs it), and the loop goes on with
35/// its next tick. The panic is reported to the thread of the page, which
36/// logs it on the console of the page ([`on_panic`](Self::on_panic)): what
37/// the thread itself logs stays in its worker.
38///
39/// The platform starts it when it is registered, for the frame loop of the
40/// page ([`BrowserSharedRenderLoop::start_render_thread`](super::BrowserSharedRenderLoop::start_render_thread)).
41/// A module built without threads cannot start it at all.
42pub struct RenderWorker;
43
44/// What the threads of the module know about the render thread.
45struct WorkerState {
46    /// Whether the thread was asked for and could be created.
47    started: AtomicBool,
48    /// The thread that asked for it and creates the canvases, as
49    /// [`thread_proxy::current_thread`] names it there.
50    page_thread: AtomicUsize,
51    /// The render thread; 0 until it has installed its handler.
52    thread: AtomicUsize,
53    /// Whether the script of the thread of the page has been told.
54    announced: AtomicBool,
55}
56
57static STATE: WorkerState = WorkerState::new();
58
59impl WorkerState {
60    const fn new() -> Self {
61        Self {
62            started: AtomicBool::new(false),
63            page_thread: AtomicUsize::new(0),
64            thread: AtomicUsize::new(0),
65            announced: AtomicBool::new(false),
66        }
67    }
68
69    /// Records that `page_thread` asks for the render thread. Returns
70    /// whether it is the first to ask.
71    fn begin(&self, page_thread: usize) -> bool {
72        if self.started.swap(true, Ordering::SeqCst) {
73            return false;
74        }
75        self.page_thread.store(page_thread, Ordering::SeqCst);
76        true
77    }
78
79    /// The thread could not be created: there is no render thread, and it
80    /// may be asked for again.
81    fn abandon(&self) {
82        self.started.store(false, Ordering::SeqCst);
83    }
84
85    /// Called by the render thread when its worker takes canvases.
86    fn running(&self, thread: usize) {
87        self.thread.store(thread, Ordering::SeqCst);
88    }
89
90    fn exists(&self) -> bool {
91        self.started.load(Ordering::SeqCst)
92    }
93
94    fn thread(&self) -> usize {
95        self.thread.load(Ordering::SeqCst)
96    }
97
98    fn page_thread(&self) -> usize {
99        self.page_thread.load(Ordering::SeqCst)
100    }
101
102    fn canvas_thread_id(&self) -> i32 {
103        if !self.exists() {
104            return 0;
105        }
106        match self.thread() {
107            0 => PENDING_RENDER_THREAD,
108            // The id is an address in a memory of at most 4 GB; the script
109            // reads the same bits as the key of the thread.
110            thread => thread as i32,
111        }
112    }
113
114    /// The id to tell the script of the thread of the page, once: `Some`
115    /// for the first call that is made on that thread after the render
116    /// thread reported itself.
117    fn take_announcement(&self, current_thread: usize) -> Option<i32> {
118        let thread = self.thread();
119        if thread == 0 || current_thread != self.page_thread() {
120            return None;
121        }
122        if self.announced.swap(true, Ordering::SeqCst) {
123            return None;
124        }
125        Some(thread as i32)
126    }
127}
128
129impl RenderWorker {
130    /// Starts the render thread and returns at once; `Ok(false)` when it
131    /// was started before. Called by the thread that creates the canvases
132    /// (the thread of the page).
133    ///
134    /// `timer` is the render timer whose frame loop the thread runs: it is
135    /// started on the thread ([`BrowserRenderTimer::start_on_this_thread`]).
136    /// `on_thread` runs on the thread before that, when its worker already
137    /// takes canvases; a caller subscribes there to what arrives on that
138    /// thread.
139    ///
140    /// # Errors
141    /// An error when the thread could not be created; in a module built
142    /// without threads always.
143    pub fn start(
144        timer: Option<Arc<BrowserRenderTimer>>,
145        on_thread: Option<Box<dyn FnOnce() + Send>>,
146    ) -> std::io::Result<bool> {
147        if !STATE.begin(thread_proxy::current_thread()) {
148            return Ok(false);
149        }
150        let spawned = native::spawn(move || {
151            if let Some(on_thread) = on_thread {
152                on_thread();
153            }
154            if let Some(timer) = timer {
155                timer.start_on_this_thread();
156            }
157            // Last: from here on a canvas is posted to this thread at once.
158            STATE.running(thread_proxy::current_thread());
159            // The thread of the page hears of it from its event loop and
160            // posts the canvases it held back. When the call cannot be
161            // queued, the next canvas that is created tells the script
162            // (`canvas_thread_id`).
163            thread_proxy::run_on_thread(STATE.page_thread(), Box::new(Self::announce));
164        });
165        match spawned {
166            Ok(()) => {
167                Self::report_panics();
168                Ok(true)
169            }
170            Err(error) => {
171                STATE.abandon();
172                Err(error)
173            }
174        }
175    }
176
177    /// Whether a render thread exists: it was started, though it may not
178    /// have reported itself yet. Upstream's condition for where the frame
179    /// loop runs (`IsThreadingEnabled`) is a property of the build; here it
180    /// is whether the thread was started, so that a module built with
181    /// threads can still run on one.
182    pub fn exists() -> bool {
183        STATE.exists()
184    }
185
186    /// The id of the render thread, as [`thread_proxy::current_thread`]
187    /// names it there (what `pthread_self` returns); 0 until the thread has
188    /// reported itself, and always without one.
189    pub fn thread_id() -> usize {
190        STATE.thread()
191    }
192
193    /// The thread a canvas is created for
194    /// ([`create_render_target_surface`](crate::interop::canvas_helper::CanvasSurface::create_render_target_surface)):
195    /// 0 without a render thread, the id of the render thread once it has
196    /// reported itself, and [`PENDING_RENDER_THREAD`] in between. Called by
197    /// the thread that creates the canvases.
198    pub fn canvas_thread_id() -> i32 {
199        Self::announce();
200        STATE.canvas_thread_id()
201    }
202
203    /// Queues `work` for the render thread, which runs it from the event
204    /// loop of its worker. Returns whether it was queued: `false` while the
205    /// thread has not reported itself, and without one.
206    pub fn post(work: impl FnOnce() + Send + 'static) -> bool {
207        thread_proxy::run_on_thread(STATE.thread(), Box::new(work))
208    }
209
210    /// The worker of the calling thread created the render target
211    /// `target_id` of a canvas that was transferred to it, and the target
212    /// is of `kind`: publishes the kind in the canvas registered under the
213    /// id ([`BrowserSurfaceShared::report_target`]), from where both
214    /// threads read that the surface is ready. The render thread subscribes
215    /// this to the reports of its registry
216    /// ([`add_render_target_registered`](crate::interop::canvas_helper::add_render_target_registered)).
217    ///
218    /// The render loop is not asked for a frame: its timer ticks with every
219    /// animation frame of the worker, and a composition target without a
220    /// render target asks its surface again on each.
221    pub fn on_render_target_registered(target_id: i32, kind: i32) {
222        BrowserSurfaceShared::report_target(target_id, kind);
223    }
224
225    /// The thread that started the render thread and creates the canvases,
226    /// as [`thread_proxy::current_thread`] names it there; 0 without a
227    /// render thread.
228    pub fn page_thread() -> usize {
229        if STATE.exists() {
230            STATE.page_thread()
231        } else {
232            0
233        }
234    }
235
236    /// Makes the panics of the render thread known to the page, from here
237    /// on: the panic hook of the process is wrapped, once. The hook that was
238    /// there runs first, as before.
239    fn report_panics() {
240        static WRAPPED: Once = Once::new();
241        WRAPPED.call_once(|| {
242            let previous = std::panic::take_hook();
243            std::panic::set_hook(Box::new(move |info| {
244                previous(info);
245                Self::on_panic(info);
246            }));
247        });
248    }
249
250    /// A thread of the module panics. When it is the render thread, the
251    /// panic is counted ([`RenderStatistics::render_thread_panics`]) and a
252    /// call is queued for the thread of the page, which logs the message as
253    /// an error on the console of the page
254    /// ([`thread_proxy::report_render_thread_panic`]). Nothing waits for
255    /// that call. The panic itself goes on as it would have: it unwinds to
256    /// whoever catches it, which for a frame is the render loop.
257    fn on_panic(info: &PanicHookInfo<'_>) {
258        let thread = thread_proxy::current_thread();
259        if thread == 0 || thread != STATE.thread() {
260            return;
261        }
262        RenderStatistics::render_thread_panicked();
263        let message = panic_message(info.payload(), info.location());
264        thread_proxy::run_on_thread(
265            STATE.page_thread(),
266            Box::new(move || thread_proxy::report_render_thread_panic(&message)),
267        );
268    }
269
270    /// Tells the script of the thread of the page that the render thread
271    /// takes canvases, once. Does nothing on another thread and before the
272    /// render thread has reported itself.
273    fn announce() {
274        if let Some(thread_id) = STATE.take_announcement(thread_proxy::current_thread()) {
275            worker_started(thread_id);
276        }
277    }
278}
279
280/// The message of a panic and where it was raised, as the page is told.
281fn panic_message(payload: &(dyn Any + Send), location: Option<&Location<'_>>) -> String {
282    let message = payload
283        .downcast_ref::<&str>()
284        .copied()
285        .or_else(|| payload.downcast_ref::<String>().map(String::as_str))
286        .unwrap_or("a panic without a message");
287    match location {
288        Some(location) => format!("{message} ({}:{})", location.file(), location.line()),
289        None => message.to_string(),
290    }
291}
292
293#[cfg(all(target_os = "emscripten", target_feature = "atomics"))]
294mod native {
295    use super::RenderWorker;
296    use crate::interop::canvas_helper::add_render_target_registered;
297    use crate::rendering::initialize_worker;
298    use std::rc::Rc;
299
300    extern "C" {
301        fn emscripten_runtime_keepalive_push();
302    }
303
304    /// Creates the render thread. The thread makes its worker take the
305    /// canvases that are transferred to it, keeps itself alive, runs
306    /// `on_thread` and returns to the event loop of its worker, where the
307    /// canvases, the animation frames and the calls queued for it arrive.
308    pub(super) fn spawn(on_thread: impl FnOnce() + Send + 'static) -> std::io::Result<()> {
309        let spawned = std::thread::Builder::new().name("ferroui-render".to_string()).spawn(move || {
310            // The subscription lasts as long as the thread; its token is not
311            // kept.
312            add_render_target_registered(Rc::new(RenderWorker::on_render_target_registered));
313            initialize_worker();
314            // The start function returns below, and the thread has to go on.
315            // SAFETY: the function takes no argument; it increments a counter
316            // of the script of the calling thread, which keeps the thread
317            // from exiting when its start function returns.
318            unsafe { emscripten_runtime_keepalive_push() };
319            on_thread();
320        });
321        // The handle is dropped: the thread is detached and nothing joins it.
322        spawned.map(drop)
323    }
324}
325
326#[cfg(not(all(target_os = "emscripten", target_feature = "atomics")))]
327mod native {
328    /// A module without threads has one thread, and it is the one of the
329    /// page.
330    pub(super) fn spawn(_on_thread: impl FnOnce() + Send + 'static) -> std::io::Result<()> {
331        Err(std::io::Error::new(std::io::ErrorKind::Unsupported, "the module was built without threads"))
332    }
333}
334
335#[cfg(test)]
336mod tests {
337    use super::*;
338    use crate::interop::canvas_helper::{RENDER_TARGET_KIND_SOFTWARE, RENDER_TARGET_KIND_WEB_GL};
339
340    #[test]
341    fn a_canvas_is_created_for_no_thread_a_pending_thread_or_the_render_thread() {
342        let state = WorkerState::new();
343        assert!(!state.exists());
344        assert_eq!(0, state.canvas_thread_id());
345
346        // Started by the thread 5, not running yet.
347        assert!(state.begin(5));
348        assert!(!state.begin(6));
349        assert!(state.exists());
350        assert_eq!(5, state.page_thread());
351        assert_eq!(0, state.thread());
352        assert_eq!(PENDING_RENDER_THREAD, state.canvas_thread_id());
353
354        state.running(4096);
355        assert_eq!(4096, state.thread());
356        assert_eq!(4096, state.canvas_thread_id());
357    }
358
359    #[test]
360    fn the_script_of_the_page_is_told_once_on_its_thread_when_the_thread_runs() {
361        let state = WorkerState::new();
362        assert!(state.begin(5));
363        // Not running: nothing to tell.
364        assert_eq!(None, state.take_announcement(5));
365
366        state.running(4096);
367        // The render thread itself, and any other, does not tell.
368        assert_eq!(None, state.take_announcement(4096));
369        assert_eq!(Some(4096), state.take_announcement(5));
370        assert_eq!(None, state.take_announcement(5));
371    }
372
373    #[test]
374    fn a_thread_that_could_not_be_created_can_be_asked_for_again() {
375        let state = WorkerState::new();
376        assert!(state.begin(5));
377        state.abandon();
378
379        assert!(!state.exists());
380        assert_eq!(0, state.canvas_thread_id());
381        assert!(state.begin(7));
382        assert_eq!(7, state.page_thread());
383    }
384
385    #[test]
386    fn without_threads_there_is_no_render_thread() {
387        let started = RenderWorker::start(None, Some(Box::new(|| panic!("there is no thread to run this"))));
388
389        assert_eq!(std::io::ErrorKind::Unsupported, started.expect_err("the build has no threads").kind());
390        assert!(!RenderWorker::exists());
391        assert_eq!(0, RenderWorker::thread_id());
392        assert_eq!(0, RenderWorker::canvas_thread_id());
393        assert!(!RenderWorker::post(|| panic!("there is no thread to run this")));
394    }
395
396    #[test]
397    fn the_message_of_a_panic_names_what_was_said_and_where() {
398        let here = Location::caller();
399        let text: Box<dyn Any + Send> = Box::new("the frame failed");
400        let owned: Box<dyn Any + Send> = Box::new(String::from("the frame failed again"));
401        let other: Box<dyn Any + Send> = Box::new(7);
402
403        assert_eq!(format!("the frame failed ({}:{})", here.file(), here.line()), panic_message(&*text, Some(here)));
404        assert_eq!("the frame failed again", panic_message(&*owned, None));
405        assert_eq!("a panic without a message", panic_message(&*other, None));
406    }
407
408    #[test]
409    fn a_report_of_the_worker_publishes_the_kind_of_the_target() {
410        let shared = BrowserSurfaceShared::new();
411        shared.register(9601);
412        // The report arrives on the render thread.
413        std::thread::spawn(|| RenderWorker::on_render_target_registered(9601, RENDER_TARGET_KIND_WEB_GL))
414            .join()
415            .unwrap();
416        assert_eq!(Some(true), shared.uses_contexts());
417
418        // A report that is faster than the thread that created the canvas.
419        RenderWorker::on_render_target_registered(9602, RENDER_TARGET_KIND_SOFTWARE);
420        let late = BrowserSurfaceShared::new();
421        late.register(9602);
422        assert_eq!(Some(false), late.uses_contexts());
423    }
424}