subetha_cxc/rlc_fec.rs
1//! Sliding-window Random Linear Code (RLC) forward erasure correction: a
2//! convolutional erasure code that interleaves repair symbols with the source
3//! symbols over a sliding window, so an isolated loss is recovered from the
4//! next repair without waiting for a block boundary.
5//!
6//! This is the convolutional counterpart of the block Cauchy Reed-Solomon code
7//! in [`crate::fec`]. A block code sends `k` source shards then `r` parity
8//! shards; to recover a loss the decoder must wait for the rest of the block,
9//! by which time the loss detector has often already fired a wasted retransmit.
10//! A sliding-window RLC, by contrast, emits one repair symbol every few source
11//! symbols, each a random linear combination of the source symbols currently in
12//! the window, so a single loss is recovered as soon as the next repair
13//! arrives - an RTT-independent, near-instant recovery that suits low-latency
14//! streams.
15//!
16//! The repair symbol over a window of source symbols `s_i` is
17//! `sum_i coef_i * s_i` over GF(2^8) with the field's `0x11D` polynomial - the
18//! *same* field as the block RS code, so the linear combination rides the
19//! GF(2^8) SIMD ladder ([`crate::fec::gf_mul_add_auto`]) with no new kernel.
20//! Each coefficient is drawn from a deterministic, seedable generator and is
21//! nonzero with probability `(DT + 1) / 16` (the density threshold `DT`), so
22//! the decoder reconstructs the exact coefficients from the repair's metadata
23//! (the repair key, the first source id, the window size, and `DT`).
24//!
25//! The decoder maintains a linear system over GF(2^8) of the received source
26//! and repair symbols and solves it by Gaussian elimination; a lost symbol is
27//! recovered the moment the received equations determine it. The heavy work -
28//! combining symbol-vectors during elimination - is `gf_mul_add` over the
29//! payload bytes (SIMD-accelerated); the small coefficient matrix (at most one
30//! column per source symbol in the window) uses scalar GF(2^8) arithmetic.
31
32use crate::fec::{gf, gf_mul_add_auto};
33use std::collections::{BTreeMap, BTreeSet, HashMap, VecDeque};
34
35/// Default density threshold: every coefficient nonzero (maximum density),
36/// which maximizes the recovery probability for small windows.
37pub const DEFAULT_DT: u8 = 15;
38
39/// Deterministic GF(2^8) coefficient for `source_id` under repair `repair_key`
40/// at density threshold `dt`. Nonzero with probability `(dt + 1) / 16`; the two
41/// independent draws (a density nibble and a value byte) come from a splitmix
42/// hash of `(repair_key, source_id)`, so encoder and decoder agree exactly.
43fn coef(repair_key: u32, source_id: u32, dt: u8) -> u8 {
44 // splitmix64 finaliser over the (key, id) pair.
45 let mut x = ((repair_key as u64) << 32) | source_id as u64;
46 x = (x ^ (x >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
47 x = (x ^ (x >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
48 x ^= x >> 31;
49 // Low nibble decides presence (uniform 0..15); next byte is the value.
50 if (x as u8 & 0x0f) <= dt {
51 let v = (x >> 8) as u8;
52 if v == 0 { 1 } else { v }
53 } else {
54 0
55 }
56}
57
58/// A repair symbol: a random linear combination of the source symbols in the
59/// sliding window at the moment it was generated. The metadata lets the decoder
60/// reconstruct the exact coefficients.
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct RepairSymbol {
63 /// Seed for the coefficient generator.
64 pub repair_key: u32,
65 /// Lowest source id in the covered window.
66 pub first_source_id: u32,
67 /// Number of source symbols in the covered window.
68 pub window_size: u16,
69 /// Density threshold (0..=15).
70 pub dt: u8,
71 /// `sum_i coef_i * source_i` over the window.
72 pub payload: Vec<u8>,
73}
74
75/// Sliding-window RLC encoder: holds the last `window_max` source symbols and
76/// emits one repair symbol every `step` source symbols.
77#[derive(Debug)]
78pub struct RlcEncoder {
79 window: VecDeque<(u32, Vec<u8>)>,
80 window_max: usize,
81 step: usize,
82 dt: u8,
83 symbol_len: usize,
84 next_source_id: u32,
85 since_last_repair: usize,
86 next_repair_key: u32,
87 /// Whether repairs are emitted at all. `false` is the disable-on-clean
88 /// state: source symbols still flow and the window is still maintained (so
89 /// re-arming is instant), but no repair rides the wire.
90 coding_on: bool,
91}
92
93impl RlcEncoder {
94 /// Build an encoder over `symbol_len`-byte symbols with a window of up to
95 /// `window_max` source symbols, emitting one repair every `step` source
96 /// symbols at density threshold `dt`. The code rate is `step / (step + 1)`.
97 pub fn new(window_max: usize, step: usize, dt: u8, symbol_len: usize) -> Self {
98 Self {
99 window: VecDeque::new(),
100 window_max: window_max.max(1),
101 step: step.max(1),
102 dt: dt.min(15),
103 symbol_len,
104 next_source_id: 0,
105 since_last_repair: 0,
106 next_repair_key: 0,
107 coding_on: true,
108 }
109 }
110
111 /// Retune the coding parameters at runtime (the adaptive control path): the
112 /// window size, the repair cadence `step` (code rate `step / (step + 1)`),
113 /// and the coefficient density `dt`. Shrinking the window trims the oldest
114 /// source symbols immediately so the next repair spans only the new window.
115 pub fn set_params(&mut self, window_max: usize, step: usize, dt: u8) {
116 self.window_max = window_max.max(1);
117 self.step = step.max(1);
118 self.dt = dt.min(15);
119 while self.window.len() > self.window_max {
120 self.window.pop_front();
121 }
122 }
123
124 /// Turn repair emission on or off (disable-on-clean). The window keeps
125 /// filling either way, so re-enabling protects the in-flight symbols at once.
126 pub fn set_coding(&mut self, on: bool) {
127 self.coding_on = on;
128 }
129
130 /// The live `(window_max, step, dt)` parameters (telemetry).
131 pub fn params(&self) -> (usize, usize, u8) {
132 (self.window_max, self.step, self.dt)
133 }
134
135 /// Whether repair emission is currently active.
136 pub fn coding_on(&self) -> bool {
137 self.coding_on
138 }
139
140 /// Add one source symbol. Returns its assigned source id and, every `step`
141 /// symbols (while coding is on), a repair symbol to interleave onto the wire
142 /// after it.
143 pub fn push_source(&mut self, payload: &[u8]) -> (u32, Option<RepairSymbol>) {
144 debug_assert_eq!(payload.len(), self.symbol_len);
145 let sid = self.next_source_id;
146 self.next_source_id = self.next_source_id.wrapping_add(1);
147 self.window.push_back((sid, payload.to_vec()));
148 while self.window.len() > self.window_max {
149 self.window.pop_front();
150 }
151 self.since_last_repair += 1;
152 let repair = if self.coding_on && self.since_last_repair >= self.step {
153 self.since_last_repair = 0;
154 Some(self.emit_repair())
155 } else {
156 None
157 };
158 (sid, repair)
159 }
160
161 /// Drop acknowledged-or-recovered source symbols below `floor` from the
162 /// window (the elastic-window feedback path); the window never protects
163 /// data the peer already has.
164 pub fn forget_below(&mut self, floor: u32) {
165 while let Some((sid, _)) = self.window.front() {
166 if *sid < floor {
167 self.window.pop_front();
168 } else {
169 break;
170 }
171 }
172 }
173
174 /// Re-base the source-id stream to `base` for a cross-code resync: the next
175 /// source symbol is assigned id `base` and the coding window starts empty, so
176 /// repairs reference only post-rebase symbols. Used when another code carried
177 /// the ids between this code's old running id and `base`, so it must resume at
178 /// `base` rather than its own (now-diverged) counter. The repair-key counter
179 /// keeps running (keys are matched to ids by coefficient, not by equality).
180 pub fn rebase_to(&mut self, base: u32) {
181 self.next_source_id = base;
182 self.window.clear();
183 self.since_last_repair = 0;
184 }
185
186 fn emit_repair(&mut self) -> RepairSymbol {
187 let repair_key = self.next_repair_key;
188 self.next_repair_key = self.next_repair_key.wrapping_add(1);
189 let first = self.window.front().map(|(id, _)| *id).unwrap_or(0);
190 let mut payload = vec![0u8; self.symbol_len];
191 for (sid, sym) in &self.window {
192 let c = coef(repair_key, *sid, self.dt);
193 if c != 0 {
194 gf_mul_add_auto(&mut payload, sym, c);
195 }
196 }
197 RepairSymbol {
198 repair_key,
199 first_source_id: first,
200 window_size: self.window.len() as u16,
201 dt: self.dt,
202 payload,
203 }
204 }
205
206 /// The next source id that will be assigned.
207 pub fn next_source_id(&self) -> u32 {
208 self.next_source_id
209 }
210}
211
212/// Sliding-window RLC decoder: stores received source and repair symbols and
213/// recovers lost source symbols by Gaussian elimination over GF(2^8).
214#[derive(Debug)]
215pub struct RlcDecoder {
216 symbol_len: usize,
217 source: BTreeMap<u32, Vec<u8>>,
218 repairs: Vec<RepairSymbol>,
219 /// Highest source id seen on any source or repair, for the recovery horizon.
220 highest: u32,
221 /// RLC solving is bounded to source ids within `horizon` of `highest`: a
222 /// sliding-window code can only recover within its window, so a gap older
223 /// than this is the ARQ floor's job, not RLC's. Bounding the solve keeps
224 /// the per-packet cost constant instead of growing with history.
225 horizon: u32,
226}
227
228impl RlcDecoder {
229 /// Build a decoder over `symbol_len`-byte symbols.
230 pub fn new(symbol_len: usize) -> Self {
231 Self {
232 symbol_len,
233 source: BTreeMap::new(),
234 repairs: Vec::new(),
235 highest: 0,
236 horizon: 1024,
237 }
238 }
239
240 /// Set the RLC recovery horizon (source ids back from the newest that the
241 /// solver considers). Should comfortably exceed the encoder's window so a
242 /// repair's whole window is in scope; gaps older than this fall to ARQ.
243 pub fn with_horizon(mut self, horizon: u32) -> Self {
244 self.horizon = horizon.max(1);
245 self
246 }
247
248 /// Record a received source symbol. A source arrival fills its own slot
249 /// directly; recovery of OTHER symbols is driven by repair arrivals
250 /// ([`on_repair`](Self::on_repair)), so this does NOT run the (potentially
251 /// expensive) Gaussian solve - that would otherwise fire on every single
252 /// packet under loss, re-solving the whole in-horizon system each time, when
253 /// a new source adds no equation. A late source that completes a pending
254 /// system is recovered on the next repair (one arrives every `step`
255 /// symbols), and anything that slips through is caught by the ARQ floor.
256 pub fn on_source(&mut self, source_id: u32, payload: &[u8]) -> Vec<u32> {
257 debug_assert_eq!(payload.len(), self.symbol_len);
258 self.highest = self.highest.max(source_id);
259 self.source
260 .entry(source_id)
261 .or_insert_with(|| payload.to_vec());
262 Vec::new()
263 }
264
265 /// Record a received repair symbol. Returns any source ids newly recovered.
266 pub fn on_repair(&mut self, r: RepairSymbol) -> Vec<u32> {
267 self.add_repair(r);
268 self.try_recover()
269 }
270
271 /// Store a repair WITHOUT solving, for a caller that drains a batch of
272 /// datagrams first (stamping their arrival before any decode) and then runs
273 /// one [`recover`](Self::recover) over the whole batch - keeping the
274 /// expensive Gaussian solve out of the receive/timing path.
275 pub fn add_repair(&mut self, r: RepairSymbol) {
276 self.highest = self
277 .highest
278 .max(r.first_source_id.wrapping_add(r.window_size as u32).saturating_sub(1));
279 self.repairs.push(r);
280 }
281
282 /// Run one recovery pass over the currently-stored source and repair symbols,
283 /// returning any source ids newly recovered. Pairs with [`add_repair`](Self::add_repair).
284 pub fn recover(&mut self) -> Vec<u32> {
285 self.try_recover()
286 }
287
288 /// The bytes of source symbol `source_id`, if received or recovered.
289 pub fn get(&self, source_id: u32) -> Option<&[u8]> {
290 self.source.get(&source_id).map(|v| v.as_slice())
291 }
292
293 /// Whether source symbol `source_id` is present (received or recovered).
294 pub fn has(&self, source_id: u32) -> bool {
295 self.source.contains_key(&source_id)
296 }
297
298 /// Drop delivered source symbols and spent repairs below `floor`, to bound
299 /// memory on a long-lived flow. A source symbol a remaining repair still
300 /// references is kept regardless (the decoder must subtract its known
301 /// contribution when solving), so the source floor is capped at the oldest
302 /// remaining repair's window start - forgetting it otherwise would make a
303 /// received symbol look unknown and corrupt the linear system.
304 pub fn forget_below(&mut self, floor: u32) {
305 self.repairs
306 .retain(|r| r.first_source_id.wrapping_add(r.window_size as u32) > floor);
307 let safe = match self.repairs.iter().map(|r| r.first_source_id).min() {
308 Some(oldest) => floor.min(oldest),
309 None => floor,
310 };
311 self.source.retain(|&sid, _| sid >= safe);
312 }
313
314 /// Re-base the decoder to deliver from `base`: drop all stored source and
315 /// repair symbols (they belong to the pre-rebase id range another code now
316 /// owns) and anchor the recovery horizon at `base`. The receiver moves its
317 /// delivery frontier to `base` in lockstep, so nothing below `base` is ever
318 /// looked up again.
319 pub fn rebase_to(&mut self, base: u32) {
320 self.source.clear();
321 self.repairs.clear();
322 self.highest = base;
323 }
324
325 fn try_recover(&mut self) -> Vec<u32> {
326 // Recovery is scoped to the horizon: a sliding-window code cannot use a
327 // repair whose window has aged out, so drop those and only treat
328 // in-horizon gaps as RLC unknowns (older gaps fall to the ARQ floor).
329 // This keeps the solve bounded instead of growing with history.
330 let lo = self.highest.saturating_sub(self.horizon);
331 self.repairs
332 .retain(|r| r.first_source_id.wrapping_add(r.window_size as u32) > lo);
333 let mut unknown_set: BTreeSet<u32> = BTreeSet::new();
334 for r in &self.repairs {
335 for off in 0..r.window_size as u32 {
336 let sid = r.first_source_id.wrapping_add(off);
337 if sid >= lo && !self.source.contains_key(&sid) {
338 unknown_set.insert(sid);
339 }
340 }
341 }
342 if unknown_set.is_empty() {
343 self.prune();
344 return Vec::new();
345 }
346 let unknowns: Vec<u32> = unknown_set.into_iter().collect();
347 let idx: HashMap<u32, usize> = unknowns.iter().enumerate().map(|(i, &s)| (s, i)).collect();
348 let ncols = unknowns.len();
349
350 // One row per repair covering at least one unknown: a coefficient
351 // vector over the unknowns and an rhs symbol-vector with the known
352 // source contributions already moved across (rhs ^= c * known_source).
353 struct Row {
354 coefs: Vec<u8>,
355 rhs: Vec<u8>,
356 }
357 let mut rows: Vec<Row> = Vec::new();
358 for r in &self.repairs {
359 let mut coefs = vec![0u8; ncols];
360 let mut rhs = r.payload.clone();
361 let mut covers_unknown = false;
362 let mut usable = true;
363 for off in 0..r.window_size as u32 {
364 let sid = r.first_source_id.wrapping_add(off);
365 let c = coef(r.repair_key, sid, r.dt);
366 if c == 0 {
367 continue;
368 }
369 if let Some(sym) = self.source.get(&sid) {
370 gf_mul_add_auto(&mut rhs, sym, c);
371 } else if sid >= lo {
372 coefs[idx[&sid]] = c;
373 covers_unknown = true;
374 } else {
375 // An unknown below the horizon is out of RLC scope; this
376 // repair cannot be used here (the ARQ floor recovers that
377 // older gap).
378 usable = false;
379 break;
380 }
381 }
382 if usable && covers_unknown {
383 rows.push(Row { coefs, rhs });
384 }
385 }
386
387 // Reduced row echelon over GF(2^8).
388 let mut pivot = 0usize;
389 for col in 0..ncols {
390 let sel = (pivot..rows.len()).find(|&r| rows[r].coefs[col] != 0);
391 let Some(sel) = sel else { continue };
392 rows.swap(pivot, sel);
393 let inv = gf::inv(rows[pivot].coefs[col]);
394 for cf in rows[pivot].coefs.iter_mut() {
395 *cf = gf::mul(*cf, inv);
396 }
397 gf_scale(&mut rows[pivot].rhs, inv);
398 // Snapshot the pivot row so the elimination loop can borrow `rows`
399 // mutably for every other row without aliasing.
400 let pivot_coefs = rows[pivot].coefs.clone();
401 let pivot_rhs = rows[pivot].rhs.clone();
402 for (r, row) in rows.iter_mut().enumerate() {
403 if r == pivot {
404 continue;
405 }
406 let f = row.coefs[col];
407 if f == 0 {
408 continue;
409 }
410 for (rc, &pc) in row.coefs.iter_mut().zip(&pivot_coefs) {
411 *rc ^= gf::mul(f, pc);
412 }
413 gf_mul_add_auto(&mut row.rhs, &pivot_rhs, f);
414 }
415 pivot += 1;
416 }
417
418 // A row that reduced to a single unit coefficient determines that
419 // unknown: x = rhs.
420 let mut recovered = Vec::new();
421 for row in &rows {
422 let nz: Vec<usize> = (0..ncols).filter(|&c| row.coefs[c] != 0).collect();
423 if nz.len() == 1 && row.coefs[nz[0]] == 1 {
424 let sid = unknowns[nz[0]];
425 if let std::collections::btree_map::Entry::Vacant(e) = self.source.entry(sid) {
426 e.insert(row.rhs.clone());
427 recovered.push(sid);
428 }
429 }
430 }
431 self.prune();
432 recovered
433 }
434
435 /// Drop repairs whose covered source symbols are all known: they carry no
436 /// further information and keeping them only grows the linear system.
437 fn prune(&mut self) {
438 let source = &self.source;
439 self.repairs.retain(|r| {
440 (0..r.window_size as u32)
441 .any(|off| !source.contains_key(&r.first_source_id.wrapping_add(off)))
442 });
443 }
444}
445
446/// `v[i] = gf::mul(v[i], coef)` in place - a scalar GF(2^8) scale of one
447/// symbol-vector (used once per pivot to normalize the pivot row's rhs).
448fn gf_scale(v: &mut [u8], coef: u8) {
449 if coef == 1 {
450 return;
451 }
452 for b in v.iter_mut() {
453 *b = gf::mul(*b, coef);
454 }
455}
456
457#[cfg(test)]
458mod tests {
459 use super::*;
460
461 /// A deterministic source symbol of `len` bytes for source id `sid`.
462 fn make_symbol(sid: u32, len: usize) -> Vec<u8> {
463 (0..len)
464 .map(|b| ((sid as usize * 131 + b * 17 + 7) & 0xff) as u8)
465 .collect()
466 }
467
468 /// After a cross-code resync, the encoder re-bases to the new id and its
469 /// repairs reference only post-rebase symbols, and a re-based decoder recovers
470 /// a post-rebase loss without ever touching the abandoned pre-rebase ids.
471 #[test]
472 fn rebase_resumes_a_clean_recoverable_stream() {
473 let len = 32;
474 let mut enc = RlcEncoder::new(8, 4, 15, len);
475 for sid in 0..10u32 {
476 enc.push_source(&make_symbol(sid, len));
477 }
478 // Resync: another code carried [10, 5000); RLC resumes at 5000.
479 enc.rebase_to(5000);
480 assert_eq!(enc.next_source_id(), 5000);
481
482 let mut dec = RlcDecoder::new(len).with_horizon(64);
483 dec.rebase_to(5000);
484 // Push four post-rebase symbols, dropping the first (5000), keeping its
485 // repair, and verify RLC recovers it - proving the re-based window is a
486 // self-contained linear system anchored at the new base.
487 let mut repair = None;
488 for sid in 5000..5004u32 {
489 let (id, rep) = enc.push_source(&make_symbol(sid, len));
490 assert_eq!(id, sid);
491 if sid != 5000 {
492 dec.on_source(sid, &make_symbol(sid, len));
493 }
494 if let Some(r) = rep {
495 repair = Some(r);
496 }
497 }
498 let recovered = dec.on_repair(repair.expect("a repair fires every step=4"));
499 assert!(recovered.contains(&5000), "re-based loss recovers: {recovered:?}");
500 assert_eq!(dec.get(5000), Some(make_symbol(5000, len).as_slice()));
501 // Nothing below the rebase base is present (the old range was abandoned).
502 assert!(!dec.has(9), "pre-rebase ids must not linger in the decoder");
503 }
504
505 #[test]
506 fn coefficient_generator_is_deterministic_and_honors_density() {
507 // Same inputs -> same coefficient.
508 for key in 0..8u32 {
509 for sid in 0..8u32 {
510 assert_eq!(coef(key, sid, 15), coef(key, sid, 15));
511 }
512 }
513 // DT = 15 -> every coefficient nonzero.
514 for sid in 0..256u32 {
515 assert_ne!(coef(7, sid, 15), 0, "DT=15 must be fully dense at sid {sid}");
516 }
517 // DT = 0 -> roughly 1/16 nonzero (sample a population).
518 let nz = (0..4096u32).filter(|&s| coef(3, s, 0) != 0).count();
519 assert!(
520 (150..420).contains(&nz),
521 "DT=0 density ~1/16 of 4096 (~256); got {nz}"
522 );
523 }
524
525 /// An isolated loss is recovered from the very next repair that covers it,
526 /// without waiting for a block to complete.
527 #[test]
528 fn isolated_loss_recovers_immediately() {
529 let len = 64;
530 let mut enc = RlcEncoder::new(8, 2, DEFAULT_DT, len);
531 let mut dec = RlcDecoder::new(len);
532 let drop_sid = 5u32;
533 let n = 12u32;
534 let mut recovered_at: Option<u32> = None;
535 let mut emitted = 0u32; // count of wire symbols fed after the drop
536 for i in 0..n {
537 let sym = make_symbol(i, len);
538 let (sid, repair) = enc.push_source(&sym);
539 if sid != drop_sid {
540 dec.on_source(sid, &sym);
541 }
542 if sid > drop_sid {
543 emitted += 1;
544 }
545 if let Some(r) = repair {
546 let rec = dec.on_repair(r);
547 if rec.contains(&drop_sid) && recovered_at.is_none() {
548 recovered_at = Some(emitted);
549 }
550 }
551 }
552 assert!(dec.has(drop_sid), "isolated loss must recover");
553 assert_eq!(
554 dec.get(drop_sid),
555 Some(make_symbol(drop_sid, len).as_slice()),
556 "recovered bytes must match the original"
557 );
558 // Recovered within a couple of symbols of the loss - not after a whole
559 // block (a block of this rate would need ~the full window first).
560 assert!(
561 recovered_at.is_some_and(|e| e <= 2),
562 "must recover within ~2 wire symbols of the loss, got {recovered_at:?}"
563 );
564 }
565
566 /// A burst of consecutive losses recovers once enough repairs span them.
567 #[test]
568 fn burst_within_capability_recovers() {
569 let len = 48;
570 let mut enc = RlcEncoder::new(16, 2, DEFAULT_DT, len);
571 let mut dec = RlcDecoder::new(len);
572 let drops: BTreeSet<u32> = [4, 5].into_iter().collect();
573 let originals: Vec<Vec<u8>> = (0..16).map(|i| make_symbol(i, len)).collect();
574 for i in 0..16u32 {
575 let (sid, repair) = enc.push_source(&originals[i as usize]);
576 if !drops.contains(&sid) {
577 dec.on_source(sid, &originals[i as usize]);
578 }
579 if let Some(r) = repair {
580 dec.on_repair(r);
581 }
582 }
583 for &sid in &drops {
584 assert!(dec.has(sid), "burst symbol {sid} must recover");
585 assert_eq!(dec.get(sid), Some(originals[sid as usize].as_slice()));
586 }
587 }
588
589 /// A loss whose repairs are also lost is reported missing, never recovered
590 /// as wrong data.
591 #[test]
592 fn unrecoverable_loss_is_not_misrecovered() {
593 let len = 32;
594 let mut enc = RlcEncoder::new(8, 2, DEFAULT_DT, len);
595 let mut dec = RlcDecoder::new(len);
596 let drop_sid = 5u32;
597 for i in 0..12u32 {
598 let sym = make_symbol(i, len);
599 let (sid, repair) = enc.push_source(&sym);
600 if sid != drop_sid {
601 dec.on_source(sid, &sym);
602 }
603 // Drop every repair whose window still contains the lost symbol, so
604 // it can never be recovered.
605 if let Some(r) = repair {
606 let covers = (0..r.window_size as u32)
607 .any(|off| r.first_source_id.wrapping_add(off) == drop_sid);
608 if !covers {
609 dec.on_repair(r);
610 }
611 }
612 }
613 assert!(!dec.has(drop_sid), "no repair covered it -> must stay missing");
614 assert_eq!(dec.get(drop_sid), None, "must not fabricate wrong data");
615 }
616
617 /// A long stream under scattered isolated losses delivers every symbol
618 /// exactly (received or recovered).
619 #[test]
620 fn long_stream_scattered_losses_all_recover() {
621 let len = 40;
622 let n = 300u32;
623 let mut enc = RlcEncoder::new(16, 2, DEFAULT_DT, len);
624 let mut dec = RlcDecoder::new(len);
625 let originals: Vec<Vec<u8>> = (0..n).map(|i| make_symbol(i, len)).collect();
626 // Deterministic ~8% isolated drops (never two in a row, so each is
627 // within the repair capability at this rate).
628 let mut rng = 0x1234_5678u32;
629 let mut prev_dropped = false;
630 for i in 0..n {
631 rng = rng.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
632 let drop = !prev_dropped && (rng >> 24) % 100 < 8;
633 prev_dropped = drop;
634 let (sid, repair) = enc.push_source(&originals[i as usize]);
635 if !drop {
636 dec.on_source(sid, &originals[i as usize]);
637 }
638 if let Some(r) = repair {
639 dec.on_repair(r);
640 }
641 }
642 for i in 0..n {
643 assert!(dec.has(i), "symbol {i} must be delivered");
644 assert_eq!(dec.get(i), Some(originals[i as usize].as_slice()));
645 }
646 }
647}