diff --git a/crates/canvas-c/src/webgl/gl.rs b/crates/canvas-c/src/webgl/gl.rs index 15d1942ba..affb8d22f 100644 --- a/crates/canvas-c/src/webgl/gl.rs +++ b/crates/canvas-c/src/webgl/gl.rs @@ -272,6 +272,23 @@ pub struct WebGLState { in_flight: AtomicU32, /// Queued presents are skipped: the context is letting go of its window (threaded only). detaching: AtomicBool, + /// A present found the context lost (threaded only: an unthreaded one is asked directly). + lost: AtomicBool, + /// The XAML surface a threaded context presents into, as the UI thread holds it: that thread + /// ends the last draw once the context moves on from it. + #[cfg(target_os = "windows")] + xaml: ShownXaml, +} + +#[cfg(target_os = "windows")] +#[derive(Default)] +struct ShownXaml(std::sync::Mutex>>); + +#[cfg(target_os = "windows")] +impl std::fmt::Debug for ShownXaml { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("ShownXaml") + } } /// Presents a threaded context may have queued before `present` waits for the oldest. @@ -334,8 +351,14 @@ impl WebGLState { /// side holds back, would otherwise queue frames without limit. pub fn present(&self) -> bool { if !self.threaded { + #[cfg(target_os = "windows")] + return self.inner_mut().present(); + #[cfg(not(target_os = "windows"))] return self.inner_mut().make_current_and_swap_buffers(); } + if self.lost.load(Ordering::Acquire) { + return false; + } if self.in_flight.load(Ordering::Acquire) >= MAX_QUEUED_FRAMES { crate::webgl::thread::sync(|| ()); } @@ -343,15 +366,40 @@ impl WebGLState { let state = StatePtr(self); crate::webgl::thread::post(move || { let state = state; - let state = unsafe { &*state.0 }; - if !state.detaching.load(Ordering::Acquire) { - state.inner_mut().make_current_and_swap_buffers(); + let this = unsafe { &*state.0 }; + if !this.detaching.load(Ordering::Acquire) { + this.present_here(state); } - crate::webgl::thread::frame_presented(state.in_flight.fetch_sub(1, Ordering::AcqRel)); + crate::webgl::thread::frame_presented(this.in_flight.fetch_sub(1, Ordering::AcqRel)); }); true } + #[cfg(not(target_os = "windows"))] + fn present_here(&self, _: StatePtr) { + self.inner_mut().make_current_and_swap_buffers(); + } + + /// The WebGL thread's half of `present`. A frame the display isn't ready for is held and + /// shown from the thread's retries: nothing here may wait on a display that could be + /// hidden, as every other context on the thread would wait with it. + #[cfg(target_os = "windows")] + fn present_here(&self, state: StatePtr) { + let inner = self.inner_mut(); + if !inner.present_or_hold() { + if canvas_webgl::webgl::canvas_native_webgl_get_is_context_lost(inner) { + self.lost.store(true, Ordering::Release); + } + return; + } + if inner.present_held() { + crate::webgl::thread::retry(state.0 as usize, move || { + let state = state; + unsafe { &*state.0 }.inner_mut().present_held() + }); + } + } + /// Runs `f` (which lets go of the window) after every queued call, skipping the presents queued /// meanwhile: they are for a window that is going away, and waiting on each would stall the /// caller for as many frames as the GPU is behind. @@ -405,6 +453,7 @@ pub extern "C" fn canvas_native_webgl_state_destroy(state: *mut WebGLState) { let state = StatePtr(state); crate::webgl::thread::post(move || { let state = state; + crate::webgl::thread::cancel_retry(state.0 as usize); let _ = unsafe { Box::from_raw(state.0 as *mut WebGLState) }; }); } else { @@ -420,6 +469,9 @@ impl WebGLState { threaded: false, in_flight: AtomicU32::new(0), detaching: AtomicBool::new(false), + lost: AtomicBool::new(false), + #[cfg(target_os = "windows")] + xaml: ShownXaml::default(), } } } @@ -713,8 +765,9 @@ impl WebGLShaderPrecisionFormat { pub struct WebGLExtension(Option>, bool); impl WebGLExtension { - pub fn new(extension: Option>) -> Self { - Self(extension, false) + /// An extension of `state`: its calls go where the context lives. + pub fn new(extension: Option>, state: &WebGLState) -> Self { + Self(extension, state.is_threaded()) } } @@ -2077,6 +2130,83 @@ pub extern "C" fn canvas_native_webgl_create_d3d( ) else { return std::ptr::null_mut(); }; + create_d3d( + width, + height, + version, + alpha, + antialias, + depth, + fail_if_major_performance_caveat, + power_preference, + premultiplied_alpha, + preserve_drawing_buffer, + stencil, + desynchronized, + xr_compatible, + ) + .map_or(std::ptr::null_mut(), |state| Box::into_raw(Box::new(state))) +} + +/// `canvas_native_webgl_create_d3d`, for a context that lives on the WebGL thread. +#[cfg(target_os = "windows")] +#[no_mangle] +pub extern "C" fn canvas_native_webgl_create_d3d_threaded( + width: i32, + height: i32, + version: i32, + alpha: bool, + antialias: bool, + depth: bool, + fail_if_major_performance_caveat: bool, + power_preference: i32, + premultiplied_alpha: bool, + preserve_drawing_buffer: bool, + stencil: bool, + desynchronized: bool, + xr_compatible: bool, +) -> *mut WebGLState { + let (Ok(version), Ok(power_preference)) = ( + WebGLVersion::try_from(version), + PowerPreference::try_from(power_preference), + ) else { + return std::ptr::null_mut(); + }; + WebGLState::create_threaded(|| { + create_d3d( + width, + height, + version, + alpha, + antialias, + depth, + fail_if_major_performance_caveat, + power_preference, + premultiplied_alpha, + preserve_drawing_buffer, + stencil, + desynchronized, + xr_compatible, + ) + }) +} + +#[cfg(target_os = "windows")] +fn create_d3d( + width: i32, + height: i32, + version: WebGLVersion, + alpha: bool, + antialias: bool, + depth: bool, + fail_if_major_performance_caveat: bool, + power_preference: PowerPreference, + premultiplied_alpha: bool, + preserve_drawing_buffer: bool, + stencil: bool, + desynchronized: bool, + xr_compatible: bool, +) -> Option { let mut attrs = canvas_core::context_attributes::ContextAttributes::new( alpha, antialias, @@ -2092,10 +2222,8 @@ pub extern "C" fn canvas_native_webgl_create_d3d( version == WebGLVersion::V1, ColorSpace::Srgb, ); - let Some(ctx) = GLContext::create_texture_context(&mut attrs, width, height) else { - return std::ptr::null_mut(); - }; - let state = WebGLState::wrap(canvas_webgl::prelude::WebGLState::new_with_context_attributes( + let ctx = GLContext::create_texture_context(&mut attrs, width, height)?; + Some(WebGLState::wrap(canvas_webgl::prelude::WebGLState::new_with_context_attributes( ctx, version, attrs.get_alpha(), @@ -2110,8 +2238,22 @@ pub extern "C" fn canvas_native_webgl_create_d3d( attrs.get_xr_compatible(), false, version == WebGLVersion::V1, - )); - Box::into_raw(Box::new(state)) + ))) +} + +#[cfg(target_os = "windows")] +impl WebGLState { + /// UI thread: the XAML surface a threaded context presents into from now on (none: it moved + /// to a panel). The one before shows its last frame. + fn show_xaml(&self, handoff: Option>) { + let previous = match self.xaml.0.lock() { + Ok(mut shown) => std::mem::replace(&mut *shown, handoff), + Err(_) => None, + }; + if let Some(previous) = previous { + previous.end_draw(); + } + } } /// Windows: shows a `canvas_native_webgl_create_d3d` context in a `SwapChainPanel` (any COM @@ -2123,7 +2265,21 @@ pub extern "C" fn canvas_native_webgl_attach_swap_chain_panel(state: *mut WebGLS return false; } let state = unsafe { &mut *state }; - unsafe { state.get_inner_mut().attach_swap_chain_panel(panel) } + if !state.is_threaded() { + return unsafe { state.get_inner_mut().attach_swap_chain_panel(panel) }; + } + // Made where the context presents; only binding it has to happen here. + let Some(swap_chain) = state.sync(|inner| inner.create_panel_swap_chain()) else { + return false; + }; + state.show_xaml(None); + match unsafe { canvas_core::gpu::dxgi::bind_swap_chain(panel, windows::core::Interface::as_raw(&swap_chain)) } { + Ok(()) => true, + Err(error) => { + log::error!("canvas: could not show the WebGL swapchain in its panel: {error}"); + false + } + } } /// Windows: presents a `canvas_native_webgl_create_d3d` context into a XAML `SurfaceImageSource` @@ -2136,7 +2292,28 @@ pub extern "C" fn canvas_native_webgl_attach_xaml_surface(state: *mut WebGLState return false; } let state = unsafe { &mut *state }; - unsafe { state.get_inner_mut().attach_xaml_surface(source) } + if !state.is_threaded() { + return unsafe { state.get_inner_mut().attach_xaml_surface(source) }; + } + let Some(device) = canvas_core::gpu::gl::angle_d3d11_device() else { + return false; + }; + let (width, height) = state.get_dimensions(); + let handoff = match unsafe { + canvas_core::gpu::dxgi::XamlHandoff::new(source, &device, width.max(1) as u32, height.max(1) as u32) + } { + Ok(handoff) => handoff, + Err(error) => { + log::error!("canvas: the XAML surface cannot be drawn from the WebGL thread: {error}"); + return false; + } + }; + let shared = std::sync::Arc::clone(&handoff); + let attached = state.sync(move |inner| inner.attach_xaml_handoff(shared)); + if attached { + state.show_xaml(Some(handoff)); + } + attached } /// Windows: maps the drawing buffer into its panel (DIPs = pixels * scale + offset). @@ -2153,10 +2330,17 @@ pub extern "C" fn canvas_native_webgl_set_swap_chain_transform( return false; } let state = unsafe { &*state }; - state.get_inner().set_swap_chain_transform(scale_x, scale_y, offset_x, offset_y) + if !state.is_threaded() { + return state.get_inner().set_swap_chain_transform(scale_x, scale_y, offset_x, offset_y); + } + state.post(move |inner| { + inner.set_swap_chain_transform(scale_x, scale_y, offset_x, offset_y); + }); + true } -/// Windows: resizes (and clears) a `canvas_native_webgl_create_d3d` drawing buffer. +/// Windows: resizes (and clears) a `canvas_native_webgl_create_d3d` drawing buffer. A threaded +/// context queues it, so it reports success. #[cfg(target_os = "windows")] #[no_mangle] pub extern "C" fn canvas_native_webgl_resize_d3d(state: *mut WebGLState, width: i32, height: i32) -> bool { @@ -2164,7 +2348,13 @@ pub extern "C" fn canvas_native_webgl_resize_d3d(state: *mut WebGLState, width: return false; } let state = unsafe { &mut *state }; - state.get_inner_mut().resize_texture_surface(width, height) + if !state.is_threaded() { + return state.get_inner_mut().resize_texture_surface(width, height); + } + state.post(move |inner| { + inner.resize_texture_surface(width, height); + }); + true } /// Ends a frame: presents it where the context is on screen (Windows panels), else flushes. @@ -2173,15 +2363,7 @@ pub extern "C" fn canvas_native_webgl_present(state: *mut WebGLState) -> bool { if state.is_null() { return false; } - let state = unsafe { &*state }; - #[cfg(target_os = "windows")] - { - state.get_inner().present() - } - #[cfg(not(target_os = "windows"))] - { - state.present() - } + unsafe { &*state }.present() } pub(crate) fn canvas_native_webgl_create_no_window_internal( @@ -3492,9 +3674,10 @@ pub extern "C" fn canvas_native_webgl_get_is_context_lost(state: *mut WebGLState return false; } let state = unsafe { &*state }; - // Only Android threads a context, and loss is only tracked on Windows: skip the round trip. + // Loss is only tracked on Windows, where a threaded context's presents record it: no round + // trip either way. if state.is_threaded() { - return false; + return state.lost.load(Ordering::Acquire); } state.sync(|state| canvas_webgl::webgl::canvas_native_webgl_get_is_context_lost(state)) } diff --git a/crates/canvas-c/src/webgl/thread.rs b/crates/canvas-c/src/webgl/thread.rs index 190450cc9..de2aed571 100644 --- a/crates/canvas-c/src/webgl/thread.rs +++ b/crates/canvas-c/src/webgl/thread.rs @@ -3,10 +3,11 @@ //! behind each other, and the rest run here while the caller waits. One thread for all of them keeps //! reads from one context into another (texImage2D from a WebGL canvas) on a single thread. -use std::cell::Cell; +use std::cell::{Cell, RefCell}; use std::collections::VecDeque; use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::{mpsc, Arc, Condvar, Mutex, OnceLock}; +use std::time::Duration; type Job = Box; @@ -32,8 +33,13 @@ static CANVASES_BEHIND: AtomicU32 = AtomicU32::new(0); thread_local! { static ON_GL_THREAD: Cell = const { Cell::new(false) }; + /// Work tried again until it's done (`retry`), by key. + static RETRIES: RefCell bool>)>> = const { RefCell::new(Vec::new()) }; } +/// How soon an idle thread tries its retries again. +const RETRY_AFTER: Duration = Duration::from_millis(4); + fn worker() -> Option> { WORKER .get_or_init(|| { @@ -52,6 +58,13 @@ fn worker() -> Option> { } fn raise_priority() { + #[cfg(target_os = "windows")] + unsafe { + use windows::Win32::System::Threading::{GetCurrentThread, SetThreadPriority, THREAD_PRIORITY_ABOVE_NORMAL}; + let _ = SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_ABOVE_NORMAL); + // XAML surfaces are drawn from here. + let _ = windows::Win32::System::Com::CoInitializeEx(None, windows::Win32::System::Com::COINIT_MULTITHREADED); + } #[cfg(target_os = "android")] unsafe { // THREAD_PRIORITY_DISPLAY, for this thread only. @@ -77,19 +90,57 @@ fn run(worker: Arc) { return; }; while queue.is_empty() { - queue = match worker.signal.wait(queue) { - Ok(queue) => queue, + if RETRIES.with(|retries| retries.borrow().is_empty()) { + queue = match worker.signal.wait(queue) { + Ok(queue) => queue, + Err(_) => return, + }; + continue; + } + let timed_out; + (queue, timed_out) = match worker.signal.wait_timeout(queue, RETRY_AFTER) { + Ok((queue, timeout)) => (queue, timeout.timed_out()), Err(_) => return, }; + if timed_out { + break; + } } queue.drain(..).collect() }; for job in jobs { job(); } + run_retries(); } } +fn run_retries() { + let mut retries = RETRIES.with(|retries| std::mem::take(&mut *retries.borrow_mut())); + retries.retain_mut(|(_, retry)| retry()); + RETRIES.with(|current| { + // Registered while they ran: those replace them. + let mut current = current.borrow_mut(); + retries.retain(|(key, _)| !current.iter().any(|(other, _)| other == key)); + current.append(&mut retries); + }); +} + +/// GL thread: runs `f` again after each batch of jobs and every few milliseconds while idle, until +/// it returns false. Replaces an earlier retry for `key`. +#[cfg_attr(not(target_os = "windows"), allow(dead_code))] +pub(crate) fn retry(key: usize, f: impl FnMut() -> bool + 'static) { + RETRIES.with(|retries| { + let mut retries = retries.borrow_mut(); + retries.retain(|(other, _)| *other != key); + retries.push((key, Box::new(f))); + }); +} + +pub(crate) fn cancel_retry(key: usize) { + RETRIES.with(|retries| retries.borrow_mut().retain(|(other, _)| *other != key)); +} + /// Whether contexts can be threaded at all: false if the thread could not be started. pub(crate) fn available() -> bool { worker().is_some() diff --git a/crates/canvas-c/tests/threaded_webgl_d3d.rs b/crates/canvas-c/tests/threaded_webgl_d3d.rs new file mode 100644 index 000000000..ef76c9002 --- /dev/null +++ b/crates/canvas-c/tests/threaded_webgl_d3d.rs @@ -0,0 +1,122 @@ +//! Threaded WebGL on ANGLE (Windows): contexts on the WebGL thread, shown in a (headless) panel +//! from this thread, next to an unthreaded one on this thread. +//! +//! cargo test -p canvas-c --features 2d,webgl,gl,d3d --test threaded_webgl_d3d +#![cfg(all(target_os = "windows", feature = "webgl", feature = "gl"))] + +use std::ffi::CString; + +use canvas_c::WebGLState; +use windows::core::Interface; + +const W: i32 = 64; +const H: i32 = 48; + +const COLOR_BUFFER_BIT: u32 = 0x4000; +const RGBA: u32 = 0x1908; +const UNSIGNED_BYTE: u32 = 0x1401; +const VERTEX_SHADER: u32 = 0x8B31; +const FRAGMENT_SHADER: u32 = 0x8B30; +const LINK_STATUS: u32 = 0x8B82; + +fn create(threaded: bool) -> Option<*mut WebGLState> { + let create = if threaded { + canvas_c::canvas_native_webgl_create_d3d_threaded + } else { + canvas_c::canvas_native_webgl_create_d3d + }; + let state = create(W, H, 2, true, false, true, false, 0, true, false, false, false, false); + if state.is_null() { + eprintln!("ANGLE unavailable: skipped"); + return None; + } + Some(state) +} + +fn clear(state: *mut WebGLState, n: usize) { + let [r, g, b] = [[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]][n % 3]; + canvas_c::canvas_native_webgl_clear_color(r, g, b, 1., state); + canvas_c::canvas_native_webgl_clear(COLOR_BUFFER_BIT, state); +} + +fn pixel(state: *mut WebGLState) -> [u8; 4] { + let mut out = [0u8; 4]; + canvas_c::canvas_native_webgl_read_pixels_u8(W / 2, H / 2, 1, 1, RGBA, UNSIGNED_BYTE, out.as_mut_ptr(), 4, state); + out +} + +fn link(state: *mut WebGLState) -> bool { + let vertex = CString::new("#version 300 es\nin vec4 p; void main() { gl_Position = p; }").unwrap(); + let fragment = CString::new("#version 300 es\nprecision mediump float; out vec4 c; uniform vec4 u; void main() { c = u; }").unwrap(); + let program = canvas_c::canvas_native_webgl_create_program(state); + for (kind, source) in [(VERTEX_SHADER, &vertex), (FRAGMENT_SHADER, &fragment)] { + let shader = canvas_c::canvas_native_webgl_create_shader(kind, state); + canvas_c::canvas_native_webgl_shader_source(shader, source.as_ptr(), state); + canvas_c::canvas_native_webgl_compile_shader(shader, state); + canvas_c::canvas_native_webgl_attach_shader(program, shader, state); + } + canvas_c::canvas_native_webgl_link_program(program, state); + let result = canvas_c::canvas_native_webgl_get_program_parameter(program, LINK_STATUS, state); + let linked = canvas_c::canvas_native_webgl_result_get_bool(result); + canvas_c::canvas_native_webgl_WebGLResult_destroy(result); + linked +} + +#[test] +fn threaded_frames_read_back_like_direct_ones() { + let (Some(direct), Some(threaded)) = (create(false), create(true)) else { return }; + assert!(unsafe { &*threaded }.is_threaded()); + for n in 0..6 { + clear(direct, n); + clear(threaded, n); + assert!(canvas_c::canvas_native_webgl_present(direct)); + assert!(canvas_c::canvas_native_webgl_present(threaded)); + assert_eq!(pixel(direct), pixel(threaded), "frame {n}"); + } + assert_eq!(canvas_c::canvas_native_webgl_get_error(threaded), 0); + canvas_c::canvas_native_webgl_state_destroy(direct); + canvas_c::canvas_native_webgl_state_destroy(threaded); +} + +#[test] +fn presents_into_a_panel_from_the_webgl_thread_and_resizes() { + let Some(state) = create(true) else { return }; + let panel = canvas_core::gpu::dxgi::headless_panel(); + assert!(canvas_c::canvas_native_webgl_attach_swap_chain_panel(state, panel.as_raw())); + assert!(canvas_c::canvas_native_webgl_set_swap_chain_transform(state, 1., 1., 0., 0.)); + // Nothing takes the headless panel's frames: they are held, then forced out. + for n in 0..8 { + clear(state, n); + assert!(canvas_c::canvas_native_webgl_present(state)); + } + // The last frame's green, and its present queued nothing that stays behind. + assert_eq!(pixel(state), [0, 255, 0, 255]); + assert!(!canvas_c::canvas_native_webgl_get_is_context_lost(state)); + + assert!(canvas_c::canvas_native_webgl_resize_d3d(state, W * 2, H * 2)); + assert_eq!(canvas_c::canvas_native_webgl_state_get_drawing_buffer_width(state), W * 2); + clear(state, 2); + assert!(canvas_c::canvas_native_webgl_present(state)); + assert_eq!(pixel(state), [0, 0, 255, 255]); + // Dropped with a frame still held. + canvas_c::canvas_native_webgl_state_destroy(state); + + let Some(next) = create(true) else { return }; + clear(next, 0); + assert_eq!(pixel(next), [255, 0, 0, 255]); + canvas_c::canvas_native_webgl_state_destroy(next); +} + +#[test] +fn links_programs_on_threaded_contexts_beside_one_on_this_thread() { + let Some(direct) = create(false) else { return }; + let threaded: Vec<_> = (0..4).filter_map(|_| create(true)).collect(); + assert!(link(direct)); + for &state in &threaded { + assert!(link(state)); + } + for state in threaded { + canvas_c::canvas_native_webgl_state_destroy(state); + } + canvas_c::canvas_native_webgl_state_destroy(direct); +} diff --git a/crates/canvas-core/src/gpu/dxgi.rs b/crates/canvas-core/src/gpu/dxgi.rs index ced601e73..cf9d0747e 100644 --- a/crates/canvas-core/src/gpu/dxgi.rs +++ b/crates/canvas-core/src/gpu/dxgi.rs @@ -623,6 +623,9 @@ pub fn headless_panel() -> windows::core::IUnknown { panel.into() } +/// A swapchain made off the UI thread, to show there with `bind_swap_chain`. +pub type SwapChainRef = windows::core::IUnknown; + /// Shows `swap_chain` (any COM pointer to it) in the panel (any COM pointer to it). UI thread. pub unsafe fn bind_swap_chain(panel: *mut c_void, swap_chain: *mut c_void) -> Result<()> { let unknown = unsafe { windows::core::IUnknown::from_raw_borrowed(&panel) }.ok_or_else(windows::core::Error::empty)?; diff --git a/crates/canvas-core/src/gpu/gl/egl.rs b/crates/canvas-core/src/gpu/gl/egl.rs index d685ee3eb..5a92cf6ec 100644 --- a/crates/canvas-core/src/gpu/gl/egl.rs +++ b/crates/canvas-core/src/gpu/gl/egl.rs @@ -155,12 +155,27 @@ fn load() -> Option { .map(|s| s.to_string_lossy().into_owned()) .unwrap_or_default(); - Some(Egl { + let egl = Egl { instance, display, extensions, _gles: gles, - }) + }; + #[cfg(target_os = "windows")] + protect_immediate_context(&egl); + Some(egl) +} + +/// Threaded WebGL contexts copy frames with ANGLE's immediate context outside ANGLE's lock while +/// contexts on other threads call into ANGLE, so D3D has to serialize the two. +#[cfg(target_os = "windows")] +fn protect_immediate_context(egl: &Egl) { + use windows::core::Interface; + use windows::Win32::Graphics::Direct3D11::ID3D11Multithread; + let Some(device) = d3d11_device(egl) else { return }; + if let Ok(multithread) = unsafe { device.GetImmediateContext() }.and_then(|context| context.cast::()) { + let _ = unsafe { multithread.SetMultithreadProtected(true) }; + } } /// ANGLE on a D3D11 device of ours, made with BGRA support: XAML SurfaceImageSources (what @@ -289,8 +304,12 @@ impl Egl { /// ANGLE's own D3D11 device. Textures that ANGLE renders into must be created on it. #[cfg(target_os = "windows")] pub fn angle_d3d11_device() -> Option { + d3d11_device(shared()?) +} + +#[cfg(target_os = "windows")] +fn d3d11_device(egl: &Egl) -> Option { use windows::core::Interface; - let egl = shared()?; let query_display: QueryDisplayAttribExt = unsafe { std::mem::transmute(egl.instance.get_proc_address("eglQueryDisplayAttribEXT")?) }; let query_device: QueryDeviceAttribExt = @@ -405,6 +424,51 @@ pub(crate) struct GLContextInner { pub(crate) struct Presenter { swap_chain: crate::gpu::dxgi::CompositionSwapChain, context: windows::Win32::Graphics::Direct3D11::ID3D11DeviceContext, + /// A frame the display wasn't ready for, copied out of the drawing buffer so it can be shown + /// later without what's drawn next (`present_or_hold`). + held: parking_lot::Mutex>, +} + +#[cfg(target_os = "windows")] +impl Presenter { + fn new(swap_chain: crate::gpu::dxgi::CompositionSwapChain, context: windows::Win32::Graphics::Direct3D11::ID3D11DeviceContext) -> Self { + Self { + swap_chain, + context, + held: parking_lot::Mutex::new(None), + } + } + + fn present_from(&self, texture: &windows::Win32::Graphics::Direct3D11::ID3D11Texture2D) -> bool { + let Ok(back_buffer) = self.swap_chain.buffer::(0) else { + return false; + }; + unsafe { self.context.CopyResource(&back_buffer, texture) }; + drop(back_buffer); + self.swap_chain.present(true).is_ok() + } +} + +#[cfg(target_os = "windows")] +fn hold_frame( + presenter: &Presenter, + texture: &windows::Win32::Graphics::Direct3D11::ID3D11Texture2D, + reuse: Option, +) -> Option { + use windows::Win32::Graphics::Direct3D11::D3D11_TEXTURE2D_DESC; + let mut desc = D3D11_TEXTURE2D_DESC::default(); + unsafe { texture.GetDesc(&mut desc) }; + let fits = |held: &windows::Win32::Graphics::Direct3D11::ID3D11Texture2D| { + let mut held_desc = D3D11_TEXTURE2D_DESC::default(); + unsafe { held.GetDesc(&mut held_desc) }; + (held_desc.Width, held_desc.Height) == (desc.Width, desc.Height) + }; + let held = match reuse.filter(fits) { + Some(held) => held, + None => create_render_texture(desc.Width as i32, desc.Height as i32)?, + }; + unsafe { presenter.context.CopyResource(&held, texture) }; + Some(held) } /// A BGRA texture on ANGLE's device that a pbuffer can wrap and a swapchain buffer can be @@ -704,11 +768,33 @@ impl GLContext { // A SurfaceImageSource has a fixed size: the host attaches one of the new size. self.0.xaml = None; match self.0.presenter.as_mut() { - Some(presenter) => presenter.swap_chain.resize(width.max(1) as u32, height.max(1) as u32).is_ok(), + Some(presenter) => { + *presenter.held.lock() = None; + presenter.swap_chain.resize(width.max(1) as u32, height.max(1) as u32).is_ok() + } None => true, } } + /// Presents into a XAML `SurfaceImageSource` through `handoff` (made on the UI thread with + /// [`angle_d3d11_device`] at the context's size), from the thread that owns the context. + #[cfg(target_os = "windows")] + pub fn attach_xaml_handoff(&mut self, handoff: Arc) -> bool { + if self.0.texture.is_none() { + return false; + } + let Some(device) = angle_d3d11_device() else { return false }; + self.0.presenter = None; + self.0.xaml = match crate::gpu::dxgi::XamlSurface::with_handoff(handoff, &device) { + Ok(surface) => Some(surface), + Err(error) => { + log::error!("canvas: could not use the XAML surface for WebGL: {error}"); + None + } + }; + self.0.xaml.is_some() + } + /// Presents a texture context into a XAML `SurfaceImageSource` (any COM pointer to it, made /// at the context's size) instead of a swapchain, so it blends with the page. The rows stay /// bottom-up: the host flips the image. UI thread. @@ -733,26 +819,35 @@ impl GLContext { /// Shows a texture context in a WinUI `SwapChainPanel` (any COM pointer to it). UI thread. #[cfg(target_os = "windows")] pub unsafe fn attach_swap_chain_panel(&mut self, panel: *mut c_void, alpha: bool) -> bool { - if self.0.texture.is_none() { + use windows::core::Interface; + let Some(swap_chain) = self.create_panel_swap_chain(alpha) else { return false }; + if let Err(error) = unsafe { crate::gpu::dxgi::bind_swap_chain(panel, swap_chain.as_raw()) } { + log::error!("canvas: could not show the WebGL swapchain in its panel: {error}"); + self.0.presenter = None; return false; } - let Some(device) = angle_d3d11_device() else { return false }; - let Ok(context) = (unsafe { device.GetImmediateContext() }) else { return false }; + true + } + + /// The swapchain a texture context now presents into, for the UI thread to show in a + /// `SwapChainPanel` (`dxgi::bind_swap_chain`). The thread that owns the context. + #[cfg(target_os = "windows")] + pub fn create_panel_swap_chain(&mut self, alpha: bool) -> Option { + self.0.texture.as_ref()?; + let device = angle_d3d11_device()?; + let context = unsafe { device.GetImmediateContext() }.ok()?; let (width, height) = self.get_surface_dimensions(); self.0.xaml = None; let swap_chain = match crate::gpu::dxgi::CompositionSwapChain::new_d3d11(&device, width as u32, height as u32, alpha) { Ok(swap_chain) => swap_chain, Err(error) => { log::error!("canvas: could not create a WebGL swapchain: {error}"); - return false; + return None; } }; - if let Err(error) = unsafe { swap_chain.bind_panel(panel) } { - log::error!("canvas: could not show the WebGL swapchain in its panel: {error}"); - return false; - } - self.0.presenter = Some(Presenter { swap_chain, context }); - true + let unknown = swap_chain.as_unknown(); + self.0.presenter = Some(Presenter::new(swap_chain, context)); + Some(unknown) } /// Maps the swapchain into its panel: DIPs = pixels * scale + offset. The texture holds GL's @@ -797,6 +892,34 @@ impl GLContext { /// leaves ANGLE's cached D3D11 pipeline state untouched, unlike a draw would. #[cfg(target_os = "windows")] pub fn present(&self) -> bool { + self.present_frame(false) + } + + /// `present` for a context on a thread of its own, with no frame loop to try again from: a + /// frame the display isn't ready for is copied aside and shown by `present_held` once it is, + /// unless a newer frame comes first. + #[cfg(target_os = "windows")] + pub fn present_or_hold(&self) -> bool { + self.present_frame(true) + } + + /// Shows the frame `present_or_hold` held if the display takes it now. `true` while one is + /// still held. + #[cfg(target_os = "windows")] + pub fn present_held(&self) -> bool { + let Some(presenter) = self.0.presenter.as_ref() else { return false }; + let mut held = presenter.held.lock(); + let Some(texture) = held.as_ref() else { return false }; + if !presenter.swap_chain.acquire_frame() { + return true; + } + presenter.present_from(texture); + *held = None; + false + } + + #[cfg(target_os = "windows")] + fn present_frame(&self, hold: bool) -> bool { if self.0.lost.load(std::sync::atomic::Ordering::Relaxed) || !self.make_current() { return false; } @@ -816,15 +939,15 @@ impl GLContext { let (Some(presenter), Some(texture)) = (self.0.presenter.as_ref(), self.0.texture.as_ref()) else { return true; }; + // A held frame is superseded by this one either way. + let superseded = if hold { presenter.held.lock().take() } else { None }; if !presenter.swap_chain.acquire_frame() { + if hold { + *presenter.held.lock() = hold_frame(presenter, texture, superseded); + } return true; } - let Ok(back_buffer) = presenter.swap_chain.buffer::(0) else { - return false; - }; - unsafe { presenter.context.CopyResource(&back_buffer, texture) }; - drop(back_buffer); - presenter.swap_chain.present(true).is_ok() + presenter.present_from(texture) } fn replace_surface(&mut self, egl: &Egl, surface: egl::Surface) { diff --git a/crates/canvas-napi/src/gl/base.rs b/crates/canvas-napi/src/gl/base.rs index 9d64b9342..b64bf5f8c 100644 --- a/crates/canvas-napi/src/gl/base.rs +++ b/crates/canvas-napi/src/gl/base.rs @@ -78,12 +78,12 @@ macro_rules! impl_webgl_context { let name = name.as_str()?; let state = unsafe { &mut *self.state }; unsafe { - canvas_webgl::webgl::canvas_native_webgl_bind_attrib_location( + state.sync(|state| canvas_webgl::webgl::canvas_native_webgl_bind_attrib_location( program.0, index, name, - state.get_inner_mut(), - ) + state, + )) } Ok(()) } @@ -454,7 +454,7 @@ macro_rules! impl_webgl_context { #[napi] pub fn get_attached_shaders(&self, program: ClassInstance) -> Vec { let state = unsafe { &mut *self.state }; - let shaders = canvas_webgl::webgl::canvas_native_webgl_get_attached_shaders(program.0, state.get_inner_mut()); + let shaders = state.sync(|state| canvas_webgl::webgl::canvas_native_webgl_get_attached_shaders(program.0, state)); shaders.into_iter() .map(|shader| { WebGLShader(shader) @@ -466,7 +466,7 @@ macro_rules! impl_webgl_context { let name = name.into_utf8()?; let name = name.as_str()?; let state = unsafe { &mut *self.state }; - Ok(canvas_webgl::webgl::canvas_native_webgl_get_attrib_location(program.0, name, state.get_inner_mut())) + Ok(state.sync(|state| canvas_webgl::webgl::canvas_native_webgl_get_attrib_location(program.0, name, state))) } #[napi] @@ -497,10 +497,10 @@ macro_rules! impl_webgl_context { } let state = unsafe { &mut *self.state }; - let ext = canvas_webgl::webgl::canvas_native_webgl_get_extension( + let ext = state.sync(|state| canvas_webgl::webgl::canvas_native_webgl_get_extension( name.as_ref(), - state.get_inner_mut(), - ); + state, + )); match ext { @@ -508,7 +508,7 @@ macro_rules! impl_webgl_context { Null.to_js(env) } Some(ext) => { - let ext = Box::into_raw(Box::new(WebGLExtension::new(Some(ext)))); + let ext = Box::into_raw(Box::new(WebGLExtension::new(Some(ext), state))); match name { "ANGLE_instanced_arrays" => { @@ -648,12 +648,12 @@ macro_rules! impl_webgl_context { #[napi] pub fn get_framebuffer_attachment_parameter<'env>(&self, env: &'env Env, target: u32, attachment: u32, pname: u32) -> Result> { let mut state = unsafe { &mut *self.state }; - let ret = canvas_webgl::webgl::canvas_native_webgl_get_framebuffer_attachment_parameter( + let ret = state.sync(|state| canvas_webgl::webgl::canvas_native_webgl_get_framebuffer_attachment_parameter( target, attachment, pname, - state.get_inner_mut(), - ); + state, + )); if ret.get_is_renderbuffer() { @@ -671,7 +671,7 @@ macro_rules! impl_webgl_context { #[napi] pub fn get_program_info_log(&self, program: ClassInstance) -> String { let state = unsafe { &mut *self.state }; - canvas_webgl::webgl::canvas_native_webgl_get_program_info_log(program.0, state.get_inner_mut()) + state.sync(|state| canvas_webgl::webgl::canvas_native_webgl_get_program_info_log(program.0, state)) } #[napi] @@ -709,7 +709,7 @@ macro_rules! impl_webgl_context { #[napi] pub fn get_shader_info_log(&self, shader: ClassInstance) -> String { let state = unsafe { &mut *self.state }; - canvas_webgl::webgl::canvas_native_webgl_get_shader_info_log(shader.0, state.get_inner_mut()) + state.sync(|state| canvas_webgl::webgl::canvas_native_webgl_get_shader_info_log(shader.0, state)) } #[napi] @@ -738,13 +738,13 @@ macro_rules! impl_webgl_context { #[napi] pub fn get_shader_source(&self, shader: ClassInstance) -> String { let state = unsafe { &mut *self.state }; - canvas_webgl::webgl::canvas_native_webgl_get_shader_source(shader.0, state.get_inner_mut()) + state.sync(|state| canvas_webgl::webgl::canvas_native_webgl_get_shader_source(shader.0, state)) } #[napi] pub fn get_supported_extensions(&self) -> Vec { let state = unsafe { &mut *self.state }; - canvas_webgl::webgl::canvas_native_webgl_get_supported_extensions(state.get_inner_mut()) + state.sync(|state| canvas_webgl::webgl::canvas_native_webgl_get_supported_extensions(state)) } #[napi] @@ -755,7 +755,7 @@ macro_rules! impl_webgl_context { #[napi] pub fn get_uniform_location<'env>(&self, env: &'env Env, program: ClassInstance, name: String) -> Result> { let state = unsafe { &mut *self.state }; - let location = canvas_webgl::webgl::canvas_native_webgl_get_uniform_location(program.0, name.as_str(), state.get_inner_mut()); + let location = state.sync(|state| canvas_webgl::webgl::canvas_native_webgl_get_uniform_location(program.0, name.as_str(), state)); if location == -1 { return Null.to_js(env); @@ -1028,7 +1028,7 @@ macro_rules! impl_webgl_context { } } - canvas_webgl::webgl::canvas_native_webgl_shader_source(shader.0, source.as_str(), state.get_inner_mut()); + state.sync(|state| canvas_webgl::webgl::canvas_native_webgl_shader_source(shader.0, source.as_str(), state)); } #[napi(ts_args_type = "face:number, func: number, ref: number, mask: number")] diff --git a/crates/canvas-napi/src/gl/mod.rs b/crates/canvas-napi/src/gl/mod.rs index 087d78cfd..60cd05aa9 100644 --- a/crates/canvas-napi/src/gl/mod.rs +++ b/crates/canvas-napi/src/gl/mod.rs @@ -115,7 +115,7 @@ where pub(crate) unsafe fn present_webgl(state: *mut std::ffi::c_void) { let state = state as *mut WebGLState; if !canvas_c::canvas_native_webgl_present(state) - && canvas_webgl::webgl::canvas_native_webgl_get_is_context_lost((*state).get_inner_mut()) + && canvas_c::canvas_native_webgl_get_is_context_lost(state) { crate::frame::report_lost(); } diff --git a/crates/canvas-napi/src/gl2/mod.rs b/crates/canvas-napi/src/gl2/mod.rs index 5985fdb51..371fe358f 100644 --- a/crates/canvas-napi/src/gl2/mod.rs +++ b/crates/canvas-napi/src/gl2/mod.rs @@ -577,11 +577,11 @@ impl web_g_l_2_rendering_context { ) -> String { let state = unsafe { &mut *self.state }; - canvas_webgl::webgl2::canvas_native_webgl2_get_active_uniform_block_name( + state.sync(|state| canvas_webgl::webgl2::canvas_native_webgl2_get_active_uniform_block_name( program.0, uniform_block_index, - state.get_inner_mut(), - ) + state, + )) } #[napi] @@ -722,17 +722,17 @@ impl web_g_l_2_rendering_context { ) -> i32 { // -1 for a name that is not an output, as WebGL 2 specifies. let state = unsafe { &mut *self.state }; - canvas_webgl::webgl2::canvas_native_webgl2_get_frag_data_location(program.0, name.as_str(), state.get_inner_mut()) + state.sync(|state| canvas_webgl::webgl2::canvas_native_webgl2_get_frag_data_location(program.0, name.as_str(), state)) } #[napi] pub fn get_indexed_parameter<'env>(&self, env: &'env Env, target: u32, index: u32) -> Result> { let state = unsafe { &mut *self.state }; - let ret = canvas_webgl::webgl2::canvas_native_webgl2_get_indexed_parameter( + let ret = state.sync(|state| canvas_webgl::webgl2::canvas_native_webgl2_get_indexed_parameter( target, index, - state.get_inner_mut(), - ); + state, + )); if ret.get_is_buffer() { WebGLBuffer(ret.get_buffer_value() as u32) @@ -1013,11 +1013,11 @@ impl web_g_l_2_rendering_context { uniform_block_name: String, ) -> u32 { let state = unsafe { &mut *self.state }; - canvas_webgl::webgl2::canvas_native_webgl2_get_uniform_block_index( + state.sync(|state| canvas_webgl::webgl2::canvas_native_webgl2_get_uniform_block_index( program.0, uniform_block_name.as_str(), - state.get_inner_mut(), - ) + state, + )) } #[napi] @@ -1028,22 +1028,22 @@ impl web_g_l_2_rendering_context { ) -> Result> { let state = unsafe { &mut *self.state }; Ok( - canvas_webgl::webgl2::canvas_native_webgl2_get_uniform_indices( + state.sync(|state| canvas_webgl::webgl2::canvas_native_webgl2_get_uniform_indices( program.0, uniform_names.deref(), - state.get_inner_mut(), - ), + state, + )), ) } #[napi] pub fn invalidate_framebuffer(&self, target: u32, attachments: Vec) { let state = unsafe { &mut *self.state }; - canvas_webgl::webgl2::canvas_native_webgl2_invalidate_framebuffer( + state.sync(|state| canvas_webgl::webgl2::canvas_native_webgl2_invalidate_framebuffer( target, attachments.as_slice(), - state.get_inner_mut(), - ) + state, + )) } #[napi] @@ -1057,15 +1057,15 @@ impl web_g_l_2_rendering_context { height: i32, ) { let state = unsafe { &mut *self.state }; - canvas_webgl::webgl2::canvas_native_webgl2_invalidate_sub_framebuffer( + state.sync(|state| canvas_webgl::webgl2::canvas_native_webgl2_invalidate_sub_framebuffer( target, attachments.as_slice(), x, y, width, height, - state.get_inner_mut(), - ) + state, + )) } #[napi] @@ -1376,12 +1376,12 @@ impl web_g_l_2_rendering_context { buffer_mode: u32, ) { let state = unsafe { &mut *self.state }; - canvas_webgl::webgl2::canvas_native_webgl2_transform_feedback_varyings( + state.sync(|state| canvas_webgl::webgl2::canvas_native_webgl2_transform_feedback_varyings( program.0, varyings.deref(), buffer_mode, - state.get_inner_mut(), - ) + state, + )) } #[napi(js_name = "uniform1ui")] diff --git a/crates/canvas-napi/src/host/windows.rs b/crates/canvas-napi/src/host/windows.rs index b06fef1ef..730825c56 100644 --- a/crates/canvas-napi/src/host/windows.rs +++ b/crates/canvas-napi/src/host/windows.rs @@ -107,6 +107,8 @@ pub struct NSCCanvas { xaml_source: Option, /// `create2DContext` rasterizes on the shared render thread. threaded_2d: bool, + /// `initContext` makes a context that runs on the shared WebGL thread. + threaded_webgl: bool, } impl ObjectFinalize for NSCCanvas { @@ -185,6 +187,7 @@ impl NSCCanvas { context: Context::None, xaml_source: None, threaded_2d: false, + threaded_webgl: false, }) } @@ -233,6 +236,17 @@ impl NSCCanvas { self.threaded_2d = threaded; } + #[napi(getter, js_name = "threadedWebGL")] + pub fn threaded_webgl(&self) -> bool { + self.threaded_webgl + } + + /// Read by the next `initContext`. + #[napi(setter, js_name = "threadedWebGL")] + pub fn set_threaded_webgl(&mut self, threaded: bool) { + self.threaded_webgl = threaded; + } + #[napi(getter)] pub fn drawing_buffer_width(&self) -> u32 { self.surface_width @@ -376,7 +390,12 @@ impl NSCCanvas { Context::None => {} } let version = if context_type.contains("webgl2") { 2 } else { 1 }; - let state = canvas_c::canvas_native_webgl_create_d3d( + let create = if self.threaded_webgl { + canvas_c::canvas_native_webgl_create_d3d_threaded + } else { + canvas_c::canvas_native_webgl_create_d3d + }; + let state = create( self.surface_width as i32, self.surface_height as i32, version, @@ -485,9 +504,7 @@ impl NSCCanvas { pub fn is_context_lost(&self) -> bool { match self.context { Context::TwoD(context) => canvas_c::canvas_native_context_is_lost(context), - Context::WebGL(state) => { - canvas_webgl::webgl::canvas_native_webgl_get_is_context_lost(unsafe { &mut *state }.get_inner_mut()) - } + Context::WebGL(state) => canvas_c::canvas_native_webgl_get_is_context_lost(state), Context::None | Context::WebGPU(_) => false, } } diff --git a/crates/canvas-napi/src/lib.rs b/crates/canvas-napi/src/lib.rs index 3412771f0..1cb41a4c4 100644 --- a/crates/canvas-napi/src/lib.rs +++ b/crates/canvas-napi/src/lib.rs @@ -32,9 +32,26 @@ pub fn install_global(exports: napi::bindgen_prelude::Object, env: napi::Env) -> logger::install(env.raw())?; frame::install_microtask_scheduler(env.raw())?; fast::install(env.raw(), napi::JsValue::raw(&exports))?; + install_canvases_behind(env.raw(), napi::JsValue::raw(&exports)); let mut global = env.get_global()?; if !global.has_named_property("CanvasModule")? { global.set_named_property("CanvasModule", exports)?; } Ok(()) } + +/// `CanvasModule.__canvasesBehind`: the count of threaded canvases behind +/// (`canvas_native_canvases_behind`) as memory, which packages/canvas reads every frame. Left out +/// where the host allows no external buffers: requestAnimationFrame is then left alone. +fn install_canvases_behind(env: napi::sys::napi_env, exports: napi::sys::napi_value) { + use napi::sys; + let address = canvas_c::canvas_native_canvases_behind_address() as *mut std::ffi::c_void; + let mut buffer = std::ptr::null_mut(); + unsafe { + if sys::napi_create_external_arraybuffer(env, address, std::mem::size_of::(), None, std::ptr::null_mut(), &mut buffer) + == sys::Status::napi_ok + { + sys::napi_set_named_property(env, exports, c"__canvasesBehind".as_ptr(), buffer); + } + } +} diff --git a/crates/canvas-webgl/src/prelude.rs b/crates/canvas-webgl/src/prelude.rs index ad19a448b..7a634f6de 100644 --- a/crates/canvas-webgl/src/prelude.rs +++ b/crates/canvas-webgl/src/prelude.rs @@ -628,12 +628,38 @@ impl WebGLState { unsafe { self.context.attach_swap_chain_panel(panel, alpha) } } + /// `attach_swap_chain_panel` for a context on another thread than the UI's: the swapchain to + /// show in the panel there. + #[cfg(target_os = "windows")] + pub fn create_panel_swap_chain(&mut self) -> Option { + let alpha = self.get_attributes().get_alpha(); + self.context.create_panel_swap_chain(alpha) + } + /// Presents into a XAML `SurfaceImageSource` instead (a canvas that blends with the page). #[cfg(target_os = "windows")] pub unsafe fn attach_xaml_surface(&mut self, source: *mut std::ffi::c_void) -> bool { unsafe { self.context.attach_xaml_surface(source) } } + /// `attach_xaml_surface` for a context on another thread than the UI's. + #[cfg(target_os = "windows")] + pub fn attach_xaml_handoff(&mut self, handoff: std::sync::Arc) -> bool { + self.context.attach_xaml_handoff(handoff) + } + + /// `present` for a context on a thread of its own (`GLContext::present_or_hold`). + #[cfg(target_os = "windows")] + pub fn present_or_hold(&self) -> bool { + self.context.present_or_hold() + } + + /// `true` while a frame `present_or_hold` held is still waiting for the display. + #[cfg(target_os = "windows")] + pub fn present_held(&self) -> bool { + self.context.present_held() + } + #[cfg(target_os = "windows")] pub fn set_swap_chain_transform(&self, scale_x: f32, scale_y: f32, offset_x: f32, offset_y: f32) -> bool { self.context.set_swap_chain_transform(scale_x, scale_y, offset_x, offset_y) diff --git a/packages/canvas/Canvas/index.d.ts b/packages/canvas/Canvas/index.d.ts index 247346ff1..251f3e1b5 100644 --- a/packages/canvas/Canvas/index.d.ts +++ b/packages/canvas/Canvas/index.d.ts @@ -42,7 +42,7 @@ export declare class Canvas extends CanvasBase { /** Default for `getContext('2d', { threaded })`: rasterize on a shared render thread (Android, iOS, Windows). */ static threaded2D: boolean; - /** Default for `getContext('webgl' | 'webgl2', { threaded })`: run the context on the WebGL thread (Android). */ + /** Default for `getContext('webgl' | 'webgl2', { threaded })`: run the context on a shared WebGL thread (Android, iOS, Windows). */ static threadedWebGL: boolean; surfaceOnTop: boolean; diff --git a/packages/canvas/Canvas/index.windows.ts b/packages/canvas/Canvas/index.windows.ts index 5cb1df921..2fd62a41d 100644 --- a/packages/canvas/Canvas/index.windows.ts +++ b/packages/canvas/Canvas/index.windows.ts @@ -59,6 +59,8 @@ const WHEEL_PIXELS_PER_NOTCH = 100; export class Canvas extends NapiCanvas { /** Default for `getContext('2d', { threaded })`: rasterize on a shared render thread. */ static threaded2D = true; + /** Default for `getContext('webgl' | 'webgl2', { threaded })`: run the context on the shared WebGL thread. */ + static threadedWebGL = true; private _panel: any; private _ignoreTouchEvents = false; diff --git a/packages/canvas/Canvas/napi-host.ts b/packages/canvas/Canvas/napi-host.ts index fb3029e36..3519d676c 100644 --- a/packages/canvas/Canvas/napi-host.ts +++ b/packages/canvas/Canvas/napi-host.ts @@ -15,7 +15,7 @@ import { WebGL2RenderingContext } from '../WebGL2/WebGL2RenderingContext'; import { GPUCanvasContext } from '../WebGPU'; import { ImageBitmapRenderingContext } from '../ImageBitmapRenderingContext'; import { ImageSource, Screen, Utils, widthProperty, heightProperty } from '@nativescript/core'; -import { handleContextOptions, microtask, CanvasContextType, isPercentLength, setParentPercentSize } from './utils'; +import { handleContextOptions, microtask, CanvasContextType, isPercentLength, setParentPercentSize, holdBackAnimationFramesWhileBehind } from './utils'; import { Helpers } from '../helpers'; export function createSVGMatrix(): DOMMatrix { @@ -131,6 +131,8 @@ export abstract class NapiCanvas extends CanvasBase { static forceGL = false; /** Default for `getContext('2d', { threaded })` where the host can rasterize on a render thread. */ static threaded2D = false; + /** Default for `getContext('webgl' | 'webgl2', { threaded })` where the host can run the context on a WebGL thread. */ + static threadedWebGL = false; surfaceOnTop = false; protected constructor() { @@ -512,6 +514,9 @@ export abstract class NapiCanvas extends CanvasBase { if ('threaded2D' in this._canvas) { this._canvas.threaded2D = threaded; } + if (threaded) { + holdBackAnimationFramesWhileBehind(); + } const ctx = this._canvas.create2DContext(opts.alpha, opts.antialias, opts.depth, opts.failIfMajorPerformanceCaveat, opts.powerPreference, opts.premultipliedAlpha, opts.preserveDrawingBuffer, opts.stencil, opts.desynchronized, opts.xrCompatible, opts.fontColor, opts.willReadFrequently ?? false, opts.colorSpace ?? 0); const context = new (CanvasRenderingContext2D as any)(ctx, opts); context._canvas = this; @@ -566,6 +571,13 @@ export abstract class NapiCanvas extends CanvasBase { const opts = { version: isWebGL2 ? 2 : 1, ...defaultOpts, ...handleContextOptions(type as CanvasContextType, options) }; // GL renders bottom-up. this._prepareSurface(opts.alpha !== false, true); + const threaded = 'threadedWebGL' in this._canvas && !!(options?.threaded ?? (this.constructor as typeof NapiCanvas).threadedWebGL); + if ('threadedWebGL' in this._canvas) { + this._canvas.threadedWebGL = threaded; + } + if (threaded) { + holdBackAnimationFramesWhileBehind(); + } this._canvas.initContext(type, opts.alpha, false, opts.depth, opts.failIfMajorPerformanceCaveat, opts.powerPreference, opts.premultipliedAlpha, opts.preserveDrawingBuffer, opts.stencil, opts.desynchronized, opts.xrCompatible, false, opts.colorSpace ?? 0); if (isWebGL2) { this._webgl2Context = new (WebGL2RenderingContext as any)(this._canvas, opts); diff --git a/packages/canvas/platforms/windows/arm64/canvasnative.node b/packages/canvas/platforms/windows/arm64/canvasnative.node index 0f87ef36f..ad5711dfa 100644 Binary files a/packages/canvas/platforms/windows/arm64/canvasnative.node and b/packages/canvas/platforms/windows/arm64/canvasnative.node differ diff --git a/packages/canvas/platforms/windows/x64/canvasnative.node b/packages/canvas/platforms/windows/x64/canvasnative.node index 1b72231a1..c680d26b2 100644 Binary files a/packages/canvas/platforms/windows/x64/canvasnative.node and b/packages/canvas/platforms/windows/x64/canvasnative.node differ