Skip to main content

base64/engine/
simd.rs

1//! SIMD-accelerated engines for the standard and URL-safe alphabets.
2//!
3//! These are gated behind the `simd-unsafe` feature because they use `unsafe`. Three engines are
4//! provided:
5//!
6//! - `Simd` detects the best available instruction set at runtime and falls back to the scalar
7//!   [`GeneralPurpose`] engine when none is available. It requires `std` for the detection.
8//! - `Avx2` and `Neon` target a specific instruction set with no runtime detection, so they can
9//!   be used in `no_std` builds when the target is known to support the instructions.
10//!
11//! Only the STANDARD and URL_SAFE alphabets are accelerated (they share indices `0..=61` and differ
12//! only at `62`/`63`). Each engine therefore has dedicated `standard` / `url_safe` constructors
13//! rather than taking an arbitrary [`Alphabet`](crate::alphabet::Alphabet); use [`GeneralPurpose`]
14//! for any other alphabet.
15//!
16//! The kernels follow Wojciech Mula's vectorized base64 algorithms
17//! (<http://0x80.pl/notesen/2016-01-17-sse-base64-decoding.html> and the companion encoding note).
18//! AVX2 uses the multiply-based bit (de)interleave; NEON, which lacks the relevant multiplies, uses
19//! the shift/mask variant. Both share the same per-alphabet lookup tables, expressed as the
20//! associated constants of the `SimdAlphabet` trait so the kernels can inline them.
21#![allow(unsafe_code)]
22
23use crate::alphabet::Symbol;
24#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
25use crate::{
26    engine::{
27        general_purpose::{
28            decode::decode_helper, encode_helper, GeneralPurpose, GeneralPurposeConfig,
29            GeneralPurposeEstimate,
30        },
31        DecodeMetadata, Engine,
32    },
33    DecodeSliceError,
34};
35
36/// A base64 alphabet family the SIMD kernels can accelerate.
37///
38/// Carries the per-alphabet lookup tables as associated constants. Private (hence sealed);
39/// implemented only by [`Standard`] and [`UrlSafe`], selected at runtime by [`SimdKind`].
40#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
41trait SimdAlphabet {
42    /// pshufb encode table: maps a reduced 6-bit index to `ascii - index`.
43    const ENCODE_LUT: [i8; 16];
44    /// pshufb decode table indexed by the high nibble; added to the byte to produce its 6-bit value.
45    const DECODE_SHIFT_LUT: [i8; 16];
46    /// pshufb decode table indexed by the low nibble; bit `hi` marks `(hi, lo)` as a valid symbol.
47    const DECODE_MASK_LUT: [u8; 16];
48    /// The high-`62`/`63` symbol whose shift needs the fixup below (`+`/`-`).
49    const DECODE_FIXUP_CHAR: i8;
50    /// The shift applied to [`DECODE_FIXUP_CHAR`](Self::DECODE_FIXUP_CHAR).
51    const DECODE_FIXUP_SHIFT: i8;
52}
53
54/// The STANDARD alphabet family (`+`/`/` at 62/63).
55#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
56enum Standard {}
57
58/// The URL_SAFE alphabet family (`-`/`_` at 62/63).
59#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
60enum UrlSafe {}
61
62#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
63impl SimdAlphabet for Standard {
64    const ENCODE_LUT: [i8; 16] = [
65        65, 71, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -19, -16, 0, 0,
66    ];
67    const DECODE_SHIFT_LUT: [i8; 16] = [0, 0, 19, 4, -65, -65, -71, -71, 0, 0, 0, 0, 0, 0, 0, 0];
68    const DECODE_MASK_LUT: [u8; 16] = [
69        0xA8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF0, 0x54, 0x50, 0x50, 0x50,
70        0x54,
71    ];
72    const DECODE_FIXUP_CHAR: i8 = 0x2F;
73    const DECODE_FIXUP_SHIFT: i8 = 16;
74}
75
76#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
77impl SimdAlphabet for UrlSafe {
78    const ENCODE_LUT: [i8; 16] = [
79        65, 71, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -17, 32, 0, 0,
80    ];
81    const DECODE_SHIFT_LUT: [i8; 16] = [0, 0, 17, 4, -65, -65, -71, -71, 0, 0, 0, 0, 0, 0, 0, 0];
82    const DECODE_MASK_LUT: [u8; 16] = [
83        0xA8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF0, 0x50, 0x50, 0x54, 0x50,
84        0x70,
85    ];
86    const DECODE_FIXUP_CHAR: i8 = 0x5F;
87    const DECODE_FIXUP_SHIFT: i8 = -32;
88}
89
90/// Which accelerated alphabet family an engine uses. Selects the kernel monomorphization at runtime.
91#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
92#[derive(Clone, Copy, Debug, PartialEq, Eq)]
93enum SimdKind {
94    Standard,
95    UrlSafe,
96}
97
98/// Minimum input length before a SIMD path is used. Below these the setup cost outweighs the gain;
99/// encode needs more data than decode to break even.
100#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
101const SIMD_MIN_INPUT_ENCODE: usize = 128;
102#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
103const SIMD_MIN_INPUT_DECODE: usize = 64;
104
105/// pshufb decode validity table: maps a high nibble to a single set bit. Shared by both alphabets.
106#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
107const BITPOS_LUT: [u8; 16] = [1, 2, 4, 8, 16, 32, 64, 128, 0, 0, 0, 0, 0, 0, 0, 0];
108
109#[cfg(target_arch = "x86_64")]
110mod avx2 {
111    use super::{SimdAlphabet, BITPOS_LUT, SIMD_MIN_INPUT_DECODE, SIMD_MIN_INPUT_ENCODE};
112    use core::arch::x86_64::*;
113
114    /// Encode leading whole 24-byte input groups (24 in -> 32 out per iteration).
115    ///
116    /// # Safety
117    ///
118    /// The running CPU must support AVX2.
119    #[target_feature(enable = "avx2")]
120    pub(super) unsafe fn encode_bulk<A: SimdAlphabet>(
121        input: &[u8],
122        output: &mut [u8],
123    ) -> (usize, usize) {
124        if input.len() < SIMD_MIN_INPUT_ENCODE {
125            return (0, 0);
126        }
127        let lut_data = A::ENCODE_LUT;
128        // SAFETY: `lut_data` is a 16-byte array; `_mm_loadu_si128` reads exactly 16 bytes from it.
129        let lut = _mm256_broadcastsi128_si256(_mm_loadu_si128(lut_data.as_ptr().cast()));
130        #[rustfmt::skip]
131        let shuf = _mm256_setr_epi8(
132            1, 0, 2, 1, 4, 3, 5, 4, 7, 6, 8, 7, 10, 9, 11, 10,
133            1, 0, 2, 1, 4, 3, 5, 4, 7, 6, 8, 7, 10, 9, 11, 10,
134        );
135        let mask_hi = _mm256_set1_epi32(0x0fc0_fc00_u32 as i32);
136        let mul_hi = _mm256_set1_epi32(0x0400_0040);
137        let mask_lo = _mm256_set1_epi32(0x003f_03f0);
138        let mul_lo = _mm256_set1_epi32(0x0100_0010);
139        let const51 = _mm256_set1_epi8(51);
140        let const25 = _mm256_set1_epi8(25);
141
142        let mut i = 0usize;
143        let mut o = 0usize;
144        // A whole 32-byte vector is read but only 24 bytes are consumed, so 32 input bytes must be
145        // available; the store writes 32 output bytes.
146        while i + 32 <= input.len() && o + 32 <= output.len() {
147            // SAFETY: the loop guard ensures `input[i..i+32]` is in bounds, so this reads 32 valid
148            // bytes; `loadu` has no alignment requirement.
149            let data = _mm256_loadu_si256(input.as_ptr().add(i).cast());
150            // TODO https://arxiv.org/abs/1704.00605 doesn't seem to require this perm step, which
151            // costs 3 cycles, and is slightly different in the reduce phase as well.
152            // Rearrange dwords so low lane = bytes[0..16], high lane = bytes[12..28].
153            let perm = _mm256_permutevar8x32_epi32(data, _mm256_setr_epi32(0, 1, 2, 3, 3, 4, 5, 6));
154            let inb = _mm256_shuffle_epi8(perm, shuf);
155
156            let t0 = _mm256_and_si256(inb, mask_hi);
157            let t1 = _mm256_mulhi_epu16(t0, mul_hi);
158            let t2 = _mm256_and_si256(inb, mask_lo);
159            let t3 = _mm256_mullo_epi16(t2, mul_lo);
160            let indices = _mm256_or_si256(t1, t3); // one 6-bit value (0..=63) per byte
161
162            let reduced = _mm256_subs_epu8(indices, const51);
163            let gt25 = _mm256_cmpgt_epi8(indices, const25);
164            let reduced = _mm256_sub_epi8(reduced, gt25);
165            let ascii = _mm256_add_epi8(indices, _mm256_shuffle_epi8(lut, reduced));
166
167            // SAFETY: the loop guard ensures `output[o..o+32]` is in bounds; `storeu` writes 32
168            // bytes with no alignment requirement.
169            _mm256_storeu_si256(output.as_mut_ptr().add(o).cast(), ascii);
170            i += 24;
171            o += 32;
172        }
173        (i, o)
174    }
175
176    /// Decode leading whole 32-byte input blocks (32 in -> 24 out per iteration).
177    ///
178    /// # Safety
179    ///
180    /// The running CPU must support AVX2.
181    #[target_feature(enable = "avx2")]
182    pub(super) unsafe fn decode_bulk<A: SimdAlphabet>(
183        input: &[u8],
184        quads_end: usize,
185        output: &mut [u8],
186    ) -> (usize, usize) {
187        if quads_end < SIMD_MIN_INPUT_DECODE {
188            return (0, 0);
189        }
190        let shift_data = A::DECODE_SHIFT_LUT;
191        let mask_data = A::DECODE_MASK_LUT;
192        // SAFETY: each LUT is a 16-byte array read in full by `_mm_loadu_si128`.
193        let shift_lut = _mm256_broadcastsi128_si256(_mm_loadu_si128(shift_data.as_ptr().cast()));
194        let mask_lut = _mm256_broadcastsi128_si256(_mm_loadu_si128(mask_data.as_ptr().cast()));
195        let bitpos_lut = _mm256_broadcastsi128_si256(_mm_loadu_si128(BITPOS_LUT.as_ptr().cast()));
196        let low_nibble_mask = _mm256_set1_epi8(0x0f);
197        let fixup_char = _mm256_set1_epi8(A::DECODE_FIXUP_CHAR);
198        let fixup_shift = _mm256_set1_epi8(A::DECODE_FIXUP_SHIFT);
199        let zero = _mm256_setzero_si256();
200        let merge_mul1 = _mm256_set1_epi32(0x0140_0140);
201        let merge_mul2 = _mm256_set1_epi32(0x0001_1000);
202        #[rustfmt::skip]
203        let pack_shuf = _mm256_setr_epi8(
204            2, 1, 0, 6, 5, 4, 10, 9, 8, 14, 13, 12, -1, -1, -1, -1,
205            2, 1, 0, 6, 5, 4, 10, 9, 8, 14, 13, 12, -1, -1, -1, -1,
206        );
207        let lane_compact = _mm256_setr_epi32(0, 1, 2, 4, 5, 6, 6, 6);
208
209        let mut i = 0usize;
210        let mut o = 0usize;
211        // Need 32 input bytes available to load; the store writes exactly 24 output bytes.
212        while i + 32 <= quads_end && o + 24 <= output.len() {
213            // SAFETY: `i + 32 <= quads_end <= input.len()`, so this reads 32 in-bounds bytes;
214            // `loadu` needs no alignment.
215            let data = _mm256_loadu_si256(input.as_ptr().add(i).cast());
216
217            let hi_nibbles = _mm256_and_si256(_mm256_srli_epi32(data, 4), low_nibble_mask);
218            let lo_nibbles = _mm256_and_si256(data, low_nibble_mask);
219
220            let m = _mm256_shuffle_epi8(mask_lut, lo_nibbles);
221            let bit = _mm256_shuffle_epi8(bitpos_lut, hi_nibbles);
222            let non_match = _mm256_cmpeq_epi8(_mm256_and_si256(m, bit), zero);
223            if _mm256_movemask_epi8(non_match) != 0 {
224                // Invalid byte in this block; let the scalar decoder report the exact offset.
225                break;
226            }
227
228            let sh = _mm256_shuffle_epi8(shift_lut, hi_nibbles);
229            let eq_fixup = _mm256_cmpeq_epi8(data, fixup_char);
230            let shift = _mm256_blendv_epi8(sh, fixup_shift, eq_fixup);
231            let values = _mm256_add_epi8(data, shift); // 6-bit value per byte
232
233            let merged = _mm256_maddubs_epi16(values, merge_mul1);
234            let packed = _mm256_madd_epi16(merged, merge_mul2);
235            let shuffled = _mm256_shuffle_epi8(packed, pack_shuf);
236            let compact = _mm256_permutevar8x32_epi32(shuffled, lane_compact);
237
238            // Store exactly 24 bytes (16 + 8); a wider store would clobber an oversized output.
239            let lo = _mm256_castsi256_si128(compact);
240            let hi = _mm256_extracti128_si256(compact, 1);
241            // SAFETY: the loop guard ensures `o + 24 <= output.len()`, so the 16-byte store at `o`
242            // and the 8-byte store at `o + 16` are both in bounds; neither needs alignment.
243            _mm_storeu_si128(output.as_mut_ptr().add(o).cast(), lo);
244            _mm_storel_epi64(output.as_mut_ptr().add(o + 16).cast(), hi);
245            i += 32;
246            o += 24;
247        }
248        (i, o)
249    }
250}
251
252#[cfg(target_arch = "aarch64")]
253mod neon {
254    use super::{SimdAlphabet, BITPOS_LUT, SIMD_MIN_INPUT_DECODE, SIMD_MIN_INPUT_ENCODE};
255    use core::arch::aarch64::*;
256
257    /// Encode leading whole 12-byte input groups (12 in -> 16 out per iteration).
258    ///
259    /// # Safety
260    ///
261    /// The running CPU must support NEON.
262    #[target_feature(enable = "neon")]
263    pub(super) unsafe fn encode_bulk<A: SimdAlphabet>(
264        input: &[u8],
265        output: &mut [u8],
266    ) -> (usize, usize) {
267        if input.len() < SIMD_MIN_INPUT_ENCODE {
268            return (0, 0);
269        }
270        let lut_data = A::ENCODE_LUT;
271        let split_bytes: [u8; 16] = [1, 0, 2, 1, 4, 3, 5, 4, 7, 6, 8, 7, 10, 9, 11, 10];
272        // SAFETY: `split_bytes` and `lut_data` are 16-byte arrays, each read in full by `vld1q_u8`.
273        let split_shuf = vld1q_u8(split_bytes.as_ptr());
274        let translate = vld1q_u8(lut_data.as_ptr().cast());
275        // note the in-memory LE byte order will affect how these match the shuffled data bytes
276        let m1 = vdupq_n_u32(0x0000_fc00);
277        let m2 = vdupq_n_u32(0x0000_03f0);
278        let m3 = vdupq_n_u32(0x0fc0_0000);
279        let m4 = vdupq_n_u32(0x003f_0000);
280        let c51 = vdupq_n_u8(51);
281        let c25 = vdupq_n_s8(25);
282
283        let mut i = 0usize;
284        let mut o = 0usize;
285        // Reads a full 16-byte vector but consumes only 12 bytes, so 16 must be readable; writes
286        // exactly 16 output bytes.
287        while i + 16 <= input.len() && o + 16 <= output.len() {
288            // SAFETY: the loop guard ensures `input[i..i+16]` is in bounds, so this reads 16 valid
289            // bytes.
290            let data = vld1q_u8(input.as_ptr().add(i));
291            let x0 = vreinterpretq_u32_u8(vqtbl1q_u8(data, split_shuf));
292            // data now is 4 32-bit words in x0, each with the 3 bytes to encode in that word:
293            // 1 0 2 1
294            // 4 3 5 4
295            // 7 6 8 7
296            // a 9 b a
297
298            // select bits in chunks of 6 into their own bytes, treating the input as a bit sequence
299
300            // select the left 6 bits of [0, 3, 6, 9] (in byte 1 of the words) and shift until the
301            // 6 bits are the low 6 bits of byte 0, then cast to be in 2 byte words
302            let x1 = vshrq_n_u16::<10>(vreinterpretq_u16_u32(vandq_u32(x0, m1)));
303            // right 2 bits of [0 3 6 9], left 4 bits of [1 4 7 a], shifted to low bits
304            let x2 = vshlq_n_u16::<4>(vreinterpretq_u16_u32(vandq_u32(x0, m2)));
305            // right 4 bits of [1 4 7 a], left 2 bits of [2 5 8 b]
306            let x3 = vshrq_n_u16::<6>(vreinterpretq_u16_u32(vandq_u32(x0, m3)));
307            // right 6 bits of [2 5 8 b]
308            let x4 = vshlq_n_u16::<8>(vreinterpretq_u16_u32(vandq_u32(x0, m4)));
309            let indices = vreinterpretq_u8_u16(vorrq_u16(vorrq_u16(x1, x2), vorrq_u16(x3, x4)));
310
311            // reduce the 6-bit index to a translate-LUT index, then add the offset to get ascii
312            let reduced = vqsubq_u8(indices, c51);
313            let gt25 = vcgtq_s8(vreinterpretq_s8_u8(indices), c25);
314            let reduced = vsubq_u8(reduced, gt25); // subtracting 0xFF adds 1 where index > 25
315            let ascii = vaddq_u8(indices, vqtbl1q_u8(translate, reduced));
316
317            // SAFETY: the loop guard ensures `output[o..o+16]` is in bounds, so this writes 16 bytes
318            // in bounds.
319            vst1q_u8(output.as_mut_ptr().add(o), ascii);
320            i += 12;
321            o += 16;
322        }
323        (i, o)
324    }
325
326    /// Decode leading whole 16-byte input blocks (16 in -> 12 out per iteration).
327    ///
328    /// # Safety
329    ///
330    /// The running CPU must support NEON.
331    #[target_feature(enable = "neon")]
332    pub(super) unsafe fn decode_bulk<A: SimdAlphabet>(
333        input: &[u8],
334        quads_end: usize,
335        output: &mut [u8],
336    ) -> (usize, usize) {
337        if quads_end < SIMD_MIN_INPUT_DECODE {
338            return (0, 0);
339        }
340        // SAFETY: each LUT is a 16-byte array read in full by `vld1q_u8`.
341        let shift_lut = vld1q_u8(A::DECODE_SHIFT_LUT.as_ptr().cast());
342        let mask_lut = vld1q_u8(A::DECODE_MASK_LUT.as_ptr());
343        let bitpos_lut = vld1q_u8(BITPOS_LUT.as_ptr());
344        let low_nibble_mask = vdupq_n_u8(0x0f);
345        let fixup_char_v = vdupq_n_u8(A::DECODE_FIXUP_CHAR as u8);
346        let fixup_shift_v = vdupq_n_u8(A::DECODE_FIXUP_SHIFT as u8);
347        let zero = vdupq_n_u8(0);
348        let mm1 = vdupq_n_u32(0x003f_003f);
349        let mm2 = vdupq_n_u32(0x3f00_3f00);
350        let out_mask = vdupq_n_u32(0x00ff_ffff);
351        let pack_bytes: [u8; 16] = [
352            2, 1, 0, 6, 5, 4, 10, 9, 8, 14, 13, 12, 0x80, 0x80, 0x80, 0x80,
353        ];
354        // SAFETY: `pack_bytes` is a 16-byte array read in full by `vld1q_u8`.
355        let pack_shuf = vld1q_u8(pack_bytes.as_ptr());
356
357        let mut i = 0usize;
358        let mut o = 0usize;
359        // Need 16 input bytes to load; writes exactly 12 output bytes.
360        while i + 16 <= quads_end && o + 12 <= output.len() {
361            // SAFETY: `i + 16 <= quads_end <= input.len()`, so this reads 16 in-bounds bytes.
362            let data = vld1q_u8(input.as_ptr().add(i));
363            let hi = vshrq_n_u8::<4>(data);
364            let lo = vandq_u8(data, low_nibble_mask);
365
366            let m = vqtbl1q_u8(mask_lut, lo);
367            let bit = vqtbl1q_u8(bitpos_lut, hi);
368            let non_match = vceqq_u8(vandq_u8(m, bit), zero);
369            if vmaxvq_u8(non_match) != 0 {
370                // Invalid byte in this block; let the scalar decoder report the exact offset.
371                break;
372            }
373
374            let sh = vqtbl1q_u8(shift_lut, hi);
375            let eq_fixup = vceqq_u8(data, fixup_char_v);
376            let shift = vbslq_u8(eq_fixup, fixup_shift_v, sh);
377            let values = vaddq_u8(data, shift); // {00aaaaaa|00bbbbbb|00cccccc|00dddddd} x4
378
379            // merge 4x6 bits -> 3 bytes per quad via shift/mask (no multiplies on NEON)
380            let v = vreinterpretq_u32_u8(values);
381            let x1 = vandq_u32(v, mm1); // {00aaaaaa|00000000|00cccccc|00000000}
382            let x2 = vandq_u32(v, mm2); // {00000000|00bbbbbb|00000000|00dddddd}
383            let x3 = vorrq_u32(vshlq_n_u32::<18>(x1), vshrq_n_u32::<10>(x1));
384            let x4 = vorrq_u32(vshlq_n_u32::<4>(x2), vshrq_n_u32::<24>(x2));
385            let merged = vandq_u32(vorrq_u32(x3, x4), out_mask);
386            let packed = vqtbl1q_u8(vreinterpretq_u8_u32(merged), pack_shuf); // 12 bytes in [0..12)
387
388            // Store exactly 12 bytes (8 + 4); a wider store would clobber an oversized output.
389            // SAFETY: the loop guard ensures `o + 12 <= output.len()`, so the 8-byte store at `o`
390            // and the 4-byte store at `o + 8` are both in bounds.
391            vst1_u8(output.as_mut_ptr().add(o), vget_low_u8(packed));
392            // Can't use vst1q_lane_u32 to directly write the last lane as it requires 4-byte
393            // alignment, which we don't have.
394            // Could also shift words and then use vst1_u8 again, but this way is more direct and
395            // doesn't double-write the middle word.
396            let tail = vgetq_lane_u32::<2>(vreinterpretq_u32_u8(packed));
397            core::ptr::write_unaligned(output.as_mut_ptr().add(o + 8).cast::<u32>(), tail);
398
399            i += 16;
400            o += 12;
401        }
402        (i, o)
403    }
404}
405
406/// Dispatch the AVX2 encode kernel to the right alphabet monomorphization.
407///
408/// # Safety
409///
410/// The running CPU must support AVX2 (the requirement is forwarded to [`avx2::encode_bulk`]).
411#[cfg(target_arch = "x86_64")]
412#[inline]
413unsafe fn avx2_encode(kind: SimdKind, input: &[u8], output: &mut [u8]) -> (usize, usize) {
414    match kind {
415        // SAFETY: this function's contract guarantees AVX2, which is all the kernel requires.
416        SimdKind::Standard => avx2::encode_bulk::<Standard>(input, output),
417        SimdKind::UrlSafe => avx2::encode_bulk::<UrlSafe>(input, output),
418    }
419}
420
421/// Dispatch the AVX2 decode kernel to the right alphabet monomorphization.
422///
423/// # Safety
424///
425/// The running CPU must support AVX2 (the requirement is forwarded to [`avx2::decode_bulk`]).
426#[cfg(target_arch = "x86_64")]
427#[inline]
428unsafe fn avx2_decode(
429    kind: SimdKind,
430    input: &[u8],
431    quads_end: usize,
432    output: &mut [u8],
433) -> (usize, usize) {
434    match kind {
435        // SAFETY: this function's contract guarantees AVX2, which is all the kernel requires.
436        SimdKind::Standard => avx2::decode_bulk::<Standard>(input, quads_end, output),
437        SimdKind::UrlSafe => avx2::decode_bulk::<UrlSafe>(input, quads_end, output),
438    }
439}
440
441/// Dispatch the NEON encode kernel to the right alphabet monomorphization.
442///
443/// # Safety
444///
445/// The running CPU must support NEON.
446#[cfg(target_arch = "aarch64")]
447#[inline]
448unsafe fn neon_encode(kind: SimdKind, input: &[u8], output: &mut [u8]) -> (usize, usize) {
449    match kind {
450        // SAFETY: this function's contract guarantees NEON, which is all the kernel requires.
451        SimdKind::Standard => neon::encode_bulk::<Standard>(input, output),
452        SimdKind::UrlSafe => neon::encode_bulk::<UrlSafe>(input, output),
453    }
454}
455
456/// Dispatch the NEON decode kernel to the right alphabet monomorphization.
457///
458/// # Safety
459///
460/// The running CPU must support NEON.
461#[cfg(target_arch = "aarch64")]
462#[inline]
463unsafe fn neon_decode(
464    kind: SimdKind,
465    input: &[u8],
466    quads_end: usize,
467    output: &mut [u8],
468) -> (usize, usize) {
469    match kind {
470        // SAFETY: this function's contract guarantees NEON, which is all the kernel requires.
471        SimdKind::Standard => neon::decode_bulk::<Standard>(input, quads_end, output),
472        SimdKind::UrlSafe => neon::decode_bulk::<UrlSafe>(input, quads_end, output),
473    }
474}
475
476/// Which instruction set an engine dispatches to.
477#[cfg(all(feature = "std", any(target_arch = "x86_64", target_arch = "aarch64")))]
478#[derive(Clone, Copy, Debug)]
479enum Backend {
480    Scalar,
481    #[cfg(target_arch = "x86_64")]
482    Avx2,
483    #[cfg(target_arch = "aarch64")]
484    Neon,
485}
486
487/// A base64 engine that uses the best SIMD instruction set detected at runtime, falling back to the
488/// scalar [`GeneralPurpose`] engine.
489///
490/// Requires the `std` feature (for runtime CPU-feature detection) and an `x86_64` or `aarch64`
491/// target. On other targets, use [`GeneralPurpose`] directly. Only the STANDARD and URL_SAFE
492/// alphabets are accelerated, so it is constructed with [`Simd::standard`] / [`Simd::url_safe`].
493#[cfg(all(feature = "std", any(target_arch = "x86_64", target_arch = "aarch64")))]
494#[derive(Debug, Clone)]
495pub struct Simd {
496    inner: GeneralPurpose,
497    kind: SimdKind,
498    backend: Backend,
499}
500
501#[cfg(all(feature = "std", any(target_arch = "x86_64", target_arch = "aarch64")))]
502impl Simd {
503    /// Create a `Simd` engine for the STANDARD alphabet, detecting the instruction set once.
504    #[must_use]
505    pub fn standard(config: GeneralPurposeConfig) -> Self {
506        Self::new(SimdKind::Standard, &crate::alphabet::STANDARD, config)
507    }
508
509    /// Create a `Simd` engine for the URL_SAFE alphabet, detecting the instruction set once.
510    #[must_use]
511    pub fn url_safe(config: GeneralPurposeConfig) -> Self {
512        Self::new(SimdKind::UrlSafe, &crate::alphabet::URL_SAFE, config)
513    }
514
515    fn new(
516        kind: SimdKind,
517        alphabet: &crate::alphabet::Alphabet,
518        config: GeneralPurposeConfig,
519    ) -> Self {
520        #[cfg(target_arch = "x86_64")]
521        let backend = if std::is_x86_feature_detected!("avx2") {
522            Backend::Avx2
523        } else {
524            Backend::Scalar
525        };
526        #[cfg(target_arch = "aarch64")]
527        let backend = if std::arch::is_aarch64_feature_detected!("neon") {
528            Backend::Neon
529        } else {
530            Backend::Scalar
531        };
532
533        Self {
534            inner: GeneralPurpose::new(alphabet, config),
535            kind,
536            backend,
537        }
538    }
539}
540
541#[cfg(all(feature = "std", any(target_arch = "x86_64", target_arch = "aarch64")))]
542impl Engine for Simd {
543    type Config = GeneralPurposeConfig;
544    type DecodeEstimate = GeneralPurposeEstimate;
545
546    fn internal_encode(&self, input: &[u8], output: &mut [u8]) -> usize {
547        let kind = self.kind;
548        match self.backend {
549            Backend::Scalar => self.inner.internal_encode(input, output),
550            #[cfg(target_arch = "x86_64")]
551            Backend::Avx2 => encode_helper(self.inner.encode_table(), input, output, |i, o| {
552                // SAFETY: the Avx2 backend is only selected when AVX2 was detected.
553                unsafe { avx2_encode(kind, i, o) }
554            }),
555            #[cfg(target_arch = "aarch64")]
556            Backend::Neon => encode_helper(self.inner.encode_table(), input, output, |i, o| {
557                // SAFETY: the Neon backend is only selected when NEON was detected.
558                unsafe { neon_encode(kind, i, o) }
559            }),
560        }
561    }
562
563    fn internal_decoded_len_estimate(&self, input_len: usize) -> Self::DecodeEstimate {
564        self.inner.internal_decoded_len_estimate(input_len)
565    }
566
567    fn internal_decode(
568        &self,
569        input: &[u8],
570        output: &mut [u8],
571        estimate: Self::DecodeEstimate,
572    ) -> Result<DecodeMetadata, DecodeSliceError> {
573        let kind = self.kind;
574        match self.backend {
575            Backend::Scalar => self.inner.internal_decode(input, output, estimate),
576            #[cfg(target_arch = "x86_64")]
577            Backend::Avx2 => decode_helper(
578                input,
579                &estimate,
580                output,
581                self.inner.decode_table(),
582                self.inner.config().decode_allow_trailing_bits(),
583                self.inner.padding(),
584                self.inner.config().decode_padding_mode(),
585                // SAFETY: the Avx2 backend is only selected when AVX2 was detected.
586                |i, end, o| unsafe { avx2_decode(kind, i, end, o) },
587            ),
588            #[cfg(target_arch = "aarch64")]
589            Backend::Neon => decode_helper(
590                input,
591                &estimate,
592                output,
593                self.inner.decode_table(),
594                self.inner.config().decode_allow_trailing_bits(),
595                self.inner.padding(),
596                self.inner.config().decode_padding_mode(),
597                // SAFETY: the Neon backend is only selected when NEON was detected.
598                |i, end, o| unsafe { neon_decode(kind, i, end, o) },
599            ),
600        }
601    }
602
603    fn config(&self) -> &Self::Config {
604        self.inner.config()
605    }
606
607    fn padding(&self) -> Symbol {
608        self.inner.padding()
609    }
610}
611
612/// A base64 engine that unconditionally uses AVX2, without runtime detection.
613///
614/// This works in `no_std` builds. Because it does not check for AVX2 support, it must only be used
615/// on a CPU that has it. Only the STANDARD and URL_SAFE alphabets are accelerated, so it is
616/// constructed with the [`Avx2::standard`] / [`Avx2::url_safe`] (checked) or
617/// [`Avx2::standard_unchecked`] / [`Avx2::url_safe_unchecked`] constructors.
618#[cfg(target_arch = "x86_64")]
619#[derive(Debug, Clone)]
620pub struct Avx2 {
621    inner: GeneralPurpose,
622    kind: SimdKind,
623}
624
625#[cfg(target_arch = "x86_64")]
626impl Avx2 {
627    /// Create an `Avx2` engine for the STANDARD alphabet if the running CPU supports AVX2, else
628    /// `None`.
629    ///
630    /// Requires the `std` feature for the detection; in `no_std` use [`Avx2::standard_unchecked`].
631    #[cfg(feature = "std")]
632    #[must_use]
633    pub fn standard(config: GeneralPurposeConfig) -> Option<Self> {
634        if std::is_x86_feature_detected!("avx2") {
635            // SAFETY: AVX2 support was just verified.
636            Some(unsafe { Self::standard_unchecked(config) })
637        } else {
638            None
639        }
640    }
641
642    /// Create an `Avx2` engine for the URL_SAFE alphabet if the running CPU supports AVX2, else
643    /// `None`.
644    ///
645    /// Requires the `std` feature for the detection; in `no_std` use [`Avx2::url_safe_unchecked`].
646    #[cfg(feature = "std")]
647    #[must_use]
648    pub fn url_safe(config: GeneralPurposeConfig) -> Option<Self> {
649        if std::is_x86_feature_detected!("avx2") {
650            // SAFETY: AVX2 support was just verified.
651            Some(unsafe { Self::url_safe_unchecked(config) })
652        } else {
653            None
654        }
655    }
656
657    /// Create an `Avx2` engine for the STANDARD alphabet without checking for AVX2 support.
658    ///
659    /// # Safety
660    ///
661    /// The CPU that will run encode/decode must support AVX2. Using the engine on a CPU without
662    /// AVX2 is undefined behavior.
663    #[must_use]
664    pub const unsafe fn standard_unchecked(config: GeneralPurposeConfig) -> Self {
665        Self {
666            inner: GeneralPurpose::new(&crate::alphabet::STANDARD, config),
667            kind: SimdKind::Standard,
668        }
669    }
670
671    /// Create an `Avx2` engine for the URL_SAFE alphabet without checking for AVX2 support.
672    ///
673    /// # Safety
674    ///
675    /// The CPU that will run encode/decode must support AVX2. Using the engine on a CPU without
676    /// AVX2 is undefined behavior.
677    #[must_use]
678    pub const unsafe fn url_safe_unchecked(config: GeneralPurposeConfig) -> Self {
679        Self {
680            inner: GeneralPurpose::new(&crate::alphabet::URL_SAFE, config),
681            kind: SimdKind::UrlSafe,
682        }
683    }
684}
685
686#[cfg(target_arch = "x86_64")]
687impl Engine for Avx2 {
688    type Config = GeneralPurposeConfig;
689    type DecodeEstimate = GeneralPurposeEstimate;
690
691    fn internal_encode(&self, input: &[u8], output: &mut [u8]) -> usize {
692        let kind = self.kind;
693        encode_helper(self.inner.encode_table(), input, output, |i, o| {
694            // SAFETY: constructing this engine asserts AVX2 support.
695            unsafe { avx2_encode(kind, i, o) }
696        })
697    }
698
699    fn internal_decoded_len_estimate(&self, input_len: usize) -> Self::DecodeEstimate {
700        self.inner.internal_decoded_len_estimate(input_len)
701    }
702
703    fn internal_decode(
704        &self,
705        input: &[u8],
706        output: &mut [u8],
707        estimate: Self::DecodeEstimate,
708    ) -> Result<DecodeMetadata, DecodeSliceError> {
709        let kind = self.kind;
710        decode_helper(
711            input,
712            &estimate,
713            output,
714            self.inner.decode_table(),
715            self.inner.config().decode_allow_trailing_bits(),
716            self.inner.padding(),
717            self.inner.config().decode_padding_mode(),
718            // SAFETY: constructing this engine asserts AVX2 support.
719            |i, end, o| unsafe { avx2_decode(kind, i, end, o) },
720        )
721    }
722
723    fn config(&self) -> &Self::Config {
724        self.inner.config()
725    }
726
727    fn padding(&self) -> Symbol {
728        self.inner.padding()
729    }
730}
731
732/// A base64 engine that unconditionally uses NEON, without runtime detection.
733///
734/// This engine is available on aarch64 targets compiled with NEON support and works in `no_std`.
735#[cfg(target_arch = "aarch64")]
736#[derive(Debug, Clone)]
737pub struct Neon {
738    inner: GeneralPurpose,
739    kind: SimdKind,
740}
741
742#[cfg(target_arch = "aarch64")]
743impl Neon {
744    /// Create a `Neon` engine for the STANDARD alphabet on a target compiled with NEON support.
745    #[must_use]
746    pub const fn standard(config: GeneralPurposeConfig) -> Self {
747        Self {
748            inner: GeneralPurpose::new(&crate::alphabet::STANDARD, config),
749            kind: SimdKind::Standard,
750        }
751    }
752
753    /// Create a `Neon` engine for the URL_SAFE alphabet on a target compiled with NEON support.
754    #[must_use]
755    pub const fn url_safe(config: GeneralPurposeConfig) -> Self {
756        Self {
757            inner: GeneralPurpose::new(&crate::alphabet::URL_SAFE, config),
758            kind: SimdKind::UrlSafe,
759        }
760    }
761}
762
763#[cfg(target_arch = "aarch64")]
764impl Engine for Neon {
765    type Config = GeneralPurposeConfig;
766    type DecodeEstimate = GeneralPurposeEstimate;
767
768    fn internal_encode(&self, input: &[u8], output: &mut [u8]) -> usize {
769        let kind = self.kind;
770        encode_helper(self.inner.encode_table(), input, output, |i, o| {
771            // SAFETY: this module is only compiled for targets with NEON enabled.
772            unsafe { neon_encode(kind, i, o) }
773        })
774    }
775
776    fn internal_decoded_len_estimate(&self, input_len: usize) -> Self::DecodeEstimate {
777        self.inner.internal_decoded_len_estimate(input_len)
778    }
779
780    fn internal_decode(
781        &self,
782        input: &[u8],
783        output: &mut [u8],
784        estimate: Self::DecodeEstimate,
785    ) -> Result<DecodeMetadata, DecodeSliceError> {
786        let kind = self.kind;
787        decode_helper(
788            input,
789            &estimate,
790            output,
791            self.inner.decode_table(),
792            self.inner.config().decode_allow_trailing_bits(),
793            self.inner.padding(),
794            self.inner.config().decode_padding_mode(),
795            // SAFETY: this module is only compiled for targets with NEON enabled.
796            |i, end, o| unsafe { neon_decode(kind, i, end, o) },
797        )
798    }
799
800    fn config(&self) -> &Self::Config {
801        self.inner.config()
802    }
803
804    fn padding(&self) -> Symbol {
805        self.inner.padding()
806    }
807}