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 + ALIGN - 1) & !(ALIGN - 1)
40    }
41
42    /// Allocate a zero matrix of size `rows × cols` with aligned rows.
43    pub fn zeros(rows: usize, cols: usize) -> Self {
44        let stride = Self::padded_stride(cols);
45        GfMatrix {
46            rows,
47            cols,
48            stride,
49            data: AlignedBuffer::zeroed(rows * stride),
50        }
51    }
52
53    /// Allocate a `k × k` identity matrix with aligned rows.
54    pub fn identity(k: usize) -> Self {
55        let mut m = Self::zeros(k, k);
56        for i in 0..k {
57            m.data.as_mut_slice()[i * m.stride + i] = 1;
58        }
59        m
60    }
61
62    /// Number of data columns (not including stride padding).
63    pub fn rows(&self) -> usize {
64        self.rows
65    }
66    /// Number of data rows.
67    pub fn cols(&self) -> usize {
68        self.cols
69    }
70    /// Padded row stride in bytes.
71    pub fn stride(&self) -> usize {
72        self.stride
73    }
74
75    /// Raw access to row `r` as a data slice (length = `cols`, 64-byte aligned).
76    #[inline]
77    pub fn row(&self, r: usize) -> &[u8] {
78        let off = r * self.stride;
79        &self.data.as_slice()[off..off + self.cols]
80    }
81
82    /// Mutable access to row `r` (64-byte aligned).
83    #[inline]
84    pub fn row_mut(&mut self, r: usize) -> &mut [u8] {
85        let off = r * self.stride;
86        let cols = self.cols;
87        &mut self.data.as_mut_slice()[off..off + cols]
88    }
89
90    /// `dst_row[i] ^= c * src_row[i]` — both rows are 64-byte aligned,
91    /// so the SIMD kernel always takes the aligned fast path.
92    ///
93    /// # Panics
94    /// Panics if `dst == src` (would create overlapping views).
95    pub fn row_axpy(&mut self, dst: usize, c: u8, src: usize) {
96        assert_ne!(dst, src, "GfMatrix::row_axpy: dst and src must differ");
97        let stride = self.stride;
98        let cols = self.cols;
99        let (dst_off, src_off) = (dst * stride, src * stride);
100
101        // Split borrow: get disjoint mutable/immutable references
102        let (src_slice, dst_slice) = if dst > src {
103            let (lo, hi) = self.data.as_mut_slice().split_at_mut(dst_off);
104            (&lo[src_off..src_off + cols], &mut hi[..cols])
105        } else {
106            let (lo, hi) = self.data.as_mut_slice().split_at_mut(src_off);
107            (&hi[..cols], &mut lo[dst_off..dst_off + cols])
108        };
109
110        kernel::axpy(c, src_slice, dst_slice);
111    }
112
113    /// Scale row `r` by scalar `c` using SIMD [`kernel::scale_inplace`].
114    pub fn row_scale(&mut self, r: usize, c: u8) {
115        let stride = self.stride;
116        let cols = self.cols;
117        let off = r * stride;
118        kernel::scale_inplace(c, &mut self.data.as_mut_slice()[off..off + cols]);
119    }
120
121    /// Swap rows `a` and `b`.
122    pub fn swap_rows(&mut self, a: usize, b: usize) {
123        if a == b {
124            return;
125        }
126        let stride = self.stride;
127        let (a_off, b_off) = (a * stride, b * stride);
128        let data = self.data.as_mut_slice();
129        if a < b {
130            let (lo, hi) = data.split_at_mut(b_off);
131            lo[a_off..a_off + stride].swap_with_slice(&mut hi[..stride]);
132        } else {
133            let (lo, hi) = data.split_at_mut(a_off);
134            lo[b_off..b_off + stride].swap_with_slice(&mut hi[..stride]);
135        }
136    }
137
138    /// In-place Gaussian elimination (full RREF).  Returns the rank.
139    pub fn gaussian_elimination(&mut self) -> usize {
140        let rows = self.rows;
141        let cols = self.cols;
142        let mut pivot_row = 0usize;
143
144        for col in 0..cols {
145            let found =
146                (pivot_row..rows).find(|&r| self.data.as_slice()[r * self.stride + col] != 0);
147            let Some(r) = found else { continue };
148
149            if r != pivot_row {
150                self.swap_rows(r, pivot_row);
151            }
152
153            let pivot_val = self.data.as_slice()[pivot_row * self.stride + col];
154            if pivot_val != 1 {
155                let inv = EXP[255 - LOG[pivot_val as usize] as usize];
156                self.row_scale(pivot_row, inv);
157            }
158
159            for r2 in 0..rows {
160                if r2 == pivot_row {
161                    continue;
162                }
163                let coeff = self.data.as_slice()[r2 * self.stride + col];
164                if coeff != 0 {
165                    self.row_axpy(r2, coeff, pivot_row);
166                }
167            }
168
169            pivot_row += 1;
170        }
171
172        pivot_row
173    }
174
175    /// True if no row is all-zero (quick non-full-rank check).
176    pub fn is_full_rank(&self) -> bool {
177        for r in 0..self.rows {
178            let off = r * self.stride;
179            if self.data.as_slice()[off..off + self.cols]
180                .iter()
181                .all(|&b| b == 0)
182            {
183                return false;
184            }
185        }
186        true
187    }
188}
189
190#[cfg(test)]
191#[cfg(feature = "alloc")]
192mod tests {
193    use super::*;
194
195    #[test]
196    fn rows_are_aligned() {
197        let m = GfMatrix::zeros(8, 100);
198        for r in 0..8 {
199            let ptr = m.row(r).as_ptr() as usize;
200            assert_eq!(ptr % ALIGN, 0, "row {r} not {ALIGN}-byte aligned");
201        }
202    }
203
204    #[test]
205    fn identity_rref_unchanged() {
206        let mut m = GfMatrix::identity(4);
207        let rank = m.gaussian_elimination();
208        assert_eq!(rank, 4);
209        let expected = GfMatrix::identity(4);
210        for i in 0..4 {
211            assert_eq!(m.row(i), expected.row(i));
212        }
213    }
214
215    #[test]
216    fn rank_of_zero_matrix() {
217        let mut m = GfMatrix::zeros(3, 3);
218        let rank = m.gaussian_elimination();
219        assert_eq!(rank, 0);
220    }
221
222    #[test]
223    fn rank_2x2_invertible() {
224        let mut m = GfMatrix::zeros(2, 2);
225        m.data.as_mut_slice()[0] = 1;
226        m.data.as_mut_slice()[1] = 2;
227        m.data.as_mut_slice()[m.stride] = 3;
228        m.data.as_mut_slice()[m.stride + 1] = 4;
229        let rank = m.gaussian_elimination();
230        assert_eq!(rank, 2);
231        assert_eq!(m.row(0)[0], 1);
232        assert_eq!(m.row(0)[1], 0);
233        assert_eq!(m.row(1)[0], 0);
234        assert_eq!(m.row(1)[1], 1);
235    }
236
237    #[test]
238    fn axpy_row_operation() {
239        use crate::kernel::scalar::gf_mul;
240
241        let mut m = GfMatrix::zeros(2, 4);
242        // row0 = [1, 0, 0, 0], row1 = [0, 1, 0, 0]
243        m.data.as_mut_slice()[0] = 1;
244        m.data.as_mut_slice()[m.stride + 1] = 1;
245        m.row_axpy(1, 0x03, 0);
246        assert_eq!(m.row(1)[0], gf_mul(0x03, 1));
247        assert_eq!(m.row(1)[1], 1);
248        assert_eq!(m.row(1)[2], 0);
249        assert_eq!(m.row(1)[3], 0);
250    }
251
252    #[test]
253    #[should_panic(expected = "dst and src must differ")]
254    fn row_axpy_panics_on_same_row() {
255        let mut m = GfMatrix::zeros(2, 4);
256        m.row_axpy(0, 0x03, 0);
257    }
258
259    #[test]
260    fn row_scale_matches_kernel() {
261        let mut m = GfMatrix::zeros(1, 8);
262        for i in 0..8 {
263            m.data.as_mut_slice()[i] = (i as u8).wrapping_add(1);
264        }
265        let before = m.row(0).to_vec();
266        m.row_scale(0, 0x53);
267        let mut expected = before.clone();
268        crate::kernel::scale_inplace(0x53, &mut expected);
269        assert_eq!(m.row(0), expected.as_slice());
270    }
271}