From cceb835a84c125990ad8e49784e01a7c52636bb3 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 22 Apr 2026 16:48:37 +0200 Subject: perf: improve rendering performance by ~20x --- rust/wasm/src/compositor.rs | 57 +++++++++++++++++++++++++++++++-------------- rust/wasm/src/perf.rs | 51 ++++++++++++++++++++++++++++++++++++++++ rust/wasm/src/wasm.rs | 4 ++++ 3 files changed, 94 insertions(+), 18 deletions(-) create mode 100644 rust/wasm/src/perf.rs (limited to 'rust/wasm/src') diff --git a/rust/wasm/src/compositor.rs b/rust/wasm/src/compositor.rs index 126c7468..84467842 100644 --- a/rust/wasm/src/compositor.rs +++ b/rust/wasm/src/compositor.rs @@ -11,6 +11,7 @@ use crate::gpu::{ import_canvas_texture, read_offscreen_canvas_property, read_serde_property, read_u32_property, with_gpu_runtime, }; +use crate::perf; struct CompositorRuntime { canvas: web_sys::HtmlCanvasElement, @@ -24,13 +25,21 @@ thread_local! { #[wasm_bindgen(js_name = initCompositor)] pub fn init_compositor(width: u32, height: u32) -> Result<(), JsValue> { with_gpu_runtime(|gpu_runtime| { - let document = web_sys::window() - .and_then(|window| window.document()) - .ok_or_else(|| JsValue::from_str("Document is not available"))?; - let canvas: web_sys::HtmlCanvasElement = document - .create_element("canvas")? - .dyn_into() - .map_err(|_| JsValue::from_str("Failed to create compositor canvas"))?; + // On WebGL, wgpu is bound to a specific canvas; reuse it so the UI + // can mount the output directly instead of copying pixels through + // an intermediate 2D canvas every frame. On WebGPU, surface rendering + // works against any canvas so we create a fresh one. + let canvas = if let Some(gl_canvas) = gpu_runtime.context.gl_canvas() { + gl_canvas.clone() + } else { + let document = web_sys::window() + .and_then(|window| window.document()) + .ok_or_else(|| JsValue::from_str("Document is not available"))?; + document + .create_element("canvas")? + .dyn_into::() + .map_err(|_| JsValue::from_str("Failed to create compositor canvas"))? + }; canvas.set_width(width); canvas.set_height(height); @@ -119,8 +128,12 @@ pub fn release_texture(id: String) -> Result<(), JsValue> { #[wasm_bindgen(js_name = renderFrame)] pub fn render_frame(options: JsValue) -> Result<(), JsValue> { + perf::reset(); + + let t_deserialize = perf::now_ms(); let frame: FrameDescriptor = serde_wasm_bindgen::from_value(options) .map_err(|error| JsValue::from_str(&format!("Invalid frame descriptor: {error}")))?; + perf::record("wasm.deserialize", perf::now_ms() - t_deserialize); with_gpu_runtime(|gpu_runtime| { COMPOSITOR_RUNTIME.with(|runtime| { @@ -132,13 +145,16 @@ pub fn render_frame(options: JsValue) -> Result<(), JsValue> { }; if gpu_runtime.context.supports_surface_rendering() { + let t_surface = perf::now_ms(); let surface = gpu_runtime .context .instance() .create_surface(wgpu::SurfaceTarget::Canvas(runtime.canvas.clone())) .map_err(|error| JsValue::from_str(&error.to_string()))?; + perf::record("wasm.surfaceCreate", perf::now_ms() - t_surface); - runtime + let t_render = perf::now_ms(); + let result = runtime .compositor .render_frame( &gpu_runtime.context, @@ -147,24 +163,29 @@ pub fn render_frame(options: JsValue) -> Result<(), JsValue> { surface: &surface, }, ) - .map_err(|error| JsValue::from_str(&error.to_string())) + .map_err(|error| JsValue::from_str(&error.to_string())); + perf::record("wasm.renderFrameToSurface", perf::now_ms() - t_render); + result } else { - // WebGL cannot surface-render to an arbitrary canvas element. - // Composite to a texture, then blit to the canvas via the 2D context. + // WebGL surface-renders to the canvas its GL context was created + // on; `init_compositor` already pointed `runtime.canvas` at that + // same canvas, so presenting writes directly to the canvas the + // UI mounts. No intermediate copy. + let t_composite = perf::now_ms(); let texture = runtime .compositor .render_frame_to_texture(&gpu_runtime.context, &frame) .map_err(|error| JsValue::from_str(&error.to_string()))?; + perf::record("wasm.compositeToTexture", perf::now_ms() - t_composite); + let t_present = perf::now_ms(); gpu_runtime .context - .render_texture_via_gl_canvas( - &texture, - &runtime.canvas, - frame.width, - frame.height, - ) - .map_err(|error| JsValue::from_str(&error.to_string())) + .render_texture_to_gl_canvas_surface(&texture, frame.width, frame.height) + .map_err(|error| JsValue::from_str(&error.to_string()))?; + perf::record("wasm.presentToGlCanvas", perf::now_ms() - t_present); + + Ok(()) } }) }) diff --git a/rust/wasm/src/perf.rs b/rust/wasm/src/perf.rs new file mode 100644 index 00000000..0c346a4d --- /dev/null +++ b/rust/wasm/src/perf.rs @@ -0,0 +1,51 @@ +#![cfg(target_arch = "wasm32")] + +//! Per-frame profile buffer for the render pipeline. +//! +//! Sub-span timings are recorded into a thread-local during `renderFrame` +//! and drained by JS via `getLastFrameProfile()`. + +use std::cell::RefCell; + +use js_sys::{Array, Object, Reflect}; +use wasm_bindgen::{JsValue, prelude::wasm_bindgen}; + +thread_local! { + static LAST_FRAME_PROFILE: RefCell> = const { RefCell::new(Vec::new()) }; +} + +pub(crate) fn now_ms() -> f64 { + web_sys::window() + .and_then(|window| window.performance()) + .map(|performance| performance.now()) + .unwrap_or(0.0) +} + +pub(crate) fn reset() { + LAST_FRAME_PROFILE.with(|cell| cell.borrow_mut().clear()); +} + +pub(crate) fn record(name: &'static str, duration_ms: f64) { + LAST_FRAME_PROFILE.with(|cell| cell.borrow_mut().push((name, duration_ms))); +} + +#[wasm_bindgen(js_name = getLastFrameProfile)] +pub fn get_last_frame_profile() -> Array { + LAST_FRAME_PROFILE.with(|cell| { + let entries = cell.borrow(); + let array = Array::new_with_length(entries.len() as u32); + for (index, (name, duration_ms)) in entries.iter().enumerate() { + let entry = Object::new(); + Reflect::set(&entry, &JsValue::from_str("name"), &JsValue::from_str(name)) + .expect("set name"); + Reflect::set( + &entry, + &JsValue::from_str("durationMs"), + &JsValue::from_f64(*duration_ms), + ) + .expect("set durationMs"); + array.set(index as u32, entry.into()); + } + array + }) +} diff --git a/rust/wasm/src/wasm.rs b/rust/wasm/src/wasm.rs index 1a33092a..0687ff4b 100644 --- a/rust/wasm/src/wasm.rs +++ b/rust/wasm/src/wasm.rs @@ -6,6 +6,8 @@ mod effects; mod gpu; #[cfg(target_arch = "wasm32")] mod masks; +#[cfg(target_arch = "wasm32")] +mod perf; #[cfg(target_arch = "wasm32")] pub use compositor::*; @@ -15,4 +17,6 @@ pub use effects::*; pub use gpu::*; #[cfg(target_arch = "wasm32")] pub use masks::*; +#[cfg(target_arch = "wasm32")] +pub use perf::*; pub use time::*; -- cgit v1.3.1