Skip to main content

ferroui_browser/rendering/
browser_web_gl_render_target.rs

1use super::web_render_target::update_size;
2use super::{BrowserSurfaceShared, RenderStatistics};
3use crate::interop::canvas_helper::{GlInfo, RENDER_TARGET_KIND_WEB_GL};
4use crate::interop::{thread_proxy, JsObject};
5use ferroui_base::platform::surfaces::IPlatformRenderSurfaceRenderTarget;
6use ferroui_base::platform::{IOptionalFeatureProvider, IPlatformGraphicsContext, RenderTargetSceneInfo};
7use ferroui_base::reactive::{Disposable, IDisposable};
8use ferroui_base::PixelSize;
9use ferroui_opengl::gl_consts;
10use ferroui_opengl::surfaces::{IGlPlatformSurfaceRenderTarget, IGlPlatformSurfaceRenderingSession};
11use ferroui_opengl::{GetProcAddress, GlInterface, GlProfileType, GlVersion, IGlContext, OpenGlException};
12use ferroui_skia::gpu::open_gl::IGlSkiaSpecificOptionsFeature;
13use std::any::{Any, TypeId};
14use std::cell::Cell;
15use std::ffi::c_void;
16use std::rc::{Rc, Weak};
17use std::sync::Arc;
18use std::thread::{self, ThreadId};
19use wasm_bindgen::prelude::*;
20
21#[wasm_bindgen(raw_module = "./ferroui.js")]
22extern "C" {
23    /// The WebGL render target of the script side.
24    type JsWebGlRenderTarget;
25
26    #[wasm_bindgen(method, getter, js_name = contextHandle)]
27    fn context_handle(this: &JsWebGlRenderTarget) -> i32;
28
29    #[wasm_bindgen(method, getter, js_name = fboId)]
30    fn fbo_id(this: &JsWebGlRenderTarget) -> u32;
31
32    #[wasm_bindgen(method, getter)]
33    fn stencil(this: &JsWebGlRenderTarget) -> i32;
34
35    #[wasm_bindgen(method, getter)]
36    fn sample(this: &JsWebGlRenderTarget) -> i32;
37
38    #[wasm_bindgen(method, getter)]
39    fn depth(this: &JsWebGlRenderTarget) -> i32;
40
41    #[wasm_bindgen(method, getter)]
42    fn attrs(this: &JsWebGlRenderTarget) -> JsWebGlAttributes;
43
44    /// The canvas of the target: an `HTMLCanvasElement`, or an
45    /// `OffscreenCanvas` on a worker.
46    #[cfg(feature = "vello")]
47    #[wasm_bindgen(method, getter)]
48    fn canvas(this: &JsWebGlRenderTarget) -> JsValue;
49
50    /// The attributes a WebGL context was created with.
51    type JsWebGlAttributes;
52
53    #[wasm_bindgen(method, getter, js_name = majorVersion)]
54    fn major_version(this: &JsWebGlAttributes) -> i32;
55
56    #[wasm_bindgen(js_namespace = WebGlRenderTarget, js_name = getCurrentContext)]
57    fn get_current_context() -> i32;
58
59    #[wasm_bindgen(js_namespace = WebGlRenderTarget, js_name = makeContextCurrent)]
60    fn make_context_current(context: i32) -> bool;
61}
62
63/// A canvas that is rendered to with WebGL, as the thread that draws to it
64/// holds it: the object of the script side of that thread, what the script
65/// knows about the WebGL context, and the context itself.
66///
67/// Upstream's class is also the render surface of the canvas. Here the
68/// surface is [`BrowserRenderSurface`](super::BrowserRenderSurface), which
69/// both threads hold and which holds nothing of a thread; this object stays
70/// in the table of the thread that renders
71/// ([`get_render_target`](super::get_render_target)) and is what the surface
72/// resolves there.
73pub struct BrowserWebGlRenderTarget {
74    js: JsObject,
75    gl_info: GlInfo,
76    gl_context: Rc<WebGlContext>,
77}
78
79impl BrowserWebGlRenderTarget {
80    /// Wraps the WebGL render target of the script side.
81    ///
82    /// The thread that calls this is the one the target belongs to.
83    pub fn new(js: JsObject) -> Rc<Self> {
84        let target = JsCast::unchecked_ref::<JsWebGlRenderTarget>(&js);
85        let gl_info = GlInfo {
86            context_id: target.context_handle(),
87            fbo_id: target.fbo_id(),
88            stencils: target.stencil(),
89            samples: target.sample(),
90            depth: target.depth(),
91        };
92        let context_id = target.context_handle();
93        let version = target.attrs().major_version();
94        let gl_context = WebGlContext::new(
95            context_id,
96            GlVersion::new(GlProfileType::OpenGLES, if version > 1 { 3 } else { 2 }, 0),
97            gl_info.samples,
98            gl_info.stencils,
99            // The canvas of a WebGL 2 context, for the backend that draws
100            // with the context of the page itself.
101            #[cfg(feature = "vello")]
102            (version > 1).then(|| target.canvas()),
103        );
104        Rc::new(Self { js, gl_info, gl_context })
105    }
106
107    /// The WebGL context of the canvas.
108    pub fn gl_context(&self) -> Rc<WebGlContext> {
109        self.gl_context.clone()
110    }
111
112    /// Creates the OpenGL render target of the canvas. `shared` is where
113    /// each frame reads the size the canvas has to have.
114    pub fn create_gl_render_target(
115        self: &Rc<Self>,
116        shared: Arc<BrowserSurfaceShared>,
117    ) -> Rc<dyn IGlPlatformSurfaceRenderTarget> {
118        Rc::new(GlSurface { target: self.clone(), shared })
119    }
120
121    fn update_size(&self, size: PixelSize) {
122        update_size(&self.js, size);
123    }
124}
125
126struct GlSession {
127    restore_context: Cell<Option<Rc<dyn IDisposable>>>,
128    context: Rc<dyn IGlContext>,
129    size: PixelSize,
130    scaling: f64,
131    shared: Arc<BrowserSurfaceShared>,
132}
133
134impl IGlPlatformSurfaceRenderingSession for GlSession {
135    fn context(&self) -> Rc<dyn IGlContext> {
136        self.context.clone()
137    }
138
139    fn size(&self) -> PixelSize {
140        self.size
141    }
142
143    // This should technically be delivered via the scaling of the composition target anyway, why do we
144    // still have this property
145    fn scaling(&self) -> f64 {
146        self.scaling
147    }
148
149    fn is_y_flipped(&self) -> bool {
150        false
151    }
152
153    fn dispose(&self) {
154        if let Some(restore_context) = self.restore_context.take() {
155            // The frame is drawn: the backend has flushed it before it ends
156            // the session. Not from upstream: where frames are drawn is
157            // otherwise invisible to the thread of the page.
158            RenderStatistics::frame_presented(
159                thread_proxy::current_thread(),
160                RENDER_TARGET_KIND_WEB_GL,
161                self.context.version().major(),
162                self.size.width,
163                self.size.height,
164            );
165            restore_context.dispose();
166            // Whoever waits for the first frame of this canvas is told.
167            self.shared.frame_presented();
168        }
169    }
170}
171
172struct GlSurface {
173    target: Rc<BrowserWebGlRenderTarget>,
174    shared: Arc<BrowserSurfaceShared>,
175}
176
177impl IPlatformRenderSurfaceRenderTarget for GlSurface {}
178
179impl IGlPlatformSurfaceRenderTarget for GlSurface {
180    fn begin_draw(&self, _scene_info: &RenderTargetSceneInfo) -> Rc<dyn IGlPlatformSurfaceRenderingSession> {
181        // The size as the thread of the user interface last wrote it. The
182        // canvas is given that size here, by the thread that draws to it:
183        // for a canvas that was transferred to a worker no other can.
184        let (size, scaling) = self.shared.size();
185        self.target.update_size(size);
186        let restore_context = self.target.gl_context.ensure_current();
187        self.target
188            .gl_context
189            .gl_interface()
190            .bind_framebuffer(gl_consts::GL_FRAMEBUFFER, self.target.gl_info.fbo_id as i32);
191        Rc::new(GlSession {
192            restore_context: Cell::new(Some(restore_context)),
193            context: self.target.gl_context.clone(),
194            size,
195            scaling,
196            shared: self.shared.clone(),
197        })
198    }
199
200    fn dispose(&self) {
201        // No-op
202    }
203}
204
205#[cfg(target_os = "emscripten")]
206mod native {
207    use std::ffi::{c_char, c_void, CString};
208
209    extern "C" {
210        fn emscripten_GetProcAddress(name: *const c_char) -> *const c_void;
211    }
212
213    /// The address of the entry point of the WebGL emulation of the module,
214    /// or null.
215    pub(super) fn get_proc_address(name: &str) -> *const c_void {
216        let Ok(name) = CString::new(name) else { return std::ptr::null() };
217        // SAFETY: the function reads the NUL-terminated name, which outlives
218        // the call, and returns the address of an entry point or null.
219        unsafe { emscripten_GetProcAddress(name.as_ptr()) }
220    }
221}
222
223#[cfg(not(target_os = "emscripten"))]
224mod native {
225    use std::ffi::c_void;
226
227    /// There is no WebGL emulation outside the module: nothing resolves.
228    pub(super) fn get_proc_address(_name: &str) -> *const c_void {
229        std::ptr::null()
230    }
231}
232
233/// A WebGL context registered with the module.
234pub struct WebGlContext {
235    this: Weak<WebGlContext>,
236    context_id: i32,
237    thread: ThreadId,
238    version: GlVersion,
239    gl_interface: Rc<GlInterface>,
240    sample_count: i32,
241    stencil_size: i32,
242    /// The canvas of the context, when it is a WebGL 2 context: an object
243    /// of the thread the context belongs to.
244    #[cfg(feature = "vello")]
245    canvas: Option<JsValue>,
246}
247
248struct RestoreContext {
249    context_id: Cell<Option<i32>>,
250}
251
252impl IDisposable for RestoreContext {
253    fn dispose(&self) {
254        if let Some(context_id) = self.context_id.take() {
255            make_context_current(context_id);
256        }
257    }
258}
259
260impl WebGlContext {
261    /// Wraps the context with the given id and resolves its entry points.
262    pub fn new(
263        context_id: i32,
264        version: GlVersion,
265        sample_count: i32,
266        stencil_size: i32,
267        #[cfg(feature = "vello")] canvas: Option<JsValue>,
268    ) -> Rc<Self> {
269        let old = get_current_context();
270        if !make_context_current(context_id) {
271            panic!("{}", OpenGlException::new("Unable to make the context current"));
272        }
273        let get_proc_address: GetProcAddress = Rc::new(|name: &str| -> *const c_void { native::get_proc_address(name) });
274        // SAFETY: in the WebAssembly module the loader resolves names in the
275        // WebGL emulation the module is linked with, whose entry points have
276        // the signatures of OpenGL ES under the calling convention of the
277        // target; anywhere else it returns null for every name, which the
278        // contract allows (and construction then fails on the first required
279        // entry point). The context was just made current on this thread,
280        // which is the one the context is used on from here on
281        // (`verify_access`). The previous context is restored below: every
282        // later use of the interface happens inside `ensure_current`, which
283        // `begin_draw` and the Skia GPU take before they call into it.
284        let gl_interface = Rc::new(unsafe { GlInterface::new(version, get_proc_address) });
285        make_context_current(old);
286
287        Rc::new_cyclic(|this| Self {
288            this: this.clone(),
289            context_id,
290            thread: thread::current().id(),
291            version,
292            gl_interface,
293            sample_count,
294            stencil_size,
295            #[cfg(feature = "vello")]
296            canvas,
297        })
298    }
299
300    fn verify_access(&self) {
301        if self.thread != thread::current().id() {
302            panic!("Call from invalid thread");
303        }
304    }
305}
306
307impl IOptionalFeatureProvider for WebGlContext {
308    fn try_get_feature(&self, feature_type: TypeId) -> Option<Rc<dyn Any>> {
309        if feature_type == TypeId::of::<dyn IGlContext>() {
310            let this: Rc<dyn IGlContext> = self.this.upgrade()?;
311            return Some(Rc::new(this));
312        }
313        if feature_type == TypeId::of::<dyn IGlSkiaSpecificOptionsFeature>() {
314            let this: Rc<dyn IGlSkiaSpecificOptionsFeature> = self.this.upgrade()?;
315            return Some(Rc::new(this));
316        }
317        // Not from upstream: the Vello backend draws with the WebGL2
318        // context of the page, which it gets from its canvas.
319        #[cfg(feature = "vello")]
320        if feature_type == TypeId::of::<dyn ferroui_vello::web_gl::IWebGlCanvasFeature>() && self.canvas.is_some() {
321            let this: Rc<dyn ferroui_vello::web_gl::IWebGlCanvasFeature> = self.this.upgrade()?;
322            return Some(Rc::new(this));
323        }
324        None
325    }
326}
327
328impl IPlatformGraphicsContext for WebGlContext {
329    // TODO: Implement
330    fn is_lost(&self) -> bool {
331        false
332    }
333
334    fn ensure_current(&self) -> Rc<dyn IDisposable> {
335        self.verify_access();
336        if get_current_context() == self.context_id {
337            return Disposable::empty();
338        }
339        self.make_current()
340    }
341
342    fn dispose(&self) {
343        // No-op, destroyed with the render target
344    }
345
346    fn as_any(&self) -> &dyn Any {
347        self
348    }
349}
350
351impl IGlContext for WebGlContext {
352    fn version(&self) -> GlVersion {
353        self.version
354    }
355
356    fn gl_interface(&self) -> Rc<GlInterface> {
357        self.gl_interface.clone()
358    }
359
360    fn sample_count(&self) -> i32 {
361        self.sample_count
362    }
363
364    fn stencil_size(&self) -> i32 {
365        self.stencil_size
366    }
367
368    fn make_current(&self) -> Rc<dyn IDisposable> {
369        self.verify_access();
370        let old = get_current_context();
371        if !make_context_current(self.context_id) {
372            panic!("{}", OpenGlException::new("Unable to make the context current"));
373        }
374        Rc::new(RestoreContext { context_id: Cell::new(Some(old)) })
375    }
376
377    fn is_shared_with(&self, _context: &dyn IGlContext) -> bool {
378        false
379    }
380
381    fn can_create_shared_context(&self) -> bool {
382        false
383    }
384
385    fn create_shared_context(&self, _preferred_versions: Option<&[GlVersion]>) -> Option<Rc<dyn IGlContext>> {
386        panic!("Specified method is not supported.");
387    }
388}
389
390#[cfg(feature = "vello")]
391impl ferroui_vello::web_gl::IWebGlCanvasFeature for WebGlContext {
392    fn canvas(&self) -> JsValue {
393        self.verify_access();
394        self.canvas.clone().expect("the feature is handed out for a context with a canvas")
395    }
396}
397
398impl IGlSkiaSpecificOptionsFeature for WebGlContext {
399    fn use_native_skia_gr_gl_interface(&self) -> bool {
400        true
401    }
402}