fix(encode/nvenc): cache the level ceiling, expose the applied bitrate, force 2-way split at 4K120

Three encode-time fixes from the 4K120 field analysis (sessions pinned ~107 of
120 fps, 14-15 ms reported encode):

- Ceiling truth: the codec-level bitrate clamp (binary search at session open)
  now records its result in a process-lifetime advisory cache keyed by
  GPU/config, so an ABR overshoot opens — or in-place reconfigures — straight
  AT the ceiling instead of re-running the ~6-open search (and a full rebuild
  + IDR) on every overshoot. New `Encoder::applied_bitrate_bps()` exposes the
  post-clamp rate so the session loop can stop pacing/acking a phantom
  requested rate (consumed host-side in a follow-up).

- Clamp-search hygiene: only NVENC parameter/caps rejections steer the search
  now (`NvCallError` keeps the raw status downcastable); a transient failure
  (busy engine, session limit, OOM, driver skew) propagates instead of
  shrinking into — and now caching — a bogus ceiling. The floor fallback also
  records the split mode it actually opened with, so a later reconfigure
  re-presents the real session params.

- Split-frame selection: one shared `resolve_split_mode` replaces the two
  byte-identical direct-SDK copies; the force-2-way threshold moves to
  `SPLIT_FORCE_PIXEL_RATE` (950 Mpix/s, shared with the libav path) because
  4K120 = 995,328,000 px/s missed the old `> 1e9` gate by 0.47% and stayed on
  AUTO — which never engages at 2160 px height, leaving the second NVENC
  engine idle in exactly the mode the threshold existed for. The Linux
  session-ready info! line now carries the final split mode (journals are
  INFO+; this was undiagnosable from user logs).

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 42e5f5ad1e
commit c1d54b835b
10 changed files with 498 additions and 111 deletions
+18
View File
@@ -985,6 +985,18 @@ async fn serve_session(
// full IDR when the encoder supports it (native-AMF LTR / Windows NVENC).
let (rfi_tx, rfi_rx) = std::sync::mpsc::channel::<(u32, u32)>();
let (bitrate_tx, bitrate_rx) = std::sync::mpsc::channel::<u32>();
// Encoder-truth bridge, data plane → control task (§ABR overdrive). The encode loop publishes
// here; the control task reads at `SetBitrate`-resolve time, so the ack the client's
// controller climbs from tracks what the encoder ACTUALLY does, not what was asked:
// - `live_bitrate`: the encoder's applied rate (kbps) — also the send pacer's/console's view.
// - `encoder_ceiling_kbps`: the discovered codec-level ceiling (0 = none discovered yet);
// resolves land at min(policy clamp, ceiling), so overshoots stop costing rebuilds.
// - `cadence_degraded`: encode can't hold the frame cadence — a climb is refused (acked at
// the current rate); the network isn't the bottleneck, more bits are anti-medicine.
// Plain atomics, not a channel: only the freshest value matters, and only at resolve time.
let live_bitrate = Arc::new(AtomicU32::new(welcome.bitrate_kbps));
let encoder_ceiling_kbps = Arc::new(AtomicU32::new(0));
let cadence_degraded = Arc::new(AtomicBool::new(false));
let (probe_tx, probe_rx) = std::sync::mpsc::channel::<ProbeRequest>();
let (probe_result_tx, probe_result_rx) = tokio::sync::mpsc::unbounded_channel::<ProbeResult>();
// Mode-switch outcome, data plane → control task (same pattern as `probe_result_tx`): the accept
@@ -1036,6 +1048,9 @@ async fn serve_session(
live_reconfig_ok,
adaptive_fec,
session_bitrate_kbps,
live_bitrate.clone(),
encoder_ceiling_kbps.clone(),
cadence_degraded.clone(),
fec_target_ctl,
reconfig_tx,
keyframe_tx,
@@ -1429,6 +1444,9 @@ async fn serve_session(
bitrate_rx,
compositor,
bitrate_kbps,
live_bitrate,
encoder_ceiling_kbps,
cadence_degraded,
bitrate_auto,
bit_depth,
chroma,