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}