Skip to main content

rlnc_simdx/
matrix.rs

1//! GF(2⁸) matrix — row-major layout, 64-byte aligned backing store.
2//!
3//! The backing store is an [`AlignedBuffer`] with each row padded to a
4//! 64-byte-multiple stride.  This guarantees that every row pointer passed
5//! to `kernel::axpy` / `kernel::scale` is 64-byte aligned, so the SIMD
6//! kernels always take the aligned fast path.
7//!
8//! Example: `symbol_size` = 100 bytes → padded stride = 128 bytes.
9//!           Row 0 starts at offset 0   (aligned).
10//!           Row 1 starts at offset 128 (aligned).
11
12#[cfg(feature = "alloc")]
13extern crate alloc;
14
15use crate::aligned::{AlignedBuffer, ALIGN};
16use crate::field::tables::{EXP, LOG};
17use crate::kernel;
18
19/// Row-major GF(2⁸) matrix backed by a 64-byte aligned `AlignedBuffer`.
20///
21/// Each row occupies `stride` bytes where `stride` is `cols` rounded up to
22/// the next multiple of `ALIGN` (64).  Padding bytes at the end of each row
23/// are always zero and are never used in arithmetic.
24#[cfg(feature = "alloc")]
25pub struct GfMatrix {
26    pub(crate) rows: usize,
27    pub(crate) cols: usize,
28    /// Padded row stride (cols rounded up to 64-byte multiple).
29    pub(crate) stride: usize,
30    /// Flat backing buffer, length = rows * stride, 64-byte aligned.
31    pub(crate) data: AlignedBuffer,
32}
33
34#[cfg(feature = "alloc")]
35impl GfMatrix {
36    /// Compute the padded row stride for `cols` data columns.
37    #[inline]
38    pub fn padded_stride(cols: usize) -> usize {
39        cols.checked_add(ALIGN - 1)
40            .map(|value| value & !(ALIGN - 1))
41            .expect("GfMatrix stride overflow")
42    }
43
44    /// Allocate a zero matrix of size `rows × cols` with aligned rows.
45    pub fn zeros(rows: usize, cols: usize) -> Self {
46        let stride = Self::padded_stride(cols);
47        GfMatrix {
48            rows,
49            cols,
50            stride,
51            data: AlignedBuffer::zeroed(rows.checked_mul(stride).expect("GfMatrix size overflow")),
52        }
53    }
54
55    /// Allocate a `k × k` identity matrix with aligned rows.
56    pub fn identity(k: usize) -> Self {
57        let mut m = Self::zeros(k, k);
58        for i in 0..k {
59            m.data.as_mut_slice()[i * m.stride + i] = 1;
60        }
61        m
62    }
63
64    /// Number of data columns (not including stride padding).
65    pub fn rows(&self) -> usize {
66        self.rows
67    }
68    /// Number of data rows.
69    pub fn cols(&self) -> usize {
70        self.cols
71    }
72    /// Padded row stride in bytes.
73    pub fn stride(&self) -> usize {
74        self.stride
75    }
76
77    /// Raw access to row `r` as a data slice (length = `cols`, 64-byte aligned).
78    #[inline]
79    pub fn row(&self, r: usize) -> &[u8] {
80        let off = r * self.stride;
81        &self.data.as_slice()[off..off + self.cols]
82    }
83
84    /// Mutable access to row `r` (64-byte aligned).
85    #[inline]
86    pub fn row_mut(&mut self, r: usize) -> &mut [u8] {
87        let off = r * self.stride;
88        let cols = self.cols;
89        &mut self.data.as_mut_slice()[off..off + cols]
90    }
91
92    /// `dst_row[i] ^= c * src_row[i]` — both rows are 64-byte aligned,
93    /// so the SIMD kernel always takes the aligned fast path.
94    ///
95    /// # Panics
96    /// Panics if `dst == src` (would create overlapping views).
97    pub fn row_axpy(&mut self, dst: usize, c: u8, src: usize) {
98        self.row_axpy_from(dst, c, src, 0);
99    }
100
101    fn row_axpy_from(&mut self, dst: usize, c: u8, src: usize, start: usize) {
102        assert_ne!(dst, src, "GfMatrix::row_axpy: dst and src must differ");
103        let stride = self.stride;
104        let cols = self.cols;
105        let (dst_off, src_off) = (dst * stride, src * stride);
106
107        // Split borrow: get disjoint mutable/immutable references
108        let (src_slice, dst_slice) = if dst > src {
109            let (lo, hi) = self.data.as_mut_slice().split_at_mut(dst_off);
110            (&lo[src_off + start..src_off + cols], &mut hi[start..cols])
111        } else {
112            let (lo, hi) = self.data.as_mut_slice().split_at_mut(src_off);
113            (&hi[start..cols], &mut lo[dst_off + start..dst_off + cols])
114        };
115
116        // SAFETY: split_at_mut created disjoint rows of equal length.
117        unsafe { kernel::axpy_unchecked(c, src_slice, dst_slice) };
118    }
119
120    /// Scale row `r` by scalar `c` using SIMD [`kernel::scale_inplace`].
121    pub fn row_scale(&mut self, r: usize, c: u8) {
122        self.row_scale_from(r, c, 0);
123    }
124
125    fn row_scale_from(&mut self, r: usize, c: u8, start: usize) {
126        let stride = self.stride;
127        let cols = self.cols;
128        let off = r * stride;
129        kernel::scale_inplace(c, &mut self.data.as_mut_slice()[off + start..off + cols]);
130    }
131
132    /// Swap rows `a` and `b`.
133    pub fn swap_rows(&mut self, a: usize, b: usize) {
134        if a == b {
135            return;
136        }
137        let stride = self.stride;
138        let (a_off, b_off) = (a * stride, b * stride);
139        let data = self.data.as_mut_slice();
140        if a < b {
141            let (lo, hi) = data.split_at_mut(b_off);
142            lo[a_off..a_off + stride].swap_with_slice(&mut hi[..stride]);
143        } else {
144            let (lo, hi) = data.split_at_mut(a_off);
145            lo[b_off..b_off + stride].swap_with_slice(&mut hi[..stride]);
146        }
147    }
148
149    /// In-place Gaussian elimination (full RREF).  Returns the rank.
150    pub fn gaussian_elimination(&mut self) -> usize {
151        let rows = self.rows;
152        let cols = self.cols;
153        let mut pivot_row = 0usize;
154
155        for col in 0..cols {
156            let found =
157                (pivot_row..rows).find(|&r| self.data.as_slice()[r * self.stride + col] != 0);
158            let Some(r) = found else { continue };
159
160            if r != pivot_row {
161                self.swap_rows(r, pivot_row);
162            }
163
164            let pivot_val = self.data.as_slice()[pivot_row * self.stride + col];
165            let operation_start = col & !(ALIGN - 1);
166            if pivot_val != 1 {
167                let inv = EXP[255 - LOG[pivot_val as usize] as usize];
168                self.row_scale_from(pivot_row, inv, operation_start);
169            }
170
171            for r2 in 0..rows {
172                if r2 == pivot_row {
173                    continue;
174                }
175                let coeff = self.data.as_slice()[r2 * self.stride + col];
176                if coeff != 0 {
177                    self.row_axpy_from(r2, coeff, pivot_row, operation_start);
178                }
179            }
180
181            pivot_row += 1;
182        }
183
184        pivot_row
185    }
186
187    /// True if no row is all-zero (quick non-full-rank check).
188    pub fn is_full_rank(&self) -> bool {
189        for r in 0..self.rows {
190            let off = r * self.stride;
191            if self.data.as_slice()[off..off + self.cols]
192                .iter()
193                .all(|&b| b == 0)
194            {
195                return false;
196            }
197        }
198        true
199    }
200}
201
202#[cfg(test)]
203#[cfg(feature = "alloc")]
204mod tests {
205    use super::*;
206
207    #[test]
208    fn rows_are_aligned() {
209        let m = GfMatrix::zeros(8, 100);
210        for r in 0..8 {
211            let ptr = m.row(r).as_ptr() as usize;
212            assert_eq!(ptr % ALIGN, 0, "row {r} not {ALIGN}-byte aligned");
213        }
214    }
215
216    #[test]
217    #[should_panic(expected = "GfMatrix stride overflow")]
218    fn oversized_columns_panic_before_allocation() {
219        let _ = GfMatrix::zeros(1, usize::MAX);
220    }
221
222    #[test]
223    fn identity_rref_unchanged() {
224        let mut m = GfMatrix::identity(4);
225        let rank = m.gaussian_elimination();
226        assert_eq!(rank, 4);
227        let expected = GfMatrix::identity(4);
228        for i in 0..4 {
229            assert_eq!(m.row(i), expected.row(i));
230        }
231    }
232
233    #[test]
234    fn rank_of_zero_matrix() {
235        let mut m = GfMatrix::zeros(3, 3);
236        let rank = m.gaussian_elimination();
237        assert_eq!(rank, 0);
238    }
239
240    #[test]
241    fn rank_2x2_invertible() {
242        let mut m = GfMatrix::zeros(2, 2);
243        m.data.as_mut_slice()[0] = 1;
244        m.data.as_mut_slice()[1] = 2;
245        m.data.as_mut_slice()[m.stride] = 3;
246        m.data.as_mut_slice()[m.stride + 1] = 4;
247        let rank = m.gaussian_elimination();
248        assert_eq!(rank, 2);
249        assert_eq!(m.row(0)[0], 1);
250        assert_eq!(m.row(0)[1], 0);
251        assert_eq!(m.row(1)[0], 0);
252        assert_eq!(m.row(1)[1], 1);
253    }
254
255    #[test]
256    fn axpy_row_operation() {
257        use crate::kernel::scalar::gf_mul;
258
259        let mut m = GfMatrix::zeros(2, 4);
260        // row0 = [1, 0, 0, 0], row1 = [0, 1, 0, 0]
261        m.data.as_mut_slice()[0] = 1;
262        m.data.as_mut_slice()[m.stride + 1] = 1;
263        m.row_axpy(1, 0x03, 0);
264        assert_eq!(m.row(1)[0], gf_mul(0x03, 1));
265        assert_eq!(m.row(1)[1], 1);
266        assert_eq!(m.row(1)[2], 0);
267        assert_eq!(m.row(1)[3], 0);
268    }
269
270    #[test]
271    #[should_panic(expected = "dst and src must differ")]
272    fn row_axpy_panics_on_same_row() {
273        let mut m = GfMatrix::zeros(2, 4);
274        m.row_axpy(0, 0x03, 0);
275    }
276
277    #[test]
278    fn row_scale_matches_kernel() {
279        let mut m = GfMatrix::zeros(1, 8);
280        for i in 0..8 {
281            m.data.as_mut_slice()[i] = (i as u8).wrapping_add(1);
282        }
283        let before = m.row(0).to_vec();
284        m.row_scale(0, 0x53);
285        let mut expected = before.clone();
286        crate::kernel::scale_inplace(0x53, &mut expected);
287        assert_eq!(m.row(0), expected.as_slice());
288    }
289}