Files
punktfunk/crates/punktfunk-core/src/fec/gf16.rs
T
enricobuehler f4f6c5556f
ci / docs-site (push) Successful in 1m0s
ci / web (push) Successful in 1m0s
decky / build-publish (push) Successful in 19s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 7s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 8s
ci / bench (push) Successful in 6m4s
windows-host / package (push) Successful in 9m3s
flatpak / build-publish (push) Failing after 8m2s
docker / deploy-docs (push) Successful in 24s
deb / build-publish (push) Successful in 12m19s
android / android (push) Successful in 12m46s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 4m22s
arch / build-publish (push) Successful in 14m45s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m37s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 13m42s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 17m34s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 4m57s
ci / rust (push) Successful in 23m21s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m57s
release / apple (push) Has been cancelled
apple / screenshots (push) Has been cancelled
apple / swift (push) Has been cancelled
perf(core): FEC encoder reuse — cached codecs + pooled parity, no per-block setup
Phase 1.4 (throughput-beyond-1gbps.md): the send path built a fresh erasure
codec and allocated fresh parity Vecs for every FEC block. New trait method
ErasureCoder::encode_into generates parity into caller-pooled buffers; the
packetizer keeps one parity pool that grows once to the session's high-water
recovery count.

- gf16: one cached reed_solomon_simd::ReedSolomonEncoder per coder, re-shaped
  per block via reset() (reuses its working space) — the old encode()
  convenience call paid engine CPU-feature detection, FFT planning, and
  work-buffer allocation per block.
- gf8: last-used (k, m) Cauchy codec cached, so the generator-matrix build
  drops out of steady-state frames; parity buffers shaped without re-zeroing
  (encode_sep's first-input pass overwrites every row). The GameStream
  VideoPacketizer now owns a persistent coder so the cache survives frames.
- encode() delegates to encode_into — one code path, and the nanors byte-exact
  parity vector keeps pinning Moonlight wire compatibility.

Validated: 145 core + 308 host tests + clippy -D warnings on .21, loss-harness
recovery curve identical, pipeline bench +0.6-2.4% thrpt (all configs, p<0.05;
the loopback bench is encoder-dominated so the alloc savings mostly land
outside it).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 23:19:21 +02:00

186 lines
6.8 KiB
Rust

//! GF(2¹⁶) Leopard-RS backend (`reed-solomon-simd`). SIMD, O(n log n), up to 65535
//! shards/block — this is what removes the GameStream 255-shard / ~1 Gbps wall.
//! Shard length must be even.
use super::{
validate_block_shape, validate_encode_shape, validate_into_shape, ErasureCoder, FecError,
};
use crate::config::FecScheme;
use reed_solomon_simd::ReedSolomonEncoder;
use std::sync::Mutex;
#[derive(Default)]
pub struct Gf16Coder {
/// Cached Leopard encoder (plan Phase 1.4): `reset()` re-shapes it per block while
/// reusing its working space, so steady-state frames cost no encoder construction (the
/// old `reed_solomon_simd::encode` convenience call built one — engine CPU-feature
/// detection, FFT planning, work-buffer allocs — per block). `Mutex` only to keep the
/// `&self` trait surface; a session's coder is driven by its one send thread, so the
/// lock is uncontended.
enc: Mutex<Option<ReedSolomonEncoder>>,
}
impl ErasureCoder for Gf16Coder {
fn scheme(&self) -> FecScheme {
FecScheme::Gf16
}
fn encode(&self, data: &[&[u8]], recovery_count: usize) -> Result<Vec<Vec<u8>>, FecError> {
let mut out = Vec::new();
self.encode_into(data, recovery_count, &mut out)?;
Ok(out)
}
fn encode_into(
&self,
data: &[&[u8]],
recovery_count: usize,
out: &mut Vec<Vec<u8>>,
) -> Result<(), FecError> {
if recovery_count == 0 {
out.clear();
return Ok(());
}
validate_encode_shape(data)?;
let k = data.len();
let shard_len = data[0].len();
if shard_len % 2 != 0 {
return Err(FecError::Config("GF(2^16) shard length must be even"));
}
let mut guard = self.enc.lock().unwrap_or_else(|p| p.into_inner());
let enc = match guard.as_mut() {
Some(enc) => {
enc.reset(k, recovery_count, shard_len)
.map_err(|_| FecError::Backend("gf16 encoder reset"))?;
enc
}
None => guard.insert(
ReedSolomonEncoder::new(k, recovery_count, shard_len)
.map_err(|_| FecError::Backend("gf16 encoder init"))?,
),
};
for shard in data {
enc.add_original_shard(shard)
.map_err(|_| FecError::Backend("gf16 add shard"))?;
}
let result = enc.encode().map_err(|_| FecError::Backend("gf16 encode"))?;
// Copy the parity into the caller's pooled buffers: existing `Vec`s are reused
// (clear keeps capacity), the pool grows once to the session's high-water M.
out.truncate(recovery_count);
let mut parity = result.recovery_iter();
for buf in out.iter_mut() {
let shard = parity
.next()
.ok_or(FecError::Backend("gf16 parity count"))?;
buf.clear();
buf.extend_from_slice(shard);
}
for shard in parity {
out.push(shard.to_vec());
}
if out.len() != recovery_count {
return Err(FecError::Backend("gf16 parity count"));
}
Ok(())
}
fn reconstruct(
&self,
data_count: usize,
recovery_count: usize,
received: &mut [Option<Vec<u8>>],
) -> Result<Vec<Vec<u8>>, FecError> {
validate_block_shape(received, data_count, recovery_count)?;
let present = received.iter().filter(|s| s.is_some()).count();
if present < data_count {
return Err(FecError::TooFewShards {
have: present,
need: data_count,
});
}
// Fast path: all originals already present, or FEC disabled.
let originals_complete = received[..data_count].iter().all(|s| s.is_some());
if recovery_count == 0 || originals_complete {
let mut out = Vec::with_capacity(data_count);
for slot in received.iter().take(data_count) {
out.push(slot.clone().ok_or(FecError::TooFewShards {
have: present,
need: data_count,
})?);
}
return Ok(out);
}
// Hand the decoder the surviving originals and recovery shards, indexed.
let original_in: Vec<(usize, &[u8])> = received[..data_count]
.iter()
.enumerate()
.filter_map(|(i, s)| s.as_deref().map(|b| (i, b)))
.collect();
let recovery_in: Vec<(usize, &[u8])> = received[data_count..data_count + recovery_count]
.iter()
.enumerate()
.filter_map(|(j, s)| s.as_deref().map(|b| (j, b)))
.collect();
let restored =
reed_solomon_simd::decode(data_count, recovery_count, original_in, recovery_in)
.map_err(|_| FecError::Backend("gf16 decode"))?;
// Merge surviving originals with the recovered ones.
let mut out: Vec<Vec<u8>> = Vec::with_capacity(data_count);
for (i, slot) in received[..data_count].iter().enumerate() {
if let Some(s) = slot {
out.push(s.clone());
} else if let Some(s) = restored.get(&i) {
out.push(s.clone());
} else {
return Err(FecError::Backend("gf16 decode left an original missing"));
}
}
Ok(out)
}
fn reconstruct_into(
&self,
recovery_count: usize,
data: &mut [&mut [u8]],
have: &[bool],
recovery: &[(usize, &[u8])],
) -> Result<(), FecError> {
validate_into_shape(data, have, recovery, recovery_count)?;
if have.iter().all(|h| *h) {
return Ok(()); // nothing missing — no codec work, no copies
}
if data[0].len() % 2 != 0 {
return Err(FecError::Config("GF(2^16) shard length must be even"));
}
let data_count = data.len();
// Present originals as indexed refs (shared reborrows of the caller's slots); the decoder
// returns the restored shards owned, so the borrows end before the write-back below.
let original_in: Vec<(usize, &[u8])> = data
.iter()
.zip(have)
.enumerate()
.filter(|(_, (_, &h))| h)
.map(|(i, (s, _))| (i, &**s))
.collect();
let restored = reed_solomon_simd::decode(
data_count,
recovery_count,
original_in,
recovery.iter().copied(),
)
.map_err(|_| FecError::Backend("gf16 decode"))?;
for (i, h) in have.iter().enumerate() {
if !*h {
let shard = restored
.get(&i)
.ok_or(FecError::Backend("gf16 decode left an original missing"))?;
data[i].copy_from_slice(shard);
}
}
Ok(())
}
}