#[cfg(feature = "wgpu")]
mod compositor_inner;
use ff_format::VideoFrame;
use crate::nodes::BlendMode;
#[derive(Debug, Clone)]
pub struct LayerTransform {
pub x: f32,
pub y: f32,
pub scale_x: f32,
pub scale_y: f32,
pub rotation: f32,
}
impl Default for LayerTransform {
fn default() -> Self {
Self {
x: 0.0,
y: 0.0,
scale_x: 1.0,
scale_y: 1.0,
rotation: 0.0,
}
}
}
impl LayerTransform {
#[must_use]
pub fn is_identity(&self) -> bool {
self.x.abs() < 1e-6
&& self.y.abs() < 1e-6
&& (self.scale_x - 1.0).abs() < 1e-6
&& (self.scale_y - 1.0).abs() < 1e-6
&& self.rotation.abs() < 1e-6
}
}
pub struct FrameLayer {
pub frame: VideoFrame,
pub transform: LayerTransform,
pub blend_mode: BlendMode,
pub opacity: f32,
pub z_order: i32,
}
#[cfg(feature = "wgpu")]
pub struct Compositor {
ctx: std::sync::Arc<crate::context::RenderContext>,
width: u32,
height: u32,
graph: Option<compositor_inner::CompositorGraph>,
last_layer_count: usize,
}
#[cfg(feature = "wgpu")]
impl Compositor {
#[must_use]
pub fn new(
ctx: std::sync::Arc<crate::context::RenderContext>,
width: u32,
height: u32,
) -> Self {
Self {
ctx,
width,
height,
graph: None,
last_layer_count: 0,
}
}
pub fn composite(
&mut self,
layers: &mut [FrameLayer],
) -> Result<wgpu::Texture, crate::error::RenderError> {
layers.sort_unstable_by_key(|l| l.z_order);
let need_rebuild = self.graph.is_none() || self.last_layer_count != layers.len();
if need_rebuild {
self.graph = Some(compositor_inner::CompositorGraph::build(
&self.ctx,
layers.len(),
self.width,
self.height,
));
self.last_layer_count = layers.len();
}
let Some(graph) = self.graph.as_mut() else {
return Err(crate::error::RenderError::Composite {
message: "compositor graph not initialized".to_string(),
});
};
graph.composite(&self.ctx, layers, self.width, self.height)
}
}
#[cfg(test)]
mod tests {
use super::*;
use ff_format::{PixelFormat, VideoFrame};
fn make_frame() -> VideoFrame {
VideoFrame::empty(2, 2, PixelFormat::Rgba).expect("test frame")
}
#[test]
fn layer_transform_default_should_be_identity() {
let t = LayerTransform::default();
assert!(
t.is_identity(),
"default LayerTransform must be the identity"
);
}
#[test]
fn layer_transform_nonzero_x_should_not_be_identity() {
let t = LayerTransform {
x: 0.1,
..Default::default()
};
assert!(
!t.is_identity(),
"LayerTransform with non-zero x must not be identity"
);
}
#[test]
fn frame_layer_should_construct_with_defaults() {
let layer = FrameLayer {
frame: make_frame(),
transform: LayerTransform::default(),
blend_mode: BlendMode::Normal,
opacity: 1.0,
z_order: 0,
};
assert_eq!(layer.z_order, 0);
assert!((layer.opacity - 1.0).abs() < 1e-6);
}
#[test]
fn compositor_layers_should_sort_by_z_order() {
let mut layers = vec![
FrameLayer {
frame: make_frame(),
transform: LayerTransform::default(),
blend_mode: BlendMode::Normal,
opacity: 1.0,
z_order: 3,
},
FrameLayer {
frame: make_frame(),
transform: LayerTransform::default(),
blend_mode: BlendMode::Normal,
opacity: 1.0,
z_order: 1,
},
FrameLayer {
frame: make_frame(),
transform: LayerTransform::default(),
blend_mode: BlendMode::Normal,
opacity: 1.0,
z_order: 2,
},
];
layers.sort_unstable_by_key(|l| l.z_order);
let z_orders: Vec<i32> = layers.iter().map(|l| l.z_order).collect();
assert_eq!(
z_orders,
vec![1, 2, 3],
"layers must sort ascending by z_order"
);
}
#[cfg(feature = "wgpu")]
#[test]
fn compositor_should_be_send() {
fn assert_send<T: Send>() {}
assert_send::<Compositor>();
}
}