From c6dd96e32d10773bf3f3986636b2d9da42e9ed0e Mon Sep 17 00:00:00 2001 From: uqio <276879906+uqio@users.noreply.github.com> Date: Mon, 27 Apr 2026 11:17:07 +1200 Subject: [PATCH 1/2] update --- src/sinker/mixed/planar_8bit.rs | 6 +- src/sinker/mixed/subsampled_4_2_2_high_bit.rs | 871 +++++++++++++++++- src/sinker/mixed/tests.rs | 205 +++++ 3 files changed, 1053 insertions(+), 29 deletions(-) diff --git a/src/sinker/mixed/planar_8bit.rs b/src/sinker/mixed/planar_8bit.rs index 10224dd..41d4707 100644 --- a/src/sinker/mixed/planar_8bit.rs +++ b/src/sinker/mixed/planar_8bit.rs @@ -31,12 +31,12 @@ impl<'a> MixedSinker<'a, Yuv420p> { /// /// ```compile_fail /// // Attaching RGBA to a sink that doesn't write it is rejected - /// // at compile time. Yuv422p10 (10‑bit 4:2:2 planar) has not yet + /// // at compile time. Yuv444p10 (10‑bit 4:4:4 planar) has not yet /// // been wired for RGBA — once a future tranche lands it the /// // negative example here moves to the next not‑yet‑wired format. - /// use colconv::{sinker::MixedSinker, yuv::Yuv422p10}; + /// use colconv::{sinker::MixedSinker, yuv::Yuv444p10}; /// let mut buf = vec![0u8; 16 * 8 * 4]; - /// let _ = MixedSinker::::new(16, 8).with_rgba(&mut buf); + /// let _ = MixedSinker::::new(16, 8).with_rgba(&mut buf); /// ``` #[cfg_attr(not(tarpaulin), inline(always))] pub fn with_rgba(mut self, buf: &'a mut [u8]) -> Result { diff --git a/src/sinker/mixed/subsampled_4_2_2_high_bit.rs b/src/sinker/mixed/subsampled_4_2_2_high_bit.rs index c63085d..0cba870 100644 --- a/src/sinker/mixed/subsampled_4_2_2_high_bit.rs +++ b/src/sinker/mixed/subsampled_4_2_2_high_bit.rs @@ -2,6 +2,7 @@ use super::{ MixedSinker, MixedSinkerError, RowSlice, check_dimensions_match, rgb_row_buf_or_scratch, + rgba_plane_row_slice, rgba_u16_plane_row_slice, }; use crate::{PixelSink, row::*, yuv::*}; @@ -31,6 +32,56 @@ impl<'a> MixedSinker<'a, Yuv422p9> { self.rgb_u16 = Some(buf); Ok(self) } + + /// Attaches a packed **8‑bit** RGBA output buffer. The 9‑bit YUV + /// source is converted to 8‑bit RGBA via the same `BITS = 9` Q15 + /// kernel family used by [`Self::with_rgb`]; the fourth byte per + /// pixel is alpha = `0xFF` (Yuv422p9 has no alpha plane). + /// + /// Returns `Err(RgbaBufferTooShort)` if + /// `buf.len() < width × height × 4`, or `Err(GeometryOverflow)` on + /// 32‑bit targets when the product overflows. + #[cfg_attr(not(tarpaulin), inline(always))] + pub fn with_rgba(mut self, buf: &'a mut [u8]) -> Result { + self.set_rgba(buf)?; + Ok(self) + } + /// In-place variant of [`with_rgba`](Self::with_rgba). + #[cfg_attr(not(tarpaulin), inline(always))] + pub fn set_rgba(&mut self, buf: &'a mut [u8]) -> Result<&mut Self, MixedSinkerError> { + let expected = self.frame_bytes(4)?; + if buf.len() < expected { + return Err(MixedSinkerError::RgbaBufferTooShort { + expected, + actual: buf.len(), + }); + } + self.rgba = Some(buf); + Ok(self) + } + + /// Attaches a packed **`u16`** RGBA output buffer. 9‑bit low‑packed + /// (`(1 << 9) - 1 = 511` max). Length is measured in `u16` + /// **elements** (`width × height × 4`). Alpha element is + /// `(1 << 9) - 1`. + #[cfg_attr(not(tarpaulin), inline(always))] + pub fn with_rgba_u16(mut self, buf: &'a mut [u16]) -> Result { + self.set_rgba_u16(buf)?; + Ok(self) + } + /// In-place variant of [`with_rgba_u16`](Self::with_rgba_u16). + #[cfg_attr(not(tarpaulin), inline(always))] + pub fn set_rgba_u16(&mut self, buf: &'a mut [u16]) -> Result<&mut Self, MixedSinkerError> { + let expected = self.frame_bytes(4)?; + if buf.len() < expected { + return Err(MixedSinkerError::RgbaU16BufferTooShort { + expected, + actual: buf.len(), + }); + } + self.rgba_u16 = Some(buf); + Ok(self) + } } impl Yuv422p9Sink for MixedSinker<'_, Yuv422p9> {} @@ -90,6 +141,8 @@ impl PixelSink for MixedSinker<'_, Yuv422p9> { let Self { rgb, rgb_u16, + rgba, + rgba_u16, luma, hsv, rgb_scratch, @@ -105,7 +158,26 @@ impl PixelSink for MixedSinker<'_, Yuv422p9> { } } - if let Some(buf) = rgb_u16.as_deref_mut() { + // ===== u16 RGB / RGBA path (Strategy A) ===== + let want_rgb_u16 = rgb_u16.is_some(); + let want_rgba_u16 = rgba_u16.is_some(); + + if want_rgba_u16 && !want_rgb_u16 { + let rgba_u16_buf = rgba_u16.as_deref_mut().unwrap(); + let rgba_u16_row = + rgba_u16_plane_row_slice(rgba_u16_buf, one_plane_start, one_plane_end, w, h)?; + yuv420p9_to_rgba_u16_row( + row.y(), + row.u_half(), + row.v_half(), + rgba_u16_row, + w, + row.matrix(), + row.full_range(), + use_simd, + ); + } else if want_rgb_u16 { + let rgb_u16_buf = rgb_u16.as_deref_mut().unwrap(); let rgb_plane_end = one_plane_end .checked_mul(3) @@ -115,19 +187,47 @@ impl PixelSink for MixedSinker<'_, Yuv422p9> { channels: 3, })?; let rgb_plane_start = one_plane_start * 3; + let rgb_u16_row = &mut rgb_u16_buf[rgb_plane_start..rgb_plane_end]; yuv420p9_to_rgb_u16_row( row.y(), row.u_half(), row.v_half(), - &mut buf[rgb_plane_start..rgb_plane_end], + rgb_u16_row, w, row.matrix(), row.full_range(), use_simd, ); + if want_rgba_u16 { + let rgba_u16_buf = rgba_u16.as_deref_mut().unwrap(); + let rgba_u16_row = + rgba_u16_plane_row_slice(rgba_u16_buf, one_plane_start, one_plane_end, w, h)?; + expand_rgb_u16_to_rgba_u16_row::(rgb_u16_row, rgba_u16_row, w); + } } - if rgb.is_none() && hsv.is_none() { + // ===== u8 RGB / RGBA / HSV path (Strategy A) ===== + let want_rgba = rgba.is_some(); + let want_hsv = hsv.is_some(); + let need_rgb_kernel = rgb.is_some() || want_hsv; + + if want_rgba && !need_rgb_kernel { + let rgba_buf = rgba.as_deref_mut().unwrap(); + let rgba_row = rgba_plane_row_slice(rgba_buf, one_plane_start, one_plane_end, w, h)?; + yuv420p9_to_rgba_row( + row.y(), + row.u_half(), + row.v_half(), + rgba_row, + w, + row.matrix(), + row.full_range(), + use_simd, + ); + return Ok(()); + } + + if !need_rgb_kernel { return Ok(()); } @@ -161,6 +261,12 @@ impl PixelSink for MixedSinker<'_, Yuv422p9> { use_simd, ); } + + if let Some(buf) = rgba.as_deref_mut() { + let rgba_row = rgba_plane_row_slice(buf, one_plane_start, one_plane_end, w, h)?; + expand_rgb_to_rgba_row(rgb_row, rgba_row, w); + } + Ok(()) } } @@ -194,6 +300,51 @@ impl<'a> MixedSinker<'a, Yuv422p10> { self.rgb_u16 = Some(buf); Ok(self) } + + /// Attaches a packed **8‑bit** RGBA output buffer. The 10‑bit YUV + /// source is converted to 8‑bit RGBA via the `BITS = 10` Q15 kernel + /// family; alpha = `0xFF` (Yuv422p10 has no alpha plane). + #[cfg_attr(not(tarpaulin), inline(always))] + pub fn with_rgba(mut self, buf: &'a mut [u8]) -> Result { + self.set_rgba(buf)?; + Ok(self) + } + /// In-place variant of [`with_rgba`](Self::with_rgba). + #[cfg_attr(not(tarpaulin), inline(always))] + pub fn set_rgba(&mut self, buf: &'a mut [u8]) -> Result<&mut Self, MixedSinkerError> { + let expected = self.frame_bytes(4)?; + if buf.len() < expected { + return Err(MixedSinkerError::RgbaBufferTooShort { + expected, + actual: buf.len(), + }); + } + self.rgba = Some(buf); + Ok(self) + } + + /// Attaches a packed **`u16`** RGBA output buffer. 10‑bit + /// low‑packed (`(1 << 10) - 1 = 1023` max). Length is measured in + /// `u16` **elements** (`width × height × 4`). Alpha element is + /// `(1 << 10) - 1`. + #[cfg_attr(not(tarpaulin), inline(always))] + pub fn with_rgba_u16(mut self, buf: &'a mut [u16]) -> Result { + self.set_rgba_u16(buf)?; + Ok(self) + } + /// In-place variant of [`with_rgba_u16`](Self::with_rgba_u16). + #[cfg_attr(not(tarpaulin), inline(always))] + pub fn set_rgba_u16(&mut self, buf: &'a mut [u16]) -> Result<&mut Self, MixedSinkerError> { + let expected = self.frame_bytes(4)?; + if buf.len() < expected { + return Err(MixedSinkerError::RgbaU16BufferTooShort { + expected, + actual: buf.len(), + }); + } + self.rgba_u16 = Some(buf); + Ok(self) + } } impl Yuv422p10Sink for MixedSinker<'_, Yuv422p10> {} @@ -253,6 +404,8 @@ impl PixelSink for MixedSinker<'_, Yuv422p10> { let Self { rgb, rgb_u16, + rgba, + rgba_u16, luma, hsv, rgb_scratch, @@ -268,7 +421,26 @@ impl PixelSink for MixedSinker<'_, Yuv422p10> { } } - if let Some(buf) = rgb_u16.as_deref_mut() { + // ===== u16 RGB / RGBA path (Strategy A) ===== + let want_rgb_u16 = rgb_u16.is_some(); + let want_rgba_u16 = rgba_u16.is_some(); + + if want_rgba_u16 && !want_rgb_u16 { + let rgba_u16_buf = rgba_u16.as_deref_mut().unwrap(); + let rgba_u16_row = + rgba_u16_plane_row_slice(rgba_u16_buf, one_plane_start, one_plane_end, w, h)?; + yuv420p10_to_rgba_u16_row( + row.y(), + row.u_half(), + row.v_half(), + rgba_u16_row, + w, + row.matrix(), + row.full_range(), + use_simd, + ); + } else if want_rgb_u16 { + let rgb_u16_buf = rgb_u16.as_deref_mut().unwrap(); let rgb_plane_end = one_plane_end .checked_mul(3) @@ -278,19 +450,47 @@ impl PixelSink for MixedSinker<'_, Yuv422p10> { channels: 3, })?; let rgb_plane_start = one_plane_start * 3; + let rgb_u16_row = &mut rgb_u16_buf[rgb_plane_start..rgb_plane_end]; yuv420p10_to_rgb_u16_row( row.y(), row.u_half(), row.v_half(), - &mut buf[rgb_plane_start..rgb_plane_end], + rgb_u16_row, w, row.matrix(), row.full_range(), use_simd, ); + if want_rgba_u16 { + let rgba_u16_buf = rgba_u16.as_deref_mut().unwrap(); + let rgba_u16_row = + rgba_u16_plane_row_slice(rgba_u16_buf, one_plane_start, one_plane_end, w, h)?; + expand_rgb_u16_to_rgba_u16_row::(rgb_u16_row, rgba_u16_row, w); + } } - if rgb.is_none() && hsv.is_none() { + // ===== u8 RGB / RGBA / HSV path (Strategy A) ===== + let want_rgba = rgba.is_some(); + let want_hsv = hsv.is_some(); + let need_rgb_kernel = rgb.is_some() || want_hsv; + + if want_rgba && !need_rgb_kernel { + let rgba_buf = rgba.as_deref_mut().unwrap(); + let rgba_row = rgba_plane_row_slice(rgba_buf, one_plane_start, one_plane_end, w, h)?; + yuv420p10_to_rgba_row( + row.y(), + row.u_half(), + row.v_half(), + rgba_row, + w, + row.matrix(), + row.full_range(), + use_simd, + ); + return Ok(()); + } + + if !need_rgb_kernel { return Ok(()); } @@ -324,6 +524,12 @@ impl PixelSink for MixedSinker<'_, Yuv422p10> { use_simd, ); } + + if let Some(buf) = rgba.as_deref_mut() { + let rgba_row = rgba_plane_row_slice(buf, one_plane_start, one_plane_end, w, h)?; + expand_rgb_to_rgba_row(rgb_row, rgba_row, w); + } + Ok(()) } } @@ -348,6 +554,51 @@ impl<'a> MixedSinker<'a, Yuv422p12> { self.rgb_u16 = Some(buf); Ok(self) } + + /// Attaches a packed **8‑bit** RGBA output buffer. The 12‑bit YUV + /// source is converted to 8‑bit RGBA via the `BITS = 12` Q15 kernel + /// family; alpha = `0xFF` (Yuv422p12 has no alpha plane). + #[cfg_attr(not(tarpaulin), inline(always))] + pub fn with_rgba(mut self, buf: &'a mut [u8]) -> Result { + self.set_rgba(buf)?; + Ok(self) + } + /// In-place variant of [`with_rgba`](Self::with_rgba). + #[cfg_attr(not(tarpaulin), inline(always))] + pub fn set_rgba(&mut self, buf: &'a mut [u8]) -> Result<&mut Self, MixedSinkerError> { + let expected = self.frame_bytes(4)?; + if buf.len() < expected { + return Err(MixedSinkerError::RgbaBufferTooShort { + expected, + actual: buf.len(), + }); + } + self.rgba = Some(buf); + Ok(self) + } + + /// Attaches a packed **`u16`** RGBA output buffer. 12‑bit + /// low‑packed (`(1 << 12) - 1 = 4095` max). Length is measured in + /// `u16` **elements** (`width × height × 4`). Alpha element is + /// `(1 << 12) - 1`. + #[cfg_attr(not(tarpaulin), inline(always))] + pub fn with_rgba_u16(mut self, buf: &'a mut [u16]) -> Result { + self.set_rgba_u16(buf)?; + Ok(self) + } + /// In-place variant of [`with_rgba_u16`](Self::with_rgba_u16). + #[cfg_attr(not(tarpaulin), inline(always))] + pub fn set_rgba_u16(&mut self, buf: &'a mut [u16]) -> Result<&mut Self, MixedSinkerError> { + let expected = self.frame_bytes(4)?; + if buf.len() < expected { + return Err(MixedSinkerError::RgbaU16BufferTooShort { + expected, + actual: buf.len(), + }); + } + self.rgba_u16 = Some(buf); + Ok(self) + } } impl Yuv422p12Sink for MixedSinker<'_, Yuv422p12> {} @@ -407,6 +658,8 @@ impl PixelSink for MixedSinker<'_, Yuv422p12> { let Self { rgb, rgb_u16, + rgba, + rgba_u16, luma, hsv, rgb_scratch, @@ -422,7 +675,26 @@ impl PixelSink for MixedSinker<'_, Yuv422p12> { } } - if let Some(buf) = rgb_u16.as_deref_mut() { + // ===== u16 RGB / RGBA path (Strategy A) ===== + let want_rgb_u16 = rgb_u16.is_some(); + let want_rgba_u16 = rgba_u16.is_some(); + + if want_rgba_u16 && !want_rgb_u16 { + let rgba_u16_buf = rgba_u16.as_deref_mut().unwrap(); + let rgba_u16_row = + rgba_u16_plane_row_slice(rgba_u16_buf, one_plane_start, one_plane_end, w, h)?; + yuv420p12_to_rgba_u16_row( + row.y(), + row.u_half(), + row.v_half(), + rgba_u16_row, + w, + row.matrix(), + row.full_range(), + use_simd, + ); + } else if want_rgb_u16 { + let rgb_u16_buf = rgb_u16.as_deref_mut().unwrap(); let rgb_plane_end = one_plane_end .checked_mul(3) @@ -432,19 +704,47 @@ impl PixelSink for MixedSinker<'_, Yuv422p12> { channels: 3, })?; let rgb_plane_start = one_plane_start * 3; + let rgb_u16_row = &mut rgb_u16_buf[rgb_plane_start..rgb_plane_end]; yuv420p12_to_rgb_u16_row( row.y(), row.u_half(), row.v_half(), - &mut buf[rgb_plane_start..rgb_plane_end], + rgb_u16_row, w, row.matrix(), row.full_range(), use_simd, ); + if want_rgba_u16 { + let rgba_u16_buf = rgba_u16.as_deref_mut().unwrap(); + let rgba_u16_row = + rgba_u16_plane_row_slice(rgba_u16_buf, one_plane_start, one_plane_end, w, h)?; + expand_rgb_u16_to_rgba_u16_row::(rgb_u16_row, rgba_u16_row, w); + } } - if rgb.is_none() && hsv.is_none() { + // ===== u8 RGB / RGBA / HSV path (Strategy A) ===== + let want_rgba = rgba.is_some(); + let want_hsv = hsv.is_some(); + let need_rgb_kernel = rgb.is_some() || want_hsv; + + if want_rgba && !need_rgb_kernel { + let rgba_buf = rgba.as_deref_mut().unwrap(); + let rgba_row = rgba_plane_row_slice(rgba_buf, one_plane_start, one_plane_end, w, h)?; + yuv420p12_to_rgba_row( + row.y(), + row.u_half(), + row.v_half(), + rgba_row, + w, + row.matrix(), + row.full_range(), + use_simd, + ); + return Ok(()); + } + + if !need_rgb_kernel { return Ok(()); } @@ -478,6 +778,12 @@ impl PixelSink for MixedSinker<'_, Yuv422p12> { use_simd, ); } + + if let Some(buf) = rgba.as_deref_mut() { + let rgba_row = rgba_plane_row_slice(buf, one_plane_start, one_plane_end, w, h)?; + expand_rgb_to_rgba_row(rgb_row, rgba_row, w); + } + Ok(()) } } @@ -502,6 +808,51 @@ impl<'a> MixedSinker<'a, Yuv422p14> { self.rgb_u16 = Some(buf); Ok(self) } + + /// Attaches a packed **8‑bit** RGBA output buffer. The 14‑bit YUV + /// source is converted to 8‑bit RGBA via the `BITS = 14` Q15 kernel + /// family; alpha = `0xFF` (Yuv422p14 has no alpha plane). + #[cfg_attr(not(tarpaulin), inline(always))] + pub fn with_rgba(mut self, buf: &'a mut [u8]) -> Result { + self.set_rgba(buf)?; + Ok(self) + } + /// In-place variant of [`with_rgba`](Self::with_rgba). + #[cfg_attr(not(tarpaulin), inline(always))] + pub fn set_rgba(&mut self, buf: &'a mut [u8]) -> Result<&mut Self, MixedSinkerError> { + let expected = self.frame_bytes(4)?; + if buf.len() < expected { + return Err(MixedSinkerError::RgbaBufferTooShort { + expected, + actual: buf.len(), + }); + } + self.rgba = Some(buf); + Ok(self) + } + + /// Attaches a packed **`u16`** RGBA output buffer. 14‑bit + /// low‑packed (`(1 << 14) - 1 = 16383` max). Length is measured in + /// `u16` **elements** (`width × height × 4`). Alpha element is + /// `(1 << 14) - 1`. + #[cfg_attr(not(tarpaulin), inline(always))] + pub fn with_rgba_u16(mut self, buf: &'a mut [u16]) -> Result { + self.set_rgba_u16(buf)?; + Ok(self) + } + /// In-place variant of [`with_rgba_u16`](Self::with_rgba_u16). + #[cfg_attr(not(tarpaulin), inline(always))] + pub fn set_rgba_u16(&mut self, buf: &'a mut [u16]) -> Result<&mut Self, MixedSinkerError> { + let expected = self.frame_bytes(4)?; + if buf.len() < expected { + return Err(MixedSinkerError::RgbaU16BufferTooShort { + expected, + actual: buf.len(), + }); + } + self.rgba_u16 = Some(buf); + Ok(self) + } } impl Yuv422p14Sink for MixedSinker<'_, Yuv422p14> {} @@ -561,6 +912,8 @@ impl PixelSink for MixedSinker<'_, Yuv422p14> { let Self { rgb, rgb_u16, + rgba, + rgba_u16, luma, hsv, rgb_scratch, @@ -576,7 +929,26 @@ impl PixelSink for MixedSinker<'_, Yuv422p14> { } } - if let Some(buf) = rgb_u16.as_deref_mut() { + // ===== u16 RGB / RGBA path (Strategy A) ===== + let want_rgb_u16 = rgb_u16.is_some(); + let want_rgba_u16 = rgba_u16.is_some(); + + if want_rgba_u16 && !want_rgb_u16 { + let rgba_u16_buf = rgba_u16.as_deref_mut().unwrap(); + let rgba_u16_row = + rgba_u16_plane_row_slice(rgba_u16_buf, one_plane_start, one_plane_end, w, h)?; + yuv420p14_to_rgba_u16_row( + row.y(), + row.u_half(), + row.v_half(), + rgba_u16_row, + w, + row.matrix(), + row.full_range(), + use_simd, + ); + } else if want_rgb_u16 { + let rgb_u16_buf = rgb_u16.as_deref_mut().unwrap(); let rgb_plane_end = one_plane_end .checked_mul(3) @@ -586,19 +958,47 @@ impl PixelSink for MixedSinker<'_, Yuv422p14> { channels: 3, })?; let rgb_plane_start = one_plane_start * 3; + let rgb_u16_row = &mut rgb_u16_buf[rgb_plane_start..rgb_plane_end]; yuv420p14_to_rgb_u16_row( row.y(), row.u_half(), row.v_half(), - &mut buf[rgb_plane_start..rgb_plane_end], + rgb_u16_row, w, row.matrix(), row.full_range(), use_simd, ); + if want_rgba_u16 { + let rgba_u16_buf = rgba_u16.as_deref_mut().unwrap(); + let rgba_u16_row = + rgba_u16_plane_row_slice(rgba_u16_buf, one_plane_start, one_plane_end, w, h)?; + expand_rgb_u16_to_rgba_u16_row::(rgb_u16_row, rgba_u16_row, w); + } } - if rgb.is_none() && hsv.is_none() { + // ===== u8 RGB / RGBA / HSV path (Strategy A) ===== + let want_rgba = rgba.is_some(); + let want_hsv = hsv.is_some(); + let need_rgb_kernel = rgb.is_some() || want_hsv; + + if want_rgba && !need_rgb_kernel { + let rgba_buf = rgba.as_deref_mut().unwrap(); + let rgba_row = rgba_plane_row_slice(rgba_buf, one_plane_start, one_plane_end, w, h)?; + yuv420p14_to_rgba_row( + row.y(), + row.u_half(), + row.v_half(), + rgba_row, + w, + row.matrix(), + row.full_range(), + use_simd, + ); + return Ok(()); + } + + if !need_rgb_kernel { return Ok(()); } @@ -632,6 +1032,12 @@ impl PixelSink for MixedSinker<'_, Yuv422p14> { use_simd, ); } + + if let Some(buf) = rgba.as_deref_mut() { + let rgba_row = rgba_plane_row_slice(buf, one_plane_start, one_plane_end, w, h)?; + expand_rgb_to_rgba_row(rgb_row, rgba_row, w); + } + Ok(()) } } @@ -662,6 +1068,51 @@ impl<'a> MixedSinker<'a, Yuv422p16> { self.rgb_u16 = Some(buf); Ok(self) } + + /// Attaches a packed **8‑bit** RGBA output buffer. The 16‑bit YUV + /// source is converted to 8‑bit RGBA via the `BITS = 16` Q15 kernel + /// family; alpha = `0xFF` (Yuv422p16 has no alpha plane). + #[cfg_attr(not(tarpaulin), inline(always))] + pub fn with_rgba(mut self, buf: &'a mut [u8]) -> Result { + self.set_rgba(buf)?; + Ok(self) + } + /// In-place variant of [`with_rgba`](Self::with_rgba). + #[cfg_attr(not(tarpaulin), inline(always))] + pub fn set_rgba(&mut self, buf: &'a mut [u8]) -> Result<&mut Self, MixedSinkerError> { + let expected = self.frame_bytes(4)?; + if buf.len() < expected { + return Err(MixedSinkerError::RgbaBufferTooShort { + expected, + actual: buf.len(), + }); + } + self.rgba = Some(buf); + Ok(self) + } + + /// Attaches a packed **`u16`** RGBA output buffer. Output covers the + /// full `u16` range `[0, 65535]` (16 active bits, no packing). Length + /// is measured in `u16` **elements** (`width × height × 4`). Alpha + /// element is `0xFFFF`. + #[cfg_attr(not(tarpaulin), inline(always))] + pub fn with_rgba_u16(mut self, buf: &'a mut [u16]) -> Result { + self.set_rgba_u16(buf)?; + Ok(self) + } + /// In-place variant of [`with_rgba_u16`](Self::with_rgba_u16). + #[cfg_attr(not(tarpaulin), inline(always))] + pub fn set_rgba_u16(&mut self, buf: &'a mut [u16]) -> Result<&mut Self, MixedSinkerError> { + let expected = self.frame_bytes(4)?; + if buf.len() < expected { + return Err(MixedSinkerError::RgbaU16BufferTooShort { + expected, + actual: buf.len(), + }); + } + self.rgba_u16 = Some(buf); + Ok(self) + } } impl Yuv422p16Sink for MixedSinker<'_, Yuv422p16> {} @@ -721,6 +1172,8 @@ impl PixelSink for MixedSinker<'_, Yuv422p16> { let Self { rgb, rgb_u16, + rgba, + rgba_u16, luma, hsv, rgb_scratch, @@ -736,7 +1189,28 @@ impl PixelSink for MixedSinker<'_, Yuv422p16> { } } - if let Some(buf) = rgb_u16.as_deref_mut() { + // ===== u16 RGB / RGBA path (Strategy A) ===== + // Reuses Yuv420p16's u16-output kernel — 4:2:2 per-row shape + // matches 4:2:0's (half-width UV, one pair per Y pair). + let want_rgb_u16 = rgb_u16.is_some(); + let want_rgba_u16 = rgba_u16.is_some(); + + if want_rgba_u16 && !want_rgb_u16 { + let rgba_u16_buf = rgba_u16.as_deref_mut().unwrap(); + let rgba_u16_row = + rgba_u16_plane_row_slice(rgba_u16_buf, one_plane_start, one_plane_end, w, h)?; + yuv420p16_to_rgba_u16_row( + row.y(), + row.u_half(), + row.v_half(), + rgba_u16_row, + w, + row.matrix(), + row.full_range(), + use_simd, + ); + } else if want_rgb_u16 { + let rgb_u16_buf = rgb_u16.as_deref_mut().unwrap(); let rgb_plane_end = one_plane_end .checked_mul(3) @@ -746,21 +1220,47 @@ impl PixelSink for MixedSinker<'_, Yuv422p16> { channels: 3, })?; let rgb_plane_start = one_plane_start * 3; - // Reuses Yuv420p16's u16-output kernel — 4:2:2 per-row shape - // matches 4:2:0's (half-width UV, one pair per Y pair). + let rgb_u16_row = &mut rgb_u16_buf[rgb_plane_start..rgb_plane_end]; yuv420p16_to_rgb_u16_row( row.y(), row.u_half(), row.v_half(), - &mut buf[rgb_plane_start..rgb_plane_end], + rgb_u16_row, w, row.matrix(), row.full_range(), use_simd, ); + if want_rgba_u16 { + let rgba_u16_buf = rgba_u16.as_deref_mut().unwrap(); + let rgba_u16_row = + rgba_u16_plane_row_slice(rgba_u16_buf, one_plane_start, one_plane_end, w, h)?; + expand_rgb_u16_to_rgba_u16_row::(rgb_u16_row, rgba_u16_row, w); + } } - if rgb.is_none() && hsv.is_none() { + // ===== u8 RGB / RGBA / HSV path (Strategy A) ===== + let want_rgba = rgba.is_some(); + let want_hsv = hsv.is_some(); + let need_rgb_kernel = rgb.is_some() || want_hsv; + + if want_rgba && !need_rgb_kernel { + let rgba_buf = rgba.as_deref_mut().unwrap(); + let rgba_row = rgba_plane_row_slice(rgba_buf, one_plane_start, one_plane_end, w, h)?; + yuv420p16_to_rgba_row( + row.y(), + row.u_half(), + row.v_half(), + rgba_row, + w, + row.matrix(), + row.full_range(), + use_simd, + ); + return Ok(()); + } + + if !need_rgb_kernel { return Ok(()); } @@ -794,6 +1294,12 @@ impl PixelSink for MixedSinker<'_, Yuv422p16> { use_simd, ); } + + if let Some(buf) = rgba.as_deref_mut() { + let rgba_row = rgba_plane_row_slice(buf, one_plane_start, one_plane_end, w, h)?; + expand_rgb_to_rgba_row(rgb_row, rgba_row, w); + } + Ok(()) } } @@ -1130,6 +1636,52 @@ impl<'a> MixedSinker<'a, P210> { self.rgb_u16 = Some(buf); Ok(self) } + + /// Attaches a packed **8‑bit** RGBA output buffer. The 10‑bit P210 + /// source (semi‑planar, high‑bit‑packed) is converted to 8‑bit RGBA + /// via the `BITS = 10` Q15 kernel family; alpha = `0xFF` (P210 has + /// no alpha plane). + #[cfg_attr(not(tarpaulin), inline(always))] + pub fn with_rgba(mut self, buf: &'a mut [u8]) -> Result { + self.set_rgba(buf)?; + Ok(self) + } + /// In-place variant of [`with_rgba`](Self::with_rgba). + #[cfg_attr(not(tarpaulin), inline(always))] + pub fn set_rgba(&mut self, buf: &'a mut [u8]) -> Result<&mut Self, MixedSinkerError> { + let expected = self.frame_bytes(4)?; + if buf.len() < expected { + return Err(MixedSinkerError::RgbaBufferTooShort { + expected, + actual: buf.len(), + }); + } + self.rgba = Some(buf); + Ok(self) + } + + /// Attaches a packed **`u16`** RGBA output buffer. Output is + /// **low‑bit‑packed** 10‑bit values (`yuv420p10le` convention) — not + /// P210 high‑bit packing. Length is measured in `u16` **elements** + /// (`width × height × 4`). Alpha element is `(1 << 10) - 1`. + #[cfg_attr(not(tarpaulin), inline(always))] + pub fn with_rgba_u16(mut self, buf: &'a mut [u16]) -> Result { + self.set_rgba_u16(buf)?; + Ok(self) + } + /// In-place variant of [`with_rgba_u16`](Self::with_rgba_u16). + #[cfg_attr(not(tarpaulin), inline(always))] + pub fn set_rgba_u16(&mut self, buf: &'a mut [u16]) -> Result<&mut Self, MixedSinkerError> { + let expected = self.frame_bytes(4)?; + if buf.len() < expected { + return Err(MixedSinkerError::RgbaU16BufferTooShort { + expected, + actual: buf.len(), + }); + } + self.rgba_u16 = Some(buf); + Ok(self) + } } impl P210Sink for MixedSinker<'_, P210> {} @@ -1146,6 +1698,10 @@ impl PixelSink for MixedSinker<'_, P210> { } fn process(&mut self, row: P210Row<'_>) -> Result<(), Self::Error> { + // P210 stores 10‑bit samples high‑bit‑packed; bit depth is fixed + // by the format. Used for the u16 RGBA expand path's alpha pad. + const BITS: u32 = 10; + let w = self.width; let h = self.height; let idx = row.row(); @@ -1180,6 +1736,8 @@ impl PixelSink for MixedSinker<'_, P210> { let Self { rgb, rgb_u16, + rgba, + rgba_u16, luma, hsv, rgb_scratch, @@ -1195,7 +1753,27 @@ impl PixelSink for MixedSinker<'_, P210> { } } - if let Some(buf) = rgb_u16.as_deref_mut() { + // ===== u16 RGB / RGBA path (Strategy A) ===== + // u16 outputs are low-bit-packed (yuv420p10le convention), not + // P210's high-bit packing. + let want_rgb_u16 = rgb_u16.is_some(); + let want_rgba_u16 = rgba_u16.is_some(); + + if want_rgba_u16 && !want_rgb_u16 { + let rgba_u16_buf = rgba_u16.as_deref_mut().unwrap(); + let rgba_u16_row = + rgba_u16_plane_row_slice(rgba_u16_buf, one_plane_start, one_plane_end, w, h)?; + p010_to_rgba_u16_row( + row.y(), + row.uv_half(), + rgba_u16_row, + w, + row.matrix(), + row.full_range(), + use_simd, + ); + } else if want_rgb_u16 { + let rgb_u16_buf = rgb_u16.as_deref_mut().unwrap(); let rgb_plane_end = one_plane_end .checked_mul(3) @@ -1205,18 +1783,45 @@ impl PixelSink for MixedSinker<'_, P210> { channels: 3, })?; let rgb_plane_start = one_plane_start * 3; + let rgb_u16_row = &mut rgb_u16_buf[rgb_plane_start..rgb_plane_end]; p010_to_rgb_u16_row( row.y(), row.uv_half(), - &mut buf[rgb_plane_start..rgb_plane_end], + rgb_u16_row, w, row.matrix(), row.full_range(), use_simd, ); + if want_rgba_u16 { + let rgba_u16_buf = rgba_u16.as_deref_mut().unwrap(); + let rgba_u16_row = + rgba_u16_plane_row_slice(rgba_u16_buf, one_plane_start, one_plane_end, w, h)?; + expand_rgb_u16_to_rgba_u16_row::(rgb_u16_row, rgba_u16_row, w); + } } - if rgb.is_none() && hsv.is_none() { + // ===== u8 RGB / RGBA / HSV path (Strategy A) ===== + let want_rgba = rgba.is_some(); + let want_hsv = hsv.is_some(); + let need_rgb_kernel = rgb.is_some() || want_hsv; + + if want_rgba && !need_rgb_kernel { + let rgba_buf = rgba.as_deref_mut().unwrap(); + let rgba_row = rgba_plane_row_slice(rgba_buf, one_plane_start, one_plane_end, w, h)?; + p010_to_rgba_row( + row.y(), + row.uv_half(), + rgba_row, + w, + row.matrix(), + row.full_range(), + use_simd, + ); + return Ok(()); + } + + if !need_rgb_kernel { return Ok(()); } @@ -1249,6 +1854,12 @@ impl PixelSink for MixedSinker<'_, P210> { use_simd, ); } + + if let Some(buf) = rgba.as_deref_mut() { + let rgba_row = rgba_plane_row_slice(buf, one_plane_start, one_plane_end, w, h)?; + expand_rgb_to_rgba_row(rgb_row, rgba_row, w); + } + Ok(()) } } @@ -1279,6 +1890,52 @@ impl<'a> MixedSinker<'a, P212> { self.rgb_u16 = Some(buf); Ok(self) } + + /// Attaches a packed **8‑bit** RGBA output buffer. The 12‑bit P212 + /// source (semi‑planar, high‑bit‑packed) is converted to 8‑bit RGBA + /// via the `BITS = 12` Q15 kernel family; alpha = `0xFF` (P212 has + /// no alpha plane). + #[cfg_attr(not(tarpaulin), inline(always))] + pub fn with_rgba(mut self, buf: &'a mut [u8]) -> Result { + self.set_rgba(buf)?; + Ok(self) + } + /// In-place variant of [`with_rgba`](Self::with_rgba). + #[cfg_attr(not(tarpaulin), inline(always))] + pub fn set_rgba(&mut self, buf: &'a mut [u8]) -> Result<&mut Self, MixedSinkerError> { + let expected = self.frame_bytes(4)?; + if buf.len() < expected { + return Err(MixedSinkerError::RgbaBufferTooShort { + expected, + actual: buf.len(), + }); + } + self.rgba = Some(buf); + Ok(self) + } + + /// Attaches a packed **`u16`** RGBA output buffer. Output is + /// **low‑bit‑packed** 12‑bit values (`yuv420p12le` convention) — not + /// P212 high‑bit packing. Length is measured in `u16` **elements** + /// (`width × height × 4`). Alpha element is `(1 << 12) - 1`. + #[cfg_attr(not(tarpaulin), inline(always))] + pub fn with_rgba_u16(mut self, buf: &'a mut [u16]) -> Result { + self.set_rgba_u16(buf)?; + Ok(self) + } + /// In-place variant of [`with_rgba_u16`](Self::with_rgba_u16). + #[cfg_attr(not(tarpaulin), inline(always))] + pub fn set_rgba_u16(&mut self, buf: &'a mut [u16]) -> Result<&mut Self, MixedSinkerError> { + let expected = self.frame_bytes(4)?; + if buf.len() < expected { + return Err(MixedSinkerError::RgbaU16BufferTooShort { + expected, + actual: buf.len(), + }); + } + self.rgba_u16 = Some(buf); + Ok(self) + } } impl P212Sink for MixedSinker<'_, P212> {} @@ -1295,6 +1952,10 @@ impl PixelSink for MixedSinker<'_, P212> { } fn process(&mut self, row: P212Row<'_>) -> Result<(), Self::Error> { + // P212 stores 12‑bit samples high‑bit‑packed; bit depth is fixed + // by the format. Used for the u16 RGBA expand path's alpha pad. + const BITS: u32 = 12; + let w = self.width; let h = self.height; let idx = row.row(); @@ -1329,6 +1990,8 @@ impl PixelSink for MixedSinker<'_, P212> { let Self { rgb, rgb_u16, + rgba, + rgba_u16, luma, hsv, rgb_scratch, @@ -1344,7 +2007,27 @@ impl PixelSink for MixedSinker<'_, P212> { } } - if let Some(buf) = rgb_u16.as_deref_mut() { + // ===== u16 RGB / RGBA path (Strategy A) ===== + // u16 outputs are low-bit-packed (yuv420p12le convention), not + // P212's high-bit packing. + let want_rgb_u16 = rgb_u16.is_some(); + let want_rgba_u16 = rgba_u16.is_some(); + + if want_rgba_u16 && !want_rgb_u16 { + let rgba_u16_buf = rgba_u16.as_deref_mut().unwrap(); + let rgba_u16_row = + rgba_u16_plane_row_slice(rgba_u16_buf, one_plane_start, one_plane_end, w, h)?; + p012_to_rgba_u16_row( + row.y(), + row.uv_half(), + rgba_u16_row, + w, + row.matrix(), + row.full_range(), + use_simd, + ); + } else if want_rgb_u16 { + let rgb_u16_buf = rgb_u16.as_deref_mut().unwrap(); let rgb_plane_end = one_plane_end .checked_mul(3) @@ -1354,18 +2037,45 @@ impl PixelSink for MixedSinker<'_, P212> { channels: 3, })?; let rgb_plane_start = one_plane_start * 3; + let rgb_u16_row = &mut rgb_u16_buf[rgb_plane_start..rgb_plane_end]; p012_to_rgb_u16_row( row.y(), row.uv_half(), - &mut buf[rgb_plane_start..rgb_plane_end], + rgb_u16_row, w, row.matrix(), row.full_range(), use_simd, ); + if want_rgba_u16 { + let rgba_u16_buf = rgba_u16.as_deref_mut().unwrap(); + let rgba_u16_row = + rgba_u16_plane_row_slice(rgba_u16_buf, one_plane_start, one_plane_end, w, h)?; + expand_rgb_u16_to_rgba_u16_row::(rgb_u16_row, rgba_u16_row, w); + } } - if rgb.is_none() && hsv.is_none() { + // ===== u8 RGB / RGBA / HSV path (Strategy A) ===== + let want_rgba = rgba.is_some(); + let want_hsv = hsv.is_some(); + let need_rgb_kernel = rgb.is_some() || want_hsv; + + if want_rgba && !need_rgb_kernel { + let rgba_buf = rgba.as_deref_mut().unwrap(); + let rgba_row = rgba_plane_row_slice(rgba_buf, one_plane_start, one_plane_end, w, h)?; + p012_to_rgba_row( + row.y(), + row.uv_half(), + rgba_row, + w, + row.matrix(), + row.full_range(), + use_simd, + ); + return Ok(()); + } + + if !need_rgb_kernel { return Ok(()); } @@ -1398,6 +2108,12 @@ impl PixelSink for MixedSinker<'_, P212> { use_simd, ); } + + if let Some(buf) = rgba.as_deref_mut() { + let rgba_row = rgba_plane_row_slice(buf, one_plane_start, one_plane_end, w, h)?; + expand_rgb_to_rgba_row(rgb_row, rgba_row, w); + } + Ok(()) } } @@ -1427,6 +2143,52 @@ impl<'a> MixedSinker<'a, P216> { self.rgb_u16 = Some(buf); Ok(self) } + + /// Attaches a packed **8‑bit** RGBA output buffer. The 16‑bit P216 + /// source (semi‑planar, 16 active bits) is converted to 8‑bit RGBA + /// via the `BITS = 16` Q15 kernel family; alpha = `0xFF` (P216 has + /// no alpha plane). + #[cfg_attr(not(tarpaulin), inline(always))] + pub fn with_rgba(mut self, buf: &'a mut [u8]) -> Result { + self.set_rgba(buf)?; + Ok(self) + } + /// In-place variant of [`with_rgba`](Self::with_rgba). + #[cfg_attr(not(tarpaulin), inline(always))] + pub fn set_rgba(&mut self, buf: &'a mut [u8]) -> Result<&mut Self, MixedSinkerError> { + let expected = self.frame_bytes(4)?; + if buf.len() < expected { + return Err(MixedSinkerError::RgbaBufferTooShort { + expected, + actual: buf.len(), + }); + } + self.rgba = Some(buf); + Ok(self) + } + + /// Attaches a packed **`u16`** RGBA output buffer. Output covers the + /// full `u16` range `[0, 65535]` (16 active bits). Length is + /// measured in `u16` **elements** (`width × height × 4`). Alpha + /// element is `0xFFFF`. + #[cfg_attr(not(tarpaulin), inline(always))] + pub fn with_rgba_u16(mut self, buf: &'a mut [u16]) -> Result { + self.set_rgba_u16(buf)?; + Ok(self) + } + /// In-place variant of [`with_rgba_u16`](Self::with_rgba_u16). + #[cfg_attr(not(tarpaulin), inline(always))] + pub fn set_rgba_u16(&mut self, buf: &'a mut [u16]) -> Result<&mut Self, MixedSinkerError> { + let expected = self.frame_bytes(4)?; + if buf.len() < expected { + return Err(MixedSinkerError::RgbaU16BufferTooShort { + expected, + actual: buf.len(), + }); + } + self.rgba_u16 = Some(buf); + Ok(self) + } } impl P216Sink for MixedSinker<'_, P216> {} @@ -1443,6 +2205,10 @@ impl PixelSink for MixedSinker<'_, P216> { } fn process(&mut self, row: P216Row<'_>) -> Result<(), Self::Error> { + // P216 is 16-bit semi-planar (every bit active); used for the u16 + // RGBA expand path's alpha pad (alpha = 0xFFFF). + const BITS: u32 = 16; + let w = self.width; let h = self.height; let idx = row.row(); @@ -1477,6 +2243,8 @@ impl PixelSink for MixedSinker<'_, P216> { let Self { rgb, rgb_u16, + rgba, + rgba_u16, luma, hsv, rgb_scratch, @@ -1493,7 +2261,25 @@ impl PixelSink for MixedSinker<'_, P216> { } } - if let Some(buf) = rgb_u16.as_deref_mut() { + // ===== u16 RGB / RGBA path (Strategy A) ===== + let want_rgb_u16 = rgb_u16.is_some(); + let want_rgba_u16 = rgba_u16.is_some(); + + if want_rgba_u16 && !want_rgb_u16 { + let rgba_u16_buf = rgba_u16.as_deref_mut().unwrap(); + let rgba_u16_row = + rgba_u16_plane_row_slice(rgba_u16_buf, one_plane_start, one_plane_end, w, h)?; + p016_to_rgba_u16_row( + row.y(), + row.uv_half(), + rgba_u16_row, + w, + row.matrix(), + row.full_range(), + use_simd, + ); + } else if want_rgb_u16 { + let rgb_u16_buf = rgb_u16.as_deref_mut().unwrap(); let rgb_plane_end = one_plane_end .checked_mul(3) @@ -1503,18 +2289,45 @@ impl PixelSink for MixedSinker<'_, P216> { channels: 3, })?; let rgb_plane_start = one_plane_start * 3; + let rgb_u16_row = &mut rgb_u16_buf[rgb_plane_start..rgb_plane_end]; p016_to_rgb_u16_row( row.y(), row.uv_half(), - &mut buf[rgb_plane_start..rgb_plane_end], + rgb_u16_row, w, row.matrix(), row.full_range(), use_simd, ); + if want_rgba_u16 { + let rgba_u16_buf = rgba_u16.as_deref_mut().unwrap(); + let rgba_u16_row = + rgba_u16_plane_row_slice(rgba_u16_buf, one_plane_start, one_plane_end, w, h)?; + expand_rgb_u16_to_rgba_u16_row::(rgb_u16_row, rgba_u16_row, w); + } } - if rgb.is_none() && hsv.is_none() { + // ===== u8 RGB / RGBA / HSV path (Strategy A) ===== + let want_rgba = rgba.is_some(); + let want_hsv = hsv.is_some(); + let need_rgb_kernel = rgb.is_some() || want_hsv; + + if want_rgba && !need_rgb_kernel { + let rgba_buf = rgba.as_deref_mut().unwrap(); + let rgba_row = rgba_plane_row_slice(rgba_buf, one_plane_start, one_plane_end, w, h)?; + p016_to_rgba_row( + row.y(), + row.uv_half(), + rgba_row, + w, + row.matrix(), + row.full_range(), + use_simd, + ); + return Ok(()); + } + + if !need_rgb_kernel { return Ok(()); } @@ -1547,6 +2360,12 @@ impl PixelSink for MixedSinker<'_, P216> { use_simd, ); } + + if let Some(buf) = rgba.as_deref_mut() { + let rgba_row = rgba_plane_row_slice(buf, one_plane_start, one_plane_end, w, h)?; + expand_rgb_to_rgba_row(rgb_row, rgba_row, w); + } + Ok(()) } } diff --git a/src/sinker/mixed/tests.rs b/src/sinker/mixed/tests.rs index b65f7de..c5e87dd 100644 --- a/src/sinker/mixed/tests.rs +++ b/src/sinker/mixed/tests.rs @@ -6216,3 +6216,208 @@ fn custom_luma_coefficients_at_max_does_not_overflow_kernel() { ); } } + +// ---- Ship 8 PR 5d: high-bit 4:2:2 RGBA wiring ------------------------- +// +// Strategy A combine for the eight 4:2:2 high-bit sinker formats wired +// in the 4:2:2 high-bit file. Mirrors the 4:2:0 PR #26 test suite; +// covers Yuv422p10 (planar BITS-generic), Yuv422p16 (planar 16-bit +// dedicated kernel), and P210 (semi-planar BITS-generic) — the row +// layer is exhaustively tested elsewhere. + +#[test] +#[cfg_attr( + miri, + ignore = "SIMD-dispatched row kernels use intrinsics unsupported by Miri" +)] +fn yuv422p10_rgba_u8_only_gray_with_opaque_alpha() { + // 10-bit mid-gray → 8-bit RGBA ≈ (128, 128, 128, 255) per pixel. + let (yp, up, vp) = solid_yuv422p_n_frame(16, 8, 512, 512, 512); + let src = Yuv422p10Frame::new(&yp, &up, &vp, 16, 8, 16, 8, 8); + + let mut rgba = std::vec![0u8; 16 * 8 * 4]; + let mut sink = MixedSinker::::new(16, 8) + .with_rgba(&mut rgba) + .unwrap(); + yuv422p10_to(&src, true, ColorMatrix::Bt601, &mut sink).unwrap(); + + for px in rgba.chunks(4) { + assert!(px[0].abs_diff(128) <= 1); + assert_eq!(px[0], px[1]); + assert_eq!(px[1], px[2]); + assert_eq!(px[3], 0xFF, "alpha must be opaque"); + } +} + +#[test] +#[cfg_attr( + miri, + ignore = "SIMD-dispatched row kernels use intrinsics unsupported by Miri" +)] +fn yuv422p10_rgba_u16_only_native_depth_gray_with_opaque_alpha() { + // 10-bit mid-gray → u16 RGBA: each color element ≈ 512, alpha = 1023. + let (yp, up, vp) = solid_yuv422p_n_frame(16, 8, 512, 512, 512); + let src = Yuv422p10Frame::new(&yp, &up, &vp, 16, 8, 16, 8, 8); + + let mut rgba = std::vec![0u16; 16 * 8 * 4]; + let mut sink = MixedSinker::::new(16, 8) + .with_rgba_u16(&mut rgba) + .unwrap(); + yuv422p10_to(&src, true, ColorMatrix::Bt601, &mut sink).unwrap(); + + for px in rgba.chunks(4) { + assert!(px[0].abs_diff(512) <= 1, "got {px:?}"); + assert_eq!(px[0], px[1]); + assert_eq!(px[1], px[2]); + assert_eq!(px[3], 1023, "alpha must equal (1 << 10) - 1"); + } +} + +#[test] +#[cfg_attr( + miri, + ignore = "SIMD-dispatched row kernels use intrinsics unsupported by Miri" +)] +fn yuv422p10_with_rgb_and_with_rgba_produce_byte_identical_rgb_bytes() { + // Strategy A: when both rgb and rgba are attached, the rgb buffer is + // populated by the RGB kernel and the rgba buffer is populated via a + // cheap expand pass. RGB triples must be byte-identical to the + // standalone RGB-only run. + let (yp, up, vp) = solid_yuv422p_n_frame(64, 16, 600, 400, 700); + let src = Yuv422p10Frame::new(&yp, &up, &vp, 64, 16, 64, 32, 32); + + let mut rgb_solo = std::vec![0u8; 64 * 16 * 3]; + let mut s_solo = MixedSinker::::new(64, 16) + .with_rgb(&mut rgb_solo) + .unwrap(); + yuv422p10_to(&src, true, ColorMatrix::Bt709, &mut s_solo).unwrap(); + + let mut rgb_combined = std::vec![0u8; 64 * 16 * 3]; + let mut rgba = std::vec![0u8; 64 * 16 * 4]; + let mut s_combined = MixedSinker::::new(64, 16) + .with_rgb(&mut rgb_combined) + .unwrap() + .with_rgba(&mut rgba) + .unwrap(); + yuv422p10_to(&src, true, ColorMatrix::Bt709, &mut s_combined).unwrap(); + + assert_eq!(rgb_solo, rgb_combined, "RGB bytes must match across runs"); + for (rgb_px, rgba_px) in rgb_combined.chunks(3).zip(rgba.chunks(4)) { + assert_eq!(rgb_px[0], rgba_px[0]); + assert_eq!(rgb_px[1], rgba_px[1]); + assert_eq!(rgb_px[2], rgba_px[2]); + assert_eq!(rgba_px[3], 0xFF); + } +} + +#[test] +#[cfg_attr( + miri, + ignore = "SIMD-dispatched row kernels use intrinsics unsupported by Miri" +)] +fn yuv422p10_with_rgb_u16_and_with_rgba_u16_produce_byte_identical_rgb_elems() { + // Strategy A on the u16 path: rgb_u16 buffer populated by the u16 RGB + // kernel, rgba_u16 fanned out via expand_rgb_u16_to_rgba_u16_row<10>. + let (yp, up, vp) = solid_yuv422p_n_frame(64, 16, 600, 400, 700); + let src = Yuv422p10Frame::new(&yp, &up, &vp, 64, 16, 64, 32, 32); + + let mut rgb_solo = std::vec![0u16; 64 * 16 * 3]; + let mut s_solo = MixedSinker::::new(64, 16) + .with_rgb_u16(&mut rgb_solo) + .unwrap(); + yuv422p10_to(&src, true, ColorMatrix::Bt709, &mut s_solo).unwrap(); + + let mut rgb_combined = std::vec![0u16; 64 * 16 * 3]; + let mut rgba = std::vec![0u16; 64 * 16 * 4]; + let mut s_combined = MixedSinker::::new(64, 16) + .with_rgb_u16(&mut rgb_combined) + .unwrap() + .with_rgba_u16(&mut rgba) + .unwrap(); + yuv422p10_to(&src, true, ColorMatrix::Bt709, &mut s_combined).unwrap(); + + assert_eq!( + rgb_solo, rgb_combined, + "RGB u16 elements must match across runs" + ); + for (rgb_px, rgba_px) in rgb_combined.chunks(3).zip(rgba.chunks(4)) { + assert_eq!(rgb_px[0], rgba_px[0]); + assert_eq!(rgb_px[1], rgba_px[1]); + assert_eq!(rgb_px[2], rgba_px[2]); + assert_eq!(rgba_px[3], 1023, "alpha = (1 << 10) - 1"); + } +} + +#[test] +fn yuv422p10_rgba_too_short_returns_err() { + let mut rgba = std::vec![0u8; 10]; + let err = MixedSinker::::new(16, 8) + .with_rgba(&mut rgba) + .err() + .expect("expected RgbaBufferTooShort"); + assert!(matches!(err, MixedSinkerError::RgbaBufferTooShort { .. })); +} + +#[test] +fn yuv422p10_rgba_u16_too_short_returns_err() { + let mut rgba = std::vec![0u16; 10]; + let err = MixedSinker::::new(16, 8) + .with_rgba_u16(&mut rgba) + .err() + .expect("expected RgbaU16BufferTooShort"); + assert!(matches!( + err, + MixedSinkerError::RgbaU16BufferTooShort { .. } + )); +} + +#[test] +#[cfg_attr( + miri, + ignore = "SIMD-dispatched row kernels use intrinsics unsupported by Miri" +)] +fn p210_rgba_u16_only_native_depth_gray_with_opaque_alpha() { + // P210 stores 10-bit samples high-bit-packed (`<< 6`). Mid-gray u16 + // RGBA elements ≈ 512 (low-bit-packed, yuv420p10le convention) and + // alpha = (1 << 10) - 1 = 1023. + let (yp, uvp) = solid_p2x0_frame(16, 8, 10, 512, 512, 512); + let src = P210Frame::new(&yp, &uvp, 16, 8, 16, 16); + + let mut rgba = std::vec![0u16; 16 * 8 * 4]; + let mut sink = MixedSinker::::new(16, 8) + .with_rgba_u16(&mut rgba) + .unwrap(); + p210_to(&src, true, ColorMatrix::Bt709, &mut sink).unwrap(); + + for px in rgba.chunks(4) { + assert!(px[0].abs_diff(512) <= 1, "got {px:?}"); + assert_eq!(px[0], px[1]); + assert_eq!(px[1], px[2]); + assert_eq!(px[3], 1023, "alpha must equal (1 << 10) - 1"); + } +} + +#[test] +#[cfg_attr( + miri, + ignore = "SIMD-dispatched row kernels use intrinsics unsupported by Miri" +)] +fn yuv422p16_rgba_u16_only_native_depth_gray_with_opaque_alpha() { + // 16-bit mid-gray → u16 RGBA: each color element ≈ 32768, alpha = 0xFFFF. + // Covers the 16-bit dedicated kernel family (no Q15 downshift). + let (yp, up, vp) = solid_yuv422p_n_frame(16, 8, 32768, 32768, 32768); + let src = Yuv422p16Frame::new(&yp, &up, &vp, 16, 8, 16, 8, 8); + + let mut rgba = std::vec![0u16; 16 * 8 * 4]; + let mut sink = MixedSinker::::new(16, 8) + .with_rgba_u16(&mut rgba) + .unwrap(); + yuv422p16_to(&src, true, ColorMatrix::Bt709, &mut sink).unwrap(); + + for px in rgba.chunks(4) { + assert!(px[0].abs_diff(32768) <= 256, "got {px:?}"); + assert_eq!(px[0], px[1]); + assert_eq!(px[1], px[2]); + assert_eq!(px[3], 0xFFFF, "alpha must equal 0xFFFF"); + } +} From 556ecaa2ff6ccc56820d96435ddb8c6e222beec8 Mon Sep 17 00:00:00 2001 From: uqio <276879906+uqio@users.noreply.github.com> Date: Mon, 27 Apr 2026 11:45:10 +1200 Subject: [PATCH 2/2] docs(sinker): fix Yuv422p16/P216 with_rgba doc to name the dedicated 16-bit kernel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `with_rgba` doc on `MixedSinker` and `MixedSinker` claimed the 16-bit YUV → 8-bit RGBA path uses "the `BITS = 16` Q15 kernel family", but both formats actually call `yuv420p16_to_rgba_row` / `p016_to_rgba_row` — the **dedicated 16-bit kernel family** (i64 chroma multiply, separate from the BITS-generic Q15 pipeline used at BITS=10/12/14). Aligns the wording with the existing `Yuv420p16` / `P016` doc convention in `subsampled_4_2_0_high_bit.rs` and explicitly contrasts the Q15 pipeline so future readers don't conflate the two kernel families. Addresses Copilot review comments on PR #28. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/sinker/mixed/subsampled_4_2_2_high_bit.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/sinker/mixed/subsampled_4_2_2_high_bit.rs b/src/sinker/mixed/subsampled_4_2_2_high_bit.rs index 0cba870..0f1a062 100644 --- a/src/sinker/mixed/subsampled_4_2_2_high_bit.rs +++ b/src/sinker/mixed/subsampled_4_2_2_high_bit.rs @@ -1070,8 +1070,9 @@ impl<'a> MixedSinker<'a, Yuv422p16> { } /// Attaches a packed **8‑bit** RGBA output buffer. The 16‑bit YUV - /// source is converted to 8‑bit RGBA via the `BITS = 16` Q15 kernel - /// family; alpha = `0xFF` (Yuv422p16 has no alpha plane). + /// source is converted to 8‑bit RGBA via the dedicated `BITS = 16` + /// kernel family (i64 chroma multiply — not the BITS-generic Q15 + /// pipeline); alpha = `0xFF` (Yuv422p16 has no alpha plane). #[cfg_attr(not(tarpaulin), inline(always))] pub fn with_rgba(mut self, buf: &'a mut [u8]) -> Result { self.set_rgba(buf)?; @@ -2146,7 +2147,8 @@ impl<'a> MixedSinker<'a, P216> { /// Attaches a packed **8‑bit** RGBA output buffer. The 16‑bit P216 /// source (semi‑planar, 16 active bits) is converted to 8‑bit RGBA - /// via the `BITS = 16` Q15 kernel family; alpha = `0xFF` (P216 has + /// via the dedicated `BITS = 16` kernel family (i64 chroma multiply + /// — not the BITS-generic Q15 pipeline); alpha = `0xFF` (P216 has /// no alpha plane). #[cfg_attr(not(tarpaulin), inline(always))] pub fn with_rgba(mut self, buf: &'a mut [u8]) -> Result {