1use ffcine_core::{Error, Result};
5
6pub 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
28pub 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 pub default: Option<f64>,
51}
52
53impl ParamInfo {
54 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 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 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
93pub 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 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 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}