ffcine_render/layer.rs
1//! Picture layers composited over a pipeline's graded frames in the same GPU pass: split-screen
2//! panels, picture-in-picture, stickers and logos, the incoming side of a transition.
3//!
4//! Each [`Layer`] has its own source (a video decoded alongside the main input, or a still
5//! image) and its own [`Grade`]. The backend renders the layer's frame into an RGBA texture of
6//! the grade's `size` (its hooks can write alpha: [`crate::shaders::alpha`] for opacity and
7//! rounded corners), then draws it over the main picture at the frame's placement, under the
8//! text [`Overlay`](crate::Overlay)s. A layer that is turned or not [`BlendMode::Normal`] is
9//! composited by a shader pass that reads the picture under it.
10
11use std::path::PathBuf;
12use std::sync::Arc;
13use std::time::Duration;
14
15use crate::{BlendMode, Crop, Grade};
16
17/// An RGBA image, 8 bits per channel, not premultiplied, row-major.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct Image {
20 pub width: u32,
21 pub height: u32,
22 pub data: Vec<u8>,
23}
24
25/// The largest still a [`LayerSource::Still`] decodes, px a side.
26pub const STILL_SIDE: u32 = 16384;
27/// The largest still a [`LayerSource::Still`] decodes, px in all (256 MP).
28pub const STILL_PIXELS: u64 = 1 << 28;
29
30/// Whether a `w` x `h` still is within [`STILL_SIDE`] and [`STILL_PIXELS`].
31pub fn still_fits(w: u32, h: u32) -> bool {
32 w > 0 && h > 0 && w.max(h) <= STILL_SIDE && u64::from(w) * u64::from(h) <= STILL_PIXELS
33}
34
35/// Where a layer's pictures come from.
36#[derive(Debug, Clone, PartialEq)]
37pub enum LayerSource {
38 /// A video file, decoded from `start` and conformed to the pipeline's frame rate. Past its
39 /// end the last frame holds.
40 ///
41 /// With a `map` (a time remap, `docs/compositing.md#time-remapping`), layer frame `i` shows
42 /// source frame `map[i]`, counted from the first frame at or after `start` (0) on the
43 /// source's own rate, and nothing is conformed. A non-decreasing map decodes forward,
44 /// repeating and dropping frames; a step back seeks (cheap on an all-intra file).
45 Video {
46 input: PathBuf,
47 start: Duration,
48 map: Option<Arc<[i64]>>,
49 },
50 /// A still picture, drawn on every frame. Its values are taken as the output's colour
51 /// space (no conversion) for an SDR output, so a logo keeps its colours, and as sRGB with
52 /// white at the HDR reference white (203 nits, BT.2408) for a PQ or HLG one.
53 Image(Arc<Image>),
54 /// A still file (PNG, JPEG, WebP, HEIC with tile grids and `irot`), decoded through libav
55 /// once per layer of a rendered piece. A tagged file (primaries or transfer set: HEIC
56 /// `colr`, PNG `cICP` or `sRGB`) is converted from its tags; an untagged one is taken as an
57 /// [`Image`](Self::Image) is. JPEG EXIF orientation is not applied; at most
58 /// [`STILL_SIDE`] px a side and [`STILL_PIXELS`] in all.
59 Still(PathBuf),
60 /// A nested composition rendered in the same pass into its own texture.
61 Scene(Arc<Scene>),
62 /// A video through a libavfilter graph (`docs/compositing.md#libavfilter-escape-hatch`):
63 /// the renderer runs the [`Prep`](crate::filter_node::Prep) into a lossless intermediate and
64 /// replaces this with a [`Video`](Self::Video) of it before the pipeline runs (a backend
65 /// refuses it unprepared).
66 Filtered(Arc<crate::filter_node::Prep>),
67}
68
69/// A composition drawn into a layer's source texture: its `grade` (the background hook, `size`
70/// the scene's size) over a black frame, then `layers` bottom first and `overlays`, frame
71/// ranges relative to the using layer's `from`.
72#[derive(Debug, Clone, PartialEq)]
73pub struct Scene {
74 pub size: crate::Size,
75 pub grade: Grade,
76 pub layers: Vec<Layer>,
77 pub overlays: Vec<crate::Overlay>,
78}
79
80/// One layer over output frames `from..to`.
81#[derive(Debug, Clone, PartialEq)]
82pub struct Layer {
83 pub source: LayerSource,
84 /// The layer's grade; `size` is its texture size (default: the source's). Hook tracks and
85 /// `crops` are indexed by layer frame (`0` = output frame `from`).
86 pub grade: Grade,
87 pub from: i64,
88 pub to: i64,
89 /// Placement in output pixels per layer frame (the last one holds); empty fills the frame.
90 pub rects: Vec<Crop>,
91 /// Rotation in degrees per layer frame, counter-clockwise on screen about the rectangle's
92 /// middle (the last one holds); empty is unrotated. Corners outside the picture stay
93 /// transparent.
94 pub rotation: Vec<f32>,
95 pub blend: BlendMode,
96}
97
98impl Layer {
99 /// The placement at output frame `frame`, `None` outside `from..to`.
100 pub fn rect_at(&self, frame: i64, width: u32, height: u32) -> Option<Crop> {
101 if frame < self.from || frame >= self.to {
102 return None;
103 }
104 let i = usize::try_from(frame - self.from).unwrap_or(0);
105 #[allow(clippy::cast_precision_loss)]
106 Some(
107 self.rects
108 .get(i)
109 .or(self.rects.last())
110 .copied()
111 .unwrap_or(Crop {
112 x: 0.0,
113 y: 0.0,
114 width: width as f32,
115 height: height as f32,
116 }),
117 )
118 }
119
120 /// The rotation at output frame `frame` (0 outside `from..to`).
121 pub fn rotation_at(&self, frame: i64) -> f32 {
122 if frame < self.from || frame >= self.to {
123 return 0.0;
124 }
125 let i = usize::try_from(frame - self.from).unwrap_or(0);
126 self.rotation
127 .get(i)
128 .or(self.rotation.last())
129 .copied()
130 .unwrap_or(0.0)
131 }
132
133 /// Whether frame `frame` needs the composite pass (turned, or blended other than normal).
134 pub fn needs_composite(&self, frame: i64) -> bool {
135 self.blend != BlendMode::Normal || self.rotation_at(frame) % 360.0 != 0.0
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 #[test]
144 fn rect_holds_and_ranges() {
145 let r = |x| Crop {
146 x,
147 y: 0.0,
148 width: 10.0,
149 height: 10.0,
150 };
151 let l = Layer {
152 source: LayerSource::Image(Arc::new(Image {
153 width: 1,
154 height: 1,
155 data: vec![0; 4],
156 })),
157 grade: Grade::default(),
158 from: 5,
159 to: 10,
160 rects: vec![r(0.0), r(1.0)],
161 rotation: vec![0.0, 30.0],
162 blend: BlendMode::Normal,
163 };
164 assert_eq!(l.rect_at(4, 100, 50), None);
165 assert_eq!(l.rect_at(5, 100, 50), Some(r(0.0)));
166 assert_eq!(l.rect_at(9, 100, 50), Some(r(1.0)));
167 assert_eq!(l.rect_at(10, 100, 50), None);
168 let full = Layer {
169 rects: Vec::new(),
170 ..l
171 };
172 assert_eq!(full.rect_at(5, 100, 50).map(|c| c.width), Some(100.0));
173 assert!(!full.needs_composite(5));
174 assert!(full.needs_composite(7));
175 assert!((full.rotation_at(9) - 30.0).abs() < f32::EPSILON);
176 assert!(full.rotation_at(10).abs() < f32::EPSILON);
177 let multiply = Layer {
178 rotation: Vec::new(),
179 blend: BlendMode::Multiply,
180 ..full
181 };
182 assert!(multiply.needs_composite(5));
183 }
184}