Merge PR #3066: frame: convert full-size 8-bit Y'CbCr row by row, 28 % faster and bit-identical

# Conflicts:
#	crates/frame/src/lib.rs
This commit is contained in:
aiongg
2026-10-31 02:11:58 +00:00
2 changed files with 125 additions and 0 deletions
+33
View File
@@ -516,6 +516,39 @@ impl VideoFrame {
});
return;
}
if n == 1 {
// One source pixel per output pixel (export, full-size playback): each row's
// samples are found once, not per pixel, and the arithmetic is the general
// loop's (`0 + q(..) * a`, then `* inv`), so every value is the same, bit for bit.
// Samples the planes do not hold come out transparent instead of panicking.
let ytab: [f32; 256] = std::array::from_fn(|v| ytab.get(v).copied().unwrap_or(0.0));
let ctab: [f32; 256] = std::array::from_fn(|v| ctab.get(v).copied().unwrap_or(0.0));
/// The samples a plane of rows `len` samples long holds of row `at`: the whole
/// row, the part of it before the plane ends, or none.
fn line(p: &[u8], at: usize, len: usize) -> &[u8] {
let start = at.saturating_mul(len);
p.get(start..start.saturating_add(len).min(p.len())).unwrap_or(&[])
}
out.par_chunks_mut(r.w * 4).enumerate().for_each(|(ry, row)| {
let y = (r.y + ry).min(h - 1);
let cy = y >> sy;
let (yrow, urow, vrow) = (line(&planes[0], y, w), line(&planes[1], cy, cw), line(&planes[2], cy, cw));
let arow = alpha.as_ref().map(|al| line(al, y, w));
for (rx, px) in row.as_chunks_mut::<4>().0.iter_mut().enumerate() {
let x = (r.x + rx).min(w - 1);
let cx = x >> sx;
let (Some(&ys), Some(&us), Some(&vs)) = (yrow.get(x), urow.get(cx), vrow.get(cx)) else {
*px = [0.0; 4];
continue;
};
let (yy, u, v) = (ytab[usize::from(ys)], ctab[usize::from(us)], ctab[usize::from(vs)]);
let a = arow.map_or(1.0, |al| al.get(x).map_or(0.0, |&s| f32::from(s) / 255.0));
*px = [
(0.0 + q(yy + cr_r * v) * a) * inv,
(0.0 + q(yy - cr_g * v - cb_g * u) * a) * inv,
(0.0 + q(yy + cb_b * u) * a) * inv,
(0.0 + a) * inv,
];
out.par_chunks_mut(r.w * 4).enumerate().for_each(|(ry, row)| {
let oy = r.y + ry;
for rx in 0..r.w {
+92
View File
@@ -459,3 +459,95 @@ fn opaque_yuv8_to_srgb_bytes_is_the_linear_picture_in_srgb() {
};
assert_eq!(short.to_opaque_srgb8(1), None);
}
/// The conversion of an 8-bit Y'CbCr picture written out one pixel at a time, in the general
/// loop's arithmetic: what [`VideoFrame::to_linear_f32_decimated_with`] gives at full size.
fn yuv8_reference(f: &VideoFrame, decode: Option<&DecodeTable>) -> Vec<f32> {
let PixelData::Yuv8 { planes, chroma, alpha } = &f.data else { panic!("8-bit Y'CbCr only") };
let (w, h) = (f.width as usize, f.height as usize);
let (sx, sy) = chroma.shifts();
let cw = w.div_ceil(1 << sx);
let info = f.color;
let q = |v: f32| match decode {
Some(t) => t.lookup(v),
None => to_linear(((v.clamp(0.0, 1.0) * 4095.0 + 0.5) as usize) as f32 / 4095.0, info.transfer),
};
let (kr, kb) = info.matrix.kr_kb();
let kg = 1.0 - kr - kb;
let (cr_r, cb_b) = (2.0 * (1.0 - kr), 2.0 * (1.0 - kb));
let (cr_g, cb_g) = (cr_r * kr / kg, cb_b * kb / kg);
let mut out = Vec::with_capacity(w * h * 4);
for y in 0..h {
for x in 0..w {
let c = (y >> sy) * cw + (x >> sx);
let yy = normalize_y(u32::from(planes[0][y * w + x]), 8, info.range);
let (u, v) = (normalize_c(u32::from(planes[1][c]), 8, info.range), normalize_c(u32::from(planes[2][c]), 8, info.range));
let a = alpha.as_ref().map_or(1.0, |al| f32::from(al[y * w + x]) / 255.0);
out.extend([(0.0 + q(yy + cr_r * v) * a), (0.0 + q(yy - cr_g * v - cb_g * u) * a), (0.0 + q(yy + cb_b * u) * a), (0.0 + a)]);
}
}
out
}
/// The one-to-one path gives every value of the pixel-by-pixel definition, bit for bit, for
/// every chroma format, with and without alpha, in both ranges and with a decode table.
#[test]
fn a_full_size_yuv8_picture_converts_exactly_pixel_by_pixel() {
let mut s = 0x2545_f491_4f6c_dd1du64;
let mut byte = || {
s ^= s << 13;
s ^= s >> 7;
s ^= s << 17;
s as u8
};
let pq = DecodeTable::new(0.0, 1.0, 4096, |v| f64::from(filmcraft_color::pq_eotf(v as f32)) * 100.0);
for (w, h) in [(1usize, 1usize), (2, 2), (3, 5), (37, 23), (64, 2)] {
for chroma in [Chroma::C420, Chroma::C422, Chroma::C444] {
let (sx, sy) = chroma.shifts();
let (cw, ch) = (w.div_ceil(1 << sx), h.div_ceil(1 << sy));
for (alpha, color) in [(false, ColorInfo::REC709), (true, ColorInfo { range: Range::Full, matrix: Matrix::Bt601, ..ColorInfo::SRGB_FULL })] {
let mut plane = |n: usize| Arc::new((0..n).map(|_| byte()).collect::<Vec<u8>>());
let planes = [plane(w * h), plane(cw * ch), plane(cw * ch)];
let f = VideoFrame {
width: w as u32,
height: h as u32,
data: PixelData::Yuv8 { planes, chroma, alpha: alpha.then(|| plane(w * h)) },
color,
par: (1, 1),
pts: Tick::ZERO,
};
for decode in [None, Some(&pq)] {
let (_, _, got) = f.to_linear_f32_decimated_with(1, decode);
let want = yuv8_reference(&f, decode);
let bits = |v: &[f32]| v.iter().map(|x| x.to_bits()).collect::<Vec<_>>();
assert_eq!(bits(&got), bits(&want), "{w}x{h} {chroma:?} alpha {alpha} {color:?} decode {}", decode.is_some());
}
}
}
}
}
/// Planes shorter than the picture never make the conversion panic: the pixels whose samples
/// are there come out as usual (also in a row the plane ends in), the others transparent.
#[test]
fn a_yuv8_picture_with_short_planes_converts_without_panicking() {
// 8x8 4:2:0 whose planes hold three and a half rows of luma, two of chroma and five of
// alpha: rows 0-2 are whole, row 3 has its first four pixels, the rest is transparent
let f = VideoFrame {
data: PixelData::Yuv8 {
planes: [Arc::new(vec![235; 28]), Arc::new(vec![128; 8]), Arc::new(vec![128; 8])],
chroma: Chroma::C420,
alpha: Some(Arc::new(vec![255; 40])),
},
color: ColorInfo::REC709,
..VideoFrame::rgba8(8, 8, vec![0; 256])
};
let (_, _, px) = f.to_linear_f32_decimated(1);
assert_eq!(px.len(), 8 * 8 * 4);
let white = |p: &[f32]| p[3] == 1.0 && p[0] > 0.99;
let row = |y: usize| &px[y * 32..(y + 1) * 32];
assert!((0..3).all(|y| row(y).chunks(4).all(white)), "whole rows of samples: {:?}", row(0));
assert!(row(3)[..16].chunks(4).all(white), "the samples the last luma row holds: {:?}", row(3));
assert!(row(3)[16..].iter().all(|v| *v == 0.0), "the rest of that row is transparent: {:?}", row(3));
assert!((4..8).all(|y| row(y).iter().all(|v| *v == 0.0)), "rows without luma are transparent");
}