parent
dbd68ff139
commit
8230faf082
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -7,5 +7,5 @@ members = [
|
|||
"rust/crates/effects",
|
||||
"rust/crates/gpu",
|
||||
"rust/crates/masks",
|
||||
"rust/wasm",
|
||||
"rust/wasm", "rust/crates/compositor",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<wgpu::Texture, CompositorError> {
|
||||
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<EffectPassDescriptor>],
|
||||
) -> Result<wgpu::Texture, CompositorError> {
|
||||
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<wgpu::Texture, CompositorError> {
|
||||
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<EffectPass> {
|
||||
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()
|
||||
}
|
||||
|
|
@ -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<FrameItemDescriptor>,
|
||||
}
|
||||
|
||||
#[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<Vec<EffectPassDescriptor>>,
|
||||
},
|
||||
}
|
||||
|
||||
#[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<Vec<EffectPassDescriptor>>,
|
||||
pub mask: Option<LayerMaskDescriptor>,
|
||||
}
|
||||
|
||||
#[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<String, EffectUniformValueDescriptor>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum EffectUniformValueDescriptor {
|
||||
Number(f32),
|
||||
Vector(Vec<f32>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CanvasTextureDescriptor {
|
||||
pub id: String,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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<f32>;
|
||||
@group(0) @binding(1) var base_sampler: sampler;
|
||||
@group(1) @binding(0) var layer_texture: texture_2d<f32>;
|
||||
@group(1) @binding(1) var layer_sampler: sampler;
|
||||
@group(2) @binding(0) var<uniform> 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);
|
||||
}
|
||||
|
|
@ -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<f32>;
|
||||
@group(0) @binding(1) var source_sampler: sampler;
|
||||
@group(1) @binding(0) var<uniform> 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);
|
||||
}
|
||||
|
|
@ -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<f32>;
|
||||
@group(0) @binding(1) var layer_sampler: sampler;
|
||||
@group(1) @binding(0) var mask_texture: texture_2d<f32>;
|
||||
@group(1) @binding(1) var mask_sampler: sampler;
|
||||
@group(2) @binding(0) var<uniform> 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);
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use gpu::{GpuContext, wgpu};
|
||||
|
||||
type TextureKey = (u32, u32);
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct TexturePool {
|
||||
available: HashMap<TextureKey, Vec<wgpu::Texture>>,
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String, StoredTexture>,
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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")]
|
||||
|
|
|
|||
Loading…
Reference in New Issue