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
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>
186 lines
6.8 KiB
Rust
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(())
|
|
}
|
|
}
|