Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions crates/canvas-2d/src/context/surface_d3d.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -30,6 +31,18 @@ thread_local! {
static SKIA_D3D: RefCell<Option<(Rc<D3D12Context>, DirectContext)>> = const { RefCell::new(None) };
/// The thread's D3D canvases (`Context::register_d3d`), for `release_lost_canvases`.
static CANVASES: RefCell<Vec<*mut Context>> = const { RefCell::new(Vec::new()) };
static LAST_PURGE: Cell<Option<Instant>> = 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
Expand Down Expand Up @@ -84,7 +97,8 @@ fn shared_direct_context(device: &Rc<D3D12Context>) -> Option<DirectContext> {
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)
})
Expand Down Expand Up @@ -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 };
Expand All @@ -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 };
Expand Down
30 changes: 30 additions & 0 deletions crates/canvas-core/src/gpu/dxgi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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<bool> = 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 {
Expand Down Expand Up @@ -317,6 +329,7 @@ pub struct CompositionSwapChain {
width: u32,
height: u32,
flags: DXGI_SWAP_CHAIN_FLAG,
deferred_since: parking_lot::Mutex<Option<Instant>>,
}

impl CompositionSwapChain {
Expand Down Expand Up @@ -375,6 +388,7 @@ impl CompositionSwapChain {
width: width.max(1),
height: height.max(1),
flags,
deferred_since: parking_lot::Mutex::new(None),
})
}

Expand Down Expand Up @@ -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())
Expand Down
3 changes: 3 additions & 0 deletions crates/canvas-core/src/gpu/gl/egl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<windows::Win32::Graphics::Direct3D11::ID3D11Texture2D>(0) else {
return false;
};
Expand Down
100 changes: 93 additions & 7 deletions crates/canvas-napi/src/frame.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -18,6 +18,7 @@ use napi::JsValue;
pub struct FrameSlot {
dirty: Cell<bool>,
paused: Cell<bool>,
retrying: Cell<bool>,
target: *mut c_void,
flush: unsafe fn(*mut c_void),
}
Expand All @@ -28,6 +29,7 @@ impl FrameSlot {
Rc::new(FrameSlot {
dirty: Cell::new(false),
paused: Cell::new(false),
retrying: Cell::new(false),
target,
flush,
})
Expand All @@ -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<Self>) {
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<Vec<Weak<FrameSlot>>> = const { RefCell::new(Vec::new()) };
static RETRY: RefCell<Vec<Weak<FrameSlot>>> = const { RefCell::new(Vec::new()) };
static SCHEDULER: RefCell<Option<Box<dyn Fn()>>> = const { RefCell::new(None) };
/// A flush found a context's GPU device lost.
static LOST: Cell<bool> = const { Cell::new(false) };
Expand All @@ -81,14 +104,25 @@ pub fn mark_dirty(slot: &Rc<FrameSlot>) {
/// 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() {
Expand Down Expand Up @@ -159,6 +193,8 @@ struct MicrotaskScheduler {
queue_microtask: sys::napi_ref,
flush: sys::napi_ref,
queued: Cell<bool>,
retry: sys::napi_ref,
retry_queued: Cell<bool>,
}

thread_local! {
Expand All @@ -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;
Expand Down Expand Up @@ -222,16 +296,28 @@ 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 {
env,
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())));
Expand Down
2 changes: 1 addition & 1 deletion crates/canvas-napi/src/gl/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
Loading