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
+82 -38
View File
@@ -44,13 +44,14 @@
//! the retrieve thread the same way it would hang the encode thread today (Linux has no //! the retrieve thread the same way it would hang the encode thread today (Linux has no
//! event-timeout escape) — no regression, just no new watchdog either. //! event-timeout escape) — no regression, just no new watchdog either.
//! //!
//! **Sub-frame chunked poll** (`PUNKTFUNK_NVENC_SLICES=N` + `PUNKTFUNK_NVENC_SUBFRAME=1` — latency //! **Sub-frame chunked poll** (latency plan §7 LN1 — **default-on since Phase 3**: 4 slices +
//! plan §7 LN1 Phase 1): on a sync depth-1 session, [`Encoder::poll_chunk`] hands the in-flight AU //! sub-frame readback on every session whose GPU advertises `SUBFRAME_READBACK`; escapes are
//! out as slice-boundary chunks read through `doNotWait` sub-frame locks while the tail is still //! `PUNKTFUNK_NVENC_SLICES=1` and `PUNKTFUNK_NVENC_SUBFRAME=0`): on a sync depth-1 session,
//! encoding; one final blocking lock closes the AU (the completion authority — `numSlices` alone //! [`Encoder::poll_chunk`] hands the in-flight AU out as slice-boundary chunks read through
//! is not trusted across driver branches). Mutually exclusive with the pipelined retrieve (the //! `doNotWait` sub-frame locks while the tail is still encoding; one final blocking lock closes
//! escalated rebuild drops it); composes with stream-ordered submit (both are sync depth-1 //! the AU (the completion authority — `numSlices` alone is not trusted across driver branches).
//! features). //! Mutually exclusive with the pipelined retrieve (the escalated rebuild drops it); composes
//! with stream-ordered submit (both are sync depth-1 features).
//! //!
//! Needs a real NVIDIA GPU at runtime (session creation fails otherwise); compiles GPU-less and //! Needs a real NVIDIA GPU at runtime (session creation fails otherwise); compiles GPU-less and
//! starts driver-less (the `.so` resolves at runtime — on an AMD/Intel box [`try_api`] fails cleanly //! starts driver-less (the `.so` resolves at runtime — on an AMD/Intel box [`try_api`] fails cleanly
@@ -60,7 +61,7 @@
#![deny(clippy::undocumented_unsafe_blocks)] #![deny(clippy::undocumented_unsafe_blocks)]
use super::nvenc_core::{ use super::nvenc_core::{
apply_low_latency_config, build_init_params, codec_guid, slices_env, subframe_requested, apply_low_latency_config, build_init_params, codec_guid, resolve_slices, resolve_subframe,
LowLatencyConfig, NvStatusExt, RFI_DPB, LowLatencyConfig, NvStatusExt, RFI_DPB,
}; };
use super::nvenc_status; use super::nvenc_status;
@@ -559,9 +560,19 @@ pub struct NvencCudaEncoder {
/// Stream-ordered submit armed for the live session (sync-retrieve mode only; see /// Stream-ordered submit armed for the live session (sync-retrieve mode only; see
/// [`stream_ordered_requested`]). The per-frame gate additionally requires `pending` empty. /// [`stream_ordered_requested`]). The per-frame gate additionally requires `pending` empty.
stream_ordered: bool, stream_ordered: bool,
/// Slice count the live session was configured with (`PUNKTFUNK_NVENC_SLICES`, latched at /// Slice count the live session was configured with ([`resolve_slices`] — env override,
/// init; 1 = the preset's single slice). Chunked poll needs ≥ 2 to have boundaries to cut at. /// else the Linux direct-NVENC default of 4 since Phase 3; 1 = the preset's single slice).
/// Chunked poll needs ≥ 2 to have boundaries to cut at. Latched at init, consumed by
/// `build_config` (so an in-place reconfigure presents the same slicing).
slices: u32, slices: u32,
/// `NV_ENC_CAPS_SUPPORT_SUBFRAME_READBACK` from the caps probe — gates the DEFAULT-on
/// sub-frame arming (an unsupported GPU must not have `enableSubFrameWrite` forced into its
/// init params, which could fail the session open). `PUNKTFUNK_NVENC_SUBFRAME=1` overrides.
subframe_cap: bool,
/// Sub-frame readback resolved for the live session ([`resolve_subframe`] over
/// [`subframe_cap`](Self::subframe_cap)); consumed by every `build_init_params` call so the
/// open and the in-place reconfigure present identical init params.
subframe_on: bool,
/// Sub-frame chunked poll armed for the live session (§7 LN1 Phase 1): multi-slice + /// Sub-frame chunked poll armed for the live session (§7 LN1 Phase 1): multi-slice +
/// sub-frame readback configured AND sync retrieve at init. See [`Encoder::poll_chunk`]. /// sub-frame readback configured AND sync retrieve at init. See [`Encoder::poll_chunk`].
subframe_chunks: bool, subframe_chunks: bool,
@@ -650,6 +661,8 @@ impl NvencCudaEncoder {
io_stream: ptr::null_mut(), io_stream: ptr::null_mut(),
stream_ordered: false, stream_ordered: false,
slices: 1, slices: 1,
subframe_cap: false,
subframe_on: false,
subframe_chunks: false, subframe_chunks: false,
chunk: None, chunk: None,
}) })
@@ -810,6 +823,15 @@ impl NvencCudaEncoder {
} }
self.rfi_supported = rfi != 0; self.rfi_supported = rfi != 0;
self.custom_vbv = custom_vbv != 0; self.custom_vbv = custom_vbv != 0;
self.subframe_cap = subframe != 0;
// Phase-3 default-on (nvenc-subframe-slice-output.md): 4 slices + sub-frame readback on
// every Linux direct-NVENC session, resolved HERE (before the session opens) so the
// config author, the init params and the chunked-poll latch all agree; the caps probe
// gates the sub-frame default so a GPU without SUBFRAME_READBACK never has it forced
// into its init params. PUNKTFUNK_NVENC_SLICES=1 / PUNKTFUNK_NVENC_SUBFRAME=0 are the
// escapes.
self.slices = resolve_slices(self.codec, 4);
self.subframe_on = resolve_subframe(self.subframe_cap);
tracing::info!( tracing::info!(
rfi = self.rfi_supported, rfi = self.rfi_supported,
custom_vbv = self.custom_vbv, custom_vbv = self.custom_vbv,
@@ -923,6 +945,7 @@ impl NvencCudaEncoder {
av1_input_depth_minus8: 0, av1_input_depth_minus8: 0,
hdr: self.hdr, hdr: self.hdr,
rfi_supported: self.rfi_supported, rfi_supported: self.rfi_supported,
slices: self.slices,
}, },
); );
Ok(cfg) Ok(cfg)
@@ -963,6 +986,7 @@ impl NvencCudaEncoder {
&mut cfg, &mut cfg,
split_mode, split_mode,
false, false,
self.subframe_on,
); );
match (api().initialize_encoder)(enc, &mut init).nv_ok() { match (api().initialize_encoder)(enc, &mut init).nv_ok() {
@@ -1186,14 +1210,14 @@ impl NvencCudaEncoder {
} }
} }
} }
// Sub-frame chunked poll (§7 LN1 Phase 1): armed iff this session was CONFIGURED // Sub-frame chunked poll (§7 LN1 Phase 1; default-on since Phase 3): armed iff this
// multi-slice + sub-frame readback (apply_low_latency_config / build_init_params // session was CONFIGURED multi-slice + sub-frame readback (`self.slices` /
// consume the same env parses, so the latch can't disagree with the session config) // `self.subframe_on` were resolved once in `query_caps` and consumed by
// and the retrieve is sync — chunked poll is a depth-1 sync feature; a pipelined // `build_config` / `build_init_params`, so the latch can't disagree with the session
// session's non-blocking poll owns the bitstream from the retrieve thread instead. // config) and the retrieve is sync — chunked poll is a depth-1 sync feature; a
self.slices = slices_env(self.codec).unwrap_or(1); // pipelined session's non-blocking poll owns the bitstream from the retrieve thread
self.subframe_chunks = // instead (the sub-frame write itself stays armed there; it's harmless).
self.slices >= 2 && subframe_requested() && self.async_rt.is_none(); self.subframe_chunks = self.slices >= 2 && self.subframe_on && self.async_rt.is_none();
if self.subframe_chunks { if self.subframe_chunks {
tracing::info!( tracing::info!(
slices = self.slices, slices = self.slices,
@@ -1762,7 +1786,7 @@ impl Encoder for NvencCudaEncoder {
// (version set, doNotWait) and `offsets` are live stack locals across the // (version set, doNotWait) and `offsets` are live stack locals across the
// synchronous call; `reportSliceOffsets` was armed at init so the driver may write // synchronous call; `reportSliceOffsets` was armed at init so the driver may write
// up to `numSlices` ≤ 32 offsets (`sliceModeData` is clamped 2..=32 by // up to `numSlices` ≤ 32 offsets (`sliceModeData` is clamped 2..=32 by
// `slices_env`). On a successful sub-frame lock `bitstreamBufferPtr` holds // `resolve_slices`). On a successful sub-frame lock `bitstreamBufferPtr` holds
// `bitstreamSizeInBytes` readable bytes of COMPLETED slices (enableSubFrameWrite // `bitstreamSizeInBytes` readable bytes of COMPLETED slices (enableSubFrameWrite
// publishes them mid-encode; proven by the on-hw probe) valid until the matching // publishes them mid-encode; proven by the on-hw probe) valid until the matching
// unlock; the emitted range is copied out BEFORE the unlock. Every successful lock // unlock; the emitted range is copied out BEFORE the unlock. Every successful lock
@@ -1934,6 +1958,7 @@ impl Encoder for NvencCudaEncoder {
&mut cfg, &mut cfg,
self.split_mode, self.split_mode,
false, false,
self.subframe_on,
), ),
..Default::default() ..Default::default()
}; };
@@ -2585,10 +2610,11 @@ mod tests {
d.starts_with(&[0, 0, 0, 1]) || d.starts_with(&[0, 0, 1]) d.starts_with(&[0, 0, 0, 1]) || d.starts_with(&[0, 0, 1])
} }
/// ON-HARDWARE (RTX box `.21`): LN1 Phase 1 — the chunked poll end to end. With 4 slices + /// ON-HARDWARE (RTX box `.21`): LN1 Phase 1 — the chunked poll end to end, at the Phase-3
/// sub-frame readback armed, `poll_chunk` must (a) report the mode armed, (b) hand every AU /// DEFAULTS (no env knobs: 4 slices + sub-frame readback arm on their own on a
/// out as chunks whose first chunk opens the AU with the right metadata and whose `last` /// SUBFRAME_READBACK-capable GPU). `poll_chunk` must (a) report the mode armed, (b) hand
/// closes it, (c) cut every chunk at an Annex-B start code, and (d) reassemble byte- /// every AU out as chunks whose first chunk opens the AU with the right metadata and whose
/// `last` closes it, (c) cut every chunk at an Annex-B start code, and (d) reassemble byte-
/// identically to the finishing blocking lock's AU (enforced by the debug-build shadow check /// identically to the finishing blocking lock's AU (enforced by the debug-build shadow check
/// inside `poll_chunk` — a mismatch errors the test). At least one frame must actually chunk /// inside `poll_chunk` — a mismatch errors the test). At least one frame must actually chunk
/// (>1 chunk) — the 5070 Ti probe shows every frame does. Run single-threaded (env vars are /// (>1 chunk) — the 5070 Ti probe shows every frame does. Run single-threaded (env vars are
@@ -2598,16 +2624,9 @@ mod tests {
fn nvenc_cuda_chunked_poll_end_to_end() { fn nvenc_cuda_chunked_poll_end_to_end() {
const W: u32 = 1920; const W: u32 = 1920;
const H: u32 = 1080; const H: u32 = 1080;
struct EnvGuard; // Defaults under test — make sure another test's knobs aren't leaking in.
impl Drop for EnvGuard { std::env::remove_var("PUNKTFUNK_NVENC_SLICES");
fn drop(&mut self) { std::env::remove_var("PUNKTFUNK_NVENC_SUBFRAME");
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"); pf_zerocopy::cuda::make_current().expect("shared CUDA context current");
let mut enc = NvencCudaEncoder::open( let mut enc = NvencCudaEncoder::open(
@@ -2690,7 +2709,8 @@ mod tests {
assert!(!au.data.is_empty()); assert!(!au.data.is_empty());
} }
/// ON-HARDWARE (RTX box `.21`): without the slice/subframe knobs, `poll_chunk` must degrade /// ON-HARDWARE (RTX box `.21`): the Phase-3 default-on ESCAPES — `PUNKTFUNK_NVENC_SLICES=1`
/// must fully disarm chunked poll (and `poll_chunk` degrades
/// to exactly one self-closing whole-AU chunk (the default-path contract every non-chunked /// to exactly one self-closing whole-AU chunk (the default-path contract every non-chunked
/// session shares). Run with `--test-threads=1` (env vars are process-global). /// session shares). Run with `--test-threads=1` (env vars are process-global).
#[test] #[test]
@@ -2698,16 +2718,25 @@ mod tests {
fn nvenc_cuda_chunked_poll_fallback_whole_au() { fn nvenc_cuda_chunked_poll_fallback_whole_au() {
const W: u32 = 1280; const W: u32 = 1280;
const H: u32 = 720; const H: u32 = 720;
// Belt-and-braces: another test in this process may have set the knobs (env is global). struct EnvGuard;
std::env::remove_var("PUNKTFUNK_NVENC_SLICES"); impl Drop for EnvGuard {
std::env::remove_var("PUNKTFUNK_NVENC_SUBFRAME"); fn drop(&mut self) {
std::env::remove_var("PUNKTFUNK_NVENC_SLICES");
std::env::remove_var("PUNKTFUNK_NVENC_SUBFRAME");
}
}
let _guard = EnvGuard;
pf_zerocopy::cuda::make_current().expect("shared CUDA context current"); pf_zerocopy::cuda::make_current().expect("shared CUDA context current");
// Escape 1: explicit single slice — no boundaries to cut, chunked poll disarmed.
std::env::set_var("PUNKTFUNK_NVENC_SLICES", "1");
std::env::remove_var("PUNKTFUNK_NVENC_SUBFRAME");
let mut enc = open_h265(); let mut enc = open_h265();
let frame = nv12_frame(W, H, 0); let frame = nv12_frame(W, H, 0);
enc.submit_indexed(&frame, 0).expect("submit"); enc.submit_indexed(&frame, 0).expect("submit");
assert!( assert!(
!enc.supports_chunked_poll(), !enc.supports_chunked_poll(),
"no knobs → chunked poll must not arm" "PUNKTFUNK_NVENC_SLICES=1 → chunked poll must not arm"
); );
let c = enc let c = enc
.poll_chunk() .poll_chunk()
@@ -2720,5 +2749,20 @@ mod tests {
enc.poll_chunk().expect("poll_chunk").is_none(), enc.poll_chunk().expect("poll_chunk").is_none(),
"nothing in flight → None" "nothing in flight → None"
); );
drop(enc);
// Escape 2: sub-frame readback vetoed — slices stay (default 4) but chunked poll
// disarms and the plain poll path carries the session.
std::env::remove_var("PUNKTFUNK_NVENC_SLICES");
std::env::set_var("PUNKTFUNK_NVENC_SUBFRAME", "0");
let mut enc = open_h265();
let frame = nv12_frame(W, H, 0);
enc.submit_indexed(&frame, 0).expect("submit");
assert!(
!enc.supports_chunked_poll(),
"PUNKTFUNK_NVENC_SUBFRAME=0 → chunked poll must not arm"
);
let au = enc.poll().expect("poll").expect("AU");
assert!(au.keyframe && !au.data.is_empty());
} }
} }
+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 /// Resolved per-frame slice count for a session (latency plan §7 LN1, Phase 3): the
/// plan §7 LN1), H.264/HEVC only (AV1 partitions via tiles, so the knob never applies there). /// `PUNKTFUNK_NVENC_SLICES` env override wins (1..=32; **1 = the explicit single-slice
/// `None` = the preset's default single slice. ONE parse shared by the config author /// escape**, needed now that a backend can default higher), else the backend's
/// ([`apply_low_latency_config`]) and the Linux backend's chunked-poll arming, so the two can /// `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. /// 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) { if !matches!(codec, Codec::H264 | Codec::H265) {
return None; return 1;
} }
std::env::var("PUNKTFUNK_NVENC_SLICES") std::env::var("PUNKTFUNK_NVENC_SLICES")
.ok() .ok()
.and_then(|s| s.parse::<u32>().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` + /// Resolved sub-frame readback (`enableSubFrameWrite` + `reportSliceOffsets`; sync sessions
/// `reportSliceOffsets`; sync sessions only, see [`build_init_params`]). Shared for the same /// only, see [`build_init_params`]): `PUNKTFUNK_NVENC_SUBFRAME` tri-state — `0` = never (the
/// reason as [`slices_env`]. /// default-on escape), `1` = force (even where the caps probe says unsupported — an operator
pub(super) fn subframe_requested() -> bool { /// explicitly testing), unset = the backend's `default_on` (Linux direct-NVENC passes its
std::env::var("PUNKTFUNK_NVENC_SUBFRAME").as_deref() == Ok("1") /// 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 /// 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, pub hdr: bool,
/// This GPU supports reference-frame invalidation (a deeper DPB for graceful loss recovery). /// This GPU supports reference-frame invalidation (a deeper DPB for graceful loss recovery).
pub rfi_supported: bool, 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) /// 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, cfg: &mut nv::NV_ENC_CONFIG,
split_mode: u32, split_mode: u32,
enable_async: bool, enable_async: bool,
subframe: bool,
) -> nv::NV_ENC_INITIALIZE_PARAMS { ) -> nv::NV_ENC_INITIALIZE_PARAMS {
let mut init = nv::NV_ENC_INITIALIZE_PARAMS { let mut init = nv::NV_ENC_INITIALIZE_PARAMS {
version: nv::NV_ENC_INITIALIZE_PARAMS_VER, 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. // 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 // Sub-frame readback (latency plan §7 LN1; default-on for Linux direct-NVENC since Phase 3 —
// writes each slice into the output buffer as it completes and reports per-slice offsets, so a // the caller resolves `subframe` via [`resolve_subframe`] + its caps probe): the driver
// sync-mode consumer can read slices out while the frame is still encoding. Pair with // writes each slice into the output buffer as it completes and reports per-slice offsets, so
// `PUNKTFUNK_NVENC_SLICES` (a single-slice frame yields nothing to read early). // a sync-mode consumer can read slices out while the frame is still encoding. Pair with
// `reportSliceOffsets` requires `enableEncodeAsync = 0`, so async (Windows) sessions never arm. // multi-slice (a single-slice frame yields nothing to read early). `reportSliceOffsets`
if !enable_async && subframe_requested() { // requires `enableEncodeAsync = 0`, so async (Windows) sessions never arm.
if !enable_async && subframe {
init.set_enableSubFrameWrite(1); init.set_enableSubFrameWrite(1);
init.set_reportSliceOffsets(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"), Codec::PyroWave => unreachable!("PyroWave never opens the direct-NVENC backend"),
} }
// Multi-slice frames (latency plan §7 LN1 groundwork — EXPERIMENTAL, default off = the preset's // Multi-slice frames (latency plan §7 LN1): `c.slices` splits every frame into N slices
// 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 // (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 // concealment can discard independently. Costs ~1-2 % bitrate in slice headers. H.264/HEVC
// only — AV1 partitions via tiles, not slices. // only — AV1 partitions via tiles, not slices (the resolver already returns 1 there).
if let Some(n) = slices_env(c.codec) { // 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 { match c.codec {
Codec::H264 => { Codec::H264 => {
cfg.encodeCodecConfig.h264Config.sliceMode = 3; cfg.encodeCodecConfig.h264Config.sliceMode = 3;
+10 -1
View File
@@ -37,7 +37,8 @@
#![deny(clippy::undocumented_unsafe_blocks)] #![deny(clippy::undocumented_unsafe_blocks)]
use super::nvenc_core::{ use super::nvenc_core::{
apply_low_latency_config, build_init_params, codec_guid, LowLatencyConfig, NvStatusExt, RFI_DPB, apply_low_latency_config, build_init_params, codec_guid, resolve_slices, resolve_subframe,
LowLatencyConfig, NvStatusExt, RFI_DPB,
}; };
use super::nvenc_status; use super::nvenc_status;
use super::{ChromaFormat, Codec, EncodedFrame, Encoder, EncoderCaps}; use super::{ChromaFormat, Codec, EncodedFrame, Encoder, EncoderCaps};
@@ -744,6 +745,10 @@ impl NvencD3d11Encoder {
av1_input_depth_minus8: if ten_bit_in { 2 } else { 0 }, av1_input_depth_minus8: if ten_bit_in { 2 } else { 0 },
hdr: self.hdr, hdr: self.hdr,
rfi_supported: self.rfi_supported, rfi_supported: self.rfi_supported,
// Env-only on Windows (default single slice): the Phase-3 default-on is a
// Linux-direct-NVENC decision — the Windows async path stays untouched, and a
// Windows operator opting in must choose slices+sync over async retrieve.
slices: resolve_slices(self.codec, 1),
}, },
); );
Ok(cfg) Ok(cfg)
@@ -791,6 +796,9 @@ impl NvencD3d11Encoder {
&mut cfg, &mut cfg,
split_mode, split_mode,
enable_async, enable_async,
// Windows: env opt-in only ("1"), never a default — and build_init_params
// additionally refuses it on an async session.
resolve_subframe(false),
); );
match (api().initialize_encoder)(enc, &mut init).nv_ok() { match (api().initialize_encoder)(enc, &mut init).nv_ok() {
@@ -1568,6 +1576,7 @@ impl Encoder for NvencD3d11Encoder {
&mut cfg, &mut cfg,
self.split_mode, self.split_mode,
self.session_async, self.session_async,
resolve_subframe(false),
), ),
..Default::default() ..Default::default()
}; };