feat(encode/nvenc-linux): LN1 phase-3 — multi-slice sub-frame encode DEFAULT-ON for Linux direct-NVENC
apple / swift (push) Successful in 1m16s
apple / screenshots (push) Successful in 6m15s
windows-host / package (push) Successful in 9m39s
ci / web (push) Successful in 1m10s
android / android (push) Has been cancelled
arch / build-publish (push) Has been cancelled
ci / docs-site (push) Has been cancelled
ci / rust (push) Has been cancelled
ci / bench (push) Has been cancelled
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.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 (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Has been cancelled
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Has been cancelled
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Has been cancelled
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Has been cancelled
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Has been cancelled

Gates recorded (nvenc-subframe-slice-output.md Phase 3): loss-harness curve
identical streamed-vs-whole; adversarial security review clean; live .21
3-leg A/B — streamed 0 corrupted frames, p99 8527→5363 µs vs sliced-whole,
first_slice_us ~500 µs of a ~1250 µs encode reaching the send thread; wire
bytes rate-governed under CBR + 1-frame VBV (the ~1-2 % slice-header cost is
absorbed by rate control, not added to the wire). GameStream: plane has zero
code diff (own RTP packetizer; shares only untouched send_pacing); a live
Moonlight re-test joins the standing owed Moonlight item.

- resolve_slices(codec, default): env 1..=32 wins (1 = the NEW explicit
  single-slice escape), else the backend default — 4 on Linux direct-NVENC,
  1 elsewhere. resolve_subframe(default_on): PUNKTFUNK_NVENC_SUBFRAME
  tri-state (0 = never, 1 = force, unset = backend default).
- Linux nvenc_cuda: resolves both ONCE in query_caps — the sub-frame default
  is gated on the GPU's SUBFRAME_READBACK cap so an unsupporting GPU never
  has enableSubFrameWrite forced into its init params — and feeds the same
  resolved values to build_config, build_init_params (open AND in-place
  reconfigure) and the chunked-poll latch.
- Windows: env-only as before (async path untouched, byte-identical config
  for unset env); LowLatencyConfig.slices + the explicit subframe param
  replace the shared env reads.
- Tests: chunked e2e now runs at the DEFAULTS (no knobs); the fallback test
  becomes the escape test (SLICES=1 disarms; SUBFRAME=0 disarms while
  4-slice encode continues on the plain poll path).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-20 23:27:50 +02:00
parent e6cdd79f71
commit 67b7981096
3 changed files with 130 additions and 61 deletions
+38 -22
View File
@@ -35,26 +35,36 @@ 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
/// Resolved per-frame slice count for a session (latency plan §7 LN1, Phase 3): the
/// `PUNKTFUNK_NVENC_SLICES` env override wins (1..=32; **1 = the explicit single-slice
/// escape**, needed now that a backend can default higher), else the backend's
/// `default_slices` — 4 on Linux direct-NVENC since the Phase-3 default-on, 1 everywhere else
/// (the Windows async path is deliberately untouched). H.264/HEVC only (AV1 partitions via
/// tiles). ONE parse shared by the config author ([`apply_low_latency_config`] via
/// [`LowLatencyConfig::slices`]) 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> {
pub(super) fn resolve_slices(codec: Codec, default_slices: u32) -> u32 {
if !matches!(codec, Codec::H264 | Codec::H265) {
return None;
return 1;
}
std::env::var("PUNKTFUNK_NVENC_SLICES")
.ok()
.and_then(|s| s.parse::<u32>().ok())
.filter(|n| (2..=32).contains(n))
.filter(|n| (1..=32).contains(n))
.unwrap_or(default_slices)
}
/// `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")
/// Resolved sub-frame readback (`enableSubFrameWrite` + `reportSliceOffsets`; sync sessions
/// only, see [`build_init_params`]): `PUNKTFUNK_NVENC_SUBFRAME` tri-state — `0` = never (the
/// default-on escape), `1` = force (even where the caps probe says unsupported — an operator
/// explicitly testing), unset = the backend's `default_on` (Linux direct-NVENC passes its
/// SUBFRAME_READBACK caps-probe result since Phase 3; Windows passes `false`).
pub(super) fn resolve_subframe(default_on: bool) -> bool {
match std::env::var("PUNKTFUNK_NVENC_SUBFRAME").as_deref() {
Ok("0") => false,
Ok("1") => true,
_ => default_on,
}
}
/// Reference-frame DPB depth when RFI is supported (Apollo uses 5). A deeper DPB lets an invalidated
@@ -88,6 +98,9 @@ pub(super) struct LowLatencyConfig {
pub hdr: bool,
/// This GPU supports reference-frame invalidation (a deeper DPB for graceful loss recovery).
pub rfi_supported: bool,
/// Resolved per-frame slice count ([`resolve_slices`] — env override, else the backend
/// default). ≤ 1 leaves the preset's single slice untouched.
pub slices: u32,
}
/// Author the shared `NV_ENC_INITIALIZE_PARAMS` (P1/ULL preset, PTD, the session dimensions/rate)
@@ -104,6 +117,7 @@ pub(super) fn build_init_params(
cfg: &mut nv::NV_ENC_CONFIG,
split_mode: u32,
enable_async: bool,
subframe: bool,
) -> nv::NV_ENC_INITIALIZE_PARAMS {
let mut init = nv::NV_ENC_INITIALIZE_PARAMS {
version: nv::NV_ENC_INITIALIZE_PARAMS_VER,
@@ -123,12 +137,13 @@ pub(super) fn build_init_params(
};
// splitEncodeMode is a C bitfield — set via the generated accessor, not a struct field.
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 && subframe_requested() {
// Sub-frame readback (latency plan §7 LN1; default-on for Linux direct-NVENC since Phase 3 —
// the caller resolves `subframe` via [`resolve_subframe`] + its caps probe): 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
// multi-slice (a single-slice frame yields nothing to read early). `reportSliceOffsets`
// requires `enableEncodeAsync = 0`, so async (Windows) sessions never arm.
if !enable_async && subframe {
init.set_enableSubFrameWrite(1);
init.set_reportSliceOffsets(1);
}
@@ -180,12 +195,13 @@ 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"),
}
// 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
// Multi-slice frames (latency plan §7 LN1): `c.slices` 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) = slices_env(c.codec) {
// only — AV1 partitions via tiles, not slices (the resolver already returns 1 there).
// Default 4 on Linux direct-NVENC (Phase 3), env-only elsewhere; ≤ 1 keeps the preset's
// single slice.
if let Some(n) = Some(c.slices).filter(|n| *n >= 2) {
match c.codec {
Codec::H264 => {
cfg.encodeCodecConfig.h264Config.sliceMode = 3;