Skip to main content

ffcine_hooks/
parse.rs

1//! Just enough of the mpv `.hook` format to check a shader before libplacebo sees it: the stages
2//! it hooks and its `//!PARAM`s (name, type, bounds, default).
3
4use ffcine_core::{Error, Result};
5
6/// libplacebo's hook stages in pipeline order (mpv names). `MAIN` is libplacebo's `RGB` stage,
7/// after the planes are merged, converted to RGB and the source LUT (the grade) applied.
8pub const STAGES: &[&str] = &[
9    "RGB",
10    "LUMA",
11    "CHROMA",
12    "ALPHA",
13    "XYZ",
14    "CHROMA_SCALED",
15    "ALPHA_SCALED",
16    "NATIVE",
17    "MAINPRESUB",
18    "MAIN",
19    "LINEAR",
20    "SIGMOID",
21    "PREKERNEL",
22    "POSTKERNEL",
23    "SCALED",
24    "PREOUTPUT",
25    "OUTPUT",
26];
27
28/// Where `stage` runs in the pipeline ([`STAGES`] index); unknown names sort last.
29pub fn stage_rank(stage: &str) -> usize {
30    STAGES
31        .iter()
32        .position(|s| *s == stage)
33        .unwrap_or(STAGES.len())
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum ParamKind {
38    Int,
39    Uint,
40    Float,
41}
42
43#[derive(Debug, Clone, PartialEq)]
44pub struct ParamInfo {
45    pub name: String,
46    pub kind: ParamKind,
47    pub min: Option<f64>,
48    pub max: Option<f64>,
49    /// `None` for `ENUM` params, whose default is a name.
50    pub default: Option<f64>,
51}
52
53impl ParamInfo {
54    /// `v` if it fits the type and bounds.
55    pub fn check(&self, v: f64) -> Result<f64> {
56        let ok = v.is_finite()
57            && self.min.is_none_or(|m| v >= m)
58            && self.max.is_none_or(|m| v <= m)
59            && (self.kind != ParamKind::Uint || v >= 0.0);
60        if ok {
61            Ok(v)
62        } else {
63            Err(Error::Invalid {
64                what: "shader //!PARAM value (outside its bounds)",
65                value: format!("{} = {v}", self.name),
66            })
67        }
68    }
69}
70
71#[derive(Debug, Clone, PartialEq, Default)]
72pub struct HookInfo {
73    /// Every `//!HOOK` stage, in file order.
74    pub stages: Vec<String>,
75    pub params: Vec<ParamInfo>,
76}
77
78impl HookInfo {
79    pub fn param(&self, name: &str) -> Option<&ParamInfo> {
80        self.params.iter().find(|p| p.name == name)
81    }
82
83    /// The earliest stage this shader hooks.
84    pub fn first_stage(&self) -> usize {
85        self.stages
86            .iter()
87            .map(|s| stage_rank(s))
88            .min()
89            .unwrap_or(STAGES.len())
90    }
91}
92
93/// Reads the directives of a `.hook` shader.
94pub fn parse(text: &str) -> Result<HookInfo> {
95    let invalid = |line: &str| Error::Invalid {
96        what: "mpv .hook directive",
97        value: line.to_owned(),
98    };
99    let mut info = HookInfo::default();
100    // The PARAM block being read and whether its default line was seen.
101    let mut param: Option<(ParamInfo, bool)> = None;
102    let finish = |info: &mut HookInfo, p: Option<(ParamInfo, bool)>| {
103        if let Some((p, _)) = p {
104            info.params.push(p);
105        }
106    };
107    for line in text.lines() {
108        let Some(d) = line.strip_prefix("//!") else {
109            match &mut param {
110                Some((p, seen)) if !*seen && !line.trim().is_empty() => {
111                    *seen = true;
112                    p.default = line.trim().parse().ok();
113                }
114                _ => {}
115            }
116            continue;
117        };
118        let mut words = d.split_whitespace();
119        let key = words.next().unwrap_or_default();
120        match key {
121            "PARAM" => {
122                finish(&mut info, param.take());
123                let name = words.next().ok_or_else(|| invalid(line))?;
124                param = Some((
125                    ParamInfo {
126                        name: name.to_owned(),
127                        kind: ParamKind::Float,
128                        min: None,
129                        max: None,
130                        default: None,
131                    },
132                    false,
133                ));
134            }
135            "HOOK" | "TEXTURE" | "BUFFER" => {
136                finish(&mut info, param.take());
137                if key == "HOOK" {
138                    info.stages
139                        .push(words.next().ok_or_else(|| invalid(line))?.to_owned());
140                }
141            }
142            "TYPE" | "MINIMUM" | "MAXIMUM" => {
143                let Some((p, _)) = &mut param else { continue };
144                let rest: Vec<&str> = words.collect();
145                match key {
146                    "TYPE" => {
147                        // `[ENUM|DEFINE] [DYNAMIC|CONSTANT] type`
148                        p.kind = match rest.last().copied() {
149                            Some("int" | "ENUM" | "DEFINE") => ParamKind::Int,
150                            Some("uint") => ParamKind::Uint,
151                            Some("float") => ParamKind::Float,
152                            _ => return Err(invalid(line)),
153                        };
154                        if rest.first() == Some(&"ENUM") {
155                            p.kind = ParamKind::Int;
156                        }
157                    }
158                    "MINIMUM" => p.min = rest.first().and_then(|v| v.parse().ok()),
159                    _ => p.max = rest.first().and_then(|v| v.parse().ok()),
160                }
161            }
162            _ => {}
163        }
164    }
165    finish(&mut info, param);
166    Ok(info)
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    #[test]
174    fn adaptive_sharpen_directives() {
175        let info = parse(crate::ADAPTIVE_SHARPEN).unwrap();
176        assert_eq!(info.stages, ["PREOUTPUT"]);
177        let ch = info.param("curve_height").unwrap();
178        assert_eq!(
179            (ch.kind, ch.default, ch.min),
180            (ParamKind::Float, Some(1.0), Some(0.0))
181        );
182        let oc = info.param("overshoot_ctrl").unwrap();
183        assert_eq!(
184            (oc.kind, oc.default, oc.max),
185            (ParamKind::Int, Some(0.0), Some(1.0))
186        );
187        assert!(ch.check(0.6).is_ok() && ch.check(-1.0).is_err());
188        assert!(stage_rank("MAIN") < stage_rank("SCALED"));
189        assert!(stage_rank("CHROMA") < stage_rank("MAIN"));
190        assert!(stage_rank("PREOUTPUT") < stage_rank("OUTPUT"));
191    }
192
193    #[test]
194    fn multi_pass_and_textures() {
195        let text = "//!HOOK CHROMA\n//!BIND HOOKED\nvec4 hook() { return HOOKED_tex(HOOKED_pos); }\n\
196                    //!PARAM k\n//!TYPE DYNAMIC uint\n3\n//!TEXTURE T\n//!SIZE 1 1\n00\n\
197                    //!HOOK MAIN\n//!BIND HOOKED\n";
198        let info = parse(text).unwrap();
199        assert_eq!(info.stages, ["CHROMA", "MAIN"]);
200        assert_eq!(info.first_stage(), stage_rank("CHROMA"));
201        let k = info.param("k").unwrap();
202        assert_eq!((k.kind, k.default), (ParamKind::Uint, Some(3.0)));
203        assert!(parse("//!HOOK\n").is_err());
204    }
205}