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:
2026-07-17 10:42:51 +02:00
parent 1de83ba51d
commit 9a36ea2132
31 changed files with 339 additions and 224 deletions
Generated
+23 -4
View File
@@ -2799,6 +2799,28 @@ dependencies = [
"bytemuck", "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]] [[package]]
name = "pf-ffvk" name = "pf-ffvk"
version = "0.12.0" version = "0.12.0"
@@ -3166,7 +3188,6 @@ dependencies = [
"base64", "base64",
"bytemuck", "bytemuck",
"cbc", "cbc",
"ffmpeg-next",
"futures-util", "futures-util",
"hex", "hex",
"hmac", "hmac",
@@ -3180,11 +3201,10 @@ dependencies = [
"log", "log",
"mac_address", "mac_address",
"mdns-sd", "mdns-sd",
"nvidia-video-codec-sdk",
"openh264",
"opus", "opus",
"parking_lot", "parking_lot",
"pf-driver-proto", "pf-driver-proto",
"pf-encode",
"pf-frame", "pf-frame",
"pf-gpu", "pf-gpu",
"pf-host-config", "pf-host-config",
@@ -3193,7 +3213,6 @@ dependencies = [
"pf-zerocopy", "pf-zerocopy",
"pipewire", "pipewire",
"punktfunk-core", "punktfunk-core",
"pyrowave-sys",
"quinn", "quinn",
"rand 0.8.6", "rand 0.8.6",
"rcgen", "rcgen",
+1
View File
@@ -16,6 +16,7 @@ members = [
"crates/pf-zerocopy", "crates/pf-zerocopy",
"crates/pf-frame", "crates/pf-frame",
"crates/pf-win-display", "crates/pf-win-display",
"crates/pf-encode",
"crates/pyrowave-sys", "crates/pyrowave-sys",
"clients/probe", "clients/probe",
"clients/linux", "clients/linux",
+74
View File
@@ -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 //! 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 //! signatures use — [`EncodedFrame`], [`Codec`], [`ChromaFormat`], [`EncoderCaps`] — and the
//! dimension/VBV helpers [`validate_dimensions`] and [`vbv_frames_env`]. Backend selection, 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`] //! 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::encode::*` path //! facade, which re-exports this module (`pub(crate) use codec::*;`) so every `crate::*` path
//! is unchanged. //! is unchanged.
use crate::capture::CapturedFrame;
use anyhow::Result; use anyhow::Result;
use pf_frame::CapturedFrame;
/// An encoded access unit (one NAL/AU) to hand to `punktfunk_core` for FEC + packetization. /// 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 /// `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 /// 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 { pub fn label(self) -> &'static str {
match self { match self {
Codec::H264 => "h264", 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 /// 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 /// 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 /// *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 { pub fn supports_10bit(self) -> bool {
matches!(self, Codec::H265 | Codec::Av1) 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 /// 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 /// 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 — /// 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). /// 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 //! (`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`, //! gap). Free functions and consts over borrowed handles; nothing here is per-frame `dyn`,
//! allocating, or on the zero-copy ingest path. //! allocating, or on the zero-copy ingest path.
use crate::encode::EncodedFrame; use crate::EncodedFrame;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use ffmpeg_next as ffmpeg; use ffmpeg_next as ffmpeg;
use ffmpeg_next::ffi; // = ffmpeg_sys_next 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_bit_rate(bitrate_bps as usize);
video.set_max_bit_rate(bitrate_bps as usize); video.set_max_bit_rate(bitrate_bps as usize);
video.set_max_b_frames(0); 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); .clamp(1.0, i32::MAX as f64);
// SAFETY: `video` wraps a freshly-allocated `AVCodecContext` we hold by value and have not opened // 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 // yet; `as_mut_ptr()` returns that non-null, aligned, exclusively-owned context. Writing the plain
@@ -12,12 +12,12 @@
#![deny(clippy::undocumented_unsafe_blocks)] #![deny(clippy::undocumented_unsafe_blocks)]
use super::{ChromaFormat, Codec, EncodedFrame, Encoder}; use super::{ChromaFormat, Codec, EncodedFrame, Encoder};
use crate::capture::{CapturedFrame, FramePayload, PixelFormat};
use anyhow::{anyhow, bail, Context, Result}; use anyhow::{anyhow, bail, Context, Result};
use ffmpeg::format::Pixel; use ffmpeg::format::Pixel;
use ffmpeg::util::frame::Video as VideoFrame; use ffmpeg::util::frame::Video as VideoFrame;
use ffmpeg::{codec, encoder, Dictionary}; use ffmpeg::{codec, encoder, Dictionary};
use ffmpeg_next as ffmpeg; use ffmpeg_next as ffmpeg;
use pf_frame::{CapturedFrame, FramePayload, PixelFormat};
use std::os::raw::c_int; use std::os::raw::c_int;
use std::ptr; use std::ptr;
@@ -347,7 +347,7 @@ impl NvencEncoder {
// hwdevice/hwframes contexts and set `pix_fmt = CUDA` on the raw encoder context // hwdevice/hwframes contexts and set `pix_fmt = CUDA` on the raw encoder context
// *before* open (NVENC derives the device from `hw_frames_ctx`). // *before* open (NVENC derives the device from `hw_frames_ctx`).
let cuda_hw = if cuda { 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()` // 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 // 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` // `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 /// 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 /// 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. /// 1080p, sub-millisecond on the GPU) and keeps the host fully off the pixel path.
fn submit_cuda( fn submit_cuda(&mut self, buf: &pf_zerocopy::DeviceBuffer, pts: i64, idr: bool) -> Result<()> {
&mut self,
buf: &crate::zerocopy::DeviceBuffer,
pts: i64,
idr: bool,
) -> Result<()> {
let frames_ref = self let frames_ref = self
.cuda .cuda
.as_ref() .as_ref()
@@ -735,7 +730,7 @@ impl NvencEncoder {
.frames_ref; .frames_ref;
// The device→device copy below uses our shared context directly; make it current on the // 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). // 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 // 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 // `.context(..)?` above), and the shared CUDA context was just made current on THIS thread
// (`make_current()?`), the precondition for the device-pointer copies below. // (`make_current()?`), the precondition for the device-pointer copies below.
@@ -770,11 +765,11 @@ impl NvencEncoder {
let copy_res = if buf.yuv444 { let copy_res = if buf.yuv444 {
let dsts = core::array::from_fn(|i| { 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, (*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 { } else if self.want_444 {
ffi::av_frame_free(&mut f); ffi::av_frame_free(&mut f);
bail!( bail!(
@@ -783,15 +778,15 @@ impl NvencEncoder {
CPU 4:4:4 path on this compositor" CPU 4:4:4 path on this compositor"
); );
} else if buf.is_nv12() { } 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 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; 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 { } 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; 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 { if let Err(e) = copy_res {
ffi::av_frame_free(&mut f); 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 /// 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`] /// 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 /// 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). /// 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 { pub fn probe_can_encode_444(codec: Codec) -> bool {
if codec != Codec::H265 { if codec != Codec::H265 {
@@ -36,9 +36,9 @@ use super::nvenc_core::{
}; };
use super::nvenc_status; use super::nvenc_status;
use super::{ChromaFormat, Codec, EncodedFrame, Encoder, EncoderCaps}; 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 anyhow::{anyhow, bail, Context, Result};
use pf_frame::{CapturedFrame, FramePayload};
use pf_zerocopy::cuda::{self, InputSurface};
use std::collections::VecDeque; use std::collections::VecDeque;
use std::ffi::c_void; use std::ffi::c_void;
use std::ptr; use std::ptr;
@@ -321,7 +321,7 @@ impl NvencCudaEncoder {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn open( pub fn open(
codec: Codec, codec: Codec,
_format: crate::capture::PixelFormat, _format: pf_frame::PixelFormat,
width: u32, width: u32,
height: u32, height: u32,
fps: u32, fps: u32,
@@ -1253,8 +1253,8 @@ impl Drop for NvencCudaEncoder {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::capture::{CapturedFrame, FramePayload, PixelFormat}; use pf_frame::{CapturedFrame, FramePayload, PixelFormat};
use crate::zerocopy::cuda::DeviceBuffer; use pf_zerocopy::cuda::DeviceBuffer;
fn nv12_frame(w: u32, h: u32, i: u32) -> CapturedFrame { fn nv12_frame(w: u32, h: u32, i: u32) -> CapturedFrame {
// Content is uninitialized device memory — NVENC encodes it fine; this smoke test asserts the // 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() { fn nvenc_cuda_smoke_rfi_anchor() {
const W: u32 = 1280; const W: u32 = 1280;
const H: u32 = 720; 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( let mut enc = NvencCudaEncoder::open(
Codec::H265, Codec::H265,
@@ -1358,7 +1358,7 @@ mod tests {
fn nvenc_cuda_yuv444() { fn nvenc_cuda_yuv444() {
const W: u32 = 1280; const W: u32 = 1280;
const H: u32 = 720; 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( let mut enc = NvencCudaEncoder::open(
Codec::H265, Codec::H265,
PixelFormat::Yuv444, PixelFormat::Yuv444,
@@ -1403,7 +1403,7 @@ mod tests {
fn nvenc_cuda_reconfigure_no_idr() { fn nvenc_cuda_reconfigure_no_idr() {
const W: u32 = 1280; const W: u32 = 1280;
const H: u32 = 720; 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( let mut enc = NvencCudaEncoder::open(
Codec::H265, Codec::H265,
PixelFormat::Nv12, PixelFormat::Nv12,
@@ -1510,7 +1510,7 @@ mod tests {
fn nvenc_cuda_codec_switch_reopen() { fn nvenc_cuda_codec_switch_reopen() {
const W: u32 = 1280; const W: u32 = 1280;
const H: u32 = 720; 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 [ for (leg, codec) in [
Codec::H265, Codec::H265,
Codec::Av1, Codec::Av1,
@@ -1552,7 +1552,7 @@ mod tests {
fn nvenc_cuda_dirty_teardown_reopen() { fn nvenc_cuda_dirty_teardown_reopen() {
const W: u32 = 1280; const W: u32 = 1280;
const H: u32 = 720; 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 { for round in 0..3 {
let mut enc = open_h265(); let mut enc = open_h265();
for f in 0..4u32 { for f in 0..4u32 {
@@ -1581,7 +1581,7 @@ mod tests {
fn nvenc_cuda_open_failure_diagnosis_and_recovery() { fn nvenc_cuda_open_failure_diagnosis_and_recovery() {
const W: u32 = 1280; const W: u32 = 1280;
const H: u32 = 720; 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"); try_api().expect("nvenc api");
let shared = cuda::context().expect("shared ctx"); let shared = cuda::context().expect("shared ctx");
@@ -24,11 +24,11 @@
// Every unsafe block in this module carries a `// SAFETY:` proof (parent module enforces it). // 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 super::vk_util::{color_range, find_mem, import_rgb_dmabuf, make_plain_image, pixel_to_vk};
use crate::capture::{CapturedFrame, FramePayload}; use crate::{EncodedFrame, Encoder, EncoderCaps};
use crate::encode::{EncodedFrame, Encoder, EncoderCaps};
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
use ash::vk; use ash::vk;
use ash::vk::Handle as _; use ash::vk::Handle as _;
use pf_frame::{CapturedFrame, FramePayload};
use pyrowave_sys as pw; use pyrowave_sys as pw;
use std::collections::VecDeque; use std::collections::VecDeque;
use std::os::fd::AsRawFd; use std::os::fd::AsRawFd;
@@ -637,10 +637,7 @@ impl PyroWaveEncoder {
/// Records the small upload (only when the bitmap `serial` changed) + layout transition into /// 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 /// `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. /// shared image never races a prior frame; the first use transitions it to SHADER_READ_ONLY.
unsafe fn prep_cursor( unsafe fn prep_cursor(&mut self, cursor: Option<&pf_frame::CursorOverlay>) -> Result<[i32; 4]> {
&mut self,
cursor: Option<&crate::capture::CursorOverlay>,
) -> Result<[i32; 4]> {
let dev = self.device.clone(); let dev = self.device.clone();
let cmd = self.cmd; let cmd = self.cmd;
let img = self.cursor_img; 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`. /// Import a dmabuf with per-buffer caching — same policy as `vulkan_video.rs::import_cached`.
unsafe fn import_cached( unsafe fn import_cached(
&mut self, &mut self,
d: &crate::capture::DmabufFrame, d: &pf_frame::DmabufFrame,
cw: u32, cw: u32,
ch: u32, ch: u32,
) -> Result<(vk::Image, vk::ImageView, bool)> { ) -> Result<(vk::Image, vk::ImageView, bool)> {
@@ -1303,7 +1300,7 @@ impl Drop for PyroWaveEncoder {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::capture::PixelFormat; use pf_frame::PixelFormat;
fn cpu_frame(w: u32, h: u32, pts_ns: u64, fill: [u8; 4]) -> CapturedFrame { fn cpu_frame(w: u32, h: u32, pts_ns: u64, fill: [u8; 4]) -> CapturedFrame {
let mut buf = vec![0u8; (w * h * 4) as usize]; let mut buf = vec![0u8; (w * h * 4) as usize];
@@ -23,11 +23,11 @@
#![deny(clippy::undocumented_unsafe_blocks)] #![deny(clippy::undocumented_unsafe_blocks)]
use super::{Codec, EncodedFrame, Encoder}; use super::{Codec, EncodedFrame, Encoder};
use crate::capture::{CapturedFrame, DmabufFrame, FramePayload, PixelFormat};
use anyhow::{anyhow, bail, Context, Result}; use anyhow::{anyhow, bail, Context, Result};
use ffmpeg::format::Pixel; use ffmpeg::format::Pixel;
use ffmpeg::{codec, encoder, Dictionary}; use ffmpeg::{codec, encoder, Dictionary};
use ffmpeg_next as ffmpeg; use ffmpeg_next as ffmpeg;
use pf_frame::{CapturedFrame, DmabufFrame, FramePayload, PixelFormat};
use std::ffi::{CStr, CString}; use std::ffi::{CStr, CString};
use std::os::fd::AsRawFd; use std::os::fd::AsRawFd;
use std::os::raw::c_int; use std::os::raw::c_int;
@@ -561,7 +561,7 @@ impl DmabufInner {
fps: u32, fps: u32,
bitrate_bps: u64, bitrate_bps: u64,
) -> Result<Self> { ) -> 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)"))?; .ok_or_else(|| anyhow!("no DRM fourcc for {format:?} (VAAPI zero-copy)"))?;
let node = render_node(); let node = render_node();
// SAFETY: libav is initialized (`VaapiEncoder::open` ran `ffmpeg::init()` before // SAFETY: libav is initialized (`VaapiEncoder::open` ran `ffmpeg::init()` before
@@ -3,9 +3,9 @@
//! when the PyroWave backend arrived so the two don't fork copies. //! when the PyroWave backend arrived so the two don't fork copies.
// Every unsafe block carries a `// SAFETY:` proof (parent module enforces it). // Every unsafe block carries a `// SAFETY:` proof (parent module enforces it).
use crate::capture::PixelFormat;
use anyhow::Result; use anyhow::Result;
use ash::vk; use ash::vk;
use pf_frame::PixelFormat;
pub(crate) fn color_range(layer: u32) -> vk::ImageSubresourceRange { pub(crate) fn color_range(layer: u32) -> vk::ImageSubresourceRange {
vk::ImageSubresourceRange { vk::ImageSubresourceRange {
@@ -74,7 +74,7 @@ pub(crate) unsafe fn import_rgb_dmabuf(
device: &ash::Device, device: &ash::Device,
ext_fd: &ash::khr::external_memory_fd::Device, ext_fd: &ash::khr::external_memory_fd::Device,
mem_props: &vk::PhysicalDeviceMemoryProperties, mem_props: &vk::PhysicalDeviceMemoryProperties,
d: &crate::capture::DmabufFrame, d: &pf_frame::DmabufFrame,
cw: u32, cw: u32,
ch: u32, ch: u32,
) -> Result<(vk::Image, vk::DeviceMemory, vk::ImageView)> { ) -> Result<(vk::Image, vk::DeviceMemory, vk::ImageView)> {
@@ -11,10 +11,10 @@
#![allow(clippy::too_many_arguments)] #![allow(clippy::too_many_arguments)]
use super::vk_util::{color_range, find_mem, make_plain_image, make_view, pixel_to_vk}; use super::vk_util::{color_range, find_mem, make_plain_image, make_view, pixel_to_vk};
use crate::capture::{CapturedFrame, FramePayload}; use crate::{Codec, EncodedFrame, Encoder, EncoderCaps};
use crate::encode::{Codec, EncodedFrame, Encoder, EncoderCaps};
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
use ash::vk; use ash::vk;
use pf_frame::{CapturedFrame, FramePayload};
use std::collections::VecDeque; use std::collections::VecDeque;
use std::ffi::c_void; use std::ffi::c_void;
use std::os::fd::AsRawFd; use std::os::fd::AsRawFd;
@@ -729,7 +729,7 @@ impl VulkanVideoEncoder {
&mut self, &mut self,
slot: usize, slot: usize,
compute_cmd: vk::CommandBuffer, compute_cmd: vk::CommandBuffer,
cursor: Option<&crate::capture::CursorOverlay>, cursor: Option<&pf_frame::CursorOverlay>,
) -> Result<[i32; 4]> { ) -> Result<[i32; 4]> {
let dev = self.device.clone(); let dev = self.device.clone();
let img = self.frames[slot].cursor_img; 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. /// Import a packed-RGB dmabuf as a SAMPLED VkImage (explicit DRM modifier). Caller destroys.
unsafe fn import_dmabuf( unsafe fn import_dmabuf(
&self, &self,
d: &crate::capture::DmabufFrame, d: &pf_frame::DmabufFrame,
cw: u32, cw: u32,
ch: u32, ch: u32,
) -> Result<(vk::Image, vk::DeviceMemory, vk::ImageView)> { ) -> 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). /// true only on a first import (caller uses UNDEFINED old-layout to preserve modifier-tiled data).
unsafe fn import_cached( unsafe fn import_cached(
&mut self, &mut self,
d: &crate::capture::DmabufFrame, d: &pf_frame::DmabufFrame,
cw: u32, cw: u32,
ch: u32, ch: u32,
) -> Result<(vk::Image, vk::ImageView, bool)> { ) -> Result<(vk::Image, vk::ImageView, bool)> {
@@ -2680,8 +2680,8 @@ unsafe fn build_parameters_av1(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{build_h265_rps_s0, pick_recovery_slot, VulkanVideoEncoder}; use super::{build_h265_rps_s0, pick_recovery_slot, VulkanVideoEncoder};
use crate::capture::{CapturedFrame, FramePayload, PixelFormat}; use crate::{Codec, Encoder};
use crate::encode::{Codec, Encoder}; use pf_frame::{CapturedFrame, FramePayload, PixelFormat};
/// The RFI anchor picker: newest resident wire strictly older than the loss; empty/newer /// The RFI anchor picker: newest resident wire strictly older than the loss; empty/newer
/// slots never qualify. /// 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 /// 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`] /// 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). /// 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| { let env_dim = |k: &str, d: u32| {
std::env::var(k) std::env::var(k)
.ok() .ok()
@@ -2782,7 +2782,7 @@ mod tests {
[120, 200, 80, 255], [120, 200, 80, 255],
[80, 120, 200, 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() { for (i, c) in colors.iter().enumerate() {
if i == SMOKE_ANCHOR { if i == SMOKE_ANCHOR {
// The client reports wire frame SMOKE_LOST lost → the next frame must re-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 /// 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" /// anchor's reference (frame 3 / POC 3) means reference retention regressed and the "clean"
/// re-anchor ships corruption. /// 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 { let Ok(home) = std::env::var("HOME") else {
return; return;
}; };
@@ -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. // when the GPU advertises custom-VBV support — else keep the preset default.
if c.custom_vbv { if c.custom_vbv {
// ~1-frame VBV by default; PUNKTFUNK_VBV_FRAMES scales it (parity with AMF/VAAPI/QSV). // ~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; .clamp(1.0, u32::MAX as f64) as u32;
cfg.rcParams.vbvBufferSize = vbv; cfg.rcParams.vbvBufferSize = vbv;
cfg.rcParams.vbvInitialDelay = vbv; cfg.rcParams.vbvInitialDelay = vbv;
@@ -12,7 +12,6 @@
#![deny(clippy::undocumented_unsafe_blocks)] #![deny(clippy::undocumented_unsafe_blocks)]
use super::{EncodedFrame, Encoder}; use super::{EncodedFrame, Encoder};
use crate::capture::{CapturedFrame, FramePayload, PixelFormat};
use anyhow::{bail, ensure, Context, Result}; use anyhow::{bail, ensure, Context, Result};
use openh264::encoder::{ use openh264::encoder::{
BitRate, Complexity, Encoder as Oh264, EncoderConfig, FrameRate, FrameType, IntraFramePeriod, BitRate, Complexity, Encoder as Oh264, EncoderConfig, FrameRate, FrameType, IntraFramePeriod,
@@ -20,6 +19,7 @@ use openh264::encoder::{
}; };
use openh264::formats::YUVSlices; use openh264::formats::YUVSlices;
use openh264::OpenH264API; use openh264::OpenH264API;
use pf_frame::{CapturedFrame, FramePayload, PixelFormat};
use std::collections::VecDeque; use std::collections::VecDeque;
pub struct OpenH264Encoder { pub struct OpenH264Encoder {
@@ -258,7 +258,7 @@ fn num_threads() -> u16 {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; 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 → /// 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: /// (16,128,128), pure red's Cr must hit the positive extreme 240 (it does exactly:
@@ -47,8 +47,8 @@
#![deny(clippy::undocumented_unsafe_blocks)] #![deny(clippy::undocumented_unsafe_blocks)]
use super::{ChromaFormat, Codec, EncodedFrame, Encoder, EncoderCaps}; use super::{ChromaFormat, Codec, EncodedFrame, Encoder, EncoderCaps};
use crate::capture::{CapturedFrame, FramePayload, PixelFormat};
use anyhow::{anyhow, bail, Context, Result}; use anyhow::{anyhow, bail, Context, Result};
use pf_frame::{CapturedFrame, FramePayload, PixelFormat};
use std::collections::VecDeque; use std::collections::VecDeque;
use std::ffi::c_void; use std::ffi::c_void;
use std::ptr; use std::ptr;
@@ -1334,7 +1334,7 @@ impl AmfEncoder {
/// same shape every backend ships. Shared by [`apply_static_props`](Self::apply_static_props) /// 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. /// and [`Encoder::reconfigure_bitrate`] so a dynamic retarget rescales the buffer it opened with.
fn vbv_bits(&self, bps: u64) -> i64 { 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 .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 /// 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 /// 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 /// 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 /// encoder will really open. H.264 is always `false` (High10 is not a VCN mode — the session
/// open bails on it too). /// open bails on it too).
pub fn probe_can_encode_10bit(codec: Codec) -> bool { 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) // `D3D11CreateDevice` (explicit adapter + UNKNOWN driver type, or NULL adapter + HARDWARE)
// fills `device` only on success. Everything drops with its COM wrapper. // fills `device` only on success. Everything drops with its COM wrapper.
unsafe { unsafe {
let adapter: Option<IDXGIAdapter1> = crate::win_adapter::resolve_render_adapter_luid() let adapter: Option<IDXGIAdapter1> =
.and_then(|luid| { pf_gpu::resolve_render_adapter_luid().and_then(|luid| {
let factory: IDXGIFactory4 = CreateDXGIFactory1().ok()?; let factory: IDXGIFactory4 = CreateDXGIFactory1().ok()?;
factory.EnumAdapterByLuid(luid).ok() factory.EnumAdapterByLuid(luid).ok()
}); });
@@ -2785,7 +2785,7 @@ mod tests {
height: h, height: h,
pts_ns: 1 + i as u64, pts_ns: 1 + i as u64,
format: fmt, format: fmt,
payload: FramePayload::D3d11(crate::capture::dxgi::D3d11Frame { payload: FramePayload::D3d11(pf_frame::dxgi::D3d11Frame {
texture: tex.clone(), texture: tex.clone(),
device: device.clone(), device: device.clone(),
}), }),
@@ -2856,8 +2856,8 @@ mod tests {
); );
drop(native); drop(native);
let mut ffmpeg = crate::encode::ffmpeg_win::FfmpegWinEncoder::open( let mut ffmpeg = crate::ffmpeg_win::FfmpegWinEncoder::open(
crate::encode::ffmpeg_win::WinVendor::Amf, crate::ffmpeg_win::WinVendor::Amf,
Codec::H265, Codec::H265,
PixelFormat::Nv12, PixelFormat::Nv12,
w, w,
@@ -2970,7 +2970,7 @@ mod tests {
height: h, height: h,
pts_ns: base + i as u64, pts_ns: base + i as u64,
format: PixelFormat::Nv12, format: PixelFormat::Nv12,
payload: FramePayload::D3d11(crate::capture::dxgi::D3d11Frame { payload: FramePayload::D3d11(pf_frame::dxgi::D3d11Frame {
texture: tex.clone(), texture: tex.clone(),
device: device.clone(), device: device.clone(),
}), }),
@@ -3111,7 +3111,7 @@ mod tests {
height: h, height: h,
pts_ns: 1 + i as u64, pts_ns: 1 + i as u64,
format: PixelFormat::P010, format: PixelFormat::P010,
payload: FramePayload::D3d11(crate::capture::dxgi::D3d11Frame { payload: FramePayload::D3d11(pf_frame::dxgi::D3d11Frame {
texture: tex.clone(), texture: tex.clone(),
device: device.clone(), device: device.clone(),
}), }),
@@ -3258,7 +3258,7 @@ mod tests {
height: h, height: h,
pts_ns: i, pts_ns: i,
format: PixelFormat::Nv12, format: PixelFormat::Nv12,
payload: FramePayload::D3d11(crate::capture::dxgi::D3d11Frame { payload: FramePayload::D3d11(pf_frame::dxgi::D3d11Frame {
texture: tex.clone(), texture: tex.clone(),
device: device.clone(), device: device.clone(),
}), }),
@@ -41,11 +41,11 @@
#![deny(clippy::undocumented_unsafe_blocks)] #![deny(clippy::undocumented_unsafe_blocks)]
use super::{ChromaFormat, Codec, EncodedFrame, Encoder}; use super::{ChromaFormat, Codec, EncodedFrame, Encoder};
use crate::capture::{dxgi::D3d11Frame, CapturedFrame, FramePayload, PixelFormat};
use anyhow::{anyhow, bail, Context, Result}; use anyhow::{anyhow, bail, Context, Result};
use ffmpeg::format::Pixel; use ffmpeg::format::Pixel;
use ffmpeg::{codec, encoder, Dictionary}; use ffmpeg::{codec, encoder, Dictionary};
use ffmpeg_next as ffmpeg; 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::os::raw::{c_int, c_uint, c_void};
use std::ptr; use std::ptr;
use windows::core::Interface; use windows::core::Interface;
@@ -41,8 +41,8 @@ use super::nvenc_core::{
}; };
use super::nvenc_status; use super::nvenc_status;
use super::{ChromaFormat, Codec, EncodedFrame, Encoder, EncoderCaps}; use super::{ChromaFormat, Codec, EncodedFrame, Encoder, EncoderCaps};
use crate::capture::{CapturedFrame, FramePayload, PixelFormat};
use anyhow::{anyhow, bail, Context, Result}; use anyhow::{anyhow, bail, Context, Result};
use pf_frame::{CapturedFrame, FramePayload, PixelFormat};
use std::collections::{HashMap, VecDeque}; use std::collections::{HashMap, VecDeque};
use std::ffi::c_void; use std::ffi::c_void;
use std::ptr; 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`). /// 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 /// 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. /// card without it). See [`probe_encode_cap`] for the throwaway-session mechanics.
pub fn probe_can_encode_444(codec: Codec) -> bool { 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** /// 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). /// (`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 /// 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 /// 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). /// ([`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 // 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 // (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. // (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() let adapter: Option<IDXGIAdapter1> =
.and_then(|luid| { pf_gpu::resolve_render_adapter_luid().and_then(|luid| {
let factory: IDXGIFactory4 = CreateDXGIFactory1().ok()?; let factory: IDXGIFactory4 = CreateDXGIFactory1().ok()?;
factory.EnumAdapterByLuid(luid).ok() factory.EnumAdapterByLuid(luid).ok()
}); });
@@ -1692,7 +1692,7 @@ fn probe_encode_cap(codec: Codec, cap: nv::NV_ENC_CAPS) -> bool {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::capture::{dxgi::D3d11Frame, CapturedFrame, FramePayload}; use pf_frame::{dxgi::D3d11Frame, CapturedFrame, FramePayload};
use windows::Win32::Graphics::Direct3D11::{ use windows::Win32::Graphics::Direct3D11::{
D3D11_BIND_RENDER_TARGET, D3D11_SUBRESOURCE_DATA, D3D11_TEXTURE2D_DESC, D3D11_USAGE_DEFAULT, 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 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 bytes = probe_pattern(W as usize, H as usize);
let init = D3D11_SUBRESOURCE_DATA { let init = D3D11_SUBRESOURCE_DATA {
@@ -1860,7 +1860,7 @@ mod tests {
} }
} }
let adapter = adapter.expect("no hardware DXGI adapter"); 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 bytes = probe_pattern(W as usize, H as usize);
let init = D3D11_SUBRESOURCE_DATA { 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 //! 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 //! 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 //! 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 // 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)] #![deny(clippy::undocumented_unsafe_blocks)]
use crate::capture::{CapturedFrame, PixelFormat};
use anyhow::Result; use anyhow::Result;
use pf_frame::{CapturedFrame, PixelFormat};
#[path = "enc/codec.rs"]
mod codec; mod codec;
pub(crate) use codec::*; pub use codec::*;
impl Codec { impl Codec {
/// The `quic` codec bitfield the host can currently **emit** on the punktfunk/1 native path, /// 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 /// 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) /// 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`] — /// 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 /// 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 // 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 // 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 // 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 // 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). // keeps the bit off (no Vulkan device to open).
#[cfg(all(target_os = "linux", feature = "pyrowave"))] #[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. /// otherwise.
struct TrackedEncoder { struct TrackedEncoder {
inner: Box<dyn Encoder>, 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 /// 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). /// (the VAAPI LINEAR-only policy starves it on Mutter+NVIDIA, which allocates tiled only).
#[cfg(all(target_os = "linux", feature = "pyrowave"))] #[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) 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 // 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 // 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. // backend always matches the GPU the capture ring and virtual display sit on.
// --------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum WindowsBackend { pub enum WindowsBackend {
Nvenc, Nvenc,
Amf, Amf,
Qsv, Qsv,
@@ -943,7 +949,7 @@ enum GpuVendor {
/// render adapter's vendor). Shared by [`open_video`] and the GameStream codec advertisement so /// render adapter's vendor). Shared by [`open_video`] and the GameStream codec advertisement so
/// both agree. /// both agree.
#[cfg(target_os = "windows")] #[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. // Resolved ONCE in HostConfig (Goal-1) — was re-read from PUNKTFUNK_ENCODER on every call.
match pf_host_config::config().encoder_pref.as_str() { match pf_host_config::config().encoder_pref.as_str() {
"nvenc" | "hw" | "nvidia" | "cuda" => WindowsBackend::Nvenc, "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 /// 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 /// 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 /// 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 /// 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). /// frames are GPU-resident (plan §2.4 / §W4).
#[cfg(target_os = "windows")] #[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) !matches!(windows_resolved_backend(), WindowsBackend::Software)
} }
/// Linux/other: every backend but the GPU-less software encoder (openh264) is GPU-resident. Config-backed /// 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`]). /// (mirrors `session_plan::resolve_encoder`; the NVENC vs VAAPI split is auto-detected in [`open_video`]).
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
pub(crate) fn resolved_backend_is_gpu() -> bool { pub fn resolved_backend_is_gpu() -> bool {
!matches!( !matches!(
pf_host_config::config().encoder_pref.as_str(), pf_host_config::config().encoder_pref.as_str(),
"software" | "sw" | "openh264" "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 — /// 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 /// 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 /// 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. /// backend, so the gate never consults this there.
#[cfg(target_os = "windows")] #[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 windows_resolved_backend() == WindowsBackend::Nvenc
} }
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
pub(crate) fn resolved_backend_ingests_rgb_444() -> bool { pub fn resolved_backend_ingests_rgb_444() -> bool {
false 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 /// 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. /// (GeForce consumer limit); AMF/QSV equivalents follow the same seam when they grow accounting.
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub(crate) fn can_open_another_session() -> bool { pub fn can_open_another_session() -> bool {
#[cfg(feature = "nvenc")] #[cfg(feature = "nvenc")]
{ {
nvenc::can_open_another_session() 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 // 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 // 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 — // 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. // no build feature, the driver-installed amfrt64.dll resolves at runtime like NVENC's DLL.
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
#[path = "encode/windows/amf.rs"] #[path = "enc/windows/amf.rs"]
mod amf; mod amf;
#[cfg(all(target_os = "windows", feature = "amf-qsv"))] #[cfg(all(target_os = "windows", feature = "amf-qsv"))]
#[path = "encode/windows/ffmpeg_win.rs"] #[path = "enc/windows/ffmpeg_win.rs"]
mod ffmpeg_win; mod ffmpeg_win;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
#[path = "enc/linux/mod.rs"]
mod linux; mod linux;
// Direct-SDK NVENC on Linux (CUDA input; design/linux-direct-nvenc.md) — real RFI + recovery anchor // 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 // + 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 // `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. // path, so `--features nvenc` stays safe on a driver-less/AMD Linux box.
#[cfg(all(target_os = "windows", feature = "nvenc"))] #[cfg(all(target_os = "windows", feature = "nvenc"))]
#[path = "encode/windows/nvenc.rs"] #[path = "enc/windows/nvenc.rs"]
mod nvenc; mod nvenc;
#[cfg(all(target_os = "linux", feature = "nvenc"))] #[cfg(all(target_os = "linux", feature = "nvenc"))]
#[path = "encode/linux/nvenc_cuda.rs"] #[path = "enc/linux/nvenc_cuda.rs"]
mod nvenc_cuda; mod nvenc_cuda;
// Actionable `NVENCSTATUS` → cause mapping shared by both direct-NVENC backends, so a failed // 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?)". // 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"))] #[cfg(all(any(target_os = "linux", target_os = "windows"), feature = "nvenc"))]
#[path = "encode/nvenc_status.rs"] #[path = "enc/nvenc_status.rs"]
mod nvenc_status; mod nvenc_status;
// Platform-agnostic direct-SDK NVENC glue (`NvStatusExt`/`nv_ok`, `codec_guid`) shared by both // 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`. // `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"))] #[cfg(all(any(target_os = "linux", target_os = "windows"), feature = "nvenc"))]
#[path = "encode/nvenc_core.rs"] #[path = "enc/nvenc_core.rs"]
mod nvenc_core; mod nvenc_core;
// Shared libavcodec glue (`pixel_to_av`, swscale consts) for the three libav backends — Linux // 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). // 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")))] #[cfg(any(target_os = "linux", all(target_os = "windows", feature = "amf-qsv")))]
#[path = "enc/libav.rs"]
mod libav; mod libav;
// Software (openh264) H.264 encoder — the GPU-less path on BOTH Windows and Linux (a headless / // 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 // 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. // consumes CPU RGB `CapturedFrame`s and the statically-bundled openh264 build.
#[cfg(any(target_os = "windows", target_os = "linux"))] #[cfg(any(target_os = "windows", target_os = "linux"))]
#[path = "enc/sw.rs"]
mod sw; mod sw;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
#[path = "encode/linux/vaapi.rs"] #[path = "enc/linux/vaapi.rs"]
mod vaapi; mod vaapi;
// Raw Vulkan Video HEVC encode on Linux (AMD/Intel; design/linux-vulkan-video-encode.md) — real RFI // 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 // 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 // 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`. // `PUNKTFUNK_VULKAN_ENCODE` until on-glass validated; needs `--features vulkan-encode`.
#[cfg(all(target_os = "linux", feature = "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; mod vulkan_video;
// Vendored `VK_KHR_video_encode_av1` bindings (host-only) — the AV1 encode structs our pinned // 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 // `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 // generated code rather than bumping `ash` (which breaks the SDL/Vulkan client). Consumed by
// `vulkan_video.rs` via `super::vk_av1_encode`. // `vulkan_video.rs` via `super::vk_av1_encode`.
#[cfg(all(target_os = "linux", feature = "vulkan-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; mod vk_av1_encode;
// Small ash leaf helpers shared by the Linux Vulkan encode backends (dmabuf import, image/memory // 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. // utilities) — extracted from `vulkan_video.rs` when the PyroWave backend arrived.
@@ -1171,13 +1180,13 @@ mod vk_av1_encode;
target_os = "linux", target_os = "linux",
any(feature = "vulkan-encode", feature = "pyrowave") any(feature = "vulkan-encode", feature = "pyrowave")
))] ))]
#[path = "encode/linux/vk_util.rs"] #[path = "enc/linux/vk_util.rs"]
mod vk_util; mod vk_util;
// PyroWave — the opt-in wired-LAN intra-only wavelet codec (design/pyrowave-codec-plan.md §4.3): // 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. // 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. // Explicit-only behind PUNKTFUNK_ENCODER=pyrowave; EXPERIMENTAL until CODEC_PYROWAVE lands.
#[cfg(all(target_os = "linux", feature = "pyrowave"))] #[cfg(all(target_os = "linux", feature = "pyrowave"))]
#[path = "encode/linux/pyrowave.rs"] #[path = "enc/linux/pyrowave.rs"]
mod pyrowave; mod pyrowave;
#[cfg(test)] #[cfg(test)]
+5
View File
@@ -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 /// 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 /// 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. /// 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)> { pub unsafe fn make_device(adapter: &IDXGIAdapter1) -> Result<(ID3D11Device, ID3D11DeviceContext)> {
let mut device: Option<ID3D11Device> = None; let mut device: Option<ID3D11Device> = None;
let mut context: Option<ID3D11DeviceContext> = None; let mut context: Option<ID3D11DeviceContext> = None;
+36
View File
@@ -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
}
}
}
+24 -52
View File
@@ -25,6 +25,11 @@ pf-frame = { path = "../pf-frame" }
# Windows display-topology helpers (CCD/GDI mode-set, PnP monitor devnodes, display-change watch), # 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. # 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" } 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). # M3 native control plane (the `punktfunk/1` QUIC handshake; data plane stays native-thread UDP).
quinn = "0.11" quinn = "0.11"
anyhow = "1" 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 # 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. # 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] [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" 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] [target.'cfg(target_os = "linux")'.dependencies]
# `screencast` gates the ScreenCast portal module; `remote_desktop` adds the RemoteDesktop # `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 # `open_pipe_wire_remote` is unconditional, so ashpd's own `pipewire` feature is not
# needed — we drive PipeWire with the `pipewire` crate below. # needed — we drive PipeWire with the `pipewire` crate below.
ashpd = { version = "0.13", features = ["screencast", "remote_desktop"] } ashpd = { version = "0.13", features = ["screencast", "remote_desktop"] }
ffmpeg-next = "8"
libc = "0.2" 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 # 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. # unique per build), i.e. 0.9 — NOT the 0.10 the setup doc mentions.
pipewire = "0.9" pipewire = "0.9"
@@ -254,20 +243,6 @@ winreg = "0.56"
roxmltree = "0.21" roxmltree = "0.21"
# WASAPI loopback audio capture (default render endpoint -> 48 kHz stereo f32 for the Opus path). # WASAPI loopback audio capture (default render endpoint -> 48 kHz stereo f32 for the Opus path).
wasapi = "0.23" 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 # 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, # (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 # 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" } pf-driver-proto = { path = "../pf-driver-proto" }
bytemuck = { version = "1.19", features = ["derive"] } 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] [features]
# PyroWave ships in every default build (the codec stays strictly opt-in per session — a client # 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). # must explicitly prefer CODEC_PYROWAVE; nothing changes for normal HEVC/AV1 sessions).
default = ["pyrowave"] default = ["pyrowave"]
# NVENC hardware encode (Windows). OFF by default (it pulls the NVENC SDK crate); nothing is # NVENC hardware encode (Linux CUDA + Windows D3D11). OFF by default; entry points resolved at
# needed at link time — the entry points are resolved at RUNTIME from the driver's # RUNTIME from the driver DLL/so, so the same binary starts fine on AMD/Intel boxes. Build the GPU
# nvEncodeAPI64.dll (encode/windows/nvenc.rs `load_api`), so the same binary starts fine on # host with `--features nvenc`.
# AMD/Intel-only boxes and falls through to AMF/QSV/software. Build the GPU host with nvenc = ["pf-encode/nvenc"]
# `--features nvenc`. # AMD/Intel hardware encode on Windows (AMF/QSV via ffmpeg-next). OFF by default: needs a `FFMPEG_DIR`
nvenc = ["dep:nvidia-video-codec-sdk"] # (BtbN lgpl-shared with `*_amf`/`*_qsv`) at build and bundles the FFmpeg DLLs at runtime. Build the
# AMD/Intel hardware encode on Windows (AMF/QSV via ffmpeg-next). OFF by default: it needs a # all-vendor GPU host with `--features nvenc,amf-qsv`.
# `FFMPEG_DIR` (BtbN lgpl-shared — includes `*_amf`/`*_qsv`; the GPL-only x264/x265 are never used, amf-qsv = ["pf-encode/amf-qsv"]
# so the LGPL build suffices and keeps the bundled DLLs LGPL, not GPL) at build time and bundles the # Raw Vulkan Video HEVC/AV1 encode on Linux (AMD/Intel) — real reference-frame-invalidation loss
# FFmpeg DLLs at runtime. Build the all-vendor GPU host with `--features nvenc,amf-qsv`. # recovery via explicit DPB reference slots (design/linux-vulkan-video-encode.md). OFF by default;
amf-qsv = ["dep:ffmpeg-next"] # reuses pf-encode's `ash` bindings (no new dep). Runtime-gated further by PUNKTFUNK_VULKAN_ENCODE.
# Raw Vulkan Video HEVC encode on Linux (AMD/Intel) — real reference-frame-invalidation loss vulkan-encode = ["pf-encode/vulkan-encode"]
# recovery via explicit DPB reference slots (design/linux-vulkan-video-encode.md), the open-stack # PyroWave — the opt-in wired-LAN intra-only wavelet codec (design/pyrowave-codec-plan.md). Builds
# twin of the direct-NVENC path. OFF by default; pulls NO new dependency (reuses the `ash` Vulkan # the vendored codec from source in pf-encode. ON by default (see `default`); sessions reach it only
# bindings already carried for the dmabuf zero-copy bridge). Runtime-gated further by # through explicit client opt-in.
# PUNKTFUNK_VULKAN_ENCODE (opt-in for now). Build the AMD/Intel RFI host with `--features vulkan-encode`. pyrowave = ["pf-encode/pyrowave"]
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"]
# Build-time icon/version-info embedding (build.rs; Windows dev/CI hosts only — Linux packaging # Build-time icon/version-info embedding (build.rs; Windows dev/CI hosts only — Linux packaging
# builds of this crate never execute the winresource block). # 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 // `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` it, so a layout/code drift is a compile error (the proto has `const` size asserts).
use frame::{ use frame::{
SharedHeader, DRV_STATUS_BIND_FAIL, DRV_STATUS_NO_DEVICE1, DRV_STATUS_OPENED, unpack_opened_detail, SharedHeader, DRV_STATUS_BIND_FAIL, DRV_STATUS_NONE,
DRV_STATUS_TEX_FAIL, MAGIC, RING_LEN, VERSION, 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 /// `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 // 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 // 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. // 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, LowPart: (target.adapter_luid & 0xffff_ffff) as u32,
HighPart: (target.adapter_luid >> 32) as i32, HighPart: (target.adapter_luid >> 32) as i32,
}); });
@@ -935,15 +935,63 @@ impl IddPushCapturer {
} }
if Instant::now() > deadline { if Instant::now() > deadline {
bail!( bail!(
"IDD-push: driver_status={st} but no frame published within 4s (despite compose \ "IDD-push: no frame published within 4s (despite compose kicks) — {}; \
kicks) the virtual display is likely in a format/size the ring can't match \ falling back",
(fullscreen game?); falling back" self.no_first_frame_diagnosis(st)
); );
} }
std::thread::sleep(Duration::from_millis(20)); 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] #[inline]
fn latest(&self) -> u64 { fn latest(&self) -> u64 {
// SAFETY: `self.header` is the live, owned shared-header mapping (page-aligned, sized for a // 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. /// Calls DXGI factory/adapter enumeration; returns owned COM objects or an error.
unsafe fn resolve_render_adapter() -> Result<IDXGIAdapter1> { unsafe fn resolve_render_adapter() -> Result<IDXGIAdapter1> {
let factory: IDXGIFactory4 = CreateDXGIFactory1().context("CreateDXGIFactory1")?; 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) { if let Ok(a) = factory.EnumAdapterByLuid::<IDXGIAdapter1>(luid) {
return Ok(a); return Ok(a);
} }
+7 -4
View File
@@ -35,7 +35,13 @@ mod ddc;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
#[path = "linux/drm_sync.rs"] #[path = "linux/drm_sync.rs"]
mod drm_sync; 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 events;
mod gamestream; mod gamestream;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
@@ -67,9 +73,6 @@ mod spike;
mod stats_recorder; mod stats_recorder;
mod stream_marker; mod stream_marker;
mod vdisplay; 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 // 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 // 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. // 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 /// 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 /// 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 > /// 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 /// (This was gated on the removed `PUNKTFUNK_IDD_PUSH` knob — a dispatch disagreement, since capture
/// stopped consulting it when DDA/WGC were removed.) /// stopped consulting it when DDA/WGC were removed.)
fn resolve_render_pin() -> Option<LUID> { fn resolve_render_pin() -> Option<LUID> {
tracing::info!("IDD push: pinning the render GPU (SET_RENDER_ADAPTER)"); 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 /// 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
}
}
}