perf(encode/nvenc-linux): LN0 — sampled submit/retrieve-lock split, sub-frame caps, zeroReorderDelay
Latency plan §7 LN0 (Linux/NVIDIA encode follow-on): - sampled PUNKTFUNK_PERF submit split (copy/blend/map/pic) in nvenc_cuda — the host loop's submit_us folds all four together; the D2D input copy is the LN2 zero-copy target and now measurable on its own - sampled blocking lock_bitstream timing on pf-nvenc-out — in two-thread mode the host loop's wait_us wraps a non-blocking poll, so the real encode wait was measured by no timer - caps probe + log SUPPORT_SUBFRAME_READBACK / SUPPORT_DYNAMIC_SLICE_MODE (LN1 sub-frame slice-output prerequisites, fleet visibility) - explicit zeroReorderDelay=1 in the shared low-latency config (P-only + no lookahead has no reordering anyway; pins the bit against preset or driver drift; shared with the Windows backend) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -302,7 +302,14 @@ fn retrieve_loop(
|
|||||||
if let Err(e) = cuda::make_current() {
|
if let Err(e) = cuda::make_current() {
|
||||||
tracing::warn!(error = %format!("{e:#}"), "pf-nvenc-out: cuCtxSetCurrent failed");
|
tracing::warn!(error = %format!("{e:#}"), "pf-nvenc-out: cuCtxSetCurrent failed");
|
||||||
}
|
}
|
||||||
|
let mut jobs: u64 = 0;
|
||||||
while let Ok(job) = work_rx.recv() {
|
while let Ok(job) = work_rx.recv() {
|
||||||
|
// In two-thread mode the host loop's `wait_us` wraps a non-blocking poll, so the real
|
||||||
|
// encode wait (scheduling + ASIC) is measured by NO timer there — sample it here instead
|
||||||
|
// (same PUNKTFUNK_PERF cadence as the submit split).
|
||||||
|
let sample = pf_host_config::config().perf && jobs % 120 == 0;
|
||||||
|
jobs += 1;
|
||||||
|
let t0 = std::time::Instant::now();
|
||||||
// SAFETY: `job.bs` is one of the session's pool bitstreams a prior `encode_picture`
|
// SAFETY: `job.bs` is one of the session's pool bitstreams a prior `encode_picture`
|
||||||
// targeted; both it and the session stay valid until `teardown`, which joins this thread
|
// targeted; both it and the session stay valid until `teardown`, which joins this thread
|
||||||
// first. `lock_bitstream` (version set, struct a live stack local for the synchronous
|
// first. `lock_bitstream` (version set, struct a live stack local for the synchronous
|
||||||
@@ -341,6 +348,16 @@ fn retrieve_loop(
|
|||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
if sample {
|
||||||
|
if let Ok((data, _)) = &result {
|
||||||
|
tracing::info!(
|
||||||
|
lock_us = t0.elapsed().as_micros() as u64,
|
||||||
|
au_kib = (data.len() / 1024) as u64,
|
||||||
|
"NVENC retrieve lock (sampled): blocking lock_bitstream + AU copy on \
|
||||||
|
pf-nvenc-out (the async-mode encode wait)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
if done_tx.send(RetrieveDone { bs: job.bs, result }).is_err() {
|
if done_tx.send(RetrieveDone { bs: job.bs, result }).is_err() {
|
||||||
break; // encoder side gone (teardown drains us via join)
|
break; // encoder side gone (teardown drains us via join)
|
||||||
}
|
}
|
||||||
@@ -396,6 +413,9 @@ pub struct NvencCudaEncoder {
|
|||||||
/// submit). Empty until the session is initialized.
|
/// submit). Empty until the session is initialized.
|
||||||
ring: Vec<RingSlot>,
|
ring: Vec<RingSlot>,
|
||||||
next: usize,
|
next: usize,
|
||||||
|
/// Frames submitted over the encoder's lifetime (never reset, unlike `next`) — drives the
|
||||||
|
/// sampled `PUNKTFUNK_PERF` submit-split log cadence, mirroring the VAAPI backend's counter.
|
||||||
|
frames: u64,
|
||||||
bitstreams: Vec<nv::NV_ENC_OUTPUT_PTR>,
|
bitstreams: Vec<nv::NV_ENC_OUTPUT_PTR>,
|
||||||
/// (bitstream, mapped input resource to unmap after retrieval, pts_ns, recovery-anchor) per
|
/// (bitstream, mapped input resource to unmap after retrieval, pts_ns, recovery-anchor) per
|
||||||
/// in-flight encode. The fourth field tags the first frame encoded after a successful
|
/// in-flight encode. The fourth field tags the first frame encoded after a successful
|
||||||
@@ -501,6 +521,7 @@ impl NvencCudaEncoder {
|
|||||||
hdr_meta: None,
|
hdr_meta: None,
|
||||||
ring: Vec::new(),
|
ring: Vec::new(),
|
||||||
next: 0,
|
next: 0,
|
||||||
|
frames: 0,
|
||||||
bitstreams: Vec::new(),
|
bitstreams: Vec::new(),
|
||||||
pending: VecDeque::new(),
|
pending: VecDeque::new(),
|
||||||
frame_idx: 0,
|
frame_idx: 0,
|
||||||
@@ -619,6 +640,10 @@ impl NvencCudaEncoder {
|
|||||||
enc,
|
enc,
|
||||||
nv::NV_ENC_CAPS::NV_ENC_CAPS_SUPPORT_CUSTOM_VBV_BUF_SIZE,
|
nv::NV_ENC_CAPS::NV_ENC_CAPS_SUPPORT_CUSTOM_VBV_BUF_SIZE,
|
||||||
);
|
);
|
||||||
|
// Sub-frame-output prerequisites (latency plan §7 LN1): logged for fleet visibility now,
|
||||||
|
// consumed when slice-level readback lands. Not stored — LN1 re-probes when it configures.
|
||||||
|
let subframe = self.get_cap(enc, nv::NV_ENC_CAPS::NV_ENC_CAPS_SUPPORT_SUBFRAME_READBACK);
|
||||||
|
let dyn_slice = self.get_cap(enc, nv::NV_ENC_CAPS::NV_ENC_CAPS_SUPPORT_DYNAMIC_SLICE_MODE);
|
||||||
let _ = (api().destroy_encoder)(enc);
|
let _ = (api().destroy_encoder)(enc);
|
||||||
|
|
||||||
if wmax > 0 && hmax > 0 && (self.width as i32 > wmax || self.height as i32 > hmax) {
|
if wmax > 0 && hmax > 0 && (self.width as i32 > wmax || self.height as i32 > hmax) {
|
||||||
@@ -641,6 +666,8 @@ impl NvencCudaEncoder {
|
|||||||
rfi = self.rfi_supported,
|
rfi = self.rfi_supported,
|
||||||
custom_vbv = self.custom_vbv,
|
custom_vbv = self.custom_vbv,
|
||||||
yuv444 = self.yuv444_supported,
|
yuv444 = self.yuv444_supported,
|
||||||
|
subframe_readback = subframe != 0,
|
||||||
|
dynamic_slice = dyn_slice != 0,
|
||||||
max = %format!("{wmax}x{hmax}"),
|
max = %format!("{wmax}x{hmax}"),
|
||||||
"NVENC (Linux direct) capabilities probed"
|
"NVENC (Linux direct) capabilities probed"
|
||||||
);
|
);
|
||||||
@@ -1115,8 +1142,18 @@ impl Encoder for NvencCudaEncoder {
|
|||||||
let slot = self.next % POOL;
|
let slot = self.next % POOL;
|
||||||
self.next += 1;
|
self.next += 1;
|
||||||
|
|
||||||
|
// Sampled breakdown of the submit hot path under PUNKTFUNK_PERF (~1 line per 2 s at
|
||||||
|
// 60 fps, the VAAPI submit-split convention): copy = the per-frame device→device input
|
||||||
|
// copy (the zero-copy-registration target), blend = cursor overlay kernel (0 without a
|
||||||
|
// cursor), map/pic = the NVENC map_input_resource / encode_picture launches. The host
|
||||||
|
// loop's `submit_us` folds all four together; this is what splits them apart.
|
||||||
|
let sample = pf_host_config::config().perf && self.frames % 120 == 0;
|
||||||
|
self.frames += 1;
|
||||||
|
let t0 = std::time::Instant::now();
|
||||||
|
|
||||||
// Copy the captured buffer into this slot's input surface before encoding it.
|
// Copy the captured buffer into this slot's input surface before encoding it.
|
||||||
self.copy_into_slot(buf, slot)?;
|
self.copy_into_slot(buf, slot)?;
|
||||||
|
let t_copy = t0.elapsed();
|
||||||
|
|
||||||
// Cursor-as-metadata: blend the overlay into this slot's OWNED input surface (a tiny kernel
|
// Cursor-as-metadata: blend the overlay into this slot's OWNED input surface (a tiny kernel
|
||||||
// over the cursor's rect — never the compositor's dmabuf). The PTX module loads lazily on the
|
// over the cursor's rect — never the compositor's dmabuf). The PTX module loads lazily on the
|
||||||
@@ -1180,6 +1217,9 @@ impl Encoder for NvencCudaEncoder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let t_blend = t0.elapsed() - t_copy;
|
||||||
|
let t_map: std::time::Duration;
|
||||||
|
let t_pic: std::time::Duration;
|
||||||
// SAFETY: every NVENC call goes through a function pointer from the runtime table and takes
|
// SAFETY: every NVENC call goes through a function pointer from the runtime table and takes
|
||||||
// `self.encoder`, the live session `init_session` established (non-null here). `mp`
|
// `self.encoder`, the live session `init_session` established (non-null here). `mp`
|
||||||
// (`NV_ENC_MAP_INPUT_RESOURCE`, version set) maps the ring slot's registration (created in
|
// (`NV_ENC_MAP_INPUT_RESOURCE`, version set) maps the ring slot's registration (created in
|
||||||
@@ -1196,9 +1236,11 @@ impl Encoder for NvencCudaEncoder {
|
|||||||
registeredResource: reg,
|
registeredResource: reg,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
let tm = std::time::Instant::now();
|
||||||
(api().map_input_resource)(self.encoder, &mut mp)
|
(api().map_input_resource)(self.encoder, &mut mp)
|
||||||
.nv_ok()
|
.nv_ok()
|
||||||
.map_err(|e| nvenc_status::call_err("map_input_resource", e))?;
|
.map_err(|e| nvenc_status::call_err("map_input_resource", e))?;
|
||||||
|
t_map = tm.elapsed();
|
||||||
|
|
||||||
let pts = self.frame_idx as u64;
|
let pts = self.frame_idx as u64;
|
||||||
self.frame_idx += 1;
|
self.frame_idx += 1;
|
||||||
@@ -1268,9 +1310,11 @@ impl Encoder for NvencCudaEncoder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let tp = std::time::Instant::now();
|
||||||
(api().encode_picture)(self.encoder, &mut pic)
|
(api().encode_picture)(self.encoder, &mut pic)
|
||||||
.nv_ok()
|
.nv_ok()
|
||||||
.map_err(|e| nvenc_status::call_err("encode_picture", e))?;
|
.map_err(|e| nvenc_status::call_err("encode_picture", e))?;
|
||||||
|
t_pic = tp.elapsed();
|
||||||
self.pending.push_back((
|
self.pending.push_back((
|
||||||
self.bitstreams[slot],
|
self.bitstreams[slot],
|
||||||
mp.mappedResource,
|
mp.mappedResource,
|
||||||
@@ -1278,6 +1322,16 @@ impl Encoder for NvencCudaEncoder {
|
|||||||
anchor,
|
anchor,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
if sample {
|
||||||
|
tracing::info!(
|
||||||
|
copy_us = t_copy.as_micros() as u64,
|
||||||
|
blend_us = t_blend.as_micros() as u64,
|
||||||
|
map_us = t_map.as_micros() as u64,
|
||||||
|
pic_us = t_pic.as_micros() as u64,
|
||||||
|
"NVENC submit split (sampled): copy=input D2D copy blend=cursor map=map_input \
|
||||||
|
pic=encode_picture launch"
|
||||||
|
);
|
||||||
|
}
|
||||||
// Two-thread mode: hand the blocking lock for this bitstream to the retrieve thread.
|
// Two-thread mode: hand the blocking lock for this bitstream to the retrieve thread.
|
||||||
// The sync_channel(POOL) can never fill (in-flight is capped < POOL above).
|
// The sync_channel(POOL) can never fill (in-flight is capped < POOL above).
|
||||||
if let Some(rt) = &self.async_rt {
|
if let Some(rt) = &self.async_rt {
|
||||||
|
|||||||
@@ -118,6 +118,9 @@ pub(super) unsafe fn apply_low_latency_config(cfg: &mut nv::NV_ENC_CONFIG, c: Lo
|
|||||||
cfg.gopLength = nv::NVENC_INFINITE_GOPLENGTH;
|
cfg.gopLength = nv::NVENC_INFINITE_GOPLENGTH;
|
||||||
cfg.frameIntervalP = 1;
|
cfg.frameIntervalP = 1;
|
||||||
cfg.rcParams.rateControlMode = nv::NV_ENC_PARAMS_RC_MODE::NV_ENC_PARAMS_RC_CBR;
|
cfg.rcParams.rateControlMode = nv::NV_ENC_PARAMS_RC_MODE::NV_ENC_PARAMS_RC_CBR;
|
||||||
|
// Explicit zero reorder delay: with P-only + no lookahead there is no reordering to buffer,
|
||||||
|
// but pin the bit so no preset/driver default can ever slip a frame of reorder delay in.
|
||||||
|
cfg.rcParams.set_zeroReorderDelay(1);
|
||||||
let bps = c.bitrate.min(u32::MAX as u64) as u32;
|
let bps = c.bitrate.min(u32::MAX as u64) as u32;
|
||||||
cfg.rcParams.averageBitRate = bps;
|
cfg.rcParams.averageBitRate = bps;
|
||||||
cfg.rcParams.maxBitRate = bps;
|
cfg.rcParams.maxBitRate = bps;
|
||||||
|
|||||||
Reference in New Issue
Block a user