uzor-proof-harness 1.5.1

Dev-only multi-backend (tiny-skia / vello-cpu / vello-gpu / urx-cpu / urx-gpu) headless proof-render harness: drives one RenderContext-driven draw closure through every available rasterizer backend (three CPU legs always, two GPU legs gracefully skipped when no adapter is available), reports pairwise pixel-diff fractions, and writes a labelled side-by-side composite PNG. Promoted out of uzor-examples/src/parity_harness.rs (URX Wave 6) so any publish = false engine crate (uzor-figures first) can reach it as a dev-dependency without adding a runtime dependency to its own [dependencies].
//! Pairwise pixel-diff comparator + the divergence-policy tolerance.
//!
//! Same algorithm as `uzor-examples/src/parity_harness.rs`'s own
//! `compare_tight` (itself reusing `uzor-urx-wgpu/tests/parity.rs`'s
//! proven per-primitive tolerance shape) — generalized to any two
//! equal-length premultiplied-RGBA8 buffers.
//!
//! **This is NOT a byte-tight parity gate.** tiny-skia, vello-cpu, and
//! urx-cpu are three independent rasterizer implementations with their
//! own AA algorithms, gamma handling, and (for text) shaping/hinting —
//! real, legitimate visual differences are expected on every real proof
//! scene and are NOT bugs. The one thing this comparator's default
//! tolerance ([`ChannelTolerance::default`]) IS tuned to catch is a
//! STRUCTURAL defect: a whole primitive (a mark, a run of text, a
//! gradient fill) silently missing on one leg — which drives the
//! differing-pixel fraction far above what any AA/text-rasterization
//! difference alone produces. See [`STRUCTURAL_DEFECT_FRACTION`]'s own
//! doc comment for the empirical basis of the actual number.

/// Tolerance for [`compare_tight`].
#[derive(Debug, Clone, Copy)]
pub struct ChannelTolerance {
    /// A pixel counts as "differing" when its max per-channel absolute
    /// diff (0-255) exceeds this. Generous by design — ordinary AA-edge
    /// and text-rasterization differences between three independent
    /// rasterizers routinely reach this magnitude on real content
    /// without indicating any defect.
    pub edge: i32,
    /// The whole-image differing-pixel-fraction budget
    /// [`DiffReport::within_budget`] is checked against.
    pub max_differing_fraction: f64,
}

/// Per-channel diff magnitude below which a pixel is EXPECTED to differ
/// between three independent CPU rasterizers on ordinary AA-edge/text
/// content (measured empirically across the converted proofs — see this
/// crate's own divergence-findings report; a bare solid-fill scene with
/// no AA edges at all measures 0 here across every leg pair).
pub const STRUCTURAL_EDGE_TOLERANCE: i32 = 40;

/// The generous whole-image differing-fraction gate this crate's proof
/// tests use by default (via [`ChannelTolerance::default`]).
///
/// **Reasoning**: measured pairwise `differing_fraction` at
/// [`STRUCTURAL_EDGE_TOLERANCE`] across every converted `uzor-figures`
/// proof (GapPolicy/NaN, label-rotation, a text-heavy KPI tile row, a
/// gradient-heavy heatmap) never exceeded roughly a tenth of the canvas
/// even on the densest text/gradient content — AA fringes and font
/// rasterization differences are confined to edges/glyph outlines, a
/// small minority of any real figure's own pixels. A genuinely MISSING
/// primitive (an entire mark, an entire text run, an entire gradient
/// fill silently absent on one leg) instead flips a LARGE, contiguous
/// fraction of the canvas from "differs" to "matches the background" —
/// structurally a different failure shape, not a bigger version of the
/// same one. `0.35` sits well above the highest real AA/text measurement
/// seen and well below what any single missing primitive on a
/// non-trivial figure produces, so it fires on the latter and stays
/// silent on the former.
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 }
    }
}

/// Result of [`compare_tight`].
#[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)
}

/// Whole-image comparator — generalizes `uzor-examples/src/
/// parity_harness.rs`'s own `compare_tight`/`check_whole_image_budget` to
/// any two equal-length premultiplied-RGBA8 buffers, so a proof reuses
/// the SAME proven algorithm rather than a new one.
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 }
}

/// Every pairwise [`DiffReport`] across a [`crate::render::MultiLegRender`]'s
/// available legs.
///
/// The three CPU-only pairs are always [`Some`]-equivalent — those three
/// legs always render, so those three fields are plain [`DiffReport`],
/// never optional (preserves the exact field shape/names every existing
/// caller of the pre-GPU `ThreeLegDiff` already used). Every pair
/// touching a GPU leg is [`Option`] — `None` on a GPU-less machine,
/// mirroring exactly which legs [`crate::render::MultiLegRender::legs`]
/// lists. Two of the seven GPU-touching pairs are the WITHIN-FAMILY
/// CPU-vs-GPU comparisons (`vello_cpu_vs_vello_gpu`, `urx_cpu_vs_urx_gpu`)
/// — the new axis this crate's GPU legs exist to measure: since
/// `urx_cpu`/`urx_gpu` both rasterize the SAME recorded `Scene` (one
/// `UrxRenderContext` recording, two backends), a divergence on THAT pair
/// specifically is purely rasterisation, never a recording difference.
#[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>,
    /// Within-family: vello-cpu vs vello-gpu — same recording API
    /// (`VelloGpuRenderContext`/`VelloCpuRenderContext` both build a
    /// `vello`-family scene from the same draw closure), different
    /// rasteriser (compute pipeline vs scanline).
    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>,
    /// Within-family: urx-cpu vs urx-gpu — both rasterize the exact SAME
    /// recorded `Scene` (one `UrxRenderContext` recording pass feeds
    /// both), so any divergence here is purely a rasterisation
    /// difference (SDF/scanline vs MSAA/analytic), never a recording
    /// discrepancy.
    pub urx_cpu_vs_urx_gpu: Option<DiffReport>,
}

impl MultiLegDiff {
    /// Compute every pairwise [`compare_tight`] across `render`'s
    /// available legs.
    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)),
        }
    }

    /// `true` only when EVERY AVAILABLE pairwise comparison is within its
    /// own [`ChannelTolerance::max_differing_fraction`] budget — a pair
    /// touching a skipped GPU leg is simply not checked (`None` never
    /// fails the budget), so this gate stays meaningful (and green) on a
    /// GPU-less machine, just narrower.
    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)
    }

    /// One human-readable line per AVAILABLE pair — printed by every
    /// converted proof test (`cargo test -- --nocapture`) so the
    /// measured numbers surface directly, without a human having to
    /// re-derive them. A pair touching a skipped GPU leg is simply
    /// absent from this list.
    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]; // diff = 10, well under the default edge of 40
        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);
    }
}