styx_codec/decoder/raw/
yuyv.rs

1use styx_core::prelude::*;
2
3use crate::decoder::raw::yuv_to_rgb;
4#[cfg(feature = "image")]
5use crate::decoder::{ImageDecode, process_to_dynamic};
6use crate::{Codec, CodecDescriptor, CodecError, CodecKind};
7use yuvutils_rs::{YuvPackedImage, YuvRange, YuvStandardMatrix};
8
9#[inline(always)]
10fn map_colorspace(color: ColorSpace) -> (YuvRange, YuvStandardMatrix) {
11    match color {
12        ColorSpace::Bt709 => (YuvRange::Limited, YuvStandardMatrix::Bt709),
13        ColorSpace::Bt2020 => (YuvRange::Limited, YuvStandardMatrix::Bt2020),
14        // In our frame metadata `Srgb` is used as a "full-range output" hint; libcamera often
15        // reports `sYCC` for PiSP outputs, which uses a Rec.601 YCbCr matrix.
16        ColorSpace::Srgb => (YuvRange::Full, YuvStandardMatrix::Bt601),
17        ColorSpace::Unknown => (YuvRange::Limited, YuvStandardMatrix::Bt709),
18    }
19}
20
21/// CPU YUYV422 → RGB24 decoder.
22pub struct YuyvToRgbDecoder {
23    descriptor: CodecDescriptor,
24    pool: BufferPool,
25}
26
27impl YuyvToRgbDecoder {
28    pub fn new(max_width: u32, max_height: u32) -> Self {
29        let bytes = max_width as usize * max_height as usize * 3;
30        Self::with_pool(BufferPool::with_limits(2, bytes, 4))
31    }
32
33    pub fn with_pool(pool: BufferPool) -> Self {
34        Self {
35            descriptor: CodecDescriptor {
36                kind: CodecKind::Decoder,
37                input: FourCc::new(*b"YUYV"),
38                output: FourCc::new(*b"RG24"),
39                name: "yuv2rgb",
40                impl_name: "yuyv-cpu",
41            },
42            pool,
43        }
44    }
45
46    /// Decode into a caller-provided tightly-packed RGB24 buffer.
47    ///
48    /// `dst` must be at least `width * height * 3` bytes.
49    pub fn decode_into(&self, input: &FrameLease, dst: &mut [u8]) -> Result<FrameMeta, CodecError> {
50        let meta = input.meta();
51        if meta.format.code != self.descriptor.input {
52            return Err(CodecError::FormatMismatch {
53                expected: self.descriptor.input,
54                actual: meta.format.code,
55            });
56        }
57        let width = meta.format.resolution.width.get() as usize;
58        let height = meta.format.resolution.height.get() as usize;
59        let color = meta.format.color;
60        let plane = input
61            .planes()
62            .into_iter()
63            .next()
64            .ok_or_else(|| CodecError::Codec("yuyv frame missing plane".into()))?;
65        let stride = plane.stride().max(width * 2);
66        let required = stride
67            .checked_mul(height)
68            .ok_or_else(|| CodecError::Codec("yuyv stride overflow".into()))?;
69        if plane.data().len() < required {
70            return Err(CodecError::Codec("yuyv plane buffer too short".into()));
71        }
72
73        let row_bytes = width
74            .checked_mul(3)
75            .ok_or_else(|| CodecError::Codec("yuyv output overflow".into()))?;
76        let out_len = row_bytes
77            .checked_mul(height)
78            .ok_or_else(|| CodecError::Codec("yuyv output overflow".into()))?;
79        if dst.len() < out_len {
80            return Err(CodecError::Codec("yuyv dst buffer too short".into()));
81        }
82        let dst = &mut dst[..out_len];
83
84        let src = &plane.data()[..required];
85        let packed = YuvPackedImage {
86            yuy: src,
87            yuy_stride: stride as u32,
88            width: width as u32,
89            height: height as u32,
90        };
91        let (range, matrix) = map_colorspace(color);
92        if yuvutils_rs::yuyv422_to_rgb(&packed, dst, row_bytes as u32, range, matrix).is_err() {
93            for y in 0..height {
94                let src_line = &src[y * stride..][..width * 2];
95                let dst_line = &mut dst[y * row_bytes..(y + 1) * row_bytes];
96                for (dst_px, src_px) in dst_line
97                    .chunks_exact_mut(6)
98                    .zip(src_line[..width * 2].chunks_exact(4))
99                {
100                    let y0 = unsafe { *src_px.get_unchecked(0) as i32 };
101                    let u = unsafe { *src_px.get_unchecked(1) as i32 };
102                    let y1 = unsafe { *src_px.get_unchecked(2) as i32 };
103                    let v = unsafe { *src_px.get_unchecked(3) as i32 };
104                    let (r0, g0, b0) = yuv_to_rgb(y0, u, v, color);
105                    let (r1, g1, b1) = yuv_to_rgb(y1, u, v, color);
106                    unsafe {
107                        *dst_px.get_unchecked_mut(0) = r0;
108                        *dst_px.get_unchecked_mut(1) = g0;
109                        *dst_px.get_unchecked_mut(2) = b0;
110                        *dst_px.get_unchecked_mut(3) = r1;
111                        *dst_px.get_unchecked_mut(4) = g1;
112                        *dst_px.get_unchecked_mut(5) = b1;
113                    }
114                }
115            }
116        }
117
118        Ok(FrameMeta::new(
119            MediaFormat::new(
120                self.descriptor.output,
121                meta.format.resolution,
122                meta.format.color,
123            ),
124            meta.timestamp,
125        ))
126    }
127}
128
129impl Codec for YuyvToRgbDecoder {
130    fn descriptor(&self) -> &CodecDescriptor {
131        &self.descriptor
132    }
133
134    fn process(&self, input: FrameLease) -> Result<FrameLease, CodecError> {
135        let layout = plane_layout_from_dims(
136            input.meta().format.resolution.width,
137            input.meta().format.resolution.height,
138            3,
139        );
140        let mut buf = self.pool.lease();
141        unsafe { buf.resize_uninit(layout.len) };
142        let meta = self.decode_into(&input, buf.as_mut_slice())?;
143        Ok(unsafe { FrameLease::single_plane_uninit(meta, buf, layout.len, layout.stride) })
144    }
145}
146
147#[cfg(feature = "image")]
148impl ImageDecode for YuyvToRgbDecoder {
149    fn decode_image(&self, frame: FrameLease) -> Result<image::DynamicImage, CodecError> {
150        process_to_dynamic(self, frame)
151    }
152}
153
154/// CPU YUYV422 → GREY decoder (extract luma plane).
155pub struct YuyvToLumaDecoder {
156    descriptor: CodecDescriptor,
157    pool: BufferPool,
158}
159
160impl YuyvToLumaDecoder {
161    pub fn new(max_width: u32, max_height: u32) -> Self {
162        let bytes = max_width as usize * max_height as usize;
163        Self::with_pool(BufferPool::with_limits(2, bytes, 4))
164    }
165
166    pub fn with_pool(pool: BufferPool) -> Self {
167        Self {
168            descriptor: CodecDescriptor {
169                kind: CodecKind::Decoder,
170                input: FourCc::new(*b"YUYV"),
171                output: FourCc::new(*b"GREY"),
172                name: "yuv2luma",
173                impl_name: "yuyv-luma",
174            },
175            pool,
176        }
177    }
178
179    /// Decode into a caller-provided tightly-packed GREY buffer.
180    ///
181    /// `dst` must be at least `width * height` bytes.
182    pub fn decode_into(&self, input: &FrameLease, dst: &mut [u8]) -> Result<FrameMeta, CodecError> {
183        let meta = input.meta();
184        if meta.format.code != self.descriptor.input {
185            return Err(CodecError::FormatMismatch {
186                expected: self.descriptor.input,
187                actual: meta.format.code,
188            });
189        }
190        let width = meta.format.resolution.width.get() as usize;
191        let height = meta.format.resolution.height.get() as usize;
192        let plane = input
193            .planes()
194            .into_iter()
195            .next()
196            .ok_or_else(|| CodecError::Codec("yuyv frame missing plane".into()))?;
197        let stride = plane.stride().max(width * 2);
198        let required = stride
199            .checked_mul(height)
200            .ok_or_else(|| CodecError::Codec("yuyv stride overflow".into()))?;
201        if plane.data().len() < required {
202            return Err(CodecError::Codec("yuyv plane buffer too short".into()));
203        }
204
205        let out_len = width
206            .checked_mul(height)
207            .ok_or_else(|| CodecError::Codec("yuyv luma output overflow".into()))?;
208        if dst.len() < out_len {
209            return Err(CodecError::Codec("yuyv luma dst buffer too short".into()));
210        }
211        let dst = &mut dst[..out_len];
212        let src = &plane.data()[..required];
213
214        #[cfg(target_arch = "aarch64")]
215        {
216            use std::arch::aarch64::{vld2q_u8, vst1q_u8};
217            for y in 0..height {
218                let src_line = &src[y * stride..][..width * 2];
219                let dst_line = &mut dst[y * width..(y + 1) * width];
220                unsafe {
221                    let src_ptr = src_line.as_ptr();
222                    let dst_ptr = dst_line.as_mut_ptr();
223                    let blocks = width / 16;
224                    for i in 0..blocks {
225                        let src_block = src_ptr.add(i * 32) as *const u8;
226                        let yuv = vld2q_u8(src_block);
227                        vst1q_u8(dst_ptr.add(i * 16), yuv.0);
228                    }
229                    let start = blocks * 16;
230                    for x in start..width {
231                        *dst_ptr.add(x) = *src_ptr.add(x * 2);
232                    }
233                }
234            }
235        }
236        #[cfg(not(target_arch = "aarch64"))]
237        {
238            for y in 0..height {
239                let src_line = &src[y * stride..][..width * 2];
240                let dst_line = &mut dst[y * width..(y + 1) * width];
241                unsafe {
242                    let src_ptr = src_line.as_ptr();
243                    let dst_ptr = dst_line.as_mut_ptr();
244                    for x in 0..width {
245                        *dst_ptr.add(x) = *src_ptr.add(x * 2);
246                    }
247                }
248            }
249        }
250
251        Ok(FrameMeta::new(
252            MediaFormat::new(
253                self.descriptor.output,
254                meta.format.resolution,
255                meta.format.color,
256            ),
257            meta.timestamp,
258        ))
259    }
260}
261
262impl Codec for YuyvToLumaDecoder {
263    fn descriptor(&self) -> &CodecDescriptor {
264        &self.descriptor
265    }
266
267    fn process(&self, input: FrameLease) -> Result<FrameLease, CodecError> {
268        let layout = plane_layout_from_dims(
269            input.meta().format.resolution.width,
270            input.meta().format.resolution.height,
271            1,
272        );
273        let mut buf = self.pool.lease();
274        unsafe { buf.resize_uninit(layout.len) };
275        let meta = self.decode_into(&input, buf.as_mut_slice())?;
276        Ok(unsafe { FrameLease::single_plane_uninit(meta, buf, layout.len, layout.stride) })
277    }
278}
279
280#[cfg(feature = "image")]
281impl ImageDecode for YuyvToLumaDecoder {
282    fn decode_image(&self, frame: FrameLease) -> Result<image::DynamicImage, CodecError> {
283        process_to_dynamic(self, frame)
284    }
285}
286
287#[cfg(test)]
288mod tests {
289    use super::*;
290
291    fn make_yuyv_frame(bytes: &[u8], res: Resolution, stride: usize) -> FrameLease {
292        let mut buf = BufferPool::with_limits(1, bytes.len(), 1).lease();
293        buf.resize(bytes.len());
294        buf.as_mut_slice().copy_from_slice(bytes);
295        FrameLease::single_plane(
296            FrameMeta::new(MediaFormat::new(FourCc::new(*b"YUYV"), res, ColorSpace::Srgb), 1),
297            buf,
298            bytes.len(),
299            stride,
300        )
301    }
302
303    #[test]
304    fn yuyv_decode_into_matches_process_rgb() {
305        let res = Resolution::new(2, 1).unwrap();
306        let bytes = [10u8, 128, 20, 128];
307        let make = || make_yuyv_frame(&bytes, res, 4);
308        let dec = YuyvToRgbDecoder::new(2, 1);
309
310        let processed = dec.process(make()).unwrap();
311        let plane = processed.planes().into_iter().next().unwrap();
312
313        let mut out = vec![0u8; 2 * 3];
314        let frame = make();
315        dec.decode_into(&frame, &mut out).unwrap();
316        assert_eq!(plane.data(), out.as_slice());
317    }
318
319    #[test]
320    fn yuyv_decode_into_matches_process_luma() {
321        let res = Resolution::new(2, 1).unwrap();
322        let bytes = [10u8, 128, 20, 128];
323        let make = || make_yuyv_frame(&bytes, res, 4);
324        let dec = YuyvToLumaDecoder::new(2, 1);
325
326        let processed = dec.process(make()).unwrap();
327        let plane = processed.planes().into_iter().next().unwrap();
328
329        let mut out = vec![0u8; 2];
330        let frame = make();
331        dec.decode_into(&frame, &mut out).unwrap();
332        assert_eq!(plane.data(), out.as_slice());
333    }
334}