Skip to main content

ithmb_core/simd/
mod.rs

1//! SIMD-accelerated pixel conversions.
2//!
3//! Each public function has a platform-specific implementation (e.g. SSE2 on
4//! `x86_64`) **and** a portable scalar fallback so the code compiles
5//! without SIMD.
6//!
7//! Callers select the accelerated path with the `simd` Cargo feature.
8#![allow(unsafe_code)]
9#![allow(clippy::cast_ptr_alignment, clippy::cast_possible_truncation, clippy::similar_names)]
10
11//!
12//! # Feature gate
13//!
14//! ```toml
15//! [features]
16//! simd = []
17//! ```
18//!
19//! When disabled every function in this module reduces to the same scalar code
20//! used without the module — zero behaviour change.
21
22// ---------------------------------------------------------------------------
23// Sub-modules: per-format SIMD implementations
24// ---------------------------------------------------------------------------
25#[cfg(feature = "simd")]
26mod cl;
27#[cfg(feature = "simd")]
28mod clcl;
29mod reordered;
30mod rgb555;
31#[cfg(feature = "simd")]
32mod rgb565;
33#[cfg(feature = "simd")]
34mod uyvy;
35#[cfg(feature = "simd")]
36mod yuv;
37
38// Scalar fallbacks — always available (used when SIMD is off or for
39// remainder handling in NEON routines).
40#[cfg_attr(
41    all(feature = "simd", any(target_arch = "x86_64", target_arch = "x86")),
42    allow(dead_code)
43)]
44mod scalar;
45
46#[cfg(all(feature = "simd", target_arch = "aarch64", not(target_os = "macos")))]
47mod neon;
48
49// Re-export dispatch functions that live in sub-modules.
50#[cfg(feature = "simd")]
51#[allow(unused_imports)]
52pub(crate) use clcl::clcl_row_to_bgra;
53pub(crate) use reordered::rgb555_pack_to_bgra;
54
55// ---------------------------------------------------------------------------
56// Imports for test comparisons (SIMD/scalar cross-check).
57// ---------------------------------------------------------------------------
58// No import needed — use fully-qualified `crate::yuv::` in tests.
59
60/// Nibble-to-byte lookup table: maps each 4-bit value (0–15) to `nibble << 4 (= nibble * 16)`.
61/// Used with `_mm_shuffle_epi8` (SSSE3) / `_mm256_shuffle_epi8` (AVX2) to
62/// expand packed CL/CLCL nibble chroma to full 8-bit values in a single
63/// instruction — replaces per-pixel shift+mask+multiply.
64#[allow(dead_code)]
65pub(crate) const CL_NIBBLE_TABLE: [u8; 16] = [0, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240];
66
67// ---------------------------------------------------------------------------
68// Helper functions  (used by scalar fallbacks)
69// ---------------------------------------------------------------------------
70
71#[inline]
72#[must_use]
73pub(super) fn unpack_rgb565(pixel: u16) -> [u8; 4] {
74    let r5 = u32::from((pixel >> 11) & 0x1F);
75    let g6 = u32::from((pixel >> 5) & 0x3F);
76    let b5 = u32::from(pixel & 0x1F);
77    [msb_replicate_5(b5), msb_replicate_6(g6), msb_replicate_5(r5), 255]
78}
79
80#[inline]
81#[must_use]
82pub(super) fn msb_replicate_5(v: u32) -> u8 {
83    ((v << 3) | (v >> 2)) as u8
84}
85
86#[inline]
87#[must_use]
88#[allow(clippy::cast_possible_truncation)]
89pub(super) fn msb_replicate_6(v: u32) -> u8 {
90    ((v << 2) | (v >> 4)) as u8
91}
92
93#[inline]
94#[must_use]
95pub(super) fn unpack_rgb555(pixel: u16) -> [u8; 4] {
96    // Layout: xRRRRRGGGGGBBBBB (MSB bit 15 unused)
97    let r5 = u32::from((pixel >> 10) & 0x1F);
98    let g5 = u32::from((pixel >> 5) & 0x1F);
99    let b5 = u32::from(pixel & 0x1F);
100    [msb_replicate_5(b5), msb_replicate_5(g5), msb_replicate_5(r5), 255]
101}
102
103// ---- Fill gray row (SSE2) ----
104
105/// SAFETY: must only be called on `x86`/`x86_64` where SSE2 is guaranteed.
106#[cfg(all(feature = "simd", any(target_arch = "x86_64", target_arch = "x86")))]
107#[allow(unsafe_op_in_unsafe_fn, clippy::cast_ptr_alignment)]
108pub(crate) unsafe fn fill_gray_row_sse2(gray: &[u8]) -> Vec<u8> {
109    use core::arch::x86_64::{
110        __m128i, _mm_loadl_epi64, _mm_or_si128, _mm_set1_epi32, _mm_setzero_si128, _mm_slli_epi32, _mm_storeu_si128,
111        _mm_unpackhi_epi16, _mm_unpacklo_epi8, _mm_unpacklo_epi16,
112    };
113    let n = gray.len();
114    let mut dst = vec![0u8; n * 4];
115    let mut i = 0;
116
117    let alpha_mask = _mm_set1_epi32(0xFFi32 << 24);
118    while i + 8 <= n {
119        let v = _mm_loadl_epi64(gray.as_ptr().add(i).cast::<__m128i>());
120        let w = _mm_unpacklo_epi8(v, _mm_setzero_si128());
121        let lo = _mm_unpacklo_epi16(w, _mm_setzero_si128());
122        let hi = _mm_unpackhi_epi16(w, _mm_setzero_si128());
123
124        let lo_sh8 = _mm_slli_epi32(lo, 8);
125        let lo_sh16 = _mm_slli_epi32(lo, 16);
126        let hi_sh8 = _mm_slli_epi32(hi, 8);
127        let hi_sh16 = _mm_slli_epi32(hi, 16);
128
129        let lo_bgra = _mm_or_si128(_mm_or_si128(_mm_or_si128(lo, lo_sh8), lo_sh16), alpha_mask);
130        let hi_bgra = _mm_or_si128(_mm_or_si128(_mm_or_si128(hi, hi_sh8), hi_sh16), alpha_mask);
131
132        _mm_storeu_si128(dst.as_mut_ptr().add(i * 4).cast::<__m128i>(), lo_bgra);
133        _mm_storeu_si128(dst.as_mut_ptr().add(i * 4 + 16).cast::<__m128i>(), hi_bgra);
134        i += 8;
135    }
136
137    for (j, &g) in gray.iter().enumerate().skip(i) {
138        let o = j * 4;
139        dst[o] = g;
140        dst[o + 1] = g;
141        dst[o + 2] = g;
142        dst[o + 3] = 255;
143    }
144    dst
145}
146
147// ---------------------------------------------------------------------------
148// BT.601 YCbCr → BGRA  (2 pixels sharing Cb/Cr, as in UYVY / YCbCr 4:2:2)
149// ---------------------------------------------------------------------------
150
151/// Convert one UYVY quad (4 bytes) to two BGRA pixels (8 bytes).
152///
153/// Input layout: `[U (Cb), Y0, V (Cr), Y1]`
154/// Output layout: `[B0, G0, R0, A0, B1, G1, R1, A1]` (alpha = 255).
155///
156/// # SIMD
157///
158/// On `x86_64` with SSE2 this processes the quad with 16-bit fixed-point
159/// arithmetic in a single SSE register pass, retiring both pixels in ~10
160/// instructions (versus ~40 for two scalar calls).
161#[inline]
162#[must_use]
163#[allow(clippy::trivially_copy_pass_by_ref)]
164pub fn uyvy_quad_to_bgra(quad: &[u8; 4]) -> [u8; 8] {
165    #[cfg(all(feature = "simd", any(target_arch = "x86_64", target_arch = "x86")))]
166    // SAFETY: x86_64/x86 guarantees SSE2.
167    unsafe {
168        uyvy::uyvy_quad_to_bgra_sse2(quad)
169    }
170
171    #[cfg(all(feature = "simd", target_arch = "aarch64", not(target_os = "macos")))]
172    // SAFETY: aarch64 guarantees NEON.
173    unsafe {
174        return neon::uyvy_quad_to_bgra_neon(quad);
175    }
176
177    #[cfg(not(any(
178        all(feature = "simd", any(target_arch = "x86_64", target_arch = "x86")),
179        all(feature = "simd", target_arch = "aarch64", not(target_os = "macos")),
180    )))]
181    scalar::uyvy_quad_to_bgra(quad)
182}
183
184/// Convert two UYVY quads (8 bytes) to four BGRA pixels (16 bytes).
185///
186/// Twice as wide as [`uyvy_quad_to_bgra`] — better amortises SSE register
187/// setup when callers have at least 8 bytes of input (the common case).
188#[inline]
189#[must_use]
190#[allow(clippy::trivially_copy_pass_by_ref)]
191pub fn uyvy_double_quad_to_bgra(quads: &[u8; 8]) -> [u8; 16] {
192    #[cfg(all(feature = "simd", any(target_arch = "x86_64", target_arch = "x86")))]
193    // SAFETY: x86_64/x86 guarantees SSE2.
194    unsafe {
195        uyvy::uyvy_double_quad_to_bgra_sse2(quads)
196    }
197
198    #[cfg(all(feature = "simd", target_arch = "aarch64", not(target_os = "macos")))]
199    // SAFETY: aarch64 guarantees NEON.
200    unsafe {
201        return neon::uyvy_double_quad_to_bgra_neon(quads);
202    }
203
204    #[cfg(not(any(
205        all(feature = "simd", any(target_arch = "x86_64", target_arch = "x86")),
206        all(feature = "simd", target_arch = "aarch64", not(target_os = "macos")),
207    )))]
208    scalar::uyvy_double_quad_to_bgra(quads)
209}
210
211/// Convert a full row of UYVY data (4-byte quads) to BGRA.
212///
213/// Input: `src` contains `(w/2) * 4` bytes of UYVY quads (no odd-width trailing pixel).
214/// Output: `dst` contains `(w/2) * 8` bytes of BGRA pixels.
215///
216/// # Panics
217///
218/// When `src` is not a multiple of 4 bytes, or when `dst` is not `src.len() * 2` bytes.
219#[cfg(feature = "simd")]
220#[inline]
221pub fn uyvy_row_to_bgra(src: &[u8], dst: &mut [u8]) {
222    #[cfg(all(feature = "simd", target_arch = "x86_64"))]
223    // SAFETY: checked by is_x86_feature_detected! below.
224    if is_x86_feature_detected!("avx2") {
225        return unsafe { uyvy::uyvy_row_to_bgra_avx2(src, dst) };
226    }
227    #[cfg(all(feature = "simd", target_arch = "x86_64"))]
228    // SAFETY: checked by is_x86_feature_detected! below.
229    if is_x86_feature_detected!("sse4.1") {
230        return unsafe { uyvy::uyvy_row_to_bgra_sse41(src, dst) };
231    }
232    let n = src.len();
233    debug_assert_eq!(dst.len(), (n / 4) * 8);
234    let full_end = (n / 16) * 16;
235    let mut i = 0usize;
236
237    // Process 4 quads (8 pixels = 16 input bytes) per iteration.
238    while i < full_end {
239        #[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
240        // SAFETY: x86_64/x86 guarantees SSE2.
241        unsafe {
242            let q0 = uyvy::uyvy_double_quad_to_bgra_sse2(&src[i..i + 8].try_into().unwrap());
243            let q1 = uyvy::uyvy_double_quad_to_bgra_sse2(&src[i + 8..i + 16].try_into().unwrap());
244            let d_off = i * 2;
245            dst[d_off..d_off + 16].copy_from_slice(&q0);
246            dst[d_off + 16..d_off + 32].copy_from_slice(&q1);
247        }
248
249        #[cfg(all(target_arch = "aarch64", not(target_os = "macos")))]
250        // SAFETY: aarch64 guarantees NEON.
251        unsafe {
252            let q0 = neon::uyvy_double_quad_to_bgra_neon(&src[i..i + 8].try_into().unwrap());
253            let q1 = neon::uyvy_double_quad_to_bgra_neon(&src[i + 8..i + 16].try_into().unwrap());
254            let d_off = i * 2;
255            dst[d_off..d_off + 16].copy_from_slice(&q0);
256            dst[d_off + 16..d_off + 32].copy_from_slice(&q1);
257        }
258
259        #[cfg(not(any(
260            target_arch = "x86_64",
261            target_arch = "x86",
262            all(target_arch = "aarch64", not(target_os = "macos"))
263        )))]
264        {
265            let q0 = scalar::uyvy_double_quad_to_bgra(&src[i..i + 8].try_into().unwrap());
266            let q1 = scalar::uyvy_double_quad_to_bgra(&src[i + 8..i + 16].try_into().unwrap());
267            let d_off = i * 2;
268            dst[d_off..d_off + 16].copy_from_slice(&q0);
269            dst[d_off + 16..d_off + 32].copy_from_slice(&q1);
270        }
271
272        i += 16;
273    }
274
275    // Remainder: 0-3 quads processed individually.
276    while i < n {
277        #[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
278        // SAFETY: x86_64/x86 guarantees SSE2.
279        unsafe {
280            let px = uyvy::uyvy_quad_to_bgra_sse2(&src[i..i + 4].try_into().unwrap());
281            let d_off = i * 2;
282            dst[d_off..d_off + 8].copy_from_slice(&px);
283        }
284
285        #[cfg(all(target_arch = "aarch64", not(target_os = "macos")))]
286        // SAFETY: aarch64 guarantees NEON.
287        unsafe {
288            let px = neon::uyvy_quad_to_bgra_neon(&src[i..i + 4].try_into().unwrap());
289            let d_off = i * 2;
290            dst[d_off..d_off + 8].copy_from_slice(&px);
291        }
292
293        #[cfg(not(any(
294            target_arch = "x86_64",
295            target_arch = "x86",
296            all(target_arch = "aarch64", not(target_os = "macos"))
297        )))]
298        {
299            let px = scalar::uyvy_quad_to_bgra(&src[i..i + 4].try_into().unwrap());
300            let d_off = i * 2;
301            dst[d_off..d_off + 8].copy_from_slice(&px);
302        }
303
304        i += 4;
305    }
306}
307
308// ---------------------------------------------------------------------------
309// YCbCr 4:2:0 → BGRA  (4 pixels sharing one Cb/Cr pair, as in YCbCr 4:2:0)
310// ---------------------------------------------------------------------------
311
312/// Convert 4 YCbCr 4:2:0 pixels sharing Cb/Cr to 4 BGRA pixels (16 bytes).
313///
314/// Input layout: `[Y0, Y1, Y2, Y3, Cb, Cr]` — 6 bytes
315/// Output layout: `[B0, G0, R0, A0, B1, G1, R1, A1, B2, G2, R2, A2, B3, G3, R3, A3]`
316///
317/// This is the core inner-loop primitive called by the YCbCr 4:2:0 decoder
318/// for each macroblock. On `x86_64` with SSE2 it processes all 4 pixels with
319/// packed `i32` arithmetic.
320#[inline]
321#[must_use]
322#[allow(clippy::trivially_copy_pass_by_ref)]
323pub fn yuv420_quad_to_bgra(quad: &[u8; 6]) -> [u8; 16] {
324    // SSE2 path (compile-time guaranteed on x86_64/x86)
325    #[cfg(all(feature = "simd", any(target_arch = "x86_64", target_arch = "x86")))]
326    // SAFETY: x86_64/x86 guarantees SSE2.
327    unsafe {
328        yuv::yuv420_quad_to_bgra_sse2(quad)
329    }
330
331    #[cfg(not(any(all(feature = "simd", any(target_arch = "x86_64", target_arch = "x86")),)))]
332    // Scalar fallback (used on all non-x86_64 platforms, including aarch64+simd)
333    scalar::yuv420_quad_to_bgra(quad)
334}
335
336/// Convert an entire row-pair of YCbCr 4:2:0 data (2 rows of Y, 1 row each of Cb/Cr
337///
338/// Each 4-pixel macroblock (2x2) is decoded via the platform-specific SIMD primitive,
339/// bypassing the per-macroblock dispatch overhead.
340///
341/// # Arguments
342///
343/// * `y_row` - Two rows of Y data (`2 * w` bytes)
344/// * `cb_row` - One row of Cb data (`cb_w` bytes)
345/// * `cr_row` - One row of Cr data (`cb_w` bytes)
346/// * `dst` - Output buffer (`2 * w * 4` bytes)
347/// * `w` - Width in pixels
348/// * `cb_w` - Chroma width (`w / 2`)
349#[cfg(feature = "simd")]
350#[inline]
351pub fn yuv420_row_pair_to_bgra(y_row: &[u8], cb_row: &[u8], cr_row: &[u8], dst: &mut [u8], w: usize, cb_w: usize) {
352    #[cfg(all(feature = "simd", target_arch = "x86_64"))]
353    // SAFETY: checked by is_x86_feature_detected! below.
354    if is_x86_feature_detected!("avx2") {
355        return unsafe { yuv::yuv420_row_pair_to_bgra_avx2(y_row, cb_row, cr_row, dst, w, cb_w) };
356    }
357    #[cfg(all(feature = "simd", target_arch = "x86_64"))]
358    // SAFETY: checked by is_x86_feature_detected! below.
359    if is_x86_feature_detected!("sse4.1") {
360        return unsafe { yuv::yuv420_row_pair_to_bgra_sse41(y_row, cb_row, cr_row, dst, w, cb_w) };
361    }
362    for cx in 0..cb_w {
363        let quad = [
364            y_row[cx * 2],
365            y_row[cx * 2 + 1],
366            y_row[w + cx * 2],
367            y_row[w + cx * 2 + 1],
368            cb_row[cx],
369            cr_row[cx],
370        ];
371        #[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
372        // SAFETY: x86_64/x86 guarantees SSE2.
373        let out = unsafe { yuv::yuv420_quad_to_bgra_sse2(&quad) };
374        #[cfg(not(any(
375            target_arch = "x86_64",
376            target_arch = "x86",
377            all(target_arch = "aarch64", not(target_os = "macos"))
378        )))]
379        let out = scalar::yuv420_quad_to_bgra(&quad);
380
381        let off = cx * 8;
382        dst[off..off + 4].copy_from_slice(&out[0..4]);
383        dst[off + 4..off + 8].copy_from_slice(&out[4..8]);
384        let off2 = off + w * 4;
385        dst[off2..off2 + 4].copy_from_slice(&out[8..12]);
386        dst[off2 + 4..off2 + 8].copy_from_slice(&out[12..16]);
387    }
388}
389
390// ---------------------------------------------------------------------------
391// RGB565 row → BGRA
392// ---------------------------------------------------------------------------
393
394/// In-place row conversion with runtime SIMD dispatch.
395///
396/// 1. AVX2  (16 px/iter) — `x86_64`, runtime `is_x86_feature_detected!("avx2")`
397/// 2. SSE2  (8 px/iter)  — `x86_64`/`x86` (guaranteed on these platforms)
398/// 3. Scalar fallback     — always available
399#[inline]
400pub(crate) fn rgb565_apply_row_to_bgra(src: &[u8], dst: &mut [u8]) {
401    #[cfg(all(feature = "simd", target_arch = "x86_64"))]
402    // SAFETY: checked by is_x86_feature_detected! below.
403    if is_x86_feature_detected!("avx2") {
404        unsafe {
405            return rgb565::rgb565_row_to_bgra_avx2(src, dst);
406        }
407    }
408
409    #[cfg(all(feature = "simd", any(target_arch = "x86_64", target_arch = "x86")))]
410    // SAFETY: x86_64/x86 guarantees SSE2.
411    unsafe {
412        rgb565::rgb565_row_to_bgra_sse2(src, dst);
413    }
414    #[cfg(not(all(feature = "simd", any(target_arch = "x86_64", target_arch = "x86"))))]
415    scalar::rgb565_row_to_bgra_scalar(src, dst);
416}
417
418/// Convert one row of RGB565 pixels to BGRA8.
419///
420/// Input: 2 bytes per pixel, 4 pixels = 8 bytes minimum.
421/// Output: 4 bytes per pixel (BGRA).
422///
423/// # SIMD
424///
425/// On `x86_64` with SSE2 this processes 4 pixels at a time using packed
426/// 16-bit arithmetic with bit extraction and MSB replication.
427#[inline]
428#[must_use]
429pub fn rgb565_row_to_bgra(src: &[u8]) -> Vec<u8> {
430    let n_pixels = src.len() / 2;
431    let mut dst = vec![0u8; n_pixels * 4];
432    rgb565_apply_row_to_bgra(src, &mut dst);
433    dst
434}
435
436// ---------------------------------------------------------------------------
437// RGB555 row → BGRA
438// ---------------------------------------------------------------------------
439
440/// In-place row conversion with runtime SIMD dispatch.
441///
442/// 1. AVX2  (16 px/iter) — `x86_64`, runtime `is_x86_feature_detected!("avx2")`
443/// 2. SSE2  (8 px/iter)  — `x86_64`/`x86` (guaranteed on these platforms)
444/// 3. Scalar fallback     — always available
445#[inline]
446pub(crate) fn rgb555_apply_row_to_bgra(src: &[u8], dst: &mut [u8]) {
447    #[cfg(all(feature = "simd", target_arch = "x86_64"))]
448    // SAFETY: checked by is_x86_feature_detected! below.
449    if is_x86_feature_detected!("avx2") {
450        unsafe {
451            return rgb555::rgb555_row_to_bgra_avx2(src, dst);
452        }
453    }
454
455    #[cfg(all(feature = "simd", any(target_arch = "x86_64", target_arch = "x86")))]
456    // SAFETY: x86_64/x86 guarantees SSE2.
457    unsafe {
458        rgb555::rgb555_row_to_bgra_sse2(src, dst);
459    }
460
461    #[cfg(not(all(feature = "simd", any(target_arch = "x86_64", target_arch = "x86"))))]
462    scalar::rgb555_row_to_bgra_scalar(src, dst);
463}
464
465/// Convert one row of RGB555 pixels to BGRA8.
466///
467/// Input: 2 bytes per pixel, 4 pixels = 8 bytes minimum.
468/// Output: 4 bytes per pixel (BGRA).
469#[inline]
470#[must_use]
471pub fn rgb555_row_to_bgra(src: &[u8]) -> Vec<u8> {
472    let n_pixels = src.len() / 2;
473    let mut dst = vec![0u8; n_pixels * 4];
474    rgb555_apply_row_to_bgra(src, &mut dst);
475    dst
476}
477
478// ---------------------------------------------------------------------------
479// Gray/monochrome row -> BGRA
480// ---------------------------------------------------------------------------
481
482/// Convert every byte in a gray buffer to BGRA: `gray[n] -> [gray[n], gray[n], gray[n], 255]`.
483#[must_use]
484pub fn fill_gray_row(gray: &[u8]) -> Vec<u8> {
485    #[cfg(all(feature = "simd", any(target_arch = "x86_64", target_arch = "x86")))]
486    // SAFETY: x86_64/x86 guarantees SSE2.
487    unsafe {
488        fill_gray_row_sse2(gray)
489    }
490
491    #[cfg(all(feature = "simd", target_arch = "aarch64", not(target_os = "macos")))]
492    // SAFETY: aarch64 guarantees NEON.
493    unsafe {
494        return neon::fill_gray_row_neon(gray);
495    }
496
497    #[cfg(not(any(
498        all(feature = "simd", any(target_arch = "x86_64", target_arch = "x86")),
499        all(feature = "simd", target_arch = "aarch64", not(target_os = "macos")),
500    )))]
501    scalar::fill_gray_row(gray)
502}
503
504// ---------------------------------------------------------------------------
505// Luma + shared chroma -> BGRA  (for CLCL)
506// ---------------------------------------------------------------------------
507
508/// Convert luma bytes to BGRA using a single shared Cb/Cr pair.
509///
510/// Processes batches of 4 via `yuv420_quad_to_bgra` (SIMD when possible).
511#[must_use]
512pub fn fill_yuv_row(luma: &[u8], cb: u8, cr: u8) -> Vec<u8> {
513    let n = luma.len();
514    let mut dst = vec![0u8; n * 4];
515    let mut i = 0;
516
517    while i + 4 <= n {
518        let quad = yuv420_quad_to_bgra(&[luma[i], luma[i + 1], luma[i + 2], luma[i + 3], cb, cr]);
519        let o = i * 4;
520        dst[o..o + 16].copy_from_slice(&quad);
521        i += 4;
522    }
523
524    for (j, &y) in luma.iter().enumerate().skip(i) {
525        let px = crate::yuv::yuv_to_bgra(y, cb, cr);
526        let o = j * 4;
527        dst[o..o + 4].copy_from_slice(&px);
528    }
529    dst
530}
531
532// ---------------------------------------------------------------------------
533// CL (per-pixel nibble chroma) quad -> BGRA
534// ---------------------------------------------------------------------------
535
536/// Convert 4 CL planar pixels to 16 BGRA bytes.
537///
538/// Input layout (8 bytes): `[Y0, Y1, Y2, Y3, CbCr0, CbCr1, CbCr2, CbCr3]`
539#[must_use]
540pub fn cl_quad_to_bgra(quad: &[u8; 8]) -> [u8; 16] {
541    #[cfg(all(feature = "simd", target_arch = "x86_64"))]
542    // SAFETY: checked by is_x86_feature_detected! below.
543    unsafe {
544        if is_x86_feature_detected!("sse4.1") {
545            return cl::cl_quad_to_bgra_sse41(quad);
546        }
547        cl::cl_quad_to_bgra_sse2(quad)
548    }
549
550    #[cfg(all(feature = "simd", target_arch = "aarch64", not(target_os = "macos")))]
551    // SAFETY: aarch64 guarantees NEON.
552    unsafe {
553        return neon::cl_quad_to_bgra_neon(quad);
554    }
555    #[cfg(not(all(
556        feature = "simd",
557        any(
558            target_arch = "x86_64",
559            target_arch = "x86",
560            all(target_arch = "aarch64", not(target_os = "macos"))
561        )
562    )))]
563    // Scalar fallback (not needed when SIMD covers all platforms)
564    scalar::cl_quad_to_bgra(*quad)
565}
566
567/// Convert one row of CL planar data to BGRA.
568///
569/// Input `src` layout (`w * 2` bytes):
570///   `src[0..w]` = Y bytes (one per pixel)
571///   `src[w..2*w]` = `CbCr` bytes (Cr in high nibble, Cb in low nibble)
572///
573/// Output `dst`: `w * 4` bytes BGRA.
574///
575/// # Panics
576///
577/// When `dst` is not exactly `src.len() * 2` bytes.
578#[inline]
579pub(crate) fn cl_row_to_bgra(src: &[u8], dst: &mut [u8]) {
580    debug_assert_eq!(dst.len(), src.len() * 2);
581
582    // SSE4.1 packed YUV path (runtime-detected — faster packed clamp + pack)
583    #[cfg(all(feature = "simd", target_arch = "x86_64"))]
584    // SAFETY: checked by is_x86_feature_detected! below.
585    if is_x86_feature_detected!("sse4.1") {
586        unsafe {
587            return cl::cl_row_to_bgra_sse41(src, dst);
588        }
589    }
590
591    // SSE2 path (compile-time guaranteed on x86_64/x86)
592    #[cfg(all(feature = "simd", any(target_arch = "x86_64", target_arch = "x86")))]
593    // SAFETY: x86_64/x86 guarantees SSE2.
594    unsafe {
595        cl::cl_row_to_bgra_sse2(src, dst);
596    }
597
598    // NEON path (compile-time guaranteed on aarch64, gated on macOS — known NEON edge case)
599    #[cfg(all(feature = "simd", target_arch = "aarch64", not(target_os = "macos")))]
600    // SAFETY: aarch64 guarantees NEON.
601    unsafe {
602        return neon::cl_row_to_bgra_neon(src, dst);
603    }
604
605    #[cfg(not(all(
606        feature = "simd",
607        any(
608            target_arch = "x86_64",
609            target_arch = "x86",
610            all(target_arch = "aarch64", not(target_os = "macos"))
611        )
612    )))]
613    scalar::cl_row_to_bgra_scalar(src, dst);
614}
615
616// ---------------------------------------------------------------------------
617// Tests
618// ---------------------------------------------------------------------------
619
620#[cfg(test)]
621#[allow(clippy::unwrap_used)]
622mod tests {
623    use super::*;
624
625    #[test]
626    fn uyvy_quad_neutral_chroma() {
627        // U=128 (neutral Cb), V=128 (neutral Cr), Y0=100, Y1=200
628        // Both pixels should be gray: [100,100,100,255] and [200,200,200,255]
629        let quad = [128u8, 100, 128, 200];
630        let result = uyvy_quad_to_bgra(&quad);
631        assert_eq!(result[..4], [100, 100, 100, 255], "pixel 0 gray");
632        assert_eq!(result[4..], [200, 200, 200, 255], "pixel 1 gray");
633    }
634
635    #[test]
636    fn uyvy_quad_matches_scalar() {
637        // Compare result to scalar yuv_to_bgra for random-ish values.
638        for u in [0u8, 64, 128, 192, 255] {
639            for v in [0u8, 64, 128, 192, 255] {
640                for y0 in [0u8, 64, 128, 192, 255] {
641                    for y1 in [0u8, 64, 128, 192, 255] {
642                        let quad = [u, y0, v, y1];
643                        let result = uyvy_quad_to_bgra(&quad);
644                        let expected_p0 = crate::yuv::yuv_to_bgra(y0, u, v);
645                        let expected_p1 = crate::yuv::yuv_to_bgra(y1, u, v);
646                        assert_eq!(result[..4], expected_p0, "p0 mismatch: u={u}, y0={y0}, v={v}");
647                        assert_eq!(result[4..], expected_p1, "p1 mismatch: u={u}, y1={y1}, v={v}");
648                    }
649                }
650            }
651        }
652    }
653
654    #[test]
655    fn uyvy_double_quad_matches_scalar() {
656        let quads = [128u8, 100, 128, 200, 64, 50, 192, 180];
657        let result = uyvy_double_quad_to_bgra(&quads);
658        let expected_left = uyvy_quad_to_bgra(&[128, 100, 128, 200]);
659        let expected_right = uyvy_quad_to_bgra(&[64, 50, 192, 180]);
660        assert_eq!(result[..8], expected_left);
661        assert_eq!(result[8..], expected_right);
662    }
663
664    #[test]
665    fn yuv420_quad_neutral_chroma_gray() {
666        // Cb=128, Cr=128 (neutral), Y values 0,128,200,255
667        // All pixels should be gray: Y,Y,Y,255
668        let quad = [0u8, 128, 200, 255, 128, 128];
669        let result = yuv420_quad_to_bgra(&quad);
670        let expected: Vec<[u8; 4]> = (0..4)
671            .map(|i| {
672                let y = quad[i];
673                crate::yuv::yuv_to_bgra(y, 128, 128)
674            })
675            .collect();
676        assert_eq!(result[..4], expected[0], "pixel 0");
677        assert_eq!(result[4..8], expected[1], "pixel 1");
678        assert_eq!(result[8..12], expected[2], "pixel 2");
679        assert_eq!(result[12..], expected[3], "pixel 3");
680    }
681
682    #[test]
683    #[cfg_attr(miri, ignore)]
684    fn yuv420_quad_matches_scalar_exhaustive() {
685        // combinations of Y0,Y1,Y2,Y3,Cb,Cr from [0,64,128,192,255].
686        for y0 in [0u8, 64, 128, 192, 255] {
687            for y1 in [0u8, 64, 128, 192, 255] {
688                for y2 in [0u8, 64, 128, 192, 255] {
689                    for y3 in [0u8, 64, 128, 192, 255] {
690                        for cb in [0u8, 64, 128, 192, 255] {
691                            for cr in [0u8, 64, 128, 192, 255] {
692                                let quad = [y0, y1, y2, y3, cb, cr];
693                                let result = yuv420_quad_to_bgra(&quad);
694                                let expected = [
695                                    crate::yuv::yuv_to_bgra(y0, cb, cr),
696                                    crate::yuv::yuv_to_bgra(y1, cb, cr),
697                                    crate::yuv::yuv_to_bgra(y2, cb, cr),
698                                    crate::yuv::yuv_to_bgra(y3, cb, cr),
699                                ];
700                                for (i, exp) in expected.iter().enumerate() {
701                                    let slice = &result[i * 4..(i + 1) * 4];
702                                    assert_eq!(slice, exp, "pixel {i}: yuv420({y0},{y1},{y2},{y3},cb={cb},cr={cr})");
703                                }
704                            }
705                        }
706                    }
707                }
708            }
709        }
710    }
711
712    #[test]
713    #[cfg_attr(miri, ignore)]
714    fn uyvy_quad_matches_scalar_10k_random() {
715        // Generate 10,000 random UYVY quads and compare SSE4.1 result vs scalar.
716        let mut state: u32 = 0xABCD_0001;
717        for _ in 0..10_000 {
718            state = state.wrapping_mul(1_103_515_245).wrapping_add(12_345);
719            let u = (state >> 16) as u8;
720            state = state.wrapping_mul(1_103_515_245).wrapping_add(12_345);
721            let y0 = (state >> 16) as u8;
722            state = state.wrapping_mul(1_103_515_245).wrapping_add(12_345);
723            let v = (state >> 16) as u8;
724            state = state.wrapping_mul(1_103_515_245).wrapping_add(12_345);
725            let y1 = (state >> 16) as u8;
726            let quad = [u, y0, v, y1];
727            let result = uyvy_quad_to_bgra(&quad);
728            let expected_p0 = crate::yuv::yuv_to_bgra(y0, u, v);
729            let expected_p1 = crate::yuv::yuv_to_bgra(y1, u, v);
730            assert_eq!(&result[..4], &expected_p0, "p0 mismatch");
731            assert_eq!(&result[4..], &expected_p1, "p1 mismatch");
732        }
733    }
734
735    #[test]
736    #[cfg_attr(miri, ignore)]
737    fn yuv420_quad_matches_scalar_10k_random() {
738        // Generate 10,000 random YCbCr 4:2:0 quads and compare SSE4.1 vs scalar.
739        let mut state: u32 = 0xABCD_0001;
740        for _ in 0..10_000 {
741            let mut quad = [0u8; 6];
742            for b in &mut quad {
743                state = state.wrapping_mul(1_103_515_245).wrapping_add(12_345);
744                *b = (state >> 16) as u8;
745            }
746            let result = yuv420_quad_to_bgra(&quad);
747            let expected = [
748                crate::yuv::yuv_to_bgra(quad[0], quad[4], quad[5]),
749                crate::yuv::yuv_to_bgra(quad[1], quad[4], quad[5]),
750                crate::yuv::yuv_to_bgra(quad[2], quad[4], quad[5]),
751                crate::yuv::yuv_to_bgra(quad[3], quad[4], quad[5]),
752            ];
753            for (i, exp) in expected.iter().enumerate() {
754                let slice = &result[i * 4..(i + 1) * 4];
755                assert_eq!(slice, exp, "pixel {i}: quad={quad:?}");
756            }
757        }
758    }
759
760    // ---- RGB565 cross-validation ----
761
762    /// Generate a pseudo-random u16 value.
763    fn random_u16(state: &mut u32) -> u16 {
764        *state = state.wrapping_mul(1_103_515_245).wrapping_add(12_345);
765        (*state >> 16) as u16
766    }
767
768    #[test]
769    fn rgb565_row_matches_scalar_10k() {
770        // Generate 10_000 random RGB565 pixel values (20_000 bytes)
771        let mut state: u32 = 0xABCD_0001;
772        let mut src = Vec::with_capacity(20_000);
773        for _ in 0..10_000 {
774            let p = random_u16(&mut state);
775            src.push((p & 0xFF) as u8);
776            src.push((p >> 8) as u8);
777        }
778
779        let result = super::rgb565_row_to_bgra(&src);
780        let mut expected = vec![0u8; 10_000 * 4];
781        super::scalar::rgb565_row_to_bgra_scalar(&src, &mut expected);
782        assert_eq!(result, expected, "RGB565 SIMD/scalar mismatch for 10K random pixels");
783    }
784
785    #[test]
786    fn rgb565_quad_edge_cases() {
787        let pixels: [u16; 8] = [0x0000, 0xFFFF, 0x001F, 0x07E0, 0xF800, 0xFFE0, 0x07FF, 0x1B6A];
788        for &p0 in &pixels {
789            for &p1 in &pixels {
790                let quad = [
791                    (p0 & 0xFF) as u8,
792                    (p0 >> 8) as u8,
793                    (p1 & 0xFF) as u8,
794                    (p1 >> 8) as u8,
795                    (p0 & 0xFF) as u8,
796                    (p0 >> 8) as u8,
797                    (p1 & 0xFF) as u8,
798                    (p1 >> 8) as u8,
799                ];
800
801                let mut expected = vec![0u8; 16];
802                super::scalar::rgb565_row_to_bgra_scalar(&quad, &mut expected);
803
804                let result = super::rgb565_row_to_bgra(&quad);
805                assert_eq!(
806                    &result[..16],
807                    &expected[..],
808                    "RGB565 dispatch mismatch for pixels {p0:#06x}, {p1:#06x}"
809                );
810            }
811        }
812    }
813
814    #[test]
815    fn rgb565_row_remainder_handling() {
816        // Test with 5 pixels (1 remainder after 1 quad)
817        let mut state: u32 = 0xDEAD_BEEF;
818        let mut src = Vec::with_capacity(10);
819        for _ in 0..5 {
820            let p = random_u16(&mut state);
821            src.push((p & 0xFF) as u8);
822            src.push((p >> 8) as u8);
823        }
824
825        let result = super::rgb565_row_to_bgra(&src);
826        let mut expected = vec![0u8; 20];
827        super::scalar::rgb565_row_to_bgra_scalar(&src, &mut expected);
828        assert_eq!(result, expected, "RGB565 remainder handling mismatch");
829    }
830
831    #[test]
832    fn rgb565_row_single_pixel() {
833        let src = [0x00, 0xF8]; // red
834        let result = super::rgb565_row_to_bgra(&src);
835        let mut expected = vec![0u8; 4];
836        super::scalar::rgb565_row_to_bgra_scalar(&src, &mut expected);
837        assert_eq!(result, expected, "RGB565 single pixel mismatch");
838    }
839
840    // ---- RGB555 cross-validation ----
841
842    #[test]
843    fn rgb555_row_matches_scalar_10k() {
844        let mut state: u32 = 0xABCD_0001;
845        let mut src = Vec::with_capacity(20_000);
846        for _ in 0..10_000 {
847            let p = random_u16(&mut state) & 0x7FFF; // mask off bit 15
848            src.push((p & 0xFF) as u8);
849            src.push((p >> 8) as u8);
850        }
851
852        let result = super::rgb555_row_to_bgra(&src);
853        let mut expected = vec![0u8; 10_000 * 4];
854        super::scalar::rgb555_row_to_bgra_scalar(&src, &mut expected);
855        assert_eq!(result, expected, "RGB555 SIMD/scalar mismatch for 10K random pixels");
856    }
857
858    #[test]
859    fn rgb555_quad_edge_cases() {
860        let pixels: [u16; 6] = [0x0000, 0x7FFF, 0x001F, 0x03E0, 0x7C00, 0x7C1F];
861        for &p0 in &pixels {
862            for &p1 in &pixels {
863                let quad = [
864                    (p0 & 0xFF) as u8,
865                    (p0 >> 8) as u8,
866                    (p1 & 0xFF) as u8,
867                    (p1 >> 8) as u8,
868                    (p0 & 0xFF) as u8,
869                    (p0 >> 8) as u8,
870                    (p1 & 0xFF) as u8,
871                    (p1 >> 8) as u8,
872                ];
873
874                let mut expected = vec![0u8; 16];
875                super::scalar::rgb555_row_to_bgra_scalar(&quad, &mut expected);
876
877                let result = super::rgb555_row_to_bgra(&quad);
878                assert_eq!(
879                    &result[..16],
880                    &expected[..],
881                    "RGB555 dispatch mismatch for pixels {p0:#06x}, {p1:#06x}"
882                );
883            }
884        }
885    }
886
887    #[test]
888    fn rgb555_row_remainder_handling() {
889        let mut state: u32 = 0xCAFE_BABE;
890        let mut src = Vec::with_capacity(10);
891        for _ in 0..5 {
892            let p = random_u16(&mut state) & 0x7FFF;
893            src.push((p & 0xFF) as u8);
894            src.push((p >> 8) as u8);
895        }
896
897        let result = super::rgb555_row_to_bgra(&src);
898        let mut expected = vec![0u8; 20];
899        super::scalar::rgb555_row_to_bgra_scalar(&src, &mut expected);
900        assert_eq!(result, expected, "RGB555 remainder handling mismatch");
901    }
902
903    #[test]
904    fn rgb555_row_single_pixel() {
905        let src = [0x00, 0x7C]; // red (RGB555)
906        let result = super::rgb555_row_to_bgra(&src);
907        let mut expected = vec![0u8; 4];
908        super::scalar::rgb555_row_to_bgra_scalar(&src, &mut expected);
909        assert_eq!(result, expected, "RGB555 single pixel mismatch");
910    }
911
912    // ---- fill_gray_row cross-validation ----
913
914    #[test]
915    #[allow(clippy::cast_possible_truncation)]
916    fn fill_gray_row_matches_scalar_1000_random() {
917        let mut state: u32 = 0x1234_5678;
918        let lengths = [1usize, 2, 3, 4, 5, 7, 8, 9, 15, 16, 17, 31, 32, 33, 64, 128, 256];
919        for &len in &lengths {
920            for _ in 0..60 {
921                let mut gray = Vec::with_capacity(len);
922                for _ in 0..len {
923                    state = state.wrapping_mul(1_103_515_245).wrapping_add(12_345);
924                    gray.push((state >> 16) as u8);
925                }
926                let result = fill_gray_row(&gray);
927                let expected = super::scalar::fill_gray_row(&gray);
928                assert_eq!(result, expected, "fill_gray_row mismatch for len={len}");
929            }
930        }
931    }
932
933    // ---- fill_yuv_row cross-validation ----
934
935    #[test]
936    #[allow(clippy::cast_possible_truncation)]
937    fn fill_yuv_row_matches_scalar_1000_random() {
938        let mut state: u32 = 0x9ABC_DEF0;
939        let lengths = [1usize, 2, 3, 4, 5, 7, 8, 9, 15, 16, 17, 31, 32, 33, 64, 128, 256];
940        for &len in &lengths {
941            for _ in 0..60 {
942                let mut luma = Vec::with_capacity(len);
943                for _ in 0..len {
944                    state = state.wrapping_mul(1_103_515_245).wrapping_add(12_345);
945                    luma.push((state >> 16) as u8);
946                }
947                let cb = (state >> 8) as u8;
948                let cr = (state >> 16) as u8;
949                let result = fill_yuv_row(&luma, cb, cr);
950                let mut expected = vec![0u8; len * 4];
951                for (j, &y) in luma.iter().enumerate() {
952                    let px = crate::yuv::yuv_to_bgra(y, cb, cr);
953                    expected[j * 4..j * 4 + 4].copy_from_slice(&px);
954                }
955                assert_eq!(result, expected, "fill_yuv_row mismatch for len={len}");
956            }
957        }
958    }
959
960    // ---- cl_quad_to_bgra cross-validation ----
961
962    #[test]
963    fn cl_quad_matches_scalar_all_nibble_combos() {
964        let y_vals = [0u8, 64, 128, 192, 255];
965        let n_vals = [0u8, 1, 7, 8, 15];
966        for &y0 in &y_vals {
967            for &y1 in &y_vals {
968                for &y2 in &y_vals {
969                    for &y3 in &y_vals {
970                        for &cb_n in &n_vals {
971                            for &cr_n in &n_vals {
972                                let chroma = (cr_n << 4) | cb_n;
973                                let quad = [y0, y1, y2, y3, chroma, chroma, chroma, chroma];
974                                let result = super::cl_quad_to_bgra(&quad);
975                                let expected = super::scalar::cl_quad_to_bgra(quad);
976                                assert_eq!(&result[..], &expected[..], "cl_quad mismatch");
977                            }
978                        }
979                    }
980                }
981            }
982        }
983    }
984
985    #[test]
986    #[cfg_attr(miri, ignore)]
987    fn cl_quad_ssse3_exhaustive_nibble_pair() {
988        // Validate SSSE3 pshufb path against mathematical *17 expansion.
989        #[cfg(all(feature = "simd", any(target_arch = "x86_64", target_arch = "x86")))]
990        if is_x86_feature_detected!("ssse3") {
991            for cb_n in 0..=15u8 {
992                for cr_n in 0..=15u8 {
993                    let chroma = (cr_n << 4) | cb_n;
994                    let quad = [128u8, 128, 128, 128, chroma, chroma, chroma, chroma];
995                    let result = unsafe { cl::cl_quad_to_bgra_ssse3(&quad) };
996                    let cb = cb_n << 4;
997                    let cr = cr_n << 4;
998                    let expected = crate::yuv::yuv_to_bgra(128, cb, cr);
999                    for p in 0..4 {
1000                        let off = p * 4;
1001                        assert_eq!(
1002                            result[off..off + 4],
1003                            expected,
1004                            "SSSE3 pixel {p} mismatch at cb_n={cb_n} cr_n={cr_n}",
1005                        );
1006                    }
1007                }
1008            }
1009        }
1010    }
1011
1012    #[test]
1013    #[cfg_attr(miri, ignore)]
1014    fn cl_quad_avx2_exhaustive_nibble_pair() {
1015        // Validate AVX2 vpshufb path against mathematical *17 expansion.
1016        #[cfg(all(feature = "simd", target_arch = "x86_64"))]
1017        if is_x86_feature_detected!("avx2") {
1018            for cb_n in 0..=15u8 {
1019                for cr_n in 0..=15u8 {
1020                    let chroma = (cr_n << 4) | cb_n;
1021                    let quad = [128u8, 128, 128, 128, chroma, chroma, chroma, chroma];
1022                    let result = unsafe { cl::cl_quad_to_bgra_avx2(&quad) };
1023                    let cb = cb_n << 4;
1024                    let cr = cr_n << 4;
1025                    let expected = crate::yuv::yuv_to_bgra(128, cb, cr);
1026                    for p in 0..4 {
1027                        let off = p * 4;
1028                        assert_eq!(
1029                            result[off..off + 4],
1030                            expected,
1031                            "AVX2 pixel {p} mismatch at cb_n={cb_n} cr_n={cr_n}",
1032                        );
1033                    }
1034                }
1035            }
1036        }
1037    }
1038    #[test]
1039    #[allow(clippy::cast_possible_truncation)]
1040    fn cl_quad_matches_scalar_varying_chroma() {
1041        let mut state: u32 = 0xDEAD_BEEF;
1042        for _ in 0..1000 {
1043            let mut quad = [0u8; 8];
1044            for b in &mut quad {
1045                state = state.wrapping_mul(1_103_515_245).wrapping_add(12_345);
1046                *b = (state >> 16) as u8;
1047            }
1048            let result = super::cl_quad_to_bgra(&quad);
1049            let expected = super::scalar::cl_quad_to_bgra(quad);
1050            assert_eq!(&result[..], &expected[..], "cl_quad mismatch for random quad");
1051        }
1052    }
1053
1054    // ---- cl_row_to_bgra cross-validation ----
1055
1056    #[test]
1057    #[allow(clippy::cast_possible_truncation)]
1058    fn cl_row_matches_scalar_at_various_widths() {
1059        // Test that cl_row_to_bgra matches per-pixel yuv_to_bgra at widths
1060        // that exercise the SIMD batch loop AND the odd-pixel remainder path.
1061        let widths = [1, 2, 3, 4, 5, 7, 8, 9, 15, 16, 17, 31, 32, 33, 64, 128, 256];
1062        let mut state: u32 = 0xDEAD_BEEF;
1063        for &w in &widths {
1064            for _ in 0..20 {
1065                let n = w;
1066                let mut src = vec![0u8; n * 2];
1067                for b in &mut src {
1068                    state = state.wrapping_mul(1_103_515_245).wrapping_add(12_345);
1069                    *b = (state >> 16) as u8;
1070                }
1071                let mut dst = vec![0u8; n * 4];
1072                cl_row_to_bgra(&src, &mut dst);
1073                let (y, chroma) = src.split_at(n);
1074                let mut expected = vec![0u8; n * 4];
1075                for i in 0..n {
1076                    let cr = chroma[i] & 0xF0;
1077                    let cb = (chroma[i] & 0x0F) << 4;
1078                    let px = crate::yuv::yuv_to_bgra(y[i], cb, cr);
1079                    let o = i * 4;
1080                    expected[o..o + 4].copy_from_slice(&px);
1081                }
1082                assert_eq!(dst, expected, "cl_row_to_bgra mismatch at width={w}");
1083            }
1084        }
1085    }
1086
1087    // ---- rgb555_pack tests ----
1088
1089    #[test]
1090    fn rgb555_pack_known_values() {
1091        // White: R=31, G=31, B=31 -> all channels 255
1092        // 0x7FFF LE: [0xFF, 0x7F]
1093        let white = [0xFFu8, 0x7Fu8];
1094        let result = super::rgb555_pack_to_bgra([white, white, white, white], false);
1095        assert_eq!(result, [0xFFu8; 16], "white");
1096
1097        // Black: all zeros
1098        let black = [0x00u8, 0x00u8];
1099        let result = super::rgb555_pack_to_bgra([black, black, black, black], false);
1100        // Black: [B=0, G=0, R=0, A=255] for each of 4 pixels
1101        let mut black_expected = [0u8; 16];
1102        for i in 0..4 {
1103            black_expected[i * 4 + 3] = 255;
1104        }
1105        assert_eq!(result, black_expected, "black");
1106
1107        // Red: R=31 -> 0x7C00 LE: [0x00, 0x7C]
1108        let red = [0x00u8, 0x7Cu8];
1109        let result = super::rgb555_pack_to_bgra([red, red, red, red], false);
1110        for i in 0..4 {
1111            assert_eq!(result[i * 4..][..4], [0, 0, 255, 255], "red pixel {i}");
1112        }
1113
1114        // Green: G=31 -> 0x03E0 LE: [0xE0, 0x03]
1115        let green = [0xE0u8, 0x03u8];
1116        let result = super::rgb555_pack_to_bgra([green, green, green, green], false);
1117        for i in 0..4 {
1118            assert_eq!(result[i * 4..][..4], [0, 255, 0, 255], "green pixel {i}");
1119        }
1120
1121        // Blue: B=31 -> 0x001F LE: [0x1F, 0x00]
1122        let blue = [0x1Fu8, 0x00u8];
1123        let result = super::rgb555_pack_to_bgra([blue, blue, blue, blue], false);
1124        for i in 0..4 {
1125            assert_eq!(result[i * 4..][..4], [255, 0, 0, 255], "blue pixel {i}");
1126        }
1127    }
1128
1129    #[test]
1130    fn rgb555_pack_swap_mode() {
1131        // swap=true: layout xBBBBBGGGGGRRRRR
1132        // B=31 in high bits -> pixel 0x7C00 LE: [0x00, 0x7C] -> B_out=255
1133        let pixel = [0x00u8, 0x7Cu8];
1134        let result = super::rgb555_pack_to_bgra([pixel, pixel, pixel, pixel], true);
1135        for i in 0..4 {
1136            assert_eq!(result[i * 4..][..4], [255, 0, 0, 255], "swap B=31->B_out pixel {i}");
1137        }
1138
1139        // R=31 in low bits -> pixel 0x001F LE: [0x1F, 0x00] -> R_out=255
1140        let pixel = [0x1Fu8, 0x00u8];
1141        let result = super::rgb555_pack_to_bgra([pixel, pixel, pixel, pixel], true);
1142        for i in 0..4 {
1143            assert_eq!(result[i * 4..][..4], [0, 0, 255, 255], "swap R=31->R_out pixel {i}");
1144        }
1145    }
1146
1147    #[test]
1148    fn rgb555_pack_different_pixels() {
1149        // 4 different pixels in one call, LE byte order.
1150        let white = [0xFFu8, 0x7Fu8]; // 0x7FFF
1151        let red = [0x00u8, 0x7Cu8]; // 0x7C00
1152        let green = [0xE0u8, 0x03u8]; // 0x03E0
1153        let blue = [0x1Fu8, 0x00u8]; // 0x001F
1154
1155        let pixels = [white, red, green, blue];
1156        let result = super::rgb555_pack_to_bgra(pixels, false);
1157
1158        assert_eq!(result[0..4], [255, 255, 255, 255], "p0 white");
1159        assert_eq!(result[4..8], [0, 0, 255, 255], "p1 red");
1160        assert_eq!(result[8..12], [0, 255, 0, 255], "p2 green");
1161        assert_eq!(result[12..16], [255, 0, 0, 255], "p3 blue");
1162    }
1163
1164    #[test]
1165    #[allow(clippy::cast_possible_truncation)]
1166    fn rgb555_pack_1000_random_cross_check() {
1167        // Deterministic PCG-style RNG.
1168        let mut state: u64 = 42;
1169        let mut rng = || {
1170            state = state
1171                .wrapping_mul(6_364_136_223_846_793_005)
1172                .wrapping_add(1_442_695_040_888_963_407);
1173            (state >> 33) as u32
1174        };
1175
1176        for _ in 0..1000 {
1177            let mut pixels = [[0u8; 2]; 4];
1178            for p in &mut pixels {
1179                let val = (rng() & 0x7FFF) as u16; // 15-bit RGB555
1180                *p = val.to_le_bytes();
1181            }
1182            let swap = (rng() & 1) != 0;
1183
1184            let result = super::rgb555_pack_to_bgra(pixels, swap);
1185
1186            // Manually compute expected.
1187            let mut expected = [0u8; 16];
1188            for i in 0..4 {
1189                let raw = u16::from_le_bytes(pixels[i]);
1190                let (r5, g5, b5) = if swap {
1191                    (raw & 0x1F, (raw >> 5) & 0x1F, (raw >> 10) & 0x1F)
1192                } else {
1193                    ((raw >> 10) & 0x1F, (raw >> 5) & 0x1F, raw & 0x1F)
1194                };
1195                expected[i * 4] = ((b5 << 3) | (b5 >> 2)) as u8;
1196                expected[i * 4 + 1] = ((g5 << 3) | (g5 >> 2)) as u8;
1197                expected[i * 4 + 2] = ((r5 << 3) | (r5 >> 2)) as u8;
1198                expected[i * 4 + 3] = 255;
1199            }
1200
1201            assert_eq!(result, expected, "Mismatch for swap={swap}");
1202        }
1203    }
1204
1205    // ---- SSSE3/AVX2 rgb555_pack random cross-checks ----
1206
1207    #[test]
1208    #[cfg(all(feature = "simd", any(target_arch = "x86_64", target_arch = "x86")))]
1209    #[cfg_attr(miri, ignore)]
1210    fn rgb555_pack_10k_random_ssse3() {
1211        use super::scalar;
1212        // Only run on SSSE3-capable hardware.
1213        if !is_x86_feature_detected!("ssse3") {
1214            return;
1215        }
1216        let mut state: u64 = 42;
1217        let mut rng = || {
1218            state = state
1219                .wrapping_mul(6_364_136_223_846_793_005)
1220                .wrapping_add(1_442_695_040_888_963_407);
1221            (state >> 33) as u32
1222        };
1223        for _ in 0..10_000 {
1224            let mut pixels = [[0u8; 2]; 4];
1225            for p in &mut pixels {
1226                let val = (rng() & 0x7FFF) as u16;
1227                *p = val.to_le_bytes();
1228            }
1229            let swap = (rng() & 1) != 0;
1230            let expected = scalar::rgb555_pack_to_bgra(pixels, swap);
1231            let result;
1232            // SAFETY: checked is_x86_feature_detected!("ssse3") above.
1233            unsafe {
1234                result = super::reordered::rgb555_pack_to_bgra_ssse3(&pixels, swap);
1235            }
1236            assert_eq!(result, expected, "SSSE3 variant mismatch for swap={swap}");
1237        }
1238    }
1239
1240    #[test]
1241    #[cfg(all(feature = "simd", target_arch = "x86_64"))]
1242    #[cfg_attr(miri, ignore)]
1243    fn rgb555_pack_10k_random_avx2() {
1244        use super::scalar;
1245        if !is_x86_feature_detected!("avx2") {
1246            return;
1247        }
1248        let mut state: u64 = 42;
1249        let mut rng = || {
1250            state = state
1251                .wrapping_mul(6_364_136_223_846_793_005)
1252                .wrapping_add(1_442_695_040_888_963_407);
1253            (state >> 33) as u32
1254        };
1255        for _ in 0..10_000 {
1256            let mut pixels = [[0u8; 2]; 4];
1257            for p in &mut pixels {
1258                let val = (rng() & 0x7FFF) as u16;
1259                *p = val.to_le_bytes();
1260            }
1261            let swap = (rng() & 1) != 0;
1262            let expected = scalar::rgb555_pack_to_bgra(pixels, swap);
1263            let result;
1264            // SAFETY: checked is_x86_feature_detected!("avx2") above.
1265            unsafe {
1266                result = super::reordered::rgb555_pack_to_bgra_avx2(&pixels, swap);
1267            }
1268            assert_eq!(result, expected, "AVX2 variant mismatch for swap={swap}");
1269        }
1270    }
1271}