Skip to main content

ffcine_render/
pipeline.rs

1//! A pipeline: one input, a chain of GPU stages, one encoded output.
2
3use std::path::{Path, PathBuf};
4use std::time::Duration;
5
6use crate::{EncoderSettings, Error, Grade, Layer, MediaInfo, Overlay, Rational, Result};
7use ffcine_core::color::named_enum;
8use std::fmt;
9use std::str::FromStr;
10
11named_enum!(
12    /// Hardware decode API. Both drive the same video decoder; Vulkan hands frames straight to
13    /// the renderer, CUDA (NVDEC) needs a copy into Vulkan memory per frame.
14    #[derive(Default)]
15    Decode {
16        #[default]
17        Vulkan = "vulkan",
18        Cuda = "cuda",
19    }
20);
21
22/// Which GPU to run on.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
24#[non_exhaustive]
25pub enum Device {
26    /// Vulkan device 0 (else CUDA device 0).
27    #[default]
28    Auto,
29    /// A CUDA device, with the Vulkan device derived from it so decode, processing and encode
30    /// share one physical GPU regardless of Vulkan enumeration order. No Vulkan video encode.
31    Cuda(u32),
32    /// A Vulkan physical device index, with CUDA (NVENC) derived from it.
33    Vulkan(u32),
34}
35
36/// Frames read around a pipeline's range for temporal filters. See [`Pipeline::context`].
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
38pub struct FrameContext {
39    pub before: u64,
40    pub after: u64,
41}
42
43/// One processing step. Frames stay on the GPU between stages.
44#[derive(Debug, Clone, PartialEq)]
45#[non_exhaustive]
46pub enum Stage {
47    /// A libplacebo render pass (tone mapping, crop/scale, LUT, shaders).
48    Grade(Grade),
49}
50
51/// Decode `input`, run `stages` in order, encode with `encoder` into `output`.
52#[derive(Debug, Clone, PartialEq)]
53pub struct Pipeline {
54    pub input: PathBuf,
55    pub output: PathBuf,
56    /// Seek here first; earlier frames are decoded and dropped (ffmpeg's accurate `-ss`).
57    pub start: Duration,
58    /// Output exactly this many frames (repeating the last one if the source runs out).
59    pub frames: Option<u64>,
60    /// Frames before `start` and after the last output frame that a temporal denoiser reads as
61    /// history and lookahead but that aren't output (fewer where the input runs out). Ignored
62    /// without one.
63    pub context: FrameContext,
64    /// Output frame rate (ffmpeg's `fps` filter); `None` keeps the source's frames.
65    pub frame_rate: Option<Rational>,
66    pub stages: Vec<Stage>,
67    pub encoder: EncoderSettings,
68    pub device: Device,
69    pub decode: Decode,
70    /// Pictures composited over the graded frames in the same pass, in order, under the
71    /// overlays.
72    pub layers: Vec<Layer>,
73    /// Drawn over the graded frames in the same pass, each over its frame range.
74    pub overlays: Vec<Overlay>,
75}
76
77impl Pipeline {
78    pub fn new(input: impl Into<PathBuf>, output: impl Into<PathBuf>) -> Self {
79        Self {
80            input: input.into(),
81            output: output.into(),
82            start: Duration::ZERO,
83            frames: None,
84            context: FrameContext::default(),
85            frame_rate: None,
86            stages: Vec::new(),
87            encoder: EncoderSettings::default(),
88            device: Device::Auto,
89            decode: Decode::default(),
90            layers: Vec::new(),
91            overlays: Vec::new(),
92        }
93    }
94
95    #[must_use]
96    pub fn stage(mut self, stage: Stage) -> Self {
97        self.stages.push(stage);
98        self
99    }
100
101    #[must_use]
102    pub fn grade(self, grade: Grade) -> Self {
103        self.stage(Stage::Grade(grade))
104    }
105
106    /// Source range: start at `start`, output `frames` frames.
107    #[must_use]
108    pub fn range(mut self, start: Duration, frames: Option<u64>) -> Self {
109        self.start = start;
110        self.frames = frames;
111        self
112    }
113
114    /// See [`Pipeline::context`].
115    #[must_use]
116    pub fn context(mut self, before: u64, after: u64) -> Self {
117        self.context = FrameContext { before, after };
118        self
119    }
120
121    #[must_use]
122    pub fn decode(mut self, decode: Decode) -> Self {
123        self.decode = decode;
124        self
125    }
126
127    #[must_use]
128    pub fn frame_rate(mut self, rate: Rational) -> Self {
129        self.frame_rate = Some(rate);
130        self
131    }
132
133    #[must_use]
134    pub fn encoder(mut self, encoder: EncoderSettings) -> Self {
135        self.encoder = encoder;
136        self
137    }
138
139    /// Overlays (text) to composite, frame ranges relative to the first output frame.
140    #[must_use]
141    pub fn overlays(mut self, overlays: Vec<Overlay>) -> Self {
142        self.overlays = overlays;
143        self
144    }
145
146    /// Layers to composite, frame ranges relative to the first output frame.
147    #[must_use]
148    pub fn layers(mut self, layers: Vec<Layer>) -> Self {
149        self.layers = layers;
150        self
151    }
152
153    #[must_use]
154    pub fn device(mut self, device: Device) -> Self {
155        self.device = device;
156        self
157    }
158
159    /// Runs the pipeline on `backend`.
160    pub fn run(&self, backend: &dyn Backend) -> Result<RunStats> {
161        backend.run(self)
162    }
163}
164
165/// An implementation of ffcine's operations. The library's types don't depend on any backend;
166/// the ffmpeg + libplacebo one is `crate::backend::Ffmpeg` (feature `ffmpeg`).
167pub trait Backend {
168    fn name(&self) -> &'static str;
169    fn probe(&self, path: &Path) -> Result<MediaInfo>;
170    fn run(&self, pipeline: &Pipeline) -> Result<RunStats>;
171}
172
173/// What a [`Pipeline::run`] did.
174#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
175pub struct RunStats {
176    /// Frames encoded.
177    pub frames: u64,
178    /// Time before the first frame: opening, codec setup, LUT/shader upload (and, on the first
179    /// run of a process, device init).
180    pub setup: Duration,
181    /// Time until the first frame was handed to the encoder.
182    pub first_frame: Duration,
183    /// Total wall time.
184    pub wall: Duration,
185}
186
187impl RunStats {
188    /// Frames per second after the first frame (excludes setup and pipeline fill).
189    #[allow(clippy::cast_precision_loss)]
190    pub fn steady_fps(&self) -> Option<f64> {
191        let t = self.wall.checked_sub(self.first_frame)?.as_secs_f64();
192        (self.frames > 1 && t > 0.0).then(|| (self.frames - 1) as f64 / t)
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199
200    #[test]
201    fn builder() {
202        let p = Pipeline::new("in.mov", "out.mkv")
203            .grade(Grade::default())
204            .device(Device::Cuda(0));
205        assert_eq!(p.stages.len(), 1);
206        assert_eq!(p.device, Device::Cuda(0));
207        assert_eq!(p.encoder, EncoderSettings::nvenc_hevc_master());
208    }
209}