feat(core/fec): adaptive FEC — size recovery to measured loss, not a flat 20%
On a clean link the flat 20% FEC is pure waste: extra wire bytes AND extra
packets. On a packet-rate-bound uplink (the Steam Deck's WiFi tx caps ~22k pps
regardless of bitrate) those extra packets directly cost goodput — measured at
200 Mbps goodput, 20% FEC drove ~10% loss vs ~2.6% at 0% (it saturated the link).
Adaptive FEC closes the loop:
- Client measures the loss FEC is absorbing each ~750 ms window from session stats
(recovered shards / received, + a bump when a frame went unrecoverable) and sends
a periodic `LossReport { loss_ppm }` on the control stream (new message;
`window_loss_ppm` helper, shared + unit-tested). Connector (Apple/Linux/Windows)
and probe both report; suppressed during a speed test so its filler can't skew it.
- Host maps loss → recovery % (`adapt_fec`: ≈ loss×1.4 + 1pt, clamped 1..50) and
applies it live via `Session::set_fec_percent` (the wire is self-describing — each
packet carries its block's data/recovery counts, so the receiver needs no notice).
A clean link decays to ~1%; loss ramps it up and converges.
- `PUNKTFUNK_FEC_PCT`, when set, now PINS FEC static (disables adaptation) so
speed-test / measurement runs keep a fixed, known overhead. Unset ⇒ adaptive,
starting at 10%.
An older host ignores LossReport (unknown control message) and keeps static FEC;
an older client simply never reports and the host holds its start value. Builds +
clippy + fmt + tests green (adapt_fec / window_loss_ppm / loss_report unit tests).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -201,6 +201,18 @@ impl Session {
|
||||
r.map(|_| ())
|
||||
}
|
||||
|
||||
/// Host: live-adjust the FEC recovery percentage (adaptive FEC). Affects the next
|
||||
/// [`submit_frame`](Self::submit_frame)/[`seal_frame`](Self::seal_frame); the receiver needs no
|
||||
/// notification (each packet's header carries its block's data/recovery shard counts).
|
||||
pub fn set_fec_percent(&mut self, pct: u8) {
|
||||
self.packetizer.set_fec_percent(pct);
|
||||
}
|
||||
|
||||
/// The current FEC recovery percentage (host side).
|
||||
pub fn fec_percent(&self) -> u8 {
|
||||
self.packetizer.fec_percent()
|
||||
}
|
||||
|
||||
/// Host: drain one pending input event from the client, if any.
|
||||
pub fn poll_input(&mut self) -> Result<Option<InputEvent>> {
|
||||
if self.config.role != Role::Host {
|
||||
|
||||
Reference in New Issue
Block a user