diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-04-07 01:09:13 +0200 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-04-07 01:09:13 +0200 |
| commit | e4b67094e757917588fb624a30aa046667403add (patch) | |
| tree | 55452692eac69a5a63b58f2a5cdb701dd7023d68 /rust/wasm/src | |
| parent | 79df73643185c02493364a077402726d1fa7b9ec (diff) | |
| download | opencut-e4b67094e757917588fb624a30aa046667403add.tar.gz | |
refactor: split effects and masks into dedicated rust crates, introduce MediaTime and FrameRate
Diffstat (limited to 'rust/wasm/src')
| -rw-r--r-- | rust/wasm/src/effects.rs | 101 | ||||
| -rw-r--r-- | rust/wasm/src/gpu.rs | 123 | ||||
| -rw-r--r-- | rust/wasm/src/gpu_bridge.rs | 256 | ||||
| -rw-r--r-- | rust/wasm/src/masks.rs | 60 | ||||
| -rw-r--r-- | rust/wasm/src/wasm.rs | 12 |
5 files changed, 294 insertions, 258 deletions
diff --git a/rust/wasm/src/effects.rs b/rust/wasm/src/effects.rs new file mode 100644 index 00000000..1f5c2947 --- /dev/null +++ b/rust/wasm/src/effects.rs @@ -0,0 +1,101 @@ +#![cfg(target_arch = "wasm32")] + +use effects::{ApplyEffectsOptions, EffectPass, UniformValue}; +use gpu::wgpu; +use js_sys::Object; +use serde::Deserialize; +use wasm_bindgen::{JsCast, JsValue, prelude::wasm_bindgen}; + +use crate::gpu::{ + import_canvas_texture, read_offscreen_canvas_property, read_serde_property, read_u32_property, + render_texture_to_canvas, with_gpu_runtime, +}; + +struct ApplyEffectPassesOptions { + source: wgpu::web_sys::OffscreenCanvas, + width: u32, + height: u32, + passes: Vec<EffectPassInput>, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct EffectPassInput { + shader: String, + uniforms: Vec<EffectUniformInput>, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct EffectUniformInput { + name: String, + value: Vec<f32>, +} + +#[wasm_bindgen(js_name = applyEffectPasses)] +pub fn apply_effect_passes(options: JsValue) -> Result<wgpu::web_sys::OffscreenCanvas, JsValue> { + let ApplyEffectPassesOptions { + source, + width, + height, + passes, + } = parse_apply_effect_passes_options(options)?; + + with_gpu_runtime(|runtime| { + let source_texture = import_canvas_texture( + &runtime.context, + &source, + width, + height, + "effects-input-texture", + ); + let effect_passes = map_effect_passes(passes); + let result_texture = runtime + .effects + .apply( + &runtime.context, + ApplyEffectsOptions { + source: &source_texture, + width, + height, + passes: &effect_passes, + }, + ) + .map_err(|error| JsValue::from_str(&error.to_string()))?; + render_texture_to_canvas(&runtime.context, &result_texture, width, height) + }) +} + +fn map_effect_passes(effect_passes: Vec<EffectPassInput>) -> Vec<EffectPass> { + effect_passes + .into_iter() + .map(|pass| EffectPass { + shader: pass.shader, + uniforms: pass + .uniforms + .into_iter() + .map(|uniform| { + let value = if uniform.value.len() == 1 { + UniformValue::Number(uniform.value[0]) + } else { + UniformValue::Vector(uniform.value) + }; + (uniform.name, value) + }) + .collect(), + }) + .collect() +} + +fn parse_apply_effect_passes_options(value: JsValue) -> Result<ApplyEffectPassesOptions, JsValue> { + let object: Object = value + .dyn_into() + .map_err(|_| JsValue::from_str("applyEffectPasses expects an options object"))?; + + Ok(ApplyEffectPassesOptions { + source: read_offscreen_canvas_property(&object, "source")?, + width: read_u32_property(&object, "width")?, + height: read_u32_property(&object, "height")?, + passes: read_serde_property(&object, "passes")?, + }) +} diff --git a/rust/wasm/src/gpu.rs b/rust/wasm/src/gpu.rs new file mode 100644 index 00000000..70f3a241 --- /dev/null +++ b/rust/wasm/src/gpu.rs @@ -0,0 +1,123 @@ +#![cfg(target_arch = "wasm32")] + +use std::cell::RefCell; + +use effects::EffectPipeline; +use gpu::{GpuContext, wgpu}; +use js_sys::{Object, Reflect}; +use masks::MaskFeatherPipeline; +use serde::Deserialize; +use wasm_bindgen::{JsCast, JsValue, prelude::wasm_bindgen}; + +pub(crate) struct GpuRuntime { + pub(crate) context: GpuContext, + pub(crate) effects: EffectPipeline, + pub(crate) masks: MaskFeatherPipeline, +} + +thread_local! { + static GPU_RUNTIME: RefCell<Option<GpuRuntime>> = const { RefCell::new(None) }; +} + +#[wasm_bindgen(js_name = initializeGpu)] +pub async fn initialize_gpu() -> Result<(), JsValue> { + if GPU_RUNTIME.with(|runtime| runtime.borrow().is_some()) { + return Ok(()); + } + + let context = GpuContext::new() + .await + .map_err(|error| JsValue::from_str(&error.to_string()))?; + let effects = EffectPipeline::new(&context); + let masks = MaskFeatherPipeline::new(&context); + + GPU_RUNTIME.with(|runtime| { + runtime.replace(Some(GpuRuntime { + context, + effects, + masks, + })); + }); + + Ok(()) +} + +pub(crate) fn with_gpu_runtime<T>( + action: impl FnOnce(&GpuRuntime) -> Result<T, JsValue>, +) -> Result<T, JsValue> { + GPU_RUNTIME.with(|runtime| { + let borrow = runtime.borrow(); + let Some(gpu_runtime) = borrow.as_ref() else { + return Err(JsValue::from_str( + "GPU context not initialized. Call initializeGpu() first.", + )); + }; + action(gpu_runtime) + }) +} + +pub(crate) fn import_canvas_texture( + context: &GpuContext, + canvas: &wgpu::web_sys::OffscreenCanvas, + width: u32, + height: u32, + label: &'static str, +) -> wgpu::Texture { + context.import_offscreen_canvas_texture(canvas, width, height, label) +} + +pub(crate) fn render_texture_to_canvas( + context: &GpuContext, + texture: &wgpu::Texture, + width: u32, + height: u32, +) -> Result<wgpu::web_sys::OffscreenCanvas, JsValue> { + let canvas = wgpu::web_sys::OffscreenCanvas::new(width, height)?; + context + .render_texture_to_offscreen_canvas(texture, &canvas, width, height) + .map_err(|error| JsValue::from_str(&error.to_string()))?; + Ok(canvas) +} + +pub(crate) fn read_property(object: &Object, name: &str) -> Result<JsValue, JsValue> { + Reflect::get(object, &JsValue::from_str(name)) + .map_err(|_| JsValue::from_str(&format!("Missing property '{name}'"))) +} + +pub(crate) fn read_offscreen_canvas_property( + object: &Object, + name: &str, +) -> Result<wgpu::web_sys::OffscreenCanvas, JsValue> { + read_property(object, name)? + .dyn_into::<wgpu::web_sys::OffscreenCanvas>() + .map_err(|_| JsValue::from_str(&format!("Property '{name}' must be an OffscreenCanvas"))) +} + +pub(crate) fn read_u32_property(object: &Object, name: &str) -> Result<u32, JsValue> { + let value = read_property(object, name)?; + let Some(number) = value.as_f64() else { + return Err(JsValue::from_str(&format!( + "Property '{name}' must be a number" + ))); + }; + Ok(number as u32) +} + +pub(crate) fn read_f32_property(object: &Object, name: &str) -> Result<f32, JsValue> { + let value = read_property(object, name)?; + let Some(number) = value.as_f64() else { + return Err(JsValue::from_str(&format!( + "Property '{name}' must be a number" + ))); + }; + Ok(number as f32) +} + +pub(crate) fn read_serde_property<T>(object: &Object, name: &str) -> Result<T, JsValue> +where + T: for<'de> Deserialize<'de>, +{ + let value = read_property(object, name)?; + serde_wasm_bindgen::from_value(value) + .map_err(|error| JsValue::from_str(&format!("Invalid property '{name}': {error}"))) +} diff --git a/rust/wasm/src/gpu_bridge.rs b/rust/wasm/src/gpu_bridge.rs deleted file mode 100644 index 85db0c8b..00000000 --- a/rust/wasm/src/gpu_bridge.rs +++ /dev/null @@ -1,256 +0,0 @@ -#![cfg(target_arch = "wasm32")] - -use std::cell::RefCell; - -use gpu::{EffectPass, GpuContext, UniformValue, wgpu}; -use js_sys::{Object, Reflect}; -use serde::Deserialize; -use wasm_bindgen::{JsCast, JsValue, prelude::wasm_bindgen}; - -thread_local! { - static GPU_CONTEXT: RefCell<Option<GpuContext>> = const { RefCell::new(None) }; -} - -struct ApplyEffectPassesOptions { - source: wgpu::web_sys::OffscreenCanvas, - width: u32, - height: u32, - passes: Vec<EffectPassInput>, -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct EffectPassInput { - shader: String, - uniforms: Vec<EffectUniformInput>, -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct EffectUniformInput { - name: String, - value: Vec<f32>, -} - -struct ApplyMaskFeatherOptions { - mask: wgpu::web_sys::OffscreenCanvas, - width: u32, - height: u32, - feather: f32, -} - -#[wasm_bindgen(js_name = initializeGpu)] -pub async fn initialize_gpu() -> Result<(), JsValue> { - if GPU_CONTEXT.with(|context| context.borrow().is_some()) { - return Ok(()); - } - - let context = GpuContext::new() - .await - .map_err(|error| JsValue::from_str(&error.to_string()))?; - GPU_CONTEXT.with(|gpu_context| { - gpu_context.replace(Some(context)); - }); - Ok(()) -} - -#[wasm_bindgen(js_name = applyEffectPasses)] -pub fn apply_effect_passes( - options: JsValue, -) -> Result<wgpu::web_sys::OffscreenCanvas, JsValue> { - let ApplyEffectPassesOptions { - source, - width, - height, - passes, - } = parse_apply_effect_passes_options(options)?; - - with_gpu_context(|context| { - let source_texture = import_canvas_texture(context, &source, width, height)?; - let effect_passes = map_effect_passes(passes); - let result_texture = context - .apply_effects(gpu::ApplyEffectsOptions { - source: &source_texture, - width, - height, - passes: &effect_passes, - }) - .map_err(|error| JsValue::from_str(&error.to_string()))?; - let output_canvas = wgpu::web_sys::OffscreenCanvas::new(width, height)?; - let surface = context - .instance() - .create_surface(wgpu::SurfaceTarget::OffscreenCanvas(output_canvas.clone())) - .map_err(|error| JsValue::from_str(&error.to_string()))?; - - context - .render_texture_to_surface(&result_texture, &surface, width, height) - .map_err(|error| JsValue::from_str(&error.to_string()))?; - Ok(output_canvas) - }) -} - -#[wasm_bindgen(js_name = applyMaskFeather)] -pub fn apply_mask_feather( - options: JsValue, -) -> Result<wgpu::web_sys::OffscreenCanvas, JsValue> { - let ApplyMaskFeatherOptions { - mask, - width, - height, - feather, - } = parse_apply_mask_feather_options(options)?; - - with_gpu_context(|context| { - let mask_texture = import_canvas_texture(context, &mask, width, height)?; - let result_texture = context.apply_mask_feather(gpu::ApplyMaskFeatherOptions { - mask: &mask_texture, - width, - height, - feather, - }); - let output_canvas = wgpu::web_sys::OffscreenCanvas::new(width, height)?; - let surface = context - .instance() - .create_surface(wgpu::SurfaceTarget::OffscreenCanvas(output_canvas.clone())) - .map_err(|error| JsValue::from_str(&error.to_string()))?; - - context - .render_texture_to_surface(&result_texture, &surface, width, height) - .map_err(|error| JsValue::from_str(&error.to_string()))?; - Ok(output_canvas) - }) -} - -fn with_gpu_context<T>( - action: impl FnOnce(&GpuContext) -> Result<T, JsValue>, -) -> Result<T, JsValue> { - GPU_CONTEXT.with(|context| { - let borrow = context.borrow(); - let Some(gpu_context) = borrow.as_ref() else { - return Err(JsValue::from_str( - "GPU context not initialized. Call initializeGpu() first.", - )); - }; - action(gpu_context) - }) -} - -fn import_canvas_texture( - context: &GpuContext, - canvas: &wgpu::web_sys::OffscreenCanvas, - width: u32, - height: u32, -) -> Result<wgpu::Texture, JsValue> { - let texture = context.create_render_texture(width, height, "gpu-bridge-input-texture"); - context.queue().copy_external_image_to_texture( - &wgpu::CopyExternalImageSourceInfo { - source: wgpu::ExternalImageSource::OffscreenCanvas(canvas.clone()), - origin: wgpu::Origin2d::ZERO, - flip_y: true, - }, - wgpu::CopyExternalImageDestInfo { - texture: &texture, - mip_level: 0, - origin: wgpu::Origin3d::ZERO, - aspect: wgpu::TextureAspect::All, - color_space: wgpu::PredefinedColorSpace::Srgb, - premultiplied_alpha: false, - }, - wgpu::Extent3d { - width, - height, - depth_or_array_layers: 1, - }, - ); - Ok(texture) -} - -fn map_effect_passes(effect_passes: Vec<EffectPassInput>) -> Vec<EffectPass> { - effect_passes - .into_iter() - .map(|pass| EffectPass { - shader: pass.shader, - uniforms: pass - .uniforms - .into_iter() - .map(|uniform| { - let value = if uniform.value.len() == 1 { - UniformValue::Number(uniform.value[0]) - } else { - UniformValue::Vector(uniform.value) - }; - (uniform.name, value) - }) - .collect(), - }) - .collect() -} - -fn parse_apply_effect_passes_options(value: JsValue) -> Result<ApplyEffectPassesOptions, JsValue> { - let object: Object = value - .dyn_into() - .map_err(|_| JsValue::from_str("applyEffectPasses expects an options object"))?; - - Ok(ApplyEffectPassesOptions { - source: read_offscreen_canvas_property(&object, "source")?, - width: read_u32_property(&object, "width")?, - height: read_u32_property(&object, "height")?, - passes: read_serde_property(&object, "passes")?, - }) -} - -fn parse_apply_mask_feather_options(value: JsValue) -> Result<ApplyMaskFeatherOptions, JsValue> { - let object: Object = value - .dyn_into() - .map_err(|_| JsValue::from_str("applyMaskFeather expects an options object"))?; - - Ok(ApplyMaskFeatherOptions { - mask: read_offscreen_canvas_property(&object, "mask")?, - width: read_u32_property(&object, "width")?, - height: read_u32_property(&object, "height")?, - feather: read_f32_property(&object, "feather")?, - }) -} - -fn read_property(object: &Object, name: &str) -> Result<JsValue, JsValue> { - Reflect::get(object, &JsValue::from_str(name)) - .map_err(|_| JsValue::from_str(&format!("Missing property '{name}'"))) -} - -fn read_offscreen_canvas_property( - object: &Object, - name: &str, -) -> Result<wgpu::web_sys::OffscreenCanvas, JsValue> { - read_property(object, name)? - .dyn_into::<wgpu::web_sys::OffscreenCanvas>() - .map_err(|_| JsValue::from_str(&format!("Property '{name}' must be an OffscreenCanvas"))) -} - -fn read_u32_property(object: &Object, name: &str) -> Result<u32, JsValue> { - let value = read_property(object, name)?; - let Some(number) = value.as_f64() else { - return Err(JsValue::from_str(&format!( - "Property '{name}' must be a number" - ))); - }; - Ok(number as u32) -} - -fn read_f32_property(object: &Object, name: &str) -> Result<f32, JsValue> { - let value = read_property(object, name)?; - let Some(number) = value.as_f64() else { - return Err(JsValue::from_str(&format!( - "Property '{name}' must be a number" - ))); - }; - Ok(number as f32) -} - -fn read_serde_property<T>(object: &Object, name: &str) -> Result<T, JsValue> -where - T: for<'de> Deserialize<'de>, -{ - let value = read_property(object, name)?; - serde_wasm_bindgen::from_value(value) - .map_err(|error| JsValue::from_str(&format!("Invalid property '{name}': {error}"))) -} diff --git a/rust/wasm/src/masks.rs b/rust/wasm/src/masks.rs new file mode 100644 index 00000000..e4f30e21 --- /dev/null +++ b/rust/wasm/src/masks.rs @@ -0,0 +1,60 @@ +#![cfg(target_arch = "wasm32")] + +use gpu::wgpu; +use js_sys::Object; +use wasm_bindgen::{JsCast, JsValue, prelude::wasm_bindgen}; + +use crate::gpu::{ + import_canvas_texture, read_f32_property, read_offscreen_canvas_property, read_u32_property, + render_texture_to_canvas, with_gpu_runtime, +}; + +struct ApplyMaskFeatherOptions { + mask: wgpu::web_sys::OffscreenCanvas, + width: u32, + height: u32, + feather: f32, +} + +#[wasm_bindgen(js_name = applyMaskFeather)] +pub fn apply_mask_feather(options: JsValue) -> Result<wgpu::web_sys::OffscreenCanvas, JsValue> { + let ApplyMaskFeatherOptions { + mask, + width, + height, + feather, + } = parse_apply_mask_feather_options(options)?; + + with_gpu_runtime(|runtime| { + let mask_texture = import_canvas_texture( + &runtime.context, + &mask, + width, + height, + "masks-input-texture", + ); + let result_texture = runtime.masks.apply_mask_feather( + &runtime.context, + masks::ApplyMaskFeatherOptions { + mask: &mask_texture, + width, + height, + feather, + }, + ); + render_texture_to_canvas(&runtime.context, &result_texture, width, height) + }) +} + +fn parse_apply_mask_feather_options(value: JsValue) -> Result<ApplyMaskFeatherOptions, JsValue> { + let object: Object = value + .dyn_into() + .map_err(|_| JsValue::from_str("applyMaskFeather expects an options object"))?; + + Ok(ApplyMaskFeatherOptions { + mask: read_offscreen_canvas_property(&object, "mask")?, + width: read_u32_property(&object, "width")?, + height: read_u32_property(&object, "height")?, + feather: read_f32_property(&object, "feather")?, + }) +} diff --git a/rust/wasm/src/wasm.rs b/rust/wasm/src/wasm.rs index 0a78a314..27ece766 100644 --- a/rust/wasm/src/wasm.rs +++ b/rust/wasm/src/wasm.rs @@ -1,6 +1,14 @@ #[cfg(target_arch = "wasm32")] -mod gpu_bridge; +mod effects; +#[cfg(target_arch = "wasm32")] +mod gpu; +#[cfg(target_arch = "wasm32")] +mod masks; #[cfg(target_arch = "wasm32")] -pub use gpu_bridge::*; +pub use effects::*; +#[cfg(target_arch = "wasm32")] +pub use gpu::*; +#[cfg(target_arch = "wasm32")] +pub use masks::*; pub use time::*; |
