feat(encode/nvenc): LN1 phase-1 — slice-boundary chunked encoder poll (poll_chunk/AuChunk)
apple / swift (push) Successful in 1m20s
apple / screenshots (push) Successful in 6m24s
windows-host / package (push) Successful in 10m45s
android / android (push) Has been cancelled
arch / build-publish (push) Has been cancelled
ci / docs-site (push) Has been cancelled
ci / bench (push) Has been cancelled
ci / rust (push) Has been cancelled
ci / web (push) Has been cancelled
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 11s
deb / build-publish (push) Has been cancelled
deb / build-publish-host (push) Has been cancelled
decky / build-publish (push) Has been cancelled
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Has been cancelled
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Has been cancelled
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Has been cancelled
docker / deploy-docs (push) Has been cancelled
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Has been cancelled
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Has been cancelled
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled

The encoder half of sub-frame slice output (latency §7 LN1, planning
design/nvenc-subframe-slice-output.md Phase 1): with PUNKTFUNK_NVENC_SLICES=N +
PUNKTFUNK_NVENC_SUBFRAME=1 on a sync depth-1 session, Encoder::poll_chunk hands
the in-flight AU out as slice-boundary chunks read through doNotWait sub-frame
locks while the tail is still encoding — the readback loop the on-hw probe
validated (~200 µs slice spacing on the 5070 Ti), productionized.

- codec.rs: AuChunk (AU metadata on the first chunk, `last` closes the AU;
  chunks concatenate to exactly the poll() bytes) + supports_chunked_poll /
  poll_chunk trait surface. Default impl wraps poll() as one self-closing
  chunk, so a chunk-driven consumer works against every backend.
- nvenc_cuda: chunked readback cut at slice boundaries only (bitstream size at
  n reported slices = end of slice n, Annex-B contiguous); completion is NEVER
  numSlices alone — one finishing BLOCKING lock is the authority and the wedge
  watchdog, so the final chunk blocks exactly like sync poll (the depth-1 pump
  contract; 6dc195f9 bug class). Keyframe on early chunks is the submit-time
  IDR prediction (exact under P-only/infinite-GOP), cross-checked at finish.
  Debug builds shadow-check emitted chunks against the finished AU prefix.
  Mutually exclusive with pipelined retrieve (gated off when async_rt exists,
  dropped by the escalation rebuild); composes with stream-ordered submit.
- nvenc_core: slices_env/subframe_requested shared parses so the config author
  and the chunked-poll arming can't disagree.
- TrackedEncoder forwards both new methods (the set_wire_chunking trap class).

Host loop untouched — Phase 2 (VIDEO_CAP_STREAMED_AU seal/send) consumes this.
On-hw: nvenc_cuda_chunked_poll_end_to_end + nvenc_cuda_chunked_poll_fallback_whole_au.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-20 20:50:04 +02:00
parent 411c6dc06a
commit 9d3b114fd6
4 changed files with 512 additions and 16 deletions
+24 -6
View File
@@ -35,6 +35,28 @@ pub(super) fn codec_guid(codec: Codec) -> nv::GUID {
}
}
/// `PUNKTFUNK_NVENC_SLICES` — the per-frame slice count (2..=32) for multi-slice encode (latency
/// plan §7 LN1), H.264/HEVC only (AV1 partitions via tiles, so the knob never applies there).
/// `None` = the preset's default single slice. ONE parse shared by the config author
/// ([`apply_low_latency_config`]) and the Linux backend's chunked-poll arming, so the two can
/// never disagree about whether a session is multi-slice.
pub(super) fn slices_env(codec: Codec) -> Option<u32> {
if !matches!(codec, Codec::H264 | Codec::H265) {
return None;
}
std::env::var("PUNKTFUNK_NVENC_SLICES")
.ok()
.and_then(|s| s.parse::<u32>().ok())
.filter(|n| (2..=32).contains(n))
}
/// `PUNKTFUNK_NVENC_SUBFRAME=1` — arm sub-frame readback (`enableSubFrameWrite` +
/// `reportSliceOffsets`; sync sessions only, see [`build_init_params`]). Shared for the same
/// reason as [`slices_env`].
pub(super) fn subframe_requested() -> bool {
std::env::var("PUNKTFUNK_NVENC_SUBFRAME").as_deref() == Ok("1")
}
/// Reference-frame DPB depth when RFI is supported (Apollo uses 5). A deeper DPB lets an invalidated
/// reference fall back to an older still-valid frame instead of a full IDR; `numRefL0 = 1` keeps each
/// P-frame single-reference for low latency. Also the window the backends' `invalidate_ref_frames`
@@ -106,7 +128,7 @@ pub(super) fn build_init_params(
// 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") {
if !enable_async && subframe_requested() {
init.set_enableSubFrameWrite(1);
init.set_reportSliceOffsets(1);
}
@@ -163,11 +185,7 @@ pub(super) unsafe fn apply_low_latency_config(cfg: &mut nv::NV_ENC_CONFIG, c: Lo
// (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))
{
if let Some(n) = slices_env(c.codec) {
match c.codec {
Codec::H264 => {
cfg.encodeCodecConfig.h264Config.sliceMode = 3;