diff --git a/crates/canvas-2d/src/context/surface_d3d.rs b/crates/canvas-2d/src/context/surface_d3d.rs index c94fcc0e..f7a167d1 100644 --- a/crates/canvas-2d/src/context/surface_d3d.rs +++ b/crates/canvas-2d/src/context/surface_d3d.rs @@ -6,9 +6,10 @@ //! thread share one D3D12 device and one Skia `DirectContext`, so drawing one canvas into another //! stays on the GPU. -use std::cell::RefCell; +use std::cell::{Cell, RefCell}; use std::ffi::c_void; use std::rc::Rc; +use std::time::{Duration, Instant}; use canvas_core::context_attributes::ColorSpace; use canvas_core::gpu::d3d::{D3D12Context, PowerPreference}; @@ -30,6 +31,18 @@ thread_local! { static SKIA_D3D: RefCell, DirectContext)>> = const { RefCell::new(None) }; /// The thread's D3D canvases (`Context::register_d3d`), for `release_lost_canvases`. static CANVASES: RefCell> = const { RefCell::new(Vec::new()) }; + static LAST_PURGE: Cell> = const { Cell::new(None) }; +} + +const RESOURCE_CACHE_LIMIT: usize = 64 << 20; + +fn purge_idle_resources(context: &mut DirectContext) { + let now = Instant::now(); + if LAST_PURGE.with(|last| last.get().is_some_and(|at| now - at < Duration::from_secs(1))) { + return; + } + LAST_PURGE.with(|last| last.set(Some(now))); + context.perform_deferred_cleanup(Duration::from_secs(5), None); } /// The thread's device, a new one after a device loss. The adapter makes no new device while the @@ -84,7 +97,8 @@ fn shared_direct_context(device: &Rc) -> Option { if let Some((_, mut stale)) = shared.take() { stale.abandon(); } - let context = device.make_direct_context()?; + let mut context = device.make_direct_context()?; + context.set_resource_cache_limit(RESOURCE_CACHE_LIMIT); *shared = Some((device.clone(), context.clone())); Some(context) }) @@ -483,6 +497,9 @@ impl Context { return; } self.flush_surface(); + if let Some(direct_context) = self.direct_context.as_mut() { + purge_idle_resources(direct_context); + } let Some(target) = self.d3d.as_mut() else { return }; if let Some(xaml) = target.xaml.as_mut() { let Some(direct_context) = self.direct_context.as_mut() else { return }; @@ -507,6 +524,9 @@ impl Context { return; } let Some(swap_chain) = target.swap_chain.as_ref() else { return }; + if !swap_chain.acquire_frame() { + return; + } let Some(direct_context) = self.direct_context.as_mut() else { return }; let index = swap_chain.current_index() as usize; let Some(back_buffer) = target.back_buffers.get_mut(index) else { return }; diff --git a/crates/canvas-core/src/gpu/dxgi.rs b/crates/canvas-core/src/gpu/dxgi.rs index 6ae3ed55..48e0bb3f 100644 --- a/crates/canvas-core/src/gpu/dxgi.rs +++ b/crates/canvas-core/src/gpu/dxgi.rs @@ -6,7 +6,9 @@ //! maps it back (1 / composition scale), which is also how the canvas "fit" modes are applied //! without resizing buffers. +use std::cell::Cell; use std::ffi::c_void; +use std::time::{Duration, Instant}; use windows::core::{Interface, Result, HRESULT}; use windows::Win32::Foundation::{CloseHandle, HANDLE, WAIT_OBJECT_0}; @@ -36,6 +38,16 @@ pub unsafe trait ISwapChainPanelNative: windows::core::IUnknown { pub const BUFFER_COUNT: u32 = 2; +const MAX_FRAME_DEFERRAL: Duration = Duration::from_millis(250); + +thread_local! { + static PRESENT_DEFERRED: Cell = const { Cell::new(false) }; +} + +pub fn take_present_deferred() -> bool { + PRESENT_DEFERRED.with(|deferred| deferred.replace(false)) +} + /// WinUI 3's `ISurfaceImageSourceNative` (microsoft.ui.xaml.media.dxinterop.h; not the UWP IID). #[windows::core::interface("e4cecd6c-f14b-4f46-83c3-8bbda27c6504")] pub unsafe trait ISurfaceImageSourceNative: windows::core::IUnknown { @@ -317,6 +329,7 @@ pub struct CompositionSwapChain { width: u32, height: u32, flags: DXGI_SWAP_CHAIN_FLAG, + deferred_since: parking_lot::Mutex>, } impl CompositionSwapChain { @@ -375,6 +388,7 @@ impl CompositionSwapChain { width: width.max(1), height: height.max(1), flags, + deferred_since: parking_lot::Mutex::new(None), }) } @@ -439,6 +453,22 @@ impl CompositionSwapChain { unsafe { WaitForSingleObjectEx(self.waitable, 0, false) == WAIT_OBJECT_0 } } + /// `false`: the display hasn't taken the queued frames yet; skip this present. + pub fn acquire_frame(&self) -> bool { + let mut deferred_since = self.deferred_since.lock(); + if self.frame_ready() { + *deferred_since = None; + return true; + } + let now = Instant::now(); + if now.duration_since(*deferred_since.get_or_insert(now)) >= MAX_FRAME_DEFERRAL { + *deferred_since = None; + return true; + } + PRESENT_DEFERRED.with(|deferred| deferred.set(true)); + false + } + pub fn present(&self, vsync: bool) -> Result<()> { let (interval, flags) = if vsync { (1, DXGI_PRESENT::default()) diff --git a/crates/canvas-core/src/gpu/gl/egl.rs b/crates/canvas-core/src/gpu/gl/egl.rs index 3b84772a..d685ee3e 100644 --- a/crates/canvas-core/src/gpu/gl/egl.rs +++ b/crates/canvas-core/src/gpu/gl/egl.rs @@ -816,6 +816,9 @@ impl GLContext { let (Some(presenter), Some(texture)) = (self.0.presenter.as_ref(), self.0.texture.as_ref()) else { return true; }; + if !presenter.swap_chain.acquire_frame() { + return true; + } let Ok(back_buffer) = presenter.swap_chain.buffer::(0) else { return false; }; diff --git a/crates/canvas-napi/src/frame.rs b/crates/canvas-napi/src/frame.rs index 82fcf704..f1a979ee 100644 --- a/crates/canvas-napi/src/frame.rs +++ b/crates/canvas-napi/src/frame.rs @@ -7,7 +7,7 @@ //! frame. use std::cell::{Cell, RefCell}; -use std::ffi::c_void; +use std::ffi::{c_void, CStr}; use std::rc::{Rc, Weak}; use napi::sys; @@ -18,6 +18,7 @@ use napi::JsValue; pub struct FrameSlot { dirty: Cell, paused: Cell, + retrying: Cell, target: *mut c_void, flush: unsafe fn(*mut c_void), } @@ -28,6 +29,7 @@ impl FrameSlot { Rc::new(FrameSlot { dirty: Cell::new(false), paused: Cell::new(false), + retrying: Cell::new(false), target, flush, }) @@ -46,15 +48,36 @@ impl FrameSlot { } /// Renders now if dirty (e.g. before a readback). - pub fn flush_now(&self) { - if self.dirty.replace(false) { - unsafe { (self.flush)(self.target) }; + pub fn flush_now(self: &Rc) { + if !self.dirty.replace(false) { + return; + } + take_present_deferred(); + unsafe { (self.flush)(self.target) }; + if take_present_deferred() { + self.dirty.set(true); + if !self.retrying.replace(true) { + RETRY.with(|r| r.borrow_mut().push(Rc::downgrade(self))); + } + request_retry(); } } } +fn take_present_deferred() -> bool { + #[cfg(target_os = "windows")] + { + canvas_core::gpu::dxgi::take_present_deferred() + } + #[cfg(not(target_os = "windows"))] + { + false + } +} + thread_local! { static PENDING: RefCell>> = const { RefCell::new(Vec::new()) }; + static RETRY: RefCell>> = const { RefCell::new(Vec::new()) }; static SCHEDULER: RefCell>> = const { RefCell::new(None) }; /// A flush found a context's GPU device lost. static LOST: Cell = const { Cell::new(false) }; @@ -81,14 +104,25 @@ pub fn mark_dirty(slot: &Rc) { /// Flushes every queued, still-alive, dirty and unpaused context. Called by the host at the end /// of a frame (and by `CanvasModule.__flushAll()`). pub fn flush_all() { - let pending = PENDING.with(|p| std::mem::take(&mut *p.borrow_mut())); - for slot in pending.iter().filter_map(Weak::upgrade) { + let mut pending = PENDING.with(|p| std::mem::take(&mut *p.borrow_mut())); + pending.extend(RETRY.with(|r| std::mem::take(&mut *r.borrow_mut()))); + let pending: Vec<_> = pending.iter().filter_map(Weak::upgrade).collect(); + for slot in &pending { + slot.retrying.set(false); + } + for slot in &pending { if !slot.paused.get() { slot.flush_now(); } } } +fn request_retry() { + if let Some(scheduler) = MICROTASK.with(|m| m.borrow().clone()) { + scheduler.request_retry(); + } +} + /// A context found its GPU device lost while flushing. The listener /// (`__setContextLostListener`) hears about it once the flush is done. pub fn report_lost() { @@ -159,6 +193,8 @@ struct MicrotaskScheduler { queue_microtask: sys::napi_ref, flush: sys::napi_ref, queued: Cell, + retry: sys::napi_ref, + retry_queued: Cell, } thread_local! { @@ -176,7 +212,45 @@ unsafe extern "C" fn microtask_flush(env: sys::napi_env, _: sys::napi_callback_i undefined } +unsafe extern "C" fn retry_flush(env: sys::napi_env, _: sys::napi_callback_info) -> sys::napi_value { + if let Some(scheduler) = MICROTASK.with(|m| m.borrow().clone()) { + scheduler.retry_queued.set(false); + if RETRY.with(|r| !r.borrow().is_empty()) { + scheduler.request(); + } + } + let mut undefined = std::ptr::null_mut(); + unsafe { sys::napi_get_undefined(env, &mut undefined) }; + undefined +} + +unsafe fn call_global(env: sys::napi_env, global: sys::napi_value, name: &CStr, arg: sys::napi_value) -> bool { + let (mut function, mut kind, mut result) = (std::ptr::null_mut(), 0, std::ptr::null_mut()); + unsafe { + sys::napi_get_named_property(env, global, name.as_ptr(), &mut function) == sys::Status::napi_ok + && sys::napi_typeof(env, function, &mut kind) == sys::Status::napi_ok + && kind == sys::ValueType::napi_function + && sys::napi_call_function(env, global, function, 1, &arg, &mut result) == sys::Status::napi_ok + } +} + impl MicrotaskScheduler { + fn request_retry(&self) { + if self.retry_queued.replace(true) { + return; + } + unsafe { + let (mut retry, mut global) = (std::ptr::null_mut(), std::ptr::null_mut()); + let ok = sys::napi_get_reference_value(self.env, self.retry, &mut retry) == sys::Status::napi_ok + && sys::napi_get_global(self.env, &mut global) == sys::Status::napi_ok + && (call_global(self.env, global, c"requestAnimationFrame", retry) + || call_global(self.env, global, c"setTimeout", retry)); + if !ok { + self.retry_queued.set(false); + } + } + } + fn request(&self) { if self.queued.replace(true) { return; @@ -222,9 +296,19 @@ pub fn install_microtask_scheduler(env: sys::napi_env) -> napi::Result<()> { std::ptr::null_mut(), &mut flush, ))?; - let (mut queue_ref, mut flush_ref) = (std::ptr::null_mut(), std::ptr::null_mut()); + let mut retry = std::ptr::null_mut(); + napi::check_status!(sys::napi_create_function( + env, + c"__canvasRetry".as_ptr(), + -1, + Some(retry_flush), + std::ptr::null_mut(), + &mut retry, + ))?; + let (mut queue_ref, mut flush_ref, mut retry_ref) = (std::ptr::null_mut(), std::ptr::null_mut(), std::ptr::null_mut()); napi::check_status!(sys::napi_create_reference(env, queue_microtask, 1, &mut queue_ref))?; napi::check_status!(sys::napi_create_reference(env, flush, 1, &mut flush_ref))?; + napi::check_status!(sys::napi_create_reference(env, retry, 1, &mut retry_ref))?; napi::check_status!(sys::napi_add_env_cleanup_hook(env, Some(microtask_teardown), std::ptr::null_mut()))?; let scheduler = Rc::new(MicrotaskScheduler { @@ -232,6 +316,8 @@ pub fn install_microtask_scheduler(env: sys::napi_env) -> napi::Result<()> { queue_microtask: queue_ref, flush: flush_ref, queued: Cell::new(false), + retry: retry_ref, + retry_queued: Cell::new(false), }); MICROTASK.with(|m| *m.borrow_mut() = Some(scheduler.clone())); set_scheduler(Some(Box::new(move || scheduler.request()))); diff --git a/crates/canvas-napi/src/gl/base.rs b/crates/canvas-napi/src/gl/base.rs index e1243a00..9d64b934 100644 --- a/crates/canvas-napi/src/gl/base.rs +++ b/crates/canvas-napi/src/gl/base.rs @@ -339,7 +339,7 @@ macro_rules! impl_webgl_context { } #[napi] - pub fn delete_shader(&self, shader: &WebGLRenderbuffer) { + pub fn delete_shader(&self, shader: &WebGLShader) { canvas_c::canvas_native_webgl_delete_shader(shader.0, self.state) }