summaryrefslogtreecommitdiff
path: root/rust/wasm
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-04-26 20:44:07 +0200
committerMaze Winther <mazewinther@gmail.com>2026-04-26 20:44:07 +0200
commit6b1f38a9a31a0eedaa7abea70f232960469351d5 (patch)
tree4050d69056c1d9f14075b784535e2b33719f477b /rust/wasm
parent56ca09692a308b22455420d11376ff8d41244c47 (diff)
parent0b7597b31fa956c7f3d4838923b545ba5e341422 (diff)
downloadopencut-6b1f38a9a31a0eedaa7abea70f232960469351d5.tar.gz
refactor: split animation helpers by domain
Diffstat (limited to 'rust/wasm')
-rw-r--r--rust/wasm/Cargo.toml4
-rw-r--r--rust/wasm/src/compositor.rs116
-rw-r--r--rust/wasm/src/perf.rs51
-rw-r--r--rust/wasm/src/wasm.rs4
4 files changed, 139 insertions, 36 deletions
diff --git a/rust/wasm/Cargo.toml b/rust/wasm/Cargo.toml
index f12a8cbc..0f2bfc56 100644
--- a/rust/wasm/Cargo.toml
+++ b/rust/wasm/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "opencut-wasm"
-version = "0.2.9"
+version = "0.2.10"
edition = "2024"
description = "Shared video editor logic compiled to WebAssembly"
repository = "https://github.com/opencut/opencut"
@@ -24,7 +24,7 @@ serde-wasm-bindgen = "0.6.5"
time = { version = "0.1.0", path = "../crates/time", features = ["wasm"] }
wasm-bindgen = "0.2.116"
wasm-bindgen-futures = "0.4.66"
-web-sys = { version = "0.3.93", features = ["OffscreenCanvas", "HtmlCanvasElement", "CanvasRenderingContext2d", "Document", "Window"] }
+web-sys = { version = "0.3.93", features = ["OffscreenCanvas", "HtmlCanvasElement", "CanvasRenderingContext2d", "Document", "Window", "Performance"] }
[features]
default = ["wasm"]
diff --git a/rust/wasm/src/compositor.rs b/rust/wasm/src/compositor.rs
index 126c7468..7652b3f3 100644
--- a/rust/wasm/src/compositor.rs
+++ b/rust/wasm/src/compositor.rs
@@ -11,10 +11,13 @@ 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,
compositor: Compositor,
+ surface: wgpu::Surface<'static>,
+ surface_size: (u32, u32),
}
thread_local! {
@@ -24,20 +27,42 @@ 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::<web_sys::HtmlCanvasElement>()
+ .map_err(|_| JsValue::from_str("Failed to create compositor canvas"))?
+ };
canvas.set_width(width);
canvas.set_height(height);
let compositor = Compositor::new(&gpu_runtime.context);
+ let surface = gpu_runtime
+ .context
+ .instance()
+ .create_surface(wgpu::SurfaceTarget::Canvas(canvas.clone()))
+ .map_err(|error| JsValue::from_str(&error.to_string()))?;
+ gpu_runtime
+ .context
+ .configure_surface(&surface, width, height)
+ .map_err(|error| JsValue::from_str(&error.to_string()))?;
COMPOSITOR_RUNTIME.with(|runtime| {
- runtime.replace(Some(CompositorRuntime { canvas, compositor }));
+ runtime.replace(Some(CompositorRuntime {
+ canvas,
+ compositor,
+ surface,
+ surface_size: (width, height),
+ }));
});
Ok(())
@@ -46,16 +71,25 @@ pub fn init_compositor(width: u32, height: u32) -> Result<(), JsValue> {
#[wasm_bindgen(js_name = resizeCompositor)]
pub fn resize_compositor(width: u32, height: u32) -> Result<(), JsValue> {
- COMPOSITOR_RUNTIME.with(|runtime| {
- let mut borrow = runtime.borrow_mut();
- let Some(runtime) = borrow.as_mut() else {
- return Err(JsValue::from_str(
- "Compositor is not initialized. Call initCompositor() first.",
- ));
- };
- runtime.canvas.set_width(width);
- runtime.canvas.set_height(height);
- Ok(())
+ with_gpu_runtime(|gpu_runtime| {
+ COMPOSITOR_RUNTIME.with(|runtime| {
+ let mut borrow = runtime.borrow_mut();
+ let Some(runtime) = borrow.as_mut() else {
+ return Err(JsValue::from_str(
+ "Compositor is not initialized. Call initCompositor() first.",
+ ));
+ };
+ runtime.canvas.set_width(width);
+ runtime.canvas.set_height(height);
+ if runtime.surface_size != (width, height) {
+ gpu_runtime
+ .context
+ .configure_surface(&runtime.surface, width, height)
+ .map_err(|error| JsValue::from_str(&error.to_string()))?;
+ runtime.surface_size = (width, height);
+ }
+ Ok(())
+ })
})
}
@@ -119,8 +153,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| {
@@ -131,40 +169,50 @@ pub fn render_frame(options: JsValue) -> Result<(), JsValue> {
));
};
- if gpu_runtime.context.supports_surface_rendering() {
- let surface = gpu_runtime
+ if runtime.surface_size != (frame.width, frame.height) {
+ runtime.canvas.set_width(frame.width);
+ runtime.canvas.set_height(frame.height);
+ let t_surface = perf::now_ms();
+ gpu_runtime
.context
- .instance()
- .create_surface(wgpu::SurfaceTarget::Canvas(runtime.canvas.clone()))
+ .configure_surface(&runtime.surface, frame.width, frame.height)
.map_err(|error| JsValue::from_str(&error.to_string()))?;
+ perf::record("wasm.surfaceConfigure", perf::now_ms() - t_surface);
+ runtime.surface_size = (frame.width, frame.height);
+ }
- runtime
+ if gpu_runtime.context.supports_surface_rendering() {
+ let t_render = perf::now_ms();
+ let result = runtime
.compositor
.render_frame(
&gpu_runtime.context,
RenderFrameOptions {
frame: &frame,
- surface: &surface,
+ surface: &runtime.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 still needs a separate composition pass, but the output
+ // surface is now persistent just like the WebGPU path.
+ 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()))
+ .present_texture_to_surface(&texture, &runtime.surface)
+ .map_err(|error| JsValue::from_str(&error.to_string()))?;
+ perf::record("wasm.presentToSurface", 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<Vec<(&'static str, f64)>> = 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::*;