Skip to main content

subetha_cxc/
fec.rs

1//! Forward error correction over GF(256): systematic Cauchy
2//! Reed-Solomon erasure coding.
3//!
4//! Packet loss on UDP is an *erasure* - the receiver knows WHICH
5//! packet is missing from the gap in the sequence numbers - so the
6//! decoder recovers from any K survivors out of K + R coded packets
7//! without needing to locate the error. This is the FEC half of the
8//! reliable-UDP transport's FEC-primary / ARQ-fallback design: a block
9//! of K source packets ships with R parity packets, and up to R losses
10//! per block are recovered with no retransmit round-trip.
11//!
12//! The code is *systematic* (the K source shards are transmitted
13//! verbatim; only the R parity shards are computed) and *MDS* (any K of
14//! the K + R shards reconstruct the block). The MDS property comes from
15//! a Cauchy parity matrix: every square submatrix of a Cauchy matrix is
16//! invertible, so any K-row submatrix of the `[I_K ; Cauchy]` encoding
17//! matrix is invertible.
18//!
19//! GF(256) uses the primitive polynomial `0x11D` with log / antilog
20//! tables computed at compile time (`const fn`), so there is no runtime
21//! initialization.
22
23#![allow(clippy::needless_range_loop)]
24
25/// GF(256) arithmetic with the primitive polynomial `x^8 + x^4 + x^3 +
26/// x^2 + 1` (`0x11D`) and generator `2`.
27pub mod gf {
28    /// `(LOG, EXP)`: `EXP` is doubled to 512 entries so `EXP[log a +
29    /// log b]` needs no modular reduction (`log a + log b <= 508`).
30    const fn build_tables() -> ([u8; 256], [u8; 512]) {
31        let mut log = [0u8; 256];
32        let mut exp = [0u8; 512];
33        let mut x: u16 = 1;
34        let mut i = 0usize;
35        while i < 255 {
36            exp[i] = x as u8;
37            log[x as usize] = i as u8;
38            x <<= 1;
39            if x & 0x100 != 0 {
40                x ^= 0x11D;
41            }
42            i += 1;
43        }
44        // Second period for multiply without a modulo.
45        let mut j = 255usize;
46        while j < 512 {
47            exp[j] = exp[j - 255];
48            j += 1;
49        }
50        (log, exp)
51    }
52
53    const TABLES: ([u8; 256], [u8; 512]) = build_tables();
54    const LOG: [u8; 256] = TABLES.0;
55    const EXP: [u8; 512] = TABLES.1;
56
57    /// Addition (and subtraction) in GF(256) is XOR.
58    #[inline(always)]
59    pub const fn add(a: u8, b: u8) -> u8 {
60        a ^ b
61    }
62
63    /// Multiplication via the log / antilog tables.
64    #[inline(always)]
65    pub fn mul(a: u8, b: u8) -> u8 {
66        if a == 0 || b == 0 {
67            0
68        } else {
69            EXP[LOG[a as usize] as usize + LOG[b as usize] as usize]
70        }
71    }
72
73    /// Multiplicative inverse (`a != 0`).
74    #[inline(always)]
75    pub fn inv(a: u8) -> u8 {
76        debug_assert!(a != 0, "GF(256) has no inverse of 0");
77        EXP[255 - LOG[a as usize] as usize]
78    }
79
80    /// Division `a / b` (`b != 0`).
81    #[inline(always)]
82    pub fn div(a: u8, b: u8) -> u8 {
83        if a == 0 {
84            0
85        } else {
86            EXP[LOG[a as usize] as usize + 255 - LOG[b as usize] as usize]
87        }
88    }
89}
90
91/// GF(256) multiply-add backend, exposed so an A/B bench can compare every
92/// SIMD rung against the scalar baseline and so the GFNI / AVX-512 logic can be
93/// validated on a host without the silicon via the bit-exact software affine
94/// emulation. The production hot loop (`gf_mul_add`) auto-selects the fastest
95/// rung this host can run; the ladder always bottoms out at `Scalar`.
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97#[repr(u8)]
98pub enum GfBackend {
99    /// Portable table-lookup multiply. Always available; the fallback floor.
100    Scalar = 0,
101    /// SSSE3 PSHUFB nibble-table multiply, 16 bytes per op.
102    Ssse3 = 1,
103    /// AVX2 PSHUFB nibble-table multiply, 32 bytes per op.
104    Avx2 = 2,
105    /// AVX-512BW PSHUFB nibble-table multiply, 64 bytes per op (no GFNI).
106    Avx512Pshufb = 3,
107    /// GFNI affine GF(2^8) multiply on 256-bit lanes: a hardware field multiply
108    /// (no table), broad consumer reach (`gfni` + `avx2`, no AVX-512 needed).
109    Gfni256 = 4,
110    /// GFNI affine GF(2^8) multiply on 512-bit lanes: 64 bytes per op in one
111    /// hardware instruction.
112    Gfni512 = 5,
113    /// Software emulation of the GFNI affine transform: bit-exact to the GFNI
114    /// hardware path, runnable on any host. Validates the GFNI logic where the
115    /// silicon is absent, the same emulate-to-verify approach the AVX-512
116    /// substrate uses.
117    AffineScalar = 6,
118    /// ARM NEON TBL nibble-table multiply, 16 bytes per `vqtbl1q_u8`. The
119    /// aarch64 byte-shuffle rung: the same nibble-table technique as SSSE3, so
120    /// bit-identical to it (and to scalar). NEON is baseline on every aarch64
121    /// CPU, so this rung is always available on Apple Silicon / Neoverse.
122    Neon = 7,
123}
124
125impl GfBackend {
126    /// Whether this backend can execute on the current host.
127    pub fn available(self) -> bool {
128        match self {
129            GfBackend::Scalar | GfBackend::AffineScalar => true,
130            #[cfg(target_arch = "aarch64")]
131            GfBackend::Neon => std::arch::is_aarch64_feature_detected!("neon"),
132            #[cfg(not(target_arch = "aarch64"))]
133            GfBackend::Neon => false,
134            #[cfg(target_arch = "x86_64")]
135            GfBackend::Ssse3 => std::is_x86_feature_detected!("ssse3"),
136            #[cfg(target_arch = "x86_64")]
137            GfBackend::Avx2 => std::is_x86_feature_detected!("avx2"),
138            #[cfg(target_arch = "x86_64")]
139            GfBackend::Avx512Pshufb => {
140                std::is_x86_feature_detected!("avx512f")
141                    && std::is_x86_feature_detected!("avx512bw")
142            }
143            #[cfg(target_arch = "x86_64")]
144            GfBackend::Gfni256 => {
145                std::is_x86_feature_detected!("gfni") && std::is_x86_feature_detected!("avx2")
146            }
147            #[cfg(target_arch = "x86_64")]
148            GfBackend::Gfni512 => {
149                std::is_x86_feature_detected!("gfni")
150                    && std::is_x86_feature_detected!("avx512f")
151                    && std::is_x86_feature_detected!("avx512bw")
152            }
153            #[cfg(not(target_arch = "x86_64"))]
154            _ => false,
155        }
156    }
157
158    /// Short identifier for diagnostics / bench output.
159    pub fn name(self) -> &'static str {
160        match self {
161            GfBackend::Scalar => "scalar",
162            GfBackend::Ssse3 => "ssse3",
163            GfBackend::Avx2 => "avx2",
164            GfBackend::Avx512Pshufb => "avx512-pshufb",
165            GfBackend::Gfni256 => "gfni256",
166            GfBackend::Gfni512 => "gfni512",
167            GfBackend::AffineScalar => "affine-emulated",
168            GfBackend::Neon => "neon",
169        }
170    }
171
172    fn from_u8(v: u8) -> Self {
173        match v {
174            1 => GfBackend::Ssse3,
175            2 => GfBackend::Avx2,
176            3 => GfBackend::Avx512Pshufb,
177            4 => GfBackend::Gfni256,
178            5 => GfBackend::Gfni512,
179            6 => GfBackend::AffineScalar,
180            7 => GfBackend::Neon,
181            _ => GfBackend::Scalar,
182        }
183    }
184}
185
186/// The fastest-first ladder. The production dispatcher walks it and picks the
187/// first available rung; the A/B bench confirms each rung beats the next, so
188/// the order is empirically grounded rather than assumed.
189#[cfg(target_arch = "x86_64")]
190const GF_LADDER: [GfBackend; 6] = [
191    GfBackend::Gfni512,
192    GfBackend::Avx512Pshufb,
193    GfBackend::Gfni256,
194    GfBackend::Avx2,
195    GfBackend::Ssse3,
196    GfBackend::Scalar,
197];
198/// aarch64 rungs: NEON TBL then the scalar floor. NEON is baseline on every
199/// aarch64 CPU, so the auto-detector picks NEON on all Apple Silicon /
200/// Neoverse hosts and reserves scalar for the (unreachable) no-NEON case.
201#[cfg(target_arch = "aarch64")]
202const GF_LADDER: [GfBackend; 2] = [GfBackend::Neon, GfBackend::Scalar];
203/// Every other architecture has only the portable scalar rung.
204#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
205const GF_LADDER: [GfBackend; 1] = [GfBackend::Scalar];
206
207fn detect_best_backend() -> GfBackend {
208    GF_LADDER
209        .into_iter()
210        .find(|b| b.available())
211        .unwrap_or(GfBackend::Scalar)
212}
213
214/// Detection cache: the auto-selected backend, computed once on first use.
215/// `BACKEND_UNINIT` until then. This is a pure cache - there is no process-wide
216/// override (a global mutable backend would bleed across parallel tests and is
217/// the wrong shape); per-call backend selection goes through
218/// [`gf_mul_add_backend`] and per-code-instance selection through
219/// [`RsCode::with_backend`].
220const BACKEND_UNINIT: u8 = 0xFF;
221static SELECTED_BACKEND: std::sync::atomic::AtomicU8 =
222    std::sync::atomic::AtomicU8::new(BACKEND_UNINIT);
223
224#[inline]
225fn current_backend() -> GfBackend {
226    use std::sync::atomic::Ordering;
227    let v = SELECTED_BACKEND.load(Ordering::Relaxed);
228    if v == BACKEND_UNINIT {
229        let b = detect_best_backend();
230        SELECTED_BACKEND.store(b as u8, Ordering::Relaxed);
231        b
232    } else {
233        GfBackend::from_u8(v)
234    }
235}
236
237/// `out[i] ^= gf::mul(coef, src[i])` over the whole slice - the hot inner step
238/// of RS encode and decode, run through the given GF(256) `backend` (GFNI /
239/// AVX-512 / AVX2 / SSSE3 / scalar). The `coef == 0 / 1` shortcuts skip the
240/// multiply entirely.
241#[inline]
242fn gf_mul_add(backend: GfBackend, out: &mut [u8], src: &[u8], coef: u8) {
243    debug_assert_eq!(out.len(), src.len());
244    if coef == 0 {
245        return;
246    }
247    if coef == 1 {
248        for (o, &s) in out.iter_mut().zip(src) {
249            *o ^= s;
250        }
251        return;
252    }
253    gf_mul_add_backend(backend, out, src, coef);
254}
255
256/// `out[i] ^= gf::mul(coef, src[i])` through the auto-detected fastest backend.
257/// The public entry point for callers outside `RsCode` (the sliding-window RLC
258/// FEC) that want the SIMD-accelerated GF(2^8) multiply-add without managing a
259/// backend. The `coef == 0 / 1` shortcuts skip the multiply.
260pub fn gf_mul_add_auto(out: &mut [u8], src: &[u8], coef: u8) {
261    gf_mul_add(current_backend(), out, src, coef);
262}
263
264/// Run `gf_mul_add` through a specific [`GfBackend`] - the A/B-bench and
265/// emulation-validation entry point. The hardware rungs require their ISA
266/// feature; call only those `GfBackend::available()` reports (the
267/// `debug_assert` catches a mismatch in test builds).
268pub fn gf_mul_add_backend(backend: GfBackend, out: &mut [u8], src: &[u8], coef: u8) {
269    debug_assert_eq!(out.len(), src.len());
270    debug_assert!(
271        backend.available(),
272        "GF backend {} is not available on this host",
273        backend.name()
274    );
275    match backend {
276        GfBackend::Scalar => gf_mul_add_scalar(out, src, coef),
277        GfBackend::AffineScalar => gf_mul_add_affine_scalar(out, src, coef),
278        #[cfg(target_arch = "aarch64")]
279        // SAFETY: reached only for an available() Neon backend; aarch64 has
280        // baseline NEON; lengths are matched by the debug_assert.
281        GfBackend::Neon => unsafe { gf_mul_add_neon(out, src, coef) },
282        #[cfg(not(target_arch = "aarch64"))]
283        GfBackend::Neon => gf_mul_add_scalar(out, src, coef),
284        #[cfg(target_arch = "x86_64")]
285        // SAFETY: each arm is reached only for an available() backend, so its
286        // ISA feature is present; lengths are matched by the debug_assert.
287        GfBackend::Ssse3 => unsafe { gf_mul_add_ssse3(out, src, coef) },
288        #[cfg(target_arch = "x86_64")]
289        GfBackend::Avx2 => unsafe { gf_mul_add_avx2(out, src, coef) },
290        #[cfg(target_arch = "x86_64")]
291        GfBackend::Avx512Pshufb => unsafe { gf_mul_add_avx512(out, src, coef) },
292        #[cfg(target_arch = "x86_64")]
293        GfBackend::Gfni256 => unsafe { gf_mul_add_gfni256(out, src, coef) },
294        #[cfg(target_arch = "x86_64")]
295        GfBackend::Gfni512 => unsafe { gf_mul_add_gfni512(out, src, coef) },
296        #[cfg(not(target_arch = "x86_64"))]
297        _ => gf_mul_add_scalar(out, src, coef),
298    }
299}
300
301fn gf_mul_add_scalar(out: &mut [u8], src: &[u8], coef: u8) {
302    for (o, &s) in out.iter_mut().zip(src) {
303        *o ^= gf::mul(coef, s);
304    }
305}
306
307/// Low / high nibble multiply tables for `coef`: `lo[i] = coef*i`,
308/// `hi[i] = coef*(i<<4)` over GF(256). The byte-shuffle backends gather each
309/// into place by 16-byte table lookup: x86 PSHUFB (`_mm_shuffle_epi8`) and
310/// ARM NEON TBL (`vqtbl1q_u8`). On targets with neither, the scalar path
311/// multiplies directly and this table is unused.
312#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
313#[inline]
314fn gf_nibble_tables(coef: u8) -> ([u8; 16], [u8; 16]) {
315    let mut lo = [0u8; 16];
316    let mut hi = [0u8; 16];
317    for i in 0..16u8 {
318        lo[i as usize] = gf::mul(coef, i);
319        hi[i as usize] = gf::mul(coef, i << 4);
320    }
321    (lo, hi)
322}
323
324#[cfg(target_arch = "x86_64")]
325#[target_feature(enable = "ssse3")]
326unsafe fn gf_mul_add_ssse3(out: &mut [u8], src: &[u8], coef: u8) {
327    use std::arch::x86_64::*;
328    let (lo, hi) = gf_nibble_tables(coef);
329    let n = out.len();
330    // SAFETY: ssse3 enabled at the call site; every load/store is bounded
331    // by `i + 16 <= n` or the scalar tail.
332    unsafe {
333        let lo_v = _mm_loadu_si128(lo.as_ptr() as *const __m128i);
334        let hi_v = _mm_loadu_si128(hi.as_ptr() as *const __m128i);
335        let mask = _mm_set1_epi8(0x0f);
336        let mut i = 0usize;
337        while i + 16 <= n {
338            let s = _mm_loadu_si128(src.as_ptr().add(i) as *const __m128i);
339            let lo_n = _mm_and_si128(s, mask);
340            let hi_n = _mm_and_si128(_mm_srli_epi16(s, 4), mask);
341            let prod = _mm_xor_si128(_mm_shuffle_epi8(lo_v, lo_n), _mm_shuffle_epi8(hi_v, hi_n));
342            let o = _mm_loadu_si128(out.as_ptr().add(i) as *const __m128i);
343            _mm_storeu_si128(out.as_mut_ptr().add(i) as *mut __m128i, _mm_xor_si128(o, prod));
344            i += 16;
345        }
346        while i < n {
347            out[i] ^= gf::mul(coef, src[i]);
348            i += 1;
349        }
350    }
351}
352
353#[cfg(target_arch = "x86_64")]
354#[target_feature(enable = "avx2")]
355unsafe fn gf_mul_add_avx2(out: &mut [u8], src: &[u8], coef: u8) {
356    use std::arch::x86_64::*;
357    let (lo, hi) = gf_nibble_tables(coef);
358    let n = out.len();
359    // SAFETY: avx2 enabled at the call site; every load/store is bounded
360    // by `i + 32 <= n` or the scalar tail.
361    unsafe {
362        let lo_v = _mm256_broadcastsi128_si256(_mm_loadu_si128(lo.as_ptr() as *const __m128i));
363        let hi_v = _mm256_broadcastsi128_si256(_mm_loadu_si128(hi.as_ptr() as *const __m128i));
364        let mask = _mm256_set1_epi8(0x0f);
365        let mut i = 0usize;
366        while i + 32 <= n {
367            let s = _mm256_loadu_si256(src.as_ptr().add(i) as *const __m256i);
368            let lo_n = _mm256_and_si256(s, mask);
369            let hi_n = _mm256_and_si256(_mm256_srli_epi16(s, 4), mask);
370            let prod = _mm256_xor_si256(
371                _mm256_shuffle_epi8(lo_v, lo_n),
372                _mm256_shuffle_epi8(hi_v, hi_n),
373            );
374            let o = _mm256_loadu_si256(out.as_ptr().add(i) as *const __m256i);
375            _mm256_storeu_si256(out.as_mut_ptr().add(i) as *mut __m256i, _mm256_xor_si256(o, prod));
376            i += 32;
377        }
378        while i < n {
379            out[i] ^= gf::mul(coef, src[i]);
380            i += 1;
381        }
382    }
383}
384
385/// The 8x8 GF(2) matrix (packed into a u64 in GFNI byte order) that maps
386/// `x -> gf::mul(coef, x)` over GF(256) with the field's 0x11D polynomial.
387///
388/// `GF2P8AFFINEQB` computes output bit `i` as `parity(A.byte[7-i] AND x)`, so
389/// `A.byte[7-i]` is the linear form for output bit `i`: its bit `j` is set if
390/// and only if bit `i` of `coef * 2^j` is set (multiply-by-`coef` is
391/// GF(2)-linear, and the polynomial lives entirely in this precomputed matrix).
392/// The matrix feeds both the hardware GFNI instruction and the
393/// [`gf2p8affine_byte`] software emulation, so the two are bit-identical by
394/// construction.
395fn gf_affine_matrix(coef: u8) -> u64 {
396    let mut m = 0u64;
397    for i in 0..8usize {
398        let mut row = 0u8;
399        for j in 0..8usize {
400            if (gf::mul(coef, 1u8 << j) >> i) & 1 == 1 {
401                row |= 1u8 << j;
402            }
403        }
404        let byte_idx = 7 - i;
405        m |= (row as u64) << (8 * byte_idx);
406    }
407    m
408}
409
410/// Software emulation of one `GF2P8AFFINEQB` byte (imm = 0): bit-exact to the
411/// GFNI hardware instruction, so the affine GF(2^8) multiply can be validated
412/// on a host without the silicon. `out.bit[i] = parity(m.byte[7-i] AND x)`.
413#[inline]
414fn gf2p8affine_byte(x: u8, m: u64) -> u8 {
415    let mut out = 0u8;
416    for i in 0..8usize {
417        let mat_byte = ((m >> (8 * (7 - i))) & 0xff) as u8;
418        if (mat_byte & x).count_ones() & 1 == 1 {
419            out |= 1u8 << i;
420        }
421    }
422    out
423}
424
425/// `gf_mul_add` via the software affine transform: the bit-exact emulation of
426/// the GFNI hardware path, runnable on any host. The matrix is built once per
427/// coefficient, then applied per byte.
428fn gf_mul_add_affine_scalar(out: &mut [u8], src: &[u8], coef: u8) {
429    let m = gf_affine_matrix(coef);
430    for (o, &s) in out.iter_mut().zip(src) {
431        *o ^= gf2p8affine_byte(s, m);
432    }
433}
434
435/// AVX-512BW PSHUFB nibble-table multiply: the same nibble-table technique as
436/// the AVX2 path, 64 bytes per `_mm512_shuffle_epi8`. For AVX-512 hosts without
437/// GFNI (Skylake-X / Cascade Lake). Bit-identical to the AVX2 path, which is
438/// its emulation on a narrower host.
439#[cfg(target_arch = "x86_64")]
440#[target_feature(enable = "avx512f,avx512bw")]
441unsafe fn gf_mul_add_avx512(out: &mut [u8], src: &[u8], coef: u8) {
442    use std::arch::x86_64::*;
443    let (lo, hi) = gf_nibble_tables(coef);
444    let n = out.len();
445    // SAFETY: avx512f+avx512bw enabled at the call site; every load/store is
446    // bounded by `i + 64 <= n` or the scalar tail.
447    unsafe {
448        let lo_v = _mm512_broadcast_i32x4(_mm_loadu_si128(lo.as_ptr() as *const __m128i));
449        let hi_v = _mm512_broadcast_i32x4(_mm_loadu_si128(hi.as_ptr() as *const __m128i));
450        let mask = _mm512_set1_epi8(0x0f);
451        let mut i = 0usize;
452        while i + 64 <= n {
453            let s = _mm512_loadu_si512(src.as_ptr().add(i) as *const __m512i);
454            let lo_n = _mm512_and_si512(s, mask);
455            let hi_n = _mm512_and_si512(_mm512_srli_epi16::<4>(s), mask);
456            let prod = _mm512_xor_si512(
457                _mm512_shuffle_epi8(lo_v, lo_n),
458                _mm512_shuffle_epi8(hi_v, hi_n),
459            );
460            let o = _mm512_loadu_si512(out.as_ptr().add(i) as *const __m512i);
461            _mm512_storeu_si512(
462                out.as_mut_ptr().add(i) as *mut __m512i,
463                _mm512_xor_si512(o, prod),
464            );
465            i += 64;
466        }
467        while i < n {
468            out[i] ^= gf::mul(coef, src[i]);
469            i += 1;
470        }
471    }
472}
473
474/// GFNI affine multiply on 256-bit lanes: one `_mm256_gf2p8affine_epi64_epi8`
475/// does the GF(2^8) multiply-by-`coef` in hardware, no nibble table. Needs only
476/// `gfni` + `avx2`, so it reaches consumer Zen 4 / Alder Lake+ without AVX-512.
477#[cfg(target_arch = "x86_64")]
478#[target_feature(enable = "gfni,avx2")]
479unsafe fn gf_mul_add_gfni256(out: &mut [u8], src: &[u8], coef: u8) {
480    use std::arch::x86_64::*;
481    let matrix = _mm256_set1_epi64x(gf_affine_matrix(coef) as i64);
482    let n = out.len();
483    // SAFETY: gfni+avx2 enabled at the call site; loads/stores bounded by
484    // `i + 32 <= n` or the scalar tail.
485    unsafe {
486        let mut i = 0usize;
487        while i + 32 <= n {
488            let s = _mm256_loadu_si256(src.as_ptr().add(i) as *const __m256i);
489            let prod = _mm256_gf2p8affine_epi64_epi8::<0>(s, matrix);
490            let o = _mm256_loadu_si256(out.as_ptr().add(i) as *const __m256i);
491            _mm256_storeu_si256(
492                out.as_mut_ptr().add(i) as *mut __m256i,
493                _mm256_xor_si256(o, prod),
494            );
495            i += 32;
496        }
497        while i < n {
498            out[i] ^= gf::mul(coef, src[i]);
499            i += 1;
500        }
501    }
502}
503
504/// GFNI affine multiply on 512-bit lanes: 64 bytes of GF(2^8) multiply-by-`coef`
505/// per `_mm512_gf2p8affine_epi64_epi8`, a hardware field multiply with no table
506/// lookup. The top rung (Genoa / Sapphire Rapids / Zen 4+).
507#[cfg(target_arch = "x86_64")]
508#[target_feature(enable = "gfni,avx512f,avx512bw")]
509unsafe fn gf_mul_add_gfni512(out: &mut [u8], src: &[u8], coef: u8) {
510    use std::arch::x86_64::*;
511    let matrix = _mm512_set1_epi64(gf_affine_matrix(coef) as i64);
512    let n = out.len();
513    // SAFETY: gfni+avx512f+avx512bw enabled at the call site; loads/stores
514    // bounded by `i + 64 <= n` or the scalar tail.
515    unsafe {
516        let mut i = 0usize;
517        while i + 64 <= n {
518            let s = _mm512_loadu_si512(src.as_ptr().add(i) as *const __m512i);
519            let prod = _mm512_gf2p8affine_epi64_epi8::<0>(s, matrix);
520            let o = _mm512_loadu_si512(out.as_ptr().add(i) as *const __m512i);
521            _mm512_storeu_si512(
522                out.as_mut_ptr().add(i) as *mut __m512i,
523                _mm512_xor_si512(o, prod),
524            );
525            i += 64;
526        }
527        while i < n {
528            out[i] ^= gf::mul(coef, src[i]);
529            i += 1;
530        }
531    }
532}
533
534/// ARM NEON TBL nibble-table multiply: the aarch64 mirror of the SSSE3 path,
535/// 16 bytes per `vqtbl1q_u8`. `vshrq_n_u8::<4>` extracts each byte's high
536/// nibble in one byte-wise shift (no mask needed, unlike x86's 16-bit shift),
537/// and the two table lookups XOR to `coef * byte` over GF(256) - bit-identical
538/// to the SSSE3 / scalar result.
539#[cfg(target_arch = "aarch64")]
540#[target_feature(enable = "neon")]
541unsafe fn gf_mul_add_neon(out: &mut [u8], src: &[u8], coef: u8) {
542    use std::arch::aarch64::*;
543    let (lo, hi) = gf_nibble_tables(coef);
544    let n = out.len();
545    // SAFETY: neon enabled at the call site; every load/store is bounded by
546    // `i + 16 <= n` or the scalar tail.
547    unsafe {
548        let lo_v = vld1q_u8(lo.as_ptr());
549        let hi_v = vld1q_u8(hi.as_ptr());
550        let mask = vdupq_n_u8(0x0f);
551        let mut i = 0usize;
552        while i + 16 <= n {
553            let s = vld1q_u8(src.as_ptr().add(i));
554            let lo_n = vandq_u8(s, mask);
555            let hi_n = vshrq_n_u8::<4>(s);
556            let prod = veorq_u8(vqtbl1q_u8(lo_v, lo_n), vqtbl1q_u8(hi_v, hi_n));
557            let o = vld1q_u8(out.as_ptr().add(i));
558            vst1q_u8(out.as_mut_ptr().add(i), veorq_u8(o, prod));
559            i += 16;
560        }
561        while i < n {
562            out[i] ^= gf::mul(coef, src[i]);
563            i += 1;
564        }
565    }
566}
567
568/// A systematic Cauchy Reed-Solomon erasure code: `k` data shards plus
569/// `r` parity shards, recovering any `k` of the `k + r`.
570#[derive(Debug, Clone)]
571pub struct RsCode {
572    k: usize,
573    r: usize,
574    /// Row-major `r * k` Cauchy parity matrix: `parity[j] = sum_c
575    /// cauchy[j*k + c] * data[c]`.
576    cauchy: Vec<u8>,
577    /// Optional GF(256) backend override for this code (`None` = the auto-
578    /// detected fastest rung). Per-instance, not a process global, so an A/B
579    /// bench or an emulation-validation test can pin a backend without bleeding
580    /// into other code paths.
581    backend: Option<GfBackend>,
582}
583
584/// Error from constructing or decoding an [`RsCode`].
585#[derive(Debug, Clone, Copy, PartialEq, Eq)]
586pub enum FecError {
587    /// `k == 0`, `r == 0`, or `k + r > 256` (field size).
588    BadParams,
589    /// Fewer than `k` shards survived; FEC cannot recover (ARQ falls
590    /// back here).
591    TooFewShards,
592    /// Shards had unequal or zero length.
593    BadShardLen,
594}
595
596impl RsCode {
597    /// Build a `(k, r)` systematic Cauchy-RS code. `k >= 1`, `r >= 1`,
598    /// `k + r <= 256`.
599    pub fn new(k: usize, r: usize) -> Result<Self, FecError> {
600        if k == 0 || r == 0 || k + r > 256 {
601            return Err(FecError::BadParams);
602        }
603        // Cauchy entry C[j][c] = 1 / ((k + j) XOR c). Parity indices
604        // {k..k+r} and data indices {0..k} are disjoint, so the XOR is
605        // never zero and every square submatrix of `[I_k ; C]` is
606        // invertible (MDS).
607        let mut cauchy = vec![0u8; r * k];
608        for j in 0..r {
609            for c in 0..k {
610                let x = (k + j) as u8 ^ c as u8;
611                cauchy[j * k + c] = gf::inv(x);
612            }
613        }
614        Ok(Self { k, r, cauchy, backend: None })
615    }
616
617    /// Pin the GF(256) backend for this code (an A/B-bench / emulation-
618    /// validation knob), or `None` to use the auto-detected fastest rung. The
619    /// override is per-instance, so it never affects another `RsCode`.
620    pub fn with_backend(mut self, backend: Option<GfBackend>) -> Self {
621        self.backend = backend;
622        self
623    }
624
625    /// The backend this code uses: its pinned override, else the auto-detected
626    /// fastest available rung.
627    #[inline]
628    fn effective_backend(&self) -> GfBackend {
629        self.backend.unwrap_or_else(current_backend)
630    }
631
632    /// Number of data shards.
633    pub fn k(&self) -> usize {
634        self.k
635    }
636
637    /// Number of parity shards.
638    pub fn r(&self) -> usize {
639        self.r
640    }
641
642    /// Compute the `r` parity shards from the `k` data shards. All
643    /// shards (data and parity) must have the same length.
644    pub fn encode(
645        &self,
646        data: &[&[u8]],
647        parity: &mut [&mut [u8]],
648    ) -> Result<(), FecError> {
649        if data.len() != self.k || parity.len() != self.r {
650            return Err(FecError::BadParams);
651        }
652        let len = data[0].len();
653        if len == 0
654            || data.iter().any(|s| s.len() != len)
655            || parity.iter().any(|s| s.len() != len)
656        {
657            return Err(FecError::BadShardLen);
658        }
659        let backend = self.effective_backend();
660        for j in 0..self.r {
661            let row = &self.cauchy[j * self.k..(j + 1) * self.k];
662            parity[j].fill(0);
663            for c in 0..self.k {
664                let coef = row[c];
665                if coef != 0 {
666                    gf_mul_add(backend, parity[j], data[c], coef);
667                }
668            }
669        }
670        Ok(())
671    }
672
673    /// Recover the missing data shards in place. `shards` has `k + r`
674    /// entries (data shards first, then parity); `Some` = received,
675    /// `None` = lost. On success every data-shard slot `0..k` is
676    /// `Some`. Returns `TooFewShards` if fewer than `k` survived.
677    pub fn decode(&self, shards: &mut [Option<Vec<u8>>]) -> Result<(), FecError> {
678        let n = self.k + self.r;
679        if shards.len() != n {
680            return Err(FecError::BadParams);
681        }
682        // Already-present data shards need nothing.
683        if (0..self.k).all(|i| shards[i].is_some()) {
684            return Ok(());
685        }
686        // Pick the first k surviving shard positions.
687        let mut surv: Vec<usize> = Vec::with_capacity(self.k);
688        let mut len = 0usize;
689        for (idx, s) in shards.iter().enumerate() {
690            if let Some(v) = s
691                && surv.len() < self.k
692            {
693                if len == 0 {
694                    len = v.len();
695                } else if v.len() != len {
696                    return Err(FecError::BadShardLen);
697                }
698                surv.push(idx);
699            }
700        }
701        if surv.len() < self.k || len == 0 {
702            return Err(FecError::TooFewShards);
703        }
704        // Build the k x k encoding-matrix submatrix for the survivors,
705        // then invert it: data = A^-1 * survivors.
706        let mut a = vec![0u8; self.k * self.k];
707        for (row, &pos) in surv.iter().enumerate() {
708            for c in 0..self.k {
709                a[row * self.k + c] = self.enc_entry(pos, c);
710            }
711        }
712        let inv = invert(&a, self.k).ok_or(FecError::TooFewShards)?;
713        let backend = self.effective_backend();
714        // Recover each missing data shard i: data[i] = sum_j inv[i][j]
715        // * survivor_shard[j].
716        for i in 0..self.k {
717            if shards[i].is_some() {
718                continue;
719            }
720            let mut out = vec![0u8; len];
721            for j in 0..self.k {
722                let coef = inv[i * self.k + j];
723                if coef == 0 {
724                    continue;
725                }
726                let src = shards[surv[j]].as_ref().unwrap();
727                gf_mul_add(backend, &mut out, src, coef);
728            }
729            shards[i] = Some(out);
730        }
731        Ok(())
732    }
733
734    /// Entry `(row, col)` of the full `(k + r) x k` encoding matrix
735    /// `[I_k ; Cauchy]`.
736    #[inline]
737    fn enc_entry(&self, row: usize, col: usize) -> u8 {
738        if row < self.k {
739            if row == col {
740                1
741            } else {
742                0
743            }
744        } else {
745            self.cauchy[(row - self.k) * self.k + col]
746        }
747    }
748}
749
750/// Invert an `n x n` GF(256) matrix (row-major) by Gauss-Jordan
751/// elimination. Returns `None` if singular.
752fn invert(m: &[u8], n: usize) -> Option<Vec<u8>> {
753    let mut a = m.to_vec();
754    let mut inv = vec![0u8; n * n];
755    for i in 0..n {
756        inv[i * n + i] = 1;
757    }
758    for col in 0..n {
759        // Find a pivot row with a nonzero entry in `col`.
760        let mut piv = col;
761        while piv < n && a[piv * n + col] == 0 {
762            piv += 1;
763        }
764        if piv == n {
765            return None; // singular
766        }
767        if piv != col {
768            for c in 0..n {
769                a.swap(col * n + c, piv * n + c);
770                inv.swap(col * n + c, piv * n + c);
771            }
772        }
773        // Normalize the pivot row.
774        let pv = a[col * n + col];
775        let pinv = gf::inv(pv);
776        for c in 0..n {
777            a[col * n + c] = gf::mul(a[col * n + c], pinv);
778            inv[col * n + c] = gf::mul(inv[col * n + c], pinv);
779        }
780        // Eliminate `col` from every other row.
781        for row in 0..n {
782            if row == col {
783                continue;
784            }
785            let factor = a[row * n + col];
786            if factor == 0 {
787                continue;
788            }
789            for c in 0..n {
790                a[row * n + c] ^= gf::mul(factor, a[col * n + c]);
791                inv[row * n + c] ^= gf::mul(factor, inv[col * n + c]);
792            }
793        }
794    }
795    Some(inv)
796}
797
798#[cfg(test)]
799mod tests {
800    use super::*;
801
802    #[test]
803    fn gf_field_laws() {
804        // 0 is additive identity; 1 is multiplicative identity.
805        for a in 0u8..=255 {
806            assert_eq!(gf::add(a, 0), a);
807            assert_eq!(gf::mul(a, 1), a);
808            assert_eq!(gf::mul(a, 0), 0);
809        }
810        // a * inv(a) == 1 for every nonzero a.
811        for a in 1u8..=255 {
812            assert_eq!(gf::mul(a, gf::inv(a)), 1, "inverse of {a}");
813        }
814        // mul is commutative and div is its inverse.
815        for a in 0u8..=255 {
816            for b in 1u8..=255 {
817                assert_eq!(gf::mul(a, b), gf::mul(b, a));
818                assert_eq!(gf::div(gf::mul(a, b), b), a);
819            }
820        }
821    }
822
823    /// Exhaustively check that EVERY loss pattern dropping up to `r`
824    /// shards recovers the original data exactly. This validates the
825    /// Cauchy matrix construction and the decoder together.
826    fn exhaustive_recovery(k: usize, r: usize, len: usize) {
827        exhaustive_recovery_backend(k, r, len, None);
828    }
829
830    /// As [`exhaustive_recovery`], but pinning the GF(256) `backend` on the
831    /// code instance (no process-global state, so it is safe under the
832    /// parallel test runner).
833    fn exhaustive_recovery_backend(k: usize, r: usize, len: usize, backend: Option<GfBackend>) {
834        let code = RsCode::new(k, r).expect("code").with_backend(backend);
835        // Deterministic pseudo-random data shards.
836        let data: Vec<Vec<u8>> = (0..k)
837            .map(|i| {
838                (0..len)
839                    .map(|b| ((i * 131 + b * 17 + 7) & 0xFF) as u8)
840                    .collect()
841            })
842            .collect();
843        let mut parity: Vec<Vec<u8>> = vec![vec![0u8; len]; r];
844        {
845            let data_refs: Vec<&[u8]> = data.iter().map(|s| s.as_slice()).collect();
846            let mut par_refs: Vec<&mut [u8]> =
847                parity.iter_mut().map(|s| s.as_mut_slice()).collect();
848            code.encode(&data_refs, &mut par_refs).expect("encode");
849        }
850        let n = k + r;
851        // All shards present.
852        let all: Vec<Vec<u8>> = data.iter().chain(parity.iter()).cloned().collect();
853        // Every subset of lost positions of size 1..=r.
854        for lost_count in 1..=r {
855            // Iterate all combinations via bit masks of n bits with
856            // exactly `lost_count` bits set.
857            for mask in 0u32..(1 << n) {
858                if (mask.count_ones() as usize) != lost_count {
859                    continue;
860                }
861                let mut shards: Vec<Option<Vec<u8>>> = all
862                    .iter()
863                    .enumerate()
864                    .map(|(i, s)| {
865                        if mask & (1 << i) != 0 {
866                            None
867                        } else {
868                            Some(s.clone())
869                        }
870                    })
871                    .collect();
872                code.decode(&mut shards).expect("decode");
873                for i in 0..k {
874                    assert_eq!(
875                        shards[i].as_ref().unwrap(),
876                        &data[i],
877                        "k={k} r={r} mask={mask:b}: data shard {i} mismatch"
878                    );
879                }
880            }
881        }
882    }
883
884    #[test]
885    fn recovery_k4_r2() {
886        exhaustive_recovery(4, 2, 32);
887    }
888
889    #[test]
890    fn recovery_k6_r3() {
891        exhaustive_recovery(6, 3, 16);
892    }
893
894    #[test]
895    fn recovery_k8_r4() {
896        exhaustive_recovery(8, 4, 8);
897    }
898
899    #[test]
900    fn recovery_k1_r1() {
901        exhaustive_recovery(1, 1, 64);
902    }
903
904    #[test]
905    fn recovery_k16_r16_high_parity() {
906        // r=16 (k+r=32, the per-block bitmap max) is far past the exhaustive
907        // tests' r<=4, and 2^32 masks cannot be enumerated. Spot-check the worst
908        // cases (all 16 data shards dropped -> reconstruct entirely from parity;
909        // all parity dropped) plus a deterministic spread of 16-of-32 erasures,
910        // confirming the Cauchy decode is sound at the high parity the lifted
911        // r_max allows.
912        let (k, r, len) = (16usize, 16usize, 64usize);
913        let code = RsCode::new(k, r).expect("code");
914        let data: Vec<Vec<u8>> = (0..k)
915            .map(|i| (0..len).map(|b| ((i * 131 + b * 17 + 7) & 0xFF) as u8).collect())
916            .collect();
917        let mut parity: Vec<Vec<u8>> = vec![vec![0u8; len]; r];
918        {
919            let dr: Vec<&[u8]> = data.iter().map(|s| s.as_slice()).collect();
920            let mut pr: Vec<&mut [u8]> = parity.iter_mut().map(|s| s.as_mut_slice()).collect();
921            code.encode(&dr, &mut pr).expect("encode");
922        }
923        let all: Vec<Vec<u8>> = data.iter().chain(parity.iter()).cloned().collect();
924        let n = (k + r) as u32;
925        // n == 32 makes `1 << n` overflow u32; build the full n-bit mask safely.
926        let full: u32 = if n >= 32 { u32::MAX } else { (1u32 << n) - 1 };
927        let mut masks: Vec<u32> = vec![
928            (1u32 << k) - 1,                  // all data shards dropped
929            full & !((1u32 << k) - 1),        // all parity shards dropped
930            0x5555_5555 & full,               // every other shard
931        ];
932        let mut st: u32 = 0x1234_5678;
933        while masks.len() < 30 {
934            let (mut m, mut cnt) = (0u32, 0);
935            while cnt < 16 {
936                st = st.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
937                let bit = (st >> 16) % n;
938                if m & (1 << bit) == 0 {
939                    m |= 1 << bit;
940                    cnt += 1;
941                }
942            }
943            masks.push(m);
944        }
945        for mask in masks {
946            assert_eq!(mask.count_ones(), 16, "mask must drop exactly r=16");
947            let mut shards: Vec<Option<Vec<u8>>> = all
948                .iter()
949                .enumerate()
950                .map(|(i, s)| if mask & (1 << i) != 0 { None } else { Some(s.clone()) })
951                .collect();
952            code.decode(&mut shards).expect("decode r=16");
953            for i in 0..k {
954                assert_eq!(shards[i].as_ref().unwrap(), &data[i], "mask={mask:b}: data {i} mismatch");
955            }
956        }
957    }
958
959    #[test]
960    fn too_few_shards_is_reported() {
961        let code = RsCode::new(4, 2).expect("code");
962        let len = 16;
963        let data: Vec<Vec<u8>> = (0..4).map(|_| vec![1u8; len]).collect();
964        let mut parity: Vec<Vec<u8>> = vec![vec![0u8; len]; 2];
965        {
966            let dr: Vec<&[u8]> = data.iter().map(|s| s.as_slice()).collect();
967            let mut pr: Vec<&mut [u8]> = parity.iter_mut().map(|s| s.as_mut_slice()).collect();
968            code.encode(&dr, &mut pr).expect("encode");
969        }
970        // Drop 3 of 6 (more than r=2): unrecoverable by FEC.
971        let mut shards: Vec<Option<Vec<u8>>> = vec![
972            None,
973            None,
974            None,
975            Some(data[3].clone()),
976            Some(parity[0].clone()),
977            Some(parity[1].clone()),
978        ];
979        assert_eq!(code.decode(&mut shards), Err(FecError::TooFewShards));
980    }
981
982    #[test]
983    fn gfni_affine_matrix_matches_field_multiply() {
984        // Multiply-by-1 is the GF2P8AFFINEQB identity matrix - the anchor that
985        // pins the byte/bit convention to the hardware spec.
986        assert_eq!(
987            gf_affine_matrix(1),
988            0x0102_0408_1020_4080,
989            "multiply-by-1 must be the GF2P8AFFINEQB identity matrix"
990        );
991        // For every coefficient and byte, the software affine transform fed the
992        // per-coefficient matrix equals the field multiply. Since the hardware
993        // GFNI instruction implements the same spec with the same matrix, this
994        // validates the GFNI path's correctness without the silicon.
995        for coef in 0u16..=255 {
996            let m = gf_affine_matrix(coef as u8);
997            for x in 0u16..=255 {
998                assert_eq!(
999                    gf2p8affine_byte(x as u8, m),
1000                    gf::mul(coef as u8, x as u8),
1001                    "affine(coef={coef}, x={x}) != gf::mul"
1002                );
1003            }
1004        }
1005    }
1006
1007    #[test]
1008    fn all_available_backends_match_scalar() {
1009        // Every GF backend this host can run must produce byte-identical output
1010        // to the scalar reference - the fallback-chain correctness contract.
1011        // On a GFNI / AVX-512 host this also validates the hardware rungs; here
1012        // it validates scalar, the affine emulation, SSSE3, and AVX2.
1013        use GfBackend::*;
1014        let candidates = [Scalar, AffineScalar, Ssse3, Avx2, Avx512Pshufb, Gfni256, Gfni512, Neon];
1015        let n = 1000usize;
1016        let src: Vec<u8> = (0..n).map(|i| ((i * 73 + 11) & 0xff) as u8).collect();
1017        let init: Vec<u8> = (0..n).map(|i| ((i * 31 + 7) & 0xff) as u8).collect();
1018        for coef in [2u8, 7, 100, 255] {
1019            let mut want = init.clone();
1020            gf_mul_add_backend(Scalar, &mut want, &src, coef);
1021            for &b in &candidates {
1022                if !b.available() {
1023                    continue;
1024                }
1025                let mut got = init.clone();
1026                gf_mul_add_backend(b, &mut got, &src, coef);
1027                assert_eq!(got, want, "backend {} disagrees at coef {coef}", b.name());
1028            }
1029        }
1030    }
1031
1032    #[cfg(target_arch = "aarch64")]
1033    #[test]
1034    fn neon_backend_matches_scalar_and_recovers() {
1035        // NEON is baseline on every aarch64 CPU, so the rung must be available
1036        // and auto-selected here.
1037        assert!(GfBackend::Neon.available(), "NEON must be available on aarch64");
1038        assert_eq!(detect_best_backend(), GfBackend::Neon, "aarch64 must pick NEON");
1039        // Bit-exact vs scalar across coefficients and a length that exercises
1040        // both the 16-byte NEON body and the scalar tail.
1041        let n = 1000usize;
1042        let src: Vec<u8> = (0..n).map(|i| ((i * 73 + 11) & 0xff) as u8).collect();
1043        let init: Vec<u8> = (0..n).map(|i| ((i * 31 + 7) & 0xff) as u8).collect();
1044        for coef in [2u8, 7, 100, 255] {
1045            let mut want = init.clone();
1046            gf_mul_add_backend(GfBackend::Scalar, &mut want, &src, coef);
1047            let mut got = init.clone();
1048            gf_mul_add_backend(GfBackend::Neon, &mut got, &src, coef);
1049            assert_eq!(got, want, "NEON disagrees with scalar at coef {coef}");
1050        }
1051        // Full RS encode/decode-with-loss recovery pinned to the NEON backend.
1052        exhaustive_recovery_backend(4, 2, 32, Some(GfBackend::Neon));
1053        exhaustive_recovery_backend(6, 3, 16, Some(GfBackend::Neon));
1054        exhaustive_recovery_backend(8, 4, 8, Some(GfBackend::Neon));
1055    }
1056
1057    #[test]
1058    fn exhaustive_recovery_via_gfni_emulation() {
1059        // Run the whole encode/decode path through the GFNI software emulation
1060        // (pinned per-instance, not via any global), so RS recovery is
1061        // validated end-to-end through the affine transform - the GFNI logic
1062        // proven without the silicon. Because the backend is on the code
1063        // instance, this is safe under the parallel test runner.
1064        exhaustive_recovery_backend(4, 2, 32, Some(GfBackend::AffineScalar));
1065        exhaustive_recovery_backend(6, 3, 16, Some(GfBackend::AffineScalar));
1066        exhaustive_recovery_backend(8, 4, 8, Some(GfBackend::AffineScalar));
1067    }
1068}