fix(pf-encode): arbitrate NVENC split-encode vs sub-frame readback (Phase 8)
Verified against nvEncodeAPI.h's own splitEncodeMode doc (user-prompted — the audit's 'exclusive for HEVC' one-liner deserved checking): - H.264: split 'is not applicable' — hard-DISABLE the mode so the written config, CeilingKey, the split diagnostic log and the rejection-retry stay truthful (the retry used to re-open a byte-identical session after an H.264 'split rejection'). The libav path's operator arm gains the codec gate its auto arm always had. - HEVC: split 'not supported if … subframe mode' — when WE force split (TWO/THREE/AUTO_FORCED, the 4K120 throughput lever), sub-frame yields with a logged escape (PUNKTFUNK_SPLIT_ENCODE=0 chooses sub-frame). ⚠ Keyed on FORCED modes only, never != DISABLE: AUTO(0) is the resolver's fallthrough for every sub-950Mpix session, and the wider key would have disarmed the Phase-3 chunked-poll feature fleet-wide (critic catch). Under AUTO the driver arbitrates — the shipped state. - AV1: untouched — per-tile sub-frame + split are legal together. The arbitration is a pure nvenc_core fn called by each backend BEFORE the ladder, the ceiling key and the chunked-poll latch — all three see the post-arbitration truth. A drop inside build_init_params would have left poll_chunk busy-polling its whole budget every AU (numSlices stays 0 without reportSliceOffsets; both loop exits dead — critic catch). Linux latches subframe_forced beside subframe_on at query_caps (no env re-reads after open); Windows records the arbitrated state so reconfigure presents exactly the params the open had (also closes the pre-existing mid-session env-flip hazard there). Truth-table tests in nvenc_core; PUNKTFUNK_NVENC_SUBFRAME documented (it never was); PUNKTFUNK_SPLIT_ENCODE row updated. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -448,7 +448,17 @@ impl NvencEncoder {
|
|||||||
let pix_rate = width as u64 * height as u64 * fps as u64;
|
let pix_rate = width as u64 * height as u64 * fps as u64;
|
||||||
let split = std::env::var("PUNKTFUNK_SPLIT_ENCODE").ok();
|
let split = std::env::var("PUNKTFUNK_SPLIT_ENCODE").ok();
|
||||||
match split.as_deref() {
|
match split.as_deref() {
|
||||||
Some(mode) => opts.set("split_encode_mode", mode),
|
// The operator arm gains the codec gate the auto arm always had (Phase 8): split
|
||||||
|
// "is not applicable to H264" per nvEncodeAPI.h, and h264_nvenc has no such AVOption
|
||||||
|
// — setting it would fail the open on a leftover dict entry.
|
||||||
|
Some(mode) if matches!(codec, Codec::H265 | Codec::Av1) => {
|
||||||
|
opts.set("split_encode_mode", mode)
|
||||||
|
}
|
||||||
|
Some(_) => tracing::warn!(
|
||||||
|
codec = codec.nvenc_name(),
|
||||||
|
"PUNKTFUNK_SPLIT_ENCODE ignored — split encoding is not applicable to H.264 \
|
||||||
|
(nvEncodeAPI.h)"
|
||||||
|
),
|
||||||
None if matches!(codec, Codec::H265 | Codec::Av1)
|
None if matches!(codec, Codec::H265 | Codec::Av1)
|
||||||
&& pix_rate >= super::SPLIT_FORCE_PIXEL_RATE =>
|
&& pix_rate >= super::SPLIT_FORCE_PIXEL_RATE =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -62,8 +62,8 @@
|
|||||||
|
|
||||||
use super::nvenc_core::{
|
use super::nvenc_core::{
|
||||||
apply_low_latency_config, build_init_params, cached_ceiling, codec_guid, plan_range_recovery,
|
apply_low_latency_config, build_init_params, cached_ceiling, codec_guid, plan_range_recovery,
|
||||||
resolve_slices, resolve_split_mode, resolve_subframe, store_ceiling, CeilingKey,
|
resolve_slices, resolve_split_mode, resolve_split_subframe, resolve_subframe, store_ceiling,
|
||||||
LowLatencyConfig, NvStatusExt, RangePlan,
|
subframe_env_forced, CeilingKey, LowLatencyConfig, NvStatusExt, RangePlan,
|
||||||
};
|
};
|
||||||
use super::nvenc_status;
|
use super::nvenc_status;
|
||||||
use super::{AuChunk, ChromaFormat, Codec, EncodedFrame, Encoder, EncoderCaps};
|
use super::{AuChunk, ChromaFormat, Codec, EncodedFrame, Encoder, EncoderCaps};
|
||||||
@@ -629,6 +629,10 @@ pub struct NvencCudaEncoder {
|
|||||||
/// [`subframe_cap`](Self::subframe_cap)); consumed by every `build_init_params` call so the
|
/// [`subframe_cap`](Self::subframe_cap)); consumed by every `build_init_params` call so the
|
||||||
/// open and the in-place reconfigure present identical init params.
|
/// open and the in-place reconfigure present identical init params.
|
||||||
subframe_on: bool,
|
subframe_on: bool,
|
||||||
|
/// Whether `PUNKTFUNK_NVENC_SUBFRAME=1` was EXPLICITLY forced — latched with `subframe_on`
|
||||||
|
/// (same invariant: open and reconfigure must present identical init params, so no env
|
||||||
|
/// re-reads after `query_caps`). Only consumed by [`resolve_split_subframe`]'s log severity.
|
||||||
|
subframe_forced: 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,
|
||||||
@@ -721,6 +725,7 @@ impl NvencCudaEncoder {
|
|||||||
slices: 1,
|
slices: 1,
|
||||||
subframe_cap: false,
|
subframe_cap: false,
|
||||||
subframe_on: false,
|
subframe_on: false,
|
||||||
|
subframe_forced: false,
|
||||||
subframe_chunks: false,
|
subframe_chunks: false,
|
||||||
chunk: None,
|
chunk: None,
|
||||||
})
|
})
|
||||||
@@ -921,6 +926,7 @@ impl NvencCudaEncoder {
|
|||||||
// escapes.
|
// escapes.
|
||||||
self.slices = resolve_slices(self.codec, 4);
|
self.slices = resolve_slices(self.codec, 4);
|
||||||
self.subframe_on = resolve_subframe(self.subframe_cap);
|
self.subframe_on = resolve_subframe(self.subframe_cap);
|
||||||
|
self.subframe_forced = subframe_env_forced();
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
rfi = self.rfi_supported,
|
rfi = self.rfi_supported,
|
||||||
custom_vbv = self.custom_vbv,
|
custom_vbv = self.custom_vbv,
|
||||||
@@ -1136,6 +1142,16 @@ impl NvencCudaEncoder {
|
|||||||
// [`resolve_split_mode`] for the precedence (env override / 10-bit / pixel rate).
|
// [`resolve_split_mode`] for the precedence (env override / 10-bit / pixel rate).
|
||||||
let pixel_rate = self.width as u64 * self.height as u64 * self.fps.max(1) as u64;
|
let pixel_rate = self.width as u64 * self.height as u64 * self.fps.max(1) as u64;
|
||||||
let split_mode: u32 = resolve_split_mode(self.bit_depth, pixel_rate);
|
let split_mode: u32 = resolve_split_mode(self.bit_depth, pixel_rate);
|
||||||
|
// Split × sub-frame arbitration (Phase 8) BEFORE the ladder, the ceiling key and the
|
||||||
|
// chunked-poll latch — all three must see the post-arbitration truth (a drop inside
|
||||||
|
// build_init_params would leave poll_chunk busy-polling its whole budget per AU).
|
||||||
|
let (split_mode, subframe_on) = resolve_split_subframe(
|
||||||
|
self.codec,
|
||||||
|
split_mode,
|
||||||
|
self.subframe_on,
|
||||||
|
self.subframe_forced,
|
||||||
|
);
|
||||||
|
self.subframe_on = subframe_on;
|
||||||
const CLAMP_TOL_BPS: u64 = 20_000_000;
|
const CLAMP_TOL_BPS: u64 = 20_000_000;
|
||||||
|
|
||||||
// Ceiling cache (process lifetime, `nvenc_core`): a prior clamp search already found
|
// Ceiling cache (process lifetime, `nvenc_core`): a prior clamp search already found
|
||||||
|
|||||||
@@ -102,6 +102,141 @@ pub(super) fn resolve_split_mode(bit_depth: u8, pixel_rate: u64) -> u32 {
|
|||||||
mode
|
mode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the operator EXPLICITLY forced sub-frame readback on (`PUNKTFUNK_NVENC_SUBFRAME=1`)
|
||||||
|
/// — the log-severity input to [`resolve_split_subframe`]: a forced knob being overridden
|
||||||
|
/// deserves a `warn`, a default being tuned an `info`. Callers LATCH this once next to their
|
||||||
|
/// resolved subframe state (an env re-read at reconfigure would violate the "open and
|
||||||
|
/// reconfigure present identical init params" invariant).
|
||||||
|
/// Linux-cfg'd like its ONLY caller (the `nvenc_cuda` query_caps latch) — Windows sessions have
|
||||||
|
/// `subframe == forced` by construction (env opt-in only) and never consult this; without the
|
||||||
|
/// cfg it is dead code on every Windows leg (item-level dead_code, the recurring trap).
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pub(super) fn subframe_env_forced() -> bool {
|
||||||
|
matches!(
|
||||||
|
std::env::var("PUNKTFUNK_NVENC_SUBFRAME").as_deref(),
|
||||||
|
Ok("1")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The split-encode × sub-frame arbitration (Phase 8; verified against `nvEncodeAPI.h`'s own
|
||||||
|
/// `splitEncodeMode` doc, not folklore):
|
||||||
|
/// - **H.264**: split "is not applicable" — hard-DISABLE the mode so the written config, the
|
||||||
|
/// ceiling-cache key, the split diagnostic log and the rejection-retry all stay truthful (the
|
||||||
|
/// retry used to re-open a byte-identical session after an H.264 "split rejection").
|
||||||
|
/// - **HEVC**: split is "not supported if … subframe mode" — when WE force split
|
||||||
|
/// (TWO/THREE/AUTO_FORCED, e.g. the 4K120 throughput requirement), sub-frame yields. Under
|
||||||
|
/// plain AUTO the driver arbitrates — the shipped fleet state (1080p–1440p240 all run
|
||||||
|
/// AUTO+subframe); keying on `!= DISABLE` here would have disarmed the Phase-3 chunked-poll
|
||||||
|
/// feature fleet-wide.
|
||||||
|
/// - **AV1**: both legal (sub-frame is per-tile; split is constrained only by
|
||||||
|
/// output-into-vidmem, which we never use) — untouched.
|
||||||
|
///
|
||||||
|
/// Returns the `(split_mode, subframe)` to ACTUALLY configure. The caller must store BOTH back
|
||||||
|
/// (the chunked-poll latch and `CeilingKey` key on them) — a silent in-params drop would leave
|
||||||
|
/// `poll_chunk` busy-polling its full budget every AU (`numSlices` stays 0 without
|
||||||
|
/// `reportSliceOffsets`, so neither loop exit ever fires).
|
||||||
|
pub(super) fn resolve_split_subframe(
|
||||||
|
codec: Codec,
|
||||||
|
split_mode: u32,
|
||||||
|
subframe: bool,
|
||||||
|
subframe_forced: bool,
|
||||||
|
) -> (u32, bool) {
|
||||||
|
use nv::NV_ENC_SPLIT_ENCODE_MODE as M;
|
||||||
|
if codec == Codec::H264 {
|
||||||
|
return (M::NV_ENC_SPLIT_DISABLE_MODE as u32, subframe);
|
||||||
|
}
|
||||||
|
let split_forced = split_mode == M::NV_ENC_SPLIT_TWO_FORCED_MODE as u32
|
||||||
|
|| split_mode == M::NV_ENC_SPLIT_THREE_FORCED_MODE as u32
|
||||||
|
|| split_mode == M::NV_ENC_SPLIT_AUTO_FORCED_MODE as u32;
|
||||||
|
if codec == Codec::H265 && split_forced && subframe {
|
||||||
|
if subframe_forced {
|
||||||
|
tracing::warn!(
|
||||||
|
split_mode,
|
||||||
|
"HEVC forced split-encode and PUNKTFUNK_NVENC_SUBFRAME=1 are mutually \
|
||||||
|
unsupported (nvEncodeAPI.h) — sub-frame readback disabled for this session; \
|
||||||
|
set PUNKTFUNK_SPLIT_ENCODE=0 to choose sub-frame instead"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
tracing::info!(
|
||||||
|
split_mode,
|
||||||
|
"HEVC forced split-encode supersedes default-on sub-frame readback (mutually \
|
||||||
|
unsupported per nvEncodeAPI.h; split is the 4K120 throughput lever) — set \
|
||||||
|
PUNKTFUNK_SPLIT_ENCODE=0 to choose sub-frame instead"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (split_mode, false);
|
||||||
|
}
|
||||||
|
(split_mode, subframe)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod split_subframe_tests {
|
||||||
|
use super::{resolve_split_subframe, Codec};
|
||||||
|
use nvidia_video_codec_sdk::sys::nvEncodeAPI::NV_ENC_SPLIT_ENCODE_MODE as M;
|
||||||
|
|
||||||
|
const AUTO: u32 = M::NV_ENC_SPLIT_AUTO_MODE as u32;
|
||||||
|
const TWO: u32 = M::NV_ENC_SPLIT_TWO_FORCED_MODE as u32;
|
||||||
|
const AUTO_F: u32 = M::NV_ENC_SPLIT_AUTO_FORCED_MODE as u32;
|
||||||
|
const DISABLE: u32 = M::NV_ENC_SPLIT_DISABLE_MODE as u32;
|
||||||
|
|
||||||
|
/// THE FLEET CASE: plain AUTO + default-on sub-frame must pass through untouched — the
|
||||||
|
/// driver arbitrates. Keying the rule on `!= DISABLE` would disarm sub-frame on every
|
||||||
|
/// default Linux HEVC session (AUTO == 0 is the resolver's fallthrough).
|
||||||
|
#[test]
|
||||||
|
fn hevc_auto_keeps_subframe() {
|
||||||
|
assert_eq!(
|
||||||
|
resolve_split_subframe(Codec::H265, AUTO, true, false),
|
||||||
|
(AUTO, true)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hevc_forced_split_drops_subframe() {
|
||||||
|
assert_eq!(
|
||||||
|
resolve_split_subframe(Codec::H265, TWO, true, false),
|
||||||
|
(TWO, false)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resolve_split_subframe(Codec::H265, AUTO_F, true, true),
|
||||||
|
(AUTO_F, false)
|
||||||
|
);
|
||||||
|
// No sub-frame to drop → nothing changes.
|
||||||
|
assert_eq!(
|
||||||
|
resolve_split_subframe(Codec::H265, TWO, false, false),
|
||||||
|
(TWO, false)
|
||||||
|
);
|
||||||
|
// Explicitly disabled split → sub-frame kept (the documented escape).
|
||||||
|
assert_eq!(
|
||||||
|
resolve_split_subframe(Codec::H265, DISABLE, true, true),
|
||||||
|
(DISABLE, true)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// H.264: split "is not applicable" (nvEncodeAPI.h) — hard-DISABLE regardless of the
|
||||||
|
/// resolved mode; sub-frame (H.264 slices) is unaffected.
|
||||||
|
#[test]
|
||||||
|
fn h264_split_hard_disabled() {
|
||||||
|
assert_eq!(
|
||||||
|
resolve_split_subframe(Codec::H264, TWO, true, false),
|
||||||
|
(DISABLE, true)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resolve_split_subframe(Codec::H264, AUTO, false, false),
|
||||||
|
(DISABLE, false)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// AV1: both features are legal together (per-tile sub-frame; split constrained only by
|
||||||
|
/// output-into-vidmem) — the arbitration must not touch it.
|
||||||
|
#[test]
|
||||||
|
fn av1_untouched() {
|
||||||
|
assert_eq!(
|
||||||
|
resolve_split_subframe(Codec::Av1, TWO, true, true),
|
||||||
|
(TWO, true)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// One session config's identity for the process-lifetime bitrate-ceiling cache
|
/// One session config's identity for the process-lifetime bitrate-ceiling cache
|
||||||
/// ([`cached_ceiling`]/[`store_ceiling`]). Everything the driver's codec-level validation keys
|
/// ([`cached_ceiling`]/[`store_ceiling`]). Everything the driver's codec-level validation keys
|
||||||
/// off: the GPU (different NVENC generations have different level ceilings), dims/fps (the luma
|
/// off: the GPU (different NVENC generations have different level ceilings), dims/fps (the luma
|
||||||
|
|||||||
@@ -38,8 +38,8 @@
|
|||||||
|
|
||||||
use super::nvenc_core::{
|
use super::nvenc_core::{
|
||||||
apply_low_latency_config, build_init_params, cached_ceiling, codec_guid, plan_range_recovery,
|
apply_low_latency_config, build_init_params, cached_ceiling, codec_guid, plan_range_recovery,
|
||||||
resolve_slices, resolve_split_mode, resolve_subframe, store_ceiling, CeilingKey,
|
resolve_slices, resolve_split_mode, resolve_split_subframe, resolve_subframe, store_ceiling,
|
||||||
LowLatencyConfig, NvStatusExt, RangePlan,
|
CeilingKey, LowLatencyConfig, NvStatusExt, RangePlan,
|
||||||
};
|
};
|
||||||
use super::nvenc_status;
|
use super::nvenc_status;
|
||||||
use super::{ChromaFormat, Codec, EncodedFrame, Encoder, EncoderCaps};
|
use super::{ChromaFormat, Codec, EncodedFrame, Encoder, EncoderCaps};
|
||||||
@@ -603,6 +603,11 @@ pub struct NvencD3d11Encoder {
|
|||||||
/// `reconfigure_bitrate` must present the SAME init params as the open (only the config's
|
/// `reconfigure_bitrate` must present the SAME init params as the open (only the config's
|
||||||
/// rate fields may move). Meaningless while `inited` is false.
|
/// rate fields may move). Meaningless while `inited` is false.
|
||||||
split_mode: u32,
|
split_mode: u32,
|
||||||
|
/// The session's arbitrated sub-frame state ([`resolve_split_subframe`] at open) — recorded
|
||||||
|
/// so reconfigure presents EXACTLY the init params the session opened with (an env re-read
|
||||||
|
/// there could flip `enableSubFrameWrite` mid-session, and would re-log the arbitration on
|
||||||
|
/// every ABR retarget).
|
||||||
|
subframe_on: bool,
|
||||||
session_async: bool,
|
session_async: bool,
|
||||||
/// The last reference-frame range we invalidated — dedupes repeated RFI requests for the same
|
/// The last reference-frame range we invalidated — dedupes repeated RFI requests for the same
|
||||||
/// loss event (the client resends until it sees recovery).
|
/// loss event (the client resends until it sees recovery).
|
||||||
@@ -686,6 +691,7 @@ impl NvencD3d11Encoder {
|
|||||||
rfi_supported: false,
|
rfi_supported: false,
|
||||||
custom_vbv: false,
|
custom_vbv: false,
|
||||||
split_mode: nv::NV_ENC_SPLIT_ENCODE_MODE::NV_ENC_SPLIT_DISABLE_MODE as u32,
|
split_mode: nv::NV_ENC_SPLIT_ENCODE_MODE::NV_ENC_SPLIT_DISABLE_MODE as u32,
|
||||||
|
subframe_on: false,
|
||||||
session_async: false,
|
session_async: false,
|
||||||
last_rfi_range: None,
|
last_rfi_range: None,
|
||||||
init_device: ptr::null_mut(),
|
init_device: ptr::null_mut(),
|
||||||
@@ -980,6 +986,7 @@ impl NvencD3d11Encoder {
|
|||||||
bitrate: u64,
|
bitrate: u64,
|
||||||
split_mode: u32,
|
split_mode: u32,
|
||||||
enable_async: bool,
|
enable_async: bool,
|
||||||
|
subframe: bool,
|
||||||
) -> Result<*mut c_void> {
|
) -> Result<*mut c_void> {
|
||||||
let mut params = nv::NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS {
|
let mut params = nv::NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS {
|
||||||
version: nv::NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS_VER,
|
version: nv::NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS_VER,
|
||||||
@@ -1014,9 +1021,10 @@ 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
|
// Windows: env opt-in only ("1"), never a default; arbitrated against split by the
|
||||||
// additionally refuses it on an async session.
|
// caller (resolve_split_subframe) — and build_init_params additionally refuses it
|
||||||
resolve_subframe(false),
|
// on an async session.
|
||||||
|
subframe,
|
||||||
);
|
);
|
||||||
|
|
||||||
match (api().initialize_encoder)(enc, &mut init).nv_ok() {
|
match (api().initialize_encoder)(enc, &mut init).nv_ok() {
|
||||||
@@ -1069,6 +1077,11 @@ impl NvencD3d11Encoder {
|
|||||||
// The init-failure fallback below disables it if a codec/config rejects it.
|
// The init-failure fallback below disables it if a codec/config rejects it.
|
||||||
let pixel_rate = self.width as u64 * self.height as u64 * self.fps.max(1) as u64;
|
let pixel_rate = self.width as u64 * self.height as u64 * self.fps.max(1) as u64;
|
||||||
let split_mode: u32 = resolve_split_mode(self.bit_depth, pixel_rate);
|
let split_mode: u32 = resolve_split_mode(self.bit_depth, pixel_rate);
|
||||||
|
// Split × sub-frame arbitration (Phase 8) before the ladder/ceiling key. On Windows
|
||||||
|
// sub-frame is env-opt-in only, so resolved == forced by construction.
|
||||||
|
let subframe_req = resolve_subframe(false);
|
||||||
|
let (split_mode, subframe_req) =
|
||||||
|
resolve_split_subframe(self.codec, split_mode, subframe_req, subframe_req);
|
||||||
// Find the highest bitrate the GPU's codec LEVEL accepts and CLAMP to it. NVENC rejects
|
// Find the highest bitrate the GPU's codec LEVEL accepts and CLAMP to it. NVENC rejects
|
||||||
// `initialize_encoder` (InvalidParam) when the bitrate exceeds the level ceiling (e.g. a
|
// `initialize_encoder` (InvalidParam) when the bitrate exceeds the level ceiling (e.g. a
|
||||||
// 1 Gbps request on HEVC). Strategy: try the requested rate; if the only problem is a forced
|
// 1 Gbps request on HEVC). Strategy: try the requested rate; if the only problem is a forced
|
||||||
@@ -1098,12 +1111,19 @@ impl NvencD3d11Encoder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut probe = self.try_open_session(device, target_bps, split_mode, use_async);
|
let mut probe =
|
||||||
|
self.try_open_session(device, target_bps, split_mode, use_async, subframe_req);
|
||||||
// The cache is advisory: a stale entry (driver change, identity collision) must not
|
// The cache is advisory: a stale entry (driver change, identity collision) must not
|
||||||
// wedge the open — retry the requested rate and let the search below rediscover.
|
// wedge the open — retry the requested rate and let the search below rediscover.
|
||||||
if probe.is_err() && target_bps < requested_bps {
|
if probe.is_err() && target_bps < requested_bps {
|
||||||
target_bps = requested_bps;
|
target_bps = requested_bps;
|
||||||
probe = self.try_open_session(device, requested_bps, split_mode, use_async);
|
probe = self.try_open_session(
|
||||||
|
device,
|
||||||
|
requested_bps,
|
||||||
|
split_mode,
|
||||||
|
use_async,
|
||||||
|
subframe_req,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// Disambiguate a forced-split rejection from a bitrate-cap rejection: retry once at the
|
// Disambiguate a forced-split rejection from a bitrate-cap rejection: retry once at the
|
||||||
// requested rate with split disabled — if THAT succeeds, split was the problem, not bitrate.
|
// requested rate with split disabled — if THAT succeeds, split was the problem, not bitrate.
|
||||||
@@ -1117,7 +1137,9 @@ impl NvencD3d11Encoder {
|
|||||||
split_mode != nv::NV_ENC_SPLIT_ENCODE_MODE::NV_ENC_SPLIT_DISABLE_MODE as u32;
|
split_mode != nv::NV_ENC_SPLIT_ENCODE_MODE::NV_ENC_SPLIT_DISABLE_MODE as u32;
|
||||||
if probe.is_err() && split_on {
|
if probe.is_err() && split_on {
|
||||||
let no_split = nv::NV_ENC_SPLIT_ENCODE_MODE::NV_ENC_SPLIT_DISABLE_MODE as u32;
|
let no_split = nv::NV_ENC_SPLIT_ENCODE_MODE::NV_ENC_SPLIT_DISABLE_MODE as u32;
|
||||||
if let Ok(e) = self.try_open_session(device, target_bps, no_split, use_async) {
|
if let Ok(e) =
|
||||||
|
self.try_open_session(device, target_bps, no_split, use_async, subframe_req)
|
||||||
|
{
|
||||||
tracing::warn!("NVENC: split-encode rejected by codec/config — disabled");
|
tracing::warn!("NVENC: split-encode rejected by codec/config — disabled");
|
||||||
used_split = no_split;
|
used_split = no_split;
|
||||||
probe = Ok(e);
|
probe = Ok(e);
|
||||||
@@ -1144,7 +1166,13 @@ impl NvencD3d11Encoder {
|
|||||||
let mut best_bps = 0u64;
|
let mut best_bps = 0u64;
|
||||||
while hi > lo + CLAMP_TOL_BPS {
|
while hi > lo + CLAMP_TOL_BPS {
|
||||||
let mid = lo + (hi - lo) / 2;
|
let mid = lo + (hi - lo) / 2;
|
||||||
match self.try_open_session(device, mid, used_split, use_async) {
|
match self.try_open_session(
|
||||||
|
device,
|
||||||
|
mid,
|
||||||
|
used_split,
|
||||||
|
use_async,
|
||||||
|
subframe_req,
|
||||||
|
) {
|
||||||
Ok(e) => {
|
Ok(e) => {
|
||||||
if !best.is_null() {
|
if !best.is_null() {
|
||||||
let _ = (api().destroy_encoder)(best);
|
let _ = (api().destroy_encoder)(best);
|
||||||
@@ -1169,12 +1197,23 @@ impl NvencD3d11Encoder {
|
|||||||
// trying split-disabled in case a forced split (not the bitrate) is the blocker.
|
// trying split-disabled in case a forced split (not the bitrate) is the blocker.
|
||||||
let no_split =
|
let no_split =
|
||||||
nv::NV_ENC_SPLIT_ENCODE_MODE::NV_ENC_SPLIT_DISABLE_MODE as u32;
|
nv::NV_ENC_SPLIT_ENCODE_MODE::NV_ENC_SPLIT_DISABLE_MODE as u32;
|
||||||
best = match self.try_open_session(device, FLOOR_BPS, used_split, use_async)
|
best = match self.try_open_session(
|
||||||
{
|
device,
|
||||||
|
FLOOR_BPS,
|
||||||
|
used_split,
|
||||||
|
use_async,
|
||||||
|
subframe_req,
|
||||||
|
) {
|
||||||
Ok(e) => e,
|
Ok(e) => e,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
let e = self
|
let e = self
|
||||||
.try_open_session(device, FLOOR_BPS, no_split, use_async)
|
.try_open_session(
|
||||||
|
device,
|
||||||
|
FLOOR_BPS,
|
||||||
|
no_split,
|
||||||
|
use_async,
|
||||||
|
subframe_req,
|
||||||
|
)
|
||||||
.context(
|
.context(
|
||||||
"NVENC initialize_encoder rejected even at the floor bitrate",
|
"NVENC initialize_encoder rejected even at the floor bitrate",
|
||||||
)?;
|
)?;
|
||||||
@@ -1200,6 +1239,7 @@ impl NvencD3d11Encoder {
|
|||||||
self.init_device_com = Some(device.clone());
|
self.init_device_com = Some(device.clone());
|
||||||
// Session init params a later `reconfigure_bitrate` must re-present verbatim.
|
// Session init params a later `reconfigure_bitrate` must re-present verbatim.
|
||||||
self.split_mode = used_split;
|
self.split_mode = used_split;
|
||||||
|
self.subframe_on = subframe_req;
|
||||||
self.session_async = use_async;
|
self.session_async = use_async;
|
||||||
// Session-budget accounting (Stage W3): record what this open holds so admission can
|
// Session-budget accounting (Stage W3): record what this open holds so admission can
|
||||||
// decline a parallel display the hardware can't afford. Weighted by the FINAL split
|
// decline a parallel display the hardware can't afford. Weighted by the FINAL split
|
||||||
@@ -1855,7 +1895,10 @@ impl Encoder for NvencD3d11Encoder {
|
|||||||
&mut cfg,
|
&mut cfg,
|
||||||
self.split_mode,
|
self.split_mode,
|
||||||
self.session_async,
|
self.session_async,
|
||||||
resolve_subframe(false),
|
// The SESSION's recorded state, not a fresh env read: reconfigure must
|
||||||
|
// present exactly the init params the open arbitrated (an env re-read could
|
||||||
|
// flip enableSubFrameWrite mid-session — the Linux latch invariant).
|
||||||
|
self.subframe_on,
|
||||||
),
|
),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -143,7 +143,8 @@ notes for context.
|
|||||||
| Setting | Values | Meaning |
|
| Setting | Values | Meaning |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `PUNKTFUNK_GSO` | `1` · `0` | UDP Generic Segmentation Offload on the send path (coalesce a frame's packets into kernel super-buffers) — cuts send CPU ~30%, but its line-rate packet trains can cost delivered throughput on constrained links (measured on a 2.5GbE hop). Off by default until send pacing spaces the super-buffers; set `1` to opt in (auto-falls back to `sendmmsg` on kernels/paths without support). |
|
| `PUNKTFUNK_GSO` | `1` · `0` | UDP Generic Segmentation Offload on the send path (coalesce a frame's packets into kernel super-buffers) — cuts send CPU ~30%, but its line-rate packet trains can cost delivered throughput on constrained links (measured on a 2.5GbE hop). Off by default until send pacing spaces the super-buffers; set `1` to opt in (auto-falls back to `sendmmsg` on kernels/paths without support). |
|
||||||
| `PUNKTFUNK_SPLIT_ENCODE` | `0`/`disable` · `1`/`auto` · `2` · `3` | NVENC N-way split-encode for very high pixel rates (5K@240). `auto` picks automatically above ~1 Gpix/s. |
|
| `PUNKTFUNK_SPLIT_ENCODE` | `0`/`disable` · `1`/`auto` · `2` · `3` | NVENC N-way split-encode for very high pixel rates (5K@240). `auto` picks automatically above ~1 Gpix/s. H.264 never splits (not applicable per the SDK); on HEVC a *forced* split disables sub-frame readback (mutually unsupported) — set `0` to choose sub-frame instead. |
|
||||||
|
| `PUNKTFUNK_NVENC_SUBFRAME` | `0` · `1` | NVENC sub-frame (slice-level) readback for lower latency on sync sessions. Default: on where the GPU supports it (Linux direct NVENC). `0` = never; `1` = force. On HEVC it yields to a forced split-encode (the SDK documents the pair unsupported). |
|
||||||
| `PUNKTFUNK_GPU_PRIORITY_CLASS` | `off` · `normal` · `high` · `realtime` · `auto` | **(Windows)** GPU scheduling priority for capture/encode under a GPU-saturating game. Default `auto` (starts `high`, upgrades to `realtime` when it's safe — e.g. HAGS off); `high` pins the static pre-gate behaviour; `realtime` is the strongest lever but can freeze NVENC on some setups. |
|
| `PUNKTFUNK_GPU_PRIORITY_CLASS` | `off` · `normal` · `high` · `realtime` · `auto` | **(Windows)** GPU scheduling priority for capture/encode under a GPU-saturating game. Default `auto` (starts `high`, upgrades to `realtime` when it's safe — e.g. HAGS off); `high` pins the static pre-gate behaviour; `realtime` is the strongest lever but can freeze NVENC on some setups. |
|
||||||
| `PUNKTFUNK_IDD_DEPTH` | `N` (default `2`) | **(Windows)** IDD-push pipeline depth. `1` cuts latency once GPU priority is raised; higher smooths a contended GPU. |
|
| `PUNKTFUNK_IDD_DEPTH` | `N` (default `2`) | **(Windows)** IDD-push pipeline depth. `1` cuts latency once GPU priority is raised; higher smooths a contended GPU. |
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user