Skip to main content

rlnc_simdx/kernel/
mod.rs

1//! Kernel dispatch layer — **safe public API** + runtime CPU feature detection.
2//!
3//! ## For application code (safe, no `unsafe`)
4//!
5//! Use only:
6//!
7//! - [`axpy`] — `y[i] ^= c * x[i]` (equal lengths; **non-overlapping** buffers)
8//! - [`scale`] — `y[i] = c * x[i]` (equal lengths; **non-overlapping**; use
9//!   [`scale_inplace`] for in-place)
10//! - [`scale_inplace`] — `y[i] = c * y[i]`
11//! - [`axpy_multi`] — adaptive blocked/fused multi-source AXPY for encoding
12//! - [`dot`] — GF(2⁸) dot product (GFNI accelerated when available)
13//! - [`active_kernel_name`] — which SIMD tier is active
14//!
15//! Length and overlap are **asserted in release** on the safe wrappers.
16//! Tier modules (`x86`, `arm`, `wasm`) are **`pub(crate)`** — not part of the
17//! external API.
18//!
19//! ## Dispatch
20//!
21//! All SIMD variants are compiled in. On the **first** call to [`axpy`] /
22//! [`scale`] / [`scale_inplace`] (with `std`), the best tier is selected via
23//! `is_x86_feature_detected!` and cached in `OnceLock<KernelSet>`.
24//!
25//! ## Dispatch priority (`x86_64`)
26//!
27//! | Tier | Runtime check                          | Width   | Instruction     |
28//! |------|----------------------------------------|---------|-----------------|
29//! |  1   | gfni + avx512f + avx512bw              | 512-bit | GF2P8MULB zmm  |
30//! |  2   | gfni + avx2                            | 256-bit | GF2P8MULB ymm  |
31//! |  3   | gfni + sse4.2                          | 128-bit | GF2P8MULB xmm  |
32//! |  4   | avx512f + avx512bw + ssse3             | 512-bit | vpshufb zmm    |
33//! |  5   | avx2 + ssse3                           | 256-bit | vpshufb ymm    |
34//! |  6   | ssse3                                  | 128-bit | pshufb xmm     |
35//! |  7   | neon (`AArch64`)                       | 128-bit | `vqtbl1q`      |
36//! |  8   | wasm simd128 (compile-time)            | 128-bit | `i8x16_swizzle` |
37//! |  9   | scalar (universal fallback)            | 1 byte  | table lookup   |
38//!
39//! ## `no_std` targets
40//!
41//! `OnceLock` requires `std`. Without `std`, dispatch uses compile-time
42//! `#[cfg(target_feature)]` selection (bare-metal / embedded).
43
44// The existing cache-blocked AXPY implementation intentionally keeps its block
45// size declaration adjacent to the loop. Preserve that hot-loop source shape.
46#![allow(clippy::items_after_statements)]
47
48/// Unstable scalar kernel internals exposed only for workspace benchmarks.
49///
50/// This module is not covered by the crate's stable API guarantees and may
51/// change or disappear without a semver-major release.
52#[cfg(feature = "bench-internals")]
53pub mod scalar;
54#[cfg(not(feature = "bench-internals"))]
55pub(crate) mod scalar;
56
57#[cfg(test)]
58mod proptest;
59
60// Tier kernels are crate-private; external code must use the safe wrappers
61// below (`axpy` / `scale` / `scale_inplace`), which enforce length + overlap.
62// Raw intrinsic code intentionally uses ISA wildcard imports, explicit pointer
63// casts, fixed alignment masks, and aggressive inlining. Rewriting those hot
64// loops solely to satisfy style lints risks code-generation regressions.
65#[allow(
66    clippy::cast_possible_wrap,
67    clippy::cast_ptr_alignment,
68    clippy::incompatible_msrv,
69    clippy::inline_always,
70    clippy::if_not_else,
71    clippy::ptr_as_ptr,
72    clippy::too_many_lines,
73    clippy::unreadable_literal,
74    clippy::verbose_bit_mask,
75    clippy::wildcard_imports
76)]
77#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
78pub(crate) mod x86;
79
80#[cfg(target_arch = "aarch64")]
81pub(crate) mod arm;
82
83pub(crate) mod wasm;
84
85/// Safe direct-tier handles for workspace benchmarks.
86///
87/// This module exists only with `std + bench-internals`. It is not covered by
88/// the crate's stable API guarantees. Handles are returned only after runtime
89/// CPU-feature detection, while each call retains the public kernels' length
90/// and overlap checks.
91#[cfg(all(feature = "bench-internals", feature = "std"))]
92pub mod bench {
93    use super::ranges_overlap;
94
95    type DirectAxpyFn = unsafe fn(c: u8, x: &[u8], y: &mut [u8]);
96
97    /// A CPU-validated direct AXPY tier used to isolate dispatch overhead and
98    /// compare implementations in benchmarks.
99    #[derive(Clone, Copy)]
100    pub struct DirectAxpyTier {
101        name: &'static str,
102        function: DirectAxpyFn,
103    }
104
105    impl DirectAxpyTier {
106        /// Human-readable ISA tier name.
107        #[must_use]
108        pub fn name(self) -> &'static str {
109            self.name
110        }
111
112        /// Run this tier through the same safety contract as [`super::axpy`].
113        ///
114        /// # Panics
115        /// Panics when lengths differ or the source and destination overlap.
116        pub fn axpy(self, c: u8, x: &[u8], y: &mut [u8]) {
117            assert_eq!(
118                x.len(),
119                y.len(),
120                "rlnc_simdx::kernel::bench::DirectAxpyTier::axpy: length mismatch"
121            );
122            assert!(
123                !ranges_overlap(x.as_ptr(), x.len(), y.as_ptr(), y.len()),
124                "rlnc_simdx::kernel::bench::DirectAxpyTier::axpy: overlapping buffers are not allowed"
125            );
126            // SAFETY: the constructor is private, so handles are created only
127            // after checking their required CPU features. Buffer invariants
128            // were validated above.
129            unsafe { (self.function)(c, x, y) };
130        }
131    }
132
133    /// Return every direct AXPY tier supported by the current CPU.
134    #[must_use]
135    pub fn available_axpy_tiers() -> Vec<DirectAxpyTier> {
136        let mut tiers = Vec::new();
137
138        #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
139        {
140            use super::x86;
141
142            if std::is_x86_feature_detected!("gfni")
143                && std::is_x86_feature_detected!("avx512f")
144                && std::is_x86_feature_detected!("avx512bw")
145            {
146                tiers.push(DirectAxpyTier {
147                    name: "gfni+avx512",
148                    function: x86::gfni_avx512::axpy_gfni_avx512 as DirectAxpyFn,
149                });
150            }
151            if std::is_x86_feature_detected!("gfni") && std::is_x86_feature_detected!("avx2") {
152                tiers.push(DirectAxpyTier {
153                    name: "gfni+avx2",
154                    function: x86::gfni_avx2::axpy_gfni_avx2 as DirectAxpyFn,
155                });
156            }
157            if std::is_x86_feature_detected!("gfni") && std::is_x86_feature_detected!("sse4.2") {
158                tiers.push(DirectAxpyTier {
159                    name: "gfni+sse4.2",
160                    function: x86::gfni_sse::axpy_gfni_sse as DirectAxpyFn,
161                });
162            }
163            if std::is_x86_feature_detected!("avx512f")
164                && std::is_x86_feature_detected!("avx512bw")
165                && std::is_x86_feature_detected!("ssse3")
166            {
167                tiers.push(DirectAxpyTier {
168                    name: "avx512+ssse3",
169                    function: x86::avx512_ssse3::axpy_avx512_ssse3 as DirectAxpyFn,
170                });
171            }
172            if std::is_x86_feature_detected!("avx2") && std::is_x86_feature_detected!("ssse3") {
173                tiers.push(DirectAxpyTier {
174                    name: "avx2+ssse3",
175                    function: x86::avx2_ssse3::axpy_avx2_ssse3 as DirectAxpyFn,
176                });
177            }
178            if std::is_x86_feature_detected!("ssse3") {
179                tiers.push(DirectAxpyTier {
180                    name: "ssse3",
181                    function: x86::ssse3::axpy_ssse3 as DirectAxpyFn,
182                });
183            }
184        }
185
186        #[cfg(target_arch = "aarch64")]
187        tiers.push(DirectAxpyTier {
188            name: "neon",
189            function: super::arm::neon::axpy_neon as DirectAxpyFn,
190        });
191
192        #[cfg(all(target_family = "wasm", target_feature = "simd128"))]
193        tiers.push(DirectAxpyTier {
194            name: "wasm-simd128",
195            function: super::wasm::simd128::axpy_wasm as DirectAxpyFn,
196        });
197
198        tiers
199    }
200}
201
202// ---------------------------------------------------------------------------
203// Kernel function-pointer types
204// ---------------------------------------------------------------------------
205
206/// Signature for `axpy`: `y[i] ^= c * x[i]` over GF(2⁸).
207#[cfg(feature = "std")]
208pub(crate) type AxpyFn = unsafe fn(c: u8, x: &[u8], y: &mut [u8]);
209/// Signature for `scale`: `y[i] = c * x[i]` over GF(2⁸).
210#[cfg(feature = "std")]
211pub(crate) type ScaleFn = unsafe fn(c: u8, x: &[u8], y: &mut [u8]);
212/// Signature for in-place scale: `y[i] = c * y[i]`.
213#[cfg(feature = "std")]
214pub(crate) type ScaleInplaceFn = unsafe fn(c: u8, y: &mut [u8]);
215/// Signature for a validated multi-source AXPY implementation.
216#[cfg(feature = "std")]
217pub(crate) type AxpyMultiFn = unsafe fn(coeffs: &[u8], sources: &[&[u8]], y: &mut [u8]);
218/// Signature for a vectorized GF dot product.
219#[cfg(feature = "std")]
220pub(crate) type DotFn = unsafe fn(a: &[u8], b: &[u8]) -> u8;
221
222/// A resolved set of kernel function pointers for the detected CPU tier.
223#[cfg(feature = "std")]
224pub(crate) struct KernelSet {
225    /// Best available axpy kernel.
226    pub(crate) axpy: AxpyFn,
227    /// Best available scale kernel.
228    pub(crate) scale: ScaleFn,
229    /// Best available in-place scale kernel.
230    pub(crate) scale_inplace: ScaleInplaceFn,
231    /// Fused multi-source kernel when the selected ISA provides one.
232    pub(crate) axpy_multi: Option<AxpyMultiFn>,
233    /// Vectorized dot product when the selected ISA provides one.
234    pub(crate) dot: Option<DotFn>,
235    /// Human-readable tier name for diagnostics.
236    pub(crate) name: &'static str,
237}
238
239// ---------------------------------------------------------------------------
240// Runtime detection (std targets only)
241// ---------------------------------------------------------------------------
242
243#[cfg(feature = "std")]
244mod runtime {
245    #[cfg(target_arch = "aarch64")]
246    use super::arm;
247    #[cfg(all(target_family = "wasm", target_feature = "simd128"))]
248    use super::wasm;
249    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
250    use super::x86;
251    #[cfg(not(any(
252        target_arch = "aarch64",
253        all(target_family = "wasm", target_feature = "simd128")
254    )))]
255    use super::{scalar_axpy_wrapper, scalar_scale_inplace_wrapper, scalar_scale_wrapper};
256    use super::{AxpyFn, KernelSet, ScaleFn, ScaleInplaceFn};
257    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
258    use super::{AxpyMultiFn, DotFn};
259    use std::sync::OnceLock;
260
261    static KERNEL: OnceLock<KernelSet> = OnceLock::new();
262
263    /// Return (or initialise) the globally cached best kernel set.
264    pub(crate) fn get() -> &'static KernelSet {
265        KERNEL.get_or_init(detect)
266    }
267
268    /// Probe the CPU at runtime and return the highest-tier kernel set.
269    #[allow(clippy::too_many_lines)]
270    fn detect() -> KernelSet {
271        // ── x86 / x86_64 ────────────────────────────────────────────────────
272        #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
273        {
274            // Tier 1 — GFNI + AVX-512
275            if is_x86_feature_detected!("gfni")
276                && is_x86_feature_detected!("avx512f")
277                && is_x86_feature_detected!("avx512bw")
278            {
279                return KernelSet {
280                    axpy: x86::gfni_avx512::axpy_gfni_avx512 as AxpyFn,
281                    scale: x86::gfni_avx512::scale_gfni_avx512 as ScaleFn,
282                    scale_inplace: x86::gfni_avx512::scale_inplace_gfni_avx512 as ScaleInplaceFn,
283                    axpy_multi: Some(x86::gfni_avx512::axpy_multi_gfni_avx512 as AxpyMultiFn),
284                    dot: Some(x86::gfni_avx512::dot_gfni_avx512 as DotFn),
285                    name: "gfni+avx512 (tier1)",
286                };
287            }
288
289            // Tier 2 — GFNI + AVX2
290            if is_x86_feature_detected!("gfni") && is_x86_feature_detected!("avx2") {
291                return KernelSet {
292                    axpy: x86::gfni_avx2::axpy_gfni_avx2 as AxpyFn,
293                    scale: x86::gfni_avx2::scale_gfni_avx2 as ScaleFn,
294                    scale_inplace: x86::gfni_avx2::scale_inplace_gfni_avx2 as ScaleInplaceFn,
295                    axpy_multi: Some(x86::gfni_avx2::axpy_multi_gfni_avx2 as AxpyMultiFn),
296                    dot: Some(x86::gfni_avx2::dot_gfni_avx2 as DotFn),
297                    name: "gfni+avx2 (tier2)",
298                };
299            }
300
301            // Tier 3 — GFNI + SSE4.2
302            if is_x86_feature_detected!("gfni") && is_x86_feature_detected!("sse4.2") {
303                return KernelSet {
304                    axpy: x86::gfni_sse::axpy_gfni_sse as AxpyFn,
305                    scale: x86::gfni_sse::scale_gfni_sse as ScaleFn,
306                    scale_inplace: x86::gfni_sse::scale_inplace_gfni_sse as ScaleInplaceFn,
307                    axpy_multi: Some(x86::gfni_sse::axpy_multi_gfni_sse as AxpyMultiFn),
308                    dot: Some(x86::gfni_sse::dot_gfni_sse as DotFn),
309                    name: "gfni+sse4.2 (tier3)",
310                };
311            }
312
313            // Tier 4 — AVX-512 + SSSE3
314            if is_x86_feature_detected!("avx512f")
315                && is_x86_feature_detected!("avx512bw")
316                && is_x86_feature_detected!("ssse3")
317            {
318                return KernelSet {
319                    axpy: x86::avx512_ssse3::axpy_avx512_ssse3 as AxpyFn,
320                    scale: x86::avx512_ssse3::scale_avx512_ssse3 as ScaleFn,
321                    scale_inplace: x86::avx512_ssse3::scale_inplace_avx512_ssse3 as ScaleInplaceFn,
322                    axpy_multi: None,
323                    dot: None,
324                    name: "avx512+ssse3 (tier4)",
325                };
326            }
327
328            // Tier 5 — AVX2 + SSSE3
329            if is_x86_feature_detected!("avx2") && is_x86_feature_detected!("ssse3") {
330                return KernelSet {
331                    axpy: x86::avx2_ssse3::axpy_avx2_ssse3 as AxpyFn,
332                    scale: x86::avx2_ssse3::scale_avx2_ssse3 as ScaleFn,
333                    scale_inplace: x86::avx2_ssse3::scale_inplace_avx2_ssse3 as ScaleInplaceFn,
334                    axpy_multi: None,
335                    dot: None,
336                    name: "avx2+ssse3 (tier5)",
337                };
338            }
339
340            // Tier 6 — SSSE3
341            if is_x86_feature_detected!("ssse3") {
342                return KernelSet {
343                    axpy: x86::ssse3::axpy_ssse3 as AxpyFn,
344                    scale: x86::ssse3::scale_ssse3 as ScaleFn,
345                    scale_inplace: x86::ssse3::scale_inplace_ssse3 as ScaleInplaceFn,
346                    axpy_multi: None,
347                    dot: None,
348                    name: "ssse3 (tier6)",
349                };
350            }
351        }
352
353        // ── AArch64 ─────────────────────────────────────────────────────────
354        #[cfg(target_arch = "aarch64")]
355        {
356            // NEON is mandatory on all AArch64 — SVE is experimental (not wired).
357            return KernelSet {
358                axpy: arm::neon::axpy_neon as AxpyFn,
359                scale: arm::neon::scale_neon as ScaleFn,
360                scale_inplace: arm::neon::scale_inplace_neon as ScaleInplaceFn,
361                axpy_multi: None,
362                dot: None,
363                name: "neon (tier7)",
364            };
365        }
366
367        // ── WASM SIMD128 ─────────────────────────────────────────────────────
368        // Runtime feature detection is unavailable on wasm32. SIMD128 is a
369        // compile-time feature even when the crate is built with `std`.
370        #[cfg(all(target_family = "wasm", target_feature = "simd128"))]
371        {
372            return KernelSet {
373                axpy: wasm::simd128::axpy_wasm as AxpyFn,
374                scale: wasm::simd128::scale_wasm as ScaleFn,
375                scale_inplace: wasm::simd128::scale_inplace_wasm as ScaleInplaceFn,
376                axpy_multi: None,
377                dot: None,
378                name: "wasm-simd128 (tier8)",
379            };
380        }
381
382        // ── Scalar fallback ──────────────────────────────────────────────────
383        // AArch64 returned the mandatory NEON kernel above, so compiling this
384        // fallback there would only create unreachable-code/dead-import noise.
385        #[cfg(not(any(
386            target_arch = "aarch64",
387            all(target_family = "wasm", target_feature = "simd128")
388        )))]
389        KernelSet {
390            axpy: scalar_axpy_wrapper as AxpyFn,
391            scale: scalar_scale_wrapper as ScaleFn,
392            scale_inplace: scalar_scale_inplace_wrapper as ScaleInplaceFn,
393            axpy_multi: None,
394            dot: None,
395            name: "scalar (tier9)",
396        }
397    }
398}
399
400// ---------------------------------------------------------------------------
401// Thin wrappers so scalar fns have the right unsafe fn signature
402// ---------------------------------------------------------------------------
403
404#[cfg(all(
405    feature = "std",
406    not(any(
407        target_arch = "aarch64",
408        all(target_family = "wasm", target_feature = "simd128")
409    ))
410))]
411unsafe fn scalar_axpy_wrapper(c: u8, x: &[u8], y: &mut [u8]) {
412    scalar::axpy(c, x, y);
413}
414
415#[cfg(all(
416    feature = "std",
417    not(any(
418        target_arch = "aarch64",
419        all(target_family = "wasm", target_feature = "simd128")
420    ))
421))]
422unsafe fn scalar_scale_wrapper(c: u8, x: &[u8], y: &mut [u8]) {
423    scalar::scale(c, x, y);
424}
425
426#[cfg(all(
427    feature = "std",
428    not(any(
429        target_arch = "aarch64",
430        all(target_family = "wasm", target_feature = "simd128")
431    ))
432))]
433unsafe fn scalar_scale_inplace_wrapper(c: u8, y: &mut [u8]) {
434    scalar::scale_inplace(c, y);
435}
436
437// ---------------------------------------------------------------------------
438// Public kernel API
439// ---------------------------------------------------------------------------
440
441/// `y[i] ^= c * x[i]`  in GF(2⁸) — the fundamental RLNC primitive.
442///
443/// On `std` targets the best SIMD kernel is selected at first call and
444/// cached for all subsequent calls.  On `no_std` targets the kernel is
445/// selected at compile time from the available `target_feature` flags.
446///
447/// # Panics
448/// - Panics if `x.len() != y.len()`.
449/// - Panics if `x` and `y` memory ranges **overlap** (including full alias).
450///   Use a temporary buffer for in-place-style work; `c == 1` still requires
451///   disjoint ranges.
452///
453/// # Aliasing
454/// Buffers must be completely disjoint. Overlap is checked in **release**
455/// builds (pointer compare only).
456#[inline]
457pub fn axpy(c: u8, x: &[u8], y: &mut [u8]) {
458    assert_eq!(
459        x.len(),
460        y.len(),
461        "rlnc_simdx::kernel::axpy: length mismatch"
462    );
463    assert!(
464        !ranges_overlap(x.as_ptr(), x.len(), y.as_ptr(), y.len()),
465        "rlnc_simdx::kernel::axpy: overlapping buffers are not allowed"
466    );
467    // SAFETY: length and non-overlap were checked above.
468    unsafe { axpy_unchecked(c, x, y) }
469}
470
471/// Dispatch AXPY after the caller has established equal lengths and disjoint
472/// source/destination storage. This is crate-private so public callers cannot
473/// bypass the safe wrapper's validation.
474#[inline]
475pub(crate) unsafe fn axpy_unchecked(c: u8, x: &[u8], y: &mut [u8]) {
476    debug_assert_eq!(x.len(), y.len());
477    #[cfg(feature = "std")]
478    (runtime::get().axpy)(c, x, y);
479
480    #[cfg(not(feature = "std"))]
481    axpy_static(c, x, y)
482}
483
484/// `y[i] = c * x[i]`  in GF(2⁸).
485///
486/// # Panics
487/// - Panics if `x.len() != y.len()`.
488/// - Panics if `x` and `y` memory ranges **overlap** (including full alias).
489///   For in-place multiply use [`scale_inplace`].
490///
491/// # Aliasing
492/// Buffers must be completely disjoint. Overlap is checked in **release**
493/// builds (pointer compare only).
494#[inline]
495pub fn scale(c: u8, x: &[u8], y: &mut [u8]) {
496    assert_eq!(
497        x.len(),
498        y.len(),
499        "rlnc_simdx::kernel::scale: length mismatch"
500    );
501    assert!(
502        !ranges_overlap(x.as_ptr(), x.len(), y.as_ptr(), y.len()),
503        "rlnc_simdx::kernel::scale: overlapping buffers are not allowed; use scale_inplace for in-place"
504    );
505    #[cfg(feature = "std")]
506    unsafe {
507        (runtime::get().scale)(c, x, y);
508    }
509
510    #[cfg(not(feature = "std"))]
511    scale_static(c, x, y)
512}
513
514/// In-place scale: `y[i] = c * y[i]`  in GF(2⁸).
515///
516/// Prefer this over [`scale`] when the source and destination are the same buffer
517/// (e.g. pivot normalisation in Gaussian elimination). Uses the same SIMD tier
518/// as [`scale`] (GFNI / nibble-split / NEON) via runtime dispatch.
519#[inline]
520pub fn scale_inplace(c: u8, y: &mut [u8]) {
521    #[cfg(feature = "std")]
522    // SAFETY: runtime::get() only returns a kernel verified for this CPU.
523    unsafe {
524        (runtime::get().scale_inplace)(c, y);
525    }
526
527    #[cfg(not(feature = "std"))]
528    scale_inplace_static(c, y)
529}
530
531/// Multi-source fused AXPY with improved cache behaviour:
532/// for each chunk of the destination, apply `y ^= c_i * source_i` for all `i`.
533///
534/// `coeffs.len() == sources.len()`, every `sources[i].len() == y.len()`, and
535/// every source's full memory range must be completely disjoint from `y`.
536/// Sources may overlap each other because they are read-only.
537///
538/// # Panics
539/// - Panics if `coeffs.len() != sources.len()`.
540/// - Panics if any source length differs from `y.len()`; the message identifies
541///   the source index.
542/// - Panics if any source memory range overlaps `y` (including full alias); the
543///   message identifies the source index. This rule applies even when that
544///   source's coefficient is zero.
545#[inline]
546pub fn axpy_multi(coeffs: &[u8], sources: &[&[u8]], y: &mut [u8]) {
547    assert_eq!(
548        coeffs.len(),
549        sources.len(),
550        "axpy_multi: coeffs/sources len"
551    );
552
553    // Complete every validation pass before mutating y. Lengths are checked
554    // first so all ranges passed to the overlap check have the expected size.
555    for (i, src) in sources.iter().enumerate() {
556        assert_eq!(
557            src.len(),
558            y.len(),
559            "axpy_multi: source[{i}] length mismatch"
560        );
561    }
562    for (i, src) in sources.iter().enumerate() {
563        assert!(
564            !ranges_overlap(src.as_ptr(), src.len(), y.as_ptr(), y.len()),
565            "axpy_multi: source[{i}] overlaps destination"
566        );
567    }
568
569    #[cfg(feature = "std")]
570    let axpy_kernel = {
571        let kernels = runtime::get();
572        // Full fusion trades repeated destination traffic for a dynamic
573        // multi-stream inner loop. Measurements on tier1 show the latter wins
574        // only once the symbol has left the small-cache regime.
575        if y.len() >= 32 * 1024 && coeffs.len() >= 8 {
576            if let Some(fused) = kernels.axpy_multi {
577                // SAFETY: all lengths and destination overlaps were validated
578                // above, and runtime dispatch verified the CPU feature set.
579                unsafe { fused(coeffs, sources, y) };
580                return;
581            }
582        }
583        kernels.axpy
584    };
585
586    #[cfg(not(feature = "std"))]
587    if y.len() >= 32 * 1024 && coeffs.len() >= 8 && axpy_multi_static(coeffs, sources, y) {
588        return;
589    }
590
591    // Cache-blocked loop interchange: keep a destination chunk hot while
592    // streaming all sources (better for large symbols / many sources).
593    const BLOCK: usize = 4096;
594    let n = y.len();
595    let mut off = 0usize;
596    while off < n {
597        let end = (off + BLOCK).min(n);
598        for (i, &c) in coeffs.iter().enumerate() {
599            if c != 0 {
600                let source_block = &sources[i][off..end];
601                let destination_block = &mut y[off..end];
602
603                #[cfg(feature = "std")]
604                // SAFETY: runtime dispatch verified the kernel's CPU features;
605                // validation completed before mutation; disjoint full source and
606                // destination ranges imply that their corresponding subranges
607                // are also disjoint.
608                unsafe {
609                    axpy_kernel(c, source_block, destination_block);
610                }
611
612                #[cfg(not(feature = "std"))]
613                axpy_static(c, source_block, destination_block);
614            }
615        }
616        off = end;
617    }
618}
619
620/// Returns true if two non-empty byte ranges share any byte (including full alias).
621/// Empty ranges never overlap. Used by the **safe** public API only.
622#[inline]
623fn ranges_overlap(a: *const u8, a_len: usize, b: *const u8, b_len: usize) -> bool {
624    if a_len == 0 || b_len == 0 {
625        return false;
626    }
627    let a0 = a as usize;
628    let b0 = b as usize;
629    let a1 = a0 + a_len;
630    let b1 = b0 + b_len;
631    a0 < b1 && b0 < a1
632}
633
634/// `sum(a[i] * b[i])` in GF(2⁸).
635///
636/// # Panics
637/// Panics if `a.len() != b.len()`.
638#[inline]
639#[must_use]
640pub fn dot(a: &[u8], b: &[u8]) -> u8 {
641    assert_eq!(a.len(), b.len(), "rlnc_simdx::kernel::dot: length mismatch");
642    #[cfg(feature = "std")]
643    if let Some(dot_kernel) = runtime::get().dot {
644        // SAFETY: runtime dispatch verified the feature set and lengths match.
645        return unsafe { dot_kernel(a, b) };
646    }
647    #[cfg(not(feature = "std"))]
648    if let Some(result) = dot_static(a, b) {
649        return result;
650    }
651    scalar::dot(a, b)
652}
653
654/// Returns the name of the currently active kernel tier.
655#[must_use]
656pub fn active_kernel_name() -> &'static str {
657    #[cfg(feature = "std")]
658    {
659        runtime::get().name
660    }
661
662    #[cfg(not(feature = "std"))]
663    {
664        active_kernel_name_static()
665    }
666}
667
668// ---------------------------------------------------------------------------
669// Compile-time fallback dispatch (no_std targets)
670// Used when std is unavailable (embedded, WASM without SIMD, etc.)
671// ---------------------------------------------------------------------------
672
673#[cfg(not(feature = "std"))]
674#[inline]
675fn axpy_static(c: u8, x: &[u8], y: &mut [u8]) {
676    // WASM SIMD128
677    #[cfg(all(target_family = "wasm", target_feature = "simd128"))]
678    // SAFETY: this branch is compiled only when SIMD128 is enabled.
679    return unsafe { wasm::simd128::axpy_wasm(c, x, y) };
680
681    // AArch64 NEON (always present on aarch64)
682    #[cfg(target_arch = "aarch64")]
683    return unsafe { arm::neon::axpy_neon(c, x, y) };
684
685    // x86 compile-time fallback chain
686    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
687    {
688        #[cfg(all(
689            target_feature = "gfni",
690            target_feature = "avx512f",
691            target_feature = "avx512bw"
692        ))]
693        return unsafe { x86::gfni_avx512::axpy_gfni_avx512(c, x, y) };
694        #[cfg(all(
695            target_feature = "gfni",
696            target_feature = "avx2",
697            not(all(target_feature = "avx512f", target_feature = "avx512bw"))
698        ))]
699        return unsafe { x86::gfni_avx2::axpy_gfni_avx2(c, x, y) };
700        #[cfg(all(
701            target_feature = "gfni",
702            target_feature = "sse4.2",
703            not(target_feature = "avx2")
704        ))]
705        return unsafe { x86::gfni_sse::axpy_gfni_sse(c, x, y) };
706        #[cfg(all(
707            target_feature = "avx512f",
708            target_feature = "avx512bw",
709            target_feature = "ssse3",
710            not(target_feature = "gfni")
711        ))]
712        return unsafe { x86::avx512_ssse3::axpy_avx512_ssse3(c, x, y) };
713        #[cfg(all(
714            target_feature = "avx2",
715            target_feature = "ssse3",
716            not(target_feature = "gfni"),
717            not(all(target_feature = "avx512f", target_feature = "avx512bw"))
718        ))]
719        return unsafe { x86::avx2_ssse3::axpy_avx2_ssse3(c, x, y) };
720        #[cfg(all(target_feature = "ssse3", not(target_feature = "avx2")))]
721        return unsafe { x86::ssse3::axpy_ssse3(c, x, y) };
722    }
723
724    // Universal scalar fallback
725    #[cfg(not(any(
726        target_arch = "aarch64",
727        all(target_family = "wasm", target_feature = "simd128")
728    )))]
729    scalar::axpy(c, x, y)
730}
731
732#[cfg(not(feature = "std"))]
733#[inline]
734fn scale_static(c: u8, x: &[u8], y: &mut [u8]) {
735    #[cfg(all(target_family = "wasm", target_feature = "simd128"))]
736    // SAFETY: this branch is compiled only when SIMD128 is enabled.
737    return unsafe { wasm::simd128::scale_wasm(c, x, y) };
738
739    #[cfg(target_arch = "aarch64")]
740    return unsafe { arm::neon::scale_neon(c, x, y) };
741
742    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
743    {
744        #[cfg(all(
745            target_feature = "gfni",
746            target_feature = "avx512f",
747            target_feature = "avx512bw"
748        ))]
749        return unsafe { x86::gfni_avx512::scale_gfni_avx512(c, x, y) };
750        #[cfg(all(
751            target_feature = "gfni",
752            target_feature = "avx2",
753            not(all(target_feature = "avx512f", target_feature = "avx512bw"))
754        ))]
755        return unsafe { x86::gfni_avx2::scale_gfni_avx2(c, x, y) };
756        #[cfg(all(
757            target_feature = "gfni",
758            target_feature = "sse4.2",
759            not(target_feature = "avx2")
760        ))]
761        return unsafe { x86::gfni_sse::scale_gfni_sse(c, x, y) };
762        #[cfg(all(
763            target_feature = "avx512f",
764            target_feature = "avx512bw",
765            target_feature = "ssse3",
766            not(target_feature = "gfni")
767        ))]
768        return unsafe { x86::avx512_ssse3::scale_avx512_ssse3(c, x, y) };
769        #[cfg(all(
770            target_feature = "avx2",
771            target_feature = "ssse3",
772            not(target_feature = "gfni"),
773            not(all(target_feature = "avx512f", target_feature = "avx512bw"))
774        ))]
775        return unsafe { x86::avx2_ssse3::scale_avx2_ssse3(c, x, y) };
776        #[cfg(all(target_feature = "ssse3", not(target_feature = "avx2")))]
777        return unsafe { x86::ssse3::scale_ssse3(c, x, y) };
778    }
779
780    #[cfg(not(any(
781        target_arch = "aarch64",
782        all(target_family = "wasm", target_feature = "simd128")
783    )))]
784    scalar::scale(c, x, y)
785}
786
787#[cfg(not(feature = "std"))]
788#[inline]
789fn scale_inplace_static(c: u8, y: &mut [u8]) {
790    #[cfg(all(target_family = "wasm", target_feature = "simd128"))]
791    // SAFETY: this branch is compiled only when SIMD128 is enabled.
792    return unsafe { wasm::simd128::scale_inplace_wasm(c, y) };
793
794    #[cfg(target_arch = "aarch64")]
795    return unsafe { arm::neon::scale_inplace_neon(c, y) };
796
797    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
798    {
799        #[cfg(all(
800            target_feature = "gfni",
801            target_feature = "avx512f",
802            target_feature = "avx512bw"
803        ))]
804        return unsafe { x86::gfni_avx512::scale_inplace_gfni_avx512(c, y) };
805        #[cfg(all(
806            target_feature = "gfni",
807            target_feature = "avx2",
808            not(all(target_feature = "avx512f", target_feature = "avx512bw"))
809        ))]
810        return unsafe { x86::gfni_avx2::scale_inplace_gfni_avx2(c, y) };
811        #[cfg(all(
812            target_feature = "gfni",
813            target_feature = "sse4.2",
814            not(target_feature = "avx2")
815        ))]
816        return unsafe { x86::gfni_sse::scale_inplace_gfni_sse(c, y) };
817        #[cfg(all(
818            target_feature = "avx512f",
819            target_feature = "avx512bw",
820            target_feature = "ssse3",
821            not(target_feature = "gfni")
822        ))]
823        return unsafe { x86::avx512_ssse3::scale_inplace_avx512_ssse3(c, y) };
824        #[cfg(all(
825            target_feature = "avx2",
826            target_feature = "ssse3",
827            not(target_feature = "gfni"),
828            not(all(target_feature = "avx512f", target_feature = "avx512bw"))
829        ))]
830        return unsafe { x86::avx2_ssse3::scale_inplace_avx2_ssse3(c, y) };
831        #[cfg(all(target_feature = "ssse3", not(target_feature = "avx2")))]
832        return unsafe { x86::ssse3::scale_inplace_ssse3(c, y) };
833    }
834
835    #[cfg(not(any(
836        target_arch = "aarch64",
837        all(target_family = "wasm", target_feature = "simd128")
838    )))]
839    scalar::scale_inplace(c, y)
840}
841
842#[cfg(not(feature = "std"))]
843fn active_kernel_name_static() -> &'static str {
844    #[cfg(all(target_family = "wasm", target_feature = "simd128"))]
845    return "wasm-simd128 (tier8)";
846    #[cfg(target_arch = "aarch64")]
847    return "neon (tier7)";
848    #[cfg(all(
849        target_feature = "gfni",
850        target_feature = "avx512f",
851        target_feature = "avx512bw"
852    ))]
853    return "gfni+avx512 (tier1)";
854    #[cfg(all(
855        target_feature = "gfni",
856        target_feature = "avx2",
857        not(all(target_feature = "avx512f", target_feature = "avx512bw"))
858    ))]
859    return "gfni+avx2 (tier2)";
860    #[cfg(all(
861        target_feature = "gfni",
862        target_feature = "sse4.2",
863        not(target_feature = "avx2")
864    ))]
865    return "gfni+sse4.2 (tier3)";
866    #[cfg(all(
867        target_feature = "avx512f",
868        target_feature = "avx512bw",
869        target_feature = "ssse3",
870        not(target_feature = "gfni")
871    ))]
872    return "avx512+ssse3 (tier4)";
873    #[cfg(all(
874        target_feature = "avx2",
875        target_feature = "ssse3",
876        not(target_feature = "gfni"),
877        not(all(target_feature = "avx512f", target_feature = "avx512bw"))
878    ))]
879    return "avx2+ssse3 (tier5)";
880    #[cfg(all(target_feature = "ssse3", not(target_feature = "avx2")))]
881    return "ssse3 (tier6)";
882    #[cfg(not(any(
883        target_arch = "aarch64",
884        all(target_family = "wasm", target_feature = "simd128")
885    )))]
886    return "scalar (tier9)";
887}
888
889#[cfg(not(feature = "std"))]
890#[inline]
891fn axpy_multi_static(coeffs: &[u8], sources: &[&[u8]], y: &mut [u8]) -> bool {
892    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
893    {
894        #[cfg(all(
895            target_feature = "gfni",
896            target_feature = "avx512f",
897            target_feature = "avx512bw"
898        ))]
899        unsafe {
900            x86::gfni_avx512::axpy_multi_gfni_avx512(coeffs, sources, y);
901            return true;
902        }
903        #[cfg(all(
904            target_feature = "gfni",
905            target_feature = "avx2",
906            not(all(target_feature = "avx512f", target_feature = "avx512bw"))
907        ))]
908        unsafe {
909            x86::gfni_avx2::axpy_multi_gfni_avx2(coeffs, sources, y);
910            return true;
911        }
912        #[cfg(all(
913            target_feature = "gfni",
914            target_feature = "sse4.2",
915            not(target_feature = "avx2")
916        ))]
917        unsafe {
918            x86::gfni_sse::axpy_multi_gfni_sse(coeffs, sources, y);
919            return true;
920        }
921    }
922    let _ = (coeffs, sources, y);
923    false
924}
925
926#[cfg(not(feature = "std"))]
927#[inline]
928fn dot_static(a: &[u8], b: &[u8]) -> Option<u8> {
929    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
930    {
931        #[cfg(all(
932            target_feature = "gfni",
933            target_feature = "avx512f",
934            target_feature = "avx512bw"
935        ))]
936        return Some(unsafe { x86::gfni_avx512::dot_gfni_avx512(a, b) });
937        #[cfg(all(
938            target_feature = "gfni",
939            target_feature = "avx2",
940            not(all(target_feature = "avx512f", target_feature = "avx512bw"))
941        ))]
942        return Some(unsafe { x86::gfni_avx2::dot_gfni_avx2(a, b) });
943        #[cfg(all(
944            target_feature = "gfni",
945            target_feature = "sse4.2",
946            not(target_feature = "avx2")
947        ))]
948        return Some(unsafe { x86::gfni_sse::dot_gfni_sse(a, b) });
949    }
950    let _ = (a, b);
951    None
952}
953
954// ---------------------------------------------------------------------------
955// Tests
956// ---------------------------------------------------------------------------
957
958#[cfg(test)]
959mod tests {
960    use super::*;
961
962    #[test]
963    fn axpy_round_trip() {
964        let c = 0x53u8;
965        let x: Vec<u8> = (0u8..128).collect();
966        let mut y = vec![0u8; 128];
967        axpy(c, &x, &mut y);
968        axpy(c, &x, &mut y);
969        assert_eq!(y, vec![0u8; 128]);
970    }
971
972    #[test]
973    fn scale_axpy_consistency() {
974        let c = 0xC7u8;
975        let x: Vec<u8> = (1u8..=64).collect();
976        let mut y_scale = vec![0u8; 64];
977        let mut y_axpy = vec![0u8; 64];
978        scale(c, &x, &mut y_scale);
979        axpy(c, &x, &mut y_axpy);
980        assert_eq!(y_scale, y_axpy);
981    }
982
983    #[test]
984    fn dispatched_dot_matches_scalar_across_vector_tails() {
985        for len in [0usize, 1, 15, 16, 17, 31, 32, 33, 63, 64, 65, 1025] {
986            let a: Vec<u8> = (0..len)
987                .map(|index| (index as u8).wrapping_mul(17).wrapping_add(1))
988                .collect();
989            let b: Vec<u8> = (0..len)
990                .map(|index| (index as u8).wrapping_mul(29).wrapping_add(3))
991                .collect();
992            assert_eq!(dot(&a, &b), scalar::dot(&a, &b), "len={len}");
993        }
994    }
995
996    #[test]
997    fn kernel_name_not_empty() {
998        let name = active_kernel_name();
999        assert!(!name.is_empty());
1000        println!("Active kernel: {name}");
1001    }
1002
1003    #[test]
1004    #[cfg(feature = "std")]
1005    fn runtime_dispatch_selects_best() {
1006        // The selected kernel name should reflect what is actually available.
1007        #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
1008        {
1009            let name = active_kernel_name();
1010            if is_x86_feature_detected!("gfni") && is_x86_feature_detected!("avx512f") {
1011                assert!(name.contains("tier1"), "Expected tier1, got: {name}");
1012            } else if is_x86_feature_detected!("gfni") && is_x86_feature_detected!("avx2") {
1013                assert!(name.contains("tier2"), "Expected tier2, got: {name}");
1014            } else if is_x86_feature_detected!("ssse3") {
1015                assert!(
1016                    name.contains("tier3")
1017                        || name.contains("tier4")
1018                        || name.contains("tier5")
1019                        || name.contains("tier6"),
1020                    "Expected SSSE3-tier, got: {name}"
1021                );
1022            }
1023        }
1024
1025        #[cfg(target_arch = "aarch64")]
1026        assert_eq!(active_kernel_name(), "neon (tier7)");
1027
1028        #[cfg(not(any(target_arch = "x86", target_arch = "x86_64", target_arch = "aarch64")))]
1029        assert!(!active_kernel_name().is_empty());
1030    }
1031}