use ff_render::{
AlphaMatteNode, BlendMode, BlendModeNode, ChromaKeyNode, ColorGradeNode, CrossfadeNode,
LumaMaskNode, OverlayNode, RenderGraph, ScaleAlgorithm, ScaleNode, ShapeMaskNode,
TransformNode, YuvFormat, YuvUploadNode,
};
fn solid_rgba(r: u8, g: u8, b: u8, a: u8, w: u32, h: u32) -> Vec<u8> {
let n = (w * h * 4) as usize;
let mut v = Vec::with_capacity(n);
for _ in 0..(w * h) as usize {
v.push(r);
v.push(g);
v.push(b);
v.push(a);
}
v
}
#[test]
fn color_grade_node_brightness_boost_should_increase_rgb_channels() {
let rgba = solid_rgba(100, 100, 100, 255, 4, 4);
let graph = RenderGraph::new_cpu().push_cpu(ColorGradeNode::new(0.3, 1.0, 1.0, 0.0, 0.0));
let out = graph.process_cpu(&rgba, 4, 4);
assert!(
out[0] > 100,
"brightness +0.3 must increase R; got {}",
out[0]
);
assert!(
out[1] > 100,
"brightness +0.3 must increase G; got {}",
out[1]
);
assert!(
out[2] > 100,
"brightness +0.3 must increase B; got {}",
out[2]
);
assert_eq!(out[3], 255, "alpha must be unchanged");
}
#[test]
fn color_grade_node_saturation_zero_should_produce_equal_rgb_channels() {
let rgba = solid_rgba(200, 100, 50, 255, 2, 2);
let graph = RenderGraph::new_cpu().push_cpu(ColorGradeNode::new(0.0, 0.0, 1.0, 0.0, 0.0));
let out = graph.process_cpu(&rgba, 2, 2);
assert_eq!(
out[0], out[1],
"saturation=0 must equalise R and G; got R={} G={}",
out[0], out[1]
);
assert_eq!(
out[1], out[2],
"saturation=0 must equalise G and B; got G={} B={}",
out[1], out[2]
);
}
#[test]
fn scale_node_cpu_path_is_passthrough_and_returns_input_unchanged() {
let rgba = solid_rgba(128, 64, 32, 255, 4, 4);
let graph = RenderGraph::new_cpu().push_cpu(ScaleNode::new(2, 2, ScaleAlgorithm::Bilinear));
let out = graph.process_cpu(&rgba, 4, 4);
assert_eq!(out, rgba, "ScaleNode CPU path must be a passthrough");
}
#[test]
fn overlay_node_fully_opaque_overlay_should_replace_base_color() {
let base = solid_rgba(0, 0, 0, 255, 4, 4);
let overlay = solid_rgba(200, 100, 50, 255, 4, 4);
let node = OverlayNode::new(overlay, 4, 4);
let graph = RenderGraph::new_cpu().push_cpu(node);
let out = graph.process_cpu(&base, 4, 4);
assert!(
out[0] >= 195,
"opaque overlay must dominate base R; got {}",
out[0]
);
}
#[test]
fn crossfade_node_half_factor_should_average_from_and_to_colors() {
let from = solid_rgba(0, 0, 0, 255, 2, 2);
let to = solid_rgba(200, 200, 200, 255, 2, 2);
let node = CrossfadeNode::new(0.5, to, 2, 2);
let graph = RenderGraph::new_cpu().push_cpu(node);
let out = graph.process_cpu(&from, 2, 2);
let r = out[0] as i32;
assert!(
(r - 100).abs() <= 5,
"factor=0.5 must blend R to ≈100; got {r}"
);
}
#[test]
fn blend_mode_multiply_node_should_darken_base() {
let base = solid_rgba(128, 128, 128, 255, 2, 2);
let overlay = solid_rgba(128, 128, 128, 255, 2, 2);
let node = BlendModeNode::new(BlendMode::Multiply, 1.0, overlay, 2, 2);
let graph = RenderGraph::new_cpu().push_cpu(node);
let out = graph.process_cpu(&base, 2, 2);
assert!(
out[0] < 128,
"Multiply blend must darken base R; got {}",
out[0]
);
}
#[test]
fn blend_mode_screen_node_should_lighten_base() {
let base = solid_rgba(100, 100, 100, 255, 2, 2);
let overlay = solid_rgba(100, 100, 100, 255, 2, 2);
let node = BlendModeNode::new(BlendMode::Screen, 1.0, overlay, 2, 2);
let graph = RenderGraph::new_cpu().push_cpu(node);
let out = graph.process_cpu(&base, 2, 2);
assert!(
out[0] > 100,
"Screen blend must lighten base R; got {}",
out[0]
);
}
#[test]
fn blend_mode_normal_at_zero_opacity_should_leave_base_unchanged() {
let base = solid_rgba(200, 100, 50, 255, 2, 2);
let overlay = solid_rgba(0, 0, 0, 255, 2, 2);
let node = BlendModeNode::new(BlendMode::Normal, 0.0, overlay, 2, 2);
let graph = RenderGraph::new_cpu().push_cpu(node);
let out = graph.process_cpu(&base, 2, 2);
assert_eq!(out[0], 200, "opacity=0.0 must leave R unchanged");
assert_eq!(out[1], 100, "opacity=0.0 must leave G unchanged");
assert_eq!(out[2], 50, "opacity=0.0 must leave B unchanged");
}
#[test]
fn transform_node_identity_cpu_should_return_input_unchanged() {
let rgba = solid_rgba(77, 88, 99, 255, 4, 4);
let node = TransformNode::new([0.0, 0.0], 0.0, [1.0, 1.0]);
let graph = RenderGraph::new_cpu().push_cpu(node);
let out = graph.process_cpu(&rgba, 4, 4);
assert_eq!(out, rgba, "identity transform must return input unchanged");
}
#[test]
fn chroma_key_node_pure_green_pixels_should_become_transparent() {
let rgba = solid_rgba(0, 255, 0, 255, 2, 2);
let node = ChromaKeyNode::new([0.0, 1.0, 0.0], 0.3, 0.0);
let graph = RenderGraph::new_cpu().push_cpu(node);
let out = graph.process_cpu(&rgba, 2, 2);
assert_eq!(
out[3], 0,
"pure key color must produce alpha=0 (transparent)"
);
}
#[test]
fn chroma_key_node_non_key_pixel_should_remain_opaque() {
let rgba = solid_rgba(255, 0, 0, 255, 2, 2); let node = ChromaKeyNode::new([0.0, 1.0, 0.0], 0.3, 0.0);
let graph = RenderGraph::new_cpu().push_cpu(node);
let out = graph.process_cpu(&rgba, 2, 2);
assert_eq!(
out[3], 255,
"red pixels must not be keyed out by a green chroma key"
);
}
#[test]
fn shape_mask_node_opaque_mask_should_preserve_base_alpha() {
let rgba = solid_rgba(128, 64, 32, 200, 2, 2);
let mask = solid_rgba(255, 255, 255, 255, 2, 2); let node = ShapeMaskNode::new(mask, 2, 2);
let graph = RenderGraph::new_cpu().push_cpu(node);
let out = graph.process_cpu(&rgba, 2, 2);
assert_eq!(out[3], 200, "white mask must preserve original alpha");
}
#[test]
fn shape_mask_node_transparent_mask_should_zero_alpha() {
let rgba = solid_rgba(128, 64, 32, 255, 2, 2);
let mask = solid_rgba(0, 0, 0, 0, 2, 2); let node = ShapeMaskNode::new(mask, 2, 2);
let graph = RenderGraph::new_cpu().push_cpu(node);
let out = graph.process_cpu(&rgba, 2, 2);
assert_eq!(out[3], 0, "fully transparent mask must produce alpha=0");
}
#[test]
fn luma_mask_node_white_mask_should_preserve_alpha() {
let rgba = solid_rgba(128, 64, 32, 200, 2, 2);
let mask = solid_rgba(255, 255, 255, 255, 2, 2); let node = LumaMaskNode::new(mask, 2, 2);
let graph = RenderGraph::new_cpu().push_cpu(node);
let out = graph.process_cpu(&rgba, 2, 2);
assert_eq!(out[3], 200, "white luma mask must preserve original alpha");
}
#[test]
fn luma_mask_node_black_mask_should_zero_alpha() {
let rgba = solid_rgba(128, 64, 32, 255, 2, 2);
let mask = solid_rgba(0, 0, 0, 255, 2, 2); let node = LumaMaskNode::new(mask, 2, 2);
let graph = RenderGraph::new_cpu().push_cpu(node);
let out = graph.process_cpu(&rgba, 2, 2);
assert_eq!(out[3], 0, "black luma mask must produce alpha=0");
}
#[test]
fn alpha_matte_node_transparent_fg_should_reveal_background() {
let fg = solid_rgba(255, 0, 0, 0, 2, 2); let bg = solid_rgba(0, 0, 255, 255, 2, 2); let node = AlphaMatteNode::new(bg, 2, 2);
let graph = RenderGraph::new_cpu().push_cpu(node);
let out = graph.process_cpu(&fg, 2, 2);
assert!(
out[2] > 200,
"transparent fg must show blue background; got B={}",
out[2]
);
}
#[test]
fn alpha_matte_node_opaque_fg_should_show_foreground() {
let fg = solid_rgba(255, 0, 0, 255, 2, 2); let bg = solid_rgba(0, 0, 255, 255, 2, 2); let node = AlphaMatteNode::new(bg, 2, 2);
let graph = RenderGraph::new_cpu().push_cpu(node);
let out = graph.process_cpu(&fg, 2, 2);
assert!(
out[0] > 200,
"opaque red fg must dominate; got R={}",
out[0]
);
assert!(
out[2] < 50,
"opaque red fg must hide blue background; got B={}",
out[2]
);
}
#[test]
fn yuv_upload_node_cpu_black_frame_should_produce_near_black_rgba() {
let mut node = YuvUploadNode::new(YuvFormat::Yuv420p, 4, 4);
node.set_planes(vec![16u8; 4 * 4], vec![128u8; 2 * 2], vec![128u8; 2 * 2]);
let dummy = vec![0u8; 4 * 4 * 4];
let graph = RenderGraph::new_cpu().push_cpu(node);
let out = graph.process_cpu(&dummy, 4, 4);
assert!(
out[0] < 20,
"Y=16 must produce near-black R; got {}",
out[0]
);
assert!(
out[1] < 20,
"Y=16 must produce near-black G; got {}",
out[1]
);
assert!(
out[2] < 20,
"Y=16 must produce near-black B; got {}",
out[2]
);
assert_eq!(out[3], 255, "alpha must be 255");
}
#[test]
fn yuv_upload_node_cpu_white_frame_should_produce_near_white_rgba() {
let mut node = YuvUploadNode::new(YuvFormat::Yuv420p, 4, 4);
node.set_planes(vec![235u8; 4 * 4], vec![128u8; 2 * 2], vec![128u8; 2 * 2]);
let dummy = vec![0u8; 4 * 4 * 4];
let graph = RenderGraph::new_cpu().push_cpu(node);
let out = graph.process_cpu(&dummy, 4, 4);
assert!(
out[0] > 230,
"Y=235 must produce near-white R; got {}",
out[0]
);
assert!(
out[1] > 230,
"Y=235 must produce near-white G; got {}",
out[1]
);
assert!(
out[2] > 230,
"Y=235 must produce near-white B; got {}",
out[2]
);
}
#[test]
fn multi_node_pipeline_brightness_then_multiply_should_accumulate() {
let base = solid_rgba(128, 128, 128, 255, 2, 2);
let overlay = solid_rgba(128, 128, 128, 255, 2, 2);
let graph = RenderGraph::new_cpu()
.push_cpu(ColorGradeNode::new(0.2, 1.0, 1.0, 0.0, 0.0))
.push_cpu(BlendModeNode::new(BlendMode::Multiply, 1.0, overlay, 2, 2));
let out = graph.process_cpu(&base, 2, 2);
assert!(
out[0] < 128,
"brightness+multiply pipeline must reduce R below 128; got {}",
out[0]
);
}
#[test]
fn render_graph_empty_pipeline_should_return_input_unchanged() {
let rgba = solid_rgba(99, 111, 123, 200, 2, 2);
let graph = RenderGraph::new_cpu();
let out = graph.process_cpu(&rgba, 2, 2);
assert_eq!(out, rgba, "empty graph must return input unchanged");
}