use crate::interop::canvas_helper::{RENDER_TARGET_KIND_SOFTWARE, RENDER_TARGET_KIND_WEB_GL};
use crate::interop::thread_proxy;
use ferroui_base::PixelSize;
use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU32, AtomicU64, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex, MutexGuard, OnceLock, PoisonError, Weak};
pub struct BrowserSurfaceShared {
target_id: AtomicI32,
size_version: AtomicU32,
width: AtomicI32,
height: AtomicI32,
scaling: AtomicU64,
target_kind: AtomicI32,
disposed: AtomicBool,
frames: AtomicU64,
first_frame_thread: AtomicUsize,
}
const _: fn() = || {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<BrowserSurfaceShared>();
};
#[derive(Default)]
struct SurfaceRegistry {
surfaces: HashMap<i32, Weak<BrowserSurfaceShared>>,
reported: HashMap<i32, i32>,
}
static REGISTRY: OnceLock<Mutex<SurfaceRegistry>> = OnceLock::new();
fn registry() -> MutexGuard<'static, SurfaceRegistry> {
REGISTRY.get_or_init(Mutex::default).lock().unwrap_or_else(PoisonError::into_inner)
}
type FirstFrameHandler = Box<dyn FnOnce()>;
ferroui_base::ferro_thread_local! {
static FIRST_FRAME_HANDLERS: RefCell<HashMap<i32, FirstFrameHandler>> = RefCell::new(HashMap::new());
}
fn first_frame_drawn(target_id: i32) {
let handler = FIRST_FRAME_HANDLERS.with(|handlers| handlers.borrow_mut().remove(&target_id));
if let Some(handler) = handler {
handler();
}
}
impl BrowserSurfaceShared {
pub fn new() -> Arc<Self> {
Arc::new(Self {
target_id: AtomicI32::new(0),
size_version: AtomicU32::new(0),
width: AtomicI32::new(0),
height: AtomicI32::new(0),
scaling: AtomicU64::new(0f64.to_bits()),
target_kind: AtomicI32::new(0),
disposed: AtomicBool::new(false),
frames: AtomicU64::new(0),
first_frame_thread: AtomicUsize::new(0),
})
}
pub fn target_id(&self) -> i32 {
self.target_id.load(Ordering::SeqCst)
}
pub fn set_target_id(&self, target_id: i32) {
self.target_id.store(target_id, Ordering::SeqCst);
}
pub fn register(self: &Arc<Self>, target_id: i32) {
self.set_target_id(target_id);
let mut registry = registry();
registry.surfaces.insert(target_id, Arc::downgrade(self));
if let Some(kind) = registry.reported.remove(&target_id) {
self.set_target_kind(kind);
}
}
pub fn unregister(&self) {
let target_id = self.target_id();
let mut registry = registry();
if registry.surfaces.get(&target_id).is_some_and(|surface| std::ptr::eq(surface.as_ptr(), self)) {
registry.surfaces.remove(&target_id);
}
}
pub fn find(target_id: i32) -> Option<Arc<Self>> {
registry().surfaces.get(&target_id).and_then(Weak::upgrade)
}
pub fn report_target(target_id: i32, kind: i32) -> Option<Arc<Self>> {
assert_target_kind(kind);
let mut registry = registry();
match registry.surfaces.get(&target_id).and_then(Weak::upgrade) {
Some(surface) => {
surface.set_target_kind(kind);
Some(surface)
}
None => {
registry.surfaces.remove(&target_id);
registry.reported.insert(target_id, kind);
None
}
}
}
pub fn size(&self) -> (PixelSize, f64) {
loop {
let version = self.size_version.load(Ordering::SeqCst);
if version & 1 == 0 {
let width = self.width.load(Ordering::SeqCst);
let height = self.height.load(Ordering::SeqCst);
let scaling = f64::from_bits(self.scaling.load(Ordering::SeqCst));
if self.size_version.load(Ordering::SeqCst) == version {
return (PixelSize::new(width, height), scaling);
}
}
std::hint::spin_loop();
}
}
pub fn set_size(&self, size: PixelSize, scaling: f64) {
let mut version = self.size_version.load(Ordering::SeqCst);
loop {
if version & 1 == 1 {
std::hint::spin_loop();
version = self.size_version.load(Ordering::SeqCst);
continue;
}
match self.size_version.compare_exchange_weak(
version,
version.wrapping_add(1),
Ordering::SeqCst,
Ordering::SeqCst,
) {
Ok(_) => break,
Err(current) => version = current,
}
}
self.width.store(size.width, Ordering::SeqCst);
self.height.store(size.height, Ordering::SeqCst);
self.scaling.store(scaling.to_bits(), Ordering::SeqCst);
self.size_version.store(version.wrapping_add(2), Ordering::SeqCst);
}
pub fn on_size_changed(&self, pixel_width: f64, pixel_height: f64, dpr: f64) {
self.set_size(PixelSize::new(pixel_width as i32, pixel_height as i32), dpr);
}
pub fn set_target_kind(&self, kind: i32) {
assert_target_kind(kind);
self.target_kind.store(kind, Ordering::SeqCst);
}
pub fn target_kind(&self) -> i32 {
self.target_kind.load(Ordering::SeqCst)
}
pub fn has_target(&self) -> bool {
self.target_kind() != 0
}
pub fn uses_contexts(&self) -> Option<bool> {
match self.target_kind() {
0 => None,
kind => Some(kind == RENDER_TARGET_KIND_WEB_GL),
}
}
pub fn dispose(&self) {
self.disposed.store(true, Ordering::SeqCst);
}
pub fn is_disposed(&self) -> bool {
self.disposed.load(Ordering::SeqCst)
}
pub fn is_ready(&self) -> bool {
self.has_target() && !self.is_disposed() && self.size().0 != PixelSize::default()
}
pub fn frames(&self) -> u64 {
self.frames.load(Ordering::SeqCst)
}
pub fn on_first_frame(&self, handler: Box<dyn FnOnce()>) {
let target_id = self.target_id();
self.first_frame_thread.store(thread_proxy::current_thread(), Ordering::SeqCst);
FIRST_FRAME_HANDLERS.with(|handlers| handlers.borrow_mut().insert(target_id, handler));
if self.frames() > 0 {
first_frame_drawn(target_id);
}
}
pub fn forget_first_frame(&self) {
let target_id = self.target_id();
let handler = FIRST_FRAME_HANDLERS.with(|handlers| handlers.borrow_mut().remove(&target_id));
drop(handler);
}
pub(crate) fn frame_presented(&self) {
if self.frames.fetch_add(1, Ordering::SeqCst) != 0 {
return;
}
let target_id = self.target_id();
let thread = self.first_frame_thread.load(Ordering::SeqCst);
if thread_proxy::current_thread() == thread {
first_frame_drawn(target_id);
} else {
thread_proxy::run_on_thread(thread, Box::new(move || first_frame_drawn(target_id)));
}
}
}
fn assert_target_kind(kind: i32) {
assert!(
kind == RENDER_TARGET_KIND_WEB_GL || kind == RENDER_TARGET_KIND_SOFTWARE,
"{kind} is not a kind of render target"
);
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::Cell;
use std::rc::Rc;
#[test]
fn a_new_surface_has_nothing_and_is_not_ready() {
let shared = BrowserSurfaceShared::new();
assert_eq!(0, shared.target_id());
assert_eq!((PixelSize::default(), 0.0), shared.size());
assert!(!shared.has_target());
assert_eq!(None, shared.uses_contexts());
assert!(!shared.is_disposed());
assert!(!shared.is_ready());
}
#[test]
fn it_is_ready_with_a_target_and_a_size_until_it_is_disposed() {
let shared = BrowserSurfaceShared::new();
shared.set_target_id(3);
shared.set_target_kind(RENDER_TARGET_KIND_SOFTWARE);
assert_eq!(Some(false), shared.uses_contexts());
assert!(!shared.is_ready());
shared.on_size_changed(300.0, 180.0, 1.5);
assert_eq!(3, shared.target_id());
assert_eq!((PixelSize::new(300, 180), 1.5), shared.size());
assert!(shared.is_ready());
shared.set_target_kind(RENDER_TARGET_KIND_WEB_GL);
assert_eq!(Some(true), shared.uses_contexts());
shared.dispose();
assert!(shared.is_disposed());
assert!(!shared.is_ready());
}
#[test]
#[should_panic(expected = "is not a kind of render target")]
fn an_unknown_kind_is_refused() {
BrowserSurfaceShared::new().set_target_kind(7);
}
#[test]
fn a_registered_surface_is_found_by_its_id_while_it_is_alive() {
let shared = BrowserSurfaceShared::new();
assert!(BrowserSurfaceShared::find(9101).is_none());
shared.register(9101);
assert_eq!(9101, shared.target_id());
assert!(Arc::ptr_eq(&shared, &BrowserSurfaceShared::find(9101).expect("the surface is registered")));
let found = std::thread::spawn(|| BrowserSurfaceShared::find(9101)).join().unwrap();
assert!(Arc::ptr_eq(&shared, &found.expect("the surface is registered")));
drop(shared);
assert!(BrowserSurfaceShared::find(9101).is_none());
}
#[test]
fn a_reported_target_publishes_its_kind_in_the_surface_of_its_id() {
let shared = BrowserSurfaceShared::new();
shared.register(9201);
shared.on_size_changed(10.0, 10.0, 1.0);
assert!(!shared.is_ready());
let reported =
std::thread::spawn(|| BrowserSurfaceShared::report_target(9201, RENDER_TARGET_KIND_WEB_GL)).join().unwrap();
assert!(Arc::ptr_eq(&shared, &reported.expect("the surface is registered")));
assert_eq!(Some(true), shared.uses_contexts());
assert!(shared.is_ready());
}
#[test]
fn a_target_reported_before_its_surface_is_registered_is_kept_for_it() {
assert!(BrowserSurfaceShared::report_target(9301, RENDER_TARGET_KIND_SOFTWARE).is_none());
let shared = BrowserSurfaceShared::new();
assert!(!shared.has_target());
shared.register(9301);
assert_eq!(Some(false), shared.uses_contexts());
let next = BrowserSurfaceShared::new();
next.register(9301);
assert!(!next.has_target());
assert!(Arc::ptr_eq(&next, &BrowserSurfaceShared::find(9301).expect("the surface is registered")));
}
#[test]
fn an_unregistered_surface_is_not_found_and_takes_no_report() {
let shared = BrowserSurfaceShared::new();
shared.register(9401);
shared.unregister();
shared.unregister();
assert!(BrowserSurfaceShared::find(9401).is_none());
assert!(BrowserSurfaceShared::report_target(9401, RENDER_TARGET_KIND_WEB_GL).is_none());
assert!(!shared.has_target());
let next = BrowserSurfaceShared::new();
next.register(9401);
shared.unregister();
assert!(Arc::ptr_eq(&next, &BrowserSurfaceShared::find(9401).expect("the surface is registered")));
}
#[test]
fn the_first_frame_runs_the_handler_once() {
let shared = BrowserSurfaceShared::new();
shared.set_target_id(9801);
let runs = Rc::new(Cell::new(0));
shared.on_first_frame(Box::new({
let runs = runs.clone();
move || runs.set(runs.get() + 1)
}));
assert_eq!((0, 0), (shared.frames(), runs.get()));
shared.frame_presented();
assert_eq!((1, 1), (shared.frames(), runs.get()));
shared.frame_presented();
assert_eq!((2, 1), (shared.frames(), runs.get()));
}
#[test]
fn a_handler_given_after_the_first_frame_runs_at_once() {
let shared = BrowserSurfaceShared::new();
shared.set_target_id(9802);
shared.frame_presented();
let ran = Rc::new(Cell::new(false));
shared.on_first_frame(Box::new({
let ran = ran.clone();
move || ran.set(true)
}));
assert!(ran.get());
}
#[test]
fn a_forgotten_handler_does_not_run() {
let shared = BrowserSurfaceShared::new();
shared.set_target_id(9803);
shared.on_first_frame(Box::new(|| panic!("the view was closed before its first frame")));
shared.forget_first_frame();
shared.frame_presented();
assert_eq!(1, shared.frames());
}
#[test]
#[should_panic(expected = "is not a kind of render target")]
fn a_report_of_an_unknown_kind_is_refused() {
BrowserSurfaceShared::report_target(9501, 7);
}
#[test]
fn a_reader_on_another_thread_never_sees_half_of_a_write() {
let shared = BrowserSurfaceShared::new();
let stop = Arc::new(AtomicBool::new(false));
let reader = std::thread::spawn({
let shared = shared.clone();
let stop = stop.clone();
move || {
let mut reads = 0u32;
loop {
let (size, scaling) = shared.size();
assert_eq!(size.width, size.height);
assert_eq!(f64::from(size.width), scaling);
reads += 1;
if stop.load(Ordering::SeqCst) {
return reads;
}
}
}
});
for value in 1..=20_000 {
shared.set_size(PixelSize::new(value, value), f64::from(value));
}
stop.store(true, Ordering::SeqCst);
assert!(reader.join().unwrap() > 0);
assert_eq!((PixelSize::new(20_000, 20_000), 20_000.0), shared.size());
}
}