Skip to main content

ferroui_browser/rendering/
web_render_target.rs

1use super::{BrowserSoftwareRenderTarget, BrowserWebGlRenderTarget};
2use crate::interop::canvas_helper::{RENDER_TARGET_KIND_SOFTWARE, RENDER_TARGET_KIND_WEB_GL};
3use crate::interop::{non_null, JsObject};
4use ferroui_base::platform::IPlatformGraphicsContext;
5use ferroui_base::PixelSize;
6use std::cell::{Cell, RefCell};
7use std::collections::HashMap;
8use std::rc::Rc;
9use wasm_bindgen::prelude::*;
10
11#[wasm_bindgen(raw_module = "./ferroui.js")]
12extern "C" {
13    /// A render target of the script side.
14    pub type JsRenderTarget;
15
16    #[wasm_bindgen(method, getter, js_name = renderTargetType)]
17    fn render_target_type(this: &JsRenderTarget) -> String;
18
19    #[wasm_bindgen(js_namespace = WebRenderTargetRegistry, js_name = getRenderTarget)]
20    fn get_js_render_target(id: i32) -> JsObject;
21
22    #[wasm_bindgen(js_namespace = WebRenderTarget, js_name = setSize)]
23    fn set_js_size(target: &JsObject, w: i32, h: i32);
24
25    #[wasm_bindgen(js_namespace = WebRenderTargetRegistry, js_name = initializeWorker)]
26    fn initialize_js_worker();
27
28    #[wasm_bindgen(js_namespace = WebRenderTargetRegistry, js_name = workerStarted)]
29    fn js_worker_started(thread_id: i32);
30
31    #[wasm_bindgen(js_namespace = WebRenderTargetRegistry, js_name = unregister)]
32    fn js_unregister(id: i32);
33}
34
35/// Makes the worker of the calling thread take the canvases that are
36/// transferred to it: installs the message handler of the registry of the
37/// thread's script, which creates the render target of each canvas and
38/// reports it
39/// ([`on_render_target_registered`](crate::interop::canvas_helper::on_render_target_registered)).
40///
41/// Called once, by a thread other than the one of the page, before a canvas
42/// is created with its id. The thread has to stay alive afterwards and
43/// return to the event loop of its worker, where the messages arrive.
44pub fn initialize_worker() {
45    initialize_js_worker();
46}
47
48/// The id a canvas is created with for a render thread that has been started
49/// and has not reported itself yet: the registry of the script side
50/// transfers the control of the canvas at once and keeps the message for the
51/// worker until [`worker_started`] names it.
52///
53/// Not from upstream, which waits for its render worker before it creates a
54/// view. No thread has this id: the id of a thread is the address of its
55/// descriptor.
56pub const PENDING_RENDER_THREAD: i32 = -1;
57
58/// Tells the registry of the script side of the calling thread, which has to
59/// be the thread that creates the canvases, that the thread `thread_id` has
60/// installed its handler ([`initialize_worker`]): the registry posts the
61/// canvases it held back for it ([`PENDING_RENDER_THREAD`]) and posts the
62/// later ones at once.
63pub(crate) fn worker_started(thread_id: i32) {
64    js_worker_started(thread_id);
65}
66
67/// Tells the registry of the script side of the calling thread, which has to
68/// be the thread that created the canvas `id`, that the canvas is closed:
69/// the registry releases the render target of a canvas that thread kept, or
70/// tells the worker the canvas was transferred to (`unregisterCanvas`),
71/// which releases it there.
72///
73/// Called last in the disposal of a view, when the thread that renders has
74/// released what it drew to the canvas with and has taken the target out of
75/// its table ([`remove_render_target`]). Upstream's script has the handler
76/// of the message and nothing that sends it.
77pub(crate) fn unregister_canvas(id: i32) {
78    let unregister = SCRIPT_UNREGISTER.with(|unregister| unregister.get());
79    unregister(id);
80}
81
82/// The render target of a canvas, as the thread that draws to it holds it.
83///
84/// Upstream has an abstract class with the two kinds as subclasses; the two
85/// kinds are closed here, so they are the variants of an enum. A value
86/// belongs to its thread (it holds an object of the script side of that
87/// thread) and is handed out by [`get_render_target`].
88#[derive(Clone)]
89pub enum BrowserRenderTarget {
90    /// A canvas that is rendered to with WebGL.
91    WebGl(Rc<BrowserWebGlRenderTarget>),
92    /// A canvas that is rendered to in memory.
93    Software(Rc<BrowserSoftwareRenderTarget>),
94}
95
96impl BrowserRenderTarget {
97    /// The graphics context the target renders with; `None` for a target
98    /// that is rendered to in software.
99    pub fn platform_graphics_context(&self) -> Option<Rc<dyn IPlatformGraphicsContext>> {
100        match self {
101            Self::WebGl(target) => Some(target.gl_context()),
102            Self::Software(_) => None,
103        }
104    }
105
106    /// The kind of the target as a worker reports it and as
107    /// [`BrowserSurfaceShared`](super::BrowserSurfaceShared) publishes it:
108    /// [`RENDER_TARGET_KIND_WEB_GL`] or [`RENDER_TARGET_KIND_SOFTWARE`].
109    pub fn kind(&self) -> i32 {
110        match self {
111            Self::WebGl(_) => RENDER_TARGET_KIND_WEB_GL,
112            Self::Software(_) => RENDER_TARGET_KIND_SOFTWARE,
113        }
114    }
115}
116
117/// How a thread asks the registry of its script for a render target.
118type ScriptRenderTargets = fn(i32) -> Option<BrowserRenderTarget>;
119
120ferroui_base::ferro_thread_local! {
121    /// The render targets of this thread, by their id in the registry of the
122    /// script side. Each thread has a table of its own: a render target
123    /// exists on the thread whose script created it (the page, or the worker
124    /// of a render thread a canvas was transferred to) and nowhere else.
125    static RENDER_TARGETS: RefCell<HashMap<i32, BrowserRenderTarget>> = RefCell::new(HashMap::new());
126    /// The registry of the script of this thread; a test puts a table of its
127    /// own in its place.
128    static SCRIPT_RENDER_TARGETS: Cell<ScriptRenderTargets> = Cell::new(wrap_script_render_target as ScriptRenderTargets);
129    /// How this thread tells the registry of its script that a canvas is
130    /// closed; a test puts a recorder in its place.
131    static SCRIPT_UNREGISTER: Cell<fn(i32)> = Cell::new(js_unregister as fn(i32));
132}
133
134/// The render target the script of the calling thread created under `id`,
135/// wrapped for the framework.
136///
137/// # Panics
138/// Panics when the target is of a kind the framework does not know.
139fn wrap_script_render_target(id: i32) -> Option<BrowserRenderTarget> {
140    let js = non_null(get_js_render_target(id))?;
141    let type_ = JsCast::unchecked_ref::<JsRenderTarget>(&js).render_target_type();
142    if type_ == "webgl" {
143        return Some(BrowserRenderTarget::WebGl(BrowserWebGlRenderTarget::new(js)));
144    }
145    if type_ == "software" {
146        return Some(BrowserRenderTarget::Software(BrowserSoftwareRenderTarget::new(js)));
147    }
148    panic!("{type_}");
149}
150
151/// The render target with the id `id` on the calling thread, when the
152/// script of this thread has created it.
153///
154/// The first call that finds the target wraps it and keeps it in the table
155/// of the thread; later calls answer from the table. A thread whose script
156/// does not have the target (the thread of the page, for a canvas that was
157/// transferred to a worker) gets `None` each time.
158///
159/// # Panics
160/// Panics when the target is of a kind the framework does not know.
161pub fn get_render_target(id: i32) -> Option<BrowserRenderTarget> {
162    if let Some(target) = RENDER_TARGETS.with(|targets| targets.borrow().get(&id).cloned()) {
163        return Some(target);
164    }
165    // Outside the borrow: wrapping a WebGL target calls into the script and
166    // into the module.
167    let script = SCRIPT_RENDER_TARGETS.with(|script| script.get());
168    let target = script(id)?;
169    RENDER_TARGETS.with(|targets| targets.borrow_mut().insert(id, target.clone()));
170    Some(target)
171}
172
173/// Takes the render target with the id `id` out of the table of the calling
174/// thread; returns whether it was there. What still draws to the target (a
175/// render target a backend created from it) keeps it alive until it is
176/// released, on this thread.
177///
178/// For the thread that renders, when a view is closed.
179pub fn remove_render_target(id: i32) -> bool {
180    RENDER_TARGETS.with(|targets| targets.borrow_mut().remove(&id)).is_some()
181}
182
183/// Replaces the registry of the script for the calling thread.
184#[cfg(test)]
185pub(crate) fn set_script_render_targets_for_unit_tests(script: fn(i32) -> Option<BrowserRenderTarget>) {
186    SCRIPT_RENDER_TARGETS.with(|current| current.set(script));
187}
188
189/// Replaces the call that tells the script of the calling thread that a
190/// canvas is closed.
191#[cfg(test)]
192pub(crate) fn set_script_unregister_for_unit_tests(unregister: fn(i32)) {
193    SCRIPT_UNREGISTER.with(|current| current.set(unregister));
194}
195
196/// Sets the size of the canvas behind a render target.
197pub(crate) fn update_size(js: &JsObject, size: PixelSize) {
198    set_js_size(js, size.width, size.height);
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    ferroui_base::ferro_thread_local! {
206        /// How often the thread of a test asked its script for a target.
207        static ASKED: Cell<u32> = const { Cell::new(0) };
208    }
209
210    fn asked() -> u32 {
211        ASKED.with(|asked| asked.get())
212    }
213
214    /// A script that has the software targets 41 and 42 and no other.
215    fn script(id: i32) -> Option<BrowserRenderTarget> {
216        ASKED.with(|asked| asked.set(asked.get() + 1));
217        (id == 41 || id == 42).then(|| BrowserRenderTarget::Software(BrowserSoftwareRenderTarget::new(JsObject::NULL)))
218    }
219
220    /// A script without a target, as the thread of the page has for a canvas
221    /// it transferred.
222    fn empty_script(_id: i32) -> Option<BrowserRenderTarget> {
223        ASKED.with(|asked| asked.set(asked.get() + 1));
224        None
225    }
226
227    fn same(a: &BrowserRenderTarget, b: &BrowserRenderTarget) -> bool {
228        match (a, b) {
229            (BrowserRenderTarget::Software(a), BrowserRenderTarget::Software(b)) => Rc::ptr_eq(a, b),
230            (BrowserRenderTarget::WebGl(a), BrowserRenderTarget::WebGl(b)) => Rc::ptr_eq(a, b),
231            _ => false,
232        }
233    }
234
235    #[test]
236    fn a_target_is_wrapped_once_and_kept_for_its_thread() {
237        set_script_render_targets_for_unit_tests(script);
238
239        let first = get_render_target(41).expect("the script has the target");
240        let again = get_render_target(41).expect("the table has the target");
241        assert!(same(&first, &again));
242        assert_eq!(1, asked());
243        assert_eq!(RENDER_TARGET_KIND_SOFTWARE, first.kind());
244        assert!(first.platform_graphics_context().is_none());
245
246        // Another id is another target.
247        let other = get_render_target(42).expect("the script has the target");
248        assert!(!same(&first, &other));
249        assert_eq!(2, asked());
250    }
251
252    #[test]
253    fn a_target_the_script_does_not_have_is_asked_for_each_time() {
254        set_script_render_targets_for_unit_tests(script);
255
256        assert!(get_render_target(7).is_none());
257        assert!(get_render_target(7).is_none());
258        assert_eq!(2, asked());
259    }
260
261    #[test]
262    fn each_thread_has_a_table_of_its_own() {
263        set_script_render_targets_for_unit_tests(script);
264        let here = get_render_target(41).expect("the script has the target");
265
266        // The thread of the page, for a canvas it transferred: its script
267        // has no target, and the table of the other thread is not its own.
268        let there = std::thread::spawn(|| {
269            set_script_render_targets_for_unit_tests(empty_script);
270            let found = get_render_target(41).is_some();
271            (found, asked())
272        })
273        .join()
274        .unwrap();
275        assert_eq!((false, 1), there);
276
277        // A second thread with a script that has the target wraps its own.
278        let wrapped = std::thread::spawn(|| {
279            set_script_render_targets_for_unit_tests(script);
280            get_render_target(41).is_some() && get_render_target(41).is_some() && asked() == 1
281        })
282        .join()
283        .unwrap();
284        assert!(wrapped);
285
286        assert!(same(&here, &get_render_target(41).expect("the table has the target")));
287        assert_eq!(1, asked());
288    }
289
290    #[test]
291    fn a_removed_target_is_asked_for_again() {
292        set_script_render_targets_for_unit_tests(script);
293        let first = get_render_target(42).expect("the script has the target");
294
295        assert!(remove_render_target(42));
296        assert!(!remove_render_target(42));
297
298        let second = get_render_target(42).expect("the script has the target");
299        assert!(!same(&first, &second));
300        assert_eq!(2, asked());
301    }
302}