pub mod parse;
pub mod shader;
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 fn vendored(name: &str) -> Option<&'static str> {
match name {
"adaptive-sharpen" => Some(ADAPTIVE_SHARPEN),
_ => None,
}
}
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));
}
#[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
]
);
}
}