Skip to main content

ffcine_filters/
lib.rs

1//! Frame filters that are ours rather than libplacebo render params: a motion-compensated
2//! temporal denoiser, a spatial NLM, and film grain. `docs/denoise.md`,
3//! `docs/design.md#frame-filters`.
4//!
5//! A [`Filter`] says where it runs ([`Point`]) and what it reads ([`Footprint`], motion), and
6//! [`FilterChain::plan`] groups a chain into dispatches: a pointwise filter folds into the
7//! dispatch before it; one that gathers neighbours (spatial or temporal) needs the whole previous
8//! result written out first, so it starts a new dispatch.
9//!
10//! How they run (`ffcine-render`, `backend/denoise.rs`), all on the grade pass's `pl_gpu` (so
11//! ffmpeg's `VkDevice`, its frames), nothing leaving VRAM:
12//!
13//! - [`Point::Source`] (denoise) runs as compute dispatches on the decoded planes right before
14//!   `pl_render_image`, into textures the render then reads as its source frame. Not a `pl_hook`:
15//!   temporal filters need frames *after* the current one, and a hook can't delay its output.
16//! - [`Point::Output`] (grain) is pointwise, so it becomes an mpv `.hook` on `OUTPUT` and fuses
17//!   into the render pass itself.
18//!
19//! Kernels are generated GLSL ([`denoise`], [`grain`]); this crate has no GPU dependency.
20
21pub mod denoise;
22pub mod effect;
23pub mod grain;
24
25pub use denoise::{Denoise, Motion, Overrides, Preset, Spatial, Temporal};
26pub use grain::Grain;
27
28use std::fmt;
29
30/// Where in the libplacebo grade pass a filter runs.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
32pub enum Point {
33    /// Source planes before colour conversion, tone mapping and scaling (`PL_HOOK_NATIVE`):
34    /// denoise, deband.
35    Source,
36    /// Graded, scaled output before dither (`PL_HOOK_OUTPUT`): grain, sharpen.
37    Output,
38}
39
40/// What a filter reads around the pixel it writes.
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
42pub enum Footprint {
43    /// Only the same pixel of the current frame; fuses into the dispatch before it.
44    Pixel,
45    /// Pixels within `radius` of it in the current frame.
46    Spatial { radius: u32 },
47    /// Pixels within `radius` in the current frame and the `past` frames before and `future`
48    /// frames after it (a ring of textures, motion-compensated when [`Filter::motion`] is set).
49    /// `future` frames delay the output by as many frames.
50    Temporal { past: u32, future: u32, radius: u32 },
51}
52
53impl Footprint {
54    /// Whether the filter reads anything but its own pixel, so can't fuse into the previous
55    /// dispatch.
56    pub const fn gathers(self) -> bool {
57        !matches!(self, Self::Pixel)
58    }
59
60    /// Previous frames it needs.
61    pub const fn past(self) -> u32 {
62        match self {
63            Self::Temporal { past, .. } => past,
64            _ => 0,
65        }
66    }
67
68    /// Later frames it needs (output delay).
69    pub const fn future(self) -> u32 {
70        match self {
71            Self::Temporal { future, .. } => future,
72            _ => 0,
73        }
74    }
75}
76
77/// One frame filter.
78pub trait Filter: fmt::Debug + Send + Sync {
79    /// Short name for logs and the timeline (`denoise`, `grain`, ...).
80    fn name(&self) -> &'static str;
81    fn point(&self) -> Point;
82    fn footprint(&self) -> Footprint;
83    /// Whether it needs motion vectors to the other frames it reads.
84    fn motion(&self) -> bool {
85        false
86    }
87}
88
89/// One compute dispatch: a gathering filter (or the frame as is) followed by pointwise ones.
90#[derive(Debug, Clone, PartialEq, Eq)]
91pub struct Pass {
92    pub point: Point,
93    /// Indices into the chain, in order.
94    pub filters: Vec<usize>,
95    /// Past frames to keep for this pass.
96    pub past: u32,
97    /// Frames of lookahead.
98    pub future: u32,
99    pub motion: bool,
100}
101
102/// Filters in the order they apply within each [`Point`].
103#[derive(Debug, Default)]
104pub struct FilterChain {
105    pub filters: Vec<Box<dyn Filter>>,
106}
107
108impl FilterChain {
109    #[must_use]
110    pub fn push(mut self, filter: impl Filter + 'static) -> Self {
111        self.filters.push(Box::new(filter));
112        self
113    }
114
115    /// Groups the filters into dispatches, `Source` passes first.
116    pub fn plan(&self) -> Vec<Pass> {
117        let mut order: Vec<usize> = (0..self.filters.len()).collect();
118        order.sort_by_key(|&i| self.filters[i].point());
119        let mut passes: Vec<Pass> = Vec::new();
120        for i in order {
121            let f = &self.filters[i];
122            let fp = f.footprint();
123            match passes.last_mut() {
124                Some(p) if p.point == f.point() && !fp.gathers() => p.filters.push(i),
125                _ => passes.push(Pass {
126                    point: f.point(),
127                    filters: vec![i],
128                    past: fp.past(),
129                    future: fp.future(),
130                    motion: f.motion(),
131                }),
132            }
133        }
134        passes
135    }
136}
137
138impl Filter for Temporal {
139    fn name(&self) -> &'static str {
140        "denoise.temporal"
141    }
142    fn point(&self) -> Point {
143        Point::Source
144    }
145    fn footprint(&self) -> Footprint {
146        Footprint::Temporal {
147            past: self.radius,
148            future: self.radius,
149            radius: 1,
150        }
151    }
152    fn motion(&self) -> bool {
153        true
154    }
155}
156
157impl Filter for Spatial {
158    fn name(&self) -> &'static str {
159        "denoise.spatial"
160    }
161    fn point(&self) -> Point {
162        Point::Source
163    }
164    fn footprint(&self) -> Footprint {
165        Footprint::Spatial {
166            radius: self.radius + 1,
167        }
168    }
169}
170
171impl Filter for Grain {
172    fn name(&self) -> &'static str {
173        "grain"
174    }
175    fn point(&self) -> Point {
176        Point::Output
177    }
178    fn footprint(&self) -> Footprint {
179        Footprint::Pixel
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186
187    #[derive(Debug)]
188    struct Stub(&'static str, Point, Footprint);
189
190    impl Filter for Stub {
191        fn name(&self) -> &'static str {
192            self.0
193        }
194        fn point(&self) -> Point {
195            self.1
196        }
197        fn footprint(&self) -> Footprint {
198            self.2
199        }
200        fn motion(&self) -> bool {
201            self.2.past() > 0
202        }
203    }
204
205    fn filters(p: &Pass) -> Vec<usize> {
206        p.filters.clone()
207    }
208
209    #[test]
210    fn pointwise_filters_fuse_behind_a_gather() {
211        let chain = FilterChain::default()
212            .push(Stub(
213                "denoise",
214                Point::Source,
215                Footprint::Temporal {
216                    past: 2,
217                    future: 2,
218                    radius: 3,
219                },
220            ))
221            .push(Stub("lift", Point::Source, Footprint::Pixel))
222            .push(Stub("grain", Point::Output, Footprint::Pixel));
223        let plan = chain.plan();
224        assert_eq!(plan.len(), 2);
225        assert_eq!(
226            (filters(&plan[0]), plan[0].past, plan[0].motion),
227            (vec![0, 1], 2, true)
228        );
229        assert_eq!((plan[1].point, filters(&plan[1])), (Point::Output, vec![2]));
230    }
231
232    #[test]
233    fn each_gather_starts_a_dispatch_and_points_sort() {
234        let chain = FilterChain::default()
235            .push(Stub(
236                "sharpen",
237                Point::Output,
238                Footprint::Spatial { radius: 2 },
239            ))
240            .push(Stub(
241                "deband",
242                Point::Source,
243                Footprint::Spatial { radius: 16 },
244            ))
245            .push(Stub(
246                "denoise",
247                Point::Source,
248                Footprint::Spatial { radius: 7 },
249            ));
250        let plan: Vec<_> = chain.plan().iter().map(filters).collect();
251        assert_eq!(plan, [vec![1], vec![2], vec![0]]);
252    }
253}