Skip to main content

ferroui_browser/rendering/
render_target_browser_surface.rs

1use super::web_render_target::{get_render_target, remove_render_target, unregister_canvas};
2use super::{
3    BrowserRenderSurface, BrowserSharedRenderLoop, BrowserSurface, BrowserSurfaceShared, RenderStatistics,
4    RenderWorker,
5};
6use crate::browser_app_builder::BrowserRenderingMode;
7use crate::interop::canvas_helper::CanvasSurface;
8use crate::interop::{thread_proxy, JsObject};
9use ferroui_base::media::MediaContext;
10use ferroui_base::platform::surfaces::IPlatformRenderSurface;
11use ferroui_base::platform::{
12    IOptionalFeatureProvider, IPlatformGraphics, IPlatformGraphicsContext, IPlatformGraphicsReadyStateFeature,
13};
14use ferroui_base::rendering::composition::server::ServerCompositor;
15use ferroui_base::rendering::composition::Compositor;
16use ferroui_base::threading::Dispatcher;
17use ferroui_base::{PixelSize, Size};
18use std::any::{Any, TypeId};
19use std::cell::Cell;
20use std::rc::Rc;
21use std::sync::Arc;
22
23/// The surface of a view that is rendered through a render target of the
24/// page (a WebGL context or a 2D canvas).
25///
26/// The object belongs to the thread of the user interface. What the thread
27/// that renders needs of the canvas is in the two objects it shares with
28/// that thread: [`BrowserSurfaceShared`] (the id of the render target, the
29/// size, whether the target exists and its kind) and the
30/// [`BrowserRenderSurface`] over it, which is the surface the compositor
31/// draws to.
32pub struct RenderTargetBrowserSurface {
33    base: BrowserSurface,
34    shared: Arc<BrowserSurfaceShared>,
35    render_surface: Arc<BrowserRenderSurface>,
36    /// Whether [`dispose`](Self::dispose) has run.
37    disposed: Cell<bool>,
38}
39
40impl RenderTargetBrowserSurface {
41    /// `thread_id` is what the canvas was created with: 0 when this thread
42    /// kept the canvas and has its render target.
43    fn new(js_surface: CanvasSurface, thread_id: i32) -> Rc<Self> {
44        let target_id = js_surface.target_id();
45        let shared = BrowserSurfaceShared::new();
46        // Known to every thread by the id from here on: the worker of a
47        // render thread reports the target under it, and may already have.
48        shared.register(target_id);
49        if thread_id == 0 {
50            // This thread owns the target, and its script created it with
51            // the canvas: it publishes that the target exists and its kind.
52            // Upstream asks its registry for the target at each frame; here
53            // the answer has to be where a second thread can read it.
54            if let Some(target) = get_render_target(target_id) {
55                shared.set_target_kind(target.kind());
56            }
57        }
58        let gpu: Arc<dyn IPlatformGraphics> = Arc::new(BrowserPlatformGraphics { shared: shared.clone() });
59        let compositor = if thread_id == 0 {
60            Compositor::with_scheduler(
61                BrowserSharedRenderLoop::render_loop(),
62                Some(gpu),
63                false,
64                &MediaContext::instance().scheduler(),
65                Dispatcher::ui_thread(),
66                None,
67                None,
68            )
69        } else {
70            // The canvas belongs to the render thread, and so does everything
71            // that draws to it: the compositor is confined to that thread
72            // (the render-thread mode, this thread never renders, a loop
73            // that runs in the background). This thread commits batches and
74            // waits for a frame at the synchronous points. Upstream creates
75            // one kind of compositor in both of its modes, with a timer that
76            // does not run in the background (see `DEVIATIONS.md`).
77            Compositor::with_render_thread(
78                BrowserSharedRenderLoop::render_loop(),
79                Some(gpu),
80                false,
81                &MediaContext::instance().scheduler(),
82                Dispatcher::ui_thread(),
83                None,
84                None,
85            )
86        };
87
88        let render_surface = BrowserRenderSurface::new(shared.clone());
89        let this = Rc::new(Self {
90            base: BrowserSurface::new(js_surface, compositor),
91            shared,
92            render_surface,
93            disposed: Cell::new(false),
94        });
95        if let Some((w, h, s)) = this.base.initial_size() {
96            this.on_size_changed(w, h, s);
97        }
98        this
99    }
100
101    /// Creates a canvas in `container` with a render target of the first of
102    /// `modes` the browser supports, and the surface over it.
103    pub fn create(container: &JsObject, modes: &[BrowserRenderingMode], top_level_id: i32) -> Rc<Self> {
104        let modes: Vec<i32> = modes.iter().map(|m| *m as i32).collect();
105        // 0 while the render loop of the page ticks on this thread: the
106        // render target is created and used here. When it ticks on the
107        // render thread, the script transfers the control of the canvas to
108        // the worker of that thread, at once or when the thread has
109        // reported itself. A render thread that does not run the loop of
110        // the page (somebody else started it) gets no canvas of a view.
111        let thread_id =
112            if BrowserSharedRenderLoop::renders_on_render_thread() { RenderWorker::canvas_thread_id() } else { 0 };
113        let js = CanvasSurface::create_render_target_surface(container, &modes, top_level_id, thread_id);
114        Self::new(js, thread_id)
115    }
116
117    /// The compositor that renders to the surface.
118    pub fn compositor(&self) -> Rc<Compositor> {
119        self.base.compositor()
120    }
121
122    /// The scaling from logical units to device pixels.
123    pub fn scaling(&self) -> f64 {
124        self.base.scaling()
125    }
126
127    /// The size in logical units.
128    pub fn client_size(&self) -> Size {
129        self.base.client_size()
130    }
131
132    /// The size in device pixels.
133    pub fn render_size(&self) -> PixelSize {
134        self.base.render_size()
135    }
136
137    /// Whether the surface has a size it can be rendered at.
138    pub fn is_valid(&self) -> bool {
139        self.base.is_valid()
140    }
141
142    /// Subscribes to changes of the client size.
143    pub fn size_changed(&self, handler: Rc<dyn Fn()>) -> u64 {
144        self.base.size_changed(handler)
145    }
146
147    /// Subscribes to changes of the scaling.
148    pub fn scaling_changed(&self, handler: Rc<dyn Fn()>) -> u64 {
149        self.base.scaling_changed(handler)
150    }
151
152    /// The surfaces a render backend can draw to: the render surface of the
153    /// canvas. It is handed out from the start and is not ready until the
154    /// thread that owns the render target has published it (upstream hands
155    /// out nothing until then).
156    pub fn get_render_surfaces(&self) -> Vec<Arc<dyn IPlatformRenderSurface>> {
157        let surface: Arc<dyn IPlatformRenderSurface> = self.render_surface.clone();
158        vec![surface]
159    }
160
161    /// What the thread of the user interface and the thread that renders
162    /// share about the canvas.
163    pub fn shared(&self) -> &Arc<BrowserSurfaceShared> {
164        &self.shared
165    }
166
167    /// The canvas changed its size or its scaling.
168    pub fn on_size_changed(&self, pixel_width: f64, pixel_height: f64, dpr: f64) {
169        // Where the thread that renders reads it at the start of a frame.
170        self.shared.on_size_changed(pixel_width, pixel_height, dpr);
171        self.base.on_size_changed(pixel_width, pixel_height, dpr);
172    }
173
174    /// Releases the canvas surface.
175    ///
176    /// Upstream posts a job to the compositor, which takes its server
177    /// compositor out of the render loop, and then destroys the surface of
178    /// the script side. A job is not what this port can use: it runs with a
179    /// batch, and the batch of a closed view is the one that is likely never
180    /// applied (the compositor is dropped as soon as the media context has
181    /// committed it, and its graph is released in place of the next frame);
182    /// a job does not run either while the graphics are not ready. So the
183    /// thread that renders is given the work directly ([`release_canvas`]):
184    ///
185    /// - this thread, when it renders the view itself: the work is done
186    ///   here, before the call returns;
187    /// - the render thread, when the compositor is confined to it: the work
188    ///   is queued for that thread, which enters the graph under the
189    ///   compositor lock. Whether it gets there before or after the graph is
190    ///   released does not matter: a graph that is released has released
191    ///   what it drew with, in its own graphics context.
192    ///
193    /// The top-level is disposed before its renderer: the composition target
194    /// of the view is disposed after this call, in a batch of its own that
195    /// this thread waits for. The work does not depend on which of the two
196    /// reaches the thread that renders first.
197    ///
198    /// A canvas that never reached the render thread (the view was closed
199    /// before that thread had reported itself) is let go of here: the script
200    /// of this thread still holds it back. When the work cannot be queued
201    /// for a render thread that runs, the canvas is only marked as disposed
202    /// and stays alive in the worker, unused.
203    ///
204    /// A surface that is dropped without this call makes it itself: a view
205    /// that is let go of without being closed releases what it drew with in
206    /// the same way, and not when its compositor happens to be dropped.
207    pub fn dispose(&self) {
208        if self.disposed.replace(true) {
209            return;
210        }
211        // Nothing waits for the first frame of a closed view.
212        self.shared.forget_first_frame();
213        // A target reported under the id from here on belongs to no canvas.
214        self.shared.unregister();
215        let shared = self.shared.clone();
216        // This thread created the canvas, and its script is the one to tell.
217        let page_thread = thread_proxy::current_thread();
218        let compositor = self.base.compositor();
219        if compositor.is_confined_to_render_thread() {
220            let server = compositor.locked_server();
221            let posted = RenderWorker::post({
222                let shared = shared.clone();
223                move || {
224                    let released = server.try_with(|server| release_canvas(server, &shared, page_thread));
225                    if released.is_none() {
226                        // The graph is gone, and what it drew with went
227                        // with it.
228                        release_canvas_objects(&shared, page_thread);
229                    }
230                }
231            });
232            if !posted {
233                if RenderWorker::thread_id() == 0 {
234                    release_canvas_objects(&shared, page_thread);
235                } else {
236                    shared.dispose();
237                }
238            }
239        } else {
240            compositor.with_server(|server| release_canvas(server, &shared, page_thread));
241        }
242        self.base.dispose();
243    }
244}
245
246impl Drop for RenderTargetBrowserSurface {
247    fn drop(&mut self) {
248        // Not while the thread unwinds: the release calls into the script
249        // and into the graph, and a second panic would end the module.
250        if !std::thread::panicking() {
251            self.dispose();
252        }
253    }
254}
255
256/// What the thread that renders does when a view is closed. Runs under the
257/// compositor lock, outside a frame.
258///
259/// 1. With the graphics context of the canvas current, the render targets
260///    and layers that are still there and the backend context (Skia's
261///    context, which holds the objects of the graphics interface it drew
262///    with) are released
263///    ([`release_gpu_resources`](ServerCompositor::release_gpu_resources)).
264///    A thread that renders several canvases has several contexts, and an
265///    object that is deleted while another context is current is deleted
266///    there, where the same number names something else; with no context
267///    current the call fails in the script of the module. The composition
268///    target of the view, when it is disposed after this, finds nothing to
269///    release.
270/// 2. The compositor stops rendering
271///    ([`stop_rendering`](ServerCompositor::stop_rendering)): it still
272///    applies the batches of the view that is being closed, and asks for no
273///    tick. Upstream's job takes the server compositor out of the render
274///    loop here.
275/// 3. The rest is [`release_canvas_objects`].
276fn release_canvas(server: &ServerCompositor, shared: &Arc<BrowserSurfaceShared>, page_thread: usize) {
277    server.release_gpu_resources();
278    server.stop_rendering();
279    release_canvas_objects(shared, page_thread);
280}
281
282/// What the thread that renders does with the canvas of a closed view once
283/// nothing draws to it:
284///
285/// 1. the canvas is marked as disposed: its surface is of no kind and its
286///    graphics is not ready from here on, so nothing creates a backend
287///    context or a render target for it again;
288/// 2. the render target leaves the table of this thread;
289/// 3. the script of the thread that created the canvas (`page_thread`) is
290///    told ([`unregister_canvas`]): directly when that is this thread,
291///    through its event loop otherwise. It releases the target of the script
292///    side, or sends `unregisterCanvas` to the worker that has it.
293fn release_canvas_objects(shared: &Arc<BrowserSurfaceShared>, page_thread: usize) {
294    let target_id = shared.target_id();
295    shared.dispose();
296    remove_render_target(target_id);
297    RenderStatistics::canvas_released();
298    if thread_proxy::current_thread() == page_thread {
299        unregister_canvas(target_id);
300    } else {
301        // When the call cannot be queued the script keeps the target: the
302        // canvas of the closed view stays alive in the worker, unused.
303        thread_proxy::run_on_thread(page_thread, Box::new(move || unregister_canvas(target_id)));
304    }
305}
306
307/// The platform graphics of one canvas, whose render target may not exist
308/// yet when the compositor is created.
309///
310/// Upstream's object keeps the render target and the size of the canvas in
311/// fields of its own. This one is created by the thread of the user
312/// interface and asked by the thread that renders, so it holds only what
313/// the two share: whether it is ready and whether the target renders with a
314/// context are read from there, and the context is the one of the render
315/// target of the calling thread ([`get_render_target`]).
316struct BrowserPlatformGraphics {
317    shared: Arc<BrowserSurfaceShared>,
318}
319
320impl BrowserPlatformGraphics {
321    fn uses_contexts(&self) -> bool {
322        self.shared.uses_contexts().expect("the render target exists")
323    }
324}
325
326impl IPlatformGraphics for BrowserPlatformGraphics {
327    fn uses_shared_context(&self) -> bool {
328        self.uses_contexts()
329    }
330
331    fn create_context(&self) -> Rc<dyn IPlatformGraphicsContext> {
332        panic!("Specified method is not supported.");
333    }
334
335    /// Called by the thread that renders.
336    fn get_shared_context(&self) -> Rc<dyn IPlatformGraphicsContext> {
337        let target = get_render_target(self.shared.target_id()).expect("the render target exists");
338        match target.platform_graphics_context() {
339            Some(context) => context,
340            None => panic!(
341                "This platform graphics instance represents software rendering mode and cant create contexts, you are supposed to query IPlatformGraphicsReadyStateFeature to know this"
342            ),
343        }
344    }
345
346    fn as_feature_provider(&self) -> Option<&dyn IOptionalFeatureProvider> {
347        Some(self)
348    }
349}
350
351impl IOptionalFeatureProvider for BrowserPlatformGraphics {
352    fn try_get_feature(&self, feature_type: TypeId) -> Option<Rc<dyn Any>> {
353        if feature_type == TypeId::of::<dyn IPlatformGraphicsReadyStateFeature>() {
354            // Upstream answers with the object itself. A second object over
355            // the same shared state is the same answer, and the feature then
356            // holds no handle of the platform graphics.
357            let this: Arc<dyn IPlatformGraphicsReadyStateFeature> = Arc::new(Self { shared: self.shared.clone() });
358            return Some(Rc::new(this));
359        }
360        None
361    }
362}
363
364impl IPlatformGraphicsReadyStateFeature for BrowserPlatformGraphics {
365    fn is_ready(&self) -> bool {
366        self.shared.is_ready()
367    }
368
369    fn uses_contexts(&self) -> bool {
370        BrowserPlatformGraphics::uses_contexts(self)
371    }
372}
373
374#[cfg(test)]
375mod tests {
376    use super::*;
377    use crate::interop::canvas_helper::{RENDER_TARGET_KIND_SOFTWARE, RENDER_TARGET_KIND_WEB_GL};
378    use crate::interop::JsObject;
379    use crate::rendering::web_render_target::{
380        set_script_render_targets_for_unit_tests, set_script_unregister_for_unit_tests, BrowserRenderTarget,
381    };
382    use std::cell::RefCell;
383    use crate::rendering::BrowserSoftwareRenderTarget;
384
385    fn software_script(_id: i32) -> Option<BrowserRenderTarget> {
386        Some(BrowserRenderTarget::Software(BrowserSoftwareRenderTarget::new(JsObject::NULL)))
387    }
388
389    fn ready_state(graphics: &BrowserPlatformGraphics) -> Arc<dyn IPlatformGraphicsReadyStateFeature> {
390        let features: &dyn IOptionalFeatureProvider = graphics;
391        features.try_get_shared::<dyn IPlatformGraphicsReadyStateFeature>().expect("the graphics has the feature")
392    }
393
394    #[test]
395    fn the_graphics_is_ready_when_a_thread_published_the_target_and_the_canvas_has_a_size() {
396        let shared = BrowserSurfaceShared::new();
397        let graphics = BrowserPlatformGraphics { shared: shared.clone() };
398        let feature = ready_state(&graphics);
399        assert!(!feature.is_ready());
400
401        shared.on_size_changed(300.0, 180.0, 1.5);
402        assert!(!feature.is_ready());
403
404        // Published by another thread, as the render thread does.
405        std::thread::spawn({
406            let shared = shared.clone();
407            move || shared.set_target_kind(RENDER_TARGET_KIND_WEB_GL)
408        })
409        .join()
410        .unwrap();
411        assert!(feature.is_ready());
412        assert!(feature.uses_contexts());
413        assert!(graphics.uses_shared_context());
414
415        shared.on_size_changed(0.0, 0.0, 1.5);
416        assert!(!feature.is_ready());
417    }
418
419    #[test]
420    fn a_software_target_uses_no_context() {
421        let shared = BrowserSurfaceShared::new();
422        shared.set_target_kind(RENDER_TARGET_KIND_SOFTWARE);
423        let graphics = BrowserPlatformGraphics { shared };
424
425        assert!(!ready_state(&graphics).uses_contexts());
426        assert!(!graphics.uses_shared_context());
427        let features: &dyn IOptionalFeatureProvider = &graphics;
428        assert!(features.try_get_feature(TypeId::of::<dyn IPlatformGraphics>()).is_none());
429    }
430
431    #[test]
432    #[should_panic(expected = "the render target exists")]
433    fn whether_contexts_are_used_is_not_known_before_the_target_exists() {
434        let graphics = BrowserPlatformGraphics { shared: BrowserSurfaceShared::new() };
435
436        graphics.uses_shared_context();
437    }
438
439    #[test]
440    #[should_panic(expected = "represents software rendering mode")]
441    fn a_software_target_has_no_shared_context() {
442        set_script_render_targets_for_unit_tests(software_script);
443        let shared = BrowserSurfaceShared::new();
444        shared.set_target_id(11);
445        shared.set_target_kind(RENDER_TARGET_KIND_SOFTWARE);
446        let graphics = BrowserPlatformGraphics { shared };
447
448        graphics.get_shared_context();
449    }
450
451    ferroui_base::ferro_thread_local! {
452        /// The canvases the thread of a test told its script to let go of.
453        static UNREGISTERED: RefCell<Vec<i32>> = const { RefCell::new(Vec::new()) };
454    }
455
456    fn record_unregistered(id: i32) {
457        UNREGISTERED.with(|unregistered| unregistered.borrow_mut().push(id));
458    }
459
460    #[test]
461    fn a_released_canvas_is_disposed_leaves_the_table_of_its_thread_and_is_unregistered() {
462        set_script_render_targets_for_unit_tests(software_script);
463        set_script_unregister_for_unit_tests(record_unregistered);
464        let shared = BrowserSurfaceShared::new();
465        shared.register(9701);
466        shared.set_target_kind(RENDER_TARGET_KIND_SOFTWARE);
467        shared.on_size_changed(300.0, 180.0, 1.0);
468        let graphics = BrowserPlatformGraphics { shared: shared.clone() };
469        let feature = ready_state(&graphics);
470        let surface = BrowserRenderSurface::new(shared.clone());
471        assert!(get_render_target(9701).is_some());
472        assert!(feature.is_ready() && surface.is_ready());
473        assert!(surface.as_framebuffer_surface().is_some());
474
475        // The one thread of a build without threads is the thread of the
476        // page: its script is told directly.
477        release_canvas_objects(&shared, thread_proxy::current_thread());
478
479        assert!(shared.is_disposed());
480        assert!(!feature.is_ready() && !surface.is_ready());
481        // The surface is of no kind any more, and nothing wraps the target a
482        // second time.
483        assert!(surface.as_framebuffer_surface().is_none());
484        assert!(!remove_render_target(9701));
485        assert_eq!(vec![9701], UNREGISTERED.with(|unregistered| unregistered.borrow().clone()));
486    }
487
488    #[test]
489    fn a_thread_that_did_not_create_the_canvas_does_not_tell_its_own_script() {
490        set_script_render_targets_for_unit_tests(software_script);
491        set_script_unregister_for_unit_tests(record_unregistered);
492        let shared = BrowserSurfaceShared::new();
493        shared.register(9702);
494        shared.set_target_kind(RENDER_TARGET_KIND_SOFTWARE);
495
496        // Created by a thread with another id: the call is queued for it
497        // (and dropped here, where there are no threads to queue for).
498        release_canvas_objects(&shared, thread_proxy::current_thread() + 1);
499
500        assert!(shared.is_disposed());
501        assert!(UNREGISTERED.with(|unregistered| unregistered.borrow().is_empty()));
502    }
503
504    #[test]
505    #[should_panic(expected = "Specified method is not supported")]
506    fn the_graphics_creates_no_context() {
507        let graphics = BrowserPlatformGraphics { shared: BrowserSurfaceShared::new() };
508
509        graphics.create_context();
510    }
511}