Skip to main content

rusty_erasure_core/
kernel.rs

1//! Scalar erasure-coding kernels — the permanent oracles every SIMD twin (M4)
2//! is gated against, and the fallback on every CPU.
3//!
4//! All kernels consume ISA-L's expanded 32-byte nibble tables
5//! (`tables::init_tables` / `tables::mul_table32`), exactly as the vector
6//! kernels will: per byte, `c*x = tbl[x & 0xf] ^ tbl[16 + (x >> 4)]`. Length
7//! agreement is validated — a mismatch is a typed error, never a silent
8//! truncation and never a panic.
9
10use core::sync::atomic::{AtomicU64, Ordering};
11
12use crate::error::CodeError;
13use crate::tables::{TABLE_BYTES, table_mul};
14
15/// Census counter for the scalar kernel set: source bytes processed. The
16/// reach census (mission plan §7.1) is always on — one relaxed add per call,
17/// never per element — so "which kernels does production actually run" is a
18/// measured fact, not an assumption.
19pub static SCALAR_CENSUS_BYTES: AtomicU64 = AtomicU64::new(0);
20
21/// Full-encode kernel: `out[l] = XOR_j (c[l][j] · data[j])` for every row.
22pub type EncodeFn = fn(gftbls: &[u8], data: &[&[u8]], out: &mut [&mut [u8]]);
23
24/// Fused-update kernel: fold source `vec_i` into every output row in one pass.
25pub type UpdateFn = fn(gftbls: &[u8], k: usize, vec_i: usize, src: &[u8], outs: &mut [&mut [u8]]);
26
27/// A pluggable kernel set. The choice is made ONCE, at coder construction or
28/// the facade surface (never inside a loop — the dispatch-placement law), and
29/// core stays free of all detection machinery: this crate only ever provides
30/// [`Kernels::scalar`]; SIMD sets come from `rusty_erasure-accel` via the
31/// facade.
32///
33/// Contract for implementations: the CALLER has validated everything —
34/// `data` slices all equal length, `out` slices all equal that length,
35/// `gftbls.len() == out.len() * data.len() * 32`. Implementations must be
36/// exact (byte-identical to the scalar set) and must add the source bytes
37/// they process to their census counter.
38#[derive(Clone, Copy, Debug)]
39pub struct Kernels {
40    /// Expand a row-major coefficient block into THIS set's table format.
41    /// **Table formats are kernel-private** (nibble 32 B/coeff for the
42    /// scalar/PSHUFB sets, affine 8 B/coeff for GFNI): tables built by one
43    /// set's `init` are meaningful only to that set's `encode`/`mad` — mixing
44    /// formats produces wrong parity at plausible speed (learned from ISA-L's
45    /// own dispatched-init-vs-avx2-encode mismatch, LEDGER M4).
46    pub init: fn(coeffs: &[u8]) -> alloc::vec::Vec<u8>,
47    /// Bytes per expanded coefficient in this set's format.
48    pub table_bytes: usize,
49    /// Full encode: `out[l] = XOR_j (c[l][j] · data[j])` for every output row,
50    /// `gftbls` row-major from this set's `init`.
51    pub encode: EncodeFn,
52    /// `dest ^= c · src` for one expanded table (`table_bytes` long, from
53    /// this set's `init`).
54    pub mad: fn(tbl: &[u8], src: &[u8], dest: &mut [u8]),
55    /// Fused incremental update: fold source `vec_i` into EVERY output row in
56    /// one pass over the source (`outs[l] ^= c[l][vec_i] · src`), tables for
57    /// row `l` at `(l*k + vec_i) * table_bytes` in `gftbls`. One source read
58    /// instead of `outs.len()` — the brick that fixed S7's shape.
59    pub update: UpdateFn,
60    /// Kernel-set name, for reporting.
61    pub name: &'static str,
62    /// The census counter this set accumulates into.
63    pub census: &'static AtomicU64,
64}
65
66impl Kernels {
67    /// The scalar set — the permanent oracle and the fallback on every CPU.
68    pub const fn scalar() -> Self {
69        Self {
70            init: crate::tables::init_tables,
71            table_bytes: TABLE_BYTES,
72            encode: scalar_encode,
73            mad: scalar_mad,
74            update: scalar_update,
75            name: "scalar",
76            census: &SCALAR_CENSUS_BYTES,
77        }
78    }
79}
80
81fn scalar_update(gftbls: &[u8], k: usize, vec_i: usize, src: &[u8], outs: &mut [&mut [u8]]) {
82    SCALAR_CENSUS_BYTES.fetch_add(src.len() as u64, Ordering::Relaxed);
83    // Fused: one walk of the source updates every row (p cache streams).
84    let tbls: alloc::vec::Vec<&[u8; TABLE_BYTES]> = (0..outs.len())
85        .map(|l| {
86            let start = (l * k + vec_i) * TABLE_BYTES;
87            gftbls[start..start + TABLE_BYTES]
88                .try_into()
89                .expect("caller-validated tables")
90        })
91        .collect();
92    for (i, &s) in src.iter().enumerate() {
93        for (out, tbl) in outs.iter_mut().zip(&tbls) {
94            out[i] ^= table_mul(tbl, s);
95        }
96    }
97}
98
99fn scalar_encode(gftbls: &[u8], data: &[&[u8]], out: &mut [&mut [u8]]) {
100    let k = data.len();
101    let len = out.first().map_or(0, |b| b.len());
102    SCALAR_CENSUS_BYTES.fetch_add((k * len) as u64, Ordering::Relaxed);
103    for (l, dest) in out.iter_mut().enumerate() {
104        dest.fill(0);
105        for (j, src) in data.iter().enumerate() {
106            let start = (l * k + j) * TABLE_BYTES;
107            let tbl: &[u8; TABLE_BYTES] = gftbls[start..start + TABLE_BYTES]
108                .try_into()
109                .expect("caller-validated tables");
110            for (d, &s) in dest.iter_mut().zip(*src) {
111                *d ^= table_mul(tbl, s);
112            }
113        }
114    }
115}
116
117fn scalar_mad(tbl: &[u8], src: &[u8], dest: &mut [u8]) {
118    let tbl: &[u8; TABLE_BYTES] = tbl.try_into().expect("scalar mad takes a 32-byte table");
119    SCALAR_CENSUS_BYTES.fetch_add(src.len() as u64, Ordering::Relaxed);
120    for (d, &s) in dest.iter_mut().zip(src) {
121        *d ^= table_mul(tbl, s);
122    }
123}
124
125fn tbl32(gftbls: &[u8], index: usize) -> Result<&[u8; TABLE_BYTES], CodeError> {
126    let start = index * TABLE_BYTES;
127    let slice = gftbls
128        .get(start..start + TABLE_BYTES)
129        .ok_or(CodeError::ShardCount {
130            expected: index + 1,
131            got: gftbls.len() / TABLE_BYTES,
132        })?;
133    // Infallible: the slice is exactly TABLE_BYTES long.
134    Ok(slice.try_into().expect("length checked above"))
135}
136
137/// `dest = c · src` where `tbl` is `c`'s expanded 32-byte table
138/// (ISA-L's `gf_vect_mul`, without its len-multiple-of-32 restriction).
139pub fn vect_mul(dest: &mut [u8], tbl: &[u8; TABLE_BYTES], src: &[u8]) -> Result<(), CodeError> {
140    if dest.len() != src.len() {
141        return Err(CodeError::ShardLength {
142            index: 0,
143            expected: dest.len(),
144            got: src.len(),
145        });
146    }
147    for (d, &s) in dest.iter_mut().zip(src) {
148        *d = table_mul(tbl, s);
149    }
150    Ok(())
151}
152
153/// `dest ^= c · src` — multiply-and-add, the incremental-update primitive
154/// (ISA-L's `gf_vect_mad`).
155pub fn vect_mad(dest: &mut [u8], tbl: &[u8; TABLE_BYTES], src: &[u8]) -> Result<(), CodeError> {
156    if dest.len() != src.len() {
157        return Err(CodeError::ShardLength {
158            index: 0,
159            expected: dest.len(),
160            got: src.len(),
161        });
162    }
163    for (d, &s) in dest.iter_mut().zip(src) {
164        *d ^= table_mul(tbl, s);
165    }
166    Ok(())
167}
168
169/// `dest[i] = XOR_j (c_j · srcs[j][i])` — the GF(2^8) dot product at the heart
170/// of encode (ISA-L's `gf_vect_dot_prod`). `gftbls` holds one 32-byte table
171/// per source, in source order.
172pub fn vect_dot_prod(dest: &mut [u8], gftbls: &[u8], srcs: &[&[u8]]) -> Result<(), CodeError> {
173    if gftbls.len() != srcs.len() * TABLE_BYTES {
174        return Err(CodeError::ShardCount {
175            expected: srcs.len(),
176            got: gftbls.len() / TABLE_BYTES,
177        });
178    }
179    for (index, src) in srcs.iter().enumerate() {
180        if src.len() != dest.len() {
181            return Err(CodeError::ShardLength {
182                index,
183                expected: dest.len(),
184                got: src.len(),
185            });
186        }
187    }
188    dest.fill(0);
189    for (j, src) in srcs.iter().enumerate() {
190        let tbl = tbl32(gftbls, j)?;
191        for (d, &s) in dest.iter_mut().zip(*src) {
192            *d ^= table_mul(tbl, s);
193        }
194    }
195    Ok(())
196}