feat(encode/nvenc): LN1 phase-0 — slice-count + sub-frame-readback knobs and the slice-timing probe
apple / swift (push) Successful in 1m16s
apple / screenshots (push) Successful in 6m55s
windows-host / package (push) Successful in 11m18s
ci / web (push) Successful in 54s
ci / docs-site (push) Successful in 56s
ci / bench (push) Successful in 5m19s
android / android (push) Successful in 12m34s
decky / build-publish (push) Successful in 28s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 12s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 12s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 10s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 11s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 12s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 14s
arch / build-publish (push) Successful in 15m25s
deb / build-publish (push) Successful in 11m48s
deb / build-publish-host (push) Successful in 9m33s
ci / rust (push) Successful in 24m38s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 16m29s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m36s
docker / deploy-docs (push) Successful in 10s
apple / swift (push) Successful in 1m16s
apple / screenshots (push) Successful in 6m55s
windows-host / package (push) Successful in 11m18s
ci / web (push) Successful in 54s
ci / docs-site (push) Successful in 56s
ci / bench (push) Successful in 5m19s
android / android (push) Successful in 12m34s
decky / build-publish (push) Successful in 28s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 12s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 12s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 10s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 11s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 12s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 14s
arch / build-publish (push) Successful in 15m25s
deb / build-publish (push) Successful in 11m48s
deb / build-publish-host (push) Successful in 9m33s
ci / rust (push) Successful in 24m38s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 16m29s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m36s
docker / deploy-docs (push) Successful in 10s
PUNKTFUNK_NVENC_SLICES=N (2..=32, default off) splits H.264/HEVC frames into N slices (sliceMode 3); PUNKTFUNK_NVENC_SUBFRAME=1 (default off) arms enableSubFrameWrite + reportSliceOffsets on sync sessions only. Both experimental groundwork for sub-frame slice output (plan §7 LN1). nvenc_cuda_subframe_slice_probe (on-hardware, ignored) answers the LN1 go/no-go: spins lock_bitstream(doNotWait) against an in-flight frame and prints the (t_us, status, numSlices, bytes) timeline — incremental slice availability and its spacing, or all-at-completion. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2089,4 +2089,110 @@ mod tests {
|
|||||||
"the boxed CUstream must be held while armed"
|
"the boxed CUstream must be held while armed"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// ON-HARDWARE (RTX box `.21`), MEASUREMENT probe for latency plan §7 LN1 — answers the
|
||||||
|
/// go/no-go question for sub-frame slice output: with `PUNKTFUNK_NVENC_SLICES=4` +
|
||||||
|
/// `PUNKTFUNK_NVENC_SUBFRAME=1`, do slices become READABLE incrementally while the frame is
|
||||||
|
/// still encoding (and with what spacing), or does the driver only publish them at frame
|
||||||
|
/// completion? Spins `lock_bitstream(doNotWait)` against the in-flight bitstream and prints a
|
||||||
|
/// `(t_us, numSlices, bytes)` timeline. Asserts only the config half (4 slices materialize);
|
||||||
|
/// the timeline is the experiment's output — read it with `--nocapture`. Run single-threaded
|
||||||
|
/// (env vars are process-global): `-- --ignored --test-threads=1`.
|
||||||
|
#[test]
|
||||||
|
#[ignore = "requires an NVIDIA GPU + driver — run manually on the RTX box (.21)"]
|
||||||
|
fn nvenc_cuda_subframe_slice_probe() {
|
||||||
|
const W: u32 = 1920;
|
||||||
|
const H: u32 = 1080;
|
||||||
|
struct EnvGuard;
|
||||||
|
impl Drop for EnvGuard {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
std::env::remove_var("PUNKTFUNK_NVENC_SLICES");
|
||||||
|
std::env::remove_var("PUNKTFUNK_NVENC_SUBFRAME");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::env::set_var("PUNKTFUNK_NVENC_SLICES", "4");
|
||||||
|
std::env::set_var("PUNKTFUNK_NVENC_SUBFRAME", "1");
|
||||||
|
let _guard = EnvGuard;
|
||||||
|
|
||||||
|
pf_zerocopy::cuda::make_current().expect("shared CUDA context current");
|
||||||
|
let mut enc = NvencCudaEncoder::open(
|
||||||
|
Codec::H265,
|
||||||
|
PixelFormat::Nv12,
|
||||||
|
W,
|
||||||
|
H,
|
||||||
|
60,
|
||||||
|
20_000_000,
|
||||||
|
true,
|
||||||
|
8,
|
||||||
|
ChromaFormat::Yuv420,
|
||||||
|
)
|
||||||
|
.expect("open NVENC CUDA session");
|
||||||
|
|
||||||
|
// Frame 0 opens the session (IDR) — drain it normally.
|
||||||
|
let frame = nv12_frame(W, H, 0);
|
||||||
|
enc.submit_indexed(&frame, 0).expect("submit opening frame");
|
||||||
|
enc.poll().expect("poll").expect("opening AU");
|
||||||
|
|
||||||
|
// Frame 1: spin doNotWait locks against the in-flight bitstream BEFORE the blocking poll.
|
||||||
|
let frame = nv12_frame(W, H, 1);
|
||||||
|
enc.submit_indexed(&frame, 1).expect("submit probed frame");
|
||||||
|
let bs = enc.pending.back().expect("in-flight entry").0;
|
||||||
|
let t0 = std::time::Instant::now();
|
||||||
|
let mut timeline: Vec<(u64, nv::NVENCSTATUS, u32, u32)> = Vec::new();
|
||||||
|
let mut offsets = [0u32; 32];
|
||||||
|
loop {
|
||||||
|
let mut lock = nv::NV_ENC_LOCK_BITSTREAM {
|
||||||
|
version: nv::NV_ENC_LOCK_BITSTREAM_VER,
|
||||||
|
outputBitstream: bs,
|
||||||
|
sliceOffsets: offsets.as_mut_ptr(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
lock.set_doNotWait(1);
|
||||||
|
// SAFETY: `bs` is the pool bitstream the just-submitted `encode_picture` targets and
|
||||||
|
// the session is live for the whole test; `lock` (version set, doNotWait) and
|
||||||
|
// `offsets` are live stack locals across the synchronous call; a successful lock is
|
||||||
|
// unlocked before the next iteration reuses the struct. `reportSliceOffsets` was
|
||||||
|
// armed at init so `sliceOffsets` may be written up to `numSlices` ≤ 32 entries
|
||||||
|
// (sliceModeData = 4).
|
||||||
|
let (status, n, bytes) = unsafe {
|
||||||
|
let st = (api().lock_bitstream)(enc.encoder, &mut lock);
|
||||||
|
let ok = st == nv::NVENCSTATUS::NV_ENC_SUCCESS;
|
||||||
|
let (n, b) = if ok {
|
||||||
|
(lock.numSlices, lock.bitstreamSizeInBytes)
|
||||||
|
} else {
|
||||||
|
(0, 0)
|
||||||
|
};
|
||||||
|
if ok {
|
||||||
|
let _ = (api().unlock_bitstream)(enc.encoder, bs);
|
||||||
|
}
|
||||||
|
(st, n, b)
|
||||||
|
};
|
||||||
|
let t_us = t0.elapsed().as_micros() as u64;
|
||||||
|
timeline.push((t_us, status, n, bytes));
|
||||||
|
// A successful doNotWait lock on a COMPLETE frame reports the final slice count; on
|
||||||
|
// LOCK_BUSY the frame is still encoding. Stop once complete (all 4 slices) or after
|
||||||
|
// a generous 50 ms safety window.
|
||||||
|
if (status == nv::NVENCSTATUS::NV_ENC_SUCCESS && n >= 4) || t_us > 50_000 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
std::thread::sleep(std::time::Duration::from_micros(50));
|
||||||
|
}
|
||||||
|
println!("subframe probe timeline (t_us, status, numSlices, bytes):");
|
||||||
|
for (t, st, n, b) in &timeline {
|
||||||
|
println!(" {t:>7} us {st:?} slices={n} bytes={b}");
|
||||||
|
}
|
||||||
|
// Drain the probed frame through the normal path (lock again + unmap) — proves the probe
|
||||||
|
// locks didn't corrupt the session.
|
||||||
|
let au = enc.poll().expect("poll probed frame").expect("probed AU");
|
||||||
|
assert!(!au.data.is_empty(), "probed AU must carry data");
|
||||||
|
let last = timeline.last().expect("at least one sample");
|
||||||
|
assert_eq!(
|
||||||
|
last.2, 4,
|
||||||
|
"4 slices must materialize (PUNKTFUNK_NVENC_SLICES=4 + subframe readback armed)"
|
||||||
|
);
|
||||||
|
// One more frame end-to-end for session health.
|
||||||
|
let frame = nv12_frame(W, H, 2);
|
||||||
|
enc.submit_indexed(&frame, 2).expect("submit follow-up");
|
||||||
|
enc.poll().expect("poll").expect("follow-up AU");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,6 +101,15 @@ pub(super) fn build_init_params(
|
|||||||
};
|
};
|
||||||
// splitEncodeMode is a C bitfield — set via the generated accessor, not a struct field.
|
// splitEncodeMode is a C bitfield — set via the generated accessor, not a struct field.
|
||||||
init.set_splitEncodeMode(split_mode);
|
init.set_splitEncodeMode(split_mode);
|
||||||
|
// Sub-frame readback (latency plan §7 LN1 groundwork — EXPERIMENTAL, default off): the driver
|
||||||
|
// writes each slice into the output buffer as it completes and reports per-slice offsets, so a
|
||||||
|
// sync-mode consumer can read slices out while the frame is still encoding. Pair with
|
||||||
|
// `PUNKTFUNK_NVENC_SLICES` (a single-slice frame yields nothing to read early).
|
||||||
|
// `reportSliceOffsets` requires `enableEncodeAsync = 0`, so async (Windows) sessions never arm.
|
||||||
|
if !enable_async && std::env::var("PUNKTFUNK_NVENC_SUBFRAME").as_deref() == Ok("1") {
|
||||||
|
init.set_enableSubFrameWrite(1);
|
||||||
|
init.set_reportSliceOffsets(1);
|
||||||
|
}
|
||||||
init
|
init
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,6 +158,29 @@ pub(super) unsafe fn apply_low_latency_config(cfg: &mut nv::NV_ENC_CONFIG, c: Lo
|
|||||||
Codec::PyroWave => unreachable!("PyroWave never opens the direct-NVENC backend"),
|
Codec::PyroWave => unreachable!("PyroWave never opens the direct-NVENC backend"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Multi-slice frames (latency plan §7 LN1 groundwork — EXPERIMENTAL, default off = the preset's
|
||||||
|
// single slice): `PUNKTFUNK_NVENC_SLICES=N` (2..=32) splits every frame into N slices
|
||||||
|
// (sliceMode 3 = "N slices per frame"), the unit sub-frame readback ships early and loss
|
||||||
|
// concealment can discard independently. Costs ~1-2 % bitrate in slice headers. H.264/HEVC
|
||||||
|
// only — AV1 partitions via tiles, not slices.
|
||||||
|
if let Some(n) = std::env::var("PUNKTFUNK_NVENC_SLICES")
|
||||||
|
.ok()
|
||||||
|
.and_then(|s| s.parse::<u32>().ok())
|
||||||
|
.filter(|n| (2..=32).contains(n))
|
||||||
|
{
|
||||||
|
match c.codec {
|
||||||
|
Codec::H264 => {
|
||||||
|
cfg.encodeCodecConfig.h264Config.sliceMode = 3;
|
||||||
|
cfg.encodeCodecConfig.h264Config.sliceModeData = n;
|
||||||
|
}
|
||||||
|
Codec::H265 => {
|
||||||
|
cfg.encodeCodecConfig.hevcConfig.sliceMode = 3;
|
||||||
|
cfg.encodeCodecConfig.hevcConfig.sliceModeData = n;
|
||||||
|
}
|
||||||
|
Codec::Av1 | Codec::PyroWave => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Chroma + bit depth. Full-chroma 4:4:4 (HEVC Range Extensions, chromaFormatIDC=3 under the FREXT
|
// Chroma + bit depth. Full-chroma 4:4:4 (HEVC Range Extensions, chromaFormatIDC=3 under the FREXT
|
||||||
// profile) takes precedence and composes with 10-bit (Main 4:4:4 10); it needs a full-chroma-
|
// profile) takes precedence and composes with 10-bit (Main 4:4:4 10); it needs a full-chroma-
|
||||||
// capable input. Otherwise 10-bit selects Main10 (HEVC) or the AV1 output depth — stamping the
|
// capable input. Otherwise 10-bit selects Main10 (HEVC) or the AV1 output depth — stamping the
|
||||||
|
|||||||
Reference in New Issue
Block a user