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}