refactor(host/W6.2): extract the video encode backends into the pf-encode crate
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>
This commit is contained in:
Generated
+23
-4
@@ -2799,6 +2799,28 @@ dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pf-encode"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"ash",
|
||||
"ffmpeg-next",
|
||||
"libc",
|
||||
"libloading",
|
||||
"nvidia-video-codec-sdk",
|
||||
"openh264",
|
||||
"pf-frame",
|
||||
"pf-gpu",
|
||||
"pf-host-config",
|
||||
"pf-zerocopy",
|
||||
"punktfunk-core",
|
||||
"pyrowave-sys",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"windows 0.62.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pf-ffvk"
|
||||
version = "0.12.0"
|
||||
@@ -3166,7 +3188,6 @@ dependencies = [
|
||||
"base64",
|
||||
"bytemuck",
|
||||
"cbc",
|
||||
"ffmpeg-next",
|
||||
"futures-util",
|
||||
"hex",
|
||||
"hmac",
|
||||
@@ -3180,11 +3201,10 @@ dependencies = [
|
||||
"log",
|
||||
"mac_address",
|
||||
"mdns-sd",
|
||||
"nvidia-video-codec-sdk",
|
||||
"openh264",
|
||||
"opus",
|
||||
"parking_lot",
|
||||
"pf-driver-proto",
|
||||
"pf-encode",
|
||||
"pf-frame",
|
||||
"pf-gpu",
|
||||
"pf-host-config",
|
||||
@@ -3193,7 +3213,6 @@ dependencies = [
|
||||
"pf-zerocopy",
|
||||
"pipewire",
|
||||
"punktfunk-core",
|
||||
"pyrowave-sys",
|
||||
"quinn",
|
||||
"rand 0.8.6",
|
||||
"rcgen",
|
||||
|
||||
@@ -16,6 +16,7 @@ members = [
|
||||
"crates/pf-zerocopy",
|
||||
"crates/pf-frame",
|
||||
"crates/pf-win-display",
|
||||
"crates/pf-encode",
|
||||
"crates/pyrowave-sys",
|
||||
"clients/probe",
|
||||
"clients/linux",
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
# Hardware/software video encode (plan §7 / §W6): the per-vendor backends (NVENC, VAAPI, AMF, QSV,
|
||||
# Vulkan-Video, PyroWave, openh264) behind one `Encoder` trait + `open_video` selector, extracted
|
||||
# from the host so it depends on the shared frame vocabulary (pf-frame) rather than living inside
|
||||
# the orchestrator. Speaks pf-frame (CapturedFrame/PixelFormat/dxgi identity) and pf-zerocopy
|
||||
# (CUDA), never pf-capture — the capture→encode edge is one-way (plan §2.4).
|
||||
[package]
|
||||
name = "pf-encode"
|
||||
version = "0.12.0"
|
||||
edition = "2021"
|
||||
rust-version.workspace = true
|
||||
license = "MIT OR Apache-2.0"
|
||||
description = "punktfunk host video encode: NVENC/VAAPI/AMF/QSV/Vulkan-Video/PyroWave/openh264 backends behind one Encoder trait."
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
punktfunk-core = { path = "../punktfunk-core", features = ["quic"] }
|
||||
pf-frame = { path = "../pf-frame" }
|
||||
pf-gpu = { path = "../pf-gpu" }
|
||||
pf-host-config = { path = "../pf-host-config" }
|
||||
pf-zerocopy = { path = "../pf-zerocopy" }
|
||||
anyhow = "1"
|
||||
tracing = "0.1"
|
||||
|
||||
[dev-dependencies]
|
||||
# A test writer for the NVENC backend's unit tests (`with_test_writer().try_init()`).
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "windows"))'.dependencies]
|
||||
# Software H.264 (openh264, BSD-2) — the GPU-less encode path on both platforms.
|
||||
openh264 = "0.9"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
# libavcodec (NVENC libav + VAAPI backends). `ffmpeg-sys-next` auto-detects the FFmpeg version.
|
||||
ffmpeg-next = "8"
|
||||
libc = "0.2"
|
||||
# Vulkan bindings for the raw Vulkan-Video encode + PyroWave compute backends (feature-gated below;
|
||||
# the dep stays unconditional to mirror the host's Linux target — unused-but-declared is harmless).
|
||||
ash = "0.38"
|
||||
# `libnvidia-encode.so.1` is dlopen'd at runtime for the direct-SDK NVENC/CUDA backend.
|
||||
libloading = "0.8"
|
||||
# Direct-SDK NVENC (raw `sys::nvEncodeAPI` types; entry points resolved at runtime). `ci-check` =
|
||||
# vendored bindings, no CUDA toolkit at build.
|
||||
nvidia-video-codec-sdk = { version = "0.4", features = ["ci-check"], optional = true }
|
||||
# PyroWave (opt-in wired-LAN wavelet codec) — vendored codec + bindgen'd C API, only under `pyrowave`.
|
||||
pyrowave-sys = { path = "../pyrowave-sys", optional = true }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
# NVENC (direct SDK, D3D11 input) + the shared D3D11/DXGI vocabulary via pf-frame.
|
||||
nvidia-video-codec-sdk = { version = "0.4", features = ["ci-check"], optional = true }
|
||||
# AMD (AMF) + Intel (QSV) hardware encode via libavcodec (behind `amf-qsv`; link-imports FFmpeg).
|
||||
ffmpeg-next = { version = "8", optional = true }
|
||||
# `libnvidia-encode`/`nvEncodeAPI64.dll` resolved at runtime; the NVENC status→cause table dlopen.
|
||||
libloading = "0.8"
|
||||
windows = { version = "0.62", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Graphics_Direct3D",
|
||||
"Win32_Graphics_Direct3D11",
|
||||
"Win32_Graphics_Dxgi",
|
||||
"Win32_Graphics_Dxgi_Common",
|
||||
"Win32_Storage_FileSystem",
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_System_Threading",
|
||||
] }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
# NVENC hardware encode (Linux CUDA + Windows D3D11); entry points resolved at runtime.
|
||||
nvenc = ["dep:nvidia-video-codec-sdk"]
|
||||
# AMD (AMF) + Intel (QSV) hardware encode on Windows via libavcodec.
|
||||
amf-qsv = ["dep:ffmpeg-next"]
|
||||
# Raw Vulkan-Video HEVC/AV1 encode on Linux (reuses the `ash` bindings; no new dep).
|
||||
vulkan-encode = []
|
||||
# PyroWave — the opt-in wired-LAN intra-only wavelet codec (Linux encode backend).
|
||||
pyrowave = ["dep:pyrowave-sys"]
|
||||
@@ -1,11 +1,11 @@
|
||||
//! The encoder contract (plan §7, Tier 1): the [`Encoder`] trait plus the plain-data value types its
|
||||
//! signatures use — [`EncodedFrame`], [`Codec`], [`ChromaFormat`], [`EncoderCaps`] — and the
|
||||
//! dimension/VBV helpers [`validate_dimensions`] and [`vbv_frames_env`]. Backend selection, the
|
||||
//! capability probes that mirror it, and `Codec::host_wire_caps` stay in the parent [`crate::encode`]
|
||||
//! facade, which re-exports this module (`pub(crate) use codec::*;`) so every `crate::encode::*` path
|
||||
//! capability probes that mirror it, and `Codec::host_wire_caps` stay in the parent the `pf-encode` crate root
|
||||
//! facade, which re-exports this module (`pub(crate) use codec::*;`) so every `crate::*` path
|
||||
//! is unchanged.
|
||||
use crate::capture::CapturedFrame;
|
||||
use anyhow::Result;
|
||||
use pf_frame::CapturedFrame;
|
||||
|
||||
/// An encoded access unit (one NAL/AU) to hand to `punktfunk_core` for FEC + packetization.
|
||||
/// `data` is in-band Annex-B (the encoder is opened without a global header), so each
|
||||
@@ -94,7 +94,7 @@ impl Codec {
|
||||
}
|
||||
|
||||
/// Lowercase stats/console label (`"h264"` / `"hevc"` / `"av1"`) — the codec string seeded into
|
||||
/// the web console's session meta ([`crate::stats_recorder::StatsRecorder::register_session`]).
|
||||
/// the web console's session meta (the host `stats_recorder::StatsRecorder::register_session`).
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
Codec::H264 => "h264",
|
||||
@@ -108,7 +108,7 @@ impl Codec {
|
||||
/// H.264 is always 8-bit (High10 is neither an NVENC nor a VCN encode mode — negotiation
|
||||
/// never asks), and PyroWave's wavelet path ingests 8-bit. `true` here is only the
|
||||
/// *codec-level* gate: the active GPU/backend must still pass
|
||||
/// [`can_encode_10bit`](crate::encode::can_encode_10bit) before the host negotiates 10-bit.
|
||||
/// [`can_encode_10bit`](crate::can_encode_10bit) before the host negotiates 10-bit.
|
||||
pub fn supports_10bit(self) -> bool {
|
||||
matches!(self, Codec::H265 | Codec::Av1)
|
||||
}
|
||||
@@ -311,7 +311,7 @@ impl Codec {
|
||||
}
|
||||
|
||||
/// The codec's *spec* top level/tier bitrate (bits/s) — the usual boundary at which NVENC
|
||||
/// starts rejecting `avcodec_open2` with EINVAL. NOT a hard cap: [`open_video`](crate::encode::
|
||||
/// starts rejecting `avcodec_open2` with EINVAL. NOT a hard cap: [`open_video`](crate::
|
||||
/// open_video) probes the actual GPU ceiling by stepping DOWN from the requested bitrate only on
|
||||
/// EINVAL, and uses this purely as the first step-down candidate (so a card that accepts more —
|
||||
/// an RTX 5070 Ti does >1 Gbps HEVC where a 4090 caps at ~800 Mbps — is never clamped to it).
|
||||
@@ -3,7 +3,7 @@
|
||||
//! (`encode/windows/ffmpeg_win.rs`) — so the byte-identical pieces live once (plan §2.2, the Tier-2
|
||||
//! gap). Free functions and consts over borrowed handles; nothing here is per-frame `dyn`,
|
||||
//! allocating, or on the zero-copy ingest path.
|
||||
use crate::encode::EncodedFrame;
|
||||
use crate::EncodedFrame;
|
||||
use anyhow::{Context, Result};
|
||||
use ffmpeg_next as ffmpeg;
|
||||
use ffmpeg_next::ffi; // = ffmpeg_sys_next
|
||||
@@ -54,7 +54,7 @@ pub(crate) fn apply_low_latency_rc(video: &mut encoder::video::Video, fps: u32,
|
||||
video.set_bit_rate(bitrate_bps as usize);
|
||||
video.set_max_bit_rate(bitrate_bps as usize);
|
||||
video.set_max_b_frames(0);
|
||||
let vbv_bits = ((bitrate_bps as f64 / fps.max(1) as f64) * crate::encode::vbv_frames_env())
|
||||
let vbv_bits = ((bitrate_bps as f64 / fps.max(1) as f64) * crate::vbv_frames_env())
|
||||
.clamp(1.0, i32::MAX as f64);
|
||||
// SAFETY: `video` wraps a freshly-allocated `AVCodecContext` we hold by value and have not opened
|
||||
// yet; `as_mut_ptr()` returns that non-null, aligned, exclusively-owned context. Writing the plain
|
||||
+12
-17
@@ -12,12 +12,12 @@
|
||||
#![deny(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
use super::{ChromaFormat, Codec, EncodedFrame, Encoder};
|
||||
use crate::capture::{CapturedFrame, FramePayload, PixelFormat};
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use ffmpeg::format::Pixel;
|
||||
use ffmpeg::util::frame::Video as VideoFrame;
|
||||
use ffmpeg::{codec, encoder, Dictionary};
|
||||
use ffmpeg_next as ffmpeg;
|
||||
use pf_frame::{CapturedFrame, FramePayload, PixelFormat};
|
||||
use std::os::raw::c_int;
|
||||
use std::ptr;
|
||||
|
||||
@@ -347,7 +347,7 @@ impl NvencEncoder {
|
||||
// hwdevice/hwframes contexts and set `pix_fmt = CUDA` on the raw encoder context
|
||||
// *before* open (NVENC derives the device from `hw_frames_ctx`).
|
||||
let cuda_hw = if cuda {
|
||||
let cu_ctx = crate::zerocopy::cuda::context().context("shared CUDA context")?;
|
||||
let cu_ctx = pf_zerocopy::cuda::context().context("shared CUDA context")?;
|
||||
// SAFETY: `CudaHw::new` (an `unsafe fn`) requires libav initialized (the `ffmpeg::init()`
|
||||
// above ran) and a valid `CUcontext`; `cu_ctx` is the shared importer context from
|
||||
// `zerocopy::cuda::context()?`, non-null on the `Ok` path. `nvenc_pixel` is a valid `Pixel`
|
||||
@@ -722,12 +722,7 @@ impl NvencEncoder {
|
||||
/// device pointer with a bounded table, so a fresh pointer every frame would thrash/overflow
|
||||
/// it — the pool recycles a small set of pointers. The extra copy is device-local (~8 MB at
|
||||
/// 1080p, sub-millisecond on the GPU) and keeps the host fully off the pixel path.
|
||||
fn submit_cuda(
|
||||
&mut self,
|
||||
buf: &crate::zerocopy::DeviceBuffer,
|
||||
pts: i64,
|
||||
idr: bool,
|
||||
) -> Result<()> {
|
||||
fn submit_cuda(&mut self, buf: &pf_zerocopy::DeviceBuffer, pts: i64, idr: bool) -> Result<()> {
|
||||
let frames_ref = self
|
||||
.cuda
|
||||
.as_ref()
|
||||
@@ -735,7 +730,7 @@ impl NvencEncoder {
|
||||
.frames_ref;
|
||||
// The device→device copy below uses our shared context directly; make it current on the
|
||||
// encode thread (ffmpeg pushes its own around the pool alloc, so order is fine).
|
||||
crate::zerocopy::cuda::make_current().context("CUDA context current (encode thread)")?;
|
||||
pf_zerocopy::cuda::make_current().context("CUDA context current (encode thread)")?;
|
||||
// SAFETY: `frames_ref` is the non-null CUDA frames ctx from `self.cuda` (unwrapped via
|
||||
// `.context(..)?` above), and the shared CUDA context was just made current on THIS thread
|
||||
// (`make_current()?`), the precondition for the device-pointer copies below.
|
||||
@@ -770,11 +765,11 @@ impl NvencEncoder {
|
||||
let copy_res = if buf.yuv444 {
|
||||
let dsts = core::array::from_fn(|i| {
|
||||
(
|
||||
(*f).data[i] as crate::zerocopy::cuda::CUdeviceptr,
|
||||
(*f).data[i] as pf_zerocopy::cuda::CUdeviceptr,
|
||||
(*f).linesize[i] as usize,
|
||||
)
|
||||
});
|
||||
crate::zerocopy::cuda::copy_yuv444_to_device(buf, dsts)
|
||||
pf_zerocopy::cuda::copy_yuv444_to_device(buf, dsts)
|
||||
} else if self.want_444 {
|
||||
ffi::av_frame_free(&mut f);
|
||||
bail!(
|
||||
@@ -783,15 +778,15 @@ impl NvencEncoder {
|
||||
CPU 4:4:4 path on this compositor"
|
||||
);
|
||||
} else if buf.is_nv12() {
|
||||
let y_ptr = (*f).data[0] as crate::zerocopy::cuda::CUdeviceptr;
|
||||
let y_ptr = (*f).data[0] as pf_zerocopy::cuda::CUdeviceptr;
|
||||
let y_pitch = (*f).linesize[0] as usize;
|
||||
let uv_ptr = (*f).data[1] as crate::zerocopy::cuda::CUdeviceptr;
|
||||
let uv_ptr = (*f).data[1] as pf_zerocopy::cuda::CUdeviceptr;
|
||||
let uv_pitch = (*f).linesize[1] as usize;
|
||||
crate::zerocopy::cuda::copy_nv12_to_device(buf, y_ptr, y_pitch, uv_ptr, uv_pitch)
|
||||
pf_zerocopy::cuda::copy_nv12_to_device(buf, y_ptr, y_pitch, uv_ptr, uv_pitch)
|
||||
} else {
|
||||
let dst_ptr = (*f).data[0] as crate::zerocopy::cuda::CUdeviceptr;
|
||||
let dst_ptr = (*f).data[0] as pf_zerocopy::cuda::CUdeviceptr;
|
||||
let dst_pitch = (*f).linesize[0] as usize;
|
||||
crate::zerocopy::cuda::copy_device_to_device(buf, dst_ptr, dst_pitch)
|
||||
pf_zerocopy::cuda::copy_device_to_device(buf, dst_ptr, dst_pitch)
|
||||
};
|
||||
if let Err(e) = copy_res {
|
||||
ffi::av_frame_free(&mut f);
|
||||
@@ -827,7 +822,7 @@ impl Drop for NvencEncoder {
|
||||
/// Probe whether this NVIDIA GPU + driver + libavcodec can actually encode HEVC **4:4:4** (Range
|
||||
/// Extensions). Opens a tiny real `hevc_nvenc` 4:4:4 session — the exact path [`NvencEncoder::open`]
|
||||
/// takes for a live 4:4:4 stream — and reports whether it succeeded. HEVC-only; the result is cached
|
||||
/// by the caller ([`crate::encode::can_encode_444`]). A GPU/driver/ffmpeg without RExt 4:4:4 fails
|
||||
/// by the caller ([`crate::can_encode_444`]). A GPU/driver/ffmpeg without RExt 4:4:4 fails
|
||||
/// the open here, so the host resolves the session to 4:2:0 before the Welcome (honest downgrade).
|
||||
pub fn probe_can_encode_444(codec: Codec) -> bool {
|
||||
if codec != Codec::H265 {
|
||||
+11
-11
@@ -36,9 +36,9 @@ use super::nvenc_core::{
|
||||
};
|
||||
use super::nvenc_status;
|
||||
use super::{ChromaFormat, Codec, EncodedFrame, Encoder, EncoderCaps};
|
||||
use crate::capture::{CapturedFrame, FramePayload};
|
||||
use crate::zerocopy::cuda::{self, InputSurface};
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use pf_frame::{CapturedFrame, FramePayload};
|
||||
use pf_zerocopy::cuda::{self, InputSurface};
|
||||
use std::collections::VecDeque;
|
||||
use std::ffi::c_void;
|
||||
use std::ptr;
|
||||
@@ -321,7 +321,7 @@ impl NvencCudaEncoder {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn open(
|
||||
codec: Codec,
|
||||
_format: crate::capture::PixelFormat,
|
||||
_format: pf_frame::PixelFormat,
|
||||
width: u32,
|
||||
height: u32,
|
||||
fps: u32,
|
||||
@@ -1253,8 +1253,8 @@ impl Drop for NvencCudaEncoder {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::capture::{CapturedFrame, FramePayload, PixelFormat};
|
||||
use crate::zerocopy::cuda::DeviceBuffer;
|
||||
use pf_frame::{CapturedFrame, FramePayload, PixelFormat};
|
||||
use pf_zerocopy::cuda::DeviceBuffer;
|
||||
|
||||
fn nv12_frame(w: u32, h: u32, i: u32) -> CapturedFrame {
|
||||
// Content is uninitialized device memory — NVENC encodes it fine; this smoke test asserts the
|
||||
@@ -1281,7 +1281,7 @@ mod tests {
|
||||
fn nvenc_cuda_smoke_rfi_anchor() {
|
||||
const W: u32 = 1280;
|
||||
const H: u32 = 720;
|
||||
crate::zerocopy::cuda::make_current().expect("shared CUDA context current");
|
||||
pf_zerocopy::cuda::make_current().expect("shared CUDA context current");
|
||||
|
||||
let mut enc = NvencCudaEncoder::open(
|
||||
Codec::H265,
|
||||
@@ -1358,7 +1358,7 @@ mod tests {
|
||||
fn nvenc_cuda_yuv444() {
|
||||
const W: u32 = 1280;
|
||||
const H: u32 = 720;
|
||||
crate::zerocopy::cuda::make_current().expect("shared CUDA context current");
|
||||
pf_zerocopy::cuda::make_current().expect("shared CUDA context current");
|
||||
let mut enc = NvencCudaEncoder::open(
|
||||
Codec::H265,
|
||||
PixelFormat::Yuv444,
|
||||
@@ -1403,7 +1403,7 @@ mod tests {
|
||||
fn nvenc_cuda_reconfigure_no_idr() {
|
||||
const W: u32 = 1280;
|
||||
const H: u32 = 720;
|
||||
crate::zerocopy::cuda::make_current().expect("shared CUDA context current");
|
||||
pf_zerocopy::cuda::make_current().expect("shared CUDA context current");
|
||||
let mut enc = NvencCudaEncoder::open(
|
||||
Codec::H265,
|
||||
PixelFormat::Nv12,
|
||||
@@ -1510,7 +1510,7 @@ mod tests {
|
||||
fn nvenc_cuda_codec_switch_reopen() {
|
||||
const W: u32 = 1280;
|
||||
const H: u32 = 720;
|
||||
crate::zerocopy::cuda::make_current().expect("shared CUDA context current");
|
||||
pf_zerocopy::cuda::make_current().expect("shared CUDA context current");
|
||||
for (leg, codec) in [
|
||||
Codec::H265,
|
||||
Codec::Av1,
|
||||
@@ -1552,7 +1552,7 @@ mod tests {
|
||||
fn nvenc_cuda_dirty_teardown_reopen() {
|
||||
const W: u32 = 1280;
|
||||
const H: u32 = 720;
|
||||
crate::zerocopy::cuda::make_current().expect("shared CUDA context current");
|
||||
pf_zerocopy::cuda::make_current().expect("shared CUDA context current");
|
||||
for round in 0..3 {
|
||||
let mut enc = open_h265();
|
||||
for f in 0..4u32 {
|
||||
@@ -1581,7 +1581,7 @@ mod tests {
|
||||
fn nvenc_cuda_open_failure_diagnosis_and_recovery() {
|
||||
const W: u32 = 1280;
|
||||
const H: u32 = 720;
|
||||
crate::zerocopy::cuda::make_current().expect("shared CUDA context current");
|
||||
pf_zerocopy::cuda::make_current().expect("shared CUDA context current");
|
||||
try_api().expect("nvenc api");
|
||||
let shared = cuda::context().expect("shared ctx");
|
||||
|
||||
+5
-8
@@ -24,11 +24,11 @@
|
||||
// Every unsafe block in this module carries a `// SAFETY:` proof (parent module enforces it).
|
||||
|
||||
use super::vk_util::{color_range, find_mem, import_rgb_dmabuf, make_plain_image, pixel_to_vk};
|
||||
use crate::capture::{CapturedFrame, FramePayload};
|
||||
use crate::encode::{EncodedFrame, Encoder, EncoderCaps};
|
||||
use crate::{EncodedFrame, Encoder, EncoderCaps};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use ash::vk;
|
||||
use ash::vk::Handle as _;
|
||||
use pf_frame::{CapturedFrame, FramePayload};
|
||||
use pyrowave_sys as pw;
|
||||
use std::collections::VecDeque;
|
||||
use std::os::fd::AsRawFd;
|
||||
@@ -637,10 +637,7 @@ impl PyroWaveEncoder {
|
||||
/// Records the small upload (only when the bitmap `serial` changed) + layout transition into
|
||||
/// `cmd`, ahead of the CSC dispatch that samples binding 3. Encode is synchronous, so the single
|
||||
/// shared image never races a prior frame; the first use transitions it to SHADER_READ_ONLY.
|
||||
unsafe fn prep_cursor(
|
||||
&mut self,
|
||||
cursor: Option<&crate::capture::CursorOverlay>,
|
||||
) -> Result<[i32; 4]> {
|
||||
unsafe fn prep_cursor(&mut self, cursor: Option<&pf_frame::CursorOverlay>) -> Result<[i32; 4]> {
|
||||
let dev = self.device.clone();
|
||||
let cmd = self.cmd;
|
||||
let img = self.cursor_img;
|
||||
@@ -748,7 +745,7 @@ impl PyroWaveEncoder {
|
||||
/// Import a dmabuf with per-buffer caching — same policy as `vulkan_video.rs::import_cached`.
|
||||
unsafe fn import_cached(
|
||||
&mut self,
|
||||
d: &crate::capture::DmabufFrame,
|
||||
d: &pf_frame::DmabufFrame,
|
||||
cw: u32,
|
||||
ch: u32,
|
||||
) -> Result<(vk::Image, vk::ImageView, bool)> {
|
||||
@@ -1303,7 +1300,7 @@ impl Drop for PyroWaveEncoder {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::capture::PixelFormat;
|
||||
use pf_frame::PixelFormat;
|
||||
|
||||
fn cpu_frame(w: u32, h: u32, pts_ns: u64, fill: [u8; 4]) -> CapturedFrame {
|
||||
let mut buf = vec![0u8; (w * h * 4) as usize];
|
||||
+2
-2
@@ -23,11 +23,11 @@
|
||||
#![deny(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
use super::{Codec, EncodedFrame, Encoder};
|
||||
use crate::capture::{CapturedFrame, DmabufFrame, FramePayload, PixelFormat};
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use ffmpeg::format::Pixel;
|
||||
use ffmpeg::{codec, encoder, Dictionary};
|
||||
use ffmpeg_next as ffmpeg;
|
||||
use pf_frame::{CapturedFrame, DmabufFrame, FramePayload, PixelFormat};
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::os::fd::AsRawFd;
|
||||
use std::os::raw::c_int;
|
||||
@@ -561,7 +561,7 @@ impl DmabufInner {
|
||||
fps: u32,
|
||||
bitrate_bps: u64,
|
||||
) -> Result<Self> {
|
||||
let drm_fourcc = crate::zerocopy::drm_fourcc(format)
|
||||
let drm_fourcc = pf_frame::drm_fourcc(format)
|
||||
.ok_or_else(|| anyhow!("no DRM fourcc for {format:?} (VAAPI zero-copy)"))?;
|
||||
let node = render_node();
|
||||
// SAFETY: libav is initialized (`VaapiEncoder::open` ran `ffmpeg::init()` before
|
||||
+2
-2
@@ -3,9 +3,9 @@
|
||||
//! when the PyroWave backend arrived so the two don't fork copies.
|
||||
// Every unsafe block carries a `// SAFETY:` proof (parent module enforces it).
|
||||
|
||||
use crate::capture::PixelFormat;
|
||||
use anyhow::Result;
|
||||
use ash::vk;
|
||||
use pf_frame::PixelFormat;
|
||||
|
||||
pub(crate) fn color_range(layer: u32) -> vk::ImageSubresourceRange {
|
||||
vk::ImageSubresourceRange {
|
||||
@@ -74,7 +74,7 @@ pub(crate) unsafe fn import_rgb_dmabuf(
|
||||
device: &ash::Device,
|
||||
ext_fd: &ash::khr::external_memory_fd::Device,
|
||||
mem_props: &vk::PhysicalDeviceMemoryProperties,
|
||||
d: &crate::capture::DmabufFrame,
|
||||
d: &pf_frame::DmabufFrame,
|
||||
cw: u32,
|
||||
ch: u32,
|
||||
) -> Result<(vk::Image, vk::DeviceMemory, vk::ImageView)> {
|
||||
+10
-10
@@ -11,10 +11,10 @@
|
||||
#![allow(clippy::too_many_arguments)]
|
||||
|
||||
use super::vk_util::{color_range, find_mem, make_plain_image, make_view, pixel_to_vk};
|
||||
use crate::capture::{CapturedFrame, FramePayload};
|
||||
use crate::encode::{Codec, EncodedFrame, Encoder, EncoderCaps};
|
||||
use crate::{Codec, EncodedFrame, Encoder, EncoderCaps};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use ash::vk;
|
||||
use pf_frame::{CapturedFrame, FramePayload};
|
||||
use std::collections::VecDeque;
|
||||
use std::ffi::c_void;
|
||||
use std::os::fd::AsRawFd;
|
||||
@@ -729,7 +729,7 @@ impl VulkanVideoEncoder {
|
||||
&mut self,
|
||||
slot: usize,
|
||||
compute_cmd: vk::CommandBuffer,
|
||||
cursor: Option<&crate::capture::CursorOverlay>,
|
||||
cursor: Option<&pf_frame::CursorOverlay>,
|
||||
) -> Result<[i32; 4]> {
|
||||
let dev = self.device.clone();
|
||||
let img = self.frames[slot].cursor_img;
|
||||
@@ -837,7 +837,7 @@ impl VulkanVideoEncoder {
|
||||
/// Import a packed-RGB dmabuf as a SAMPLED VkImage (explicit DRM modifier). Caller destroys.
|
||||
unsafe fn import_dmabuf(
|
||||
&self,
|
||||
d: &crate::capture::DmabufFrame,
|
||||
d: &pf_frame::DmabufFrame,
|
||||
cw: u32,
|
||||
ch: u32,
|
||||
) -> Result<(vk::Image, vk::DeviceMemory, vk::ImageView)> {
|
||||
@@ -850,7 +850,7 @@ impl VulkanVideoEncoder {
|
||||
/// true only on a first import (caller uses UNDEFINED old-layout to preserve modifier-tiled data).
|
||||
unsafe fn import_cached(
|
||||
&mut self,
|
||||
d: &crate::capture::DmabufFrame,
|
||||
d: &pf_frame::DmabufFrame,
|
||||
cw: u32,
|
||||
ch: u32,
|
||||
) -> Result<(vk::Image, vk::ImageView, bool)> {
|
||||
@@ -2680,8 +2680,8 @@ unsafe fn build_parameters_av1(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{build_h265_rps_s0, pick_recovery_slot, VulkanVideoEncoder};
|
||||
use crate::capture::{CapturedFrame, FramePayload, PixelFormat};
|
||||
use crate::encode::{Codec, Encoder};
|
||||
use crate::{Codec, Encoder};
|
||||
use pf_frame::{CapturedFrame, FramePayload, PixelFormat};
|
||||
|
||||
/// The RFI anchor picker: newest resident wire strictly older than the loss; empty/newer
|
||||
/// slots never qualify.
|
||||
@@ -2761,7 +2761,7 @@ mod tests {
|
||||
/// the reference-slot RFI end-to-end; returns the AUs. Wire frame [`SMOKE_LOST`] is "lost", one
|
||||
/// normal P referencing it is still encoded (the in-flight window), then frame [`SMOKE_ANCHOR`]
|
||||
/// is the clean recovery anchor referencing pre-loss frame 3 (no IDR).
|
||||
fn run_smoke(codec: Codec) -> Vec<crate::encode::EncodedFrame> {
|
||||
fn run_smoke(codec: Codec) -> Vec<crate::EncodedFrame> {
|
||||
let env_dim = |k: &str, d: u32| {
|
||||
std::env::var(k)
|
||||
.ok()
|
||||
@@ -2782,7 +2782,7 @@ mod tests {
|
||||
[120, 200, 80, 255],
|
||||
[80, 120, 200, 255],
|
||||
];
|
||||
let mut aus: Vec<crate::encode::EncodedFrame> = Vec::new();
|
||||
let mut aus: Vec<crate::EncodedFrame> = Vec::new();
|
||||
for (i, c) in colors.iter().enumerate() {
|
||||
if i == SMOKE_ANCHOR {
|
||||
// The client reports wire frame SMOKE_LOST lost → the next frame must re-anchor
|
||||
@@ -2836,7 +2836,7 @@ mod tests {
|
||||
/// concealment the client's freeze hides) and NONE at the anchor — a complaint about the
|
||||
/// anchor's reference (frame 3 / POC 3) means reference retention regressed and the "clean"
|
||||
/// re-anchor ships corruption.
|
||||
fn dump_smoke(aus: &[crate::encode::EncodedFrame], ext: &str) {
|
||||
fn dump_smoke(aus: &[crate::EncodedFrame], ext: &str) {
|
||||
let Ok(home) = std::env::var("HOME") else {
|
||||
return;
|
||||
};
|
||||
+1
-1
@@ -125,7 +125,7 @@ pub(super) unsafe fn apply_low_latency_config(cfg: &mut nv::NV_ENC_CONFIG, c: Lo
|
||||
// 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::encode::vbv_frames_env())
|
||||
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;
|
||||
@@ -12,7 +12,6 @@
|
||||
#![deny(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
use super::{EncodedFrame, Encoder};
|
||||
use crate::capture::{CapturedFrame, FramePayload, PixelFormat};
|
||||
use anyhow::{bail, ensure, Context, Result};
|
||||
use openh264::encoder::{
|
||||
BitRate, Complexity, Encoder as Oh264, EncoderConfig, FrameRate, FrameType, IntraFramePeriod,
|
||||
@@ -20,6 +19,7 @@ use openh264::encoder::{
|
||||
};
|
||||
use openh264::formats::YUVSlices;
|
||||
use openh264::OpenH264API;
|
||||
use pf_frame::{CapturedFrame, FramePayload, PixelFormat};
|
||||
use std::collections::VecDeque;
|
||||
|
||||
pub struct OpenH264Encoder {
|
||||
@@ -258,7 +258,7 @@ fn num_threads() -> u16 {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::capture::{CapturedFrame, FramePayload, PixelFormat};
|
||||
use pf_frame::{CapturedFrame, FramePayload, PixelFormat};
|
||||
|
||||
/// The BT.709 limited-range anchor points: reference white → (235,128,128), black →
|
||||
/// (16,128,128), pure red's Cr must hit the positive extreme 240 (it does exactly:
|
||||
+11
-11
@@ -47,8 +47,8 @@
|
||||
#![deny(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
use super::{ChromaFormat, Codec, EncodedFrame, Encoder, EncoderCaps};
|
||||
use crate::capture::{CapturedFrame, FramePayload, PixelFormat};
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use pf_frame::{CapturedFrame, FramePayload, PixelFormat};
|
||||
use std::collections::VecDeque;
|
||||
use std::ffi::c_void;
|
||||
use std::ptr;
|
||||
@@ -1334,7 +1334,7 @@ impl AmfEncoder {
|
||||
/// same shape every backend ships. Shared by [`apply_static_props`](Self::apply_static_props)
|
||||
/// and [`Encoder::reconfigure_bitrate`] so a dynamic retarget rescales the buffer it opened with.
|
||||
fn vbv_bits(&self, bps: u64) -> i64 {
|
||||
((bps as f64 / self.fps.max(1) as f64) * crate::encode::vbv_frames_env())
|
||||
((bps as f64 / self.fps.max(1) as f64) * crate::vbv_frames_env())
|
||||
.clamp(1.0, i32::MAX as f64) as i64
|
||||
}
|
||||
|
||||
@@ -1777,7 +1777,7 @@ fn probe_can_encode_on(device: &ID3D11Device, codec: Codec) -> bool {
|
||||
/// encoder at 10-bit (Main10 profile / `*ColorBitDepth` 10, P010 input)? The driver rejects the
|
||||
/// profile/depth props on VCN generations that can't encode them, so a successful tiny `Init` is
|
||||
/// the honest per-codec answer — read *before* the Welcome by
|
||||
/// [`crate::encode::can_encode_10bit`] so the negotiated bit depth matches what the session's
|
||||
/// [`crate::can_encode_10bit`] so the negotiated bit depth matches what the session's
|
||||
/// encoder will really open. H.264 is always `false` (High10 is not a VCN mode — the session
|
||||
/// open bails on it too).
|
||||
pub fn probe_can_encode_10bit(codec: Codec) -> bool {
|
||||
@@ -1881,8 +1881,8 @@ fn selected_adapter_device() -> Option<ID3D11Device> {
|
||||
// `D3D11CreateDevice` (explicit adapter + UNKNOWN driver type, or NULL adapter + HARDWARE)
|
||||
// fills `device` only on success. Everything drops with its COM wrapper.
|
||||
unsafe {
|
||||
let adapter: Option<IDXGIAdapter1> = crate::win_adapter::resolve_render_adapter_luid()
|
||||
.and_then(|luid| {
|
||||
let adapter: Option<IDXGIAdapter1> =
|
||||
pf_gpu::resolve_render_adapter_luid().and_then(|luid| {
|
||||
let factory: IDXGIFactory4 = CreateDXGIFactory1().ok()?;
|
||||
factory.EnumAdapterByLuid(luid).ok()
|
||||
});
|
||||
@@ -2785,7 +2785,7 @@ mod tests {
|
||||
height: h,
|
||||
pts_ns: 1 + i as u64,
|
||||
format: fmt,
|
||||
payload: FramePayload::D3d11(crate::capture::dxgi::D3d11Frame {
|
||||
payload: FramePayload::D3d11(pf_frame::dxgi::D3d11Frame {
|
||||
texture: tex.clone(),
|
||||
device: device.clone(),
|
||||
}),
|
||||
@@ -2856,8 +2856,8 @@ mod tests {
|
||||
);
|
||||
drop(native);
|
||||
|
||||
let mut ffmpeg = crate::encode::ffmpeg_win::FfmpegWinEncoder::open(
|
||||
crate::encode::ffmpeg_win::WinVendor::Amf,
|
||||
let mut ffmpeg = crate::ffmpeg_win::FfmpegWinEncoder::open(
|
||||
crate::ffmpeg_win::WinVendor::Amf,
|
||||
Codec::H265,
|
||||
PixelFormat::Nv12,
|
||||
w,
|
||||
@@ -2970,7 +2970,7 @@ mod tests {
|
||||
height: h,
|
||||
pts_ns: base + i as u64,
|
||||
format: PixelFormat::Nv12,
|
||||
payload: FramePayload::D3d11(crate::capture::dxgi::D3d11Frame {
|
||||
payload: FramePayload::D3d11(pf_frame::dxgi::D3d11Frame {
|
||||
texture: tex.clone(),
|
||||
device: device.clone(),
|
||||
}),
|
||||
@@ -3111,7 +3111,7 @@ mod tests {
|
||||
height: h,
|
||||
pts_ns: 1 + i as u64,
|
||||
format: PixelFormat::P010,
|
||||
payload: FramePayload::D3d11(crate::capture::dxgi::D3d11Frame {
|
||||
payload: FramePayload::D3d11(pf_frame::dxgi::D3d11Frame {
|
||||
texture: tex.clone(),
|
||||
device: device.clone(),
|
||||
}),
|
||||
@@ -3258,7 +3258,7 @@ mod tests {
|
||||
height: h,
|
||||
pts_ns: i,
|
||||
format: PixelFormat::Nv12,
|
||||
payload: FramePayload::D3d11(crate::capture::dxgi::D3d11Frame {
|
||||
payload: FramePayload::D3d11(pf_frame::dxgi::D3d11Frame {
|
||||
texture: tex.clone(),
|
||||
device: device.clone(),
|
||||
}),
|
||||
+1
-1
@@ -41,11 +41,11 @@
|
||||
#![deny(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
use super::{ChromaFormat, Codec, EncodedFrame, Encoder};
|
||||
use crate::capture::{dxgi::D3d11Frame, CapturedFrame, FramePayload, PixelFormat};
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use ffmpeg::format::Pixel;
|
||||
use ffmpeg::{codec, encoder, Dictionary};
|
||||
use ffmpeg_next as ffmpeg;
|
||||
use pf_frame::{dxgi::D3d11Frame, CapturedFrame, FramePayload, PixelFormat};
|
||||
use std::os::raw::{c_int, c_uint, c_void};
|
||||
use std::ptr;
|
||||
use windows::core::Interface;
|
||||
+8
-8
@@ -41,8 +41,8 @@ use super::nvenc_core::{
|
||||
};
|
||||
use super::nvenc_status;
|
||||
use super::{ChromaFormat, Codec, EncodedFrame, Encoder, EncoderCaps};
|
||||
use crate::capture::{CapturedFrame, FramePayload, PixelFormat};
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use pf_frame::{CapturedFrame, FramePayload, PixelFormat};
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::ffi::c_void;
|
||||
use std::ptr;
|
||||
@@ -1568,7 +1568,7 @@ impl Drop for NvencD3d11Encoder {
|
||||
}
|
||||
|
||||
/// Probe whether the active NVIDIA GPU can encode HEVC **4:4:4** (`NV_ENC_CAPS_SUPPORT_YUV444_ENCODE`).
|
||||
/// HEVC-only; the result is cached by the caller ([`crate::encode::can_encode_444`]) and read *before*
|
||||
/// HEVC-only; the result is cached by the caller ([`crate::can_encode_444`]) and read *before*
|
||||
/// the Welcome so the host advertises the chroma it can really encode (honest downgrade to 4:2:0 on a
|
||||
/// card without it). See [`probe_encode_cap`] for the throwaway-session mechanics.
|
||||
pub fn probe_can_encode_444(codec: Codec) -> bool {
|
||||
@@ -1580,7 +1580,7 @@ pub fn probe_can_encode_444(codec: Codec) -> bool {
|
||||
|
||||
/// Probe whether the active NVIDIA GPU can encode `codec` at **10-bit**
|
||||
/// (`NV_ENC_CAPS_SUPPORT_10BIT_ENCODE` against the codec's own GUID — HEVC Main10 / AV1 10-bit).
|
||||
/// The result is cached by the caller ([`crate::encode::can_encode_10bit`]) and read *before* the
|
||||
/// The result is cached by the caller ([`crate::can_encode_10bit`]) and read *before* the
|
||||
/// Welcome so the negotiated bit depth — and the HDR label derived from it — matches what NVENC
|
||||
/// will really emit. The session-open path re-checks the same cap as a belt-and-braces guard
|
||||
/// ([`NvencD3d11Encoder::probe_caps`]'s 8-bit fallback).
|
||||
@@ -1622,8 +1622,8 @@ fn probe_encode_cap(codec: Codec, cap: nv::NV_ENC_CAPS) -> bool {
|
||||
// Probe on the SELECTED render adapter — the GPU the session will actually encode on
|
||||
// (web-console preference / PUNKTFUNK_RENDER_ADAPTER / max VRAM). The OS default adapter
|
||||
// (NULL) can be the *other* GPU on a hybrid box, answering for hardware we won't use.
|
||||
let adapter: Option<IDXGIAdapter1> = crate::win_adapter::resolve_render_adapter_luid()
|
||||
.and_then(|luid| {
|
||||
let adapter: Option<IDXGIAdapter1> =
|
||||
pf_gpu::resolve_render_adapter_luid().and_then(|luid| {
|
||||
let factory: IDXGIFactory4 = CreateDXGIFactory1().ok()?;
|
||||
factory.EnumAdapterByLuid(luid).ok()
|
||||
});
|
||||
@@ -1692,7 +1692,7 @@ fn probe_encode_cap(codec: Codec, cap: nv::NV_ENC_CAPS) -> bool {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::capture::{dxgi::D3d11Frame, CapturedFrame, FramePayload};
|
||||
use pf_frame::{dxgi::D3d11Frame, CapturedFrame, FramePayload};
|
||||
use windows::Win32::Graphics::Direct3D11::{
|
||||
D3D11_BIND_RENDER_TARGET, D3D11_SUBRESOURCE_DATA, D3D11_TEXTURE2D_DESC, D3D11_USAGE_DEFAULT,
|
||||
};
|
||||
@@ -1760,7 +1760,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
let adapter = adapter.expect("no hardware DXGI adapter");
|
||||
let (device, _ctx) = crate::capture::dxgi::make_device(&adapter).expect("make_device");
|
||||
let (device, _ctx) = pf_frame::dxgi::make_device(&adapter).expect("make_device");
|
||||
|
||||
let bytes = probe_pattern(W as usize, H as usize);
|
||||
let init = D3D11_SUBRESOURCE_DATA {
|
||||
@@ -1860,7 +1860,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
let adapter = adapter.expect("no hardware DXGI adapter");
|
||||
let (device, _ctx) = crate::capture::dxgi::make_device(&adapter).expect("make_device");
|
||||
let (device, _ctx) = pf_frame::dxgi::make_device(&adapter).expect("make_device");
|
||||
|
||||
let bytes = probe_pattern(W as usize, H as usize);
|
||||
let init = D3D11_SUBRESOURCE_DATA {
|
||||
@@ -2,21 +2,27 @@
|
||||
//! B-frames off. The backend is per-GPU: NVENC on NVIDIA (`*_nvenc`, accepts `bgr0` and does
|
||||
//! RGB→YUV on the GPU, so no host-side CSC) and VAAPI on AMD/Intel (`*_vaapi`; the CPU-input
|
||||
//! fallback swscales RGB→NV12, the zero-copy path imports the capture dmabuf straight into a
|
||||
//! VA surface). One [`Encoder`] trait, selected in [`open_video`].
|
||||
//! VA surface). One [`Encoder`] trait, selected in [`open_video`]. Extracted into a subsystem crate
|
||||
//! (plan §W6): depends on the shared frame vocabulary (`pf-frame`) + zero-copy plumbing
|
||||
//! (`pf-zerocopy`), never on capture — the capture→encode edge is one-way.
|
||||
// Scaffold: some backend paths + trait defaults are defined ahead of the per-feature build that
|
||||
// uses them (mirrors the host crate root's allow before the extraction).
|
||||
#![allow(dead_code)]
|
||||
// Every unsafe block in this module tree carries a `// SAFETY:` proof; enforce it (unsafe-proof
|
||||
// program). As a parent module this also covers the child modules (encode::windows/linux::*).
|
||||
// program). As a parent module this also covers the child modules (windows/linux backends).
|
||||
#![deny(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
use crate::capture::{CapturedFrame, PixelFormat};
|
||||
use anyhow::Result;
|
||||
use pf_frame::{CapturedFrame, PixelFormat};
|
||||
|
||||
#[path = "enc/codec.rs"]
|
||||
mod codec;
|
||||
pub(crate) use codec::*;
|
||||
pub use codec::*;
|
||||
|
||||
impl Codec {
|
||||
/// The `quic` codec bitfield the host can currently **emit** on the punktfunk/1 native path,
|
||||
/// given the resolved encode backend — the same GPU-aware advertisement GameStream builds for
|
||||
/// Moonlight ([`crate::gamestream::serverinfo`]), in `quic::CODEC_*` bits. The GPU-less software
|
||||
/// Moonlight (the host `gamestream::serverinfo`), in `quic::CODEC_*` bits. The GPU-less software
|
||||
/// encoder (openh264) produces H.264 only; the probed backends (Linux VAAPI, Windows AMF/QSV)
|
||||
/// advertise exactly what the GPU encodes ([`vaapi_codec_support`] / [`windows_codec_support`] —
|
||||
/// AV1 encode is narrow, an old iGPU might lack HEVC); NVENC keeps the Moonlight-validated
|
||||
@@ -30,7 +36,7 @@ impl Codec {
|
||||
// client explicitly prefers it (resolve_codec ignores the bit in its ladder). Advertised
|
||||
// whenever the backend could open: AMD/Intel capture hands raw dmabufs it imports
|
||||
// directly, and an NVIDIA-auto host's PyroWave sessions flip capture to CPU RGB
|
||||
// per-session instead ([`crate::session_plan::SessionPlan::output_format`]) — the EGL→CUDA
|
||||
// per-session instead (the host `session_plan::SessionPlan::output_format`) — the EGL→CUDA
|
||||
// frames the `auto` GPU path would deliver are NVENC-only. Only a software/GPU-less pref
|
||||
// keeps the bit off (no Vulkan device to open).
|
||||
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
|
||||
@@ -159,7 +165,7 @@ pub fn open_video(
|
||||
}))
|
||||
}
|
||||
|
||||
/// Ties the [`crate::gpu`] live-session record to the encoder's lifetime; pure delegation
|
||||
/// Ties the `pf_gpu` live-session record to the encoder's lifetime; pure delegation
|
||||
/// otherwise.
|
||||
struct TrackedEncoder {
|
||||
inner: Box<dyn Encoder>,
|
||||
@@ -699,7 +705,7 @@ fn linux_auto_is_vaapi() -> bool {
|
||||
/// packed-RGB fourcc — advertised by the capture when the pyrowave passthrough is active
|
||||
/// (the VAAPI LINEAR-only policy starves it on Mutter+NVIDIA, which allocates tiled only).
|
||||
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
|
||||
pub(crate) fn pyrowave_capture_modifiers(fourcc: u32) -> Vec<u64> {
|
||||
pub fn pyrowave_capture_modifiers(fourcc: u32) -> Vec<u64> {
|
||||
pyrowave::capture_modifiers(fourcc)
|
||||
}
|
||||
|
||||
@@ -918,13 +924,13 @@ pub fn can_encode_10bit(_codec: Codec) -> bool {
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Windows backend selection (the analogue of the Linux nvidia_present / linux_zero_copy_is_vaapi
|
||||
// logic). NVIDIA → NVENC, AMD → AMF, Intel → QSV; `auto` (default) reads the vendor of the
|
||||
// SELECTED render adapter (crate::gpu — web-console preference / env pin / max VRAM), so the
|
||||
// SELECTED render adapter (pf_gpu — web-console preference / env pin / max VRAM), so the
|
||||
// backend always matches the GPU the capture ring and virtual display sit on.
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum WindowsBackend {
|
||||
pub enum WindowsBackend {
|
||||
Nvenc,
|
||||
Amf,
|
||||
Qsv,
|
||||
@@ -943,7 +949,7 @@ enum GpuVendor {
|
||||
/// render adapter's vendor). Shared by [`open_video`] and the GameStream codec advertisement so
|
||||
/// both agree.
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) fn windows_resolved_backend() -> WindowsBackend {
|
||||
pub fn windows_resolved_backend() -> WindowsBackend {
|
||||
// Resolved ONCE in HostConfig (Goal-1) — was re-read from PUNKTFUNK_ENCODER on every call.
|
||||
match pf_host_config::config().encoder_pref.as_str() {
|
||||
"nvenc" | "hw" | "nvidia" | "cuda" => WindowsBackend::Nvenc,
|
||||
@@ -961,18 +967,18 @@ pub(crate) fn windows_resolved_backend() -> WindowsBackend {
|
||||
|
||||
/// True if the session's resolved encode backend produces GPU-resident frames (so the capturer should
|
||||
/// hand GPU surfaces straight through rather than CPU-stage them) — only the GPU-less software encoder
|
||||
/// wants CPU staging. This is the single source for [`crate::capture::OutputFormat`]'s `gpu` bit:
|
||||
/// wants CPU staging. This is the single source for [`pf_frame::OutputFormat`]'s `gpu` bit:
|
||||
/// resolving it in `encode` and threading it *into* the capturer (rather than having `capture` re-derive
|
||||
/// the backend) keeps the capture→encode dependency one-way, so the two can never disagree on whether
|
||||
/// frames are GPU-resident (plan §2.4 / §W4).
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) fn resolved_backend_is_gpu() -> bool {
|
||||
pub fn resolved_backend_is_gpu() -> bool {
|
||||
!matches!(windows_resolved_backend(), WindowsBackend::Software)
|
||||
}
|
||||
/// Linux/other: every backend but the GPU-less software encoder (openh264) is GPU-resident. Config-backed
|
||||
/// (mirrors `session_plan::resolve_encoder`; the NVENC vs VAAPI split is auto-detected in [`open_video`]).
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub(crate) fn resolved_backend_is_gpu() -> bool {
|
||||
pub fn resolved_backend_is_gpu() -> bool {
|
||||
!matches!(
|
||||
pf_host_config::config().encoder_pref.as_str(),
|
||||
"software" | "sw" | "openh264"
|
||||
@@ -980,16 +986,16 @@ pub(crate) fn resolved_backend_is_gpu() -> bool {
|
||||
}
|
||||
|
||||
/// True if the resolved encode backend can ingest a full-chroma (RGB) source and CSC it to 4:4:4 itself —
|
||||
/// the *encoder* half of the 4:4:4 capture gate ([`crate::capture::capturer_supports_444`]). Only Windows
|
||||
/// the *encoder* half of the 4:4:4 capture gate (the host capture `capturer_supports_444`). Only Windows
|
||||
/// direct-NVENC does (measured on-glass: ARGB + `chromaFormatIDC=3` → true 4:4:4); AMF/QSV can't. On Linux
|
||||
/// the 4:4:4 source is the capturer's own (portal RGB → `yuv444p`), independent of the auto-detected
|
||||
/// backend, so the gate never consults this there.
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) fn resolved_backend_ingests_rgb_444() -> bool {
|
||||
pub fn resolved_backend_ingests_rgb_444() -> bool {
|
||||
windows_resolved_backend() == WindowsBackend::Nvenc
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub(crate) fn resolved_backend_ingests_rgb_444() -> bool {
|
||||
pub fn resolved_backend_ingests_rgb_444() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
@@ -1095,7 +1101,7 @@ pub fn windows_codec_support() -> CodecSupport {
|
||||
/// degrading a live sibling's encode. NVENC is the only backend with hard session caps today
|
||||
/// (GeForce consumer limit); AMF/QSV equivalents follow the same seam when they grow accounting.
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) fn can_open_another_session() -> bool {
|
||||
pub fn can_open_another_session() -> bool {
|
||||
#[cfg(feature = "nvenc")]
|
||||
{
|
||||
nvenc::can_open_another_session()
|
||||
@@ -1108,62 +1114,65 @@ pub(crate) fn can_open_another_session() -> bool {
|
||||
|
||||
// Goal-1 stage 6: GPU/CPU encoders confined to `encode/windows/` (NVENC, native AMF, AMF/QSV
|
||||
// ffmpeg, software) and `encode/linux/` (NVENC/CUDA + VAAPI); `#[path]` keeps the
|
||||
// `crate::encode::*` module names flat.
|
||||
// `crate::*` module names flat.
|
||||
// Native AMF (direct SDK, design/native-amf-encoder.md): compiled unconditionally on Windows —
|
||||
// no build feature, the driver-installed amfrt64.dll resolves at runtime like NVENC's DLL.
|
||||
#[cfg(target_os = "windows")]
|
||||
#[path = "encode/windows/amf.rs"]
|
||||
#[path = "enc/windows/amf.rs"]
|
||||
mod amf;
|
||||
#[cfg(all(target_os = "windows", feature = "amf-qsv"))]
|
||||
#[path = "encode/windows/ffmpeg_win.rs"]
|
||||
#[path = "enc/windows/ffmpeg_win.rs"]
|
||||
mod ffmpeg_win;
|
||||
#[cfg(target_os = "linux")]
|
||||
#[path = "enc/linux/mod.rs"]
|
||||
mod linux;
|
||||
// Direct-SDK NVENC on Linux (CUDA input; design/linux-direct-nvenc.md) — real RFI + recovery anchor
|
||||
// + reset() lever the libavcodec `linux::NvencEncoder` can't express. Opt-in behind
|
||||
// `PUNKTFUNK_NVENC_DIRECT` until on-glass validated; the `.so` resolves at runtime like the Windows
|
||||
// path, so `--features nvenc` stays safe on a driver-less/AMD Linux box.
|
||||
#[cfg(all(target_os = "windows", feature = "nvenc"))]
|
||||
#[path = "encode/windows/nvenc.rs"]
|
||||
#[path = "enc/windows/nvenc.rs"]
|
||||
mod nvenc;
|
||||
#[cfg(all(target_os = "linux", feature = "nvenc"))]
|
||||
#[path = "encode/linux/nvenc_cuda.rs"]
|
||||
#[path = "enc/linux/nvenc_cuda.rs"]
|
||||
mod nvenc_cuda;
|
||||
// Actionable `NVENCSTATUS` → cause mapping shared by both direct-NVENC backends, so a failed
|
||||
// session open logs "update/reboot the driver" instead of the old misleading "(no NVIDIA GPU?)".
|
||||
#[cfg(all(any(target_os = "linux", target_os = "windows"), feature = "nvenc"))]
|
||||
#[path = "encode/nvenc_status.rs"]
|
||||
#[path = "enc/nvenc_status.rs"]
|
||||
mod nvenc_status;
|
||||
// Platform-agnostic direct-SDK NVENC glue (`NvStatusExt`/`nv_ok`, `codec_guid`) shared by both
|
||||
// `nvEncodeAPI` backends — the byte-identical Tier-2 leaves (plan §2.2). Sibling of `nvenc_status`.
|
||||
#[cfg(all(any(target_os = "linux", target_os = "windows"), feature = "nvenc"))]
|
||||
#[path = "encode/nvenc_core.rs"]
|
||||
#[path = "enc/nvenc_core.rs"]
|
||||
mod nvenc_core;
|
||||
// Shared libavcodec glue (`pixel_to_av`, swscale consts) for the three libav backends — Linux
|
||||
// NVENC + VAAPI and Windows AMF/QSV — so the byte-identical pieces live once (plan §2.2, Tier 2).
|
||||
#[cfg(any(target_os = "linux", all(target_os = "windows", feature = "amf-qsv")))]
|
||||
#[path = "enc/libav.rs"]
|
||||
mod libav;
|
||||
// Software (openh264) H.264 encoder — the GPU-less path on BOTH Windows and Linux (a headless /
|
||||
// GPU-less test box, or a fallback when no hardware encoder is available). Platform-agnostic: it
|
||||
// consumes CPU RGB `CapturedFrame`s and the statically-bundled openh264 build.
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
#[path = "enc/sw.rs"]
|
||||
mod sw;
|
||||
#[cfg(target_os = "linux")]
|
||||
#[path = "encode/linux/vaapi.rs"]
|
||||
#[path = "enc/linux/vaapi.rs"]
|
||||
mod vaapi;
|
||||
// Raw Vulkan Video HEVC encode on Linux (AMD/Intel; design/linux-vulkan-video-encode.md) — real RFI
|
||||
// via explicit DPB reference slots (the app owns the DPB), the open-stack twin of the direct-NVENC
|
||||
// path. Does an on-GPU RGB→NV12 compute CSC since capture delivers packed-RGB dmabufs. Opt-in behind
|
||||
// `PUNKTFUNK_VULKAN_ENCODE` until on-glass validated; needs `--features vulkan-encode`.
|
||||
#[cfg(all(target_os = "linux", feature = "vulkan-encode"))]
|
||||
#[path = "encode/linux/vulkan_video.rs"]
|
||||
#[path = "enc/linux/vulkan_video.rs"]
|
||||
mod vulkan_video;
|
||||
// Vendored `VK_KHR_video_encode_av1` bindings (host-only) — the AV1 encode structs our pinned
|
||||
// `ash 0.38.0+1.3.281` predates (finalized Vulkan 1.3.290). Copied verbatim from ash-master's
|
||||
// generated code rather than bumping `ash` (which breaks the SDL/Vulkan client). Consumed by
|
||||
// `vulkan_video.rs` via `super::vk_av1_encode`.
|
||||
#[cfg(all(target_os = "linux", feature = "vulkan-encode"))]
|
||||
#[path = "encode/linux/vk_av1_encode.rs"]
|
||||
#[path = "enc/linux/vk_av1_encode.rs"]
|
||||
mod vk_av1_encode;
|
||||
// Small ash leaf helpers shared by the Linux Vulkan encode backends (dmabuf import, image/memory
|
||||
// utilities) — extracted from `vulkan_video.rs` when the PyroWave backend arrived.
|
||||
@@ -1171,13 +1180,13 @@ mod vk_av1_encode;
|
||||
target_os = "linux",
|
||||
any(feature = "vulkan-encode", feature = "pyrowave")
|
||||
))]
|
||||
#[path = "encode/linux/vk_util.rs"]
|
||||
#[path = "enc/linux/vk_util.rs"]
|
||||
mod vk_util;
|
||||
// PyroWave — the opt-in wired-LAN intra-only wavelet codec (design/pyrowave-codec-plan.md §4.3):
|
||||
// pure Vulkan compute via the vendored `pyrowave-sys`, sub-ms encode, every frame a keyframe.
|
||||
// Explicit-only behind PUNKTFUNK_ENCODER=pyrowave; EXPERIMENTAL until CODEC_PYROWAVE lands.
|
||||
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
|
||||
#[path = "encode/linux/pyrowave.rs"]
|
||||
#[path = "enc/linux/pyrowave.rs"]
|
||||
mod pyrowave;
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -55,6 +55,11 @@ pub fn pack_luid(luid: LUID) -> i64 {
|
||||
/// adapter). Used at open and on every ACCESS_LOST: a device created on one desktop cannot sustain a
|
||||
/// duplication on a *different* desktop (perpetual ACCESS_LOST), so the secure-desktop switch needs a
|
||||
/// device made while the thread is attached to that desktop.
|
||||
///
|
||||
/// # Safety
|
||||
/// `adapter` must be a live `IDXGIAdapter1` for the duration of the call. The fn calls the D3D11 /
|
||||
/// DXGI FFI (`D3D11CreateDevice`, GPU scheduling-priority hardening) but forms no lasting alias to
|
||||
/// `adapter`; the returned device/context are the sole owners of the new COM objects.
|
||||
pub unsafe fn make_device(adapter: &IDXGIAdapter1) -> Result<(ID3D11Device, ID3D11DeviceContext)> {
|
||||
let mut device: Option<ID3D11Device> = None;
|
||||
let mut context: Option<ID3D11DeviceContext> = None;
|
||||
|
||||
@@ -914,3 +914,39 @@ mod tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Pick the render GPU LUID the Windows pipeline is created on: the IDD-push capturer's
|
||||
/// shared-texture ring, the IddCx `SET_RENDER_ADAPTER` pin, and (via the captured frame's device)
|
||||
/// NVENC/AMF/QSV all follow this one decision — see [`selected_gpu`] for the precedence (operator
|
||||
/// preference > `PUNKTFUNK_RENDER_ADAPTER` substring > max `DedicatedVideoMemory`). A configured
|
||||
/// preference that doesn't match a present GPU falls back to auto selection (with a warning) rather
|
||||
/// than returning `None`, so a stale preference never stops the host from streaming.
|
||||
///
|
||||
/// Lives here (not in a host module) so BOTH the capture and encode subsystem crates depend on it
|
||||
/// as a peer of GPU selection instead of the orchestrator — the plan's `windows/adapter.rs`, folded
|
||||
/// into `pf-gpu` (plan §W6). It was historically the SudoVDA backend's, then the host's
|
||||
/// `win_adapter.rs`; the LUID-shaped view of [`selected_gpu`] plus the per-decision logging.
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn resolve_render_adapter_luid() -> Option<windows::Win32::Foundation::LUID> {
|
||||
match selected_gpu() {
|
||||
Some(sel) => {
|
||||
tracing::info!(
|
||||
adapter = sel.info.name,
|
||||
vram_mb = sel.info.vram_bytes / (1024 * 1024),
|
||||
source = sel.source.tag(),
|
||||
"render adapter selected"
|
||||
);
|
||||
if sel.source == PickSource::PreferenceMissing {
|
||||
tracing::warn!(
|
||||
"the preferred GPU is not present — auto-selected the adapter above \
|
||||
(fix or clear the preference in the web console)"
|
||||
);
|
||||
}
|
||||
Some(sel.info.luid())
|
||||
}
|
||||
None => {
|
||||
tracing::warn!("no suitable render adapter found for SET_RENDER_ADAPTER");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,11 @@ pf-frame = { path = "../pf-frame" }
|
||||
# Windows display-topology helpers (CCD/GDI mode-set, PnP monitor devnodes, display-change watch),
|
||||
# extracted to a leaf crate (plan §W6). Empty on non-Windows, so it lives in the main deps.
|
||||
pf-win-display = { path = "../pf-win-display" }
|
||||
# Video encode backends (NVENC/VAAPI/AMF/QSV/Vulkan-Video/PyroWave/openh264) behind one Encoder
|
||||
# trait, extracted to a subsystem crate (plan §W6). The host's nvenc/amf-qsv/vulkan-encode/pyrowave
|
||||
# features forward here (see [features]); the heavy encoder deps (ffmpeg-next, the NVENC SDK,
|
||||
# openh264, pyrowave-sys) moved with it.
|
||||
pf-encode = { path = "../pf-encode" }
|
||||
# M3 native control plane (the `punktfunk/1` QUIC handshake; data plane stays native-thread UDP).
|
||||
quinn = "0.11"
|
||||
anyhow = "1"
|
||||
@@ -102,14 +107,7 @@ log = "0.4"
|
||||
# crate vendors libopus (cmake-built from source — no system lib, no vcpkg), so it builds on Windows
|
||||
# MSVC too (needs CMake + NASM, both on the box). Both platforms that have an audio-capture backend.
|
||||
[target.'cfg(any(target_os = "linux", target_os = "windows"))'.dependencies]
|
||||
# PyroWave (opt-in wired-LAN wavelet codec) — vendored codec + bindgen'd C API, only compiled
|
||||
# under `--features pyrowave`. Stub-empty on other targets, so the cfg here is belt-and-braces.
|
||||
pyrowave-sys = { path = "../pyrowave-sys", optional = true }
|
||||
opus = "0.3"
|
||||
# Software H.264 encoder — the GPU-less encode path on both Linux and Windows (and a fallback when no
|
||||
# hardware encoder is available). The default `source` feature statically compiles OpenH264 (BSD-2) —
|
||||
# no system lib, builds on MSVC; nasm on PATH adds the SIMD fast path.
|
||||
openh264 = "0.9"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
# `screencast` gates the ScreenCast portal module; `remote_desktop` adds the RemoteDesktop
|
||||
@@ -117,16 +115,7 @@ openh264 = "0.9"
|
||||
# `open_pipe_wire_remote` is unconditional, so ashpd's own `pipewire` feature is not
|
||||
# needed — we drive PipeWire with the `pipewire` crate below.
|
||||
ashpd = { version = "0.13", features = ["screencast", "remote_desktop"] }
|
||||
ffmpeg-next = "8"
|
||||
libc = "0.2"
|
||||
# Direct-SDK NVENC on Linux (design/linux-direct-nvenc.md): the RAW `sys::nvEncodeAPI` types only —
|
||||
# the entry points are resolved at RUNTIME from the driver's `libnvidia-encode.so.1`
|
||||
# (encode/linux/nvenc_cuda.rs), NOT link-imported, so the same binary starts fine on AMD/Intel
|
||||
# Linux boxes (no NVIDIA driver) and falls through to VAAPI/software. `ci-check` = vendored
|
||||
# bindings + cudarc `dynamic-loading` (no CUDA toolkit/headers at build); we never call the crate's
|
||||
# cudarc — CUDA is driven through the existing `zerocopy::cuda` dlopen table. Same crate + feature
|
||||
# as the Windows target dep (Cargo.toml, windows target section) so the `sys` structs never drift.
|
||||
nvidia-video-codec-sdk = { version = "0.4", features = ["ci-check"], optional = true }
|
||||
# Must match the pipewire crate ashpd 0.13 links (libspa/pipewire-sys `links` key is
|
||||
# unique per build), i.e. 0.9 — NOT the 0.10 the setup doc mentions.
|
||||
pipewire = "0.9"
|
||||
@@ -254,20 +243,6 @@ winreg = "0.56"
|
||||
roxmltree = "0.21"
|
||||
# WASAPI loopback audio capture (default render endpoint -> 48 kHz stereo f32 for the Opus path).
|
||||
wasapi = "0.23"
|
||||
# Virtual Xbox 360 gamepad: the in-tree XUSB companion UMDF driver (packaging/windows/xusb-driver),
|
||||
# driven over shared memory from inject/windows/gamepad_windows.rs — no ViGEmBus dependency.
|
||||
# NVENC hardware encoder (NVENC SDK, D3D11 input). The SDK pins `cudarc` with
|
||||
# `cuda-version-from-build-system` (a build-time CUDA-toolkit probe); its `ci-check` feature switches
|
||||
# cudarc to `dynamic-loading` (loads nvcuda.dll at runtime — nothing needed at build), which is how
|
||||
# the crate builds on docs.rs/CI. We enable it so the GPU-less VM/CI compiles; the DirectX NVENC path
|
||||
# never calls CUDA at runtime, so the pinned CUDA bindings version is irrelevant.
|
||||
nvidia-video-codec-sdk = { version = "0.4", features = ["ci-check"], optional = true }
|
||||
# AMD (AMF) + Intel (QSV) hardware encode on Windows via libavcodec — the analogue of the Linux
|
||||
# VAAPI backend (`src/encode/ffmpeg_win.rs`). Optional + behind the `amf-qsv` feature because it
|
||||
# link-imports the FFmpeg libs at build time (needs a `FFMPEG_DIR` with the AMF/QSV encoders — the
|
||||
# same BtbN gpl-shared tree the Windows client uses) and pulls the shared `avcodec/avutil/...` DLLs
|
||||
# at runtime. `ffmpeg-sys-next` auto-detects the FFmpeg version (7.x/avcodec-61 or 8.x/62).
|
||||
ffmpeg-next = { version = "8", optional = true }
|
||||
# Shared host<->driver wire contract for the pf-vdisplay IddCx virtual-display backend
|
||||
# (vdisplay/pf_vdisplay.rs): the control-plane IOCTL codes + `#[repr(C)] Pod` request/reply structs,
|
||||
# defined ONCE so host<->driver ABI drift is a compile error. `bytemuck` serializes those structs
|
||||
@@ -275,32 +250,29 @@ ffmpeg-next = { version = "8", optional = true }
|
||||
pf-driver-proto = { path = "../pf-driver-proto" }
|
||||
bytemuck = { version = "1.19", features = ["derive"] }
|
||||
|
||||
# The encode feature flags now FORWARD to the pf-encode subsystem crate (the heavy encoder deps —
|
||||
# ffmpeg-next, the NVENC SDK, openh264, pyrowave-sys — moved there, plan §W6). Selecting a feature
|
||||
# on the host turns on the matching backend inside pf-encode.
|
||||
[features]
|
||||
# PyroWave ships in every default build (the codec stays strictly opt-in per session — a client
|
||||
# must explicitly prefer CODEC_PYROWAVE; nothing changes for normal HEVC/AV1 sessions).
|
||||
default = ["pyrowave"]
|
||||
# NVENC hardware encode (Windows). OFF by default (it pulls the NVENC SDK crate); nothing is
|
||||
# needed at link time — the entry points are resolved at RUNTIME from the driver's
|
||||
# nvEncodeAPI64.dll (encode/windows/nvenc.rs `load_api`), so the same binary starts fine on
|
||||
# AMD/Intel-only boxes and falls through to AMF/QSV/software. Build the GPU host with
|
||||
# `--features nvenc`.
|
||||
nvenc = ["dep:nvidia-video-codec-sdk"]
|
||||
# AMD/Intel hardware encode on Windows (AMF/QSV via ffmpeg-next). OFF by default: it needs a
|
||||
# `FFMPEG_DIR` (BtbN lgpl-shared — includes `*_amf`/`*_qsv`; the GPL-only x264/x265 are never used,
|
||||
# so the LGPL build suffices and keeps the bundled DLLs LGPL, not GPL) at build time and bundles the
|
||||
# FFmpeg DLLs at runtime. Build the all-vendor GPU host with `--features nvenc,amf-qsv`.
|
||||
amf-qsv = ["dep:ffmpeg-next"]
|
||||
# Raw Vulkan Video HEVC encode on Linux (AMD/Intel) — real reference-frame-invalidation loss
|
||||
# recovery via explicit DPB reference slots (design/linux-vulkan-video-encode.md), the open-stack
|
||||
# twin of the direct-NVENC path. OFF by default; pulls NO new dependency (reuses the `ash` Vulkan
|
||||
# bindings already carried for the dmabuf zero-copy bridge). Runtime-gated further by
|
||||
# PUNKTFUNK_VULKAN_ENCODE (opt-in for now). Build the AMD/Intel RFI host with `--features vulkan-encode`.
|
||||
vulkan-encode = []
|
||||
# PyroWave — the opt-in wired-LAN intra-only wavelet codec (design/pyrowave-codec-plan.md).
|
||||
# Builds the vendored codec from source (crates/pyrowave-sys, CMake + bindgen; Linux/Windows —
|
||||
# the encoder backend itself is Linux-only, the Windows host just carries the library). ON by
|
||||
# default (see `default` above); sessions reach it only through explicit client opt-in.
|
||||
pyrowave = ["dep:pyrowave-sys"]
|
||||
# NVENC hardware encode (Linux CUDA + Windows D3D11). OFF by default; entry points resolved at
|
||||
# RUNTIME from the driver DLL/so, so the same binary starts fine on AMD/Intel boxes. Build the GPU
|
||||
# host with `--features nvenc`.
|
||||
nvenc = ["pf-encode/nvenc"]
|
||||
# AMD/Intel hardware encode on Windows (AMF/QSV via ffmpeg-next). OFF by default: needs a `FFMPEG_DIR`
|
||||
# (BtbN lgpl-shared with `*_amf`/`*_qsv`) at build and bundles the FFmpeg DLLs at runtime. Build the
|
||||
# all-vendor GPU host with `--features nvenc,amf-qsv`.
|
||||
amf-qsv = ["pf-encode/amf-qsv"]
|
||||
# Raw Vulkan Video HEVC/AV1 encode on Linux (AMD/Intel) — real reference-frame-invalidation loss
|
||||
# recovery via explicit DPB reference slots (design/linux-vulkan-video-encode.md). OFF by default;
|
||||
# reuses pf-encode's `ash` bindings (no new dep). Runtime-gated further by PUNKTFUNK_VULKAN_ENCODE.
|
||||
vulkan-encode = ["pf-encode/vulkan-encode"]
|
||||
# PyroWave — the opt-in wired-LAN intra-only wavelet codec (design/pyrowave-codec-plan.md). Builds
|
||||
# the vendored codec from source in pf-encode. ON by default (see `default`); sessions reach it only
|
||||
# through explicit client opt-in.
|
||||
pyrowave = ["pf-encode/pyrowave"]
|
||||
|
||||
# Build-time icon/version-info embedding (build.rs; Windows dev/CI hosts only — Linux packaging
|
||||
# builds of this crate never execute the winresource block).
|
||||
|
||||
@@ -65,8 +65,8 @@ use windows::Win32::UI::WindowsAndMessaging::{GetCursorPos, SetCursorPos};
|
||||
// `DRV_STATUS_*` codes and the channel-delivery struct — lives in `pf_driver_proto`; both sides
|
||||
// `use` it, so a layout/code drift is a compile error (the proto has `const` size asserts).
|
||||
use frame::{
|
||||
SharedHeader, DRV_STATUS_BIND_FAIL, DRV_STATUS_NO_DEVICE1, DRV_STATUS_OPENED,
|
||||
DRV_STATUS_TEX_FAIL, MAGIC, RING_LEN, VERSION,
|
||||
unpack_opened_detail, SharedHeader, DRV_STATUS_BIND_FAIL, DRV_STATUS_NONE,
|
||||
DRV_STATUS_NO_DEVICE1, DRV_STATUS_OPENED, DRV_STATUS_TEX_FAIL, MAGIC, RING_LEN, VERSION,
|
||||
};
|
||||
|
||||
/// `DXGI_SHARED_RESOURCE_READ | _WRITE` for `CreateSharedHandle`/`OpenSharedResourceByName`. Local (not
|
||||
@@ -561,7 +561,7 @@ impl IddPushCapturer {
|
||||
// the driver HAVE drifted — identical twin GPUs whose max-VRAM tie moved between ADD and
|
||||
// this open, or a stale kept monitor across an adapter re-init — the driver reports
|
||||
// TEX_FAIL plus the adapter it actually renders on, and the rebind below reopens on that.
|
||||
let luid = crate::win_adapter::resolve_render_adapter_luid().unwrap_or(LUID {
|
||||
let luid = pf_gpu::resolve_render_adapter_luid().unwrap_or(LUID {
|
||||
LowPart: (target.adapter_luid & 0xffff_ffff) as u32,
|
||||
HighPart: (target.adapter_luid >> 32) as i32,
|
||||
});
|
||||
@@ -935,15 +935,63 @@ impl IddPushCapturer {
|
||||
}
|
||||
if Instant::now() > deadline {
|
||||
bail!(
|
||||
"IDD-push: driver_status={st} but no frame published within 4s (despite compose \
|
||||
kicks) — the virtual display is likely in a format/size the ring can't match \
|
||||
(fullscreen game?); falling back"
|
||||
"IDD-push: no frame published within 4s (despite compose kicks) — {}; \
|
||||
falling back",
|
||||
self.no_first_frame_diagnosis(st)
|
||||
);
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
}
|
||||
}
|
||||
|
||||
/// Name a first-frame timeout from the driver's own evidence — `driver_status` plus the live
|
||||
/// OPENED detail word (proto `pack_opened_detail`) — instead of guessing. The three no-frames
|
||||
/// states look identical from the host side but have disjoint causes and fixes; the lid-closed
|
||||
/// field report burned days for lack of exactly this line. Appends a console-session hint when
|
||||
/// the host itself is in the wrong session (display writes + input kicks can't work from there).
|
||||
fn no_first_frame_diagnosis(&self, st: u32) -> String {
|
||||
let what = match st {
|
||||
// The delivery was never consumed: no swap-chain worker ran for this monitor at all.
|
||||
DRV_STATUS_NONE => "the driver never attached — the channel delivery was never \
|
||||
consumed, so the OS ran no swap-chain worker for this monitor (display not \
|
||||
composed at all: console display-off / modern standby, or the mode commit \
|
||||
never reached the adapter)"
|
||||
.to_string(),
|
||||
DRV_STATUS_OPENED => {
|
||||
// SAFETY: in-bounds, aligned u32 read of the live, owned shared-header mapping
|
||||
// (same best-effort diagnostic access as the `driver_status` read in the caller);
|
||||
// no reference into the shared region is formed.
|
||||
let detail = unsafe { (*self.header).driver_status_detail };
|
||||
match unpack_opened_detail(detail) {
|
||||
Some((0, _)) => "driver attached with a live swap-chain, but DWM composed \
|
||||
ZERO frames — an undamaged or powered-off desktop, and the compose \
|
||||
kicks didn't bite (synthetic input is blocked on the secure desktop)"
|
||||
.to_string(),
|
||||
Some((offered, mismatched)) => format!(
|
||||
"driver attached and DWM composed {offered} frame(s), but none matched \
|
||||
the ring — {mismatched} dropped for a size/format mismatch (the \
|
||||
display's actual mode differs from what the host sized the ring to: \
|
||||
a mid-open mode-set, a fullscreen game, or a stale GDI view)"
|
||||
),
|
||||
// A pre-detail driver never stamps the live bit — say so rather than guess.
|
||||
None => "driver attached but published nothing; this pf-vdisplay build \
|
||||
predates attach diagnostics, so the cause can't be named — update the \
|
||||
driver for a precise line here"
|
||||
.to_string(),
|
||||
}
|
||||
}
|
||||
other => format!("driver_status={other} (unexpected at this point)"),
|
||||
};
|
||||
match crate::interactive::console_session_mismatch() {
|
||||
Some((own, console)) => format!(
|
||||
"{what} [host is in session {own} but the console is session {console} — display \
|
||||
writes and input kicks cannot work from a non-console session; reconnect the \
|
||||
console or run via the installed service]"
|
||||
),
|
||||
None => what,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn latest(&self) -> u64 {
|
||||
// SAFETY: `self.header` is the live, owned shared-header mapping (page-aligned, sized for a
|
||||
|
||||
@@ -140,7 +140,7 @@ impl Capturer for SyntheticNv12Capturer {
|
||||
/// Calls DXGI factory/adapter enumeration; returns owned COM objects or an error.
|
||||
unsafe fn resolve_render_adapter() -> Result<IDXGIAdapter1> {
|
||||
let factory: IDXGIFactory4 = CreateDXGIFactory1().context("CreateDXGIFactory1")?;
|
||||
if let Some(luid) = crate::win_adapter::resolve_render_adapter_luid() {
|
||||
if let Some(luid) = pf_gpu::resolve_render_adapter_luid() {
|
||||
if let Ok(a) = factory.EnumAdapterByLuid::<IDXGIAdapter1>(luid) {
|
||||
return Ok(a);
|
||||
}
|
||||
|
||||
@@ -35,7 +35,13 @@ mod ddc;
|
||||
#[cfg(target_os = "linux")]
|
||||
#[path = "linux/drm_sync.rs"]
|
||||
mod drm_sync;
|
||||
mod encode;
|
||||
// The video encode backends live in the `pf-encode` leaf crate (plan §W6); this shim keeps every
|
||||
// existing `crate::encode::*` path valid (the host is the sole consumer, via the negotiator + the
|
||||
// GameStream/native/mgmt planes). Feature flags (nvenc/amf-qsv/vulkan-encode/pyrowave) forward to
|
||||
// pf-encode from this crate's `[features]`.
|
||||
mod encode {
|
||||
pub(crate) use pf_encode::*;
|
||||
}
|
||||
mod events;
|
||||
mod gamestream;
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -67,9 +73,6 @@ mod spike;
|
||||
mod stats_recorder;
|
||||
mod stream_marker;
|
||||
mod vdisplay;
|
||||
#[cfg(target_os = "windows")]
|
||||
#[path = "windows/win_adapter.rs"]
|
||||
mod win_adapter;
|
||||
// The Windows display-topology cluster (CCD/GDI mode-set, PnP monitor devnodes, the display-change
|
||||
// watch) lives in the `pf-win-display` leaf crate (plan §W6); import the modules at the crate root
|
||||
// so every existing `crate::{win_display,monitor_devnode,display_events}::*` path stays valid.
|
||||
|
||||
@@ -1445,12 +1445,12 @@ pub(crate) fn slot_id_for(client_fp: Option<[u8; 32]>, mode: (u32, u32)) -> u32
|
||||
/// The render-GPU pin (backend-neutral): IDD-push — the sole Windows capture path — runs NVENC on the
|
||||
/// render adapter, so it must always be pinned to the selected encoder GPU (a hybrid box would
|
||||
/// otherwise render on the wrong one). The selection itself (web-console preference >
|
||||
/// `PUNKTFUNK_RENDER_ADAPTER` > max VRAM) lives in [`crate::win_adapter::resolve_render_adapter_luid`].
|
||||
/// `PUNKTFUNK_RENDER_ADAPTER` > max VRAM) lives in [`pf_gpu::resolve_render_adapter_luid`].
|
||||
/// (This was gated on the removed `PUNKTFUNK_IDD_PUSH` knob — a dispatch disagreement, since capture
|
||||
/// stopped consulting it when DDA/WGC were removed.)
|
||||
fn resolve_render_pin() -> Option<LUID> {
|
||||
tracing::info!("IDD push: pinning the render GPU (SET_RENDER_ADAPTER)");
|
||||
crate::win_adapter::resolve_render_adapter_luid()
|
||||
pf_gpu::resolve_render_adapter_luid()
|
||||
}
|
||||
|
||||
/// A reused monitor keeps the render GPU the driver was pinned to at its ADD — the pin is never
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
//! Backend-neutral DXGI adapter selection.
|
||||
//!
|
||||
//! The discrete render-GPU LUID picker used to live in the SudoVDA backend (`vdisplay::sudovda`) — a
|
||||
//! historical accident, since it is display-utility, not SudoVDA-specific. It lives here so the capturers
|
||||
//! (IDD-push) and the pf-vdisplay backend depend on it as a *peer* instead of reaching into the SudoVDA
|
||||
//! module — breaking that circular reach-in, which let the SudoVDA backend be dropped without losing this
|
||||
//! helper (audit §9 / Goal 2 — done). This is the plan's `windows/adapter.rs`.
|
||||
//!
|
||||
//! The selection logic itself now lives in [`crate::gpu`] (shared with the mgmt API's GPU
|
||||
//! endpoints): **operator preference (web console) > `PUNKTFUNK_RENDER_ADAPTER` substring > max
|
||||
//! `DedicatedVideoMemory`**, WARP/Basic-Render and indirect-display ghost twins always excluded.
|
||||
//! This wrapper is the LUID-shaped view of it, plus the per-decision logging (call sites are
|
||||
//! per-session, never per-frame).
|
||||
|
||||
use windows::Win32::Foundation::LUID;
|
||||
|
||||
/// Pick the render GPU LUID the pipeline is created on: the IDD-push capturer's shared-texture
|
||||
/// ring, the IddCx SET_RENDER_ADAPTER pin, and (via the captured frame's device) NVENC/AMF/QSV all
|
||||
/// follow this one decision — see [`pf_gpu::selected_gpu`] for the precedence. A configured
|
||||
/// preference that doesn't match a present GPU falls back to auto selection (with a warning)
|
||||
/// rather than returning `None`, so a stale preference never stops the host from streaming.
|
||||
pub(crate) fn resolve_render_adapter_luid() -> Option<LUID> {
|
||||
match pf_gpu::selected_gpu() {
|
||||
Some(sel) => {
|
||||
tracing::info!(
|
||||
adapter = sel.info.name,
|
||||
vram_mb = sel.info.vram_bytes / (1024 * 1024),
|
||||
source = sel.source.tag(),
|
||||
"render adapter selected"
|
||||
);
|
||||
if sel.source == pf_gpu::PickSource::PreferenceMissing {
|
||||
tracing::warn!(
|
||||
"the preferred GPU is not present — auto-selected the adapter above \
|
||||
(fix or clear the preference in the web console)"
|
||||
);
|
||||
}
|
||||
Some(sel.info.luid())
|
||||
}
|
||||
None => {
|
||||
tracing::warn!("no suitable render adapter found for SET_RENDER_ADAPTER");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user