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:
@@ -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,
|
||||
|
||||
@@ -22,6 +22,13 @@ 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.
|
||||
live_bitrate: Arc<AtomicU32>,
|
||||
encoder_ceiling_kbps: Arc<AtomicU32>,
|
||||
cadence_degraded: Arc<AtomicBool>,
|
||||
fec_target_ctl: Arc<AtomicU8>,
|
||||
reconfig_tx: std::sync::mpsc::Sender<punktfunk_core::Mode>,
|
||||
keyframe_tx: std::sync::mpsc::Sender<()>,
|
||||
@@ -161,7 +168,31 @@ pub(super) async fn run(
|
||||
);
|
||||
session_bitrate_kbps
|
||||
} else {
|
||||
resolve_bitrate_kbps(req.bitrate_kbps)
|
||||
let mut r = resolve_bitrate_kbps(req.bitrate_kbps);
|
||||
// Encoder truth (§ABR overdrive): the ack below is the base the
|
||||
// client's controller climbs from, so it must not promise past the
|
||||
// encoder's discovered codec-level ceiling — the pre-fix path acked
|
||||
// 1.01 Gbps while the ASIC ran 794 Mbps, and the controller climbed
|
||||
// from the phantom number forever (a ~0.6 s rebuild + IDR per step).
|
||||
let ceiling = encoder_ceiling_kbps.load(Ordering::Relaxed);
|
||||
if ceiling != 0 && r > ceiling {
|
||||
r = ceiling;
|
||||
}
|
||||
// Climb refusal while encode can't hold cadence: on a fat LAN no
|
||||
// network signal ever stops the climb, and past the compute knee more
|
||||
// bits only deepen the miss. Resolve a CLIMB to the current applied
|
||||
// rate (descents pass — they're the cure); the short ack teaches the
|
||||
// client controller its ceiling.
|
||||
let live = live_bitrate.load(Ordering::Relaxed);
|
||||
if cadence_degraded.load(Ordering::Relaxed) && live != 0 && r > live {
|
||||
tracing::info!(
|
||||
requested_kbps = req.bitrate_kbps,
|
||||
held_kbps = live,
|
||||
"bitrate climb refused — encode is behind cadence"
|
||||
);
|
||||
r = live;
|
||||
}
|
||||
r
|
||||
};
|
||||
tracing::debug!(
|
||||
requested_kbps = req.bitrate_kbps,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user