// SPDX-License-Identifier: AGPL-3.0-or-later #[derive(Debug, Clone, Copy)] pub struct Nv12Layout { pub width: u32, pub height: u32, pub stride_y: u32, pub stride_uv: u32, } impl Nv12Layout { pub fn packed_size(&self) -> Option { if self.width == 0 || self.height == 0 || self.height % 2 != 0 { return None; } let w = self.width as usize; let h = self.height as usize; let y_bytes = w.checked_mul(h)?; let uv_bytes = w.checked_mul(h / 2)?; y_bytes.checked_add(uv_bytes) } pub fn packed_stride_y(&self) -> u32 { self.width } pub fn packed_stride_uv(&self) -> u32 { self.width } } pub fn pack_nv12(layout: Nv12Layout, y_plane: &[u8], uv_plane: &[u8], dst: &mut [u8]) -> bool { let Some(total) = layout.packed_size() else { return false; }; if dst.len() != total { return false; } let w = layout.width as usize; let h = layout.height as usize; let stride_y = layout.stride_y as usize; let stride_uv = layout.stride_uv as usize; if stride_y < w || stride_uv < w { return false; } if y_plane.len() < stride_y * h { return false; } if uv_plane.len() < stride_uv * (h / 2) { return false; } let y_bytes = w * h; let uv_bytes = w * (h / 2); if stride_y == w && stride_uv == w { dst[..y_bytes].copy_from_slice(&y_plane[..y_bytes]); dst[y_bytes..y_bytes + uv_bytes].copy_from_slice(&uv_plane[..uv_bytes]); } else { let (dst_y, dst_uv) = dst.split_at_mut(y_bytes); for row in 0..h { let src_off = row * stride_y; let dst_off = row * w; dst_y[dst_off..dst_off + w].copy_from_slice(&y_plane[src_off..src_off + w]); } for row in 0..h / 2 { let src_off = row * stride_uv; let dst_off = row * w; dst_uv[dst_off..dst_off + w].copy_from_slice(&uv_plane[src_off..src_off + w]); } } true } #[cfg(target_os = "linux")] fn bgra_to_nv12_dcp(w: u32, h: u32, bgra: &[u8], bgra_stride: usize, dst: &mut [u8]) -> bool { use dcv_color_primitives as dcp; let src_format = dcp::ImageFormat { pixel_format: dcp::PixelFormat::Bgra, color_space: dcp::ColorSpace::Rgb, num_planes: 1, }; let dst_format = dcp::ImageFormat { pixel_format: dcp::PixelFormat::Nv12, color_space: dcp::ColorSpace::Bt601, num_planes: 2, }; let y_bytes = (w as usize) * (h as usize); let (dst_y, dst_uv) = dst.split_at_mut(y_bytes); dcp::convert_image( w, h, &src_format, Some(&[bgra_stride]), &[bgra], &dst_format, Some(&[w as usize, w as usize]), &mut [dst_y, dst_uv], ) .is_ok() } fn flip_nv12_vertical(w: usize, h: usize, dst: &mut [u8]) { assert!(w > 0); assert!(h % 2 == 0); let y_bytes = w * h; let uv_bytes = w * (h / 2); assert!(dst.len() >= y_bytes + uv_bytes); let (y_plane, rest) = dst.split_at_mut(y_bytes); flip_plane_rows(y_plane, w, h); flip_plane_rows(&mut rest[..uv_bytes], w, h / 2); } fn flip_plane_rows(plane: &mut [u8], row_bytes: usize, rows: usize) { assert!(row_bytes > 0); assert!(plane.len() >= row_bytes * rows); for row in 0..rows / 2 { let top_start = row * row_bytes; let bottom_start = (rows - 1 - row) * row_bytes; let (head, tail) = plane.split_at_mut(bottom_start); head[top_start..top_start + row_bytes].swap_with_slice(&mut tail[..row_bytes]); } } fn bgra_to_nv12_scalar( w: usize, h: usize, bgra: &[u8], bgra_row: usize, dst: &mut [u8], flip: bool, ) -> bool { let y_bytes = w * h; let (dst_y, dst_uv) = dst.split_at_mut(y_bytes); for row in 0..h { let dst_row_index = if flip { h - 1 - row } else { row }; let src_row = &bgra[row * bgra_row..row * bgra_row + w * 4]; let dst_row = &mut dst_y[dst_row_index * w..dst_row_index * w + w]; for col in 0..w { let b = src_row[col * 4] as i32; let g = src_row[col * 4 + 1] as i32; let r = src_row[col * 4 + 2] as i32; let y = (66 * r + 129 * g + 25 * b + 128) >> 8; dst_row[col] = (y + 16).clamp(0, 255) as u8; } } for row in 0..h / 2 { let dst_row_index = if flip { h / 2 - 1 - row } else { row }; let r0 = &bgra[(row * 2) * bgra_row..(row * 2) * bgra_row + w * 4]; let r1 = &bgra[(row * 2 + 1) * bgra_row..(row * 2 + 1) * bgra_row + w * 4]; let dst_row = &mut dst_uv[dst_row_index * w..dst_row_index * w + w]; for col in 0..w / 2 { let cx0 = col * 2 * 4; let cx1 = (col * 2 + 1) * 4; let b = (r0[cx0] as i32 + r0[cx1] as i32 + r1[cx0] as i32 + r1[cx1] as i32) >> 2; let g = (r0[cx0 + 1] as i32 + r0[cx1 + 1] as i32 + r1[cx0 + 1] as i32 + r1[cx1 + 1] as i32) >> 2; let r = (r0[cx0 + 2] as i32 + r0[cx1 + 2] as i32 + r1[cx0 + 2] as i32 + r1[cx1 + 2] as i32) >> 2; let u = ((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128; let v = ((112 * r - 94 * g - 18 * b + 128) >> 8) + 128; dst_row[col * 2] = u.clamp(0, 255) as u8; dst_row[col * 2 + 1] = v.clamp(0, 255) as u8; } } true } pub fn bgra_to_nv12( layout: Nv12Layout, bgra: &[u8], bgra_stride: u32, dst: &mut [u8], flip: bool, ) -> bool { let Some(total) = layout.packed_size() else { return false; }; if dst.len() < total { return false; } let w = layout.width as usize; let h = layout.height as usize; let bgra_row = bgra_stride as usize; if bgra_row < w.checked_mul(4).unwrap_or(usize::MAX) { return false; } if bgra.len() < bgra_row.checked_mul(h).unwrap_or(usize::MAX) { return false; } #[cfg(target_os = "linux")] if bgra_to_nv12_dcp(layout.width, layout.height, bgra, bgra_row, dst) { if flip { flip_nv12_vertical(w, h, dst); } return true; } bgra_to_nv12_scalar(w, h, bgra, bgra_row, dst, flip) } #[cfg(test)] mod tests { use super::*; fn make_y(width: usize, height: usize, stride: usize) -> Vec { let mut v = vec![0u8; stride * height]; for row in 0..height { for col in 0..width { v[row * stride + col] = ((row * width + col) % 251) as u8; } } v } fn make_uv(width: usize, height_half: usize, stride: usize) -> Vec { let mut v = vec![0u8; stride * height_half]; for row in 0..height_half { for col in 0..width { v[row * stride + col] = ((row * width + col + 7) % 241) as u8; } } v } #[test] fn packed_size_rejects_odd_height() { let layout = Nv12Layout { width: 16, height: 15, stride_y: 16, stride_uv: 16, }; assert!(layout.packed_size().is_none()); } #[test] fn packed_size_rejects_zero_dims() { let layout = Nv12Layout { width: 0, height: 4, stride_y: 0, stride_uv: 0, }; assert!(layout.packed_size().is_none()); } #[test] fn packed_size_matches_yuv420_layout() { let layout = Nv12Layout { width: 1920, height: 1080, stride_y: 1920, stride_uv: 1920, }; assert_eq!(Some(1920 * 1080 * 3 / 2), layout.packed_size()); } #[test] fn pack_nv12_strips_row_padding() { let layout = Nv12Layout { width: 8, height: 4, stride_y: 16, stride_uv: 16, }; let y = make_y(8, 4, 16); let uv = make_uv(8, 2, 16); let mut dst = vec![0u8; layout.packed_size().unwrap()]; assert!(pack_nv12(layout, &y, &uv, &mut dst)); for row in 0..4 { for col in 0..8 { assert_eq!(dst[row * 8 + col], ((row * 8 + col) % 251) as u8); } } for row in 0..2 { for col in 0..8 { let off = 8 * 4 + row * 8 + col; assert_eq!(dst[off], ((row * 8 + col + 7) % 241) as u8); } } } #[test] fn pack_nv12_zero_padding_is_identity() { let layout = Nv12Layout { width: 4, height: 2, stride_y: 4, stride_uv: 4, }; let y = vec![1, 2, 3, 4, 5, 6, 7, 8]; let uv = vec![10, 11, 12, 13]; let mut dst = vec![0u8; layout.packed_size().unwrap()]; assert!(pack_nv12(layout, &y, &uv, &mut dst)); assert_eq!(dst, vec![1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13]); } #[test] fn pack_nv12_rejects_short_source() { let layout = Nv12Layout { width: 4, height: 2, stride_y: 4, stride_uv: 4, }; let y = vec![1, 2, 3]; let uv = vec![10, 11, 12, 13]; let mut dst = vec![0u8; layout.packed_size().unwrap()]; assert!(!pack_nv12(layout, &y, &uv, &mut dst)); } #[test] fn pack_nv12_rejects_undersized_stride() { let layout = Nv12Layout { width: 8, height: 2, stride_y: 4, stride_uv: 4, }; let y = vec![0; 8]; let uv = vec![0; 4]; let mut dst = vec![0u8; 24]; assert!(!pack_nv12(layout, &y, &uv, &mut dst)); } #[test] fn pack_nv12_rejects_wrong_dst_size() { let layout = Nv12Layout { width: 4, height: 2, stride_y: 4, stride_uv: 4, }; let y = vec![0; 8]; let uv = vec![0; 4]; let mut dst = vec![0u8; 11]; assert!(!pack_nv12(layout, &y, &uv, &mut dst)); } #[test] fn bgra_to_nv12_solid_black() { let layout = Nv12Layout { width: 4, height: 2, stride_y: 4, stride_uv: 4, }; let bgra = vec![0u8; 4 * 4 * 2]; let mut dst = vec![0u8; layout.packed_size().unwrap()]; assert!(bgra_to_nv12(layout, &bgra, 16, &mut dst, false)); for byte in &dst[..8] { assert_eq!(*byte, 16); } for chunk in dst[8..].chunks_exact(2) { assert_eq!(chunk[0], 128); assert_eq!(chunk[1], 128); } } #[test] fn bgra_to_nv12_solid_white_is_in_range() { let layout = Nv12Layout { width: 4, height: 2, stride_y: 4, stride_uv: 4, }; let bgra = vec![255u8; 4 * 4 * 2]; let mut dst = vec![0u8; layout.packed_size().unwrap()]; assert!(bgra_to_nv12(layout, &bgra, 16, &mut dst, false)); for byte in &dst[..8] { assert!(*byte >= 230 && *byte <= 240, "luma out of range: {byte}"); } for chunk in dst[8..].chunks_exact(2) { assert!( chunk[0] >= 124 && chunk[0] <= 132, "U out of range: {}", chunk[0] ); assert!( chunk[1] >= 124 && chunk[1] <= 132, "V out of range: {}", chunk[1] ); } } #[test] fn bgra_to_nv12_rejects_short_stride() { let layout = Nv12Layout { width: 4, height: 2, stride_y: 4, stride_uv: 4, }; let bgra = vec![0u8; 8]; let mut dst = vec![0u8; layout.packed_size().unwrap()]; assert!(!bgra_to_nv12(layout, &bgra, 4, &mut dst, false)); } fn deterministic_bgra_frame(w: usize, h: usize, seed: u64) -> Vec { let mut state = seed; let mut v = vec![0u8; w * h * 4]; for byte in v.iter_mut() { state = state .wrapping_mul(6364136223846793005) .wrapping_add(1442695040888963407); *byte = (state >> 56) as u8; } v } fn row_reversed(bgra: &[u8], w: usize, h: usize) -> Vec { let row_bytes = w * 4; assert_eq!(bgra.len(), row_bytes * h); let mut reversed = vec![0u8; bgra.len()]; for row in 0..h { let src = (h - 1 - row) * row_bytes; reversed[row * row_bytes..(row + 1) * row_bytes] .copy_from_slice(&bgra[src..src + row_bytes]); } reversed } #[test] fn bgra_to_nv12_flip_matches_pre_reversed_rows() { for (w, h, seed) in [(8usize, 4usize, 1u64), (16, 8, 2), (64, 32, 3), (12, 6, 4)] { let layout = Nv12Layout { width: w as u32, height: h as u32, stride_y: w as u32, stride_uv: w as u32, }; let bgra = deterministic_bgra_frame(w, h, seed); let reversed = row_reversed(&bgra, w, h); let total = layout.packed_size().expect("even dims"); let mut flipped = vec![0u8; total]; let mut reference = vec![0u8; total]; assert!(bgra_to_nv12( layout, &bgra, (w * 4) as u32, &mut flipped, true )); assert!(bgra_to_nv12( layout, &reversed, (w * 4) as u32, &mut reference, false )); assert_eq!(flipped, reference, "mismatch at {w}x{h} seed {seed}"); } } #[test] fn bgra_to_nv12_flip_moves_top_row_luma_to_bottom() { let layout = Nv12Layout { width: 4, height: 4, stride_y: 4, stride_uv: 4, }; let mut bgra = vec![0u8; 4 * 4 * 4]; bgra[..16].fill(255); let mut dst = vec![0u8; layout.packed_size().unwrap()]; assert!(bgra_to_nv12(layout, &bgra, 16, &mut dst, true)); let y = &dst[..16]; assert!(y[12] > 200, "bottom row should hold the white luma: {y:?}"); assert!(y[0] < 32, "top row should hold the black luma: {y:?}"); } #[test] fn bgra_to_nv12_unflipped_path_is_unchanged_by_flip_support() { let layout = Nv12Layout { width: 8, height: 4, stride_y: 8, stride_uv: 8, }; let bgra = deterministic_bgra_frame(8, 4, 9); let total = layout.packed_size().unwrap(); let mut first = vec![0u8; total]; let mut second = vec![0u8; total]; assert!(bgra_to_nv12(layout, &bgra, 32, &mut first, false)); assert!(bgra_to_nv12(layout, &bgra, 32, &mut second, false)); assert_eq!(first, second); } }