From 4d45a96ff99ce893bb204cae884cf67aa1e00144 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Fri, 31 Jul 2026 18:45:41 +0200 Subject: [PATCH] feat(encode/windows): sub-frame readback defaults on where the GPU supports it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Linux parity, validated by the .173 on-glass A/B (no regression; the win goes to clients that actually consume slice-progressive parts): the caps probe now reads NV_ENC_CAPS_SUPPORT_SUBFRAME_READBACK and seeds resolve_subframe with it instead of a hard false, so PUNKTFUNK_NVENC_SUBFRAME becomes the tri-state escape it already is on Linux, and the split×sub-frame arbitration hears the real forced flag for its log severity. The A/B also caught the default path opening every session with a WARN: the submit-time idr_hint missed that NVENC emits the session-opening frame as an IDR regardless of pic flags, so frame 1's early chunks went out unflagged and the divergence check fired at every start. The hint now carries the Linux twin's `opening` term. Co-Authored-By: Claude Fable 5 --- crates/pf-encode/src/enc/nvenc_core.rs | 9 ++++--- crates/pf-encode/src/enc/windows/nvenc.rs | 31 ++++++++++++++++------- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/crates/pf-encode/src/enc/nvenc_core.rs b/crates/pf-encode/src/enc/nvenc_core.rs index 77abf136..b591f754 100644 --- a/crates/pf-encode/src/enc/nvenc_core.rs +++ b/crates/pf-encode/src/enc/nvenc_core.rs @@ -117,10 +117,11 @@ pub(super) fn resolve_split_mode(bit_depth: u8, pixel_rate: u64) -> u32 { /// 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")] +/// Both direct-SDK backends latch it now: Linux at the `nvenc_cuda` query_caps latch, Windows at +/// session init since sub-frame defaults on there too (it used to be env opt-in only, so +/// `subframe == forced` held by construction and the item was Linux-cfg'd to avoid being dead +/// code on the Windows leg — the recurring item-level `dead_code` trap). +#[cfg(any(target_os = "linux", windows))] pub(super) fn subframe_env_forced() -> bool { matches!( std::env::var("PUNKTFUNK_NVENC_SUBFRAME").as_deref(), diff --git a/crates/pf-encode/src/enc/windows/nvenc.rs b/crates/pf-encode/src/enc/windows/nvenc.rs index cc9cec1c..c960080b 100644 --- a/crates/pf-encode/src/enc/windows/nvenc.rs +++ b/crates/pf-encode/src/enc/windows/nvenc.rs @@ -45,7 +45,7 @@ use super::nvenc_core::{ apply_low_latency_config, build_init_params, cached_ceiling, codec_guid, plan_range_recovery, resolve_slices, resolve_split_mode, resolve_split_subframe, resolve_subframe, store_ceiling, - CeilingKey, LowLatencyConfig, NvStatusExt, RangePlan, + subframe_env_forced, CeilingKey, LowLatencyConfig, NvStatusExt, RangePlan, }; use super::nvenc_status; use super::{AuChunk, ChromaFormat, Codec, EncodedFrame, Encoder, EncoderCaps}; @@ -588,6 +588,10 @@ pub struct NvencD3d11Encoder { input_ring_depth: Option, /// `NV_ENC_CAPS_ASYNC_ENCODE_SUPPORT` from the caps probe — gates the async retrieve mode. async_supported: bool, + /// `NV_ENC_CAPS_SUPPORT_SUBFRAME_READBACK` from the caps probe — gates the DEFAULT-on + /// sub-frame readback (the Linux backend's rule since its Phase 3; Windows joined after the + /// 2026-07-31 on-glass A/B), so a GPU without it never has sub-frame forced by default. + subframe_cap: bool, /// (bitstream, mapped input resource to unmap after retrieval, pts_ns, recovery-anchor) per /// in-flight encode. The fourth field tags the first frame encoded after a successful /// [`invalidate_ref_frames`](Encoder::invalidate_ref_frames) — the clean re-anchor P-frame the @@ -748,6 +752,7 @@ impl NvencD3d11Encoder { async_rt: None, input_ring_depth: None, async_supported: false, + subframe_cap: false, pending: VecDeque::new(), frame_idx: 0, force_kf: false, @@ -922,6 +927,7 @@ impl NvencD3d11Encoder { nv::NV_ENC_CAPS::NV_ENC_CAPS_SUPPORT_CUSTOM_VBV_BUF_SIZE, ); let async_enc = self.get_cap(enc, nv::NV_ENC_CAPS::NV_ENC_CAPS_ASYNC_ENCODE_SUPPORT); + let subframe = self.get_cap(enc, nv::NV_ENC_CAPS::NV_ENC_CAPS_SUPPORT_SUBFRAME_READBACK); let _ = (api().destroy_encoder)(enc); // Reject an over-range mode with a clear message instead of an opaque InvalidParam. @@ -955,10 +961,12 @@ impl NvencD3d11Encoder { self.rfi_supported = rfi != 0; self.custom_vbv = custom_vbv != 0; self.async_supported = async_enc != 0; + self.subframe_cap = subframe != 0; tracing::info!( rfi = self.rfi_supported, custom_vbv = self.custom_vbv, async_encode = self.async_supported, + subframe_readback = self.subframe_cap, max = %format!("{wmax}x{hmax}"), ten_bit = ten_bit != 0, "NVENC capabilities probed" @@ -1152,11 +1160,14 @@ impl NvencD3d11Encoder { // VIDEO_CAP_MULTI_SLICE / Moonlight slices-per-frame client gets real slices. // `PUNKTFUNK_NVENC_SLICES` stays the operator override in both directions. self.slices = resolve_slices(self.codec, 4.min(self.max_slices)); - // 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); + // Split × sub-frame arbitration (Phase 8) before the ladder/ceiling key. Sub-frame + // defaults ON where the GPU advertises SUBFRAME_READBACK (Linux parity; validated by + // the 2026-07-31 .173 on-glass A/B — no regression, and slice-progressive clients + // gain the encode/wire overlap); `PUNKTFUNK_NVENC_SUBFRAME` stays the tri-state + // operator escape in both directions. + let subframe_req = resolve_subframe(self.subframe_cap); let (split_mode, subframe_req) = - resolve_split_subframe(self.codec, split_mode, subframe_req, subframe_req); + resolve_split_subframe(self.codec, split_mode, subframe_req, subframe_env_forced()); // 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 // 1 Gbps request on HEVC). Strategy: try the requested rate; if the only problem is a forced @@ -1318,8 +1329,7 @@ impl NvencD3d11Encoder { self.session_async = use_async; // Sub-frame chunked poll (P2f, the Windows leg of the slice pipeline): sync // retrieve only — chunked poll is a depth-1 sync feature; the async retrieve's - // thread owns the bitstream lock. Sub-frame write itself stays env-gated - // (`PUNKTFUNK_NVENC_SUBFRAME=1`) until the Windows on-glass A/B validates it. + // thread owns the bitstream lock. self.subframe_chunks = self.slices >= 2 && subframe_req && !use_async; if self.subframe_chunks { tracing::info!( @@ -1659,8 +1669,11 @@ impl Encoder for NvencD3d11Encoder { let anchor = std::mem::take(&mut self.pending_anchor) && flags == 0; // Submit-time IDR intent: chunked poll must flag an AU's EARLY chunks before the // driver reports `pictureType` (only the finishing lock sees it). Exact under - // P-only + infinite GOP: IDRs happen only when forced. - let idr_hint = flags != 0; + // P-only + infinite GOP: IDRs happen only when forced — or on the session-opening + // frame, which NVENC emits as an IDR regardless of pic flags (the Linux twin's + // `is_idr`; without the `opening` term frame 1's early chunks went out unflagged + // and the divergence WARN fired at every session start). + let idr_hint = flags != 0 || opening; let mut pic = nv::NV_ENC_PIC_PARAMS { version: nv::NV_ENC_PIC_PARAMS_VER, inputWidth: self.width,