From 1b27706a9b82813d21cb1022acbee10f3cf7b995 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Sat, 25 Jul 2026 01:26:24 +0200 Subject: [PATCH] =?UTF-8?q?feat(host/native):=20truthful=20bitrate=20state?= =?UTF-8?q?=20=E2=80=94=20applied-rate=20adoption,=20ceiling=20pre-clamp,?= =?UTF-8?q?=20climb=20refusal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Host half of the §ABR-overdrive fix. The stream loop now reads `Encoder::applied_bitrate_bps()` after every bitrate apply and stores THAT into `bitrate_kbps`/`live_bitrate` — the send pacer, web console, mgmt registry and control-task acks all track what the ASIC really targets instead of the requested rate (the pre-fix ack promised 1.01 Gbps while the encoder ran 794 Mbps, and the client controller climbed from the phantom base forever). - A short apply teaches `encoder_ceiling_kbps` (shared atomic): the stream loop pre-clamps incoming requests to it and SKIPS the apply when nothing would change — ending the reconfigure-reject → full-rebuild(~0.6 s + IDR) storm — and the control task resolves future SetBitrate acks against it, so the client learns the ceiling through the existing ack path (no wire change; old clients converge too). - `cadence_degraded` (shared flag, leaky-bucket level ≥ 10 or an escalated session): while set, the control task resolves climbs to the current applied rate — on a fat LAN no network signal ever stops a climb the encoder can't serve, and past the compute knee more bits only deepen the cadence miss. Descents always pass; they're the cure. - Escalation-warmup hygiene: an ABR rebuild stall (~70 missed deadlines at 120 fps, 3.5× the escalate threshold) and the backlog scored against a heavier rate no longer feed the latency escalation — rebuilds reset the leaky bucket and re-run the warmup; in-place down-steps clear the bucket. Co-Authored-By: Claude Fable 5 --- crates/punktfunk-host/src/native/control.rs | 8 +-- crates/punktfunk-host/src/native/stream.rs | 72 +++++++++++++++++++-- 2 files changed, 70 insertions(+), 10 deletions(-) diff --git a/crates/punktfunk-host/src/native/control.rs b/crates/punktfunk-host/src/native/control.rs index 0c59e7a8..2ce0e7d0 100644 --- a/crates/punktfunk-host/src/native/control.rs +++ b/crates/punktfunk-host/src/native/control.rs @@ -22,10 +22,10 @@ pub(super) async fn run( live_reconfig_ok: bool, adaptive_fec: bool, session_bitrate_kbps: u32, - /// Encoder-truth bridge (data plane → here, §ABR overdrive): the encoder's live applied rate, - /// its discovered codec-level ceiling (0 = unknown), and the "encode can't hold cadence" - /// flag. Read at `SetBitrate`-resolve time so the ack — the base the client's controller - /// climbs from — never promises a rate the encoder won't run at. + // Encoder-truth bridge (data plane → here, §ABR overdrive): the encoder's live applied rate, + // its discovered codec-level ceiling (0 = unknown), and the "encode can't hold cadence" + // flag. Read at `SetBitrate`-resolve time so the ack — the base the client's controller + // climbs from — never promises a rate the encoder won't run at. live_bitrate: Arc, encoder_ceiling_kbps: Arc, cadence_degraded: Arc, diff --git a/crates/punktfunk-host/src/native/stream.rs b/crates/punktfunk-host/src/native/stream.rs index 17bdfb30..a84f4e0f 100644 --- a/crates/punktfunk-host/src/native/stream.rs +++ b/crates/punktfunk-host/src/native/stream.rs @@ -1453,6 +1453,10 @@ pub(super) fn virtual_stream(ctx: SessionContext, prepared: Option ceiling { + tracing::info!( + requested_kbps = *k, + ceiling_kbps = ceiling, + "bitrate request clamped to the known encoder ceiling" + ); + *k = ceiling; + } + } if let Some(new_kbps) = want_kbps.filter(|&k| k != bitrate_kbps) { if enc.reconfigure_bitrate(new_kbps as u64 * 1000) { + // Adopt the encoder's post-clamp truth, not the request: it feeds the send + // pacer, the console/mgmt view and the control task's acks, and a short apply + // teaches the ceiling used above. + let applied_kbps = enc + .applied_bitrate_bps() + .map(|b| (b / 1000) as u32) + .filter(|&k| k > 0) + .unwrap_or(new_kbps); tracing::info!( from_kbps = bitrate_kbps, - to_kbps = new_kbps, + to_kbps = applied_kbps, + requested_kbps = new_kbps, "encoder bitrate reconfigured in place (adaptive bitrate — no IDR)" ); - bitrate_kbps = new_kbps; - live_bitrate.store(new_kbps, Ordering::Relaxed); + if applied_kbps < new_kbps { + encoder_ceiling_kbps.store(applied_kbps, Ordering::Relaxed); + } + if applied_kbps < bitrate_kbps { + // Down-step: the behind-cadence backlog was scored against the old, + // heavier rate — clean slate so it can't feed a false escalation. + behind_score = 0; + } + bitrate_kbps = applied_kbps; + live_bitrate.store(applied_kbps, Ordering::Relaxed); // Same encoder, same stream: the in-flight AUs and the wire-index prediction // stay valid — no inflight forfeit, no IDR-cooldown anchor. } else { @@ -1719,9 +1757,17 @@ pub(super) fn virtual_stream(ctx: SessionContext, prepared: Option { + // The fresh encoder may have clamped to its codec-level ceiling — + // adopt (and record) ITS rate, not the request; see the in-place arm. + let applied_kbps = new_enc + .applied_bitrate_bps() + .map(|b| (b / 1000) as u32) + .filter(|&k| k > 0) + .unwrap_or(new_kbps); tracing::info!( from_kbps = bitrate_kbps, - to_kbps = new_kbps, + to_kbps = applied_kbps, + requested_kbps = new_kbps, "encoder rebuilt at new bitrate (adaptive bitrate)" ); if let Some(c) = plan.wire_chunk { @@ -1731,8 +1777,11 @@ pub(super) fn virtual_stream(ctx: SessionContext, prepared: Option { tracing::warn!(error = %format!("{e:#}"), to_kbps = new_kbps, @@ -2573,6 +2627,12 @@ pub(super) fn virtual_stream(ctx: SessionContext, prepared: Option= DEPTH_DEGRADE, Ordering::Relaxed); if behind_score >= DEPTH_ESCALATE { if cur_depth < max_depth { cur_depth = max_depth;