use ffcine_core::{Error, Result};
pub const STAGES: &[&str] = &[
"RGB",
"LUMA",
"CHROMA",
"ALPHA",
"XYZ",
"CHROMA_SCALED",
"ALPHA_SCALED",
"NATIVE",
"MAINPRESUB",
"MAIN",
"LINEAR",
"SIGMOID",
"PREKERNEL",
"POSTKERNEL",
"SCALED",
"PREOUTPUT",
"OUTPUT",
];
pub fn stage_rank(stage: &str) -> usize {
STAGES
.iter()
.position(|s| *s == stage)
.unwrap_or(STAGES.len())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParamKind {
Int,
Uint,
Float,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ParamInfo {
pub name: String,
pub kind: ParamKind,
pub min: Option<f64>,
pub max: Option<f64>,
pub default: Option<f64>,
}
impl ParamInfo {
pub fn check(&self, v: f64) -> Result<f64> {
let ok = v.is_finite()
&& self.min.is_none_or(|m| v >= m)
&& self.max.is_none_or(|m| v <= m)
&& (self.kind != ParamKind::Uint || v >= 0.0);
if ok {
Ok(v)
} else {
Err(Error::Invalid {
what: "shader //!PARAM value (outside its bounds)",
value: format!("{} = {v}", self.name),
})
}
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct HookInfo {
pub stages: Vec<String>,
pub params: Vec<ParamInfo>,
}
impl HookInfo {
pub fn param(&self, name: &str) -> Option<&ParamInfo> {
self.params.iter().find(|p| p.name == name)
}
pub fn first_stage(&self) -> usize {
self.stages
.iter()
.map(|s| stage_rank(s))
.min()
.unwrap_or(STAGES.len())
}
}
pub fn parse(text: &str) -> Result<HookInfo> {
let invalid = |line: &str| Error::Invalid {
what: "mpv .hook directive",
value: line.to_owned(),
};
let mut info = HookInfo::default();
let mut param: Option<(ParamInfo, bool)> = None;
let finish = |info: &mut HookInfo, p: Option<(ParamInfo, bool)>| {
if let Some((p, _)) = p {
info.params.push(p);
}
};
for line in text.lines() {
let Some(d) = line.strip_prefix("//!") else {
match &mut param {
Some((p, seen)) if !*seen && !line.trim().is_empty() => {
*seen = true;
p.default = line.trim().parse().ok();
}
_ => {}
}
continue;
};
let mut words = d.split_whitespace();
let key = words.next().unwrap_or_default();
match key {
"PARAM" => {
finish(&mut info, param.take());
let name = words.next().ok_or_else(|| invalid(line))?;
param = Some((
ParamInfo {
name: name.to_owned(),
kind: ParamKind::Float,
min: None,
max: None,
default: None,
},
false,
));
}
"HOOK" | "TEXTURE" | "BUFFER" => {
finish(&mut info, param.take());
if key == "HOOK" {
info.stages
.push(words.next().ok_or_else(|| invalid(line))?.to_owned());
}
}
"TYPE" | "MINIMUM" | "MAXIMUM" => {
let Some((p, _)) = &mut param else { continue };
let rest: Vec<&str> = words.collect();
match key {
"TYPE" => {
p.kind = match rest.last().copied() {
Some("int" | "ENUM" | "DEFINE") => ParamKind::Int,
Some("uint") => ParamKind::Uint,
Some("float") => ParamKind::Float,
_ => return Err(invalid(line)),
};
if rest.first() == Some(&"ENUM") {
p.kind = ParamKind::Int;
}
}
"MINIMUM" => p.min = rest.first().and_then(|v| v.parse().ok()),
_ => p.max = rest.first().and_then(|v| v.parse().ok()),
}
}
_ => {}
}
}
finish(&mut info, param);
Ok(info)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn adaptive_sharpen_directives() {
let info = parse(crate::ADAPTIVE_SHARPEN).unwrap();
assert_eq!(info.stages, ["PREOUTPUT"]);
let ch = info.param("curve_height").unwrap();
assert_eq!(
(ch.kind, ch.default, ch.min),
(ParamKind::Float, Some(1.0), Some(0.0))
);
let oc = info.param("overshoot_ctrl").unwrap();
assert_eq!(
(oc.kind, oc.default, oc.max),
(ParamKind::Int, Some(0.0), Some(1.0))
);
assert!(ch.check(0.6).is_ok() && ch.check(-1.0).is_err());
assert!(stage_rank("MAIN") < stage_rank("SCALED"));
assert!(stage_rank("CHROMA") < stage_rank("MAIN"));
assert!(stage_rank("PREOUTPUT") < stage_rank("OUTPUT"));
}
#[test]
fn multi_pass_and_textures() {
let text = "//!HOOK CHROMA\n//!BIND HOOKED\nvec4 hook() { return HOOKED_tex(HOOKED_pos); }\n\
//!PARAM k\n//!TYPE DYNAMIC uint\n3\n//!TEXTURE T\n//!SIZE 1 1\n00\n\
//!HOOK MAIN\n//!BIND HOOKED\n";
let info = parse(text).unwrap();
assert_eq!(info.stages, ["CHROMA", "MAIN"]);
assert_eq!(info.first_stage(), stage_rank("CHROMA"));
let k = info.param("k").unwrap();
assert_eq!((k.kind, k.default), (ParamKind::Uint, Some(3.0)));
assert!(parse("//!HOOK\n").is_err());
}
}