diff --git a/crates/canvas-2d/src/context/surface_vulkan.rs b/crates/canvas-2d/src/context/surface_vulkan.rs index 39605bb0e..00c966c6c 100644 --- a/crates/canvas-2d/src/context/surface_vulkan.rs +++ b/crates/canvas-2d/src/context/surface_vulkan.rs @@ -185,6 +185,53 @@ impl Context { } } + fn offscreen_vulkan_surface(&mut self, width: i32, height: i32, alpha: bool) -> Option { + let info = skia_safe::ImageInfo::new( + skia_safe::ISize::new(width.max(1), height.max(1)), + ColorType::N32, + if alpha { skia_safe::AlphaType::Premul } else { skia_safe::AlphaType::Opaque }, + >>::into(self.surface_data.color_space), + ); + gpu::surfaces::render_target( + self.direct_context.as_mut()?, + gpu::Budgeted::Yes, + &info, + None, + gpu::SurfaceOrigin::TopLeft, + None, + false, + None, + ) + } + + pub fn detach_vulkan_view(&mut self) { + if self.vulkan_context.is_none() { + return; + } + let bounds = self.surface_data.bounds; + let alpha = !self.surface_data.is_opaque; + let snapshot = self.surface.image_snapshot(); + let Some(mut surface) = + self.offscreen_vulkan_surface(bounds.width() as i32, bounds.height() as i32, alpha) + else { + return; + }; + let matrix = self.surface.canvas().local_to_device(); + let canvas = surface.canvas(); + canvas.draw_image(&snapshot, (0., 0.), None); + for _ in 0..self.state_stack.len() { + canvas.save(); + } + canvas.set_matrix(&matrix); + self.surface = surface; + self.vulkan_texture = None; + // The snapshot draw still reads the swapchain image. + self.flush_submit_and_sync_cpu(); + if let Some(vulkan_context) = self.vulkan_context.as_mut() { + vulkan_context.clear_view(); + } + } + pub fn resize_vulkan(context: &mut Context, width: f32, height: f32, alpha: bool) { // flush any pending draws before resizing context.flush_and_render_to_surface(); @@ -199,11 +246,27 @@ impl Context { let color_space = context.surface_data.color_space; + let Some(image) = image else { + let Some(surface) = context.offscreen_vulkan_surface(width as i32, height as i32, alpha) + else { + return; + }; + context.surface_data.state = Default::default(); + context.surface_data.is_opaque = !alpha; + context.surface_data.bounds = skia_safe::Rect::from_wh(width, height); + context.surface_state = SurfaceState::None; + context.surface = surface; + context.vulkan_texture = None; + context.path = Path::default(); + context.reset_state(); + return; + }; + if let Some(direct_context) = context.direct_context.as_mut() { let alloc = gpu::vk::Alloc::default(); let image_info = unsafe { gpu::vk::ImageInfo::new( - image.unwrap() as gpu::vk::Image, + image as gpu::vk::Image, alloc, gpu::vk::ImageTiling::OPTIMAL, gpu::vk::ImageLayout::UNDEFINED, diff --git a/crates/canvas-android/src/jni_compat/org_nativescript_canvas_NSCCanvas.rs b/crates/canvas-android/src/jni_compat/org_nativescript_canvas_NSCCanvas.rs index f2874130c..ab5860ae0 100644 --- a/crates/canvas-android/src/jni_compat/org_nativescript_canvas_NSCCanvas.rs +++ b/crates/canvas-android/src/jni_compat/org_nativescript_canvas_NSCCanvas.rs @@ -133,6 +133,57 @@ pub extern "system" fn nativeReleaseWebGPU(_: JNIEnv, _: JClass, context: jlong) } } +#[no_mangle] +pub extern "system" fn nativeDetachWebGPUSurface(_: JNIEnv, _: JClass, context: jlong) { + if context == 0 { + return; + } + + unsafe { + canvas_c::webgpu::gpu_canvas_context::canvas_native_webgpu_context_detach_surface( + context as _, + ); + } +} + +#[no_mangle] +pub extern "system" fn nativeDetach2DSurface(_: JNIEnv, _: JClass, context: jlong) { + if context == 0 { + return; + } + let context = unsafe { &mut *(context as *mut canvas_c::CanvasRenderingContext2D) }; + let context = context.get_context_mut(); + + if context.vulkan_context.is_some() { + context.detach_vulkan_view(); + return; + } + + let color_space = context.surface_data().color_space(); + let alpha = !context.surface_data().is_opaque(); + let width = context.surface_data().width() as i32; + let height = context.surface_data().height() as i32; + context.flush_and_render_to_surface(); + if let Some(gl_context) = context.gl_context.as_mut() { + let mut attr = canvas_core::context_attributes::ContextAttributes::new( + alpha, + false, + false, + false, + PowerPreference::Default, + true, + false, + false, + false, + true, + false, + color_space, + ); + gl_context.resize_pbuffer(&mut attr, width, height); + gl_context.make_current(); + } +} + // #[cfg(feature = "vulkan")] #[no_mangle] pub extern "system" fn nativeCreate2dContextVulkan( @@ -404,7 +455,6 @@ pub extern "system" fn nativeUpdate2DSurface( context.make_current(); } - #[cfg(feature = "vulkan")] if let Some(vulkan_context) = context.vulkan_context.as_mut() { vulkan_context.set_view( window.ptr().as_ptr() as *mut std::os::raw::c_void, diff --git a/crates/canvas-android/src/lib.rs b/crates/canvas-android/src/lib.rs index f79c1c03d..aaf825a0d 100644 --- a/crates/canvas-android/src/lib.rs +++ b/crates/canvas-android/src/lib.rs @@ -18,7 +18,7 @@ use log::LevelFilter; // #[cfg(feature = "vulkan")] use crate::jni_compat::org_nativescript_canvas_NSCCanvas::{nativeCreate2dContextVulkan, nativeGetVulkanVersion, nativeContext2DSetRenderFunc, nativeContext2DClearRenderFunc}; -use crate::jni_compat::org_nativescript_canvas_NSCCanvas::{nativeContext2DPathTest, nativeContext2DPathTestNormal, nativeContext2DRender, nativeContext2DTest, nativeContext2DTestNormal, nativeCreate2DContext, nativeCustomWithBitmapFlush, nativeInitWebGL, nativeInitWebGLNoSurface, nativeInitWebGPU, nativeMakeWebGLCurrent, nativeMakeWebGLCurrentNormal, nativeRelease2DContext, nativeRelease2DContextNormal, nativeReleaseWebGL, nativeReleaseWebGLNormal, nativeReleaseWebGPU, nativeResizeWebGPU, nativeUpdate2DSurface, nativeUpdate2DSurfaceNoSurface, nativeUpdate2DSurfaceNoSurfaceNormal, nativeUpdateGLNoSurface, nativeUpdateWebGLNoSurfaceNormal, nativeUpdateWebGLSurface, nativeWebGLC2DRender, nativeWriteCurrentWebGLContextToBitmap, nativeContext2DConicTest}; +use crate::jni_compat::org_nativescript_canvas_NSCCanvas::{nativeContext2DPathTest, nativeContext2DPathTestNormal, nativeContext2DRender, nativeContext2DTest, nativeContext2DTestNormal, nativeCreate2DContext, nativeCustomWithBitmapFlush, nativeInitWebGL, nativeInitWebGLNoSurface, nativeInitWebGPU, nativeMakeWebGLCurrent, nativeMakeWebGLCurrentNormal, nativeRelease2DContext, nativeRelease2DContextNormal, nativeReleaseWebGL, nativeReleaseWebGLNormal, nativeReleaseWebGPU, nativeDetachWebGPUSurface, nativeDetach2DSurface, nativeResizeWebGPU, nativeUpdate2DSurface, nativeUpdate2DSurfaceNoSurface, nativeUpdate2DSurfaceNoSurfaceNormal, nativeUpdateGLNoSurface, nativeUpdateWebGLNoSurfaceNormal, nativeUpdateWebGLSurface, nativeWebGLC2DRender, nativeWriteCurrentWebGLContextToBitmap, nativeContext2DConicTest}; use crate::jni_compat::org_nativescript_canvas_NSCCanvasRenderingContext2D::{nativeCreatePattern, nativeDrawAtlasWithBitmap, nativeDrawImageDxDyDwDhWithAsset, nativeDrawImageDxDyDwDhWithBitmap, nativeDrawImageDxDyWithAsset, nativeDrawImageDxDyWithBitmap, nativeDrawImageWithAsset, nativeDrawImageWithBitmap, nativeScale}; use crate::jni_compat::org_nativescript_canvas_NSCImageAsset::{nativeCreateImageAsset, nativeDestroyImageAsset, nativeGetDimensions, nativeGetError, nativeLoadFromBitmap, nativeLoadFromBuffer, nativeLoadFromBytes, nativeLoadFromEncodedBuffer, nativeLoadFromEncodedBytes, nativeLoadFromPath, nativeLoadFromUrl}; use crate::jni_compat::org_nativescript_canvas_NSCImageBitmap::{nativeLoadBitmapFromBuffer, nativeLoadBitmapFromBufferOptions, nativeLoadBitmapFromBufferRectOptions, nativeLoadBitmapFromBytes, nativeLoadBitmapFromBytesOptions, nativeLoadBitmapFromBytesRectOptions}; @@ -105,6 +105,8 @@ pub extern "system" fn JNI_OnLoad(vm: JavaVM, _reserved: *const c_void) -> jint // } canvas_method_names.push("nativeContext2DSetRenderFunc"); canvas_method_names.push("nativeContext2DClearRenderFunc"); + canvas_method_names.push("nativeDetachWebGPUSurface"); + canvas_method_names.push("nativeDetach2DSurface"); let canvas_signatures = if ret >= ANDROID_O { let mut ret = vec![ @@ -139,6 +141,8 @@ pub extern "system" fn JNI_OnLoad(vm: JavaVM, _reserved: *const c_void) -> jint ret.push("(JLjava/lang/Object;)V"); ret.push("(J)V"); + ret.push("(J)V"); + ret.push("(J)V"); ret } else { @@ -174,6 +178,8 @@ pub extern "system" fn JNI_OnLoad(vm: JavaVM, _reserved: *const c_void) -> jint ret.push("!(JLjava/lang/Object;)V"); ret.push("!(J)V"); + ret.push("!(J)V"); + ret.push("!(J)V"); ret }; @@ -238,6 +244,8 @@ pub extern "system" fn JNI_OnLoad(vm: JavaVM, _reserved: *const c_void) -> jint canvas_methods.push(nativeContext2DSetRenderFunc as *mut c_void); canvas_methods.push(nativeContext2DClearRenderFunc as *mut c_void); + canvas_methods.push(nativeDetachWebGPUSurface as *mut c_void); + canvas_methods.push(nativeDetach2DSurface as *mut c_void); let canvas_native_methods: Vec = izip!(canvas_method_names, canvas_signatures, canvas_methods) diff --git a/crates/canvas-android/src/utils/mod.rs b/crates/canvas-android/src/utils/mod.rs index ffcd1c0b9..2f5269d48 100644 --- a/crates/canvas-android/src/utils/mod.rs +++ b/crates/canvas-android/src/utils/mod.rs @@ -166,8 +166,7 @@ pub extern "system" fn nativeDestroyContext(context: jlong) { return; } - let context = context as *mut canvas_c::CanvasRenderingContext2D; - let _ = Box::from_raw(context); + canvas_c::canvas_native_context_release(context as *mut canvas_c::CanvasRenderingContext2D); } } diff --git a/crates/canvas-c/src/c2d/context.rs b/crates/canvas-c/src/c2d/context.rs index 3be25ef4d..c230280e5 100644 --- a/crates/canvas-c/src/c2d/context.rs +++ b/crates/canvas-c/src/c2d/context.rs @@ -1,6 +1,7 @@ use std::cmp::PartialEq; use std::ffi::{CStr, CString}; use std::os::raw::{c_char, c_void}; +use std::sync::atomic::{AtomicUsize, Ordering}; use canvas_2d::context::compositing::composite_operation_type::CompositeOperationType; use canvas_2d::context::fill_and_stroke_styles::paint::paint_style_set_color_with_string; @@ -42,6 +43,7 @@ pub struct CanvasRenderingContext2D { pub(crate) context: Context, alpha: bool, engine: Engine, + refs: AtomicUsize, } impl CanvasRenderingContext2D { @@ -78,12 +80,22 @@ pub extern "C" fn canvas_native_context_clear_render_func(value: i64) { context.context.cpu_context = None; } +#[no_mangle] +pub extern "C" fn canvas_native_context_reference(value: *const CanvasRenderingContext2D) { + if value.is_null() { + return; + } + unsafe { &*value }.refs.fetch_add(1, Ordering::Relaxed); +} + #[no_mangle] pub extern "C" fn canvas_native_context_release(value: *mut CanvasRenderingContext2D) { if value.is_null() { return; } - unsafe { drop(Box::from_raw(value)) }; + if unsafe { &*value }.refs.fetch_sub(1, Ordering::AcqRel) == 1 { + unsafe { drop(Box::from_raw(value)) }; + } } fn to_data_url(context: &mut CanvasRenderingContext2D, format: &str, quality: u32) -> String { @@ -180,6 +192,7 @@ impl CanvasRenderingContext2D { context, alpha, engine: Engine::CPU, + refs: AtomicUsize::new(1), } } @@ -189,6 +202,7 @@ impl CanvasRenderingContext2D { context, alpha, engine: Engine::GL, + refs: AtomicUsize::new(1), } } @@ -198,6 +212,7 @@ impl CanvasRenderingContext2D { context, alpha, engine: Engine::Vulkan, + refs: AtomicUsize::new(1), } } @@ -207,6 +222,7 @@ impl CanvasRenderingContext2D { context, alpha, engine: Engine::Metal, + refs: AtomicUsize::new(1), } } @@ -427,6 +443,7 @@ pub extern "C" fn canvas_native_context_create_d3d( context, alpha, engine: Engine::D3D, + refs: AtomicUsize::new(1), })); // Boxed: it stays at this address until released. unsafe { (*context).context.register_d3d() }; @@ -541,6 +558,7 @@ pub extern "C" fn canvas_native_context_create( ), alpha, engine: Engine::CPU, + refs: AtomicUsize::new(1), })) } @@ -574,6 +592,7 @@ pub extern "C" fn canvas_native_context_create_gl( context, alpha, engine: Engine::GL, + refs: AtomicUsize::new(1), })) } @@ -618,6 +637,7 @@ pub extern "C" fn canvas_native_context_create_gl_no_window( context, alpha, engine: Engine::GL, + refs: AtomicUsize::new(1), })) } diff --git a/crates/canvas-c/src/webgl/gl.rs b/crates/canvas-c/src/webgl/gl.rs index 13229aad6..0b82b3434 100644 --- a/crates/canvas-c/src/webgl/gl.rs +++ b/crates/canvas-c/src/webgl/gl.rs @@ -10,11 +10,10 @@ use canvas_2d::utils::image::from_image_slice; use canvas_core::context_attributes::{ColorSpace, PowerPreference}; use canvas_core::gpu::gl::GLContext; use canvas_webgl::prelude::WebGLVersion; -use std::collections::HashSet; use std::ffi::{CStr, CString}; use std::os::raw::{c_char, c_void}; use std::ptr::NonNull; -use std::sync::{Mutex, OnceLock}; +use std::sync::atomic::{AtomicUsize, Ordering}; /* GL */ @@ -261,7 +260,7 @@ pub extern "C" fn canvas_native_webgl_to_data_url( } #[derive(Debug)] -pub struct WebGLState(pub(crate) canvas_webgl::prelude::WebGLState); +pub struct WebGLState(pub(crate) canvas_webgl::prelude::WebGLState, AtomicUsize); impl WebGLState { pub fn get_dimensions(&self) -> (i32, i32) { @@ -277,41 +276,28 @@ impl WebGLState { } } -// keeps track of all live WebGLState handles so we can avoid double-freeing them -// todo : this is a bit of a hack, but it works for now. We should probably use a more robust solution in the future. -fn webgl_state_live() -> &'static Mutex> { - static LIVE: OnceLock>> = OnceLock::new(); - LIVE.get_or_init(|| Mutex::new(HashSet::new())) -} - -#[inline] -fn webgl_state_register(ptr: *mut WebGLState) -> *mut WebGLState { - if !ptr.is_null() { - webgl_state_live() - .lock() - .unwrap_or_else(|e| e.into_inner()) - .insert(ptr as usize); +#[no_mangle] +pub extern "C" fn canvas_native_webgl_state_reference(state: *const WebGLState) { + if state.is_null() { + return; } - ptr + unsafe { &*state }.1.fetch_add(1, Ordering::Relaxed); } - -fn webgl_state_free(ptr: *mut WebGLState) { - if ptr.is_null() { +#[no_mangle] +pub extern "C" fn canvas_native_webgl_state_destroy(state: *mut WebGLState) { + if state.is_null() { return; } - let present = webgl_state_live() - .lock() - .unwrap_or_else(|e| e.into_inner()) - .remove(&(ptr as usize)); - if present { - let _ = unsafe { Box::from_raw(ptr) }; + if unsafe { &*state }.1.fetch_sub(1, Ordering::AcqRel) == 1 { + let _ = unsafe { Box::from_raw(state) }; } } -#[no_mangle] -pub extern "C" fn canvas_native_webgl_state_destroy(state: *mut WebGLState) { - webgl_state_free(state); +impl WebGLState { + fn wrap(state: canvas_webgl::prelude::WebGLState) -> Self { + Self(state, AtomicUsize::new(1)) + } } impl WebGLState { @@ -358,7 +344,7 @@ impl WebGLState { GLContext::create_offscreen_context(&mut attr, width, height) }?; - Some(Self( + Some(Self::wrap( canvas_webgl::prelude::WebGLState::new_with_context_attributes( context, version, @@ -410,7 +396,7 @@ impl WebGLState { ColorSpace::Srgb, ); let context = GLContext::create_window_context(&mut attr, NonNull::new(view)?); - Some(Self( + Some(Self::wrap( canvas_webgl::prelude::WebGLState::new_with_context_attributes( context?, version, @@ -444,7 +430,7 @@ impl WebGLState { xr_compatible: bool, is_canvas: bool, ) -> Self { - Self( + Self::wrap( canvas_webgl::prelude::WebGLState::new_with_context_attributes( context, version, @@ -1702,7 +1688,7 @@ pub extern "C" fn canvas_native_webgl_create( false, ) { None => std::ptr::null_mut(), - Some(state) => webgl_state_register(Box::into_raw(Box::new(state))), + Some(state) => Box::into_raw(Box::new(state)), } } _ => std::ptr::null_mut(), @@ -1746,7 +1732,7 @@ pub extern "C" fn canvas_native_webgl_create( false, ) { None => std::ptr::null_mut(), - Some(state) => webgl_state_register(Box::into_raw(Box::new(state))), + Some(state) => Box::into_raw(Box::new(state)), } } _ => std::ptr::null_mut(), @@ -1792,7 +1778,7 @@ pub extern "C" fn canvas_native_webgl_create_no_window( is_canvas, ) { None => std::ptr::null_mut(), - Some(state) => webgl_state_register(Box::into_raw(Box::new(state))), + Some(state) => Box::into_raw(Box::new(state)), } } _ => std::ptr::null_mut(), @@ -1842,7 +1828,7 @@ pub extern "C" fn canvas_native_webgl_create_d3d( let Some(ctx) = GLContext::create_texture_context(&mut attrs, width, height) else { return std::ptr::null_mut(); }; - let state = WebGLState(canvas_webgl::prelude::WebGLState::new_with_context_attributes( + let state = WebGLState::wrap(canvas_webgl::prelude::WebGLState::new_with_context_attributes( ctx, version, attrs.get_alpha(), @@ -1858,7 +1844,7 @@ pub extern "C" fn canvas_native_webgl_create_d3d( false, version == WebGLVersion::V1, )); - webgl_state_register(Box::into_raw(Box::new(state))) + Box::into_raw(Box::new(state)) } /// Windows: shows a `canvas_native_webgl_create_d3d` context in a `SwapChainPanel` (any COM diff --git a/crates/canvas-c/src/webgpu/gpu_canvas_context.rs b/crates/canvas-c/src/webgpu/gpu_canvas_context.rs index c57075c17..39794de5d 100644 --- a/crates/canvas-c/src/webgpu/gpu_canvas_context.rs +++ b/crates/canvas-c/src/webgpu/gpu_canvas_context.rs @@ -56,6 +56,9 @@ pub struct CanvasGPUCanvasContext { pub(crate) data: parking_lot::Mutex>, pub(crate) view_data: parking_lot::Mutex, pub(crate) current_texture: parking_lot::Mutex>>, + pub(crate) surface_lost: std::sync::atomic::AtomicBool, + pub(crate) offscreen_texture: parking_lot::Mutex>, + pub(crate) last_capabilities: parking_lot::Mutex>, /// Windows: the SwapChainPanel stand-in wgpu binds its swapchain through (keeps the /// swapchain's DPI / fit transform ours). #[cfg(all(target_os = "windows", feature = "d3d"))] @@ -76,19 +79,146 @@ fn discard_current_texture( surface: &Arc, operation: &'static str, ) { - let had_current = context.current_texture.lock().take().is_some(); - if had_current - && !context + let current = context.current_texture.lock().take(); + if let Some(current) = current { + if !context .has_surface_presented .load(std::sync::atomic::Ordering::SeqCst) + { + if current.surface_id.is_some() { + if let Err(cause) = surface.discard() { + log::warn!("{operation}: surface discard failed: {cause:?}"); + } + } + context + .has_surface_presented + .store(true, std::sync::atomic::Ordering::SeqCst); + } + } +} + +fn copy_capabilities(capabilities: &SurfaceCapabilities) -> SurfaceCapabilities { + SurfaceCapabilities { + formats: capabilities.formats.clone(), + format_capabilities: capabilities.format_capabilities.clone(), + present_modes: capabilities.present_modes.clone(), + alpha_modes: capabilities.alpha_modes.clone(), + usages: capabilities.usages, + } +} + +fn surface_capabilities( + context: &CanvasGPUCanvasContext, + surface: &wgpu_core::instance::Surface, + adapter: &Arc, +) -> Result { + if context + .surface_lost + .load(std::sync::atomic::Ordering::SeqCst) { - if let Err(cause) = surface.discard() { - log::warn!("{operation}: surface discard failed: {cause:?}"); + if let Some(capabilities) = context.last_capabilities.lock().as_ref() { + return Ok(copy_capabilities(capabilities)); } - context - .has_surface_presented - .store(true, std::sync::atomic::Ordering::SeqCst); + #[cfg(not(target_os = "android"))] + let format = wgt::TextureFormat::Bgra8Unorm; + #[cfg(any(target_os = "android"))] + let format = wgt::TextureFormat::Rgba8Unorm; + return Ok(SurfaceCapabilities { + formats: vec![format], + present_modes: vec![wgt::PresentMode::Fifo], + usages: wgt::TextureUsages::RENDER_ATTACHMENT + | wgt::TextureUsages::COPY_SRC + | wgt::TextureUsages::COPY_DST + | wgt::TextureUsages::TEXTURE_BINDING, + ..Default::default() + }); } + let capabilities = surface.get_capabilities(adapter)?; + *context.last_capabilities.lock() = Some(copy_capabilities(&capabilities)); + Ok(capabilities) +} + +fn offscreen_current_texture(context: &CanvasGPUCanvasContext) -> *const CanvasGPUTexture { + let data_guard = context.data.lock(); + let Some(surface_data) = data_guard.as_ref() else { + return std::ptr::null(); + }; + let data = surface_data.texture_data; + // present_surface copies it into the read-back texture. + let usage = data.usage | wgt::TextureUsages::COPY_SRC; + + let texture = { + let mut offscreen = context.offscreen_texture.lock(); + match offscreen.as_ref() { + Some(offscreen) + if offscreen.data.size == data.size + && offscreen.data.format == data.format + && offscreen.data.usage == usage => + { + Arc::clone(&offscreen.texture) + } + _ => { + let desc = wgt::TextureDescriptor { + label: Some(Cow::Borrowed("OffscreenCanvasTexture")), + size: data.size, + mip_level_count: 1, + sample_count: 1, + dimension: wgt::TextureDimension::D2, + format: data.format, + usage, + view_formats: surface_data.previous_configuration.view_formats.clone(), + }; + let texture = surface_data.device.device.create_texture(&desc); + *offscreen = Some(ReadBackTexture { + texture: Arc::clone(&texture), + data: TextureData { usage, ..data }, + }); + texture + } + } + }; + + context + .has_surface_presented + .store(false, std::sync::atomic::Ordering::SeqCst); + + let texture = Arc::new(CanvasGPUTexture { + label: None, + instance: context.instance.clone(), + texture, + surface_id: None, + owned: false, + depth_or_array_layers: 1, + dimension: super::enums::CanvasTextureDimension::D2, + format: data.format.into(), + mipLevelCount: 1, + sampleCount: 1, + width: data.size.width, + height: data.size.height, + usage: usage.bits(), + error_sink: surface_data.error_sink.clone(), + suboptimal: false, + status: SurfaceGetCurrentTextureStatus::Success, + has_surface_presented: context.has_surface_presented.clone(), + }); + let ret = Arc::into_raw(Arc::clone(&texture)); + *context.current_texture.lock() = Some(texture); + ret +} + +#[no_mangle] +pub unsafe extern "C" fn canvas_native_webgpu_context_detach_surface( + context: *const CanvasGPUCanvasContext, +) { + if context.is_null() { + return; + } + let context = &*context; + let surface = context.surface.lock(); + discard_current_texture(context, &surface, "canvas_native_webgpu_context_detach_surface"); + context + .surface_lost + .store(true, std::sync::atomic::Ordering::SeqCst); } #[no_mangle] @@ -440,6 +570,9 @@ pub unsafe extern "C" fn canvas_native_webgpu_context_create( view_data: Mutex::new(ViewData { width, height }), read_back_texture: Mutex::default(), current_texture: Mutex::default(), + surface_lost: Default::default(), + offscreen_texture: Mutex::default(), + last_capabilities: Mutex::default(), }; Arc::into_raw(Arc::new(ctx)) @@ -483,6 +616,10 @@ pub unsafe extern "C" fn canvas_native_webgpu_context_resize( match context.instance.instance().create_surface(Some(display_handle), window_handle) { Ok(surface_id) => { *surface = surface_id; + context + .surface_lost + .store(false, std::sync::atomic::Ordering::SeqCst); + *context.offscreen_texture.lock() = None; // Guard stays held -- `surface.configure(..)` below still needs it. context .has_surface_presented @@ -587,6 +724,9 @@ pub unsafe extern "C" fn canvas_native_webgpu_context_create( view_data: Mutex::new(ViewData { width, height }), read_back_texture: Mutex::default(), current_texture: Mutex::default(), + surface_lost: Default::default(), + offscreen_texture: Mutex::default(), + last_capabilities: Mutex::default(), }; Arc::into_raw(Arc::new(ctx)) } @@ -628,6 +768,9 @@ pub unsafe extern "C" fn canvas_native_webgpu_context_create_swap_chain_panel( data: Mutex::default(), view_data: Mutex::new(ViewData { width, height }), current_texture: Mutex::default(), + surface_lost: Default::default(), + offscreen_texture: Mutex::default(), + last_capabilities: Mutex::default(), panel: Some(target), })), Err(cause) => { @@ -755,6 +898,9 @@ pub unsafe extern "C" fn canvas_native_webgpu_context_create_uiview( data: Mutex::default(), view_data: Mutex::new(ViewData { width, height }), current_texture: Mutex::default(), + surface_lost: Default::default(), + offscreen_texture: Mutex::default(), + last_capabilities: Mutex::default(), }; Arc::into_raw(Arc::new(ctx)) @@ -909,6 +1055,9 @@ pub unsafe extern "C" fn canvas_native_webgpu_context_create_nsview( view_data: Mutex::new(ViewData { width, height }), read_back_texture: Mutex::default(), current_texture: Mutex::default(), + surface_lost: Default::default(), + offscreen_texture: Mutex::default(), + last_capabilities: Mutex::default(), }; Arc::into_raw(Arc::new(ctx)) @@ -1325,7 +1474,7 @@ pub unsafe extern "C" fn canvas_native_webgpu_context_configure( width, height, present_mode: config.presentMode.into(), - alpha_mode: match surface_id.get_capabilities(&device_ref.adapter) { + alpha_mode: match surface_capabilities(&context, &surface_id, &device_ref.adapter) { Ok(caps) => negotiate_alpha_mode(config.alphaMode.into(), &caps.alpha_modes), // No capabilities to check against: let configure validate it. Err(_) => config.alphaMode.into(), @@ -1386,7 +1535,15 @@ pub unsafe extern "C" fn canvas_native_webgpu_context_configure( }); } - if let Some(cause) = surface_id.configure(&device_id, &config) { + let error = if context + .surface_lost + .load(std::sync::atomic::Ordering::SeqCst) + { + None + } else { + surface_id.configure(&device_id, &config) + }; + if let Some(cause) = error { handle_error_fatal(cause, "canvas_native_webgpu_context_configure"); let mut lock = context.data.lock(); *lock = None; @@ -1483,6 +1640,13 @@ pub extern "C" fn canvas_native_webgpu_context_get_current_texture( } } + if context + .surface_lost + .load(std::sync::atomic::Ordering::SeqCst) + { + return offscreen_current_texture(context); + } + let surface_id = context.surface.lock(); let result = surface_id.get_current_texture(); @@ -1628,7 +1792,13 @@ pub unsafe extern "C" fn canvas_native_webgpu_context_present_surface( }; } - if let Err(cause) = surface_id.present() { + let presented = if texture.surface_id.is_some() { + surface_id.present().map(|_| ()) + } else { + Ok(()) + }; + + if let Err(cause) = presented { context .has_surface_presented .store(true, std::sync::atomic::Ordering::SeqCst); @@ -1682,9 +1852,9 @@ pub extern "C" fn canvas_native_webgpu_context_get_capabilities( let adapter_id = Arc::clone(&adapter.adapter); let context = unsafe { &*context }; - let surface_id = context.surface.lock(); + let surface = context.surface.lock(); - match surface_id.get_capabilities(&adapter_id) { + match surface_capabilities(context, &surface, &adapter_id) { Ok(capabilities) => { let cap: CanvasSurfaceCapabilities = capabilities.into(); Box::into_raw(Box::new(cap)) @@ -1706,9 +1876,9 @@ pub fn canvas_native_webgpu_context_get_capabilities_rust( let adapter_id = Arc::clone(&adapter.adapter); let context = unsafe { &*context }; - let surface_id = context.surface.lock(); + let surface = context.surface.lock(); - match surface_id.get_capabilities(&adapter_id) { + match surface_capabilities(context, &surface, &adapter_id) { Ok(capabilities) => Some(capabilities), Err(cause) => { handle_error_fatal(cause, diff --git a/crates/canvas-core/src/gpu/vulkan.rs b/crates/canvas-core/src/gpu/vulkan.rs index 531bbc141..77a5d7ad6 100644 --- a/crates/canvas-core/src/gpu/vulkan.rs +++ b/crates/canvas-core/src/gpu/vulkan.rs @@ -546,6 +546,24 @@ impl VulkanContext { } } + pub fn clear_view(&mut self) { + unsafe { + let _ = self.ash.device.device_wait_idle(); + for view in self.ash.swap_chain_image_view.take().unwrap_or_default() { + self.ash.device.destroy_image_view(view, None); + } + self.ash.swap_chain_images = None; + self.ash.current_index = None; + if let Some(swap_chain) = self.ash.swap_chain.take() { + self.ash.swap_chain_loader.destroy_swapchain(swap_chain, None); + } + if let Some(surface) = self.ash.surface.take() { + self.ash.surface_loader.destroy_surface(surface, None); + } + } + self.view = std::ptr::null_mut(); + } + /// Rebuilds the swapchain; also how an out-of-date one is recovered. False means the whole /// context has to be rebuilt. pub fn resize(&mut self, width: u32, height: u32) -> bool { diff --git a/crates/canvas-ios/src/lib.rs b/crates/canvas-ios/src/lib.rs index aee175387..dd2b42e80 100644 --- a/crates/canvas-ios/src/lib.rs +++ b/crates/canvas-ios/src/lib.rs @@ -204,8 +204,7 @@ pub extern "C" fn canvas_native_ios_release_2d_context(context: i64) { if context == 0 { return; } - let context = context as *mut CanvasRenderingContext2D; - let _ = unsafe { Box::from_raw(context) }; + canvas_c::canvas_native_context_release(context as *mut CanvasRenderingContext2D); } #[no_mangle] diff --git a/crates/canvas-napi/src/c2d/mod.rs b/crates/canvas-napi/src/c2d/mod.rs index 4ce31d727..eb434191a 100644 --- a/crates/canvas-napi/src/c2d/mod.rs +++ b/crates/canvas-napi/src/c2d/mod.rs @@ -40,8 +40,6 @@ use std::rc::Rc; #[napi(custom_finalize)] pub struct CanvasRenderingContext2D { pub(crate) context: *mut CCanvasRenderingContext2D, - /// `create2DContextWithPointer` wraps a context the host owns; everything else owns its own. - owns_context: bool, /// Dirty tracking: drawing marks the context, the host renders it at frame end. pub(crate) frame: Rc, continuous_render: Cell, @@ -49,9 +47,7 @@ pub struct CanvasRenderingContext2D { impl ObjectFinalize for CanvasRenderingContext2D { fn finalize(self, _: Env) -> Result<()> { - if self.owns_context { - canvas_c::canvas_native_context_release(self.context); - } + canvas_c::canvas_native_context_release(self.context); Ok(()) } } @@ -65,10 +61,9 @@ unsafe fn render_2d(context: *mut c_void) { } impl CanvasRenderingContext2D { - pub(crate) fn from_raw(context: *mut CCanvasRenderingContext2D, owns_context: bool) -> Self { + pub(crate) fn from_raw(context: *mut CCanvasRenderingContext2D) -> Self { Self { context, - owns_context, frame: FrameSlot::new(context as *mut c_void, render_2d), continuous_render: Cell::new(false), } @@ -150,7 +145,6 @@ impl CanvasRenderingContext2D { direction, canvas_c::CanvasColorSpace::Srgb, ), - true, ) } @@ -1448,15 +1442,19 @@ impl CanvasRenderingContext2D { #[napi(js_name = "create2DContext")] pub fn create_2d_context(pointer: napi::bindgen_prelude::BigInt) -> Option { let (pointer, _) = pointer.get_i64(); - (pointer != 0).then(|| CanvasRenderingContext2D::from_raw(pointer as _, true)) + (pointer != 0).then(|| CanvasRenderingContext2D::from_raw(pointer as _)) } -/// `CanvasModule.create2DContextWithPointer(pointer)`: wraps a context the host view owns. +/// `CanvasModule.create2DContextWithPointer(pointer)`: wraps the host view's context. #[napi(js_name = "create2DContextWithPointer")] pub fn create_2d_context_with_pointer( pointer: napi::bindgen_prelude::BigInt, ) -> Option { let (pointer, _) = pointer.get_i64(); let context = canvas_c::canvas_native_context_create_with_pointer(pointer); - (!context.is_null()).then(|| CanvasRenderingContext2D::from_raw(context, false)) + if context.is_null() { + return None; + } + canvas_c::canvas_native_context_reference(context); + Some(CanvasRenderingContext2D::from_raw(context)) } diff --git a/crates/canvas-napi/src/gl/mod.rs b/crates/canvas-napi/src/gl/mod.rs index 07bff3aaf..087d78cfd 100644 --- a/crates/canvas-napi/src/gl/mod.rs +++ b/crates/canvas-napi/src/gl/mod.rs @@ -48,8 +48,6 @@ pub struct HTMLCanvasSource<'env> { pub struct web_g_l_rendering_context { pub(crate) state: *mut WebGLState, pub(crate) invalidate_state: u32, - /// `createWebGLContext(options, pointer)` wraps a state the host view owns. - pub(crate) owns_state: bool, /// Dirty tracking: drawing marks the context, the host presents it at frame end. pub(crate) frame: Rc, pub(crate) continuous_render: Cell, @@ -57,9 +55,7 @@ pub struct web_g_l_rendering_context { impl ObjectFinalize for web_g_l_rendering_context { fn finalize(self, _: Env) -> Result<()> { - if self.owns_state { - canvas_c::canvas_native_webgl_state_destroy(self.state); - } + canvas_c::canvas_native_webgl_state_destroy(self.state); Ok(()) } } @@ -126,11 +122,10 @@ pub(crate) unsafe fn present_webgl(state: *mut std::ffi::c_void) { } impl web_g_l_rendering_context { - pub(crate) fn from_raw(state: *mut WebGLState, owns_state: bool) -> Self { + pub(crate) fn from_raw(state: *mut WebGLState) -> Self { Self { state, invalidate_state: 0, - owns_state, frame: FrameSlot::new(state as *mut std::ffi::c_void, present_webgl), continuous_render: Cell::new(false), } @@ -204,18 +199,22 @@ pub(crate) fn resolve_webgl_state( options: &Unknown, target: &Unknown, height: Option, -) -> Option<(*mut WebGLState, bool)> { +) -> Option<*mut WebGLState> { let options = GLOptions::parse(options); if options.version != version { return None; } if type_of(target) == ValueType::BigInt { let (pointer, _) = unsafe { target.cast::() }.ok()?.get_i64(); - return (pointer != 0).then_some((pointer as *mut WebGLState, false)); + if pointer == 0 { + return None; + } + canvas_c::canvas_native_webgl_state_reference(pointer as *const WebGLState); + return Some(pointer as *mut WebGLState); } let width = as_number(target).unwrap_or(300.) as i32; let state = options.create_offscreen(width, height.unwrap_or(150.) as i32); - (!state.is_null()).then_some((state, true)) + (!state.is_null()).then_some(state) } /// `CanvasModule.createWebGLContext(options, pointer, scale, color, ppi, direction)` wraps the @@ -226,8 +225,8 @@ pub fn create_web_g_l_context( target: Unknown, height: Option, ) -> Option { - let (state, owns) = resolve_webgl_state(1, &options, &target, height)?; - Some(web_g_l_rendering_context::from_raw(state, owns)) + let state = resolve_webgl_state(1, &options, &target, height)?; + Some(web_g_l_rendering_context::from_raw(state)) } impl_webgl_context!(web_g_l_rendering_context); @@ -450,7 +449,7 @@ impl web_g_l_rendering_context { return Err(napi::Error::from_reason("Invalid parameter")); } - Ok(web_g_l_rendering_context::from_raw(ret, true)) + Ok(web_g_l_rendering_context::from_raw(ret)) } #[napi] diff --git a/crates/canvas-napi/src/gl2/mod.rs b/crates/canvas-napi/src/gl2/mod.rs index 75f49fa25..5985fdb51 100644 --- a/crates/canvas-napi/src/gl2/mod.rs +++ b/crates/canvas-napi/src/gl2/mod.rs @@ -23,18 +23,15 @@ use crate::{impl_webgl2_context_constants, impl_webgl_context, impl_webgl_contex pub struct web_g_l_2_rendering_context { pub(crate) state: *mut WebGLState, pub(crate) invalidate_state: u32, - /// `createWebGL2Context(options, pointer)` wraps a state the host view owns. - pub(crate) owns_state: bool, pub(crate) frame: std::rc::Rc, pub(crate) continuous_render: std::cell::Cell, } impl web_g_l_2_rendering_context { - pub(crate) fn from_raw(state: *mut WebGLState, owns_state: bool) -> Self { + pub(crate) fn from_raw(state: *mut WebGLState) -> Self { Self { state, invalidate_state: 0, - owns_state, frame: crate::frame::FrameSlot::new(state as *mut std::ffi::c_void, crate::gl::present_webgl), continuous_render: std::cell::Cell::new(false), } @@ -48,17 +45,15 @@ pub fn create_web_g_l_2_context( target: napi::bindgen_prelude::Unknown, height: Option, ) -> Option { - let (state, owns) = crate::gl::resolve_webgl_state(2, &options, &target, height)?; - Some(web_g_l_2_rendering_context::from_raw(state, owns)) + let state = crate::gl::resolve_webgl_state(2, &options, &target, height)?; + Some(web_g_l_2_rendering_context::from_raw(state)) } impl_webgl_context!(web_g_l_2_rendering_context); impl ObjectFinalize for web_g_l_2_rendering_context { fn finalize(self, _: Env) -> napi::Result<()> { - if self.owns_state { - canvas_c::canvas_native_webgl_state_destroy(self.state); - } + canvas_c::canvas_native_webgl_state_destroy(self.state); Ok(()) } } @@ -102,7 +97,7 @@ impl web_g_l_2_rendering_context { return Err(napi::Error::from_reason("Invalid parameter")); } - Ok(web_g_l_2_rendering_context::from_raw(ret, true)) + Ok(web_g_l_2_rendering_context::from_raw(ret)) } /* Transform feedback */ diff --git a/crates/canvas-napi/src/gpu/context.rs b/crates/canvas-napi/src/gpu/context.rs index 5508fb37c..7a109d6f8 100644 --- a/crates/canvas-napi/src/gpu/context.rs +++ b/crates/canvas-napi/src/gpu/context.rs @@ -29,12 +29,10 @@ use crate::gpu::parse::{ use crate::gpu::texture::g_p_u_texture; use crate::module::JsRaw; -/// `GPUCanvasContext` over a canvas-c context. `createWebGPUContextWithPointer` wraps one a host -/// view owns (and releases); the wrapper then must not release it. +/// `GPUCanvasContext` over a canvas-c context. #[napi(js_name = "GPUCanvasContext", custom_finalize)] pub struct g_p_u_canvas_context { pub(crate) context: *const CanvasGPUCanvasContext, - owns_context: bool, /// A frame whose texture `getCurrentTexture()` handed out is presented at frame end, as on /// the web, unless `presentSurface()` already did. pub(crate) frame: Rc, @@ -43,11 +41,9 @@ pub struct g_p_u_canvas_context { impl ObjectFinalize for g_p_u_canvas_context { fn finalize(self, _: Env) -> Result<()> { - if self.owns_context { - unsafe { - canvas_c::webgpu::gpu_canvas_context::canvas_native_webgpu_context_release(self.context) - }; - } + unsafe { + canvas_c::webgpu::gpu_canvas_context::canvas_native_webgpu_context_release(self.context) + }; Ok(()) } } @@ -162,10 +158,9 @@ fn present_to_present_mode(mode: &PresentMode) -> CanvasGPUPresentMode { } impl g_p_u_canvas_context { - pub(crate) fn from_raw(context: *const CanvasGPUCanvasContext, owns_context: bool) -> Self { + pub(crate) fn from_raw(context: *const CanvasGPUCanvasContext) -> Self { Self { context, - owns_context, frame: FrameSlot::new(context as *mut c_void, present_webgpu), continuous_render: Cell::new(false), } @@ -377,11 +372,14 @@ impl g_p_u_canvas_context { } } -/// `CanvasModule.createWebGPUContextWithPointer(pointer)`: wraps a `CanvasGPUCanvasContext` the -/// host view owns (it stays the host's to release). +/// `CanvasModule.createWebGPUContextWithPointer(pointer)`: wraps the host view's context. #[napi(js_name = "createWebGPUContextWithPointer")] pub fn create_webgpu_context_with_pointer(pointer: BigInt) -> Option { let (pointer, _) = pointer.get_i64(); - (pointer != 0) - .then(|| g_p_u_canvas_context::from_raw(pointer as *const CanvasGPUCanvasContext, false)) + if pointer == 0 { + return None; + } + let context = pointer as *const CanvasGPUCanvasContext; + unsafe { canvas_c::webgpu::gpu_canvas_context::canvas_native_webgpu_context_reference(context) }; + Some(g_p_u_canvas_context::from_raw(context)) } diff --git a/packages/canvas/platforms/ios/src/cpp/CanvasJSIModule.cpp b/packages/canvas/platforms/ios/src/cpp/CanvasJSIModule.cpp index 395c7f335..bd45f0b9f 100644 --- a/packages/canvas/platforms/ios/src/cpp/CanvasJSIModule.cpp +++ b/packages/canvas/platforms/ios/src/cpp/CanvasJSIModule.cpp @@ -830,9 +830,10 @@ void CanvasJSIModule::Create2DContextWithPointer(const v8::FunctionCallbackInfo< auto ptr = args[0]->ToBigInt(context).ToLocalChecked()->Int64Value(); auto context_2d = canvas_native_context_create_with_pointer(ptr); + canvas_native_context_reference(context_2d); auto ret = CanvasRenderingContext2DImpl::NewInstance(isolate, new CanvasRenderingContext2DImpl( - context_2d, false)); + context_2d, true)); args.GetReturnValue().Set(ret); } @@ -1093,6 +1094,7 @@ void CanvasJSIModule::CreateWebGLContext(const v8::FunctionCallbackInfo()->Int64Value(); auto webgl = (WebGLState *) ctx; + canvas_native_webgl_state_reference(webgl); auto renderingContext = WebGLRenderingContext::NewInstance(isolate, new WebGLRenderingContext( @@ -1177,6 +1179,7 @@ void CanvasJSIModule::CreateWebGL2Context(const v8::FunctionCallbackInfo()->Int64Value(); auto webgl = (WebGLState *) ctx; + canvas_native_webgl_state_reference(webgl); auto renderingContext = WebGL2RenderingContext::NewInstance(isolate, new WebGL2RenderingContext( webgl, @@ -1250,7 +1253,9 @@ CanvasJSIModule::CreateWebGPUContextWithPointer(const v8::FunctionCallbackInfoToBigInt(context).ToLocalChecked()->Int64Value(); auto wgpu = static_cast((void *) ptr); - + + canvas_native_webgpu_context_reference(wgpu); + auto ret = GPUCanvasContextImpl::NewInstance(isolate, new GPUCanvasContextImpl( wgpu)); args.GetReturnValue().Set(ret); diff --git a/packages/canvas/platforms/windows/arm64/canvasnative.node b/packages/canvas/platforms/windows/arm64/canvasnative.node index c3582fd73..c994e4e8e 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 b18bb85b7..cbcf22bd1 100644 Binary files a/packages/canvas/platforms/windows/x64/canvasnative.node and b/packages/canvas/platforms/windows/x64/canvasnative.node differ diff --git a/packages/canvas/src-native/canvas-android/canvas/src/main/cpp/include/canvas_native.h b/packages/canvas/src-native/canvas-android/canvas/src/main/cpp/include/canvas_native.h index d587f4498..e8ff96366 100644 --- a/packages/canvas/src-native/canvas-android/canvas/src/main/cpp/include/canvas_native.h +++ b/packages/canvas/src-native/canvas-android/canvas/src/main/cpp/include/canvas_native.h @@ -2090,6 +2090,8 @@ void canvas_native_context_set_render_func(int64_t value, void *data, void (*ren void canvas_native_context_clear_render_func(int64_t value); +void canvas_native_context_reference(const struct CanvasRenderingContext2D *value); + void canvas_native_context_release(struct CanvasRenderingContext2D *value); #if (defined(TARGET_OS_ANDROID) || defined(TARGET_OS_IOS) || defined(TARGET_OS_VISION)) @@ -4178,6 +4180,8 @@ const char *canvas_native_webgl_to_data_url(struct WebGLState *state, const char *format, uint32_t quality); +void canvas_native_webgl_state_reference(const struct WebGLState *state); + void canvas_native_webgl_state_destroy(struct WebGLState *state); void canvas_native_webgl_active_info_destroy(struct WebGLActiveInfo *info); diff --git a/packages/canvas/src-native/canvas-android/canvas/src/main/java/org/nativescript/canvas/NSCCanvas.kt b/packages/canvas/src-native/canvas-android/canvas/src/main/java/org/nativescript/canvas/NSCCanvas.kt index adf26b80a..2d48bf0e7 100644 --- a/packages/canvas/src-native/canvas-android/canvas/src/main/java/org/nativescript/canvas/NSCCanvas.kt +++ b/packages/canvas/src-native/canvas-android/canvas/src/main/java/org/nativescript/canvas/NSCCanvas.kt @@ -193,16 +193,39 @@ class NSCCanvas : FrameLayout { internal fun surfaceDestroyed() { listener?.surfaceDestroyed() - if (engine == Engine.GL && nativeContext != 0L) { - if (is2D) { - nativeUpdate2DSurfaceNoSurface(surfaceWidth, surfaceHeight, nativeContext) - } else { - nativeUpdateWebGLNoSurface(surfaceWidth, surfaceHeight, nativeContext) - } + // CPU contexts draw into their own view. + if (engine != Engine.CPU) { + detachSurface(nativeContext, engine, is2D) } isSurfaceDestroyed = true } + private fun detachSurface(ctx: Long, engine: Engine, is2D: Boolean) { + if (ctx == 0L) { + return + } + when (engine) { + Engine.GL -> { + if (is2D) { + nativeDetach2DSurface(ctx) + } else { + nativeUpdateWebGLNoSurface(surfaceWidth, surfaceHeight, ctx) + } + } + + Engine.GPU -> { + if (is2D) { + nativeDetach2DSurface(ctx) + } else { + nativeDetachWebGPUSurface(ctx) + } + } + + Engine.CPU -> nativeContext2DClearRenderFunc(ctx) + Engine.None -> {} + } + } + // GL/EGL and Skia-GPU teardown is thread-affine: the native context is // created and rendered on the thread that owns it (the main/UI thread that // drives WebGL/canvas). Whatever thread calls this, hand the release back to @@ -233,10 +256,10 @@ class NSCCanvas : FrameLayout { val engine = this.engine val is2D = this.is2D mainHandler.post { + detachSurface(ctx, engine, is2D) when (engine) { Engine.None -> {} Engine.CPU -> { - nativeContext2DClearRenderFunc(ctx) nativeRelease2DContext(ctx) } Engine.GL -> { @@ -1204,6 +1227,14 @@ class NSCCanvas : FrameLayout { @FastNative external fun nativeReleaseWebGPU(context: Long) + @JvmStatic + @FastNative + external fun nativeDetachWebGPUSurface(context: Long) + + @JvmStatic + @FastNative + external fun nativeDetach2DSurface(context: Long) + @JvmStatic @FastNative external fun nativeInitWebGL( diff --git a/packages/canvas/src-native/canvas-ios/CanvasNative/Source/CanvasHelpers.swift b/packages/canvas/src-native/canvas-ios/CanvasNative/Source/CanvasHelpers.swift index 1866f8005..1e504991d 100644 --- a/packages/canvas/src-native/canvas-ios/CanvasNative/Source/CanvasHelpers.swift +++ b/packages/canvas/src-native/canvas-ios/CanvasNative/Source/CanvasHelpers.swift @@ -196,6 +196,10 @@ public class CanvasHelpers: NSObject { public static func release2DContext(_ context: Int64) { canvas_native_ios_release_2d_context(context) } + + public static func releaseWebGPU(_ context: Int64) { + canvas_native_webgpu_context_release(OpaquePointer(bitPattern: Int(context))) + } public static func create2DContext( _ view: NSCCanvas, diff --git a/packages/canvas/src-native/canvas-ios/CanvasNative/Source/NSCCanvas.swift b/packages/canvas/src-native/canvas-ios/CanvasNative/Source/NSCCanvas.swift index 2261c3fe9..511ecb6b0 100644 --- a/packages/canvas/src-native/canvas-ios/CanvasNative/Source/NSCCanvas.swift +++ b/packages/canvas/src-native/canvas-ios/CanvasNative/Source/NSCCanvas.swift @@ -997,6 +997,8 @@ public class NSCCanvas: UIView { CanvasHelpers.release2DContext(nativeContext) }else if(engine == .GL){ CanvasHelpers.releaseWebGL(nativeContext) + }else if(engine == .GPU){ + CanvasHelpers.releaseWebGPU(nativeContext) } nativeContext = 0 } diff --git a/packages/canvas/src-native/canvas-ios/CanvasNative/include/canvas_native.h b/packages/canvas/src-native/canvas-ios/CanvasNative/include/canvas_native.h index d587f4498..e8ff96366 100644 --- a/packages/canvas/src-native/canvas-ios/CanvasNative/include/canvas_native.h +++ b/packages/canvas/src-native/canvas-ios/CanvasNative/include/canvas_native.h @@ -2090,6 +2090,8 @@ void canvas_native_context_set_render_func(int64_t value, void *data, void (*ren void canvas_native_context_clear_render_func(int64_t value); +void canvas_native_context_reference(const struct CanvasRenderingContext2D *value); + void canvas_native_context_release(struct CanvasRenderingContext2D *value); #if (defined(TARGET_OS_ANDROID) || defined(TARGET_OS_IOS) || defined(TARGET_OS_VISION)) @@ -4178,6 +4180,8 @@ const char *canvas_native_webgl_to_data_url(struct WebGLState *state, const char *format, uint32_t quality); +void canvas_native_webgl_state_reference(const struct WebGLState *state); + void canvas_native_webgl_state_destroy(struct WebGLState *state); void canvas_native_webgl_active_info_destroy(struct WebGLActiveInfo *info); diff --git a/tools/demo/canvas/spec/index.ts b/tools/demo/canvas/spec/index.ts index 6309ce41a..ac29b60fc 100644 --- a/tools/demo/canvas/spec/index.ts +++ b/tools/demo/canvas/spec/index.ts @@ -6,7 +6,7 @@ * adb logcat -d | grep 'SPEC|' * * `--es suite ` narrows the run: a group (`2d`, `webgl`, `webgpu`, - * `imagebitmap`, `bitmaprenderer`, `audio` and `media` on Windows) or a single suite (`2d.path2d`). + * `imagebitmap`, `bitmaprenderer`, `lifecycle`, `audio` and `media` on Windows) or a single suite (`2d.path2d`). * `contextlost` (Windows) runs on its own: it removes the GPU device. */ @@ -20,6 +20,7 @@ import { registerScalingSpec } from './scaling'; import { registerContextLossSpec } from './contextloss'; import { registerAudioSpec } from './audio'; import { registerMediaSpec } from './media'; +import { registerLifecycleSpec } from './lifecycle'; declare const __WINDOWS__: boolean; @@ -36,6 +37,7 @@ export async function runSpecTests(group: SpecGroup = 'all') { registerWebGPUSpec(); registerCanvasSourceSpec(); registerScalingSpec(); + registerLifecycleSpec(); if (typeof __WINDOWS__ !== 'undefined' && __WINDOWS__) { registerAudioSpec(); registerMediaSpec(); diff --git a/tools/demo/canvas/spec/lifecycle.ts b/tools/demo/canvas/spec/lifecycle.ts new file mode 100644 index 000000000..9720add2f --- /dev/null +++ b/tools/demo/canvas/spec/lifecycle.ts @@ -0,0 +1,151 @@ +import { Canvas } from '@nativescript/canvas'; +import { GridLayout, Utils } from '@nativescript/core'; +import { suite, test, ok, equal, getPageCanvas, pixelEqual } from './harness'; + +function wait(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +function frames(count: number) { + return new Promise((resolve) => { + const step = () => (--count <= 0 ? resolve() : requestAnimationFrame(step)); + requestAnimationFrame(step); + }); +} + +async function mount(): Promise { + const parent = getPageCanvas()?.parent as GridLayout; + if (!parent) { + throw new Error('no page layout to mount a canvas in'); + } + const canvas = new Canvas(); + canvas.style.width = 32; + canvas.style.height = 32; + GridLayout.setRow(canvas, 1); + const ready = new Promise((resolve) => canvas.once('ready', () => resolve())); + parent.addChild(canvas); + await Promise.race([ready, wait(3000)]); + await frames(2); + return canvas; +} + +function unmount(canvas: Canvas) { + (canvas.parent as GridLayout)?.removeChild(canvas); +} + +async function collect() { + Utils.GC(); + await frames(3); +} + +function glPixel(gl: any): number[] { + const pixel = new Uint8Array(4); + gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixel); + return Array.from(pixel); +} + +async function webglKeepsDrawing(type: 'webgl' | 'webgl2') { + const canvas = await mount(); + const gl = canvas.getContext(type) as any; + ok(gl, `no ${type} context`); + gl.clearColor(1, 0, 0, 1); + gl.clear(gl.COLOR_BUFFER_BIT); + unmount(canvas); + await frames(3); + gl.clearColor(0, 1, 0, 1); + gl.clear(gl.COLOR_BUFFER_BIT); + equal(glPixel(gl).join(','), '0,255,0,255'); + await collect(); + gl.clear(gl.COLOR_BUFFER_BIT); + equal(glPixel(gl).join(','), '0,255,0,255'); +} + +function renderFrame(device: any, ctx: any) { + const texture = ctx.getCurrentTexture(); + ok(texture, 'no current texture'); + const encoder = device.createCommandEncoder(); + const pass = encoder.beginRenderPass({ + colorAttachments: [{ view: texture.createView(), clearValue: { r: 0, g: 1, b: 0, a: 1 }, loadOp: 'clear', storeOp: 'store' }], + }); + pass.end(); + device.queue.submit([encoder.finish()]); + return texture; +} + +export function registerLifecycleSpec() { + suite('lifecycle', () => { + test('a 2d context keeps drawing after its canvas leaves the page', async () => { + const canvas = await mount(); + const ctx = canvas.getContext('2d') as any; + ok(ctx, 'no 2d context'); + ctx.fillStyle = '#ff0000'; + ctx.fillRect(0, 0, 16, 16); + unmount(canvas); + await frames(3); + ctx.fillStyle = '#00ff00'; + ctx.fillRect(0, 0, 16, 16); + pixelEqual(ctx, 4, 4, [0, 255, 0, 255]); + await collect(); + ctx.fillStyle = '#0000ff'; + ctx.fillRect(0, 0, 16, 16); + pixelEqual(ctx, 4, 4, [0, 0, 255, 255]); + }); + + test('a webgl context keeps drawing after its canvas leaves the page', () => webglKeepsDrawing('webgl')); + + test('a webgl2 context keeps drawing after its canvas leaves the page', () => webglKeepsDrawing('webgl2')); + + test('a webgpu context keeps rendering after its canvas leaves the page', async () => { + const adapter = await navigator.gpu.requestAdapter(); + const device = await adapter.requestDevice(); + const format = navigator.gpu.getPreferredCanvasFormat(); + const canvas = await mount(); + const ctx = canvas.getContext('webgpu') as any; + ok(ctx, 'no webgpu context'); + ctx.configure({ device, format, alphaMode: 'premultiplied' }); + renderFrame(device, ctx); + await frames(1); + const width = (canvas as any).width; + unmount(canvas); + for (let i = 0; i < 3; i++) { + equal(renderFrame(device, ctx).width, width, 'width'); + await frames(1); + } + ok(ctx.getCapabilities(adapter)?.format?.length, 'no formats once detached'); + ctx.configure({ device, format, alphaMode: 'premultiplied' }); + renderFrame(device, ctx); + await collect(); + renderFrame(device, ctx); + }); + + test('a webgpu context survives its surface going away and coming back', async () => { + const adapter = await navigator.gpu.requestAdapter(); + const device = await adapter.requestDevice(); + const canvas = await mount(); + const ctx = canvas.getContext('webgpu') as any; + ctx.configure({ device, format: navigator.gpu.getPreferredCanvasFormat(), alphaMode: 'premultiplied' }); + renderFrame(device, ctx); + canvas.visibility = 'collapse'; + await frames(3); + renderFrame(device, ctx); + await frames(1); + canvas.visibility = 'visible'; + await frames(3); + renderFrame(device, ctx); + await frames(1); + unmount(canvas); + }); + + test('contexts dropped with their canvas are collected safely', async () => { + for (const type of ['2d', 'webgl', 'webgl2']) { + const canvas = await mount(); + const ctx = canvas.getContext(type) as any; + ok(ctx, `no ${type} context`); + unmount(canvas); + } + await collect(); + await wait(200); + await collect(); + }); + }); +}