diff --git a/crates/effects/src/fidelity.rs b/crates/effects/src/fidelity.rs new file mode 100644 index 00000000..31243bd0 --- /dev/null +++ b/crates/effects/src/fidelity.rs @@ -0,0 +1,47 @@ +//! Deterministic rendered-frame comparison helpers for the fidelity corpus. + +use effectcraft_raster::Image; + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct FrameScore { + pub compared_pixels: usize, + pub mean_absolute_error: f64, + pub max_error: f32, +} + +/// Compare two premultiplied rendered frames. A size mismatch is reported as an error instead of +/// silently producing a misleading score. +pub fn compare_frames(actual: &Image, reference: &Image) -> Result { + if actual.width != reference.width || actual.height != reference.height { + return Err(format!("frame size mismatch: {}x{} vs {}x{}", actual.width, actual.height, reference.width, reference.height)); + } + let mut sum = 0.0f64; + let mut max = 0.0f32; + for (a, b) in actual.data.iter().zip(&reference.data) { + for (&x, &y) in a.iter().zip(b) { + let error = (x - y).abs(); + sum += f64::from(error); + max = max.max(error); + } + } + let compared_pixels = actual.data.len(); + let samples = compared_pixels.saturating_mul(4).max(1); + Ok(FrameScore { compared_pixels, mean_absolute_error: sum / samples as f64, max_error: max }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn reports_pixel_error_and_rejects_size_mismatch() { + let mut actual = Image::filled(2, 1, [0.0, 0.0, 0.0, 1.0]); + let reference = Image::filled(2, 1, [0.0, 0.0, 0.0, 1.0]); + actual.data[1][0] = 0.5; + let score = compare_frames(&actual, &reference).expect("same size"); + assert_eq!(score.compared_pixels, 2); + assert!((score.mean_absolute_error - 0.0625).abs() < f64::EPSILON); + assert_eq!(score.max_error, 0.5); + assert!(compare_frames(&actual, &Image::new(1, 1)).is_err()); + } +} diff --git a/crates/effects/src/lib.rs b/crates/effects/src/lib.rs index 9c0fe95b..ea9dc011 100644 --- a/crates/effects/src/lib.rs +++ b/crates/effects/src/lib.rs @@ -21,6 +21,7 @@ mod color2; mod color3; mod color_fx; pub mod controls; +pub mod fidelity; mod distort; mod distort2; mod distort3; diff --git a/docs/fidelity/README.md b/docs/fidelity/README.md index 682ea24e..ae88186b 100644 --- a/docs/fidelity/README.md +++ b/docs/fidelity/README.md @@ -52,6 +52,10 @@ the 168 live AE samples. ## Still unmeasured +Rendered frame comparisons now have a deterministic `effectcraft_effects::fidelity::compare_frames` +helper. Corpus runners can use it with locally generated host/reference frames and fail on either a +mean or maximum channel error without committing proprietary renders. + Mixed interpolation sides, non-unit durations, automatic/continuous Bezier, spatial speeds and paths, time remapping, expressions, actual rendered motion and other effects need separate corpus cases. The initial probe makes no claim about those features.