pub mod grade;
pub mod parse;
pub mod shader;
use std::path::{Path, PathBuf};
use ffcine_core::{Error, Result};
use ffcine_render::{Grade, Hook};
pub use parse::{HookInfo, ParamInfo, ParamKind, parse};
pub use shader::ShaderEffect;
pub const ADAPTIVE_SHARPEN: &str = include_str!("../shaders/adaptive-sharpen.glsl");
pub const ARTCNN_C4F16: &str = include_str!("../shaders/artcnn-c4f16.glsl");
pub const ARTCNN_C4F16_DS: &str = include_str!("../shaders/artcnn-c4f16-ds.glsl");
pub fn vendored(name: &str) -> Option<&'static str> {
match name {
"adaptive-sharpen" => Some(ADAPTIVE_SHARPEN),
"artcnn-c4f16" => Some(ARTCNN_C4F16),
"artcnn-c4f16-ds" => Some(ARTCNN_C4F16_DS),
_ => None,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct External {
pub name: &'static str,
pub file: &'static str,
pub url: &'static str,
pub licence: &'static str,
}
pub const EXTERNAL: &[External] = &[
External {
name: "krigbilateral",
file: "KrigBilateral.glsl",
url: "https://gist.github.com/igv/a015fc885d5c22e6891820ad89555637",
licence: "LGPL-3.0-or-later",
},
External {
name: "fsrcnnx",
file: "FSRCNNX_x2_8-0-4-1.glsl",
url: "https://github.com/igv/FSRCNN-TensorFlow/releases",
licence: "GPL-3.0",
},
External {
name: "fsrcnnx-16",
file: "FSRCNNX_x2_16-0-4-1.glsl",
url: "https://github.com/igv/FSRCNN-TensorFlow/releases",
licence: "GPL-3.0",
},
];
pub fn shader_path() -> Vec<PathBuf> {
let mut dirs: Vec<PathBuf> = std::env::var_os("FFCINE_SHADER_PATH")
.map(|p| std::env::split_paths(&p).collect())
.unwrap_or_default();
let data = std::env::var_os("XDG_DATA_HOME")
.map(PathBuf::from)
.filter(|p| p.is_absolute())
.or_else(|| std::env::var_os("HOME").map(|h| Path::new(&h).join(".local/share")));
dirs.extend(data.map(|d| d.join("ffcine/shaders")));
dirs
}
pub fn external(name: &str, dirs: &[PathBuf]) -> Option<Result<(PathBuf, String)>> {
let e = EXTERNAL.iter().find(|e| e.name == name)?;
let Some(path) = dirs.iter().map(|d| d.join(e.file)).find(|p| p.is_file()) else {
let dirs: Vec<String> = dirs.iter().map(|d| d.display().to_string()).collect();
return Some(Err(Error::Import {
path: PathBuf::from(e.file),
msg: format!(
"{} is {} and not bundled with ffcine: download it from {} into one of [{}] \
(or a directory on FFCINE_SHADER_PATH)",
e.name,
e.licence,
e.url,
dirs.join(", ")
),
}));
};
Some(
std::fs::read_to_string(&path)
.map(|t| (path.clone(), t))
.map_err(|source| Error::Io { path, source }),
)
}
pub fn named(name: &str) -> Option<Result<Hook>> {
let source = match vendored(name) {
Some(t) => Ok(t.to_owned()),
None => external(name, &shader_path())?.map(|(_, t)| retarget_output(&t)),
};
Some(source.map(|source| Hook {
name: name.to_owned(),
source,
..Hook::default()
}))
}
pub const SHARPEN_HOOK: &str = "ffcine:sharpen";
pub fn retarget_output(text: &str) -> String {
text.lines()
.map(|l| {
if l.trim_end() == "//!HOOK OUTPUT" {
"//!HOOK PREOUTPUT"
} else {
l
}
})
.collect::<Vec<_>>()
.join("\n")
+ "\n"
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Sharpen {
#[default]
Adaptive,
Unsharp,
}
impl Sharpen {
pub fn parse(s: &str) -> Option<Self> {
match s {
"adaptive" | "adaptive-sharpen" => Some(Self::Adaptive),
"unsharp" => Some(Self::Unsharp),
_ => None,
}
}
}
pub fn sharpen(grade: &mut Grade, amount: f32, kind: Sharpen) {
grade.hooks.retain(|h| h.name != SHARPEN_HOOK);
grade.sharpen = 0.0;
if amount <= 0.0 {
return;
}
match kind {
Sharpen::Unsharp => grade.sharpen = amount,
Sharpen::Adaptive => {
grade.hooks.push(Hook {
name: SHARPEN_HOOK.to_owned(),
source: ADAPTIVE_SHARPEN.to_owned(),
params: vec![("curve_height".to_owned(), f64::from(amount))],
tracks: Vec::new(),
});
order(&mut grade.hooks);
}
}
}
fn role(h: &Hook) -> u8 {
match h.name.as_str() {
ffcine_reframe::pass::GEOMETRY_HOOK => 0,
SHARPEN_HOOK => 1,
ffcine_reframe::pass::SAFE_AREA_HOOK => 3,
_ => 2,
}
}
pub fn order(hooks: &mut [Hook]) {
hooks.sort_by_cached_key(|h| {
let stage = parse(&h.source).map_or(parse::STAGES.len(), |i| i.first_stage());
(stage, role(h))
});
}
#[cfg(test)]
mod tests {
use super::*;
use ffcine_reframe::{Profile, pass};
fn user(name: &str, stage: &str) -> Hook {
Hook {
name: name.to_owned(),
source: format!(
"//!HOOK {stage}\n//!BIND HOOKED\nvec4 hook() {{ return HOOKED_texOff(0); }}\n"
),
..Hook::default()
}
}
#[test]
fn output_hooks_move_to_preoutput() {
let t = retarget_output("//!HOOK OUTPUT\n//!BIND HOOKED\n//!HOOK MAIN\n");
assert_eq!(t, "//!HOOK PREOUTPUT\n//!BIND HOOKED\n//!HOOK MAIN\n");
assert_eq!(parse(ADAPTIVE_SHARPEN).unwrap().stages, ["PREOUTPUT"]);
}
#[test]
fn licence_header_kept() {
assert!(ADAPTIVE_SHARPEN.starts_with("// Copyright (c) 2015-2021, bacondither\n"));
assert!(ADAPTIVE_SHARPEN.contains("THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS''"));
assert_eq!(vendored("adaptive-sharpen"), Some(ADAPTIVE_SHARPEN));
for s in [ARTCNN_C4F16, ARTCNN_C4F16_DS] {
assert!(s.starts_with("// MIT License\n"));
assert!(s.contains("The above copyright notice and this permission notice"));
assert!(parse(s).unwrap().stages.iter().all(|s| s == "LUMA"));
}
assert_eq!(vendored("artcnn-c4f16-ds"), Some(ARTCNN_C4F16_DS));
}
#[test]
fn external_from_shader_path() {
let dir =
std::env::temp_dir().join(format!("ffcine-hooks-external-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let dirs = [dir.join("missing"), dir.clone()];
assert!(external("adaptive-sharpen", &dirs).is_none());
let err = external("krigbilateral", &dirs).unwrap().unwrap_err();
assert!(err.to_string().contains("LGPL-3.0-or-later"), "{err}");
assert!(err.to_string().contains("gist.github.com/igv"), "{err}");
let src = "//!HOOK CHROMA\n//!BIND HOOKED\nvec4 hook() { return HOOKED_texOff(0); }\n";
std::fs::write(dir.join("KrigBilateral.glsl"), src).unwrap();
let (path, text) = external("krigbilateral", &dirs).unwrap().unwrap();
assert_eq!((path, text.as_str()), (dir.join("KrigBilateral.glsl"), src));
let _ = std::fs::remove_dir_all(&dir);
assert_eq!(named("artcnn-c4f16").unwrap().unwrap().source, ARTCNN_C4F16);
assert!(named("nope").is_none());
}
#[test]
fn sharpen_replaces_unsharp() {
let mut g = Grade::default();
sharpen(&mut g, 0.6, Sharpen::Adaptive);
assert!(g.sharpen == 0.0 && g.hooks.len() == 1);
assert_eq!(
g.hooks[0].params,
[("curve_height".to_owned(), f64::from(0.6_f32))]
);
sharpen(&mut g, 0.45, Sharpen::Unsharp);
assert!(g.hooks.is_empty() && (g.sharpen - 0.45).abs() < f32::EPSILON);
sharpen(&mut g, 0.0, Sharpen::Adaptive);
assert!(g.hooks.is_empty() && g.sharpen == 0.0);
}
#[test]
fn hooks_in_pipeline_order() {
let mut reel = Profile::preset("reel").expect("preset");
reel.safe_area = Some(reel.default_safe_area());
let r = ffcine_reframe::Reframe::default();
let shot = ffcine_reframe::Shot {
width: 3840,
height: 2160,
frames: 10,
rate: 24.0,
start: 0.0,
};
let mut g = Grade::default();
g.hooks.push(user("grain", "PREOUTPUT"));
g.hooks.push(user("krig", "CHROMA"));
pass::apply(
&mut g,
&r.frames(&shot, &reel),
&[ffcine_core::geom::IDENTITY],
(3840, 2160),
&reel,
);
sharpen(&mut g, 1.0, Sharpen::Adaptive);
let names: Vec<&str> = g.hooks.iter().map(|h| h.name.as_str()).collect();
assert_eq!(
names,
[
"krig",
pass::GEOMETRY_HOOK,
SHARPEN_HOOK,
"grain",
pass::SAFE_AREA_HOOK
]
);
}
}