diff --git a/Cargo.lock b/Cargo.lock index b673fb3e..f22bb61e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -955,6 +955,18 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "compositor" +version = "0.1.0" +dependencies = [ + "bytemuck", + "effects", + "gpu", + "masks", + "serde", + "thiserror 2.0.18", +] + [[package]] name = "compression-codecs" version = "0.4.37" @@ -982,6 +994,16 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + [[package]] name = "const-random" version = "0.1.18" @@ -2114,6 +2136,8 @@ version = "0.1.0" dependencies = [ "bytemuck", "thiserror 2.0.18", + "wasm-bindgen", + "web-sys", "wgpu", ] @@ -2158,7 +2182,7 @@ dependencies = [ "log", "presser", "thiserror 2.0.18", - "windows 0.61.3", + "windows 0.62.2", ] [[package]] @@ -3796,9 +3820,11 @@ dependencies = [ [[package]] name = "opencut-wasm" -version = "0.2.3" +version = "0.2.4" dependencies = [ "bridge", + "compositor", + "console_error_panic_hook", "effects", "gpu", "js-sys", @@ -6513,6 +6539,7 @@ dependencies = [ "thiserror 2.0.18", "wgpu-core-deps-apple", "wgpu-core-deps-emscripten", + "wgpu-core-deps-wasm", "wgpu-core-deps-windows-linux-android", "wgpu-hal", "wgpu-naga-bridge", @@ -6537,6 +6564,15 @@ dependencies = [ "wgpu-hal", ] +[[package]] +name = "wgpu-core-deps-wasm" +version = "29.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f7b75e72f49035f000dd5262e4126242e92a090a4fd75931ecfe7e60784e6fa" +dependencies = [ + "wgpu-hal", +] + [[package]] name = "wgpu-core-deps-windows-linux-android" version = "29.0.0" diff --git a/Cargo.toml b/Cargo.toml index 6c80c54f..b00c78e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,5 +7,5 @@ members = [ "rust/crates/effects", "rust/crates/gpu", "rust/crates/masks", - "rust/wasm", + "rust/wasm", "rust/crates/compositor", ] diff --git a/rust/crates/compositor/Cargo.toml b/rust/crates/compositor/Cargo.toml new file mode 100644 index 00000000..20d29063 --- /dev/null +++ b/rust/crates/compositor/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "compositor" +version = "0.1.0" +edition = "2024" + +[dependencies] +bytemuck = { version = "1.25.0", features = ["derive"] } +effects = { version = "0.1.0", path = "../effects" } +gpu = { version = "0.1.0", path = "../gpu" } +masks = { version = "0.1.0", path = "../masks" } +serde = { version = "1.0.228", features = ["derive"] } +thiserror = "2.0.18" diff --git a/rust/crates/compositor/src/blend_mode.rs b/rust/crates/compositor/src/blend_mode.rs new file mode 100644 index 00000000..8c6edcc6 --- /dev/null +++ b/rust/crates/compositor/src/blend_mode.rs @@ -0,0 +1,47 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum BlendMode { + Normal, + Darken, + Multiply, + ColorBurn, + Lighten, + Screen, + PlusLighter, + ColorDodge, + Overlay, + SoftLight, + HardLight, + Difference, + Exclusion, + Hue, + Saturation, + Color, + Luminosity, +} + +impl BlendMode { + pub fn shader_code(self) -> u32 { + match self { + Self::Normal => 0, + Self::Darken => 1, + Self::Multiply => 2, + Self::ColorBurn => 3, + Self::Lighten => 4, + Self::Screen => 5, + Self::PlusLighter => 6, + Self::ColorDodge => 7, + Self::Overlay => 8, + Self::SoftLight => 9, + Self::HardLight => 10, + Self::Difference => 11, + Self::Exclusion => 12, + Self::Hue => 13, + Self::Saturation => 14, + Self::Color => 15, + Self::Luminosity => 16, + } + } +} diff --git a/rust/crates/compositor/src/compositor.rs b/rust/crates/compositor/src/compositor.rs new file mode 100644 index 00000000..92d253d1 --- /dev/null +++ b/rust/crates/compositor/src/compositor.rs @@ -0,0 +1,816 @@ +use bytemuck::{Pod, Zeroable}; +use effects::{ApplyEffectsOptions, EffectPass, EffectPipeline, UniformValue}; +use gpu::{FULLSCREEN_SHADER_SOURCE, GpuContext, wgpu}; +use masks::{ApplyMaskFeatherOptions, MaskFeatherPipeline}; +use thiserror::Error; +use wgpu::util::DeviceExt; + +use crate::{ + BlendMode, + frame::{ + EffectPassDescriptor, EffectUniformValueDescriptor, FrameDescriptor, FrameItemDescriptor, + LayerDescriptor, + }, + texture_pool::TexturePool, + texture_store::TextureStore, +}; + +const LAYER_SHADER_SOURCE: &str = include_str!("shaders/layer.wgsl"); +const BLEND_SHADER_SOURCE: &str = include_str!("shaders/blend.wgsl"); +const MASK_SHADER_SOURCE: &str = include_str!("shaders/mask.wgsl"); + +pub struct RenderFrameOptions<'a, 'surface> { + pub frame: &'a FrameDescriptor, + pub surface: &'a wgpu::Surface<'surface>, +} + +pub struct Compositor { + textures: TextureStore, + texture_pool: TexturePool, + effects: EffectPipeline, + masks: MaskFeatherPipeline, + layer_uniform_bind_group_layout: wgpu::BindGroupLayout, + layer_pipeline: wgpu::RenderPipeline, + blend_uniform_bind_group_layout: wgpu::BindGroupLayout, + blend_pipeline: wgpu::RenderPipeline, + mask_uniform_bind_group_layout: wgpu::BindGroupLayout, + mask_pipeline: wgpu::RenderPipeline, +} + +#[derive(Debug, Error)] +pub enum CompositorError { + #[error("Texture '{texture_id}' is not available")] + MissingTexture { texture_id: String }, + #[error("Failed to apply effects: {0}")] + Effects(#[from] effects::EffectsError), + #[error("Failed to present frame: {0}")] + Gpu(#[from] gpu::GpuError), +} + +#[repr(C)] +#[derive(Clone, Copy, Pod, Zeroable)] +struct LayerUniformBuffer { + resolution: [f32; 2], + center: [f32; 2], + size: [f32; 2], + rotation_radians: f32, + opacity: f32, + flip_x: f32, + flip_y: f32, +} + +#[repr(C)] +#[derive(Clone, Copy, Pod, Zeroable)] +struct BlendUniformBuffer { + blend_mode: u32, + _padding: [u32; 3], +} + +#[repr(C)] +#[derive(Clone, Copy, Pod, Zeroable)] +struct MaskUniformBuffer { + inverted: f32, + _padding: [f32; 3], +} + +impl Compositor { + pub fn new(context: &GpuContext) -> Self { + let device = context.device(); + let fullscreen_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("compositor-fullscreen-shader"), + source: wgpu::ShaderSource::Wgsl(FULLSCREEN_SHADER_SOURCE.into()), + }); + let layer_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("compositor-layer-shader"), + source: wgpu::ShaderSource::Wgsl(LAYER_SHADER_SOURCE.into()), + }); + let blend_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("compositor-blend-shader"), + source: wgpu::ShaderSource::Wgsl(BLEND_SHADER_SOURCE.into()), + }); + let mask_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("compositor-mask-shader"), + source: wgpu::ShaderSource::Wgsl(MASK_SHADER_SOURCE.into()), + }); + + let layer_uniform_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("compositor-layer-uniform-layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + }); + let blend_uniform_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("compositor-blend-uniform-layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + }); + let mask_uniform_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("compositor-mask-uniform-layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + }); + + let layer_pipeline_layout = + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("compositor-layer-pipeline-layout"), + bind_group_layouts: &[ + Some(context.texture_sampler_bind_group_layout()), + Some(&layer_uniform_bind_group_layout), + ], + immediate_size: 0, + }); + let blend_pipeline_layout = + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("compositor-blend-pipeline-layout"), + bind_group_layouts: &[ + Some(context.texture_sampler_bind_group_layout()), + Some(context.texture_sampler_bind_group_layout()), + Some(&blend_uniform_bind_group_layout), + ], + immediate_size: 0, + }); + let mask_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("compositor-mask-pipeline-layout"), + bind_group_layouts: &[ + Some(context.texture_sampler_bind_group_layout()), + Some(context.texture_sampler_bind_group_layout()), + Some(&mask_uniform_bind_group_layout), + ], + immediate_size: 0, + }); + + let layer_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("compositor-layer-pipeline"), + layout: Some(&layer_pipeline_layout), + vertex: wgpu::VertexState { + module: &fullscreen_shader, + entry_point: Some("vertex_main"), + buffers: &[wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::<[f32; 2]>() as u64, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &[wgpu::VertexAttribute { + format: wgpu::VertexFormat::Float32x2, + offset: 0, + shader_location: 0, + }], + }], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &layer_shader, + entry_point: Some("fragment_main"), + targets: &[Some(wgpu::ColorTargetState { + format: context.texture_format(), + blend: None, + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview_mask: None, + cache: None, + }); + let blend_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("compositor-blend-pipeline"), + layout: Some(&blend_pipeline_layout), + vertex: wgpu::VertexState { + module: &fullscreen_shader, + entry_point: Some("vertex_main"), + buffers: &[wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::<[f32; 2]>() as u64, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &[wgpu::VertexAttribute { + format: wgpu::VertexFormat::Float32x2, + offset: 0, + shader_location: 0, + }], + }], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &blend_shader, + entry_point: Some("fragment_main"), + targets: &[Some(wgpu::ColorTargetState { + format: context.texture_format(), + blend: None, + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview_mask: None, + cache: None, + }); + let mask_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("compositor-mask-pipeline"), + layout: Some(&mask_pipeline_layout), + vertex: wgpu::VertexState { + module: &fullscreen_shader, + entry_point: Some("vertex_main"), + buffers: &[wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::<[f32; 2]>() as u64, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &[wgpu::VertexAttribute { + format: wgpu::VertexFormat::Float32x2, + offset: 0, + shader_location: 0, + }], + }], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &mask_shader, + entry_point: Some("fragment_main"), + targets: &[Some(wgpu::ColorTargetState { + format: context.texture_format(), + blend: None, + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview_mask: None, + cache: None, + }); + + Self { + textures: TextureStore::default(), + texture_pool: TexturePool::default(), + effects: EffectPipeline::new(context), + masks: MaskFeatherPipeline::new(context), + layer_uniform_bind_group_layout, + layer_pipeline, + blend_uniform_bind_group_layout, + blend_pipeline, + mask_uniform_bind_group_layout, + mask_pipeline, + } + } + + pub fn upsert_texture(&mut self, id: String, texture: wgpu::Texture) { + self.textures.upsert(id, texture); + } + + pub fn release_texture(&mut self, id: &str) { + self.textures.remove(id); + } + + pub fn render_frame( + &mut self, + context: &GpuContext, + options: RenderFrameOptions<'_, '_>, + ) -> Result<(), CompositorError> { + let frame = options.frame; + self.texture_pool.recycle_frame(); + context.configure_surface(options.surface, frame.width, frame.height)?; + let surface_texture = context.acquire_surface_texture(options.surface)?; + let surface_view = surface_texture + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + let mut encoder = + context + .device() + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("compositor-frame-encoder"), + }); + let mut scene = self.create_cleared_texture( + context, + &mut encoder, + frame.width, + frame.height, + frame.clear.color, + ); + + for item in &frame.items { + match item { + FrameItemDescriptor::Layer(layer) => { + let layer_texture = self.render_layer(context, &mut encoder, frame, layer)?; + scene = self.blend_texture( + context, + &mut encoder, + &scene, + &layer_texture, + layer.blend_mode, + frame.width, + frame.height, + )?; + } + FrameItemDescriptor::SceneEffect { effect_pass_groups } => { + scene = self.apply_effect_groups( + context, + &mut encoder, + &scene, + frame.width, + frame.height, + effect_pass_groups, + )?; + } + } + } + + context.encode_texture_blit_to_view( + &mut encoder, + &scene, + &surface_view, + "compositor-present-pass", + ); + context.queue().submit([encoder.finish()]); + surface_texture.present(); + Ok(()) + } + + fn render_layer( + &mut self, + context: &GpuContext, + encoder: &mut wgpu::CommandEncoder, + frame: &FrameDescriptor, + layer: &LayerDescriptor, + ) -> Result { + let source = self.textures.get(&layer.texture_id).ok_or_else(|| { + CompositorError::MissingTexture { + texture_id: layer.texture_id.clone(), + } + })?; + + let mut current = + self.texture_pool + .acquire(context, frame.width, frame.height, "compositor-layer"); + self.render_source_to_texture( + context, + encoder, + source.texture(), + ¤t, + frame.width, + frame.height, + layer, + ); + + if !layer.effect_pass_groups.is_empty() { + current = self.apply_effect_groups( + context, + encoder, + ¤t, + frame.width, + frame.height, + &layer.effect_pass_groups, + )?; + } + + if let Some(mask) = &layer.mask { + let mask_source = self.textures.get(&mask.texture_id).ok_or_else(|| { + CompositorError::MissingTexture { + texture_id: mask.texture_id.clone(), + } + })?; + let mask_source_texture = mask_source.texture().clone(); + let mask_texture = if mask.feather > 0.0 { + self.masks.apply_mask_feather_with_encoder( + context, + encoder, + ApplyMaskFeatherOptions { + mask: &mask_source_texture, + width: frame.width, + height: frame.height, + feather: mask.feather, + }, + ) + } else { + self.copy_texture( + context, + encoder, + &mask_source_texture, + frame.width, + frame.height, + ) + }; + current = self.apply_mask( + context, + encoder, + ¤t, + &mask_texture, + mask.inverted, + frame.width, + frame.height, + ); + } + + Ok(current) + } + + fn apply_effect_groups( + &mut self, + context: &GpuContext, + encoder: &mut wgpu::CommandEncoder, + source: &wgpu::Texture, + width: u32, + height: u32, + effect_pass_groups: &[Vec], + ) -> Result { + let mut current = self.copy_texture(context, encoder, source, width, height); + for group in effect_pass_groups { + let passes = map_effect_passes(group); + current = self.effects.apply_with_encoder( + context, + encoder, + ApplyEffectsOptions { + source: ¤t, + width, + height, + passes: &passes, + }, + )?; + } + Ok(current) + } + + fn create_cleared_texture( + &mut self, + context: &GpuContext, + encoder: &mut wgpu::CommandEncoder, + width: u32, + height: u32, + clear_color: [f32; 4], + ) -> wgpu::Texture { + let texture = + self.texture_pool + .acquire(context, width, height, "compositor-cleared-texture"); + let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); + { + let _pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("compositor-clear-pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &view, + resolve_target: None, + depth_slice: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color { + r: clear_color[0] as f64, + g: clear_color[1] as f64, + b: clear_color[2] as f64, + a: clear_color[3] as f64, + }), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + occlusion_query_set: None, + timestamp_writes: None, + multiview_mask: None, + }); + } + texture + } + + fn copy_texture( + &mut self, + context: &GpuContext, + encoder: &mut wgpu::CommandEncoder, + source: &wgpu::Texture, + width: u32, + height: u32, + ) -> wgpu::Texture { + let texture = self + .texture_pool + .acquire(context, width, height, "compositor-copy-texture"); + self.blit_texture(context, encoder, source, &texture); + texture + } + + fn render_source_to_texture( + &self, + context: &GpuContext, + encoder: &mut wgpu::CommandEncoder, + source: &wgpu::Texture, + target: &wgpu::Texture, + width: u32, + height: u32, + layer: &LayerDescriptor, + ) { + let source_view = source.create_view(&wgpu::TextureViewDescriptor::default()); + let target_view = target.create_view(&wgpu::TextureViewDescriptor::default()); + let source_bind_group = context + .device() + .create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("compositor-layer-source-bind-group"), + layout: context.texture_sampler_bind_group_layout(), + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&source_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(context.linear_sampler()), + }, + ], + }); + let uniform_buffer = + context + .device() + .create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("compositor-layer-uniform-buffer"), + contents: bytemuck::bytes_of(&LayerUniformBuffer { + resolution: [width as f32, height as f32], + center: [layer.transform.center_x, layer.transform.center_y], + size: [layer.transform.width, layer.transform.height], + rotation_radians: layer.transform.rotation_degrees.to_radians(), + opacity: layer.opacity, + flip_x: if layer.transform.flip_x { 1.0 } else { 0.0 }, + flip_y: if layer.transform.flip_y { 1.0 } else { 0.0 }, + }), + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + }); + let uniform_bind_group = context + .device() + .create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("compositor-layer-uniform-bind-group"), + layout: &self.layer_uniform_bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: uniform_buffer.as_entire_binding(), + }], + }); + + { + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("compositor-layer-pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &target_view, + resolve_target: None, + depth_slice: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + occlusion_query_set: None, + timestamp_writes: None, + multiview_mask: None, + }); + render_pass.set_pipeline(&self.layer_pipeline); + render_pass.set_vertex_buffer(0, context.fullscreen_quad().slice(..)); + render_pass.set_bind_group(0, &source_bind_group, &[]); + render_pass.set_bind_group(1, &uniform_bind_group, &[]); + render_pass.draw(0..6, 0..1); + } + } + + fn apply_mask( + &mut self, + context: &GpuContext, + encoder: &mut wgpu::CommandEncoder, + layer_texture: &wgpu::Texture, + mask_texture: &wgpu::Texture, + inverted: bool, + width: u32, + height: u32, + ) -> wgpu::Texture { + let target = self + .texture_pool + .acquire(context, width, height, "compositor-masked-texture"); + let layer_view = layer_texture.create_view(&wgpu::TextureViewDescriptor::default()); + let mask_view = mask_texture.create_view(&wgpu::TextureViewDescriptor::default()); + let target_view = target.create_view(&wgpu::TextureViewDescriptor::default()); + + let layer_bind_group = context + .device() + .create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("compositor-mask-layer-bind-group"), + layout: context.texture_sampler_bind_group_layout(), + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&layer_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(context.linear_sampler()), + }, + ], + }); + let mask_bind_group = context + .device() + .create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("compositor-mask-mask-bind-group"), + layout: context.texture_sampler_bind_group_layout(), + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&mask_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(context.linear_sampler()), + }, + ], + }); + let uniform_buffer = + context + .device() + .create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("compositor-mask-uniform-buffer"), + contents: bytemuck::bytes_of(&MaskUniformBuffer { + inverted: if inverted { 1.0 } else { 0.0 }, + _padding: [0.0; 3], + }), + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + }); + let uniform_bind_group = context + .device() + .create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("compositor-mask-uniform-bind-group"), + layout: &self.mask_uniform_bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: uniform_buffer.as_entire_binding(), + }], + }); + + { + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("compositor-mask-pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &target_view, + resolve_target: None, + depth_slice: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + occlusion_query_set: None, + timestamp_writes: None, + multiview_mask: None, + }); + render_pass.set_pipeline(&self.mask_pipeline); + render_pass.set_vertex_buffer(0, context.fullscreen_quad().slice(..)); + render_pass.set_bind_group(0, &layer_bind_group, &[]); + render_pass.set_bind_group(1, &mask_bind_group, &[]); + render_pass.set_bind_group(2, &uniform_bind_group, &[]); + render_pass.draw(0..6, 0..1); + } + target + } + + fn blend_texture( + &mut self, + context: &GpuContext, + encoder: &mut wgpu::CommandEncoder, + base: &wgpu::Texture, + layer: &wgpu::Texture, + blend_mode: BlendMode, + width: u32, + height: u32, + ) -> Result { + let target = + self.texture_pool + .acquire(context, width, height, "compositor-blended-texture"); + let base_view = base.create_view(&wgpu::TextureViewDescriptor::default()); + let layer_view = layer.create_view(&wgpu::TextureViewDescriptor::default()); + let target_view = target.create_view(&wgpu::TextureViewDescriptor::default()); + let base_bind_group = context + .device() + .create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("compositor-base-bind-group"), + layout: context.texture_sampler_bind_group_layout(), + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&base_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(context.linear_sampler()), + }, + ], + }); + let layer_bind_group = context + .device() + .create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("compositor-layer-bind-group"), + layout: context.texture_sampler_bind_group_layout(), + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&layer_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(context.linear_sampler()), + }, + ], + }); + let uniform_buffer = + context + .device() + .create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("compositor-blend-uniform-buffer"), + contents: bytemuck::bytes_of(&BlendUniformBuffer { + blend_mode: blend_mode.shader_code(), + _padding: [0; 3], + }), + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + }); + let uniform_bind_group = context + .device() + .create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("compositor-blend-uniform-bind-group"), + layout: &self.blend_uniform_bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: uniform_buffer.as_entire_binding(), + }], + }); + + { + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("compositor-blend-pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &target_view, + resolve_target: None, + depth_slice: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + occlusion_query_set: None, + timestamp_writes: None, + multiview_mask: None, + }); + render_pass.set_pipeline(&self.blend_pipeline); + render_pass.set_vertex_buffer(0, context.fullscreen_quad().slice(..)); + render_pass.set_bind_group(0, &base_bind_group, &[]); + render_pass.set_bind_group(1, &layer_bind_group, &[]); + render_pass.set_bind_group(2, &uniform_bind_group, &[]); + render_pass.draw(0..6, 0..1); + } + Ok(target) + } + + fn blit_texture( + &self, + context: &GpuContext, + encoder: &mut wgpu::CommandEncoder, + source: &wgpu::Texture, + target: &wgpu::Texture, + ) { + let target_view = target.create_view(&wgpu::TextureViewDescriptor::default()); + context.encode_texture_blit_to_view(encoder, source, &target_view, "compositor-blit-pass"); + } +} + +fn map_effect_passes(passes: &[EffectPassDescriptor]) -> Vec { + passes + .iter() + .map(|pass| EffectPass { + shader: pass.shader.clone(), + uniforms: pass + .uniforms + .iter() + .map(|(name, value)| { + let uniform_value = match value { + EffectUniformValueDescriptor::Number(n) => UniformValue::Number(*n), + EffectUniformValueDescriptor::Vector(v) => UniformValue::Vector(v.clone()), + }; + (name.clone(), uniform_value) + }) + .collect(), + }) + .collect() +} diff --git a/rust/crates/compositor/src/frame.rs b/rust/crates/compositor/src/frame.rs new file mode 100644 index 00000000..871a9759 --- /dev/null +++ b/rust/crates/compositor/src/frame.rs @@ -0,0 +1,83 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +use crate::BlendMode; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FrameDescriptor { + pub width: u32, + pub height: u32, + pub clear: CanvasClearDescriptor, + pub items: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CanvasClearDescriptor { + pub color: [f32; 4], +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "camelCase")] +pub enum FrameItemDescriptor { + Layer(LayerDescriptor), + SceneEffect { + effect_pass_groups: Vec>, + }, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct LayerDescriptor { + pub texture_id: String, + pub transform: QuadTransformDescriptor, + pub opacity: f32, + pub blend_mode: BlendMode, + #[serde(default)] + pub effect_pass_groups: Vec>, + pub mask: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct QuadTransformDescriptor { + pub center_x: f32, + pub center_y: f32, + pub width: f32, + pub height: f32, + pub rotation_degrees: f32, + pub flip_x: bool, + pub flip_y: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct LayerMaskDescriptor { + pub texture_id: String, + pub feather: f32, + pub inverted: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct EffectPassDescriptor { + pub shader: String, + pub uniforms: HashMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum EffectUniformValueDescriptor { + Number(f32), + Vector(Vec), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CanvasTextureDescriptor { + pub id: String, + pub width: u32, + pub height: u32, +} diff --git a/rust/crates/compositor/src/lib.rs b/rust/crates/compositor/src/lib.rs new file mode 100644 index 00000000..e749cd34 --- /dev/null +++ b/rust/crates/compositor/src/lib.rs @@ -0,0 +1,12 @@ +mod blend_mode; +mod compositor; +mod frame; +mod texture_pool; +mod texture_store; + +pub use blend_mode::BlendMode; +pub use compositor::{Compositor, CompositorError, RenderFrameOptions}; +pub use frame::{ + CanvasClearDescriptor, CanvasTextureDescriptor, EffectPassDescriptor, FrameDescriptor, + FrameItemDescriptor, LayerDescriptor, LayerMaskDescriptor, QuadTransformDescriptor, +}; diff --git a/rust/crates/compositor/src/shaders/blend.wgsl b/rust/crates/compositor/src/shaders/blend.wgsl new file mode 100644 index 00000000..9cc0e120 --- /dev/null +++ b/rust/crates/compositor/src/shaders/blend.wgsl @@ -0,0 +1,142 @@ +struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) tex_coord: vec2f, +} + +struct BlendUniforms { + blend_mode: u32, + _pad0: u32, + _pad1: u32, + _pad2: u32, +} + +@group(0) @binding(0) var base_texture: texture_2d; +@group(0) @binding(1) var base_sampler: sampler; +@group(1) @binding(0) var layer_texture: texture_2d; +@group(1) @binding(1) var layer_sampler: sampler; +@group(2) @binding(0) var uniforms: BlendUniforms; + +fn clamp01(color: vec3f) -> vec3f { + return clamp(color, vec3f(0.0), vec3f(1.0)); +} + +fn lum(c: vec3f) -> f32 { + return dot(c, vec3f(0.3, 0.59, 0.11)); +} + +fn sat(c: vec3f) -> f32 { + return max(max(c.r, c.g), c.b) - min(min(c.r, c.g), c.b); +} + +fn clip_color(c: vec3f) -> vec3f { + var result = c; + let l = lum(result); + let n = min(min(result.r, result.g), result.b); + let x = max(max(result.r, result.g), result.b); + if (n < 0.0) { + result = l + ((result - l) * l) / (l - n); + } + if (x > 1.0) { + result = l + ((result - l) * (1.0 - l)) / (x - l); + } + return result; +} + +fn set_lum(c: vec3f, l: f32) -> vec3f { + return clip_color(c + (l - lum(c))); +} + +fn set_sat(color: vec3f, target_sat: f32) -> vec3f { + var result = color; + let max_value = max(max(result.r, result.g), result.b); + let min_value = min(min(result.r, result.g), result.b); + if (max_value <= min_value) { + return vec3f(0.0); + } + let scale = target_sat / (max_value - min_value); + result = (result - vec3f(min_value)) * scale; + return result; +} + +fn hard_light(base: vec3f, layer: vec3f) -> vec3f { + let low = 2.0 * base * layer; + let high = 1.0 - 2.0 * (1.0 - base) * (1.0 - layer); + return select(low, high, layer >= vec3f(0.5)); +} + +fn soft_light_channel(base: f32, layer: f32) -> f32 { + if (layer <= 0.5) { + return base - (1.0 - 2.0 * layer) * base * (1.0 - base); + } + + let d = select( + ((16.0 * base - 12.0) * base + 4.0) * base, + sqrt(base), + base > 0.25, + ); + return base + (2.0 * layer - 1.0) * (d - base); +} + +fn soft_light(base: vec3f, layer: vec3f) -> vec3f { + return vec3f( + soft_light_channel(base.r, layer.r), + soft_light_channel(base.g, layer.g), + soft_light_channel(base.b, layer.b), + ); +} + +fn color_dodge(base: vec3f, layer: vec3f) -> vec3f { + return select( + min(vec3f(1.0), base / max(vec3f(0.0001), vec3f(1.0) - layer)), + vec3f(1.0), + layer >= vec3f(1.0), + ); +} + +fn color_burn(base: vec3f, layer: vec3f) -> vec3f { + return select( + vec3f(1.0) - min(vec3f(1.0), (vec3f(1.0) - base) / max(vec3f(0.0001), layer)), + vec3f(0.0), + layer <= vec3f(0.0), + ); +} + +fn blend_rgb(base: vec3f, layer: vec3f, mode: u32) -> vec3f { + switch mode { + case 1u { return min(base, layer); } + case 2u { return base * layer; } + case 3u { return color_burn(base, layer); } + case 4u { return max(base, layer); } + case 5u { return 1.0 - (1.0 - base) * (1.0 - layer); } + case 6u { return min(vec3f(1.0), base + layer); } + case 7u { return color_dodge(base, layer); } + case 8u { return select( + 2.0 * base * layer, + 1.0 - 2.0 * (1.0 - base) * (1.0 - layer), + base >= vec3f(0.5), + ); } + case 9u { return soft_light(base, layer); } + case 10u { return hard_light(base, layer); } + case 11u { return abs(base - layer); } + case 12u { return base + layer - 2.0 * base * layer; } + case 13u { return set_lum(set_sat(layer, sat(base)), lum(base)); } + case 14u { return set_lum(set_sat(base, sat(layer)), lum(base)); } + case 15u { return set_lum(layer, lum(base)); } + case 16u { return set_lum(base, lum(layer)); } + default { return layer; } + } +} + +@fragment +fn fragment_main(input: VertexOutput) -> @location(0) vec4f { + let base = textureSample(base_texture, base_sampler, input.tex_coord); + let layer = textureSample(layer_texture, layer_sampler, input.tex_coord); + + let blend_rgb_value = blend_rgb(base.rgb, layer.rgb, uniforms.blend_mode); + let out_alpha = layer.a + base.a * (1.0 - layer.a); + let out_rgb = + ((1.0 - layer.a) * base.rgb) + + (layer.a * ((1.0 - base.a) * layer.rgb + base.a * blend_rgb_value)); + + return vec4f(clamp01(out_rgb), out_alpha); +} diff --git a/rust/crates/compositor/src/shaders/layer.wgsl b/rust/crates/compositor/src/shaders/layer.wgsl new file mode 100644 index 00000000..9e335687 --- /dev/null +++ b/rust/crates/compositor/src/shaders/layer.wgsl @@ -0,0 +1,49 @@ +struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) tex_coord: vec2f, +} + +struct LayerUniforms { + resolution: vec2f, + center: vec2f, + size: vec2f, + rotation_radians: f32, + opacity: f32, + flip_x: f32, + flip_y: f32, +} + +@group(0) @binding(0) var source_texture: texture_2d; +@group(0) @binding(1) var source_sampler: sampler; +@group(1) @binding(0) var uniforms: LayerUniforms; + +fn rotate_inverse(point: vec2f, angle: f32) -> vec2f { + let c = cos(angle); + let s = sin(angle); + return vec2f( + point.x * c + point.y * s, + -point.x * s + point.y * c, + ); +} + +@fragment +fn fragment_main(input: VertexOutput) -> @location(0) vec4f { + let pixel = input.tex_coord * uniforms.resolution; + let local = rotate_inverse(pixel - uniforms.center, uniforms.rotation_radians); + + let uv = vec2f( + local.x / uniforms.size.x + 0.5, + local.y / uniforms.size.y + 0.5, + ); + + if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) { + return vec4f(0.0, 0.0, 0.0, 0.0); + } + + let sample_uv = vec2f( + select(uv.x, 1.0 - uv.x, uniforms.flip_x > 0.5), + select(uv.y, 1.0 - uv.y, uniforms.flip_y > 0.5), + ); + let color = textureSampleLevel(source_texture, source_sampler, sample_uv, 0.0); + return vec4f(color.rgb, color.a * uniforms.opacity); +} diff --git a/rust/crates/compositor/src/shaders/mask.wgsl b/rust/crates/compositor/src/shaders/mask.wgsl new file mode 100644 index 00000000..6bbfeccc --- /dev/null +++ b/rust/crates/compositor/src/shaders/mask.wgsl @@ -0,0 +1,25 @@ +struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) tex_coord: vec2f, +} + +struct MaskUniforms { + inverted: f32, + _pad0: f32, + _pad1: f32, + _pad2: f32, +} + +@group(0) @binding(0) var layer_texture: texture_2d; +@group(0) @binding(1) var layer_sampler: sampler; +@group(1) @binding(0) var mask_texture: texture_2d; +@group(1) @binding(1) var mask_sampler: sampler; +@group(2) @binding(0) var uniforms: MaskUniforms; + +@fragment +fn fragment_main(input: VertexOutput) -> @location(0) vec4f { + let layer = textureSample(layer_texture, layer_sampler, input.tex_coord); + let mask = textureSample(mask_texture, mask_sampler, input.tex_coord).a; + let alpha = select(mask, 1.0 - mask, uniforms.inverted > 0.5); + return vec4f(layer.rgb, layer.a * alpha); +} diff --git a/rust/crates/compositor/src/texture_pool.rs b/rust/crates/compositor/src/texture_pool.rs new file mode 100644 index 00000000..f2c59ee7 --- /dev/null +++ b/rust/crates/compositor/src/texture_pool.rs @@ -0,0 +1,36 @@ +use std::collections::HashMap; + +use gpu::{GpuContext, wgpu}; + +type TextureKey = (u32, u32); + +#[derive(Default)] +pub struct TexturePool { + available: HashMap>, + in_use: Vec<(TextureKey, wgpu::Texture)>, +} + +impl TexturePool { + pub fn recycle_frame(&mut self) { + for (key, texture) in self.in_use.drain(..) { + self.available.entry(key).or_default().push(texture); + } + } + + pub fn acquire( + &mut self, + context: &GpuContext, + width: u32, + height: u32, + label: &'static str, + ) -> wgpu::Texture { + let key = (width, height); + let texture = self + .available + .get_mut(&key) + .and_then(Vec::pop) + .unwrap_or_else(|| context.create_render_texture(width, height, label)); + self.in_use.push((key, texture.clone())); + texture + } +} diff --git a/rust/crates/compositor/src/texture_store.rs b/rust/crates/compositor/src/texture_store.rs new file mode 100644 index 00000000..ac905739 --- /dev/null +++ b/rust/crates/compositor/src/texture_store.rs @@ -0,0 +1,36 @@ +use std::collections::HashMap; + +use gpu::wgpu; + +pub struct StoredTexture { + texture: wgpu::Texture, +} + +impl StoredTexture { + pub fn new(texture: wgpu::Texture) -> Self { + Self { texture } + } + + pub fn texture(&self) -> &wgpu::Texture { + &self.texture + } +} + +#[derive(Default)] +pub struct TextureStore { + textures: HashMap, +} + +impl TextureStore { + pub fn upsert(&mut self, id: String, texture: wgpu::Texture) { + self.textures.insert(id, StoredTexture::new(texture)); + } + + pub fn get(&self, id: &str) -> Option<&StoredTexture> { + self.textures.get(id) + } + + pub fn remove(&mut self, id: &str) { + self.textures.remove(id); + } +} 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> = 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 { + 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 { + 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,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")]