9a36ea2132
encode.rs + encode/* (NVENC, VAAPI, native AMF, AMF/QSV ffmpeg, direct-SDK NVENC/CUDA, raw Vulkan-Video, PyroWave, openh264) move into crates/pf-encode behind one Encoder trait + open_video selector (plan §W6). The crate speaks the shared frame vocabulary (pf-frame: CapturedFrame/PixelFormat + the DXGI identity D3d11Frame/make_device) and pf-zerocopy (CUDA context/buffers), and NEVER pf-capture — the capture→encode edge is one-way (ZeroCopyPolicy, prior commit). Dep moves: the heavy encoder deps (ffmpeg-next, the NVENC SDK, openh264, pyrowave-sys) move from the host to pf-encode; the host's nvenc/amf-qsv/vulkan-encode/pyrowave features now FORWARD to pf-encode/*. The host keeps a mod-encode shim (pub use pf_encode) so every crate::encode::* path (negotiator + GameStream/native/mgmt planes) is unchanged. resolve_render_adapter_luid moves from the host's windows/win_adapter.rs into pf-gpu (both pf-encode and pf-capture need it as a peer of GPU selection); its 5 call sites (encode amf/nvenc, capture idd_push/synthetic_nv12, vdisplay manager) rewire to pf_gpu::resolve_render_adapter_luid and win_adapter.rs is deleted. pf-frame's make_device gains a # Safety section (public-unsafe-fn lint, latent since the pf-frame carve — a full-workspace -D warnings clippy catches it). Verified: Linux clippy -D warnings (pf-encode + host nvenc,vulkan-encode,pyrowave --all-targets) + 13/13 pf-encode + 299/299 host tests; Windows clippy -D warnings (pf-encode nvenc,amf-qsv --all-targets + host nvenc,amf-qsv --all-targets) Finished exit 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
246 lines
12 KiB
Rust
246 lines
12 KiB
Rust
//! Shared direct-SDK NVENC core — the platform-agnostic pieces of the two `nvEncodeAPI` backends,
|
|
//! Windows D3D11 (`encode/windows/nvenc.rs`) and Linux CUDA (`encode/linux/nvenc_cuda.rs`), so the
|
|
//! byte-identical glue lives once (plan §2.2, the direct-NVENC Tier-2). The per-platform parts —
|
|
//! the entry-table load (`nvEncodeAPI64.dll` via `LoadLibrary` vs `libnvidia-encode.so` via
|
|
//! `libloading`), the device binding (D3D11 vs CUDA), input-surface registration, and the
|
|
//! Windows-only async retrieve — stay in their backends. Sibling of [`super::nvenc_status`].
|
|
|
|
use super::Codec;
|
|
use nvidia_video_codec_sdk::sys::nvEncodeAPI as nv;
|
|
|
|
/// Local `NVENCSTATUS` → `Result` (replaces the sdk's `result_without_string`, which lives in the
|
|
/// crate's `safe` module — code these backends must not pull in). The raw status's Debug repr
|
|
/// (`NV_ENC_ERR_INVALID_PARAM`, …) is the error payload; callers fold it through
|
|
/// [`super::nvenc_status`] for an operator-actionable cause.
|
|
pub(super) trait NvStatusExt {
|
|
fn nv_ok(self) -> std::result::Result<(), nv::NVENCSTATUS>;
|
|
}
|
|
impl NvStatusExt for nv::NVENCSTATUS {
|
|
fn nv_ok(self) -> std::result::Result<(), nv::NVENCSTATUS> {
|
|
match self {
|
|
nv::NVENCSTATUS::NV_ENC_SUCCESS => Ok(()),
|
|
err => Err(err),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The NVENC codec GUID for a session [`Codec`]. PyroWave never opens the direct-NVENC backend
|
|
/// (guarded by the `open_video` dispatch), so it is unreachable here.
|
|
pub(super) fn codec_guid(codec: Codec) -> nv::GUID {
|
|
match codec {
|
|
Codec::H264 => nv::NV_ENC_CODEC_H264_GUID,
|
|
Codec::H265 => nv::NV_ENC_CODEC_HEVC_GUID,
|
|
Codec::Av1 => nv::NV_ENC_CODEC_AV1_GUID,
|
|
Codec::PyroWave => unreachable!("PyroWave never opens the direct-NVENC backend"),
|
|
}
|
|
}
|
|
|
|
/// 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`
|
|
/// paths check against (`frame_idx - RFI_DPB` = the oldest frame still in the DPB).
|
|
pub(super) const RFI_DPB: u32 = 5;
|
|
|
|
/// The per-session knobs both direct-NVENC backends feed [`apply_low_latency_config`]. `Copy` so the
|
|
/// backend fills it from `self` at the call. The two input-format fields bridge the only real
|
|
/// divergence between the CUDA and D3D11 paths (which surface formats can carry full chroma / 10-bit
|
|
/// input); everything else in the config is identical across platforms.
|
|
#[derive(Clone, Copy)]
|
|
pub(super) struct LowLatencyConfig {
|
|
pub codec: Codec,
|
|
/// Target bitrate (bps); CBR average == max.
|
|
pub bitrate: u64,
|
|
pub fps: u32,
|
|
/// This GPU advertises custom VBV — else leave the preset default (per the caps probe).
|
|
pub custom_vbv: bool,
|
|
/// A 4:4:4 session was negotiated (HEVC Range Extensions).
|
|
pub chroma_444: bool,
|
|
/// The input surface can carry full chroma — Linux feeds a YUV444 surface, Windows a packed-RGB
|
|
/// surface NVENC CSCs internally. 4:4:4 engages only when this AND [`chroma_444`](Self::chroma_444).
|
|
pub full_chroma_input: bool,
|
|
/// Output bit depth (8 or 10).
|
|
pub bit_depth: u8,
|
|
/// AV1 `inputPixelBitDepthMinus8` — the encoder's view of the INPUT depth (Linux is 8-bit in
|
|
/// today, so 0; Windows derives it from the surface format). `u32` to match the SDK setter.
|
|
pub av1_input_depth_minus8: u32,
|
|
pub hdr: bool,
|
|
/// This GPU supports reference-frame invalidation (a deeper DPB for graceful loss recovery).
|
|
pub rfi_supported: bool,
|
|
}
|
|
|
|
/// Author the shared `NV_ENC_INITIALIZE_PARAMS` (P1/ULL preset, PTD, the session dimensions/rate)
|
|
/// pointing at `cfg`. `enable_async` drives the Windows two-thread async retrieve — Linux is
|
|
/// sync-only and passes `false`, leaving `enableEncodeAsync` at 0 as before. The returned struct
|
|
/// borrows `cfg` as a raw pointer; the caller must keep `cfg` alive across the NVENC call it feeds
|
|
/// this into. Used at open and at in-place reconfigure, which must present the SAME init params.
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub(super) fn build_init_params(
|
|
codec_guid: nv::GUID,
|
|
width: u32,
|
|
height: u32,
|
|
fps: u32,
|
|
cfg: &mut nv::NV_ENC_CONFIG,
|
|
split_mode: u32,
|
|
enable_async: bool,
|
|
) -> nv::NV_ENC_INITIALIZE_PARAMS {
|
|
let mut init = nv::NV_ENC_INITIALIZE_PARAMS {
|
|
version: nv::NV_ENC_INITIALIZE_PARAMS_VER,
|
|
encodeGUID: codec_guid,
|
|
presetGUID: nv::NV_ENC_PRESET_P1_GUID,
|
|
tuningInfo: nv::NV_ENC_TUNING_INFO::NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY,
|
|
encodeWidth: width,
|
|
encodeHeight: height,
|
|
darWidth: width,
|
|
darHeight: height,
|
|
frameRateNum: fps,
|
|
frameRateDen: 1,
|
|
enablePTD: 1,
|
|
enableEncodeAsync: enable_async as u32,
|
|
encodeConfig: cfg,
|
|
..Default::default()
|
|
};
|
|
// splitEncodeMode is a C bitfield — set via the generated accessor, not a struct field.
|
|
init.set_splitEncodeMode(split_mode);
|
|
init
|
|
}
|
|
|
|
/// Author the shared low-latency NVENC config onto a **preset-seeded** `cfg`: CBR + infinite GOP +
|
|
/// P-only + ~1-frame VBV, per-codec tier/level, chroma + bit depth, unconditional colour signaling,
|
|
/// and the RFI DPB. The caller seeds `cfg` from the P1/ULL preset first (that call needs the
|
|
/// per-platform entry table) and passes the input-format specifics via [`LowLatencyConfig`]; the
|
|
/// per-platform surface registration, device binding, and async retrieve stay in the backends.
|
|
///
|
|
/// # Safety
|
|
/// Writes NVENC codec-config union fields on `cfg`, which must be a valid, preset-seeded
|
|
/// `NV_ENC_CONFIG` whose active union arm matches [`LowLatencyConfig::codec`].
|
|
pub(super) unsafe fn apply_low_latency_config(cfg: &mut nv::NV_ENC_CONFIG, c: LowLatencyConfig) {
|
|
// CBR, infinite GOP, P-only, ~1-frame VBV — mirrors the AMF/VAAPI/QSV libav RC config.
|
|
cfg.gopLength = nv::NVENC_INFINITE_GOPLENGTH;
|
|
cfg.frameIntervalP = 1;
|
|
cfg.rcParams.rateControlMode = nv::NV_ENC_PARAMS_RC_MODE::NV_ENC_PARAMS_RC_CBR;
|
|
let bps = c.bitrate.min(u32::MAX as u64) as u32;
|
|
cfg.rcParams.averageBitRate = bps;
|
|
cfg.rcParams.maxBitRate = bps;
|
|
// Shrink the VBV with the bitrate (NVENC validates it against the same level ceiling), but only
|
|
// when the GPU advertises custom-VBV support — else keep the preset default.
|
|
if c.custom_vbv {
|
|
// ~1-frame VBV by default; PUNKTFUNK_VBV_FRAMES scales it (parity with AMF/VAAPI/QSV).
|
|
let vbv = ((c.bitrate as f64 / c.fps.max(1) as f64) * crate::vbv_frames_env())
|
|
.clamp(1.0, u32::MAX as f64) as u32;
|
|
cfg.rcParams.vbvBufferSize = vbv;
|
|
cfg.rcParams.vbvInitialDelay = vbv;
|
|
}
|
|
|
|
// Tier + autoselect level, PER CODEC (the union writes must match the negotiated codec). HEVC
|
|
// keeps HIGH tier for its higher per-level bitrate ceiling (Main at 5K ≈ 240 Mbps, HIGH ≈ 800
|
|
// Mbps); AV1 supports the Main tier ONLY — tier=1 fails the session open with INVALID_PARAM, and
|
|
// its level enum's 0 is LEVEL 2.0 (NOT autoselect), so AV1 takes NO writes (the preset defaults
|
|
// are the only accepted config). H.264 has no tier. Level 0 = autoselect for HEVC.
|
|
match c.codec {
|
|
Codec::H265 => {
|
|
cfg.encodeCodecConfig.hevcConfig.tier = 1;
|
|
cfg.encodeCodecConfig.hevcConfig.level = 0;
|
|
}
|
|
Codec::Av1 => {}
|
|
Codec::H264 => {}
|
|
Codec::PyroWave => unreachable!("PyroWave never opens the direct-NVENC backend"),
|
|
}
|
|
|
|
// Chroma + bit depth. Full-chroma 4:4:4 (HEVC Range Extensions, chromaFormatIDC=3 under the FREXT
|
|
// profile) takes precedence and composes with 10-bit (Main 4:4:4 10); it needs a full-chroma-
|
|
// capable input. Otherwise 10-bit selects Main10 (HEVC) or the AV1 output depth — stamping the
|
|
// HEVC Main10 GUID onto an AV1 session is an INVALID_PARAM, so bit depth is set PER CODEC.
|
|
if c.chroma_444 && c.full_chroma_input {
|
|
cfg.profileGUID = nv::NV_ENC_HEVC_PROFILE_FREXT_GUID;
|
|
cfg.encodeCodecConfig.hevcConfig.set_chromaFormatIDC(3);
|
|
if c.bit_depth == 10 {
|
|
cfg.encodeCodecConfig.hevcConfig.set_pixelBitDepthMinus8(2); // Main 4:4:4 10
|
|
}
|
|
} else if c.bit_depth == 10 {
|
|
match c.codec {
|
|
Codec::H265 => {
|
|
cfg.profileGUID = nv::NV_ENC_HEVC_PROFILE_MAIN10_GUID;
|
|
cfg.encodeCodecConfig.hevcConfig.set_pixelBitDepthMinus8(2);
|
|
}
|
|
Codec::Av1 => {
|
|
cfg.encodeCodecConfig.av1Config.set_pixelBitDepthMinus8(2);
|
|
cfg.encodeCodecConfig
|
|
.av1Config
|
|
.set_inputPixelBitDepthMinus8(c.av1_input_depth_minus8);
|
|
}
|
|
Codec::H264 => {} // no 10-bit H.264 encode on NVENC — negotiation never asks
|
|
Codec::PyroWave => unreachable!("PyroWave never opens the direct-NVENC backend"),
|
|
}
|
|
}
|
|
|
|
// Colour signaling, written UNCONDITIONALLY: the input is already CSC'd to a specific matrix
|
|
// (BT.709 limited SDR, or BT.2020 PQ for HDR), so the stream must say so — a decoder whose
|
|
// "unspecified" default is 601 (Moonlight/third-party/Android-vendor at sub-HD) otherwise
|
|
// mis-renders. HEVC/H.264 carry it in the VUI; AV1 has no VUI, so the same CICP code points go in
|
|
// the sequence-header colour config.
|
|
{
|
|
let (prim, trc, mat) = if c.hdr {
|
|
(
|
|
nv::NV_ENC_VUI_COLOR_PRIMARIES::NV_ENC_VUI_COLOR_PRIMARIES_BT2020,
|
|
nv::NV_ENC_VUI_TRANSFER_CHARACTERISTIC::NV_ENC_VUI_TRANSFER_CHARACTERISTIC_SMPTE2084,
|
|
nv::NV_ENC_VUI_MATRIX_COEFFS::NV_ENC_VUI_MATRIX_COEFFS_BT2020_NCL,
|
|
)
|
|
} else {
|
|
(
|
|
nv::NV_ENC_VUI_COLOR_PRIMARIES::NV_ENC_VUI_COLOR_PRIMARIES_BT709,
|
|
nv::NV_ENC_VUI_TRANSFER_CHARACTERISTIC::NV_ENC_VUI_TRANSFER_CHARACTERISTIC_BT709,
|
|
nv::NV_ENC_VUI_MATRIX_COEFFS::NV_ENC_VUI_MATRIX_COEFFS_BT709,
|
|
)
|
|
};
|
|
match c.codec {
|
|
Codec::H265 => {
|
|
let vui = &mut cfg.encodeCodecConfig.hevcConfig.hevcVUIParameters;
|
|
vui.videoSignalTypePresentFlag = 1;
|
|
vui.videoFullRangeFlag = 0;
|
|
vui.colourDescriptionPresentFlag = 1;
|
|
vui.colourPrimaries = prim;
|
|
vui.transferCharacteristics = trc;
|
|
vui.colourMatrix = mat;
|
|
}
|
|
Codec::H264 => {
|
|
let vui = &mut cfg.encodeCodecConfig.h264Config.h264VUIParameters;
|
|
vui.videoSignalTypePresentFlag = 1;
|
|
vui.videoFullRangeFlag = 0;
|
|
vui.colourDescriptionPresentFlag = 1;
|
|
vui.colourPrimaries = prim;
|
|
vui.transferCharacteristics = trc;
|
|
vui.colourMatrix = mat;
|
|
}
|
|
Codec::Av1 => {
|
|
let av1 = &mut cfg.encodeCodecConfig.av1Config;
|
|
av1.colorPrimaries = prim;
|
|
av1.transferCharacteristics = trc;
|
|
av1.matrixCoefficients = mat;
|
|
av1.colorRange = 0; // studio/limited swing
|
|
}
|
|
Codec::PyroWave => unreachable!("PyroWave never opens the direct-NVENC backend"),
|
|
}
|
|
}
|
|
|
|
// Reference-frame invalidation: a deeper DPB so an invalidated reference can fall back to an
|
|
// older still-valid frame instead of a full IDR; `numRefL0 = 1` keeps each P-frame
|
|
// single-reference for low latency. Only when this GPU supports RFI.
|
|
if c.rfi_supported {
|
|
let one = nv::NV_ENC_NUM_REF_FRAMES::NV_ENC_NUM_REF_FRAMES_1;
|
|
match c.codec {
|
|
Codec::H264 => {
|
|
cfg.encodeCodecConfig.h264Config.maxNumRefFrames = RFI_DPB;
|
|
cfg.encodeCodecConfig.h264Config.numRefL0 = one;
|
|
}
|
|
Codec::H265 => {
|
|
cfg.encodeCodecConfig.hevcConfig.maxNumRefFramesInDPB = RFI_DPB;
|
|
cfg.encodeCodecConfig.hevcConfig.numRefL0 = one;
|
|
}
|
|
Codec::Av1 => {
|
|
cfg.encodeCodecConfig.av1Config.maxNumRefFramesInDPB = RFI_DPB;
|
|
}
|
|
Codec::PyroWave => unreachable!("PyroWave never opens the direct-NVENC backend"),
|
|
}
|
|
}
|
|
}
|