feat(host/native): truthful bitrate state — applied-rate adoption, ceiling pre-clamp, climb refusal

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 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 01:44:54 +02:00
co-authored by Claude Fable 5
parent f3c3a9427b
commit 1b27706a9b
2 changed files with 70 additions and 10 deletions
+4 -4
View File
@@ -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<AtomicU32>,
encoder_ceiling_kbps: Arc<AtomicU32>,
cadence_degraded: Arc<AtomicBool>,
+66 -6
View File
@@ -1453,6 +1453,10 @@ pub(super) fn virtual_stream(ctx: SessionContext, prepared: Option<PreparedDispl
const DEPTH_ESCALATE: u32 = 20;
const DEPTH_BEHIND_CAP: u32 = 60;
const DEPTH_WARMUP_FRAMES: u64 = 60;
// Half the escalate threshold: ~10 net behind-frames is already solid "the encoder, not the
// network, is the bottleneck" evidence — enough to flag `cadence_degraded` (the control task
// then refuses bitrate CLIMBS) well before the session pays a latency escalation for it.
const DEPTH_DEGRADE: u32 = 10;
while !stop.load(Ordering::SeqCst) && std::time::Instant::now() < deadline {
// Mid-stream session switch (the box flipped Gaming↔Desktop): rebuild the WHOLE backend in
// place — a different compositor at the SAME client mode — keeping the Session + send thread
@@ -1691,15 +1695,49 @@ pub(super) fn virtual_stream(ctx: SessionContext, prepared: Option<PreparedDispl
while let Ok(k) = bitrate_rx.try_recv() {
want_kbps = Some(k);
}
// Known-ceiling pre-clamp (§ABR overdrive): once the encoder's codec-level ceiling is
// known, resolve an over-asking request HERE — a request that clamps to the rate we're
// already at then skips the whole apply, where the pre-fix path bounced every overshoot
// off the driver into a full rebuild + IDR (~0.6 s each, four in one logged minute).
// (The control task clamps its acks from the same atomic; this covers requests already
// in flight when the ceiling was discovered.)
if let Some(k) = want_kbps.as_mut() {
let ceiling = encoder_ceiling_kbps.load(Ordering::Relaxed);
if ceiling != 0 && *k > 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<PreparedDispl
plan.cursor_blend,
) {
Ok(mut new_enc) => {
// 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<PreparedDispl
// directly so an ABR rebuild re-establishes the bound immediately.)
new_enc.set_input_ring_depth(capturer.pipeline_depth().max(1));
enc = new_enc;
bitrate_kbps = new_kbps;
live_bitrate.store(new_kbps, Ordering::Relaxed);
if applied_kbps < new_kbps {
encoder_ceiling_kbps.store(applied_kbps, Ordering::Relaxed);
}
bitrate_kbps = applied_kbps;
live_bitrate.store(applied_kbps, Ordering::Relaxed);
// The owed AUs died with the old encoder — same bookkeeping as a
// mode-switch rebuild; the fresh encoder opens on an IDR, so anchor the
// IDR cooldown too.
@@ -1740,6 +1789,11 @@ pub(super) fn virtual_stream(ctx: SessionContext, prepared: Option<PreparedDispl
last_au_at = std::time::Instant::now();
encoder_resets = 0;
last_forced_idr = Some(std::time::Instant::now());
// The rebuild stall itself (~0.6 s ≈ 70 missed deadlines at 120 fps,
// 3.5× the escalate threshold) must not feed the contention
// escalation — clean slate + re-run the warmup before judging again.
behind_score = 0;
depth_frames = 0;
}
Err(e) => {
tracing::warn!(error = %format!("{e:#}"), to_kbps = new_kbps,
@@ -2573,6 +2627,12 @@ pub(super) fn virtual_stream(ctx: SessionContext, prepared: Option<PreparedDispl
} else {
behind_score.saturating_sub(1)
};
// Export "encode can't hold cadence" for the control task's climb refusal.
// Stored BEFORE the escalate check: the firing iteration writes `true`, and a
// final-stage escalation freezes this whole block (the guard above goes false)
// — so an escalated session stays flagged, which is exactly right: its climb
// headroom is spent until something (a down-step, de-escalation) changes.
cadence_degraded.store(behind_score >= DEPTH_DEGRADE, Ordering::Relaxed);
if behind_score >= DEPTH_ESCALATE {
if cur_depth < max_depth {
cur_depth = max_depth;