#![allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "reproduces the C uint8_t/int16_t wrapping and clip semantics of \
the reference decoder: VP8Clip8 stores an already-clamped value \
into a byte, and the packed-UV halves are truncated to u8 exactly \
as libwebp's VP8YuvToRgba does"
)]
#![allow(
clippy::similar_names,
reason = "near_u/near_v/far_u/far_v mirror the reference sample roles \
(top/cur, tl/l) and keep the interpolation traceable to \
UpsampleRgbaLinePair_C"
)]
use crate::lossy::prelude::*;
use crate::lossy::work::work;
const fn mult_hi(v: i32, coeff: i32) -> i32 {
(v * coeff) >> 8
}
fn vp8_clip8(v: i32) -> u8 {
(v.clamp(0, (256 << 6) - 1) >> 6) as u8
}
fn vp8_yuv_to_r(y: i32, v: i32) -> u8 {
vp8_clip8(mult_hi(y, 19077) + mult_hi(v, 26149) - 14234)
}
fn vp8_yuv_to_g(y: i32, u: i32, v: i32) -> u8 {
vp8_clip8(mult_hi(y, 19077) - mult_hi(u, 6419) - mult_hi(v, 13320) + 8708)
}
fn vp8_yuv_to_b(y: i32, u: i32) -> u8 {
vp8_clip8(mult_hi(y, 19077) + mult_hi(u, 33050) - 17685)
}
fn yuv_to_rgba(y: i32, u: i32, v: i32, dst: &mut [u8]) {
dst[0] = vp8_yuv_to_r(y, v);
dst[1] = vp8_yuv_to_g(y, u, v);
dst[2] = vp8_yuv_to_b(y, u);
dst[3] = 0xff;
}
fn load_uv(u: u8, v: u8) -> u32 {
u32::from(u) | (u32::from(v) << 16)
}
fn emit_pixel(y: u8, uv: u32, dst: &mut [u8], col: usize) {
let u = i32::from((uv & 0xff) as u8);
let v = i32::from((uv >> 16) as u8);
yuv_to_rgba(i32::from(y), u, v, &mut dst[col * 4..col * 4 + 4]);
}
fn upsample_one_row(
y_row: &[u8],
dst: &mut [u8],
near_u: &[u8],
near_v: &[u8],
far_u: &[u8],
far_v: &[u8],
len: usize,
) {
work!(UpsampleRow);
let last_pixel_pair = (len - 1) >> 1;
let mut near_l = load_uv(near_u[0], near_v[0]);
let mut far_l = load_uv(far_u[0], far_v[0]);
let first = (3 * near_l + far_l + 0x0002_0002) >> 2;
emit_pixel(y_row[0], first, dst, 0);
for x in 1..=last_pixel_pair {
let near_r = load_uv(near_u[x], near_v[x]);
let far_r = load_uv(far_u[x], far_v[x]);
let d_l = (near_l + 3 * near_r + 3 * far_l + far_r + 0x0008_0008) >> 3;
let d_r = (3 * near_l + near_r + far_l + 3 * far_r + 0x0008_0008) >> 3;
emit_pixel(y_row[2 * x - 1], (d_l + near_l) >> 1, dst, 2 * x - 1);
emit_pixel(y_row[2 * x], (d_r + near_r) >> 1, dst, 2 * x);
near_l = near_r;
far_l = far_r;
}
if len & 1 == 0 {
let last = (3 * near_l + far_l + 0x0002_0002) >> 2;
emit_pixel(y_row[len - 1], last, dst, len - 1);
}
}
pub(crate) struct Yuv420Ref<'a> {
pub y: &'a [u8],
pub y_stride: usize,
pub u: &'a [u8],
pub v: &'a [u8],
pub uv_stride: usize,
}
pub(crate) fn upsample_output_row(
src: &Yuv420Ref<'_>,
width: usize,
height: usize,
out_y: usize,
out: &mut [u8],
) {
let chroma_rows = height.div_ceil(2);
let (near, far) = if out_y == 0 {
(0, 0)
} else if out_y & 1 == 1 {
let near = (out_y - 1) / 2;
let far = out_y.div_ceil(2);
(near, if far < chroma_rows { far } else { near })
} else {
(out_y / 2, out_y / 2 - 1)
};
let (near_off, far_off) = (near * src.uv_stride, far * src.uv_stride);
upsample_one_row(
&src.y[out_y * src.y_stride..],
out,
&src.u[near_off..],
&src.v[near_off..],
&src.u[far_off..],
&src.v[far_off..],
width,
);
}
pub(crate) fn yuv420_to_rgba(src: &Yuv420Ref<'_>, width: usize, height: usize) -> Vec<u8> {
let out_stride = width * 4;
let mut out = vec![0u8; out_stride * height];
if width == 0 || height == 0 {
return out;
}
emit_rows(src, width, height, out_stride, &mut out);
out
}
fn emit_rows_serial(
src: &Yuv420Ref<'_>,
width: usize,
height: usize,
out_stride: usize,
out: &mut [u8],
) {
for out_y in 0..height {
let (row_start, row_end) = (out_y * out_stride, (out_y + 1) * out_stride);
upsample_output_row(src, width, height, out_y, &mut out[row_start..row_end]);
}
}
#[cfg(not(feature = "rayon"))]
fn emit_rows(src: &Yuv420Ref<'_>, width: usize, height: usize, out_stride: usize, out: &mut [u8]) {
emit_rows_serial(src, width, height, out_stride, out);
}
#[cfg(feature = "rayon")]
fn emit_rows(src: &Yuv420Ref<'_>, width: usize, height: usize, out_stride: usize, out: &mut [u8]) {
use rayon::prelude::*;
const PAR_MIN_PIXELS: usize = 1 << 16;
if width.saturating_mul(height) < PAR_MIN_PIXELS {
emit_rows_serial(src, width, height, out_stride, out);
return;
}
out.par_chunks_mut(out_stride)
.enumerate()
.for_each(|(out_y, row)| upsample_output_row(src, width, height, out_y, row));
}
#[cfg(test)]
mod tests {
use super::{Yuv420Ref, yuv420_to_rgba};
use crate::lossy::prelude::*;
fn constant_frame(
width: usize,
height: usize,
yv: u8,
uv: u8,
vv: u8,
) -> (Vec<u8>, Vec<u8>, Vec<u8>, usize, usize) {
let uv_w = width.div_ceil(2);
let uv_h = height.div_ceil(2);
let y = vec![yv; width * height];
let u = vec![uv; uv_w * uv_h];
let v = vec![vv; uv_w * uv_h];
(y, u, v, width, uv_w)
}
fn assert_constant(width: usize, height: usize, yv: u8, uv: u8, vv: u8, expected: [u8; 4]) {
let (y, u, v, w, uv_w) = constant_frame(width, height, yv, uv, vv);
let out = yuv420_to_rgba(
&Yuv420Ref {
y: &y,
y_stride: w,
u: &u,
v: &v,
uv_stride: uv_w,
},
width,
height,
);
assert_eq!(out.len(), width * height * 4);
for (i, px) in out.chunks_exact(4).enumerate() {
assert_eq!(px, expected, "pixel {i} of {width}x{height}");
}
}
#[cfg(feature = "rayon")]
fn ramp(n: usize, seed: u8, mul: u8) -> Vec<u8> {
let mut s = seed;
(0..n)
.map(|_| {
s = s.wrapping_mul(mul).wrapping_add(0x2b);
s
})
.collect()
}
#[cfg(feature = "rayon")]
#[test]
fn rayon_row_parallel_matches_serial_byte_for_byte() {
use super::emit_rows_serial;
let (width, height) = (260usize, 258usize);
let uv_w = width.div_ceil(2);
let uv_h = height.div_ceil(2);
let y = ramp(width * height, 1, 7);
let u = ramp(uv_w * uv_h, 99, 5);
let v = ramp(uv_w * uv_h, 200, 11);
let src = Yuv420Ref {
y: &y,
y_stride: width,
u: &u,
v: &v,
uv_stride: uv_w,
};
let parallel = yuv420_to_rgba(&src, width, height);
let mut serial = vec![0u8; width * 4 * height];
emit_rows_serial(&src, width, height, width * 4, &mut serial);
assert_eq!(
parallel, serial,
"rayon emitter must match serial byte-for-byte"
);
}
#[test]
fn zero_width_nonzero_height_returns_empty_without_panic() {
let (y, u, v, w, uv_w) = constant_frame(0, 2, 0, 0, 0);
let out = yuv420_to_rgba(
&Yuv420Ref {
y: &y,
y_stride: w,
u: &u,
v: &v,
uv_stride: uv_w,
},
0,
2,
);
assert!(out.is_empty(), "zero-width frame must produce no bytes");
}
#[test]
fn all_zero_yuv_is_green() {
for &(w, h) in &[(1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (7, 3)] {
assert_constant(w, h, 0, 0, 0, [0, 136, 0, 255]);
}
}
#[test]
fn neutral_gray_is_130() {
for &(w, h) in &[(1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 4), (3, 6)] {
assert_constant(w, h, 128, 128, 128, [130, 130, 130, 255]);
}
}
#[test]
fn full_luma_neutral_chroma_is_white() {
assert_constant(4, 4, 255, 128, 128, [255, 255, 255, 255]);
}
#[test]
fn vertical_gradient_orients_near_far_correctly() {
let width = 2;
let height = 4;
let y = vec![255u8; width * height];
let u = vec![0u8, 100u8]; let v = vec![128u8, 128u8];
let out = yuv420_to_rgba(
&Yuv420Ref {
y: &y,
y_stride: width,
u: &u,
v: &v,
uv_stride: 1,
},
width,
height,
);
let expected_b = [20u8, 71u8, 171u8, 222u8];
for (row, &b) in expected_b.iter().enumerate() {
for col in 0..width {
let px = &out[row * width * 4 + col * 4..row * width * 4 + col * 4 + 4];
assert_eq!(px, [255, 255, b, 255], "row {row} col {col}");
}
}
}
#[test]
fn horizontal_gradient_blends_interior_pairs_and_mirrors_the_edge() {
let width = 4;
let height = 1;
let y = vec![255u8; width * height];
let u = vec![0u8, 100u8]; let v = vec![128u8, 128u8];
let out = yuv420_to_rgba(
&Yuv420Ref {
y: &y,
y_stride: width,
u: &u,
v: &v,
uv_stride: 2,
},
width,
height,
);
let expected_b = [20u8, 71u8, 171u8, 222u8];
for (col, &b) in expected_b.iter().enumerate() {
let px = &out[col * 4..col * 4 + 4];
assert_eq!(px, [255, 255, b, 255], "even-width col {col}");
}
let y3 = vec![255u8; 3];
let out3 = yuv420_to_rgba(
&Yuv420Ref {
y: &y3,
y_stride: 3,
u: &u,
v: &v,
uv_stride: 2,
},
3,
1,
);
let expected_b3 = [20u8, 71u8, 171u8];
for (col, &b) in expected_b3.iter().enumerate() {
let px = &out3[col * 4..col * 4 + 4];
assert_eq!(px, [255, 255, b, 255], "odd-width col {col}");
}
}
}