#![cfg_attr(target_os = "emscripten", no_main)]
use ferroui_base::platform::surfaces::{IFramebufferRenderTarget, IPlatformRenderSurface};
use ferroui_base::platform::RenderTargetSceneInfo;
use ferroui_base::rendering::composition::CompositionTransparencyLevel;
use ferroui_base::rendering::IRenderTimer;
use ferroui_base::threading::{IDispatcherImpl, IDispatcherSignal};
use ferroui_base::PixelSize;
use ferroui_browser::interop::canvas_helper::{
add_render_target_registered, add_size_changed, CanvasSurface, RENDER_TARGET_KIND_WEB_GL,
};
use ferroui_browser::interop::thread_proxy::current_thread;
use ferroui_browser::rendering::{
get_render_target, BrowserRenderSurface, BrowserRenderTimer, BrowserSurfaceShared, RenderWorker,
PENDING_RENDER_THREAD,
};
use ferroui_browser::{BrowserRenderingMode, BrowserSingleThreadedDispatcherImpl};
use ferroui_opengl::gl_consts;
use ferroui_opengl::surfaces::{try_get_gl_surface, IGlPlatformSurfaceRenderTarget};
use ferroui_opengl::IGlContext;
use std::any::TypeId;
use std::cell::{Cell, RefCell};
use std::rc::Rc;
use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU32, AtomicUsize, Ordering};
use std::sync::{Arc, Condvar, Mutex, OnceLock, PoisonError};
use std::time::{Duration, Instant};
use wasm_bindgen::prelude::*;
const COLOR: [u8; 3] = [32, 96, 192];
const ANIMATED_BLUE: u32 = 64;
const ANIMATED_BLUE_STEPS: u32 = 96;
const MARKER_COLOR: [u8; 3] = [224, 160, 32];
const MARKER_OFFSET: i32 = 20;
const MARKER_SIZE: i32 = 30;
const NO_TOP_LEVEL: i32 = 0;
const IDLE: i32 = 0;
const STARTED: i32 = 1;
const SURFACE_CREATED: i32 = 2;
const DONE: i32 = 3;
const FAILED: i32 = 4;
static STAGE: AtomicI32 = AtomicI32::new(IDLE);
static MAIN_THREAD: AtomicUsize = AtomicUsize::new(0);
static HELD_BACK: AtomicBool = AtomicBool::new(false);
static REQUESTED_MODE: AtomicI32 = AtomicI32::new(0);
static ANIMATED: AtomicBool = AtomicBool::new(false);
static TIMER: OnceLock<Arc<BrowserRenderTimer>> = OnceLock::new();
static WAKE_UP: OnceLock<Arc<dyn IDispatcherSignal>> = OnceLock::new();
static FRAMES: AtomicU32 = AtomicU32::new(0);
static DRAWN_ON: AtomicUsize = AtomicUsize::new(0);
static DRAWN_WIDTH: AtomicI32 = AtomicI32::new(0);
static DRAWN_HEIGHT: AtomicI32 = AtomicI32::new(0);
static DRAWN_COLOR: AtomicU32 = AtomicU32::new(0);
static DRAWN_MARKER: AtomicBool = AtomicBool::new(false);
static TARGET_ID: AtomicI32 = AtomicI32::new(0);
static TARGET_KIND: AtomicI32 = AtomicI32::new(0);
static GL_MAJOR_VERSION: AtomicI32 = AtomicI32::new(0);
static GL_ERROR: AtomicI32 = AtomicI32::new(0);
static FAILURE: OnceLock<String> = OnceLock::new();
static FRAME_COUNT: Mutex<u64> = Mutex::new(0);
static FRAME_DRAWN: Condvar = Condvar::new();
#[wasm_bindgen]
extern "C" {
type Reporter;
#[wasm_bindgen(method)]
fn frames(this: &Reporter, frames: u32, wake_ups: u32);
}
enum Painter {
WebGl {
_context: Rc<dyn IGlContext>,
render_target: Rc<dyn IGlPlatformSurfaceRenderTarget>,
marker: Option<i32>,
},
Software { render_target: Rc<dyn IFramebufferRenderTarget> },
}
struct Canvas {
shared: Arc<BrowserSurfaceShared>,
painter: Painter,
}
ferroui_base::ferro_thread_local! {
static SURFACE: RefCell<Option<CanvasSurface>> = const { RefCell::new(None) };
static SHARED: RefCell<Option<Arc<BrowserSurfaceShared>>> = const { RefCell::new(None) };
static DISPATCHER: RefCell<Option<Rc<BrowserSingleThreadedDispatcherImpl>>> = const { RefCell::new(None) };
static REPORTER: RefCell<Option<Reporter>> = const { RefCell::new(None) };
static WAKE_UPS: Cell<u32> = const { Cell::new(0) };
static CANVAS: RefCell<Option<Canvas>> = const { RefCell::new(None) };
}
fn fail(message: String) {
let _ = FAILURE.set(message);
STAGE.store(FAILED, Ordering::SeqCst);
}
fn timer() -> &'static Arc<BrowserRenderTimer> {
TIMER.get_or_init(|| BrowserRenderTimer::new(true))
}
fn frame_color(frame: u32) -> [u8; 3] {
if ANIMATED.load(Ordering::SeqCst) {
[COLOR[0], COLOR[1], (ANIMATED_BLUE + frame % ANIMATED_BLUE_STEPS) as u8]
} else {
COLOR
}
}
#[wasm_bindgen(js_name = renderWorkerClearStart)]
pub fn render_worker_clear_start(width: i32, height: i32, scaling: f64, mode: i32, animated: bool, reporter: JsValue) {
if STAGE.compare_exchange(IDLE, STARTED, Ordering::SeqCst, Ordering::SeqCst).is_err() {
return;
}
MAIN_THREAD.store(current_thread(), Ordering::SeqCst);
REQUESTED_MODE.store(mode, Ordering::SeqCst);
ANIMATED.store(animated, Ordering::SeqCst);
let shared = BrowserSurfaceShared::new();
shared.set_size(PixelSize::new(width, height), scaling);
SHARED.with(|kept| *kept.borrow_mut() = Some(shared));
add_size_changed(Rc::new(|top_level_id, pixel_width, pixel_height, dpr| {
if top_level_id == NO_TOP_LEVEL {
SHARED.with(|shared| {
if let Some(shared) = shared.borrow().as_ref() {
shared.on_size_changed(pixel_width, pixel_height, dpr);
}
});
}
}));
let dispatcher = BrowserSingleThreadedDispatcherImpl::new();
dispatcher.signaled().add(Rc::new(|()| report_frames()));
let _ = WAKE_UP.set(dispatcher.signal_handle());
DISPATCHER.with(|kept| *kept.borrow_mut() = Some(dispatcher));
if !reporter.is_undefined() && !reporter.is_null() {
REPORTER.with(|kept| *kept.borrow_mut() = Some(reporter.unchecked_into::<Reporter>()));
}
let frame_loop = animated.then(|| timer().clone());
let started = RenderWorker::start(
frame_loop,
Some(Box::new(move || {
add_render_target_registered(Rc::new(on_render_target_registered));
if animated {
timer().set_tick(Some(Arc::new(|_time: Duration| draw_next_frame())));
}
})),
);
if let Err(error) = started {
fail(format!("the thread could not start: {error}"));
}
}
#[wasm_bindgen(js_name = renderWorkerClearCreateSurface)]
pub fn render_worker_clear_create_surface(container: JsValue) -> i32 {
if STAGE.compare_exchange(STARTED, SURFACE_CREATED, Ordering::SeqCst, Ordering::SeqCst).is_err() {
return 0;
}
let requested = REQUESTED_MODE.load(Ordering::SeqCst);
let modes = if requested == 0 {
vec![BrowserRenderingMode::WebGL2 as i32, BrowserRenderingMode::WebGL1 as i32]
} else {
vec![requested]
};
let thread = RenderWorker::canvas_thread_id();
HELD_BACK.store(thread == PENDING_RENDER_THREAD, Ordering::SeqCst);
let surface = CanvasSurface::create_render_target_surface(&container, &modes, NO_TOP_LEVEL, thread);
let target_id = surface.target_id();
SHARED.with(|shared| {
if let Some(shared) = shared.borrow().as_ref() {
shared.register(target_id);
}
});
SURFACE.with(|kept| *kept.borrow_mut() = Some(surface));
RenderWorker::post(move || begin_drawing(target_id));
target_id
}
#[wasm_bindgen(js_name = renderWorkerClearHeldBack)]
pub fn render_worker_clear_held_back() -> bool {
HELD_BACK.load(Ordering::SeqCst)
}
fn on_render_target_registered(target_id: i32, kind: i32) {
TARGET_ID.store(target_id, Ordering::SeqCst);
TARGET_KIND.store(kind, Ordering::SeqCst);
begin_drawing(target_id);
}
fn begin_drawing(target_id: i32) {
if CANVAS.with(|canvas| canvas.borrow().is_some()) {
return;
}
let Some(shared) = BrowserSurfaceShared::find(target_id) else { return };
if !shared.has_target() {
return;
}
let kind = shared.target_kind();
let software_asked = REQUESTED_MODE.load(Ordering::SeqCst) == BrowserRenderingMode::Software2D as i32;
if kind != RENDER_TARGET_KIND_WEB_GL && !software_asked {
fail("the worker created a software render target, not a WebGL one".to_string());
return;
}
let Some(target) = get_render_target(target_id) else {
fail(format!("the registry of the worker has no render target {target_id}"));
return;
};
let surface = BrowserRenderSurface::new(shared.clone());
if !surface.is_ready() {
fail("the render surface is not ready although its target was reported".to_string());
return;
}
let painter = if kind == RENDER_TARGET_KIND_WEB_GL {
let context = target
.platform_graphics_context()
.and_then(|context| context.try_get_feature(TypeId::of::<dyn IGlContext>()))
.and_then(|feature| feature.downcast_ref::<Rc<dyn IGlContext>>().cloned());
let Some(context) = context else {
fail("the render target has no OpenGL context".to_string());
return;
};
let Some(gl_surface) = try_get_gl_surface(&*surface) else {
fail("the render target is not an OpenGL surface".to_string());
return;
};
GL_MAJOR_VERSION.store(context.version().major(), Ordering::SeqCst);
let marker = create_marker(&context);
Painter::WebGl { render_target: gl_surface.create_gl_render_target(&context), _context: context, marker }
} else {
let Some(framebuffer_surface) = surface.as_framebuffer_surface() else {
fail("the render target is not a framebuffer surface".to_string());
return;
};
Painter::Software { render_target: framebuffer_surface.create_framebuffer_render_target() }
};
CANVAS.with(|canvas| *canvas.borrow_mut() = Some(Canvas { shared, painter }));
draw_next_frame();
if FRAMES.load(Ordering::SeqCst) == 0 {
fail("the first frame was not drawn: the canvas has no size".to_string());
return;
}
let _ = STAGE.compare_exchange(SURFACE_CREATED, DONE, Ordering::SeqCst, Ordering::SeqCst);
}
fn create_marker(context: &Rc<dyn IGlContext>) -> Option<i32> {
let gl = context.gl_interface();
if !gl.is_blit_framebuffer_available() {
return None;
}
let restore = context.make_current();
let framebuffer = gl.gen_framebuffer();
let renderbuffer = gl.gen_renderbuffer();
gl.bind_renderbuffer(gl_consts::GL_RENDERBUFFER, renderbuffer);
gl.renderbuffer_storage(gl_consts::GL_RENDERBUFFER, gl_consts::GL_RGBA8, MARKER_SIZE, MARKER_SIZE);
gl.bind_framebuffer(gl_consts::GL_FRAMEBUFFER, framebuffer);
gl.framebuffer_renderbuffer(
gl_consts::GL_FRAMEBUFFER,
gl_consts::GL_COLOR_ATTACHMENT0,
gl_consts::GL_RENDERBUFFER,
renderbuffer,
);
let complete = gl.check_framebuffer_status(gl_consts::GL_FRAMEBUFFER) == gl_consts::GL_FRAMEBUFFER_COMPLETE;
if complete {
gl.viewport(0, 0, MARKER_SIZE, MARKER_SIZE);
clear(&gl, MARKER_COLOR);
}
restore.dispose();
complete.then_some(framebuffer)
}
fn clear(gl: &ferroui_opengl::GlInterface, color: [u8; 3]) {
gl.clear_color(f32::from(color[0]) / 255.0, f32::from(color[1]) / 255.0, f32::from(color[2]) / 255.0, 1.0);
gl.clear(gl_consts::GL_COLOR_BUFFER_BIT);
}
fn draw_next_frame() {
let drawn = CANVAS.with(|canvas| canvas.borrow().as_ref().is_some_and(draw));
if !drawn {
return;
}
FRAMES.fetch_add(1, Ordering::SeqCst);
DRAWN_ON.store(current_thread(), Ordering::SeqCst);
*FRAME_COUNT.lock().unwrap_or_else(PoisonError::into_inner) += 1;
FRAME_DRAWN.notify_all();
if let Some(wake_up) = WAKE_UP.get() {
wake_up.signal();
}
}
fn draw(canvas: &Canvas) -> bool {
let (size, scaling) = canvas.shared.size();
if size.width <= 0 || size.height <= 0 {
return false;
}
let color = frame_color(FRAMES.load(Ordering::SeqCst));
let scene_info = RenderTargetSceneInfo::new(size, scaling, CompositionTransparencyLevel::None);
let (drawn_size, has_marker) = match &canvas.painter {
Painter::WebGl { render_target, marker, .. } => {
let session = render_target.begin_draw(&scene_info);
let size = session.size();
let gl = session.context().gl_interface();
gl.viewport(0, 0, size.width, size.height);
clear(&gl, color);
if let Some(marker) = marker {
let top = size.height - MARKER_OFFSET;
gl.bind_framebuffer(gl_consts::GL_READ_FRAMEBUFFER, *marker);
gl.blit_framebuffer(
0,
0,
MARKER_SIZE,
MARKER_SIZE,
MARKER_OFFSET,
top - MARKER_SIZE,
MARKER_OFFSET + MARKER_SIZE,
top,
gl_consts::GL_COLOR_BUFFER_BIT,
gl_consts::GL_NEAREST,
);
}
let error = gl.get_error();
gl.flush();
session.dispose();
if error != 0 {
let _ = GL_ERROR.compare_exchange(0, error, Ordering::SeqCst, Ordering::SeqCst);
}
(size, marker.is_some())
}
Painter::Software { render_target } => {
let (framebuffer, _) = render_target.lock(&scene_info);
let size = framebuffer.size();
let row_bytes = framebuffer.row_bytes() as usize;
let marker = MARKER_OFFSET..MARKER_OFFSET + MARKER_SIZE;
framebuffer.with_data(&mut |data: &mut [u8]| {
for (y, row) in data.chunks_exact_mut(row_bytes).enumerate() {
let in_marker_rows = marker.contains(&(y as i32));
for (x, pixel) in row.chunks_exact_mut(4).take(size.width as usize).enumerate() {
let color = if in_marker_rows && marker.contains(&(x as i32)) { MARKER_COLOR } else { color };
pixel.copy_from_slice(&[color[0], color[1], color[2], 255]);
}
}
});
framebuffer.dispose();
(size, true)
}
};
DRAWN_WIDTH.store(drawn_size.width, Ordering::SeqCst);
DRAWN_HEIGHT.store(drawn_size.height, Ordering::SeqCst);
DRAWN_COLOR.store((u32::from(color[0]) << 16) | (u32::from(color[1]) << 8) | u32::from(color[2]), Ordering::SeqCst);
DRAWN_MARKER.store(has_marker, Ordering::SeqCst);
true
}
fn report_frames() {
let wake_ups = WAKE_UPS.with(|wake_ups| {
wake_ups.set(wake_ups.get() + 1);
wake_ups.get()
});
REPORTER.with(|reporter| {
if let Some(reporter) = reporter.borrow().as_ref() {
reporter.frames(FRAMES.load(Ordering::SeqCst), wake_ups);
}
});
}
#[wasm_bindgen(js_name = renderWorkerClearFrames)]
pub fn render_worker_clear_frames() -> u32 {
FRAMES.load(Ordering::SeqCst)
}
#[wasm_bindgen(js_name = renderWorkerClearWaitForFrame)]
pub fn render_worker_clear_wait_for_frame(out_of_turn: bool, timeout_ms: u32) -> String {
let frames_before = FRAMES.load(Ordering::SeqCst);
let start = Instant::now();
let count = FRAME_COUNT.lock().unwrap_or_else(PoisonError::into_inner);
let seen = *count;
if out_of_turn {
timer().request_tick_out_of_turn();
}
let (count, outcome) = FRAME_DRAWN
.wait_timeout_while(count, Duration::from_millis(u64::from(timeout_ms)), |count| *count == seen)
.unwrap_or_else(PoisonError::into_inner);
drop(count);
let duration_ms = start.elapsed().as_secs_f64() * 1000.0;
format!(
"ended={} duration_ms={duration_ms:.3} out_of_turn={out_of_turn} frames_before={frames_before} frames_after={}",
!outcome.timed_out(),
FRAMES.load(Ordering::SeqCst),
)
}
#[wasm_bindgen(js_name = renderWorkerClearState)]
pub fn render_worker_clear_state() -> String {
let atomics = cfg!(target_feature = "atomics");
let thread = RenderWorker::thread_id();
match STAGE.load(Ordering::SeqCst) {
IDLE => format!("atomics={atomics} state=idle"),
STARTED if thread == 0 => format!("atomics={atomics} state=starting"),
STARTED => format!("atomics={atomics} state=worker_ready thread={thread}"),
SURFACE_CREATED => format!("atomics={atomics} state=surface_created thread={thread}"),
DONE => {
let drawn_on = DRAWN_ON.load(Ordering::SeqCst);
let other_thread = drawn_on != 0 && drawn_on != MAIN_THREAD.load(Ordering::SeqCst);
let kind =
if TARGET_KIND.load(Ordering::SeqCst) == RENDER_TARGET_KIND_WEB_GL { "webgl" } else { "software" };
let color = DRAWN_COLOR.load(Ordering::SeqCst);
format!(
"atomics={atomics} state=done thread={thread} other_thread={other_thread} target={} kind={kind} gl={} gl_error={} size={}x{} color={},{},{} frames={} marker={}",
TARGET_ID.load(Ordering::SeqCst),
GL_MAJOR_VERSION.load(Ordering::SeqCst),
GL_ERROR.load(Ordering::SeqCst),
DRAWN_WIDTH.load(Ordering::SeqCst),
DRAWN_HEIGHT.load(Ordering::SeqCst),
(color >> 16) & 0xff,
(color >> 8) & 0xff,
color & 0xff,
FRAMES.load(Ordering::SeqCst),
DRAWN_MARKER.load(Ordering::SeqCst),
)
}
_ => format!(
"atomics={atomics} state=failed error={}",
FAILURE.get().map_or("unknown", |message| message.as_str())
),
}
}
#[cfg(not(target_os = "emscripten"))]
fn main() {
println!(
"render_worker_clear runs in a browser: build it with scripts/build-browser.sh render_worker_clear --threads"
);
}