Skip to main content

ferroui_browser/rendering/
browser_surface_shared.rs

1use crate::interop::canvas_helper::{RENDER_TARGET_KIND_SOFTWARE, RENDER_TARGET_KIND_WEB_GL};
2use crate::interop::thread_proxy;
3use ferroui_base::PixelSize;
4use std::cell::RefCell;
5use std::collections::HashMap;
6use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU32, AtomicU64, AtomicUsize, Ordering};
7use std::sync::{Arc, Mutex, MutexGuard, OnceLock, PoisonError, Weak};
8
9/// What the thread of the user interface and the thread that renders both
10/// know about one canvas.
11///
12/// Not from upstream, where the surface of a view is one object that both
13/// threads read by convention. With a render worker the canvas itself exists
14/// in the worker only, and its element only in the page; this object is what
15/// is left between them (`docs/porting/browser-render-worker.md`, section
16/// 4):
17///
18/// - the id of the render target, written once by the thread that created
19///   the canvas;
20/// - the size of the canvas in device pixels and its scaling, written by the
21///   thread of the user interface when the element changes, read by the
22///   render thread before each frame, which sets the size of the canvas it
23///   draws to;
24/// - the kind of the render target, written by the thread that owns the
25///   target: the thread of the page for a canvas it kept, the render thread
26///   when its worker has created the target of a canvas that was transferred
27///   to it;
28/// - whether the view is disposed;
29/// - how many frames were drawn to the canvas, counted by the thread that
30///   draws them, and the thread that is told of the first one
31///   ([`on_first_frame`](Self::on_first_frame)).
32///
33/// A canvas is found by the id of its render target
34/// ([`register`](Self::register), [`find`](Self::find),
35/// [`report_target`](Self::report_target)): a worker reports a target by its
36/// id, on a thread that has never seen the surface.
37///
38/// Everything is an atomic, so neither thread ever waits for the other here:
39/// the thread of the page may not block, and a lock that the render thread
40/// holds for a moment would make it spin. The size and the scaling are read
41/// as one value (a frame never sees a new size with an old scaling) through
42/// a version counter that is odd while they are being written.
43pub struct BrowserSurfaceShared {
44    target_id: AtomicI32,
45    size_version: AtomicU32,
46    width: AtomicI32,
47    height: AtomicI32,
48    scaling: AtomicU64,
49    target_kind: AtomicI32,
50    disposed: AtomicBool,
51    frames: AtomicU64,
52    /// The thread that asked for the first frame
53    /// ([`on_first_frame`](Self::on_first_frame)), as
54    /// [`thread_proxy::current_thread`] names it there.
55    first_frame_thread: AtomicUsize,
56}
57
58const _: fn() = || {
59    fn assert_send_sync<T: Send + Sync>() {}
60    assert_send_sync::<BrowserSurfaceShared>();
61};
62
63/// The canvases of the module by the id of their render target, and the
64/// targets that were reported before their canvas was registered.
65///
66/// One table for all threads, behind a lock, unlike the rest of this file:
67/// it is entered when a canvas is created or closed and when a worker
68/// reports a target, never during a frame, and for a look-up in a map.
69#[derive(Default)]
70struct SurfaceRegistry {
71    surfaces: HashMap<i32, Weak<BrowserSurfaceShared>>,
72    /// The kind of each target a worker reported under an id no canvas is
73    /// registered with yet. The worker can be faster than the thread that
74    /// created the canvas: the id is known to that thread only when the call
75    /// that posted the canvas to the worker returns.
76    reported: HashMap<i32, i32>,
77}
78
79static REGISTRY: OnceLock<Mutex<SurfaceRegistry>> = OnceLock::new();
80
81fn registry() -> MutexGuard<'static, SurfaceRegistry> {
82    REGISTRY.get_or_init(Mutex::default).lock().unwrap_or_else(PoisonError::into_inner)
83}
84
85/// What a thread runs when the first frame of a canvas has been drawn.
86type FirstFrameHandler = Box<dyn FnOnce()>;
87
88ferroui_base::ferro_thread_local! {
89    /// What the calling thread runs when the first frame of a canvas has
90    /// been drawn, by the id of the render target of the canvas
91    /// ([`BrowserSurfaceShared::on_first_frame`]). A handler holds objects
92    /// of the page, so it stays with the thread that gave it: only the id
93    /// crosses to the thread that draws and back.
94    static FIRST_FRAME_HANDLERS: RefCell<HashMap<i32, FirstFrameHandler>> = RefCell::new(HashMap::new());
95}
96
97/// Runs what the calling thread wanted done at the first frame of the
98/// canvas with the render target `target_id`, when it still waits.
99fn first_frame_drawn(target_id: i32) {
100    let handler = FIRST_FRAME_HANDLERS.with(|handlers| handlers.borrow_mut().remove(&target_id));
101    if let Some(handler) = handler {
102        handler();
103    }
104}
105
106impl BrowserSurfaceShared {
107    /// Creates the state of a canvas that has no render target, no id and no
108    /// size yet.
109    pub fn new() -> Arc<Self> {
110        Arc::new(Self {
111            target_id: AtomicI32::new(0),
112            size_version: AtomicU32::new(0),
113            width: AtomicI32::new(0),
114            height: AtomicI32::new(0),
115            scaling: AtomicU64::new(0f64.to_bits()),
116            target_kind: AtomicI32::new(0),
117            disposed: AtomicBool::new(false),
118            frames: AtomicU64::new(0),
119            first_frame_thread: AtomicUsize::new(0),
120        })
121    }
122
123    /// The id of the render target in the registry of the script side; 0
124    /// while the canvas has not been created.
125    pub fn target_id(&self) -> i32 {
126        self.target_id.load(Ordering::SeqCst)
127    }
128
129    /// Records the id of the render target. Called by the thread that
130    /// created the canvas, once it has.
131    pub fn set_target_id(&self, target_id: i32) {
132        self.target_id.store(target_id, Ordering::SeqCst);
133    }
134
135    /// Records the id of the render target and makes the canvas known under
136    /// it to every thread ([`find`](Self::find)). Called by the thread that
137    /// created the canvas, once it has. When a worker has already reported
138    /// the target of that id, its kind is published here.
139    ///
140    /// The table holds the canvas weakly; a canvas that is registered again
141    /// under an id takes the place of the one before.
142    pub fn register(self: &Arc<Self>, target_id: i32) {
143        self.set_target_id(target_id);
144        let mut registry = registry();
145        registry.surfaces.insert(target_id, Arc::downgrade(self));
146        if let Some(kind) = registry.reported.remove(&target_id) {
147            self.set_target_kind(kind);
148        }
149    }
150
151    /// Takes the canvas out of the table of [`register`](Self::register): a
152    /// target reported under its id from here on belongs to no canvas.
153    /// Called when the view is closed. Does nothing for a canvas that is not
154    /// registered, and leaves alone a canvas that took its id since.
155    pub fn unregister(&self) {
156        let target_id = self.target_id();
157        let mut registry = registry();
158        if registry.surfaces.get(&target_id).is_some_and(|surface| std::ptr::eq(surface.as_ptr(), self)) {
159            registry.surfaces.remove(&target_id);
160        }
161    }
162
163    /// The canvas that is registered under the id of a render target and
164    /// still alive. Any thread may ask.
165    pub fn find(target_id: i32) -> Option<Arc<Self>> {
166        registry().surfaces.get(&target_id).and_then(Weak::upgrade)
167    }
168
169    /// The render target `target_id` exists and is of `kind`
170    /// ([`RENDER_TARGET_KIND_WEB_GL`] or [`RENDER_TARGET_KIND_SOFTWARE`]):
171    /// publishes the kind in the canvas registered under the id and returns
172    /// the canvas. Called by the thread whose worker created the target.
173    ///
174    /// When no canvas is registered under the id yet, the kind is kept for
175    /// the one that will be ([`register`](Self::register)) and the answer is
176    /// `None`.
177    ///
178    /// # Panics
179    /// Panics when `kind` is neither.
180    pub fn report_target(target_id: i32, kind: i32) -> Option<Arc<Self>> {
181        assert_target_kind(kind);
182        let mut registry = registry();
183        match registry.surfaces.get(&target_id).and_then(Weak::upgrade) {
184            Some(surface) => {
185                surface.set_target_kind(kind);
186                Some(surface)
187            }
188            None => {
189                registry.surfaces.remove(&target_id);
190                registry.reported.insert(target_id, kind);
191                None
192            }
193        }
194    }
195
196    /// The size of the canvas in device pixels and its scaling, as one
197    /// value. Never waits: while a write is in progress it reads again.
198    pub fn size(&self) -> (PixelSize, f64) {
199        loop {
200            let version = self.size_version.load(Ordering::SeqCst);
201            if version & 1 == 0 {
202                let width = self.width.load(Ordering::SeqCst);
203                let height = self.height.load(Ordering::SeqCst);
204                let scaling = f64::from_bits(self.scaling.load(Ordering::SeqCst));
205                if self.size_version.load(Ordering::SeqCst) == version {
206                    return (PixelSize::new(width, height), scaling);
207                }
208            }
209            std::hint::spin_loop();
210        }
211    }
212
213    /// Records the size of the canvas in device pixels and its scaling.
214    /// Called by the thread of the user interface.
215    pub fn set_size(&self, size: PixelSize, scaling: f64) {
216        // The version is made odd for the duration of the write. One thread
217        // writes in practice; a second writer waits for the first here, for
218        // the three stores below.
219        let mut version = self.size_version.load(Ordering::SeqCst);
220        loop {
221            if version & 1 == 1 {
222                std::hint::spin_loop();
223                version = self.size_version.load(Ordering::SeqCst);
224                continue;
225            }
226            match self.size_version.compare_exchange_weak(
227                version,
228                version.wrapping_add(1),
229                Ordering::SeqCst,
230                Ordering::SeqCst,
231            ) {
232                Ok(_) => break,
233                Err(current) => version = current,
234            }
235        }
236        self.width.store(size.width, Ordering::SeqCst);
237        self.height.store(size.height, Ordering::SeqCst);
238        self.scaling.store(scaling.to_bits(), Ordering::SeqCst);
239        self.size_version.store(version.wrapping_add(2), Ordering::SeqCst);
240    }
241
242    /// Records what the observer of the canvas element reports:
243    /// `pixel_width` and `pixel_height` in device pixels and the device
244    /// pixel ratio, as
245    /// [`on_size_changed`](crate::interop::canvas_helper::on_size_changed)
246    /// receives them.
247    pub fn on_size_changed(&self, pixel_width: f64, pixel_height: f64, dpr: f64) {
248        self.set_size(PixelSize::new(pixel_width as i32, pixel_height as i32), dpr);
249    }
250
251    /// Records that the render target exists and its kind
252    /// ([`RENDER_TARGET_KIND_WEB_GL`] or [`RENDER_TARGET_KIND_SOFTWARE`]).
253    /// Called by the thread that owns the target: the thread that created
254    /// the canvas when it kept it, the render thread when its worker reports
255    /// the target ([`report_target`](Self::report_target)).
256    ///
257    /// # Panics
258    /// Panics when `kind` is neither.
259    pub fn set_target_kind(&self, kind: i32) {
260        assert_target_kind(kind);
261        self.target_kind.store(kind, Ordering::SeqCst);
262    }
263
264    /// The kind of the render target; 0 while there is none.
265    pub fn target_kind(&self) -> i32 {
266        self.target_kind.load(Ordering::SeqCst)
267    }
268
269    /// Whether the render target exists.
270    pub fn has_target(&self) -> bool {
271        self.target_kind() != 0
272    }
273
274    /// Whether the target renders with a graphics context (WebGL); `None`
275    /// while there is no target.
276    pub fn uses_contexts(&self) -> Option<bool> {
277        match self.target_kind() {
278            0 => None,
279            kind => Some(kind == RENDER_TARGET_KIND_WEB_GL),
280        }
281    }
282
283    /// Marks the view as disposed: nothing renders to its canvas any more.
284    pub fn dispose(&self) {
285        self.disposed.store(true, Ordering::SeqCst);
286    }
287
288    /// Whether the view is disposed.
289    pub fn is_disposed(&self) -> bool {
290        self.disposed.load(Ordering::SeqCst)
291    }
292
293    /// Whether a frame can be rendered: the target exists, the canvas has a
294    /// size, and the view is not disposed. Either thread may ask.
295    pub fn is_ready(&self) -> bool {
296        self.has_target() && !self.is_disposed() && self.size().0 != PixelSize::default()
297    }
298
299    /// The frames that were drawn to the canvas so far. Any thread may ask.
300    pub fn frames(&self) -> u64 {
301        self.frames.load(Ordering::SeqCst)
302    }
303
304    /// Runs `handler` once, on the calling thread, when the first frame has
305    /// been drawn to the canvas; at once when it already has. Called by the
306    /// thread that created the canvas, after the canvas has its id
307    /// ([`register`](Self::register)); a second handler takes the place of
308    /// one that still waits.
309    ///
310    /// Not from upstream, which has no such notification: its view closes
311    /// the splash screen of the page at the first animation frame of the
312    /// top-level, on the thread that also draws. That is before the frame
313    /// is drawn, and with a render thread the frame is drawn by another
314    /// thread, which may be seconds away
315    /// (`docs/porting/browser-render-worker.md`, "B3 follow-ups").
316    ///
317    /// When this thread draws the canvas itself the handler runs inside the
318    /// frame, right after it was drawn: it must not call into the view. When
319    /// a render thread draws, that thread queues a call for this one
320    /// ([`thread_proxy::run_on_thread`]) and the handler runs from the event
321    /// loop of this thread; nothing is polled and nobody waits. A frame is
322    /// only drawn while the page is visible and the canvas has a size, so
323    /// the handler of a view that never draws never runs.
324    pub fn on_first_frame(&self, handler: Box<dyn FnOnce()>) {
325        let target_id = self.target_id();
326        // Before the count is read: a thread that draws the first frame
327        // after that reads the thread to tell.
328        self.first_frame_thread.store(thread_proxy::current_thread(), Ordering::SeqCst);
329        FIRST_FRAME_HANDLERS.with(|handlers| handlers.borrow_mut().insert(target_id, handler));
330        if self.frames() > 0 {
331            first_frame_drawn(target_id);
332        }
333    }
334
335    /// Drops the handler of [`on_first_frame`](Self::on_first_frame) when it
336    /// still waits. Called by the thread that gave it, when the view is
337    /// closed.
338    pub fn forget_first_frame(&self) {
339        let target_id = self.target_id();
340        let handler = FIRST_FRAME_HANDLERS.with(|handlers| handlers.borrow_mut().remove(&target_id));
341        // Outside the table: what the handler holds may be dropped with it.
342        drop(handler);
343    }
344
345    /// A frame was drawn to the canvas by the calling thread. The first one
346    /// is made known to the thread that asked for it
347    /// ([`on_first_frame`](Self::on_first_frame)): directly when that is
348    /// this thread, through its event loop otherwise. When that call cannot
349    /// be queued, the handler does not run.
350    pub(crate) fn frame_presented(&self) {
351        if self.frames.fetch_add(1, Ordering::SeqCst) != 0 {
352            return;
353        }
354        let target_id = self.target_id();
355        let thread = self.first_frame_thread.load(Ordering::SeqCst);
356        if thread_proxy::current_thread() == thread {
357            first_frame_drawn(target_id);
358        } else {
359            thread_proxy::run_on_thread(thread, Box::new(move || first_frame_drawn(target_id)));
360        }
361    }
362}
363
364fn assert_target_kind(kind: i32) {
365    assert!(
366        kind == RENDER_TARGET_KIND_WEB_GL || kind == RENDER_TARGET_KIND_SOFTWARE,
367        "{kind} is not a kind of render target"
368    );
369}
370
371#[cfg(test)]
372mod tests {
373    use super::*;
374    use std::cell::Cell;
375    use std::rc::Rc;
376
377    #[test]
378    fn a_new_surface_has_nothing_and_is_not_ready() {
379        let shared = BrowserSurfaceShared::new();
380
381        assert_eq!(0, shared.target_id());
382        assert_eq!((PixelSize::default(), 0.0), shared.size());
383        assert!(!shared.has_target());
384        assert_eq!(None, shared.uses_contexts());
385        assert!(!shared.is_disposed());
386        assert!(!shared.is_ready());
387    }
388
389    #[test]
390    fn it_is_ready_with_a_target_and_a_size_until_it_is_disposed() {
391        let shared = BrowserSurfaceShared::new();
392        shared.set_target_id(3);
393        shared.set_target_kind(RENDER_TARGET_KIND_SOFTWARE);
394        assert_eq!(Some(false), shared.uses_contexts());
395        assert!(!shared.is_ready());
396
397        shared.on_size_changed(300.0, 180.0, 1.5);
398        assert_eq!(3, shared.target_id());
399        assert_eq!((PixelSize::new(300, 180), 1.5), shared.size());
400        assert!(shared.is_ready());
401
402        shared.set_target_kind(RENDER_TARGET_KIND_WEB_GL);
403        assert_eq!(Some(true), shared.uses_contexts());
404
405        shared.dispose();
406        assert!(shared.is_disposed());
407        assert!(!shared.is_ready());
408    }
409
410    #[test]
411    #[should_panic(expected = "is not a kind of render target")]
412    fn an_unknown_kind_is_refused() {
413        BrowserSurfaceShared::new().set_target_kind(7);
414    }
415
416    // The table of the canvases is one for the whole process, and the tests
417    // run side by side: each test below has ids of its own.
418
419    #[test]
420    fn a_registered_surface_is_found_by_its_id_while_it_is_alive() {
421        let shared = BrowserSurfaceShared::new();
422        assert!(BrowserSurfaceShared::find(9101).is_none());
423
424        shared.register(9101);
425        assert_eq!(9101, shared.target_id());
426        assert!(Arc::ptr_eq(&shared, &BrowserSurfaceShared::find(9101).expect("the surface is registered")));
427        // Another thread finds the same object.
428        let found = std::thread::spawn(|| BrowserSurfaceShared::find(9101)).join().unwrap();
429        assert!(Arc::ptr_eq(&shared, &found.expect("the surface is registered")));
430
431        // The table does not keep the surface alive.
432        drop(shared);
433        assert!(BrowserSurfaceShared::find(9101).is_none());
434    }
435
436    #[test]
437    fn a_reported_target_publishes_its_kind_in_the_surface_of_its_id() {
438        let shared = BrowserSurfaceShared::new();
439        shared.register(9201);
440        shared.on_size_changed(10.0, 10.0, 1.0);
441        assert!(!shared.is_ready());
442
443        // The report arrives on the thread of the worker.
444        let reported =
445            std::thread::spawn(|| BrowserSurfaceShared::report_target(9201, RENDER_TARGET_KIND_WEB_GL)).join().unwrap();
446
447        assert!(Arc::ptr_eq(&shared, &reported.expect("the surface is registered")));
448        assert_eq!(Some(true), shared.uses_contexts());
449        assert!(shared.is_ready());
450    }
451
452    #[test]
453    fn a_target_reported_before_its_surface_is_registered_is_kept_for_it() {
454        // The worker was faster than the thread that created the canvas.
455        assert!(BrowserSurfaceShared::report_target(9301, RENDER_TARGET_KIND_SOFTWARE).is_none());
456
457        let shared = BrowserSurfaceShared::new();
458        assert!(!shared.has_target());
459        shared.register(9301);
460        assert_eq!(Some(false), shared.uses_contexts());
461
462        // The report was for one surface: the next one under the id starts
463        // without a target.
464        let next = BrowserSurfaceShared::new();
465        next.register(9301);
466        assert!(!next.has_target());
467        assert!(Arc::ptr_eq(&next, &BrowserSurfaceShared::find(9301).expect("the surface is registered")));
468    }
469
470    #[test]
471    fn an_unregistered_surface_is_not_found_and_takes_no_report() {
472        let shared = BrowserSurfaceShared::new();
473        shared.register(9401);
474        shared.unregister();
475        shared.unregister();
476
477        assert!(BrowserSurfaceShared::find(9401).is_none());
478        assert!(BrowserSurfaceShared::report_target(9401, RENDER_TARGET_KIND_WEB_GL).is_none());
479        assert!(!shared.has_target());
480
481        // A surface that took the id is not taken out by the one before.
482        let next = BrowserSurfaceShared::new();
483        next.register(9401);
484        shared.unregister();
485        assert!(Arc::ptr_eq(&next, &BrowserSurfaceShared::find(9401).expect("the surface is registered")));
486    }
487
488    #[test]
489    fn the_first_frame_runs_the_handler_once() {
490        let shared = BrowserSurfaceShared::new();
491        shared.set_target_id(9801);
492        let runs = Rc::new(Cell::new(0));
493        shared.on_first_frame(Box::new({
494            let runs = runs.clone();
495            move || runs.set(runs.get() + 1)
496        }));
497        assert_eq!((0, 0), (shared.frames(), runs.get()));
498
499        shared.frame_presented();
500        assert_eq!((1, 1), (shared.frames(), runs.get()));
501        shared.frame_presented();
502        assert_eq!((2, 1), (shared.frames(), runs.get()));
503    }
504
505    #[test]
506    fn a_handler_given_after_the_first_frame_runs_at_once() {
507        let shared = BrowserSurfaceShared::new();
508        shared.set_target_id(9802);
509        shared.frame_presented();
510        let ran = Rc::new(Cell::new(false));
511        shared.on_first_frame(Box::new({
512            let ran = ran.clone();
513            move || ran.set(true)
514        }));
515
516        assert!(ran.get());
517    }
518
519    #[test]
520    fn a_forgotten_handler_does_not_run() {
521        let shared = BrowserSurfaceShared::new();
522        shared.set_target_id(9803);
523        shared.on_first_frame(Box::new(|| panic!("the view was closed before its first frame")));
524        shared.forget_first_frame();
525
526        shared.frame_presented();
527        assert_eq!(1, shared.frames());
528    }
529
530    #[test]
531    #[should_panic(expected = "is not a kind of render target")]
532    fn a_report_of_an_unknown_kind_is_refused() {
533        BrowserSurfaceShared::report_target(9501, 7);
534    }
535
536    #[test]
537    fn a_reader_on_another_thread_never_sees_half_of_a_write() {
538        let shared = BrowserSurfaceShared::new();
539        let stop = Arc::new(AtomicBool::new(false));
540        let reader = std::thread::spawn({
541            let shared = shared.clone();
542            let stop = stop.clone();
543            move || {
544                let mut reads = 0u32;
545                loop {
546                    let (size, scaling) = shared.size();
547                    // Every write stores one number three times.
548                    assert_eq!(size.width, size.height);
549                    assert_eq!(f64::from(size.width), scaling);
550                    reads += 1;
551                    if stop.load(Ordering::SeqCst) {
552                        return reads;
553                    }
554                }
555            }
556        });
557
558        for value in 1..=20_000 {
559            shared.set_size(PixelSize::new(value, value), f64::from(value));
560        }
561        stop.store(true, Ordering::SeqCst);
562
563        assert!(reader.join().unwrap() > 0);
564        assert_eq!((PixelSize::new(20_000, 20_000), 20_000.0), shared.size());
565    }
566}