ff_render/compositor/
mod.rs1#[cfg(feature = "wgpu")]
2mod compositor_inner;
3
4use ff_format::VideoFrame;
5
6use crate::nodes::BlendMode;
7
8#[derive(Debug, Clone)]
15pub struct LayerTransform {
16 pub x: f32,
18 pub y: f32,
20 pub scale_x: f32,
22 pub scale_y: f32,
24 pub rotation: f32,
26}
27
28impl Default for LayerTransform {
29 fn default() -> Self {
30 Self {
31 x: 0.0,
32 y: 0.0,
33 scale_x: 1.0,
34 scale_y: 1.0,
35 rotation: 0.0,
36 }
37 }
38}
39
40impl LayerTransform {
41 #[must_use]
43 pub fn is_identity(&self) -> bool {
44 self.x.abs() < 1e-6
45 && self.y.abs() < 1e-6
46 && (self.scale_x - 1.0).abs() < 1e-6
47 && (self.scale_y - 1.0).abs() < 1e-6
48 && self.rotation.abs() < 1e-6
49 }
50}
51
52pub struct FrameLayer {
56 pub frame: VideoFrame,
58 pub transform: LayerTransform,
60 pub blend_mode: BlendMode,
62 pub opacity: f32,
64 pub z_order: i32,
67}
68
69#[cfg(feature = "wgpu")]
89pub struct Compositor {
90 ctx: std::sync::Arc<crate::context::RenderContext>,
91 width: u32,
92 height: u32,
93 graph: Option<compositor_inner::CompositorGraph>,
94 last_layer_count: usize,
95}
96
97#[cfg(feature = "wgpu")]
98impl Compositor {
99 #[must_use]
101 pub fn new(
102 ctx: std::sync::Arc<crate::context::RenderContext>,
103 width: u32,
104 height: u32,
105 ) -> Self {
106 Self {
107 ctx,
108 width,
109 height,
110 graph: None,
111 last_layer_count: 0,
112 }
113 }
114
115 pub fn composite(
128 &mut self,
129 layers: &mut [FrameLayer],
130 ) -> Result<wgpu::Texture, crate::error::RenderError> {
131 layers.sort_unstable_by_key(|l| l.z_order);
132
133 let need_rebuild = self.graph.is_none() || self.last_layer_count != layers.len();
134 if need_rebuild {
135 self.graph = Some(compositor_inner::CompositorGraph::build(
136 &self.ctx,
137 layers.len(),
138 self.width,
139 self.height,
140 ));
141 self.last_layer_count = layers.len();
142 }
143
144 let Some(graph) = self.graph.as_mut() else {
145 return Err(crate::error::RenderError::Composite {
146 message: "compositor graph not initialized".to_string(),
147 });
148 };
149 graph.composite(&self.ctx, layers, self.width, self.height)
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156 use ff_format::{PixelFormat, VideoFrame};
157
158 fn make_frame() -> VideoFrame {
159 VideoFrame::empty(2, 2, PixelFormat::Rgba).expect("test frame")
160 }
161
162 #[test]
163 fn layer_transform_default_should_be_identity() {
164 let t = LayerTransform::default();
165 assert!(
166 t.is_identity(),
167 "default LayerTransform must be the identity"
168 );
169 }
170
171 #[test]
172 fn layer_transform_nonzero_x_should_not_be_identity() {
173 let t = LayerTransform {
174 x: 0.1,
175 ..Default::default()
176 };
177 assert!(
178 !t.is_identity(),
179 "LayerTransform with non-zero x must not be identity"
180 );
181 }
182
183 #[test]
184 fn frame_layer_should_construct_with_defaults() {
185 let layer = FrameLayer {
186 frame: make_frame(),
187 transform: LayerTransform::default(),
188 blend_mode: BlendMode::Normal,
189 opacity: 1.0,
190 z_order: 0,
191 };
192 assert_eq!(layer.z_order, 0);
193 assert!((layer.opacity - 1.0).abs() < 1e-6);
194 }
195
196 #[test]
197 fn compositor_layers_should_sort_by_z_order() {
198 let mut layers = vec![
199 FrameLayer {
200 frame: make_frame(),
201 transform: LayerTransform::default(),
202 blend_mode: BlendMode::Normal,
203 opacity: 1.0,
204 z_order: 3,
205 },
206 FrameLayer {
207 frame: make_frame(),
208 transform: LayerTransform::default(),
209 blend_mode: BlendMode::Normal,
210 opacity: 1.0,
211 z_order: 1,
212 },
213 FrameLayer {
214 frame: make_frame(),
215 transform: LayerTransform::default(),
216 blend_mode: BlendMode::Normal,
217 opacity: 1.0,
218 z_order: 2,
219 },
220 ];
221 layers.sort_unstable_by_key(|l| l.z_order);
222 let z_orders: Vec<i32> = layers.iter().map(|l| l.z_order).collect();
223 assert_eq!(
224 z_orders,
225 vec![1, 2, 3],
226 "layers must sort ascending by z_order"
227 );
228 }
229
230 #[cfg(feature = "wgpu")]
231 #[test]
232 fn compositor_should_be_send() {
233 fn assert_send<T: Send>() {}
234 assert_send::<Compositor>();
235 }
236}