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:
@@ -22,10 +22,10 @@ pub(super) async fn run(
|
|||||||
live_reconfig_ok: bool,
|
live_reconfig_ok: bool,
|
||||||
adaptive_fec: bool,
|
adaptive_fec: bool,
|
||||||
session_bitrate_kbps: u32,
|
session_bitrate_kbps: u32,
|
||||||
/// Encoder-truth bridge (data plane → here, §ABR overdrive): the encoder's live applied rate,
|
// 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"
|
// 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
|
// 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.
|
// climbs from — never promises a rate the encoder won't run at.
|
||||||
live_bitrate: Arc<AtomicU32>,
|
live_bitrate: Arc<AtomicU32>,
|
||||||
encoder_ceiling_kbps: Arc<AtomicU32>,
|
encoder_ceiling_kbps: Arc<AtomicU32>,
|
||||||
cadence_degraded: Arc<AtomicBool>,
|
cadence_degraded: Arc<AtomicBool>,
|
||||||
|
|||||||
@@ -1453,6 +1453,10 @@ pub(super) fn virtual_stream(ctx: SessionContext, prepared: Option<PreparedDispl
|
|||||||
const DEPTH_ESCALATE: u32 = 20;
|
const DEPTH_ESCALATE: u32 = 20;
|
||||||
const DEPTH_BEHIND_CAP: u32 = 60;
|
const DEPTH_BEHIND_CAP: u32 = 60;
|
||||||
const DEPTH_WARMUP_FRAMES: u64 = 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 {
|
while !stop.load(Ordering::SeqCst) && std::time::Instant::now() < deadline {
|
||||||
// Mid-stream session switch (the box flipped Gaming↔Desktop): rebuild the WHOLE backend in
|
// 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
|
// 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() {
|
while let Ok(k) = bitrate_rx.try_recv() {
|
||||||
want_kbps = Some(k);
|
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 let Some(new_kbps) = want_kbps.filter(|&k| k != bitrate_kbps) {
|
||||||
if enc.reconfigure_bitrate(new_kbps as u64 * 1000) {
|
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!(
|
tracing::info!(
|
||||||
from_kbps = bitrate_kbps,
|
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)"
|
"encoder bitrate reconfigured in place (adaptive bitrate — no IDR)"
|
||||||
);
|
);
|
||||||
bitrate_kbps = new_kbps;
|
if applied_kbps < new_kbps {
|
||||||
live_bitrate.store(new_kbps, Ordering::Relaxed);
|
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
|
// Same encoder, same stream: the in-flight AUs and the wire-index prediction
|
||||||
// stay valid — no inflight forfeit, no IDR-cooldown anchor.
|
// stay valid — no inflight forfeit, no IDR-cooldown anchor.
|
||||||
} else {
|
} else {
|
||||||
@@ -1719,9 +1757,17 @@ pub(super) fn virtual_stream(ctx: SessionContext, prepared: Option<PreparedDispl
|
|||||||
plan.cursor_blend,
|
plan.cursor_blend,
|
||||||
) {
|
) {
|
||||||
Ok(mut new_enc) => {
|
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!(
|
tracing::info!(
|
||||||
from_kbps = bitrate_kbps,
|
from_kbps = bitrate_kbps,
|
||||||
to_kbps = new_kbps,
|
to_kbps = applied_kbps,
|
||||||
|
requested_kbps = new_kbps,
|
||||||
"encoder rebuilt at new bitrate (adaptive bitrate)"
|
"encoder rebuilt at new bitrate (adaptive bitrate)"
|
||||||
);
|
);
|
||||||
if let Some(c) = plan.wire_chunk {
|
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.)
|
// directly so an ABR rebuild re-establishes the bound immediately.)
|
||||||
new_enc.set_input_ring_depth(capturer.pipeline_depth().max(1));
|
new_enc.set_input_ring_depth(capturer.pipeline_depth().max(1));
|
||||||
enc = new_enc;
|
enc = new_enc;
|
||||||
bitrate_kbps = new_kbps;
|
if applied_kbps < new_kbps {
|
||||||
live_bitrate.store(new_kbps, Ordering::Relaxed);
|
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
|
// 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
|
// mode-switch rebuild; the fresh encoder opens on an IDR, so anchor the
|
||||||
// IDR cooldown too.
|
// IDR cooldown too.
|
||||||
@@ -1740,6 +1789,11 @@ pub(super) fn virtual_stream(ctx: SessionContext, prepared: Option<PreparedDispl
|
|||||||
last_au_at = std::time::Instant::now();
|
last_au_at = std::time::Instant::now();
|
||||||
encoder_resets = 0;
|
encoder_resets = 0;
|
||||||
last_forced_idr = Some(std::time::Instant::now());
|
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) => {
|
Err(e) => {
|
||||||
tracing::warn!(error = %format!("{e:#}"), to_kbps = new_kbps,
|
tracing::warn!(error = %format!("{e:#}"), to_kbps = new_kbps,
|
||||||
@@ -2573,6 +2627,12 @@ pub(super) fn virtual_stream(ctx: SessionContext, prepared: Option<PreparedDispl
|
|||||||
} else {
|
} else {
|
||||||
behind_score.saturating_sub(1)
|
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 behind_score >= DEPTH_ESCALATE {
|
||||||
if cur_depth < max_depth {
|
if cur_depth < max_depth {
|
||||||
cur_depth = max_depth;
|
cur_depth = max_depth;
|
||||||
|
|||||||
Reference in New Issue
Block a user