feat(client/abr): learn the host's rate cap from short acks, and read host encode time as a signal

Two client-side halves of the §ABR-overdrive fix (the 4K120 sessions that
climbed to 1 Gbps against a 794 Mbps encoder and a 8.33 ms budget):

- Short-ack cap learning: the host now resolves a climb it can't serve to what
  the encoder actually runs at (its codec-level ceiling, or the current rate
  while encode is behind cadence). Two consecutive identical short acks latch
  that value as a host cap the climb logic folds into its ceiling — one short
  ack stays a transient (a failed rebuild also acks short once). The cap is
  mode-scoped (cleared on an accepted mode switch, tracked via a mode
  generation counter the control task bumps) and re-probes one step after ~60 s
  parked clean, so a heavy-scene refusal can't quietly cap the whole session.

- Host-encode-latency down-driver: the per-AU 0xCF `encode_us` the host
  already ships (and the overlay already draws) now feeds the controller
  through its own window accumulator — the overlay channel is lossy and
  embedder-drained, so the ABR gets a dedicated mirror of the decode-latency
  path. Baseline-relative like the decode signal (an escalated host reports
  encode_us inflated by ~a frame of queue depth; an absolute budget threshold
  would read permanently red), with the baseline rebased after our own
  decreases so one backoff doesn't train-fire into the floor. This is the only
  signal that can push an already-too-high rate back under the encoder's
  compute knee — host climb refusal stops the climb, but nothing else descends
  on a clean LAN.

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 c1d54b835b
commit f3c3a9427b
6 changed files with 616 additions and 42 deletions
@@ -224,6 +224,20 @@ pub(crate) struct DecodeLatAcc {
pub(crate) count: u32,
}
/// Host encode-stage latency accumulator — [`DecodeLatAcc`]'s mirror for the HOST side of the
/// pipeline. The datagram task adds one sample per 0xCF `HostStages::encode_us` (host encoder
/// submit → bitstream ready) and the pump drains a window mean into
/// [`crate::abr::BitrateController::on_window`]'s encode signal. Host encode time was measured,
/// shipped and drawn on the overlay, but never an ABR input — which is how a fat-LAN Automatic
/// session drove the encoder past its compute knee with nothing to stop it (§ABR overdrive).
/// Its own accumulator rather than the overlay's `host_timing` channel: that channel is a lossy
/// `try_send` the embedder may never drain, and the controller must not depend on it.
#[derive(Default)]
pub(crate) struct EncodeLatAcc {
pub(crate) sum_us: u64,
pub(crate) count: u32,
}
/// The pre-decode video hand-off from the data-plane pump to the embedder. Unlike the side planes
/// (self-contained samples that drop the newest on overflow), video AUs are reference-chained under the
/// host's infinite GOP: dropping ANY frame mid-stream corrupts every dependent frame until the next