use super::web_render_target::{worker_started, PENDING_RENDER_THREAD};
use super::{BrowserRenderTimer, BrowserSurfaceShared, RenderStatistics};
use crate::interop::thread_proxy;
use std::any::Any;
use std::panic::{Location, PanicHookInfo};
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, Once};
pub struct RenderWorker;
struct WorkerState {
started: AtomicBool,
page_thread: AtomicUsize,
thread: AtomicUsize,
announced: AtomicBool,
}
static STATE: WorkerState = WorkerState::new();
impl WorkerState {
const fn new() -> Self {
Self {
started: AtomicBool::new(false),
page_thread: AtomicUsize::new(0),
thread: AtomicUsize::new(0),
announced: AtomicBool::new(false),
}
}
fn begin(&self, page_thread: usize) -> bool {
if self.started.swap(true, Ordering::SeqCst) {
return false;
}
self.page_thread.store(page_thread, Ordering::SeqCst);
true
}
fn abandon(&self) {
self.started.store(false, Ordering::SeqCst);
}
fn running(&self, thread: usize) {
self.thread.store(thread, Ordering::SeqCst);
}
fn exists(&self) -> bool {
self.started.load(Ordering::SeqCst)
}
fn thread(&self) -> usize {
self.thread.load(Ordering::SeqCst)
}
fn page_thread(&self) -> usize {
self.page_thread.load(Ordering::SeqCst)
}
fn canvas_thread_id(&self) -> i32 {
if !self.exists() {
return 0;
}
match self.thread() {
0 => PENDING_RENDER_THREAD,
thread => thread as i32,
}
}
fn take_announcement(&self, current_thread: usize) -> Option<i32> {
let thread = self.thread();
if thread == 0 || current_thread != self.page_thread() {
return None;
}
if self.announced.swap(true, Ordering::SeqCst) {
return None;
}
Some(thread as i32)
}
}
impl RenderWorker {
pub fn start(
timer: Option<Arc<BrowserRenderTimer>>,
on_thread: Option<Box<dyn FnOnce() + Send>>,
) -> std::io::Result<bool> {
if !STATE.begin(thread_proxy::current_thread()) {
return Ok(false);
}
let spawned = native::spawn(move || {
if let Some(on_thread) = on_thread {
on_thread();
}
if let Some(timer) = timer {
timer.start_on_this_thread();
}
STATE.running(thread_proxy::current_thread());
thread_proxy::run_on_thread(STATE.page_thread(), Box::new(Self::announce));
});
match spawned {
Ok(()) => {
Self::report_panics();
Ok(true)
}
Err(error) => {
STATE.abandon();
Err(error)
}
}
}
pub fn exists() -> bool {
STATE.exists()
}
pub fn thread_id() -> usize {
STATE.thread()
}
pub fn canvas_thread_id() -> i32 {
Self::announce();
STATE.canvas_thread_id()
}
pub fn post(work: impl FnOnce() + Send + 'static) -> bool {
thread_proxy::run_on_thread(STATE.thread(), Box::new(work))
}
pub fn on_render_target_registered(target_id: i32, kind: i32) {
BrowserSurfaceShared::report_target(target_id, kind);
}
pub fn page_thread() -> usize {
if STATE.exists() {
STATE.page_thread()
} else {
0
}
}
fn report_panics() {
static WRAPPED: Once = Once::new();
WRAPPED.call_once(|| {
let previous = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
previous(info);
Self::on_panic(info);
}));
});
}
fn on_panic(info: &PanicHookInfo<'_>) {
let thread = thread_proxy::current_thread();
if thread == 0 || thread != STATE.thread() {
return;
}
RenderStatistics::render_thread_panicked();
let message = panic_message(info.payload(), info.location());
thread_proxy::run_on_thread(
STATE.page_thread(),
Box::new(move || thread_proxy::report_render_thread_panic(&message)),
);
}
fn announce() {
if let Some(thread_id) = STATE.take_announcement(thread_proxy::current_thread()) {
worker_started(thread_id);
}
}
}
fn panic_message(payload: &(dyn Any + Send), location: Option<&Location<'_>>) -> String {
let message = payload
.downcast_ref::<&str>()
.copied()
.or_else(|| payload.downcast_ref::<String>().map(String::as_str))
.unwrap_or("a panic without a message");
match location {
Some(location) => format!("{message} ({}:{})", location.file(), location.line()),
None => message.to_string(),
}
}
#[cfg(all(target_os = "emscripten", target_feature = "atomics"))]
mod native {
use super::RenderWorker;
use crate::interop::canvas_helper::add_render_target_registered;
use crate::rendering::initialize_worker;
use std::rc::Rc;
extern "C" {
fn emscripten_runtime_keepalive_push();
}
pub(super) fn spawn(on_thread: impl FnOnce() + Send + 'static) -> std::io::Result<()> {
let spawned = std::thread::Builder::new().name("ferroui-render".to_string()).spawn(move || {
add_render_target_registered(Rc::new(RenderWorker::on_render_target_registered));
initialize_worker();
unsafe { emscripten_runtime_keepalive_push() };
on_thread();
});
spawned.map(drop)
}
}
#[cfg(not(all(target_os = "emscripten", target_feature = "atomics")))]
mod native {
pub(super) fn spawn(_on_thread: impl FnOnce() + Send + 'static) -> std::io::Result<()> {
Err(std::io::Error::new(std::io::ErrorKind::Unsupported, "the module was built without threads"))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::interop::canvas_helper::{RENDER_TARGET_KIND_SOFTWARE, RENDER_TARGET_KIND_WEB_GL};
#[test]
fn a_canvas_is_created_for_no_thread_a_pending_thread_or_the_render_thread() {
let state = WorkerState::new();
assert!(!state.exists());
assert_eq!(0, state.canvas_thread_id());
assert!(state.begin(5));
assert!(!state.begin(6));
assert!(state.exists());
assert_eq!(5, state.page_thread());
assert_eq!(0, state.thread());
assert_eq!(PENDING_RENDER_THREAD, state.canvas_thread_id());
state.running(4096);
assert_eq!(4096, state.thread());
assert_eq!(4096, state.canvas_thread_id());
}
#[test]
fn the_script_of_the_page_is_told_once_on_its_thread_when_the_thread_runs() {
let state = WorkerState::new();
assert!(state.begin(5));
assert_eq!(None, state.take_announcement(5));
state.running(4096);
assert_eq!(None, state.take_announcement(4096));
assert_eq!(Some(4096), state.take_announcement(5));
assert_eq!(None, state.take_announcement(5));
}
#[test]
fn a_thread_that_could_not_be_created_can_be_asked_for_again() {
let state = WorkerState::new();
assert!(state.begin(5));
state.abandon();
assert!(!state.exists());
assert_eq!(0, state.canvas_thread_id());
assert!(state.begin(7));
assert_eq!(7, state.page_thread());
}
#[test]
fn without_threads_there_is_no_render_thread() {
let started = RenderWorker::start(None, Some(Box::new(|| panic!("there is no thread to run this"))));
assert_eq!(std::io::ErrorKind::Unsupported, started.expect_err("the build has no threads").kind());
assert!(!RenderWorker::exists());
assert_eq!(0, RenderWorker::thread_id());
assert_eq!(0, RenderWorker::canvas_thread_id());
assert!(!RenderWorker::post(|| panic!("there is no thread to run this")));
}
#[test]
fn the_message_of_a_panic_names_what_was_said_and_where() {
let here = Location::caller();
let text: Box<dyn Any + Send> = Box::new("the frame failed");
let owned: Box<dyn Any + Send> = Box::new(String::from("the frame failed again"));
let other: Box<dyn Any + Send> = Box::new(7);
assert_eq!(format!("the frame failed ({}:{})", here.file(), here.line()), panic_message(&*text, Some(here)));
assert_eq!("the frame failed again", panic_message(&*owned, None));
assert_eq!("a panic without a message", panic_message(&*other, None));
}
#[test]
fn a_report_of_the_worker_publishes_the_kind_of_the_target() {
let shared = BrowserSurfaceShared::new();
shared.register(9601);
std::thread::spawn(|| RenderWorker::on_render_target_registered(9601, RENDER_TARGET_KIND_WEB_GL))
.join()
.unwrap();
assert_eq!(Some(true), shared.uses_contexts());
RenderWorker::on_render_target_registered(9602, RENDER_TARGET_KIND_SOFTWARE);
let late = BrowserSurfaceShared::new();
late.register(9602);
assert_eq!(Some(false), late.uses_contexts());
}
}