diff options
Diffstat (limited to 'rust/wasm')
| -rw-r--r-- | rust/wasm/Cargo.toml | 4 | ||||
| -rw-r--r-- | rust/wasm/src/compositor.rs | 174 | ||||
| -rw-r--r-- | rust/wasm/src/gpu.rs | 2 | ||||
| -rw-r--r-- | rust/wasm/src/wasm.rs | 4 |
4 files changed, 183 insertions, 1 deletions
diff --git a/rust/wasm/Cargo.toml b/rust/wasm/Cargo.toml index 88f92a5c..b7a8fdf0 100644 --- a/rust/wasm/Cargo.toml +++ b/rust/wasm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "opencut-wasm" -version = "0.2.3" +version = "0.2.4" edition = "2024" description = "Shared video editor logic compiled to WebAssembly" repository = "https://github.com/opencut/opencut" @@ -12,6 +12,8 @@ crate-type = ["cdylib", "rlib"] [dependencies] bridge = { version = "0.1.0", path = "../crates/bridge" } +compositor = { version = "0.1.0", path = "../crates/compositor" } +console_error_panic_hook = "0.1.7" effects = { version = "0.1.0", path = "../crates/effects" } gpu = { version = "0.1.0", path = "../crates/gpu", features = ["wasm"] } js-sys = "0.3.93" diff --git a/rust/wasm/src/compositor.rs b/rust/wasm/src/compositor.rs new file mode 100644 index 00000000..55756a21 --- /dev/null +++ b/rust/wasm/src/compositor.rs @@ -0,0 +1,174 @@ +#![cfg(target_arch = "wasm32")] + +use std::cell::RefCell; + +use compositor::{Compositor, FrameDescriptor, RenderFrameOptions}; +use gpu::wgpu; +use js_sys::Object; +use wasm_bindgen::{JsCast, JsValue, prelude::wasm_bindgen}; + +use crate::gpu::{ + import_canvas_texture, read_offscreen_canvas_property, read_serde_property, read_u32_property, + with_gpu_runtime, +}; + +struct CompositorRuntime { + canvas: web_sys::HtmlCanvasElement, + compositor: Compositor, +} + +thread_local! { + static COMPOSITOR_RUNTIME: RefCell<Option<CompositorRuntime>> = const { RefCell::new(None) }; +} + +#[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"))?; + canvas.set_width(width); + canvas.set_height(height); + + let compositor = Compositor::new(&gpu_runtime.context); + + COMPOSITOR_RUNTIME.with(|runtime| { + runtime.replace(Some(CompositorRuntime { canvas, compositor })); + }); + + Ok(()) + }) +} + +#[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(()) + }) +} + +#[wasm_bindgen(js_name = getCompositorCanvas)] +pub fn get_compositor_canvas() -> Result<web_sys::HtmlCanvasElement, JsValue> { + COMPOSITOR_RUNTIME.with(|runtime| { + let borrow = runtime.borrow(); + let Some(runtime) = borrow.as_ref() else { + return Err(JsValue::from_str( + "Compositor is not initialized. Call initCompositor() first.", + )); + }; + Ok(runtime.canvas.clone()) + }) +} + +#[wasm_bindgen(js_name = uploadTexture)] +pub fn upload_texture(options: JsValue) -> Result<(), JsValue> { + let UploadTextureOptions { + id, + source, + width, + height, + } = parse_upload_texture_options(options)?; + + 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.", + )); + }; + + let texture = import_canvas_texture( + &gpu_runtime.context, + &source, + width, + height, + "compositor-upload-texture", + ); + runtime.compositor.upsert_texture(id, texture); + Ok(()) + }) + }) +} + +#[wasm_bindgen(js_name = releaseTexture)] +pub fn release_texture(id: String) -> 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.compositor.release_texture(&id); + Ok(()) + }) +} + +#[wasm_bindgen(js_name = renderFrame)] +pub fn render_frame(options: JsValue) -> Result<(), JsValue> { + let frame: FrameDescriptor = serde_wasm_bindgen::from_value(options) + .map_err(|error| JsValue::from_str(&format!("Invalid frame descriptor: {error}")))?; + + 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.", + )); + }; + + let surface = gpu_runtime + .context + .instance() + .create_surface(wgpu::SurfaceTarget::Canvas(runtime.canvas.clone())) + .map_err(|error| JsValue::from_str(&error.to_string()))?; + + runtime + .compositor + .render_frame( + &gpu_runtime.context, + RenderFrameOptions { + frame: &frame, + surface: &surface, + }, + ) + .map_err(|error| JsValue::from_str(&error.to_string())) + }) + }) +} + +#[derive(Debug)] +struct UploadTextureOptions { + id: String, + source: wgpu::web_sys::OffscreenCanvas, + width: u32, + height: u32, +} + +fn parse_upload_texture_options(value: JsValue) -> Result<UploadTextureOptions, JsValue> { + let object: Object = value + .dyn_into() + .map_err(|_| JsValue::from_str("uploadTexture expects an options object"))?; + + Ok(UploadTextureOptions { + id: read_serde_property(&object, "id")?, + source: read_offscreen_canvas_property(&object, "source")?, + width: read_u32_property(&object, "width")?, + height: read_u32_property(&object, "height")?, + }) +} + diff --git a/rust/wasm/src/gpu.rs b/rust/wasm/src/gpu.rs index 70f3a241..4e65978c 100644 --- a/rust/wasm/src/gpu.rs +++ b/rust/wasm/src/gpu.rs @@ -21,6 +21,8 @@ thread_local! { #[wasm_bindgen(js_name = initializeGpu)] pub async fn initialize_gpu() -> Result<(), JsValue> { + console_error_panic_hook::set_once(); + if GPU_RUNTIME.with(|runtime| runtime.borrow().is_some()) { return Ok(()); } diff --git a/rust/wasm/src/wasm.rs b/rust/wasm/src/wasm.rs index 27ece766..1a33092a 100644 --- a/rust/wasm/src/wasm.rs +++ b/rust/wasm/src/wasm.rs @@ -1,4 +1,6 @@ #[cfg(target_arch = "wasm32")] +mod compositor; +#[cfg(target_arch = "wasm32")] mod effects; #[cfg(target_arch = "wasm32")] mod gpu; @@ -6,6 +8,8 @@ mod gpu; mod masks; #[cfg(target_arch = "wasm32")] +pub use compositor::*; +#[cfg(target_arch = "wasm32")] pub use effects::*; #[cfg(target_arch = "wasm32")] pub use gpu::*; |
