use super::*;
#[cfg_attr(not(tarpaulin), inline(always))]
fn unpack_v210_word<const BE: bool>(word: &[u8]) -> ([u16; 6], [u16; 3], [u16; 3]) {
assert_eq!(word.len(), 16);
let w0 = unsafe { load_endian_u32::<BE>(word.as_ptr()) };
let w1 = unsafe { load_endian_u32::<BE>(word.as_ptr().add(4)) };
let w2 = unsafe { load_endian_u32::<BE>(word.as_ptr().add(8)) };
let w3 = unsafe { load_endian_u32::<BE>(word.as_ptr().add(12)) };
let cb0 = (w0 & 0x3FF) as u16;
let y0 = ((w0 >> 10) & 0x3FF) as u16;
let cr0 = ((w0 >> 20) & 0x3FF) as u16;
let y1 = (w1 & 0x3FF) as u16;
let cb1 = ((w1 >> 10) & 0x3FF) as u16;
let y2 = ((w1 >> 20) & 0x3FF) as u16;
let cr1 = (w2 & 0x3FF) as u16;
let y3 = ((w2 >> 10) & 0x3FF) as u16;
let cb2 = ((w2 >> 20) & 0x3FF) as u16;
let y4 = (w3 & 0x3FF) as u16;
let cr2 = ((w3 >> 10) & 0x3FF) as u16;
let y5 = ((w3 >> 20) & 0x3FF) as u16;
([y0, y1, y2, y3, y4, y5], [cb0, cb1, cb2], [cr0, cr1, cr2])
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub(crate) fn v210_to_rgb_or_rgba_row<const ALPHA: bool, const BE: bool>(
packed: &[u8],
out: &mut [u8],
width: usize,
matrix: ColorMatrix,
full_range: bool,
) {
debug_assert!(width.is_multiple_of(2), "v210 requires even width");
let total_words = width.div_ceil(6);
debug_assert!(packed.len() >= total_words * 16, "packed row too short");
let bpp: usize = if ALPHA { 4 } else { 3 };
debug_assert!(out.len() >= width * bpp, "out row too short");
let coeffs = Coefficients::for_matrix(matrix);
let (y_off, y_scale, c_scale) = range_params_n::<10, 8>(full_range);
let bias = chroma_bias::<10>();
let full_words = width / 6;
let tail_pixels = width - full_words * 6;
for w in 0..full_words {
let word = &packed[w * 16..w * 16 + 16];
let (ys, us, vs) = unpack_v210_word::<BE>(word);
for i in 0..3 {
let u_d = q15_scale(us[i] as i32 - bias, c_scale);
let v_d = q15_scale(vs[i] as i32 - bias, c_scale);
let r_chroma = q15_chroma(coeffs.r_u(), u_d, coeffs.r_v(), v_d);
let g_chroma = q15_chroma(coeffs.g_u(), u_d, coeffs.g_v(), v_d);
let b_chroma = q15_chroma(coeffs.b_u(), u_d, coeffs.b_v(), v_d);
for k in 0..2 {
let y = ys[i * 2 + k] as i32;
let y_s = q15_scale(y - y_off, y_scale);
let px = w * 6 + i * 2 + k;
let off = px * bpp;
out[off] = clamp_u8(y_s + r_chroma);
out[off + 1] = clamp_u8(y_s + g_chroma);
out[off + 2] = clamp_u8(y_s + b_chroma);
if ALPHA {
out[off + 3] = 0xFF;
}
}
}
}
if tail_pixels > 0 {
let w = full_words;
let word = &packed[w * 16..w * 16 + 16];
let (ys, us, vs) = unpack_v210_word::<BE>(word);
let pairs = tail_pixels / 2;
for i in 0..pairs {
let u_d = q15_scale(us[i] as i32 - bias, c_scale);
let v_d = q15_scale(vs[i] as i32 - bias, c_scale);
let r_chroma = q15_chroma(coeffs.r_u(), u_d, coeffs.r_v(), v_d);
let g_chroma = q15_chroma(coeffs.g_u(), u_d, coeffs.g_v(), v_d);
let b_chroma = q15_chroma(coeffs.b_u(), u_d, coeffs.b_v(), v_d);
for k in 0..2 {
let y = ys[i * 2 + k] as i32;
let y_s = q15_scale(y - y_off, y_scale);
let px = w * 6 + i * 2 + k;
let off = px * bpp;
out[off] = clamp_u8(y_s + r_chroma);
out[off + 1] = clamp_u8(y_s + g_chroma);
out[off + 2] = clamp_u8(y_s + b_chroma);
if ALPHA {
out[off + 3] = 0xFF;
}
}
}
}
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub(crate) fn v210_to_rgb_u16_or_rgba_u16_row<const ALPHA: bool, const BE: bool>(
packed: &[u8],
out: &mut [u16],
width: usize,
matrix: ColorMatrix,
full_range: bool,
) {
debug_assert!(width.is_multiple_of(2), "v210 requires even width");
let total_words = width.div_ceil(6);
debug_assert!(packed.len() >= total_words * 16, "packed row too short");
let bpp: usize = if ALPHA { 4 } else { 3 };
debug_assert!(out.len() >= width * bpp, "out row too short");
let coeffs = Coefficients::for_matrix(matrix);
let (y_off, y_scale, c_scale) = range_params_n::<10, 10>(full_range);
let bias = chroma_bias::<10>();
let out_max: i32 = (1i32 << 10) - 1;
let alpha_max: u16 = out_max as u16;
let full_words = width / 6;
let tail_pixels = width - full_words * 6;
for w in 0..full_words {
let word = &packed[w * 16..w * 16 + 16];
let (ys, us, vs) = unpack_v210_word::<BE>(word);
for i in 0..3 {
let u_d = q15_scale(us[i] as i32 - bias, c_scale);
let v_d = q15_scale(vs[i] as i32 - bias, c_scale);
let r_chroma = q15_chroma(coeffs.r_u(), u_d, coeffs.r_v(), v_d);
let g_chroma = q15_chroma(coeffs.g_u(), u_d, coeffs.g_v(), v_d);
let b_chroma = q15_chroma(coeffs.b_u(), u_d, coeffs.b_v(), v_d);
for k in 0..2 {
let y = ys[i * 2 + k] as i32;
let y_s = q15_scale(y - y_off, y_scale);
let px = w * 6 + i * 2 + k;
let off = px * bpp;
out[off] = (y_s + r_chroma).clamp(0, out_max) as u16;
out[off + 1] = (y_s + g_chroma).clamp(0, out_max) as u16;
out[off + 2] = (y_s + b_chroma).clamp(0, out_max) as u16;
if ALPHA {
out[off + 3] = alpha_max;
}
}
}
}
if tail_pixels > 0 {
let w = full_words;
let word = &packed[w * 16..w * 16 + 16];
let (ys, us, vs) = unpack_v210_word::<BE>(word);
let pairs = tail_pixels / 2;
for i in 0..pairs {
let u_d = q15_scale(us[i] as i32 - bias, c_scale);
let v_d = q15_scale(vs[i] as i32 - bias, c_scale);
let r_chroma = q15_chroma(coeffs.r_u(), u_d, coeffs.r_v(), v_d);
let g_chroma = q15_chroma(coeffs.g_u(), u_d, coeffs.g_v(), v_d);
let b_chroma = q15_chroma(coeffs.b_u(), u_d, coeffs.b_v(), v_d);
for k in 0..2 {
let y = ys[i * 2 + k] as i32;
let y_s = q15_scale(y - y_off, y_scale);
let px = w * 6 + i * 2 + k;
let off = px * bpp;
out[off] = (y_s + r_chroma).clamp(0, out_max) as u16;
out[off + 1] = (y_s + g_chroma).clamp(0, out_max) as u16;
out[off + 2] = (y_s + b_chroma).clamp(0, out_max) as u16;
if ALPHA {
out[off + 3] = alpha_max;
}
}
}
}
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub(crate) fn v210_to_luma_row<const BE: bool>(packed: &[u8], luma_out: &mut [u8], width: usize) {
debug_assert!(width.is_multiple_of(2), "v210 requires even width");
let total_words = width.div_ceil(6);
debug_assert!(packed.len() >= total_words * 16, "packed row too short");
debug_assert!(luma_out.len() >= width, "luma row too short");
let full_words = width / 6;
let tail_pixels = width - full_words * 6;
for w in 0..full_words {
let word = &packed[w * 16..w * 16 + 16];
let (ys, _, _) = unpack_v210_word::<BE>(word);
for k in 0..6 {
luma_out[w * 6 + k] = (ys[k] >> 2) as u8;
}
}
if tail_pixels > 0 {
let w = full_words;
let word = &packed[w * 16..w * 16 + 16];
let (ys, _, _) = unpack_v210_word::<BE>(word);
for k in 0..tail_pixels {
luma_out[w * 6 + k] = (ys[k] >> 2) as u8;
}
}
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub(crate) fn v210_to_luma_u16_row<const BE: bool>(
packed: &[u8],
luma_out: &mut [u16],
width: usize,
) {
debug_assert!(width.is_multiple_of(2), "v210 requires even width");
let total_words = width.div_ceil(6);
debug_assert!(packed.len() >= total_words * 16, "packed row too short");
debug_assert!(luma_out.len() >= width, "luma row too short");
let full_words = width / 6;
let tail_pixels = width - full_words * 6;
for w in 0..full_words {
let word = &packed[w * 16..w * 16 + 16];
let (ys, _, _) = unpack_v210_word::<BE>(word);
luma_out[w * 6..w * 6 + 6].copy_from_slice(&ys);
}
if tail_pixels > 0 {
let w = full_words;
let word = &packed[w * 16..w * 16 + 16];
let (ys, _, _) = unpack_v210_word::<BE>(word);
luma_out[w * 6..w * 6 + tail_pixels].copy_from_slice(&ys[..tail_pixels]);
}
}
#[cfg(all(test, feature = "std"))]
mod tests {
use super::*;
use crate::ColorMatrix;
fn pack_v210_word(samples: [u16; 12]) -> [u8; 16] {
let mut out = [0u8; 16];
let w0 = (samples[0] as u32 & 0x3FF)
| ((samples[1] as u32 & 0x3FF) << 10)
| ((samples[2] as u32 & 0x3FF) << 20);
let w1 = (samples[3] as u32 & 0x3FF)
| ((samples[4] as u32 & 0x3FF) << 10)
| ((samples[5] as u32 & 0x3FF) << 20);
let w2 = (samples[6] as u32 & 0x3FF)
| ((samples[7] as u32 & 0x3FF) << 10)
| ((samples[8] as u32 & 0x3FF) << 20);
let w3 = (samples[9] as u32 & 0x3FF)
| ((samples[10] as u32 & 0x3FF) << 10)
| ((samples[11] as u32 & 0x3FF) << 20);
out[0..4].copy_from_slice(&w0.to_le_bytes());
out[4..8].copy_from_slice(&w1.to_le_bytes());
out[8..12].copy_from_slice(&w2.to_le_bytes());
out[12..16].copy_from_slice(&w3.to_le_bytes());
out
}
fn pack_v210_word_be(samples: [u16; 12]) -> [u8; 16] {
let mut out = [0u8; 16];
let w0 = (samples[0] as u32 & 0x3FF)
| ((samples[1] as u32 & 0x3FF) << 10)
| ((samples[2] as u32 & 0x3FF) << 20);
let w1 = (samples[3] as u32 & 0x3FF)
| ((samples[4] as u32 & 0x3FF) << 10)
| ((samples[5] as u32 & 0x3FF) << 20);
let w2 = (samples[6] as u32 & 0x3FF)
| ((samples[7] as u32 & 0x3FF) << 10)
| ((samples[8] as u32 & 0x3FF) << 20);
let w3 = (samples[9] as u32 & 0x3FF)
| ((samples[10] as u32 & 0x3FF) << 10)
| ((samples[11] as u32 & 0x3FF) << 20);
out[0..4].copy_from_slice(&w0.to_be_bytes());
out[4..8].copy_from_slice(&w1.to_be_bytes());
out[8..12].copy_from_slice(&w2.to_be_bytes());
out[12..16].copy_from_slice(&w3.to_be_bytes());
out
}
#[test]
fn scalar_v210_to_rgb_gray_is_gray() {
let word = pack_v210_word([512; 12]);
let mut rgb = [0u8; 6 * 3];
v210_to_rgb_or_rgba_row::<false, false>(&word, &mut rgb, 6, ColorMatrix::Bt709, true);
for px in rgb.chunks(3) {
assert!(px[0].abs_diff(128) <= 1);
assert_eq!(px[0], px[1]);
assert_eq!(px[1], px[2]);
}
}
#[test]
fn scalar_v210_to_rgba_gray_is_gray_with_opaque_alpha() {
let word = pack_v210_word([512; 12]);
let mut rgba = [0u8; 6 * 4];
v210_to_rgb_or_rgba_row::<true, false>(&word, &mut rgba, 6, ColorMatrix::Bt709, true);
for px in rgba.chunks(4) {
assert!(px[0].abs_diff(128) <= 1);
assert_eq!(px[3], 0xFF);
}
}
#[test]
fn scalar_v210_to_rgb_u16_gray_is_gray_native_depth() {
let word = pack_v210_word([512; 12]);
let mut rgb_u16 = [0u16; 6 * 3];
v210_to_rgb_u16_or_rgba_u16_row::<false, false>(
&word,
&mut rgb_u16,
6,
ColorMatrix::Bt709,
true,
);
for px in rgb_u16.chunks(3) {
assert!(px[0].abs_diff(512) <= 2);
assert_eq!(px[0], px[1]);
assert_eq!(px[1], px[2]);
}
}
#[test]
fn scalar_v210_to_rgba_u16_alpha_is_max() {
let word = pack_v210_word([512; 12]);
let mut rgba_u16 = [0u16; 6 * 4];
v210_to_rgb_u16_or_rgba_u16_row::<true, false>(
&word,
&mut rgba_u16,
6,
ColorMatrix::Bt709,
true,
);
for px in rgba_u16.chunks(4) {
assert_eq!(px[3], 1023, "alpha must be (1 << 10) - 1");
}
}
#[test]
fn scalar_v210_to_luma_extracts_y_bytes() {
let samples = [
100, 200, 100, 300, 100, 400, 100, 500, 100, 600, 100, 700, ];
let word = pack_v210_word(samples);
let mut luma = [0u8; 6];
v210_to_luma_row::<false>(&word, &mut luma, 6);
assert_eq!(luma[0], (200u16 >> 2) as u8);
assert_eq!(luma[1], (300u16 >> 2) as u8);
assert_eq!(luma[2], (400u16 >> 2) as u8);
assert_eq!(luma[3], (500u16 >> 2) as u8);
assert_eq!(luma[4], (600u16 >> 2) as u8);
assert_eq!(luma[5], (700u16 >> 2) as u8);
}
#[test]
fn scalar_v210_to_luma_u16_extracts_y_low_bit_packed() {
let samples = [100, 200, 100, 300, 100, 400, 100, 500, 100, 600, 100, 700];
let word = pack_v210_word(samples);
let mut luma = [0u16; 6];
v210_to_luma_u16_row::<false>(&word, &mut luma, 6);
assert_eq!(luma[0], 200);
assert_eq!(luma[1], 300);
assert_eq!(luma[2], 400);
assert_eq!(luma[3], 500);
assert_eq!(luma[4], 600);
assert_eq!(luma[5], 700);
}
#[test]
fn scalar_v210_handles_two_words_36_pixels() {
let samples = [512u16; 12];
let mut packed = std::vec::Vec::with_capacity(32);
packed.extend_from_slice(&pack_v210_word(samples));
packed.extend_from_slice(&pack_v210_word(samples));
let mut rgb = std::vec![0u8; 12 * 3];
v210_to_rgb_or_rgba_row::<false, false>(&packed, &mut rgb, 12, ColorMatrix::Bt709, true);
for px in rgb.chunks(3) {
assert!(px[0].abs_diff(128) <= 1);
}
}
fn check_partial_gray(width: usize) {
let total_words = width.div_ceil(6);
let mut packed = std::vec::Vec::with_capacity(total_words * 16);
for _ in 0..total_words {
packed.extend_from_slice(&pack_v210_word([512; 12]));
}
let mut rgb = std::vec![0u8; width * 3];
v210_to_rgb_or_rgba_row::<false, false>(&packed, &mut rgb, width, ColorMatrix::Bt709, true);
for px in rgb.chunks(3) {
assert!(px[0].abs_diff(128) <= 1, "width={width}: gray RGB diverged");
assert_eq!(px[0], px[1]);
}
let mut rgba = std::vec![0u8; width * 4];
v210_to_rgb_or_rgba_row::<true, false>(&packed, &mut rgba, width, ColorMatrix::Bt709, true);
for px in rgba.chunks(4) {
assert!(px[0].abs_diff(128) <= 1);
assert_eq!(px[3], 0xFF);
}
let mut rgb_u16 = std::vec![0u16; width * 3];
v210_to_rgb_u16_or_rgba_u16_row::<false, false>(
&packed,
&mut rgb_u16,
width,
ColorMatrix::Bt709,
true,
);
for px in rgb_u16.chunks(3) {
assert!(px[0].abs_diff(512) <= 2);
}
let mut luma = std::vec![0u8; width];
v210_to_luma_row::<false>(&packed, &mut luma, width);
for &y in &luma {
assert_eq!(y, 128);
}
let mut luma_u16 = std::vec![0u16; width];
v210_to_luma_u16_row::<false>(&packed, &mut luma_u16, width);
for &y in &luma_u16 {
assert_eq!(y, 512);
}
}
#[test]
fn scalar_v210_partial_word_width_2() {
check_partial_gray(2);
}
#[test]
fn scalar_v210_partial_word_width_4() {
check_partial_gray(4);
}
#[test]
fn scalar_v210_partial_word_width_8() {
check_partial_gray(8);
}
#[test]
fn scalar_v210_partial_word_width_10() {
check_partial_gray(10);
}
#[test]
fn scalar_v210_partial_word_width_14() {
check_partial_gray(14);
}
#[test]
fn scalar_v210_partial_word_width_16() {
check_partial_gray(16);
}
#[test]
fn scalar_v210_partial_word_width_1280_720p() {
check_partial_gray(1280);
}
#[test]
fn scalar_v210_partial_word_tail_only_uses_valid_pairs() {
let samples = [
512, 600, 512, 700, 999, 800, 999, 850, 999, 900, 999, 950, ];
let word = pack_v210_word(samples);
let mut luma = [0u8; 2];
v210_to_luma_row::<false>(&word, &mut luma, 2);
assert_eq!(luma[0], (600u16 >> 2) as u8);
assert_eq!(luma[1], (700u16 >> 2) as u8);
}
#[test]
fn scalar_v210_be_rgb_matches_le() {
let samples = [
100u16, 512, 400, 600, 200, 300, 500, 700, 150, 450, 350, 800,
];
let le_word = pack_v210_word(samples);
let be_word = pack_v210_word_be(samples);
let mut le_rgb = [0u8; 6 * 3];
let mut be_rgb = [0u8; 6 * 3];
v210_to_rgb_or_rgba_row::<false, false>(&le_word, &mut le_rgb, 6, ColorMatrix::Bt709, true);
v210_to_rgb_or_rgba_row::<false, true>(&be_word, &mut be_rgb, 6, ColorMatrix::Bt709, true);
assert_eq!(le_rgb, be_rgb, "BE rgb output must match LE");
}
#[test]
fn scalar_v210_be_rgb_u16_matches_le() {
let samples = [
100u16, 512, 400, 600, 200, 300, 500, 700, 150, 450, 350, 800,
];
let le_word = pack_v210_word(samples);
let be_word = pack_v210_word_be(samples);
let mut le_rgb = [0u16; 6 * 3];
let mut be_rgb = [0u16; 6 * 3];
v210_to_rgb_u16_or_rgba_u16_row::<false, false>(
&le_word,
&mut le_rgb,
6,
ColorMatrix::Bt709,
true,
);
v210_to_rgb_u16_or_rgba_u16_row::<false, true>(
&be_word,
&mut be_rgb,
6,
ColorMatrix::Bt709,
true,
);
assert_eq!(le_rgb, be_rgb, "BE rgb_u16 output must match LE");
}
#[test]
fn scalar_v210_be_luma_matches_le() {
let samples = [
100u16, 200, 100, 300, 100, 400, 100, 500, 100, 600, 100, 700,
];
let le_word = pack_v210_word(samples);
let be_word = pack_v210_word_be(samples);
let mut le_luma = [0u8; 6];
let mut be_luma = [0u8; 6];
v210_to_luma_row::<false>(&le_word, &mut le_luma, 6);
v210_to_luma_row::<true>(&be_word, &mut be_luma, 6);
assert_eq!(le_luma, be_luma, "BE luma output must match LE");
}
#[test]
fn scalar_v210_be_luma_u16_matches_le() {
let samples = [
100u16, 200, 100, 300, 100, 400, 100, 500, 100, 600, 100, 700,
];
let le_word = pack_v210_word(samples);
let be_word = pack_v210_word_be(samples);
let mut le_luma = [0u16; 6];
let mut be_luma = [0u16; 6];
v210_to_luma_u16_row::<false>(&le_word, &mut le_luma, 6);
v210_to_luma_u16_row::<true>(&be_word, &mut be_luma, 6);
assert_eq!(le_luma, be_luma, "BE luma_u16 output must match LE");
}
}