Files
deckcraft/crates/h264/src/deblock.rs
T
Dylan M. TaylorandClaude Sonnet 5.5 d94790d97a refactor: replace redundant closures with method paths (SonarCloud S1612) (#1261)
Skipped where the receiver type is generic or not evident without a compiler (wrapping_neg, total_cmp, exists, to_vec, as_ref, split_first, Option::into_inner).

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 04:27:07 -04:00

555 lines
22 KiB
Rust

//! Deblocking filter (8.7) for progressive frames, 4:2:0, 8-bit.
use crate::picture::{MbKind, MbState};
use crate::slicedec::{PicState, SliceInfo};
use crate::tables::{ALPHA, BETA, TC0};
/// Reference picture identity + mvs of one 4x4 block, for bS = 1 decisions.
#[derive(Clone, Copy)]
struct Motion {
ids: [u32; 2],
mv: [[i16; 2]; 2],
count: u8,
}
/// Referenced picture ids per 8x8 block and list (u32::MAX = list unused).
fn ref_ids8(st: &MbState, sl: &SliceInfo) -> [[u32; 2]; 4] {
std::array::from_fn(|b8| {
std::array::from_fn(|l| {
let r = st.ref_idx[l][b8];
if r >= 0 { sl.ref_ids[l].get(r as usize).copied().unwrap_or(u32::MAX - 1) } else { u32::MAX }
})
})
}
#[inline(always)]
fn block_motion(st: &MbState, ids8: &[[u32; 2]; 4], raster: usize) -> Motion {
let b8 = (raster >> 3) * 2 + ((raster & 3) >> 1);
let ids = ids8[b8];
let count = (ids[0] != u32::MAX) as u8 + (ids[1] != u32::MAX) as u8;
Motion { ids, mv: [st.mv[0][raster], st.mv[1][raster]], count }
}
#[inline(always)]
fn mv_far(a: [i16; 2], b: [i16; 2]) -> bool {
(a[0] as i32 - b[0] as i32).abs() >= 4 || (a[1] as i32 - b[1] as i32).abs() >= 4
}
#[inline]
fn motion_bs(p: &Motion, q: &Motion) -> u8 {
if p.ids == q.ids && p.mv == q.mv {
return 0;
}
if p.count != q.count {
return 1;
}
if p.count == 1 {
let (pi, pm) = if p.ids[0] != u32::MAX { (p.ids[0], p.mv[0]) } else { (p.ids[1], p.mv[1]) };
let (qi, qm) = if q.ids[0] != u32::MAX { (q.ids[0], q.mv[0]) } else { (q.ids[1], q.mv[1]) };
if pi != qi {
return 1;
}
return mv_far(pm, qm) as u8;
}
if p.count == 0 {
return 0;
}
// two motion vectors each
let same_set = (p.ids[0] == q.ids[0] && p.ids[1] == q.ids[1]) || (p.ids[0] == q.ids[1] && p.ids[1] == q.ids[0]);
if !same_set {
return 1;
}
if p.ids[0] != p.ids[1] {
// two different reference pictures: compare mvs referring to the same picture
if p.ids[0] == q.ids[0] {
(mv_far(p.mv[0], q.mv[0]) || mv_far(p.mv[1], q.mv[1])) as u8
} else {
(mv_far(p.mv[0], q.mv[1]) || mv_far(p.mv[1], q.mv[0])) as u8
}
} else {
// both mvs refer to the same picture
((mv_far(p.mv[0], q.mv[0]) || mv_far(p.mv[1], q.mv[1])) && (mv_far(p.mv[0], q.mv[1]) || mv_far(p.mv[1], q.mv[0]))) as u8
}
}
#[cfg(test)]
fn clip3(lo: i32, hi: i32, v: i32) -> i32 {
v.clamp(lo, hi)
}
/// Filter one set of eight samples p3 p2 p1 p0 | q0 q1 q2 q3 across an edge (8.7.2.3 / 8.7.2.4):
/// the per-line reference for [`filter_lanes`].
#[cfg(test)]
fn filter8(v: &mut [u8; 8], bs: u8, alpha: i32, beta: i32, tc0: i32, chroma: bool) {
let p0 = v[3] as i32;
let q0 = v[4] as i32;
let p1 = v[2] as i32;
let q1 = v[5] as i32;
if (p0 - q0).abs() >= alpha || (p1 - p0).abs() >= beta || (q1 - q0).abs() >= beta {
return;
}
if chroma {
if bs < 4 {
let tc = tc0 + 1;
let delta = clip3(-tc, tc, (((q0 - p0) << 2) + (p1 - q1) + 4) >> 3);
v[3] = (p0 + delta).clamp(0, 255) as u8;
v[4] = (q0 - delta).clamp(0, 255) as u8;
} else {
v[3] = ((2 * p1 + p0 + q1 + 2) >> 2) as u8;
v[4] = ((2 * q1 + q0 + p1 + 2) >> 2) as u8;
}
return;
}
let p2 = v[1] as i32;
let q2 = v[6] as i32;
let ap = (p2 - p0).abs();
let aq = (q2 - q0).abs();
if bs < 4 {
let tc = tc0 + (ap < beta) as i32 + (aq < beta) as i32;
let delta = clip3(-tc, tc, (((q0 - p0) << 2) + (p1 - q1) + 4) >> 3);
v[3] = (p0 + delta).clamp(0, 255) as u8;
v[4] = (q0 - delta).clamp(0, 255) as u8;
if ap < beta {
v[2] = (p1 + clip3(-tc0, tc0, (p2 + ((p0 + q0 + 1) >> 1) - (p1 << 1)) >> 1)) as u8;
}
if aq < beta {
v[5] = (q1 + clip3(-tc0, tc0, (q2 + ((p0 + q0 + 1) >> 1) - (q1 << 1)) >> 1)) as u8;
}
} else {
let strong = (p0 - q0).abs() < ((alpha >> 2) + 2);
if ap < beta && strong {
let p3 = v[0] as i32;
v[3] = ((p2 + 2 * p1 + 2 * p0 + 2 * q0 + q1 + 4) >> 3) as u8;
v[2] = ((p2 + p1 + p0 + q0 + 2) >> 2) as u8;
v[1] = ((2 * p3 + 3 * p2 + p1 + p0 + q0 + 4) >> 3) as u8;
} else {
v[3] = ((2 * p1 + p0 + q1 + 2) >> 2) as u8;
}
if aq < beta && strong {
let q3 = v[7] as i32;
v[4] = ((p1 + 2 * p0 + 2 * q0 + 2 * q1 + q2 + 4) >> 3) as u8;
v[5] = ((p0 + q0 + q1 + q2 + 2) >> 2) as u8;
v[6] = ((2 * q3 + 3 * q2 + q1 + q0 + p0 + 4) >> 3) as u8;
} else {
v[4] = ((2 * q1 + q0 + p1 + 2) >> 2) as u8;
}
}
}
/// Edge filter parameters for `n` lines; `bs[i]` / `tc0[i]` apply to lines `i * n / 4 .. (i + 1) * n / 4`.
struct EdgeParams {
bs: [u8; 4],
tc0: [i32; 4],
alpha: i32,
beta: i32,
}
/// Filter one edge for `N` lines at once (lane `i` = line `i` across the edge). `s[k][i]` holds
/// sample p3 p2 p1 p0 q0 q1 q2 q3 (k = 0..8) of line `i`. Branch-free per lane, so the loops
/// vectorise; bit-exact with [`filter8`] applied to every line (8.7.2.3 / 8.7.2.4).
#[inline(always)]
fn filter_lanes<const N: usize>(s: &mut [[i16; N]; 8], ep: &EdgeParams, chroma: bool) {
let per = N / 4;
let (alpha, beta) = (ep.alpha as i16, ep.beta as i16);
let mut bs_on = [false; N];
let mut tc0 = [0i16; N];
for k in 0..4 {
for i in k * per..(k + 1) * per {
bs_on[i] = ep.bs[k] != 0;
tc0[i] = ep.tc0[k] as i16;
}
}
let [p3, p2, p1, p0, q0, q1, q2, q3] = *s;
// Masks are 0 / -1 per lane, selects are bitwise, and clipping to per-lane bounds uses
// min / max (`clamp` would assert its bounds per lane), so every loop below vectorises.
let lt = |x: i16, y: i16| -((x < y) as i16);
let sel = |m: i16, a: i16, b: i16| (a & m) | (b & !m);
let mut on = [0i16; N];
let mut any = 0i16;
for i in 0..N {
on[i] = -(bs_on[i] as i16) & lt((p0[i] - q0[i]).abs(), alpha) & lt((p1[i] - p0[i]).abs(), beta) & lt((q1[i] - q0[i]).abs(), beta);
any |= on[i];
}
if any == 0 {
return;
}
// bS 4 is only ever assigned to all four segments of an edge
let strong_edge = ep.bs[0] >= 4;
if chroma {
for i in 0..N {
let (np0, nq0) = if strong_edge {
((2 * p1[i] + p0[i] + q1[i] + 2) >> 2, (2 * q1[i] + q0[i] + p1[i] + 2) >> 2)
} else {
let tc = tc0[i] + 1;
let delta = ((((q0[i] - p0[i]) << 2) + (p1[i] - q1[i]) + 4) >> 3).max(-tc).min(tc);
((p0[i] + delta).clamp(0, 255), (q0[i] - delta).clamp(0, 255))
};
s[3][i] = sel(on[i], np0, p0[i]);
s[4][i] = sel(on[i], nq0, q0[i]);
}
return;
}
if strong_edge {
let lim = (alpha >> 2) + 2;
for i in 0..N {
let (a, b, c, d, e, f, g, h) = (p3[i], p2[i], p1[i], p0[i], q0[i], q1[i], q2[i], q3[i]);
let strong = lt((d - e).abs(), lim);
let sp = on[i] & lt((b - d).abs(), beta) & strong;
let sq = on[i] & lt((g - e).abs(), beta) & strong;
let wp0 = (2 * c + d + f + 2) >> 2;
let wq0 = (2 * f + e + c + 2) >> 2;
let sp0 = (b + 2 * c + 2 * d + 2 * e + f + 4) >> 3;
let sq0 = (c + 2 * d + 2 * e + 2 * f + g + 4) >> 3;
s[3][i] = sel(sp, sp0, sel(on[i], wp0, d));
s[2][i] = sel(sp, (b + c + d + e + 2) >> 2, c);
s[1][i] = sel(sp, (2 * a + 3 * b + c + d + e + 4) >> 3, b);
s[4][i] = sel(sq, sq0, sel(on[i], wq0, e));
s[5][i] = sel(sq, (d + e + f + g + 2) >> 2, f);
s[6][i] = sel(sq, (2 * h + 3 * g + f + e + d + 4) >> 3, g);
}
} else {
for i in 0..N {
let ap = lt((p2[i] - p0[i]).abs(), beta);
let aq = lt((q2[i] - q0[i]).abs(), beta);
let t0 = tc0[i];
let tc = t0 - ap - aq;
let delta = ((((q0[i] - p0[i]) << 2) + (p1[i] - q1[i]) + 4) >> 3).max(-tc).min(tc);
let avg = (p0[i] + q0[i] + 1) >> 1;
let np1 = p1[i] + ((p2[i] + avg - (p1[i] << 1)) >> 1).max(-t0).min(t0);
let nq1 = q1[i] + ((q2[i] + avg - (q1[i] << 1)) >> 1).max(-t0).min(t0);
s[3][i] = sel(on[i], (p0[i] + delta).clamp(0, 255), p0[i]);
s[4][i] = sel(on[i], (q0[i] - delta).clamp(0, 255), q0[i]);
s[2][i] = sel(on[i] & ap, np1, p1[i]);
s[5][i] = sel(on[i] & aq, nq1, q1[i]);
}
}
}
/// Filter a vertical edge whose q0 column is at `x`, for `N` lines starting at row `y`.
#[inline(always)]
fn filter_vertical<const N: usize>(pix: &mut [u8], stride: usize, x: usize, y: usize, ep: &EdgeParams, chroma: bool) {
// Each line's eight samples as one little-endian word: byte k = sample k (p3 .. q3).
let mut w = [0u64; N];
for (i, w) in w.iter_mut().enumerate() {
let o = (y + i) * stride + x - 4;
let Some(&b) = pix.get(o..).and_then(<[_]>::first_chunk::<8>) else {
return;
};
*w = u64::from_le_bytes(b);
}
let mut s = [[0i16; N]; 8];
for (k, row) in s.iter_mut().enumerate() {
for i in 0..N {
row[i] = ((w[i] >> (8 * k)) & 0xff) as i16;
}
}
filter_lanes(&mut s, ep, chroma);
for (i, w) in w.iter_mut().enumerate() {
*w = (0..8).fold(0u64, |acc, k| acc | ((s[k][i] as u8 as u64) << (8 * k)));
}
for (i, w) in w.iter().enumerate() {
let o = (y + i) * stride + x - 4;
pix[o..o + 8].copy_from_slice(&w.to_le_bytes());
}
}
/// Filter a horizontal edge whose q0 row is `y`, for `N` columns starting at `x`.
#[inline(always)]
fn filter_horizontal<const N: usize>(pix: &mut [u8], stride: usize, x: usize, y: usize, ep: &EdgeParams, chroma: bool) {
let base = (y - 4) * stride + x;
let mut s = [[0i16; N]; 8];
for (k, row) in s.iter_mut().enumerate() {
let Some(src) = pix.get(base + k * stride..).and_then(<[_]>::first_chunk::<N>) else {
return;
};
for i in 0..N {
row[i] = src[i] as i16;
}
}
filter_lanes(&mut s, ep, chroma);
let (k0, k1) = if chroma { (3, 5) } else { (1, 7) };
for (k, row) in s.iter().enumerate().take(k1).skip(k0) {
let Some(dst) = pix.get_mut(base + k * stride..).and_then(<[_]>::first_chunk_mut::<N>) else {
return;
};
for i in 0..N {
dst[i] = row[i] as u8;
}
}
}
/// Deblock one macroblock (macroblocks must be processed in raster order).
#[allow(clippy::needless_range_loop)]
pub fn deblock_mb(pic: &mut PicState, addr: usize, mb_w: usize) {
let q = &pic.mbs[addr];
if q.slice_num == u32::MAX {
return;
}
let sq = &pic.slices[q.slice_num as usize];
if sq.disable_deblocking_filter_idc == 1 {
return;
}
let (mx, my) = (addr % mb_w, addr / mb_w);
let usable = |n: Option<usize>| -> Option<usize> {
let n = n?;
let st = &pic.mbs[n];
if st.slice_num == u32::MAX || (sq.disable_deblocking_filter_idc == 2 && st.slice_num != q.slice_num) {
return None;
}
Some(n)
};
let left = usable(if mx > 0 { Some(addr - 1) } else { None });
let top = usable(if my > 0 { Some(addr - mb_w) } else { None });
let alpha_off = sq.alpha_offset;
let beta_off = sq.beta_offset;
let t8 = q.transform_8x8;
// bS[dir][edge][segment]; dir 0 = vertical edges (x), 1 = horizontal edges (y)
let mut bs = [[[0u8; 4]; 4]; 2];
if q.kind.is_intra() {
for dir in 0..2 {
if (if dir == 0 { left } else { top }).is_some() {
bs[dir][0] = [4; 4];
}
for e in 1..4 {
if !(t8 && e % 2 == 1) {
bs[dir][e] = [3; 4];
}
}
}
} else {
let qids = ref_ids8(q, sq);
let q_uniform = (0..2).all(|l| {
let r = q.ref_idx[l];
r[1] == r[0] && r[2] == r[0] && r[3] == r[0] && q.mv[l].iter().all(|m| *m == q.mv[l][0])
});
let qm: [Motion; 16] = std::array::from_fn(|r| block_motion(q, &qids, r));
for dir in 0..2 {
let neighbor = if dir == 0 { left } else { top };
if let Some(n) = neighbor {
let p = &pic.mbs[n];
if p.kind.is_intra() {
bs[dir][0] = [4; 4];
} else {
let pids = ref_ids8(p, &pic.slices[p.slice_num as usize]);
for k in 0..4 {
let (rq, rp) = if dir == 0 { (k * 4, k * 4 + 3) } else { (k, 12 + k) };
bs[dir][0][k] = if (p.nz_mask >> rp) & 1 != 0 || (q.nz_mask >> rq) & 1 != 0 {
2
} else {
motion_bs(&block_motion(p, &pids, rp), &qm[rq])
};
}
}
}
if q_uniform && q.nz_mask == 0 {
// one motion for the whole MB and no coefficients: all internal edges have bS 0
continue;
}
for e in 1..4 {
if t8 && e % 2 == 1 {
continue;
}
for k in 0..4 {
let (rq, rp) = if dir == 0 { (k * 4 + e, k * 4 + e - 1) } else { (e * 4 + k, e * 4 + k - 4) };
bs[dir][e][k] = if (q.nz_mask >> rp) & 1 != 0 || (q.nz_mask >> rq) & 1 != 0 { 2 } else { motion_bs(&qm[rp], &qm[rq]) };
}
}
}
}
let qp_of = |st: &MbState| if st.kind == MbKind::IPcm { 0 } else { st.qp as i32 };
let q_qp = qp_of(q);
let q_qpc = q.qpc;
let p_qp = [left.map(|n| (qp_of(&pic.mbs[n]), pic.mbs[n].qpc)), top.map(|n| (qp_of(&pic.mbs[n]), pic.mbs[n].qpc))];
let params = |qpp: i32, qpq: i32, b: [u8; 4]| {
let qpav = (qpp + qpq + 1) >> 1;
let index_a = (qpav + alpha_off).clamp(0, 51) as usize;
let index_b = (qpav + beta_off).clamp(0, 51) as usize;
let tc = |b: u8| if (1..4).contains(&b) { TC0[index_a][b as usize - 1] as i32 } else { 0 };
EdgeParams { bs: b, tc0: [tc(b[0]), tc(b[1]), tc(b[2]), tc(b[3])], alpha: ALPHA[index_a] as i32, beta: BETA[index_b] as i32 }
};
let width = pic.planes.width;
let cwidth = pic.planes.cwidth;
// luma
for dir in 0..2 {
for e in 0..4 {
if bs[dir][e] == [0; 4] {
continue;
}
let qpp = if e == 0 { p_qp[dir].map(|p| p.0).unwrap_or(q_qp) } else { q_qp };
let ep = params(qpp, q_qp, bs[dir][e]);
if ep.alpha == 0 || ep.beta == 0 {
// filterSamplesFlag needs |p0 - q0| < alpha and |p1 - p0| < beta (8.7.2.2): no
// sample of the edge changes (low QPs)
continue;
}
if dir == 0 {
filter_vertical::<16>(&mut pic.planes.y, width, mx * 16 + e * 4, my * 16, &ep, false);
} else {
filter_horizontal::<16>(&mut pic.planes.y, width, mx * 16, my * 16 + e * 4, &ep, false);
}
}
}
// chroma (4:2:0): edges at chroma samples 0 and 4 use the bS of luma edges 0 and 2
for c in 0..2 {
for dir in 0..2 {
for ce in 0..2 {
let e = ce * 2;
if bs[dir][e] == [0; 4] {
continue;
}
let qpp = if e == 0 { p_qp[dir].map(|p| p.1[c]).unwrap_or(q_qpc[c]) } else { q_qpc[c] } as i32;
let ep = params(qpp, q_qpc[c] as i32, bs[dir][e]);
if ep.alpha == 0 || ep.beta == 0 {
continue;
}
let plane = if c == 0 { &mut pic.planes.cb } else { &mut pic.planes.cr };
if dir == 0 {
filter_vertical::<8>(plane, cwidth, mx * 8 + ce * 4, my * 8, &ep, true);
} else {
filter_horizontal::<8>(plane, cwidth, mx * 8, my * 8 + ce * 4, &ep, true);
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
/// The vectorised edge filter equals the per-line reference [`filter8`] on random edges
/// (small differences across the edge so that every filter branch is taken).
#[test]
fn lane_filter_matches_per_line_reference() {
let mut rng = 0x9e37_79b9_7f4a_7c15u64;
let mut next = move || {
rng ^= rng << 13;
rng ^= rng >> 7;
rng ^= rng << 17;
rng
};
for iter in 0..20_000 {
let chroma = iter % 3 == 0;
let strong = iter % 4 == 1;
let bs: [u8; 4] = if strong { [4; 4] } else { std::array::from_fn(|_| (next() % 4) as u8) };
let index_a = (next() % 52) as usize;
let index_b = (next() % 52) as usize;
let tc = |b: u8| if (1..4).contains(&b) { TC0[index_a][b as usize - 1] as i32 } else { 0 };
let ep = EdgeParams { bs, tc0: bs.map(tc), alpha: ALPHA[index_a] as i32, beta: BETA[index_b] as i32 };
let base = (next() % 256) as i32;
let spread = 1 + (next() % 24) as i32;
let mut lines = [[0u8; 8]; 16];
for l in lines.iter_mut() {
for v in l.iter_mut() {
*v = (base + (next() % (2 * spread as u64 + 1)) as i32 - spread).clamp(0, 255) as u8;
}
}
let n = if chroma { 8 } else { 16 };
let mut want = lines;
for (i, l) in want.iter_mut().enumerate().take(n) {
let k = i / (n / 4);
if bs[k] != 0 {
filter8(l, bs[k], ep.alpha, ep.beta, ep.tc0[k], chroma);
}
}
let got: Vec<[u8; 8]> = if chroma {
let mut s = [[0i16; 8]; 8];
for i in 0..8 {
for k in 0..8 {
s[k][i] = lines[i][k] as i16;
}
}
filter_lanes(&mut s, &ep, true);
(0..8).map(|i| std::array::from_fn(|k| s[k][i] as u8)).collect()
} else {
let mut s = [[0i16; 16]; 8];
for i in 0..16 {
for k in 0..8 {
s[k][i] = lines[i][k] as i16;
}
}
filter_lanes(&mut s, &ep, false);
(0..16).map(|i| std::array::from_fn(|k| s[k][i] as u8)).collect()
};
assert_eq!(&got[..], &want[..n], "iteration {iter}: bs {bs:?} indexA {index_a} indexB {index_b}");
}
}
}
#[cfg(test)]
mod speed {
use super::*;
/// Rough timing of the edge filters (`cargo test --release -p deckcraft-h264 --lib edge_filter_speed -- --ignored --nocapture`).
#[test]
#[ignore]
fn edge_filter_speed() {
let (w, h) = (1024usize, 256usize);
let mut pix: Vec<u8> = (0..w * h).map(|i| (128 + ((i * 7919) % 13) as i32 - 6) as u8).collect();
let eps = [
EdgeParams { bs: [4; 4], tc0: [0; 4], alpha: 40, beta: 9 },
EdgeParams { bs: [3; 4], tc0: [2; 4], alpha: 40, beta: 9 },
EdgeParams { bs: [2, 1, 0, 2], tc0: [1, 1, 0, 1], alpha: 40, beta: 9 },
];
let t0 = std::time::Instant::now();
let mut n = 0;
for _ in 0..20 {
for my in 1..h / 16 {
for mx in 1..w / 16 {
for (j, ep) in eps.iter().enumerate() {
filter_horizontal::<16>(&mut pix, w, mx * 16, my * 16 + j * 4, ep, false);
n += 1;
}
}
}
}
let dt = t0.elapsed();
let tv = std::time::Instant::now();
for _ in 0..20 {
for my in 1..h / 16 {
for mx in 1..w / 16 {
for (j, ep) in eps.iter().enumerate() {
filter_vertical::<16>(&mut pix, w, mx * 16 + j * 4, my * 16, ep, false);
}
}
}
}
println!("vertical: {:.1} ns/edge", tv.elapsed().as_nanos() as f64 / n as f64);
let t1 = std::time::Instant::now();
for _ in 0..20 {
for my in 1..h / 16 {
for mx in 1..w / 16 {
for ep in eps.iter() {
for (vert, x, y) in [(true, mx * 16, my * 16), (false, mx * 16, my * 16)] {
for i in 0..16 {
let b = ep.bs[i / 4];
if b == 0 {
continue;
}
let mut v = [0u8; 8];
for k in 0..8 {
v[k] = if vert { pix[(y + i) * w + x - 4 + k] } else { pix[(y - 4 + k) * w + x + i] };
}
filter8(&mut v, b, ep.alpha, ep.beta, ep.tc0[i / 4], false);
for k in 1..7 {
if vert {
pix[(y + i) * w + x - 4 + k] = v[k];
} else {
pix[(y - 4 + k) * w + x + i] = v[k];
}
}
}
}
}
}
}
}
println!("scalar reference (both): {:.1} ns/edge", t1.elapsed().as_nanos() as f64 / (2 * n) as f64);
println!(
"horizontal: {n} luma edges in {dt:?}: {:.1} ns/edge (checksum {})",
dt.as_nanos() as f64 / n as f64,
pix.iter().map(|&v| v as u64).sum::<u64>()
);
}
}