rustial-engine 0.0.1

Framework-agnostic 2.5D map engine for rustial
Documentation
// ---------------------------------------------------------------------------
//! Image comparison utilities for cross-renderer parity tests.
//!
//! Provides lightweight pixel-level comparison metrics without pulling in
//! external image-comparison crates.  Used by the headless comparison test
//! harness to assert that WGPU and Bevy renderers produce structurally
//! equivalent output for the same `FrameOutput`.
// ---------------------------------------------------------------------------

/// Per-pixel RMSE (Root Mean Square Error) between two RGBA8 images.
///
/// Both buffers must have the same length (`width * height * 4`).
/// Returns a value in the range `[0.0, 255.0]`:
///
/// - `0.0` -- images are byte-identical.
/// - `< 5.0` -- visually indistinguishable (acceptance threshold).
/// - `> 50.0` -- significant structural difference.
///
/// # Panics
///
/// Panics if `a.len() != b.len()` or if the length is zero.
pub fn compute_rmse(a: &[u8], b: &[u8]) -> f64 {
    assert_eq!(a.len(), b.len(), "image buffers must have the same length");
    assert!(!a.is_empty(), "image buffers must not be empty");

    let sum_sq: f64 = a
        .iter()
        .zip(b.iter())
        .map(|(&va, &vb)| {
            let diff = va as f64 - vb as f64;
            diff * diff
        })
        .sum();

    (sum_sq / a.len() as f64).sqrt()
}

/// Count the number of pixels where any RGBA channel differs by more than
/// `threshold` (0-255).
///
/// Both buffers must have the same length (`width * height * 4`).
///
/// # Panics
///
/// Panics if `a.len() != b.len()` or if the length is not a multiple of 4.
pub fn count_differing_pixels(a: &[u8], b: &[u8], threshold: u8) -> usize {
    assert_eq!(a.len(), b.len(), "image buffers must have the same length");
    assert_eq!(
        a.len() % 4,
        0,
        "image buffer length must be a multiple of 4"
    );

    a.chunks_exact(4)
        .zip(b.chunks_exact(4))
        .filter(|(pa, pb)| {
            pa.iter()
                .zip(pb.iter())
                .any(|(&ca, &cb)| (ca as i16 - cb as i16).unsigned_abs() > threshold as u16)
        })
        .count()
}

/// Fraction of pixels that differ (0.0 to 1.0).
///
/// Convenience wrapper around [`count_differing_pixels`].
pub fn differing_pixel_fraction(a: &[u8], b: &[u8], threshold: u8) -> f64 {
    let total_pixels = a.len() / 4;
    if total_pixels == 0 {
        return 0.0;
    }
    count_differing_pixels(a, b, threshold) as f64 / total_pixels as f64
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn identical_images_have_zero_rmse() {
        let img = vec![128u8; 64 * 4];
        assert!((compute_rmse(&img, &img) - 0.0).abs() < f64::EPSILON);
    }

    #[test]
    fn different_images_have_positive_rmse() {
        let a = vec![0u8; 64 * 4];
        let b = vec![255u8; 64 * 4];
        let rmse = compute_rmse(&a, &b);
        assert!(rmse > 200.0, "rmse was {rmse}");
    }

    #[test]
    fn count_differing_pixels_exact_threshold() {
        let a = vec![100u8; 4 * 4];
        let mut b = a.clone();
        // Change one pixel by exactly threshold+1.
        b[0] = 110;
        assert_eq!(count_differing_pixels(&a, &b, 9), 1);
        assert_eq!(count_differing_pixels(&a, &b, 10), 0);
    }

    #[test]
    fn differing_fraction_returns_correct_ratio() {
        let a = vec![0u8; 8 * 4]; // 8 pixels
        let mut b = a.clone();
        b[0] = 255; // pixel 0 differs
        b[4] = 255; // pixel 1 differs
        let frac = differing_pixel_fraction(&a, &b, 0);
        assert!((frac - 0.25).abs() < 1e-9, "frac was {frac}");
    }
}