mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
feat: wasm compositor
Made-with: Cursor
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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")?,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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(());
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod compositor;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod effects;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod gpu;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod masks;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use compositor::*;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use effects::*;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
|
||||
Reference in New Issue
Block a user