#[derive(Debug, Clone, Copy)]
pub struct ChannelTolerance {
pub edge: i32,
pub max_differing_fraction: f64,
}
pub const STRUCTURAL_EDGE_TOLERANCE: i32 = 40;
pub const STRUCTURAL_DEFECT_FRACTION: f64 = 0.35;
impl Default for ChannelTolerance {
fn default() -> Self {
Self { edge: STRUCTURAL_EDGE_TOLERANCE, max_differing_fraction: STRUCTURAL_DEFECT_FRACTION }
}
}
#[derive(Debug, Clone, Copy)]
pub struct DiffReport {
pub differing_fraction: f64,
pub max_channel_diff: i32,
pub within_budget: bool,
}
fn max_channel_diff_at(a: &[u8], b: &[u8]) -> i32 {
a.iter().zip(b.iter()).map(|(x, y)| (*x as i32 - *y as i32).abs()).max().unwrap_or(0)
}
pub fn compare_tight(a: &[u8], b: &[u8], tol: ChannelTolerance) -> DiffReport {
debug_assert_eq!(a.len(), b.len(), "compare_tight: buffers must be the same length");
let total_pixels = a.len() / 4;
if total_pixels == 0 {
return DiffReport { differing_fraction: 0.0, max_channel_diff: 0, within_budget: true };
}
let mut differing = 0usize;
let mut max_diff = 0i32;
for i in 0..total_pixels {
let idx = i * 4;
let d = max_channel_diff_at(&a[idx..idx + 4], &b[idx..idx + 4]);
max_diff = max_diff.max(d);
if d > tol.edge {
differing += 1;
}
}
let differing_fraction = differing as f64 / total_pixels as f64;
DiffReport { differing_fraction, max_channel_diff: max_diff, within_budget: differing_fraction <= tol.max_differing_fraction }
}
#[derive(Debug, Clone, Copy)]
pub struct MultiLegDiff {
pub tiny_skia_vs_vello_cpu: DiffReport,
pub tiny_skia_vs_urx_cpu: DiffReport,
pub vello_cpu_vs_urx_cpu: DiffReport,
pub tiny_skia_vs_vello_gpu: Option<DiffReport>,
pub tiny_skia_vs_urx_gpu: Option<DiffReport>,
pub vello_cpu_vs_vello_gpu: Option<DiffReport>,
pub vello_cpu_vs_urx_gpu: Option<DiffReport>,
pub vello_gpu_vs_urx_cpu: Option<DiffReport>,
pub vello_gpu_vs_urx_gpu: Option<DiffReport>,
pub urx_cpu_vs_urx_gpu: Option<DiffReport>,
}
impl MultiLegDiff {
pub fn compute(render: &crate::render::MultiLegRender, tol: ChannelTolerance) -> Self {
let vello_gpu = render.vello_gpu.as_deref();
let urx_gpu = render.urx_gpu.as_ref().map(|r| r.pixels.as_slice());
Self {
tiny_skia_vs_vello_cpu: compare_tight(&render.tiny_skia, &render.vello_cpu, tol),
tiny_skia_vs_urx_cpu: compare_tight(&render.tiny_skia, &render.urx_cpu, tol),
vello_cpu_vs_urx_cpu: compare_tight(&render.vello_cpu, &render.urx_cpu, tol),
tiny_skia_vs_vello_gpu: vello_gpu.map(|g| compare_tight(&render.tiny_skia, g, tol)),
tiny_skia_vs_urx_gpu: urx_gpu.map(|g| compare_tight(&render.tiny_skia, g, tol)),
vello_cpu_vs_vello_gpu: vello_gpu.map(|g| compare_tight(&render.vello_cpu, g, tol)),
vello_cpu_vs_urx_gpu: urx_gpu.map(|g| compare_tight(&render.vello_cpu, g, tol)),
vello_gpu_vs_urx_cpu: vello_gpu.map(|g| compare_tight(g, &render.urx_cpu, tol)),
vello_gpu_vs_urx_gpu: match (vello_gpu, urx_gpu) {
(Some(a), Some(b)) => Some(compare_tight(a, b, tol)),
_ => None,
},
urx_cpu_vs_urx_gpu: urx_gpu.map(|g| compare_tight(&render.urx_cpu, g, tol)),
}
}
pub fn all_within_budget(&self) -> bool {
let opt_ok = |d: Option<DiffReport>| d.map_or(true, |r| r.within_budget);
self.tiny_skia_vs_vello_cpu.within_budget
&& self.tiny_skia_vs_urx_cpu.within_budget
&& self.vello_cpu_vs_urx_cpu.within_budget
&& opt_ok(self.tiny_skia_vs_vello_gpu)
&& opt_ok(self.tiny_skia_vs_urx_gpu)
&& opt_ok(self.vello_cpu_vs_vello_gpu)
&& opt_ok(self.vello_cpu_vs_urx_gpu)
&& opt_ok(self.vello_gpu_vs_urx_cpu)
&& opt_ok(self.vello_gpu_vs_urx_gpu)
&& opt_ok(self.urx_cpu_vs_urx_gpu)
}
pub fn report_lines(&self) -> Vec<String> {
let line = |label: &str, d: DiffReport| format!("{label}: differing_fraction={:.4} max_channel_diff={}", d.differing_fraction, d.max_channel_diff);
let mut lines = vec![
line("tiny-skia vs vello-cpu", self.tiny_skia_vs_vello_cpu),
line("tiny-skia vs urx-cpu ", self.tiny_skia_vs_urx_cpu),
line("vello-cpu vs urx-cpu ", self.vello_cpu_vs_urx_cpu),
];
let mut push_opt = |label: &str, d: Option<DiffReport>| {
if let Some(d) = d {
lines.push(line(label, d));
}
};
push_opt("tiny-skia vs vello-gpu", self.tiny_skia_vs_vello_gpu);
push_opt("tiny-skia vs urx-gpu ", self.tiny_skia_vs_urx_gpu);
push_opt("vello-cpu vs vello-gpu (WITHIN-FAMILY)", self.vello_cpu_vs_vello_gpu);
push_opt("vello-cpu vs urx-gpu ", self.vello_cpu_vs_urx_gpu);
push_opt("vello-gpu vs urx-cpu ", self.vello_gpu_vs_urx_cpu);
push_opt("vello-gpu vs urx-gpu ", self.vello_gpu_vs_urx_gpu);
push_opt("urx-cpu vs urx-gpu (WITHIN-FAMILY)", self.urx_cpu_vs_urx_gpu);
lines
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn compare_tight_identical_buffers_are_within_budget() {
let a = vec![10u8, 20, 30, 255, 40, 50, 60, 255];
let report = compare_tight(&a, &a, ChannelTolerance::default());
assert_eq!(report.max_channel_diff, 0);
assert_eq!(report.differing_fraction, 0.0);
assert!(report.within_budget);
}
#[test]
fn compare_tight_flags_a_large_difference_outside_the_edge_tolerance() {
let a = vec![0u8, 0, 0, 255];
let b = vec![255u8, 255, 255, 255];
let report = compare_tight(&a, &b, ChannelTolerance::default());
assert_eq!(report.max_channel_diff, 255);
assert_eq!(report.differing_fraction, 1.0);
assert!(!report.within_budget);
}
#[test]
fn compare_tight_tolerates_a_small_difference_within_the_edge_tolerance() {
let a = vec![100u8, 100, 100, 255];
let b = vec![110u8, 100, 100, 255]; let report = compare_tight(&a, &b, ChannelTolerance::default());
assert_eq!(report.max_channel_diff, 10);
assert_eq!(report.differing_fraction, 0.0);
assert!(report.within_budget);
}
#[test]
fn compare_tight_empty_buffers_are_within_budget() {
let report = compare_tight(&[], &[], ChannelTolerance::default());
assert_eq!(report.differing_fraction, 0.0);
assert!(report.within_budget);
}
}