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
+20 -3
View File
@@ -909,6 +909,20 @@ pub(super) struct SessionContext {
pub(super) compositor: crate::vdisplay::Compositor,
/// Negotiated encoder bitrate (kbps).
pub(super) bitrate_kbps: u32,
/// The encoder's live APPLIED rate (kbps) — shared with the send pacer, the web console, the
/// mgmt registry AND the control task (which acks climbs against it). The encode loop stores
/// `Encoder::applied_bitrate_bps` here after every apply, so everything downstream tracks
/// what the ASIC really targets, not what was requested (§ABR overdrive).
pub(super) live_bitrate: Arc<AtomicU32>,
/// The encoder's discovered codec-level bitrate ceiling (kbps; 0 = none discovered): written
/// when an apply comes back short, read by this loop (pre-clamp incoming requests — a
/// request already AT the ceiling then costs nothing) and by the control task (truthful
/// acks from the first post-discovery request).
pub(super) encoder_ceiling_kbps: Arc<AtomicU32>,
/// "Encode can't hold the frame cadence" (the escalation leaky bucket is elevated, or the
/// session escalated): while set, the control task refuses bitrate CLIMBS — the network
/// isn't the bottleneck, feeding the encoder more bits deepens the miss.
pub(super) cadence_degraded: Arc<AtomicBool>,
/// The client asked for "Automatic" (`Hello::bitrate_kbps == 0`), so `bitrate_kbps` came from
/// the host's codec-aware default. For PyroWave that default is the ~1.6 bpp operating point of
/// the NEGOTIATED MODE (`resolve_bitrate_kbps_for`) — a mid-stream mode switch re-resolves it
@@ -1035,6 +1049,9 @@ pub(super) fn virtual_stream(ctx: SessionContext, prepared: Option<PreparedDispl
bitrate_rx,
compositor,
mut bitrate_kbps,
live_bitrate,
encoder_ceiling_kbps,
cadence_degraded,
bitrate_auto,
bit_depth,
// The resolved chroma is already captured in `plan` (above); ignore the duplicate here.
@@ -1254,9 +1271,9 @@ pub(super) fn virtual_stream(ctx: SessionContext, prepared: Option<PreparedDispl
// The bounded channel applies backpressure (the encode thread blocks if the send falls behind,
// so frames slow down rather than a dropped frame freezing the infinite-GOP stream).
let (frame_tx, frame_rx) = std::sync::mpsc::sync_channel::<SendMsg>(3);
// Live encoder bitrate, shared with the send thread's stats sample: a mid-stream adaptive
// bitrate change (bitrate_rx below) updates it so the console shows the actual target.
let live_bitrate = Arc::new(AtomicU32::new(bitrate_kbps));
// `live_bitrate` (SessionContext) is shared with the send thread's stats sample AND the
// control task: a mid-stream adaptive bitrate change (bitrate_rx below) stores the
// encoder-APPLIED rate, so the console, pacer and climb-refusal acks all see the truth.
// Live session mode, same pattern (H3): a mid-stream mode switch (reconfig below) updates it so
// a stats capture armed after a resize registers the real mode. Seeded with the refresh the
// initial build actually achieved (`interval_hz`), not the request — KWin may cap a virtual