Skip to main content

core/portable-simd/crates/core_simd/src/
swizzle_dyn.rs

1// Allow these FCW: anyone soundly using the intrinsics has to enable
2// the target feature, and that will generate a warning for them.
3#![allow(aarch64_softfloat_neon, x86_softfloat_sse)]
4
5use crate::simd::Simd;
6use core::mem;
7
8impl<const N: usize> Simd<u8, N> {
9    /// Swizzle a vector of bytes according to the index vector.
10    /// Indices within range select the appropriate byte.
11    /// Indices "out of bounds" instead select 0.
12    ///
13    /// Note that the current implementation is selected during build-time
14    /// of the standard library, so `cargo build -Zbuild-std` may be necessary
15    /// to unlock better performance, especially for larger vectors.
16    /// A planned compiler improvement will enable using `#[target_feature]` instead.
17    #[inline]
18    pub fn swizzle_dyn(self, idxs: Simd<u8, N>) -> Self {
19        #![allow(unused_imports, unused_unsafe)]
20        #[cfg(all(
21            target_arch = "arm",
22            target_feature = "v7",
23            target_feature = "neon",
24            target_endian = "little"
25        ))]
26        use core::arch::arm::{uint8x8_t, vtbl1_u8};
27        #[cfg(target_arch = "wasm32")]
28        use core::arch::wasm32 as wasm;
29        #[cfg(target_arch = "wasm64")]
30        use core::arch::wasm64 as wasm;
31        #[cfg(target_arch = "x86")]
32        use core::arch::x86;
33        #[cfg(target_arch = "x86_64")]
34        use core::arch::x86_64 as x86;
35        // SAFETY: Intrinsics covered by cfg
36        unsafe {
37            #[allow(
38                unreachable_patterns,
39                reason = "avoids writing verbose cfg(not), earlier branches take priority"
40            )]
41            match N {
42                // Aarch64
43                #[cfg(all(
44                    any(target_arch = "aarch64", target_arch = "arm64ec"),
45                    target_feature = "neon",
46                    target_endian = "little"
47                ))]
48                8 | 16 | 24 | 32 | 48 | 64 => aarch64_swizzle(self, idxs),
49
50                // 32-bit ARMv7
51                #[cfg(all(
52                    target_arch = "arm",
53                    target_feature = "v7",
54                    target_feature = "neon",
55                    target_endian = "little"
56                ))]
57                16 => transize(armv7_neon_swizzle_u8x16, self, idxs),
58
59                // WASM SIMD128
60                #[cfg(target_feature = "simd128")]
61                16 => transize(wasm::i8x16_swizzle, self, idxs),
62                #[cfg(target_feature = "simd128")]
63                32 => transize(swizzle_dyn_split::<32, 16>, self, idxs),
64
65                // LoongArch64
66                #[cfg(all(target_arch = "loongarch64", target_feature = "lsx"))]
67                16 => transize(loong64_lsx_swizzle, self, idxs),
68                #[cfg(all(target_arch = "loongarch64", target_feature = "lasx"))]
69                32 => transize(loong64_lasx_swizzle, self, idxs),
70                #[cfg(all(target_arch = "loongarch64", target_feature = "lsx"))]
71                32 => transize(swizzle_dyn_split::<32, 16>, self, idxs),
72                #[cfg(all(target_arch = "loongarch64", target_feature = "lasx"))]
73                64 => transize(swizzle_dyn_split::<64, 32>, self, idxs),
74
75                // x86, x86-64
76                #[cfg(target_feature = "ssse3")]
77                16 => transize(x86::_mm_shuffle_epi8, self, zeroing_idxs(idxs)),
78                #[cfg(all(target_feature = "avx512vl", target_feature = "avx512vbmi"))]
79                32 => {
80                    // Unlike vpshufb, vpermb doesn't zero out values in the result based on the index high bit
81                    let swizzler = |bytes, idxs| {
82                        let mask = x86::_mm256_cmp_epu8_mask::<{ x86::_MM_CMPINT_LT }>(
83                            idxs,
84                            Simd::<u8, 32>::splat(N as u8).into(),
85                        );
86                        x86::_mm256_maskz_permutexvar_epi8(mask, idxs, bytes)
87                    };
88                    transize(swizzler, self, idxs)
89                }
90                #[cfg(target_feature = "avx2")]
91                32 => transize(avx2_pshufb, self, idxs),
92                #[cfg(target_feature = "ssse3")]
93                32 => transize(swizzle_dyn_split::<32, 16>, self, idxs),
94                // Notable absence: avx512bw pshufb shuffle
95                #[cfg(all(target_feature = "avx512vl", target_feature = "avx512vbmi"))]
96                64 => {
97                    // Unlike vpshufb, vpermb doesn't zero out values in the result based on the index high bit
98                    let swizzler = |bytes, idxs| {
99                        let mask = x86::_mm512_cmp_epu8_mask::<{ x86::_MM_CMPINT_LT }>(
100                            idxs,
101                            Simd::<u8, 64>::splat(N as u8).into(),
102                        );
103                        x86::_mm512_maskz_permutexvar_epi8(mask, idxs, bytes)
104                    };
105                    transize(swizzler, self, idxs)
106                }
107                #[cfg(target_feature = "avx2")]
108                64 => transize(swizzle_dyn_split::<64, 32>, self, idxs),
109
110                // scalar fallback
111                _ => {
112                    let mut array = [0; N];
113                    for (i, k) in idxs.to_array().into_iter().enumerate() {
114                        if (k as usize) < N {
115                            array[i] = self[k as usize];
116                        };
117                    }
118                    array.into()
119                }
120            }
121        }
122    }
123}
124
125#[allow(dead_code, reason = "only used on some targets/features")]
126/// Implements an arbitrary shuffle over double the native vector width
127/// using 4 native-width shuffles
128fn swizzle_dyn_split<const N: usize, const HALF: usize>(
129    bytes: Simd<u8, N>,
130    idxs: Simd<u8, N>,
131) -> Simd<u8, N> {
132    let table_low = bytes.extract::<0, HALF>();
133    let table_high = bytes.extract::<HALF, HALF>();
134    let idxs_low = idxs.extract::<0, HALF>();
135    let idxs_high = idxs.extract::<HALF, HALF>();
136    let table_high_offset = Simd::<u8, HALF>::splat(HALF as u8);
137
138    let output_low_from_low = table_low.swizzle_dyn(idxs_low);
139    let output_low_from_high = table_high.swizzle_dyn(idxs_low - table_high_offset);
140    let output_low = output_low_from_low | output_low_from_high;
141
142    let output_high_from_low = table_low.swizzle_dyn(idxs_high);
143    let output_high_from_high = table_high.swizzle_dyn(idxs_high - table_high_offset);
144    let output_high = output_high_from_low | output_high_from_high;
145
146    // This is simply a concatenation of two native-sized vectors.
147    // The swizzle does nothing - it maps the elements right back where they already are.
148    // There doesn't seem to be a more direct way to do this as of this writing.
149    // TODO: simplify once a plain `concat` is available.
150    use crate::simd::Swizzle;
151    struct CombineHalves;
152    impl<const N: usize> Swizzle<N> for CombineHalves {
153        const INDEX: [usize; N] = const {
154            let mut index = [0; N];
155            let mut i = 0;
156            while i < N {
157                index[i] = i;
158                i += 1;
159            }
160            index
161        };
162    }
163
164    CombineHalves::concat_swizzle(output_low, output_high)
165}
166
167/// armv7 neon supports swizzling `u8x16` by swizzling two u8x8 blocks
168/// with a u8x8x2 lookup table.
169///
170/// # Safety
171/// This requires armv7 neon to work
172#[cfg(all(
173    target_arch = "arm",
174    target_feature = "v7",
175    target_feature = "neon",
176    target_endian = "little"
177))]
178unsafe fn armv7_neon_swizzle_u8x16(bytes: Simd<u8, 16>, idxs: Simd<u8, 16>) -> Simd<u8, 16> {
179    use core::arch::arm::{uint8x8x2_t, vcombine_u8, vget_high_u8, vget_low_u8, vtbl2_u8};
180    // SAFETY: Caller promised arm neon support
181    unsafe {
182        let bytes = uint8x8x2_t(vget_low_u8(bytes.into()), vget_high_u8(bytes.into()));
183        let lo = vtbl2_u8(bytes, vget_low_u8(idxs.into()));
184        let hi = vtbl2_u8(bytes, vget_high_u8(idxs.into()));
185        vcombine_u8(lo, hi).into()
186    }
187}
188
189/// AArch64 NEON supports swizzling 8, 16, 24, 32, 48 or 64 by stacking multiple TBL instructions.
190///
191/// # Safety
192/// This requires AArch64 NEON to work
193#[cfg(all(
194    any(target_arch = "aarch64", target_arch = "arm64ec"),
195    target_feature = "neon",
196    target_endian = "little"
197))]
198unsafe fn aarch64_swizzle<const N: usize>(bytes: Simd<u8, N>, idxs: Simd<u8, N>) -> Simd<u8, N> {
199    use core::arch::aarch64::*;
200    use core::mem::transmute_copy;
201
202    // SAFETY: Caller promised AArch64 NEON support
203    unsafe {
204        match N {
205            8 => transmute_copy(&vtbl1_u8(transmute_copy(&bytes), transmute_copy(&idxs))),
206            16 => transmute_copy(&vqtbl1q_u8(transmute_copy(&bytes), transmute_copy(&idxs))),
207            24 => {
208                let bytes: uint8x8x3_t = transmute_copy(&bytes);
209                let idxs: uint8x8x3_t = transmute_copy(&idxs);
210
211                let ret0 = vtbl3_u8(bytes, idxs.0);
212                let ret1 = vtbl3_u8(bytes, idxs.1);
213                let ret2 = vtbl3_u8(bytes, idxs.2);
214
215                let ret = uint8x8x3_t(ret0, ret1, ret2);
216                transmute_copy(&ret)
217            }
218            32 => {
219                let bytes: uint8x16x2_t = transmute_copy(&bytes);
220                let idxs: uint8x16x2_t = transmute_copy(&idxs);
221
222                let ret0 = vqtbl2q_u8(bytes, idxs.0);
223                let ret1 = vqtbl2q_u8(bytes, idxs.1);
224
225                let ret = uint8x16x2_t(ret0, ret1);
226                transmute_copy(&ret)
227            }
228            48 => {
229                let bytes: uint8x16x3_t = transmute_copy(&bytes);
230                let idxs: uint8x16x3_t = transmute_copy(&idxs);
231
232                let ret0 = vqtbl3q_u8(bytes, idxs.0);
233                let ret1 = vqtbl3q_u8(bytes, idxs.1);
234                let ret2 = vqtbl3q_u8(bytes, idxs.2);
235
236                let ret = uint8x16x3_t(ret0, ret1, ret2);
237                transmute_copy(&ret)
238            }
239            64 => {
240                let bytes: uint8x16x4_t = transmute_copy(&bytes);
241                let idxs: uint8x16x4_t = transmute_copy(&idxs);
242
243                let ret0 = vqtbl4q_u8(bytes, idxs.0);
244                let ret1 = vqtbl4q_u8(bytes, idxs.1);
245                let ret2 = vqtbl4q_u8(bytes, idxs.2);
246                let ret3 = vqtbl4q_u8(bytes, idxs.3);
247
248                let ret = uint8x16x4_t(ret0, ret1, ret2, ret3);
249                transmute_copy(&ret)
250            }
251            _ => unreachable!(),
252        }
253    }
254}
255
256/// "vpshufb like it was meant to be" on AVX2
257///
258/// # Safety
259/// This requires AVX2 to work
260#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
261#[target_feature(enable = "avx2")]
262#[allow(unused)]
263#[inline]
264#[allow(clippy::let_and_return)]
265unsafe fn avx2_pshufb(bytes: Simd<u8, 32>, idxs: Simd<u8, 32>) -> Simd<u8, 32> {
266    #[cfg(target_arch = "x86")]
267    use core::arch::x86;
268    #[cfg(target_arch = "x86_64")]
269    use core::arch::x86_64 as x86;
270    use x86::_mm256_permute2x128_si256 as avx2_cross_shuffle;
271    use x86::_mm256_shuffle_epi8 as avx2_half_pshufb;
272    // SAFETY: Caller promised AVX2
273    unsafe {
274        let lolo = avx2_cross_shuffle::<0x00>(bytes.into(), bytes.into());
275        let hihi = avx2_cross_shuffle::<0x11>(bytes.into(), bytes.into());
276
277        // Adding 0x60 preserves the low nibble and bit 4 for valid
278        // indices 0..=31. Larger indices get their high bit set, so
279        // VPSHUFB supplies the required out-of-bounds zeroing.
280        let control = x86::_mm256_adds_epu8(idxs.into(), x86::_mm256_set1_epi8(0x60));
281
282        // Move index bit 4 into each byte's sign bit for VPBLENDVB.
283        let select_high = x86::_mm256_slli_epi16::<3>(control);
284        let from_low = avx2_half_pshufb(lolo, control);
285        let from_high = avx2_half_pshufb(hihi, control);
286        x86::_mm256_blendv_epi8(from_low, from_high, select_high).into()
287    }
288}
289
290/// LoongArch64 LSX supports swizzling `u8x16`
291///
292/// # Safety
293/// This requires LoongArch LSX to work
294#[cfg(all(target_arch = "loongarch64", target_feature = "lsx"))]
295unsafe fn loong64_lsx_swizzle(bytes: Simd<u8, 16>, idxs: Simd<u8, 16>) -> Simd<u8, 16> {
296    use core::arch::loongarch64::{lsx_vand_v, lsx_vshuf_b, lsx_vslei_bu};
297    // SAFETY: Caller promised loongarch lsx support
298    unsafe {
299        let bytes = lsx_vshuf_b(bytes.into(), bytes.into(), idxs.into());
300        let mask = lsx_vslei_bu::<15>(idxs.into());
301        lsx_vand_v(bytes, mask).into()
302    }
303}
304
305/// LoongArch64 LASX supports swizzling `u8x32`
306///
307/// # Safety
308/// This requires LoongArch LASX to work
309#[cfg(all(target_arch = "loongarch64", target_feature = "lasx"))]
310unsafe fn loong64_lasx_swizzle(bytes: Simd<u8, 32>, idxs: Simd<u8, 32>) -> Simd<u8, 32> {
311    use core::arch::loongarch64::{lasx_xvand_v, lasx_xvpermi_q, lasx_xvshuf_b, lasx_xvslei_bu};
312    // SAFETY: Caller promised loongarch lasx support
313    unsafe {
314        let lolo = lasx_xvpermi_q::<0x00>(bytes.into(), bytes.into());
315        let hihi = lasx_xvpermi_q::<0x11>(bytes.into(), bytes.into());
316        let bytes = lasx_xvshuf_b(hihi, lolo, idxs.into());
317        let mask = lasx_xvslei_bu::<31>(idxs.into());
318        lasx_xvand_v(bytes, mask).into()
319    }
320}
321
322/// This sets up a call to an architecture-specific function, and in doing so
323/// it persuades rustc that everything is the correct size. Which it is.
324/// This would not be needed if one could convince Rust that, by matching on N,
325/// N is that value, and thus it would be valid to substitute e.g. 16.
326///
327/// # Safety
328/// The correctness of this function hinges on the sizes agreeing in actuality.
329#[allow(dead_code)]
330#[inline(always)]
331unsafe fn transize<T, const N: usize>(
332    f: unsafe fn(T, T) -> T,
333    a: Simd<u8, N>,
334    b: Simd<u8, N>,
335) -> Simd<u8, N> {
336    // SAFETY: Same obligation to use this function as to use mem::transmute_copy.
337    unsafe { mem::transmute_copy(&f(mem::transmute_copy(&a), mem::transmute_copy(&b))) }
338}
339
340/// Make indices that yield 0 for x86
341#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
342#[allow(unused)]
343#[inline(always)]
344fn zeroing_idxs<const N: usize>(idxs: Simd<u8, N>) -> Simd<u8, N> {
345    // Adding this sets the high bit for indices N..=127, while PSHUFB ignores
346    // the other changed bits. The OR preserves the high bit for indices 128..=255.
347    let zeroing_bits = idxs + Simd::splat((127 - N + 1) as u8);
348    idxs | zeroing_bits
349}