Skip to main content

core_simd/
swizzle_dyn.rs

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