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)]
156mod tests {
157 use super::*;
158 use ff_format::{PixelFormat, VideoFrame};
159
160 fn make_frame() -> VideoFrame {
161 VideoFrame::empty(2, 2, PixelFormat::Rgba).expect("test frame")
162 }
163
164 #[test]
165 fn layer_transform_default_should_be_identity() {
166 let t = LayerTransform::default();
167 assert!(
168 t.is_identity(),
169 "default LayerTransform must be the identity"
170 );
171 }
172
173 #[test]
174 fn layer_transform_nonzero_x_should_not_be_identity() {
175 let t = LayerTransform {
176 x: 0.1,
177 ..Default::default()
178 };
179 assert!(
180 !t.is_identity(),
181 "LayerTransform with non-zero x must not be identity"
182 );
183 }
184
185 #[test]
186 fn frame_layer_should_construct_with_defaults() {
187 let layer = FrameLayer {
188 frame: make_frame(),
189 transform: LayerTransform::default(),
190 blend_mode: BlendMode::Normal,
191 opacity: 1.0,
192 z_order: 0,
193 };
194 assert_eq!(layer.z_order, 0);
195 assert!((layer.opacity - 1.0).abs() < 1e-6);
196 }
197
198 #[test]
199 fn compositor_layers_should_sort_by_z_order() {
200 let mut layers = vec![
201 FrameLayer {
202 frame: make_frame(),
203 transform: LayerTransform::default(),
204 blend_mode: BlendMode::Normal,
205 opacity: 1.0,
206 z_order: 3,
207 },
208 FrameLayer {
209 frame: make_frame(),
210 transform: LayerTransform::default(),
211 blend_mode: BlendMode::Normal,
212 opacity: 1.0,
213 z_order: 1,
214 },
215 FrameLayer {
216 frame: make_frame(),
217 transform: LayerTransform::default(),
218 blend_mode: BlendMode::Normal,
219 opacity: 1.0,
220 z_order: 2,
221 },
222 ];
223 layers.sort_unstable_by_key(|l| l.z_order);
224 let z_orders: Vec<i32> = layers.iter().map(|l| l.z_order).collect();
225 assert_eq!(
226 z_orders,
227 vec![1, 2, 3],
228 "layers must sort ascending by z_order"
229 );
230 }
231
232 #[cfg(feature = "wgpu")]
233 #[test]
234 fn compositor_should_be_send() {
235 fn assert_send<T: Send>() {}
236 assert_send::<Compositor>();
237 }
238}