summaryrefslogtreecommitdiff
path: root/rust/crates/compositor/src
diff options
context:
space:
mode:
Diffstat (limited to 'rust/crates/compositor/src')
-rw-r--r--rust/crates/compositor/src/blend_mode.rs47
-rw-r--r--rust/crates/compositor/src/compositor.rs816
-rw-r--r--rust/crates/compositor/src/frame.rs83
-rw-r--r--rust/crates/compositor/src/lib.rs12
-rw-r--r--rust/crates/compositor/src/shaders/blend.wgsl142
-rw-r--r--rust/crates/compositor/src/shaders/layer.wgsl49
-rw-r--r--rust/crates/compositor/src/shaders/mask.wgsl25
-rw-r--r--rust/crates/compositor/src/texture_pool.rs36
-rw-r--r--rust/crates/compositor/src/texture_store.rs36
9 files changed, 1246 insertions, 0 deletions
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<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(),
+ &current,
+ frame.width,
+ frame.height,
+ layer,
+ );
+
+ if !layer.effect_pass_groups.is_empty() {
+ current = self.apply_effect_groups(
+ context,
+ encoder,
+ &current,
+ 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,
+ &current,
+ &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: &current,
+ 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()
+}
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<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,
+}
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<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);
+}
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<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);
+}
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<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);
+}
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<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
+ }
+}
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<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);
+ }
+}