refactor(host/W6.2): extract the shared frame/format vocabulary into the pf-frame leaf crate
The captured-frame types both capture (producer) and encode (consumer) speak —
PixelFormat, OutputFormat, CursorOverlay, CapturedFrame, FramePayload,
DmabufFrame, drm_fourcc — move into crates/pf-frame, alongside the small pure
helpers that ride the same seam: hdr (HDR static metadata / in-band SEI),
metronome (the metronomic-stall detector), thread_qos (per-thread scheduling
QoS), session_tuning (Windows process tuning), and the Windows DXGI capture
IDENTITY (WinCaptureTarget, D3d11Frame, pack_luid, make_device + the GPU
scheduling-priority hardening it applies) (plan §W6).
This is the crate that breaks the capture<->encode cycle: FramePayload's GPU
variants own their backends from BELOW (Cuda -> pf_zerocopy::DeviceBuffer,
D3d11 -> dxgi::D3d11Frame), so encode can speak the vocabulary without a path to
capture, and vice versa. The Windows DXGI identity moving here lets capture,
encode, and pf-vdisplay share ONE WinCaptureTarget/device factory instead of the
old capture<->encode<->vdisplay reach-in.
The host keeps thin facades: capture.rs re-exports the vocabulary
(crate::capture::{PixelFormat,…} unchanged); capture/windows/dxgi.rs keeps the
win32u GPU-preference hook + HDR/video-engine converters + self-test and
re-exports the identity; native.rs re-exports boost_thread_priority from
pf_frame. crate::hdr/metronome/session_tuning callers rewired to pf_frame::*.
metronome's Metronome::new gained a Default impl (new_without_default fires once
the type is public across the crate boundary).
Verified: Linux clippy -D warnings (pf-frame --all-targets + host
nvenc,vulkan-encode,pyrowave --all-targets) + 9/9 pf-frame tests; Windows clippy
nvenc,amf-qsv --all-targets Finished exit 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Generated
+13
@@ -2808,6 +2808,18 @@ dependencies = [
|
|||||||
"pkg-config",
|
"pkg-config",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pf-frame"
|
||||||
|
version = "0.12.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"libc",
|
||||||
|
"pf-zerocopy",
|
||||||
|
"punktfunk-core",
|
||||||
|
"tracing",
|
||||||
|
"windows 0.62.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pf-gpu"
|
name = "pf-gpu"
|
||||||
version = "0.12.0"
|
version = "0.12.0"
|
||||||
@@ -3161,6 +3173,7 @@ dependencies = [
|
|||||||
"opus",
|
"opus",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"pf-driver-proto",
|
"pf-driver-proto",
|
||||||
|
"pf-frame",
|
||||||
"pf-gpu",
|
"pf-gpu",
|
||||||
"pf-host-config",
|
"pf-host-config",
|
||||||
"pf-paths",
|
"pf-paths",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ members = [
|
|||||||
"crates/pf-host-config",
|
"crates/pf-host-config",
|
||||||
"crates/pf-gpu",
|
"crates/pf-gpu",
|
||||||
"crates/pf-zerocopy",
|
"crates/pf-zerocopy",
|
||||||
|
"crates/pf-frame",
|
||||||
"crates/pyrowave-sys",
|
"crates/pyrowave-sys",
|
||||||
"clients/probe",
|
"clients/probe",
|
||||||
"clients/linux",
|
"clients/linux",
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# The shared media-pipeline vocabulary (plan §W6): the captured-frame types and pixel formats that
|
||||||
|
# both capture (producer) and encode (consumer) speak, plus the small pure helpers that ride the
|
||||||
|
# same seam — HDR static metadata, the metronomic-stall detector, per-thread scheduling QoS, and
|
||||||
|
# (Windows) the DXGI capture identity + D3D11 device creation. A leaf so pf-capture and pf-encode
|
||||||
|
# can depend on the vocabulary WITHOUT depending on each other.
|
||||||
|
[package]
|
||||||
|
name = "pf-frame"
|
||||||
|
version = "0.12.0"
|
||||||
|
edition = "2021"
|
||||||
|
rust-version.workspace = true
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
description = "punktfunk host shared frame/format vocabulary: CapturedFrame, PixelFormat, HDR metadata, thread QoS, and the Windows DXGI capture identity."
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
punktfunk-core = { path = "../punktfunk-core", features = ["quic"] }
|
||||||
|
anyhow = "1"
|
||||||
|
tracing = "0.1"
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
|
# `FramePayload::Cuda` owns a zero-copy `DeviceBuffer`; `libc` for the per-thread `setpriority`.
|
||||||
|
pf-zerocopy = { path = "../pf-zerocopy" }
|
||||||
|
libc = "0.2"
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "windows")'.dependencies]
|
||||||
|
# The DXGI capture identity (`WinCaptureTarget`/`D3d11Frame`/`pack_luid`/`make_device`) + the GPU
|
||||||
|
# scheduling-priority hardening `make_device` applies, and the thread-QoS `SetThreadPriority`.
|
||||||
|
windows = { version = "0.62", features = [
|
||||||
|
"Win32_Foundation",
|
||||||
|
"Win32_Security",
|
||||||
|
"Win32_Graphics_Dxgi",
|
||||||
|
"Win32_Graphics_Dxgi_Common",
|
||||||
|
"Win32_Graphics_Direct3D",
|
||||||
|
"Win32_Graphics_Direct3D11",
|
||||||
|
"Win32_System_LibraryLoader",
|
||||||
|
"Win32_System_Threading",
|
||||||
|
] }
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
//! The Windows DXGI capture identity + shared D3D11 device creation (plan §W6): the capture
|
||||||
|
//! target descriptor ([`WinCaptureTarget`]), the GPU-resident captured texture ([`D3d11Frame`]),
|
||||||
|
//! the adapter-LUID packer ([`pack_luid`]), and [`make_device`] — a fresh D3D11 device/context on
|
||||||
|
//! a chosen adapter, applying the process GPU scheduling-priority hardening. Extracted from the
|
||||||
|
//! host's `capture/windows/dxgi.rs` so the capture IDD-push path, the encode D3D11 backends, and
|
||||||
|
//! pf-vdisplay all share ONE identity type + device factory (no capture↔encode↔vdisplay cycle).
|
||||||
|
//! The win32u GPU-preference hook, the HDR/video-engine converters, and the self-tests stay in the
|
||||||
|
//! capture crate — they are capture mechanics, not shared identity.
|
||||||
|
|
||||||
|
// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program).
|
||||||
|
#![deny(clippy::undocumented_unsafe_blocks)]
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use windows::core::Interface;
|
||||||
|
use windows::Win32::Foundation::{HMODULE, LUID};
|
||||||
|
use windows::Win32::Graphics::Direct3D::{D3D_DRIVER_TYPE_UNKNOWN, D3D_FEATURE_LEVEL_11_0};
|
||||||
|
use windows::Win32::Graphics::Direct3D11::{
|
||||||
|
D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext, ID3D11Texture2D,
|
||||||
|
D3D11_CREATE_DEVICE_BGRA_SUPPORT, D3D11_SDK_VERSION,
|
||||||
|
};
|
||||||
|
use windows::Win32::Graphics::Dxgi::{IDXGIAdapter1, IDXGIDevice, IDXGIDevice1};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct WinCaptureTarget {
|
||||||
|
/// Packed DXGI adapter LUID (`(HighPart << 32) | (LowPart & 0xffff_ffff)`).
|
||||||
|
pub adapter_luid: i64,
|
||||||
|
/// The output's GDI device name, e.g. `\\.\DISPLAY3`. Can CHANGE across a secure-desktop switch.
|
||||||
|
pub gdi_name: String,
|
||||||
|
/// Stable virtual-display (IddCx) target id — re-resolved to the current GDI name on every recovery.
|
||||||
|
pub target_id: u32,
|
||||||
|
/// The pf-vdisplay driver's WUDFHost pid (from the ADD reply) — the process the IDD-push capturer
|
||||||
|
/// duplicates the sealed frame channel's handles INTO (`idd_push::ChannelBroker`). `0` = unknown
|
||||||
|
/// (a pre-v2 pairing can't occur — the version handshake is hard — so this only guards misuse).
|
||||||
|
pub wudf_pid: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A GPU-resident captured texture (future NVENC-D3D11 zero-copy path).
|
||||||
|
pub struct D3d11Frame {
|
||||||
|
pub texture: ID3D11Texture2D,
|
||||||
|
pub device: ID3D11Device,
|
||||||
|
}
|
||||||
|
// SAFETY: `D3d11Frame` owns an `ID3D11Texture2D` + `ID3D11Device`, which are COM interface pointers.
|
||||||
|
// D3D11 devices/resources use thread-safe (interlocked) COM reference counting, and the device is
|
||||||
|
// created free-threaded (`make_device` passes no `D3D11_CREATE_DEVICE_SINGLETHREADED`), so handing
|
||||||
|
// ownership of the frame to another thread — the capture→encode handoff — and releasing it there is
|
||||||
|
// sound. The value is moved, never aliased (no `Sync`), so there is no concurrent use of the
|
||||||
|
// single-threaded immediate context.
|
||||||
|
unsafe impl Send for D3d11Frame {}
|
||||||
|
|
||||||
|
pub fn pack_luid(luid: LUID) -> i64 {
|
||||||
|
((luid.HighPart as i64) << 32) | (luid.LowPart as i64 & 0xffff_ffff)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a fresh D3D11 device + context on a specific adapter (driver_type UNKNOWN with an explicit
|
||||||
|
/// adapter). Used at open and on every ACCESS_LOST: a device created on one desktop cannot sustain a
|
||||||
|
/// duplication on a *different* desktop (perpetual ACCESS_LOST), so the secure-desktop switch needs a
|
||||||
|
/// device made while the thread is attached to that desktop.
|
||||||
|
pub unsafe fn make_device(adapter: &IDXGIAdapter1) -> Result<(ID3D11Device, ID3D11DeviceContext)> {
|
||||||
|
let mut device: Option<ID3D11Device> = None;
|
||||||
|
let mut context: Option<ID3D11DeviceContext> = None;
|
||||||
|
D3D11CreateDevice(
|
||||||
|
adapter,
|
||||||
|
D3D_DRIVER_TYPE_UNKNOWN,
|
||||||
|
HMODULE::default(),
|
||||||
|
D3D11_CREATE_DEVICE_BGRA_SUPPORT,
|
||||||
|
Some(&[D3D_FEATURE_LEVEL_11_0]),
|
||||||
|
D3D11_SDK_VERSION,
|
||||||
|
Some(&mut device),
|
||||||
|
None,
|
||||||
|
Some(&mut context),
|
||||||
|
)
|
||||||
|
.context("D3D11CreateDevice")?;
|
||||||
|
let device = device.context("null D3D11 device")?;
|
||||||
|
let context = context.context("null D3D11 context")?;
|
||||||
|
|
||||||
|
// GPU scheduling hardening — the same approach Sunshine/Apollo use, reimplemented here via the
|
||||||
|
// documented D3DKMT/DXGI APIs (no GPL source copied). Our capture+encode
|
||||||
|
// shares the GPU with the streamed game; when the game saturates the GPU our process is starved of
|
||||||
|
// GPU time slices, so NVENC sits near-idle yet `lock_bitstream` waits ~20 ms for our context to be
|
||||||
|
// scheduled — capping the stream (~47 fps measured at 5K@240) and stuttering. Per-frame copy/convert
|
||||||
|
// is NOT the cause (zero-copy + thread-priority alone didn't move it); the PROCESS-level GPU
|
||||||
|
// scheduling priority class is the decisive cross-process lever. Secondary: the absolute per-device
|
||||||
|
// GPU thread priority and a 1-frame latency cap.
|
||||||
|
elevate_process_gpu_priority();
|
||||||
|
if let Ok(dxgi_dev) = device.cast::<IDXGIDevice>() {
|
||||||
|
// The absolute max GPU thread priority (0x4000001E; the same value Sunshine/Apollo use); fall back to relative +7.
|
||||||
|
if dxgi_dev.SetGPUThreadPriority(0x4000_001E).is_err()
|
||||||
|
&& dxgi_dev.SetGPUThreadPriority(7).is_err()
|
||||||
|
{
|
||||||
|
tracing::warn!("SetGPUThreadPriority failed (run as admin/SYSTEM for GPU priority)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Ok(dxgi1) = device.cast::<IDXGIDevice1>() {
|
||||||
|
let _ = dxgi1.SetMaximumFrameLatency(1);
|
||||||
|
}
|
||||||
|
Ok((device, context))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve the configured GPU scheduling-priority class from `PUNKTFUNK_GPU_PRIORITY_CLASS`
|
||||||
|
/// (`off|normal|high|realtime`, default high). `None` = leave it at the OS default (the `off` opt-out).
|
||||||
|
/// D3DKMT_SCHEDULINGPRIORITYCLASS: IDLE 0, BELOW_NORMAL 1, NORMAL 2, ABOVE_NORMAL 3, HIGH 4, REALTIME 5.
|
||||||
|
fn configured_gpu_priority_class() -> Option<i32> {
|
||||||
|
match std::env::var("PUNKTFUNK_GPU_PRIORITY_CLASS")
|
||||||
|
.ok()
|
||||||
|
.as_deref()
|
||||||
|
{
|
||||||
|
Some("off") => None,
|
||||||
|
Some("normal") => Some(2),
|
||||||
|
Some("realtime") => Some(5),
|
||||||
|
_ => Some(4), // HIGH — safe on NVIDIA+HAGS (realtime can freeze NVENC)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enable SE_INC_BASE_PRIORITY on the CURRENT process token (best-effort) — the kernel gates the
|
||||||
|
/// HIGH/REALTIME GPU scheduling-priority bump on it. Held by SYSTEM/Administrators; a UAC-FILTERED
|
||||||
|
/// token does NOT have it, which is why `elevate_process_gpu_priority` may silently no-op in a
|
||||||
|
/// restricted service context.
|
||||||
|
unsafe fn enable_inc_base_priority() {
|
||||||
|
use windows::core::PCWSTR;
|
||||||
|
use windows::Win32::Foundation::{CloseHandle, HANDLE, LUID};
|
||||||
|
use windows::Win32::Security::{
|
||||||
|
AdjustTokenPrivileges, LookupPrivilegeValueW, LUID_AND_ATTRIBUTES,
|
||||||
|
SE_INC_BASE_PRIORITY_NAME, SE_PRIVILEGE_ENABLED, TOKEN_ADJUST_PRIVILEGES, TOKEN_PRIVILEGES,
|
||||||
|
TOKEN_QUERY,
|
||||||
|
};
|
||||||
|
use windows::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
|
||||||
|
let mut token = HANDLE::default();
|
||||||
|
if OpenProcessToken(
|
||||||
|
GetCurrentProcess(),
|
||||||
|
TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY,
|
||||||
|
&mut token,
|
||||||
|
)
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
let mut luid = LUID::default();
|
||||||
|
if LookupPrivilegeValueW(PCWSTR::null(), SE_INC_BASE_PRIORITY_NAME, &mut luid).is_ok() {
|
||||||
|
let tp = TOKEN_PRIVILEGES {
|
||||||
|
PrivilegeCount: 1,
|
||||||
|
Privileges: [LUID_AND_ATTRIBUTES {
|
||||||
|
Luid: luid,
|
||||||
|
Attributes: SE_PRIVILEGE_ENABLED,
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
if AdjustTokenPrivileges(
|
||||||
|
token,
|
||||||
|
false,
|
||||||
|
Some(&tp as *const TOKEN_PRIVILEGES),
|
||||||
|
0,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
tracing::warn!("could not enable SE_INC_BASE_PRIORITY for GPU priority");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _ = CloseHandle(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Call `gdi32!D3DKMTSetProcessSchedulingPriorityClass(process, prio)` (no stable windows-rs binding —
|
||||||
|
/// loaded by name). Returns the NTSTATUS (0 = success) or `None` if the export can't be resolved. The
|
||||||
|
/// CALLING process must hold SE_INC_BASE_PRIORITY ([`enable_inc_base_priority`]) for HIGH/REALTIME; the
|
||||||
|
/// kernel checks the caller's privilege whether the target is self or a child we created.
|
||||||
|
unsafe fn d3dkmt_set_scheduling_priority_class(
|
||||||
|
process: windows::Win32::Foundation::HANDLE,
|
||||||
|
prio: i32,
|
||||||
|
) -> Option<i32> {
|
||||||
|
use windows::core::s;
|
||||||
|
use windows::Win32::Foundation::HANDLE;
|
||||||
|
use windows::Win32::System::LibraryLoader::{GetProcAddress, LoadLibraryA};
|
||||||
|
let gdi32 = LoadLibraryA(s!("gdi32.dll")).ok()?;
|
||||||
|
let p = GetProcAddress(gdi32, s!("D3DKMTSetProcessSchedulingPriorityClass"))?;
|
||||||
|
type SetPrio = unsafe extern "system" fn(HANDLE, i32) -> i32;
|
||||||
|
let f: SetPrio = std::mem::transmute(p);
|
||||||
|
Some(f(process, prio))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// GPU scheduling-priority hardening — the same approach as Sunshine/Apollo, independently
|
||||||
|
/// implemented via the documented D3DKMT APIs (no GPL source copied). On a
|
||||||
|
/// GPU-saturated game our capture+encode process is starved of GPU time slices — NVENC sits ~idle but
|
||||||
|
/// `lock_bitstream` waits ~20 ms for our context to be scheduled. Elevating the PROCESS GPU scheduling
|
||||||
|
/// priority class (the strong cross-process lever — far more effective than `SetGPUThreadPriority`
|
||||||
|
/// alone, which we measured as no help) lets our brief encode preempt the game. Uses HIGH, NOT
|
||||||
|
/// realtime: realtime on NVIDIA + HAGS can freeze/crash NVENC (Apollo downgrades it for exactly this).
|
||||||
|
/// Runs once per process; best-effort. `PUNKTFUNK_GPU_PRIORITY_CLASS = off|normal|high|realtime`
|
||||||
|
/// (default high). Best-effort: silently no-ops under a UAC-filtered token (the process will not
|
||||||
|
/// hold SE_INC_BASE_PRIORITY, so the D3DKMT call is a no-op).
|
||||||
|
fn elevate_process_gpu_priority() {
|
||||||
|
use std::sync::Once;
|
||||||
|
static ONCE: Once = Once::new();
|
||||||
|
// SAFETY: the closure calls two of this module's `unsafe fn`s — `enable_inc_base_priority`
|
||||||
|
// (adjusts the current-process token; it has no caller precondition and builds all its FFI args
|
||||||
|
// locally) and `d3dkmt_set_scheduling_priority_class` (loads gdi32 by name and calls the export).
|
||||||
|
// The latter requires `process` to be a valid process handle; `GetCurrentProcess()` returns the
|
||||||
|
// current-process pseudo-handle, which is always valid and needs no close. Runs once via
|
||||||
|
// `Once::call_once`; no raw pointers are dereferenced here.
|
||||||
|
ONCE.call_once(|| unsafe {
|
||||||
|
use windows::Win32::System::Threading::GetCurrentProcess;
|
||||||
|
let Some(prio) = configured_gpu_priority_class() else {
|
||||||
|
tracing::info!("GPU process scheduling priority class left at default (off)");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
enable_inc_base_priority();
|
||||||
|
match d3dkmt_set_scheduling_priority_class(GetCurrentProcess(), prio) {
|
||||||
|
Some(0) => tracing::info!(
|
||||||
|
priority_class = prio,
|
||||||
|
"GPU process scheduling priority class set (2=normal 4=high 5=realtime)"
|
||||||
|
),
|
||||||
|
Some(st) => tracing::warn!(
|
||||||
|
status = format!("0x{st:08X}"),
|
||||||
|
"D3DKMTSetProcessSchedulingPriorityClass failed (run as admin/SYSTEM for GPU priority)"
|
||||||
|
),
|
||||||
|
None => tracing::warn!("D3DKMTSetProcessSchedulingPriorityClass export not found"),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
//! The shared media-pipeline vocabulary (plan §W6): the frame + pixel-format types that capture
|
||||||
|
//! (producer) and encode (consumer) both speak, extracted into a leaf crate so `pf-capture` and
|
||||||
|
//! `pf-encode` depend on the vocabulary WITHOUT depending on each other. The GPU payloads pull
|
||||||
|
//! their heavy backends in from below: `FramePayload::Cuda` owns a [`pf_zerocopy::DeviceBuffer`],
|
||||||
|
//! `FramePayload::D3d11` a [`dxgi::D3d11Frame`].
|
||||||
|
//!
|
||||||
|
//! Alongside the vocabulary live the small pure helpers that ride the same capture-encode seam:
|
||||||
|
//! [`hdr`] (HDR static metadata / in-band SEI), [`metronome`] (the metronomic-stall detector),
|
||||||
|
//! [`thread_qos`] (per-thread scheduling QoS), [`session_tuning`] (Windows process session
|
||||||
|
//! tuning), and — on Windows — [`dxgi`] (the capture identity + D3D11 device creation).
|
||||||
|
|
||||||
|
// Unsafe-proof program: every `unsafe {}` / `unsafe impl` must carry a `// SAFETY:` proof.
|
||||||
|
#![deny(clippy::undocumented_unsafe_blocks)]
|
||||||
|
|
||||||
|
pub mod hdr;
|
||||||
|
pub mod metronome;
|
||||||
|
pub mod session_tuning;
|
||||||
|
pub mod thread_qos;
|
||||||
|
|
||||||
|
// The Windows DXGI capture identity + shared D3D11 device creation (plan §W6). Consumed by the
|
||||||
|
// capture IDD-push path, the encode D3D11 backends, and pf-vdisplay's `WinCaptureTarget`.
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub mod dxgi;
|
||||||
|
|
||||||
|
/// Packed pixel layout of a [`CapturedFrame`]. The ScreenCast portal negotiates the
|
||||||
|
/// format; on wlroots it is commonly packed `RGB` (3 bytes/pixel). The encoder maps these
|
||||||
|
/// to an NVENC-accepted input format (`rgb0`/`bgr0`/`rgba`/`bgra`), expanding 3→4 bytes
|
||||||
|
/// where needed — no host-side colour conversion.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum PixelFormat {
|
||||||
|
/// `[B,G,R,x]`, 4 bpp.
|
||||||
|
Bgrx,
|
||||||
|
/// `[R,G,B,x]`, 4 bpp.
|
||||||
|
Rgbx,
|
||||||
|
/// `[B,G,R,A]`, 4 bpp.
|
||||||
|
Bgra,
|
||||||
|
/// `[R,G,B,A]`, 4 bpp.
|
||||||
|
Rgba,
|
||||||
|
/// `[R,G,B]`, 3 bpp.
|
||||||
|
Rgb,
|
||||||
|
/// `[B,G,R]`, 3 bpp.
|
||||||
|
Bgr,
|
||||||
|
/// 10-bit RGB packed as `R10G10B10A2` (DXGI `R10G10B10A2_UNORM`), 4 bpp. The HDR capture path
|
||||||
|
/// produces this: scRGB FP16 desktop pixels are converted to BT.2020 PQ and written here, then
|
||||||
|
/// handed to NVENC as `ABGR10` for an HEVC Main10 / HDR10 encode.
|
||||||
|
Rgb10a2,
|
||||||
|
/// `NV12` (DXGI `NV12`): 8-bit BT.709 limited-range YUV 4:2:0. Produced by the D3D11 **video
|
||||||
|
/// processor** (video engine, not the 3D engine) so the per-frame colour conversion doesn't fight a
|
||||||
|
/// GPU-saturating game; handed to NVENC as `NV12` (it encodes YUV natively — no internal RGB→YUV).
|
||||||
|
Nv12,
|
||||||
|
/// `P010` (DXGI `P010`): 10-bit BT.2020 PQ limited-range YUV 4:2:0. HDR analogue of [`Nv12`]:
|
||||||
|
/// video-processor output for HEVC Main10 / HDR10, handed to NVENC as `YUV420_10BIT`.
|
||||||
|
P010,
|
||||||
|
/// Planar 8-bit YUV **4:4:4** (BT.709; range per `PUNKTFUNK_444_FULLRANGE`). Produced by the
|
||||||
|
/// Linux zero-copy worker's GPU convert for a 4:4:4 session ([`FramePayload::Cuda`] with
|
||||||
|
/// `DeviceBuffer::yuv444` — three full-res planes stacked in one allocation); NVENC encodes
|
||||||
|
/// it natively under the Range-Extensions profile. Never a CPU payload.
|
||||||
|
Yuv444,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PixelFormat {
|
||||||
|
pub fn bytes_per_pixel(self) -> usize {
|
||||||
|
match self {
|
||||||
|
PixelFormat::Rgb | PixelFormat::Bgr => 3,
|
||||||
|
// Three full-res 1-byte planes (GPU-resident only; no CPU payload carries this).
|
||||||
|
PixelFormat::Yuv444 => 3,
|
||||||
|
_ => 4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// DRM FourCC for a packed 32-bit format name (little-endian, e.g. `b"XR24"`).
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
const fn drm_fourcc_code(c: &[u8; 4]) -> u32 {
|
||||||
|
(c[0] as u32) | ((c[1] as u32) << 8) | ((c[2] as u32) << 16) | ((c[3] as u32) << 24)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Map a SPA/our [`PixelFormat`] to the DRM FourCC EGL expects for import. SPA byte order `BGRx`
|
||||||
|
/// ⇒ DRM `XRGB8888` (memory B,G,R,X), etc. Lives with the frame vocabulary (not in
|
||||||
|
/// `pf-zerocopy`) because it consumes [`PixelFormat`], which sits above that crate.
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pub fn drm_fourcc(format: PixelFormat) -> Option<u32> {
|
||||||
|
use PixelFormat::*;
|
||||||
|
Some(match format {
|
||||||
|
Bgrx => drm_fourcc_code(b"XR24"), // DRM_FORMAT_XRGB8888
|
||||||
|
Bgra => drm_fourcc_code(b"AR24"), // DRM_FORMAT_ARGB8888
|
||||||
|
Rgbx => drm_fourcc_code(b"XB24"), // DRM_FORMAT_XBGR8888
|
||||||
|
Rgba => drm_fourcc_code(b"AB24"), // DRM_FORMAT_ABGR8888
|
||||||
|
// 24-bit packed RGB/BGR have no straightforward dmabuf import here; use the CPU path.
|
||||||
|
// Rgb10a2/Nv12/P010 are the Windows HDR / video-processor formats — never produced on
|
||||||
|
// Linux; Yuv444 is OUR convert's OUTPUT, never a capture source format.
|
||||||
|
Rgb | Bgr | Rgb10a2 | Nv12 | P010 | Yuv444 => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What a Windows capturer should produce, resolved **once** per session and passed **into**
|
||||||
|
/// `capture_virtual_output` (Goal-1 stage 5, plan §2.3/§5). Passing the format in is what lets a
|
||||||
|
/// capturer stop re-deriving the encode backend itself — it kills the
|
||||||
|
/// `capture/dxgi.rs → encode::windows_resolved_backend()` back-reference (the highest-severity coupling:
|
||||||
|
/// capture and encode could otherwise disagree on whether frames are GPU-resident). Neutral type; the
|
||||||
|
/// Linux portal capturer ignores it (it negotiates its own format with PipeWire).
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct OutputFormat {
|
||||||
|
/// Produce GPU-resident D3D11 frames (zero-copy for a GPU encoder — NVENC/AMF/QSV) rather than CPU
|
||||||
|
/// staging. `false` **only** for the GPU-less software encoder.
|
||||||
|
pub gpu: bool,
|
||||||
|
/// HDR: the capturer converts to 10-bit (IDD-push FP16 → `P010`, or `Rgb10a2` for a 4:4:4 source).
|
||||||
|
/// `false` = 8-bit SDR.
|
||||||
|
pub hdr: bool,
|
||||||
|
/// Full-chroma 4:4:4 session: the capturer must keep full chroma. On Windows the IDD-push
|
||||||
|
/// capturer hands the **BGRA** slot through (skipping the subsampling BGRA→NV12
|
||||||
|
/// VideoConverter) so NVENC ingests full-chroma RGB and CSCs to 4:4:4 itself — measured
|
||||||
|
/// on-glass (RTX 5070 Ti): ARGB + `chromaFormatIDC=3` yields TRUE 4:4:4 and the conversion
|
||||||
|
/// follows the configured VUI matrix (BT.709 limited since the VUI is always written). On
|
||||||
|
/// Linux it forces the CPU RGB path the encoder swscales to `YUV444P`. `false` on every
|
||||||
|
/// 4:2:0 session.
|
||||||
|
pub chroma_444: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OutputFormat {
|
||||||
|
/// Resolve the output format for an entry point that doesn't build a full [`SessionPlan`]
|
||||||
|
/// (`crate::session_plan`) — the GameStream + spike paths. `gpu` is the encoder's GPU-residency,
|
||||||
|
/// resolved by the caller via `pf_encode::resolved_backend_is_gpu` and passed **in** (capture
|
||||||
|
/// never re-derives the backend — the one-way capture→encode edge, plan §2.4 / §W4); `hdr` as given.
|
||||||
|
/// The native punktfunk/1 path uses `SessionPlan::output_format()` instead (it already resolved the
|
||||||
|
/// encoder), so neither path makes a capturer re-derive it.
|
||||||
|
pub fn resolve(hdr: bool, gpu: bool) -> Self {
|
||||||
|
OutputFormat {
|
||||||
|
gpu,
|
||||||
|
hdr,
|
||||||
|
// The GameStream + spike paths are always 4:2:0 (4:4:4 is punktfunk/1-native only).
|
||||||
|
chroma_444: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A mouse-cursor overlay to composite onto a frame at encode time (cursor-as-metadata). Rides on
|
||||||
|
/// [`CapturedFrame::cursor`] for the GPU zero-copy payloads (Cuda/Dmabuf), whose pixels never touch
|
||||||
|
/// the CPU — the encoder blends this small bitmap into its owned surface (Vulkan CSC image / CUDA
|
||||||
|
/// devbuf / VA surface). The CPU de-pad path composites the cursor inline instead, so it leaves
|
||||||
|
/// this `None`. `rgba` is `Arc` so attaching the (unchanged) bitmap to every frame is a refcount
|
||||||
|
/// bump, not a copy; `serial` bumps only when the bitmap image changes, so the encoder re-uploads
|
||||||
|
/// its small GPU texture on change and just moves a push-constant otherwise.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct CursorOverlay {
|
||||||
|
/// Top-left in frame pixels where the bitmap is drawn (already = reported position − hotspot).
|
||||||
|
pub x: i32,
|
||||||
|
pub y: i32,
|
||||||
|
pub w: u32,
|
||||||
|
pub h: u32,
|
||||||
|
/// Straight-alpha RGBA pixels, `w*h*4` (bytes R,G,B,A).
|
||||||
|
pub rgba: std::sync::Arc<Vec<u8>>,
|
||||||
|
/// Bumps whenever `rgba`/`w`/`h` change; stable across position-only moves.
|
||||||
|
pub serial: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A captured frame. [`format`](Self::format)/dimensions describe the pixels regardless of
|
||||||
|
/// where they live — [`payload`](Self::payload) is either a CPU buffer (the spike/fallback path)
|
||||||
|
/// or a GPU buffer already on the device (the zero-copy path, plan §9).
|
||||||
|
pub struct CapturedFrame {
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
pub pts_ns: u64,
|
||||||
|
/// Pixel layout of the payload.
|
||||||
|
pub format: PixelFormat,
|
||||||
|
pub payload: FramePayload,
|
||||||
|
/// Cursor overlay to blend at encode time (GPU zero-copy payloads only); `None` when there's no
|
||||||
|
/// visible cursor or the pixels were already composited on the CPU de-pad path. See
|
||||||
|
/// [`CursorOverlay`].
|
||||||
|
pub cursor: Option<CursorOverlay>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A captured frame still living in a single-plane packed-RGB dmabuf (the VAAPI zero-copy path).
|
||||||
|
/// Owns a *dup* of the PipeWire buffer's fd, so the frame can travel to the encode thread and be
|
||||||
|
/// imported into a VA surface there without the compositor's buffer being closed underneath it.
|
||||||
|
/// (Content stability across the brief import window relies on the compositor's buffer pool depth,
|
||||||
|
/// same as any zero-copy capture — the VAAPI importer copies into its own NV12 surface promptly.)
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pub struct DmabufFrame {
|
||||||
|
pub fd: std::os::fd::OwnedFd,
|
||||||
|
/// DRM FourCC of the packed-RGB plane (e.g. `XR24` for BGRx).
|
||||||
|
pub fourcc: u32,
|
||||||
|
/// DRM format modifier the compositor allocated (0 = LINEAR).
|
||||||
|
pub modifier: u64,
|
||||||
|
pub offset: u32,
|
||||||
|
pub stride: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Where a captured frame's pixels live.
|
||||||
|
pub enum FramePayload {
|
||||||
|
/// Tightly-packed CPU pixels in `format`, `width*height*bytes_per_pixel` (no row padding).
|
||||||
|
Cpu(Vec<u8>),
|
||||||
|
/// A pitched GPU buffer (BGRA-order, on the shared CUDA context) — the NVIDIA zero-copy path.
|
||||||
|
/// The dmabuf has already been imported + copied into this owned device buffer.
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
Cuda(pf_zerocopy::DeviceBuffer),
|
||||||
|
/// A raw packed-RGB dmabuf — the AMD/Intel (VAAPI) zero-copy path. The encoder imports it into
|
||||||
|
/// a VA surface and does RGB→NV12 on the GPU video engine (no host CSC, no upload).
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
Dmabuf(DmabufFrame),
|
||||||
|
/// A GPU-resident D3D11 texture (Windows zero-copy path for NVENC). Owns the copied frame.
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
D3d11(dxgi::D3d11Frame),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CapturedFrame {
|
||||||
|
/// True if the frame's pixels are a GPU/CUDA buffer (the NVIDIA zero-copy path).
|
||||||
|
pub fn is_cuda(&self) -> bool {
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
matches!(self.payload, FramePayload::Cuda(_))
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
{
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// True if the frame is a raw dmabuf (the VAAPI zero-copy path).
|
||||||
|
pub fn is_dmabuf(&self) -> bool {
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
matches!(self.payload, FramePayload::Dmabuf(_))
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
{
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,8 @@ use std::time::{Duration, Instant};
|
|||||||
/// the gaps between the last [`Self::STREAK`] events are all within ±[`Self::TOLERANCE`] of their
|
/// the gaps between the last [`Self::STREAK`] events are all within ±[`Self::TOLERANCE`] of their
|
||||||
/// mean, [`Self::note`] returns the mean period for the caller to warn with, then stays quiet for
|
/// mean, [`Self::note`] returns the mean period for the caller to warn with, then stays quiet for
|
||||||
/// [`Self::REWARN`] while the cycle persists.
|
/// [`Self::REWARN`] while the cycle persists.
|
||||||
pub(crate) struct Metronome {
|
#[derive(Default)]
|
||||||
|
pub struct Metronome {
|
||||||
events: VecDeque<Instant>,
|
events: VecDeque<Instant>,
|
||||||
last_warn: Option<Instant>,
|
last_warn: Option<Instant>,
|
||||||
}
|
}
|
||||||
@@ -32,7 +33,7 @@ impl Metronome {
|
|||||||
/// Once warned, re-warn at most this often while the cycle persists.
|
/// Once warned, re-warn at most this often while the cycle persists.
|
||||||
const REWARN: Duration = Duration::from_secs(30);
|
const REWARN: Duration = Duration::from_secs(30);
|
||||||
|
|
||||||
pub(crate) fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
events: VecDeque::new(),
|
events: VecDeque::new(),
|
||||||
last_warn: None,
|
last_warn: None,
|
||||||
@@ -41,7 +42,7 @@ impl Metronome {
|
|||||||
|
|
||||||
/// Record a disturbance at `now`; `Some(mean period)` exactly when the metronomic-cycle
|
/// Record a disturbance at `now`; `Some(mean period)` exactly when the metronomic-cycle
|
||||||
/// warning should fire.
|
/// warning should fire.
|
||||||
pub(crate) fn note(&mut self, now: Instant) -> Option<Duration> {
|
pub fn note(&mut self, now: Instant) -> Option<Duration> {
|
||||||
if self
|
if self
|
||||||
.events
|
.events
|
||||||
.back()
|
.back()
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
//! Per-thread OS scheduling QoS for the native data plane (plan §W1 — carved out of the [`super`]
|
//! Per-thread OS scheduling QoS for the data plane (plan §W1/§W6 — now in the shared `pf-frame`
|
||||||
//! module). The capture/encode and send threads raise their own priority so a CPU-saturating game
|
//! leaf). The capture/encode and send threads raise their own priority so a CPU-saturating game
|
||||||
//! can't deschedule them; the GameStream path and the direct-NVENC send thread reach this the same
|
//! can't deschedule them; the native, GameStream, and direct-NVENC send threads all reach this the
|
||||||
//! way (`crate::native::boost_thread_priority`).
|
//! same way (`pf_frame::thread_qos::boost_thread_priority`).
|
||||||
|
|
||||||
// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program).
|
// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program).
|
||||||
#![deny(clippy::undocumented_unsafe_blocks)]
|
#![deny(clippy::undocumented_unsafe_blocks)]
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
/// uncapped GPU-saturating title (e.g. CS2 direct on a virtual output, not capped by gamescope) is
|
/// uncapped GPU-saturating title (e.g. CS2 direct on a virtual output, not capped by gamescope) is
|
||||||
/// also a CPU hog and can deschedule our submit threads. `critical` → highest non-realtime class
|
/// also a CPU hog and can deschedule our submit threads. `critical` → highest non-realtime class
|
||||||
/// (the capture+encode loop); otherwise above-normal (the send/relay thread).
|
/// (the capture+encode loop); otherwise above-normal (the send/relay thread).
|
||||||
pub(crate) fn boost_thread_priority(critical: bool) {
|
pub fn boost_thread_priority(critical: bool) {
|
||||||
// Windows host-process/thread session tuning (timer 1ms, DWM MMCSS, HIGH class once; MMCSS +
|
// Windows host-process/thread session tuning (timer 1ms, DWM MMCSS, HIGH class once; MMCSS +
|
||||||
// keep-display-awake per thread). No-op off Windows. Both stream threads call us, so this covers
|
// keep-display-awake per thread). No-op off Windows. Both stream threads call us, so this covers
|
||||||
// capture/encode (critical) and send (non-critical).
|
// capture/encode (critical) and send (non-critical).
|
||||||
@@ -19,6 +19,9 @@ pf-gpu = { path = "../pf-gpu" }
|
|||||||
# Linux GPU zero-copy plumbing (CUDA/EGL/Vulkan dmabuf import + the isolated worker), extracted
|
# Linux GPU zero-copy plumbing (CUDA/EGL/Vulkan dmabuf import + the isolated worker), extracted
|
||||||
# to a leaf crate (plan §W6). Compiles empty on non-Linux, so it lives in the main deps.
|
# to a leaf crate (plan §W6). Compiles empty on non-Linux, so it lives in the main deps.
|
||||||
pf-zerocopy = { path = "../pf-zerocopy" }
|
pf-zerocopy = { path = "../pf-zerocopy" }
|
||||||
|
# Shared frame/format vocabulary (CapturedFrame/PixelFormat/…), HDR metadata, thread QoS, and the
|
||||||
|
# Windows DXGI capture identity — the leaf both capture and encode speak (plan §W6).
|
||||||
|
pf-frame = { path = "../pf-frame" }
|
||||||
# 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"
|
||||||
|
|||||||
@@ -8,212 +8,13 @@
|
|||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
/// Packed pixel layout of a [`CapturedFrame`]. The ScreenCast portal negotiates the
|
// The shared frame vocabulary lives in the `pf-frame` leaf crate (plan §W6); re-export it here so
|
||||||
/// format; on wlroots it is commonly packed `RGB` (3 bytes/pixel). The encoder maps these
|
// every existing `crate::capture::{PixelFormat, CapturedFrame, …}` path stays valid.
|
||||||
/// to an NVENC-accepted input format (`rgb0`/`bgr0`/`rgba`/`bgra`), expanding 3→4 bytes
|
pub use pf_frame::{CapturedFrame, FramePayload, OutputFormat, PixelFormat};
|
||||||
/// where needed — no host-side colour conversion.
|
// `CursorOverlay` (cursor-as-metadata) and the dmabuf vocabulary are named only by the Linux
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
// capture/encode paths; gate the re-exports so the Windows build doesn't flag them unused.
|
||||||
pub enum PixelFormat {
|
|
||||||
/// `[B,G,R,x]`, 4 bpp.
|
|
||||||
Bgrx,
|
|
||||||
/// `[R,G,B,x]`, 4 bpp.
|
|
||||||
Rgbx,
|
|
||||||
/// `[B,G,R,A]`, 4 bpp.
|
|
||||||
Bgra,
|
|
||||||
/// `[R,G,B,A]`, 4 bpp.
|
|
||||||
Rgba,
|
|
||||||
/// `[R,G,B]`, 3 bpp.
|
|
||||||
Rgb,
|
|
||||||
/// `[B,G,R]`, 3 bpp.
|
|
||||||
Bgr,
|
|
||||||
/// 10-bit RGB packed as `R10G10B10A2` (DXGI `R10G10B10A2_UNORM`), 4 bpp. The HDR capture path
|
|
||||||
/// produces this: scRGB FP16 desktop pixels are converted to BT.2020 PQ and written here, then
|
|
||||||
/// handed to NVENC as `ABGR10` for an HEVC Main10 / HDR10 encode.
|
|
||||||
Rgb10a2,
|
|
||||||
/// `NV12` (DXGI `NV12`): 8-bit BT.709 limited-range YUV 4:2:0. Produced by the D3D11 **video
|
|
||||||
/// processor** (video engine, not the 3D engine) so the per-frame colour conversion doesn't fight a
|
|
||||||
/// GPU-saturating game; handed to NVENC as `NV12` (it encodes YUV natively — no internal RGB→YUV).
|
|
||||||
Nv12,
|
|
||||||
/// `P010` (DXGI `P010`): 10-bit BT.2020 PQ limited-range YUV 4:2:0. HDR analogue of [`Nv12`]:
|
|
||||||
/// video-processor output for HEVC Main10 / HDR10, handed to NVENC as `YUV420_10BIT`.
|
|
||||||
P010,
|
|
||||||
/// Planar 8-bit YUV **4:4:4** (BT.709; range per `PUNKTFUNK_444_FULLRANGE`). Produced by the
|
|
||||||
/// Linux zero-copy worker's GPU convert for a 4:4:4 session ([`FramePayload::Cuda`] with
|
|
||||||
/// `DeviceBuffer::yuv444` — three full-res planes stacked in one allocation); NVENC encodes
|
|
||||||
/// it natively under the Range-Extensions profile. Never a CPU payload.
|
|
||||||
Yuv444,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PixelFormat {
|
|
||||||
pub fn bytes_per_pixel(self) -> usize {
|
|
||||||
match self {
|
|
||||||
PixelFormat::Rgb | PixelFormat::Bgr => 3,
|
|
||||||
// Three full-res 1-byte planes (GPU-resident only; no CPU payload carries this).
|
|
||||||
PixelFormat::Yuv444 => 3,
|
|
||||||
_ => 4,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// DRM FourCC for a packed 32-bit format name (little-endian, e.g. `b"XR24"`).
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
const fn drm_fourcc_code(c: &[u8; 4]) -> u32 {
|
pub use pf_frame::{drm_fourcc, CursorOverlay, DmabufFrame};
|
||||||
(c[0] as u32) | ((c[1] as u32) << 8) | ((c[2] as u32) << 16) | ((c[3] as u32) << 24)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Map a SPA/our [`PixelFormat`] to the DRM FourCC EGL expects for import. SPA byte order `BGRx`
|
|
||||||
/// ⇒ DRM `XRGB8888` (memory B,G,R,X), etc. Lives with the frame vocabulary (not in
|
|
||||||
/// `pf-zerocopy`) because it consumes [`PixelFormat`], which sits above that crate.
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
pub fn drm_fourcc(format: PixelFormat) -> Option<u32> {
|
|
||||||
use PixelFormat::*;
|
|
||||||
Some(match format {
|
|
||||||
Bgrx => drm_fourcc_code(b"XR24"), // DRM_FORMAT_XRGB8888
|
|
||||||
Bgra => drm_fourcc_code(b"AR24"), // DRM_FORMAT_ARGB8888
|
|
||||||
Rgbx => drm_fourcc_code(b"XB24"), // DRM_FORMAT_XBGR8888
|
|
||||||
Rgba => drm_fourcc_code(b"AB24"), // DRM_FORMAT_ABGR8888
|
|
||||||
// 24-bit packed RGB/BGR have no straightforward dmabuf import here; use the CPU path.
|
|
||||||
// Rgb10a2/Nv12/P010 are the Windows HDR / video-processor formats — never produced on
|
|
||||||
// Linux; Yuv444 is OUR convert's OUTPUT, never a capture source format.
|
|
||||||
Rgb | Bgr | Rgb10a2 | Nv12 | P010 | Yuv444 => return None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// What a Windows capturer should produce, resolved **once** per session and passed **into**
|
|
||||||
/// [`capture_virtual_output`] (Goal-1 stage 5, plan §2.3/§5). Passing the format in is what lets a
|
|
||||||
/// capturer stop re-deriving the encode backend itself — it kills the
|
|
||||||
/// `capture/dxgi.rs → encode::windows_resolved_backend()` back-reference (the highest-severity coupling:
|
|
||||||
/// capture and encode could otherwise disagree on whether frames are GPU-resident). Neutral type; the
|
|
||||||
/// Linux portal capturer ignores it (it negotiates its own format with PipeWire).
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
|
||||||
pub struct OutputFormat {
|
|
||||||
/// Produce GPU-resident D3D11 frames (zero-copy for a GPU encoder — NVENC/AMF/QSV) rather than CPU
|
|
||||||
/// staging. `false` **only** for the GPU-less software encoder.
|
|
||||||
pub gpu: bool,
|
|
||||||
/// HDR: the capturer converts to 10-bit (IDD-push FP16 → `P010`, or `Rgb10a2` for a 4:4:4 source).
|
|
||||||
/// `false` = 8-bit SDR.
|
|
||||||
pub hdr: bool,
|
|
||||||
/// Full-chroma 4:4:4 session: the capturer must keep full chroma. On Windows the IDD-push
|
|
||||||
/// capturer hands the **BGRA** slot through (skipping the subsampling BGRA→NV12
|
|
||||||
/// VideoConverter) so NVENC ingests full-chroma RGB and CSCs to 4:4:4 itself — measured
|
|
||||||
/// on-glass (RTX 5070 Ti): ARGB + `chromaFormatIDC=3` yields TRUE 4:4:4 and the conversion
|
|
||||||
/// follows the configured VUI matrix (BT.709 limited since the VUI is always written). On
|
|
||||||
/// Linux it forces the CPU RGB path the encoder swscales to `YUV444P`. `false` on every
|
|
||||||
/// 4:2:0 session.
|
|
||||||
pub chroma_444: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl OutputFormat {
|
|
||||||
/// Resolve the output format for an entry point that doesn't build a full [`SessionPlan`]
|
|
||||||
/// (`crate::session_plan`) — the GameStream + spike paths. `gpu` is the encoder's GPU-residency,
|
|
||||||
/// resolved by the caller via [`crate::encode::resolved_backend_is_gpu`] and passed **in** (capture
|
|
||||||
/// never re-derives the backend — the one-way capture→encode edge, plan §2.4 / §W4); `hdr` as given.
|
|
||||||
/// The native punktfunk/1 path uses `SessionPlan::output_format()` instead (it already resolved the
|
|
||||||
/// encoder), so neither path makes a capturer re-derive it.
|
|
||||||
pub fn resolve(hdr: bool, gpu: bool) -> Self {
|
|
||||||
OutputFormat {
|
|
||||||
gpu,
|
|
||||||
hdr,
|
|
||||||
// The GameStream + spike paths are always 4:2:0 (4:4:4 is punktfunk/1-native only).
|
|
||||||
chroma_444: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A mouse-cursor overlay to composite onto a frame at encode time (cursor-as-metadata). Rides on
|
|
||||||
/// [`CapturedFrame::cursor`] for the GPU zero-copy payloads (Cuda/Dmabuf), whose pixels never touch
|
|
||||||
/// the CPU — the encoder blends this small bitmap into its owned surface (Vulkan CSC image / CUDA
|
|
||||||
/// devbuf / VA surface). The CPU de-pad path composites the cursor inline instead, so it leaves
|
|
||||||
/// this `None`. `rgba` is `Arc` so attaching the (unchanged) bitmap to every frame is a refcount
|
|
||||||
/// bump, not a copy; `serial` bumps only when the bitmap image changes, so the encoder re-uploads
|
|
||||||
/// its small GPU texture on change and just moves a push-constant otherwise.
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct CursorOverlay {
|
|
||||||
/// Top-left in frame pixels where the bitmap is drawn (already = reported position − hotspot).
|
|
||||||
pub x: i32,
|
|
||||||
pub y: i32,
|
|
||||||
pub w: u32,
|
|
||||||
pub h: u32,
|
|
||||||
/// Straight-alpha RGBA pixels, `w*h*4` (bytes R,G,B,A).
|
|
||||||
pub rgba: std::sync::Arc<Vec<u8>>,
|
|
||||||
/// Bumps whenever `rgba`/`w`/`h` change; stable across position-only moves.
|
|
||||||
pub serial: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A captured frame. [`format`](Self::format)/dimensions describe the pixels regardless of
|
|
||||||
/// where they live — [`payload`](Self::payload) is either a CPU buffer (the spike/fallback path)
|
|
||||||
/// or a GPU buffer already on the device (the zero-copy path, plan §9).
|
|
||||||
pub struct CapturedFrame {
|
|
||||||
pub width: u32,
|
|
||||||
pub height: u32,
|
|
||||||
pub pts_ns: u64,
|
|
||||||
/// Pixel layout of the payload.
|
|
||||||
pub format: PixelFormat,
|
|
||||||
pub payload: FramePayload,
|
|
||||||
/// Cursor overlay to blend at encode time (GPU zero-copy payloads only); `None` when there's no
|
|
||||||
/// visible cursor or the pixels were already composited on the CPU de-pad path. See
|
|
||||||
/// [`CursorOverlay`].
|
|
||||||
pub cursor: Option<CursorOverlay>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A captured frame still living in a single-plane packed-RGB dmabuf (the VAAPI zero-copy path).
|
|
||||||
/// Owns a *dup* of the PipeWire buffer's fd, so the frame can travel to the encode thread and be
|
|
||||||
/// imported into a VA surface there without the compositor's buffer being closed underneath it.
|
|
||||||
/// (Content stability across the brief import window relies on the compositor's buffer pool depth,
|
|
||||||
/// same as any zero-copy capture — the VAAPI importer copies into its own NV12 surface promptly.)
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
pub struct DmabufFrame {
|
|
||||||
pub fd: std::os::fd::OwnedFd,
|
|
||||||
/// DRM FourCC of the packed-RGB plane (e.g. `XR24` for BGRx).
|
|
||||||
pub fourcc: u32,
|
|
||||||
/// DRM format modifier the compositor allocated (0 = LINEAR).
|
|
||||||
pub modifier: u64,
|
|
||||||
pub offset: u32,
|
|
||||||
pub stride: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Where a captured frame's pixels live.
|
|
||||||
pub enum FramePayload {
|
|
||||||
/// Tightly-packed CPU pixels in `format`, `width*height*bytes_per_pixel` (no row padding).
|
|
||||||
Cpu(Vec<u8>),
|
|
||||||
/// A pitched GPU buffer (BGRA-order, on the shared CUDA context) — the NVIDIA zero-copy path.
|
|
||||||
/// The dmabuf has already been imported + copied into this owned device buffer.
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
Cuda(crate::zerocopy::DeviceBuffer),
|
|
||||||
/// A raw packed-RGB dmabuf — the AMD/Intel (VAAPI) zero-copy path. The encoder imports it into
|
|
||||||
/// a VA surface and does RGB→NV12 on the GPU video engine (no host CSC, no upload).
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
Dmabuf(DmabufFrame),
|
|
||||||
/// A GPU-resident D3D11 texture (Windows zero-copy path for NVENC). Owns the copied frame.
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
D3d11(dxgi::D3d11Frame),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CapturedFrame {
|
|
||||||
/// True if the frame's pixels are a GPU/CUDA buffer (the NVIDIA zero-copy path).
|
|
||||||
pub fn is_cuda(&self) -> bool {
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
{
|
|
||||||
matches!(self.payload, FramePayload::Cuda(_))
|
|
||||||
}
|
|
||||||
#[cfg(not(target_os = "linux"))]
|
|
||||||
{
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// True if the frame is a raw dmabuf (the VAAPI zero-copy path).
|
|
||||||
pub fn is_dmabuf(&self) -> bool {
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
{
|
|
||||||
matches!(self.payload, FramePayload::Dmabuf(_))
|
|
||||||
}
|
|
||||||
#[cfg(not(target_os = "linux"))]
|
|
||||||
{
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Produces frames from a captured output. Lives on its own thread, feeding the encoder
|
/// Produces frames from a captured output. Lives on its own thread, feeding the encoder
|
||||||
/// over a bounded drop-oldest channel (never block the compositor).
|
/// over a bounded drop-oldest channel (never block the compositor).
|
||||||
|
|||||||
@@ -1,20 +1,27 @@
|
|||||||
//! Shared Windows GPU primitives — D3D11 device creation, GPU scheduling priority hooks,
|
//! Windows capture GPU mechanics — the win32u GPU-preference hook, HLSL shader compilation, HDR
|
||||||
//! HLSL shader compilation, HDR FP16→P010 conversion ([`HdrP010Converter`]), video-engine
|
//! FP16→P010 conversion ([`HdrP010Converter`]), video-engine colour conversion ([`VideoConverter`]),
|
||||||
//! colour conversion ([`VideoConverter`]), and the IDD-push capture identity
|
//! and the P010 self-test. Consumed by [`super::idd_push`].
|
||||||
//! ([`WinCaptureTarget`], [`pack_luid`]). Consumed by [`super::idd_push`].
|
//!
|
||||||
//! DXGI Desktop Duplication has been removed; this module contains no capturer.
|
//! The shared IDD-push capture IDENTITY — [`WinCaptureTarget`], [`D3d11Frame`], [`pack_luid`], and
|
||||||
|
//! [`make_device`] (the D3D11 device factory + GPU scheduling-priority hardening) — moved into the
|
||||||
|
//! `pf-frame` leaf crate so capture, encode, and pf-vdisplay share one identity type without a
|
||||||
|
//! capture↔encode↔vdisplay cycle (plan §W6); this module re-exports it so every existing
|
||||||
|
//! `crate::capture::dxgi::*` path keeps resolving. DXGI Desktop Duplication has been removed; this
|
||||||
|
//! module contains no capturer.
|
||||||
|
|
||||||
// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program).
|
// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program).
|
||||||
#![deny(clippy::undocumented_unsafe_blocks)]
|
#![deny(clippy::undocumented_unsafe_blocks)]
|
||||||
|
|
||||||
|
pub use pf_frame::dxgi::{make_device, pack_luid, D3d11Frame, WinCaptureTarget};
|
||||||
|
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
use std::ffi::c_void;
|
use std::ffi::c_void;
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
use windows::core::{s, Interface, PCSTR};
|
use windows::core::{s, Interface, PCSTR};
|
||||||
use windows::Win32::Foundation::{HMODULE, LUID};
|
use windows::Win32::Foundation::HMODULE;
|
||||||
use windows::Win32::Graphics::Direct3D::Fxc::D3DCompile;
|
use windows::Win32::Graphics::Direct3D::Fxc::D3DCompile;
|
||||||
use windows::Win32::Graphics::Direct3D::{
|
use windows::Win32::Graphics::Direct3D::{
|
||||||
ID3DBlob, D3D_DRIVER_TYPE_UNKNOWN, D3D_FEATURE_LEVEL_11_0, D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST,
|
ID3DBlob, D3D_FEATURE_LEVEL_11_0, D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST,
|
||||||
};
|
};
|
||||||
use windows::Win32::Graphics::Direct3D11::{
|
use windows::Win32::Graphics::Direct3D11::{
|
||||||
D3D11CreateDevice, ID3D11Buffer, ID3D11Device, ID3D11DeviceContext, ID3D11PixelShader,
|
D3D11CreateDevice, ID3D11Buffer, ID3D11Device, ID3D11DeviceContext, ID3D11PixelShader,
|
||||||
@@ -32,205 +39,6 @@ use windows::Win32::Graphics::Dxgi::Common::{
|
|||||||
DXGI_FORMAT, DXGI_FORMAT_P010, DXGI_FORMAT_R16G16B16A16_FLOAT, DXGI_FORMAT_R16G16_UNORM,
|
DXGI_FORMAT, DXGI_FORMAT_P010, DXGI_FORMAT_R16G16B16A16_FLOAT, DXGI_FORMAT_R16G16_UNORM,
|
||||||
DXGI_FORMAT_R16_UNORM, DXGI_SAMPLE_DESC,
|
DXGI_FORMAT_R16_UNORM, DXGI_SAMPLE_DESC,
|
||||||
};
|
};
|
||||||
use windows::Win32::Graphics::Dxgi::{IDXGIAdapter1, IDXGIDevice, IDXGIDevice1};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct WinCaptureTarget {
|
|
||||||
/// Packed DXGI adapter LUID (`(HighPart << 32) | (LowPart & 0xffff_ffff)`).
|
|
||||||
pub adapter_luid: i64,
|
|
||||||
/// The output's GDI device name, e.g. `\\.\DISPLAY3`. Can CHANGE across a secure-desktop switch.
|
|
||||||
pub gdi_name: String,
|
|
||||||
/// Stable virtual-display (IddCx) target id — re-resolved to the current GDI name on every recovery.
|
|
||||||
pub target_id: u32,
|
|
||||||
/// The pf-vdisplay driver's WUDFHost pid (from the ADD reply) — the process the IDD-push capturer
|
|
||||||
/// duplicates the sealed frame channel's handles INTO (`idd_push::ChannelBroker`). `0` = unknown
|
|
||||||
/// (a pre-v2 pairing can't occur — the version handshake is hard — so this only guards misuse).
|
|
||||||
pub wudf_pid: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A GPU-resident captured texture (future NVENC-D3D11 zero-copy path).
|
|
||||||
pub struct D3d11Frame {
|
|
||||||
pub texture: ID3D11Texture2D,
|
|
||||||
pub device: ID3D11Device,
|
|
||||||
}
|
|
||||||
// SAFETY: `D3d11Frame` owns an `ID3D11Texture2D` + `ID3D11Device`, which are COM interface pointers.
|
|
||||||
// D3D11 devices/resources use thread-safe (interlocked) COM reference counting, and the device is
|
|
||||||
// created free-threaded (`make_device` passes no `D3D11_CREATE_DEVICE_SINGLETHREADED`), so handing
|
|
||||||
// ownership of the frame to another thread — the capture→encode handoff — and releasing it there is
|
|
||||||
// sound. The value is moved, never aliased (no `Sync`), so there is no concurrent use of the
|
|
||||||
// single-threaded immediate context.
|
|
||||||
unsafe impl Send for D3d11Frame {}
|
|
||||||
|
|
||||||
pub fn pack_luid(luid: LUID) -> i64 {
|
|
||||||
((luid.HighPart as i64) << 32) | (luid.LowPart as i64 & 0xffff_ffff)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a fresh D3D11 device + context on a specific adapter (driver_type UNKNOWN with an explicit
|
|
||||||
/// adapter). Used at open and on every ACCESS_LOST: a device created on one desktop cannot sustain a
|
|
||||||
/// duplication on a *different* desktop (perpetual ACCESS_LOST), so the secure-desktop switch needs a
|
|
||||||
/// device made while the thread is attached to that desktop.
|
|
||||||
pub(crate) unsafe fn make_device(
|
|
||||||
adapter: &IDXGIAdapter1,
|
|
||||||
) -> Result<(ID3D11Device, ID3D11DeviceContext)> {
|
|
||||||
let mut device: Option<ID3D11Device> = None;
|
|
||||||
let mut context: Option<ID3D11DeviceContext> = None;
|
|
||||||
D3D11CreateDevice(
|
|
||||||
adapter,
|
|
||||||
D3D_DRIVER_TYPE_UNKNOWN,
|
|
||||||
HMODULE::default(),
|
|
||||||
D3D11_CREATE_DEVICE_BGRA_SUPPORT,
|
|
||||||
Some(&[D3D_FEATURE_LEVEL_11_0]),
|
|
||||||
D3D11_SDK_VERSION,
|
|
||||||
Some(&mut device),
|
|
||||||
None,
|
|
||||||
Some(&mut context),
|
|
||||||
)
|
|
||||||
.context("D3D11CreateDevice")?;
|
|
||||||
let device = device.context("null D3D11 device")?;
|
|
||||||
let context = context.context("null D3D11 context")?;
|
|
||||||
|
|
||||||
// GPU scheduling hardening — the same approach Sunshine/Apollo use, reimplemented here via the
|
|
||||||
// documented D3DKMT/DXGI APIs (no GPL source copied). Our capture+encode
|
|
||||||
// shares the GPU with the streamed game; when the game saturates the GPU our process is starved of
|
|
||||||
// GPU time slices, so NVENC sits near-idle yet `lock_bitstream` waits ~20 ms for our context to be
|
|
||||||
// scheduled — capping the stream (~47 fps measured at 5K@240) and stuttering. Per-frame copy/convert
|
|
||||||
// is NOT the cause (zero-copy + thread-priority alone didn't move it); the PROCESS-level GPU
|
|
||||||
// scheduling priority class is the decisive cross-process lever. Secondary: the absolute per-device
|
|
||||||
// GPU thread priority and a 1-frame latency cap.
|
|
||||||
elevate_process_gpu_priority();
|
|
||||||
if let Ok(dxgi_dev) = device.cast::<IDXGIDevice>() {
|
|
||||||
// The absolute max GPU thread priority (0x4000001E; the same value Sunshine/Apollo use); fall back to relative +7.
|
|
||||||
if dxgi_dev.SetGPUThreadPriority(0x4000_001E).is_err()
|
|
||||||
&& dxgi_dev.SetGPUThreadPriority(7).is_err()
|
|
||||||
{
|
|
||||||
tracing::warn!("SetGPUThreadPriority failed (run as admin/SYSTEM for GPU priority)");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Ok(dxgi1) = device.cast::<IDXGIDevice1>() {
|
|
||||||
let _ = dxgi1.SetMaximumFrameLatency(1);
|
|
||||||
}
|
|
||||||
Ok((device, context))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resolve the configured GPU scheduling-priority class from `PUNKTFUNK_GPU_PRIORITY_CLASS`
|
|
||||||
/// (`off|normal|high|realtime`, default high). `None` = leave it at the OS default (the `off` opt-out).
|
|
||||||
/// D3DKMT_SCHEDULINGPRIORITYCLASS: IDLE 0, BELOW_NORMAL 1, NORMAL 2, ABOVE_NORMAL 3, HIGH 4, REALTIME 5.
|
|
||||||
fn configured_gpu_priority_class() -> Option<i32> {
|
|
||||||
match std::env::var("PUNKTFUNK_GPU_PRIORITY_CLASS")
|
|
||||||
.ok()
|
|
||||||
.as_deref()
|
|
||||||
{
|
|
||||||
Some("off") => None,
|
|
||||||
Some("normal") => Some(2),
|
|
||||||
Some("realtime") => Some(5),
|
|
||||||
_ => Some(4), // HIGH — safe on NVIDIA+HAGS (realtime can freeze NVENC)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Enable SE_INC_BASE_PRIORITY on the CURRENT process token (best-effort) — the kernel gates the
|
|
||||||
/// HIGH/REALTIME GPU scheduling-priority bump on it. Held by SYSTEM/Administrators; a UAC-FILTERED
|
|
||||||
/// token does NOT have it, which is why `elevate_process_gpu_priority` may silently no-op in a
|
|
||||||
/// restricted service context.
|
|
||||||
unsafe fn enable_inc_base_priority() {
|
|
||||||
use windows::core::PCWSTR;
|
|
||||||
use windows::Win32::Foundation::{CloseHandle, HANDLE, LUID};
|
|
||||||
use windows::Win32::Security::{
|
|
||||||
AdjustTokenPrivileges, LookupPrivilegeValueW, LUID_AND_ATTRIBUTES,
|
|
||||||
SE_INC_BASE_PRIORITY_NAME, SE_PRIVILEGE_ENABLED, TOKEN_ADJUST_PRIVILEGES, TOKEN_PRIVILEGES,
|
|
||||||
TOKEN_QUERY,
|
|
||||||
};
|
|
||||||
use windows::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
|
|
||||||
let mut token = HANDLE::default();
|
|
||||||
if OpenProcessToken(
|
|
||||||
GetCurrentProcess(),
|
|
||||||
TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY,
|
|
||||||
&mut token,
|
|
||||||
)
|
|
||||||
.is_ok()
|
|
||||||
{
|
|
||||||
let mut luid = LUID::default();
|
|
||||||
if LookupPrivilegeValueW(PCWSTR::null(), SE_INC_BASE_PRIORITY_NAME, &mut luid).is_ok() {
|
|
||||||
let tp = TOKEN_PRIVILEGES {
|
|
||||||
PrivilegeCount: 1,
|
|
||||||
Privileges: [LUID_AND_ATTRIBUTES {
|
|
||||||
Luid: luid,
|
|
||||||
Attributes: SE_PRIVILEGE_ENABLED,
|
|
||||||
}],
|
|
||||||
};
|
|
||||||
if AdjustTokenPrivileges(
|
|
||||||
token,
|
|
||||||
false,
|
|
||||||
Some(&tp as *const TOKEN_PRIVILEGES),
|
|
||||||
0,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.is_err()
|
|
||||||
{
|
|
||||||
tracing::warn!("could not enable SE_INC_BASE_PRIORITY for GPU priority");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let _ = CloseHandle(token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Call `gdi32!D3DKMTSetProcessSchedulingPriorityClass(process, prio)` (no stable windows-rs binding —
|
|
||||||
/// loaded by name). Returns the NTSTATUS (0 = success) or `None` if the export can't be resolved. The
|
|
||||||
/// CALLING process must hold SE_INC_BASE_PRIORITY ([`enable_inc_base_priority`]) for HIGH/REALTIME; the
|
|
||||||
/// kernel checks the caller's privilege whether the target is self or a child we created.
|
|
||||||
unsafe fn d3dkmt_set_scheduling_priority_class(
|
|
||||||
process: windows::Win32::Foundation::HANDLE,
|
|
||||||
prio: i32,
|
|
||||||
) -> Option<i32> {
|
|
||||||
use windows::core::s;
|
|
||||||
use windows::Win32::Foundation::HANDLE;
|
|
||||||
use windows::Win32::System::LibraryLoader::{GetProcAddress, LoadLibraryA};
|
|
||||||
let gdi32 = LoadLibraryA(s!("gdi32.dll")).ok()?;
|
|
||||||
let p = GetProcAddress(gdi32, s!("D3DKMTSetProcessSchedulingPriorityClass"))?;
|
|
||||||
type SetPrio = unsafe extern "system" fn(HANDLE, i32) -> i32;
|
|
||||||
let f: SetPrio = std::mem::transmute(p);
|
|
||||||
Some(f(process, prio))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// GPU scheduling-priority hardening — the same approach as Sunshine/Apollo, independently
|
|
||||||
/// implemented via the documented D3DKMT APIs (no GPL source copied). On a
|
|
||||||
/// GPU-saturated game our capture+encode process is starved of GPU time slices — NVENC sits ~idle but
|
|
||||||
/// `lock_bitstream` waits ~20 ms for our context to be scheduled. Elevating the PROCESS GPU scheduling
|
|
||||||
/// priority class (the strong cross-process lever — far more effective than `SetGPUThreadPriority`
|
|
||||||
/// alone, which we measured as no help) lets our brief encode preempt the game. Uses HIGH, NOT
|
|
||||||
/// realtime: realtime on NVIDIA + HAGS can freeze/crash NVENC (Apollo downgrades it for exactly this).
|
|
||||||
/// Runs once per process; best-effort. `PUNKTFUNK_GPU_PRIORITY_CLASS = off|normal|high|realtime`
|
|
||||||
/// (default high). Best-effort: silently no-ops under a UAC-filtered token (the process will not
|
|
||||||
/// hold SE_INC_BASE_PRIORITY, so the D3DKMT call is a no-op).
|
|
||||||
fn elevate_process_gpu_priority() {
|
|
||||||
use std::sync::Once;
|
|
||||||
static ONCE: Once = Once::new();
|
|
||||||
// SAFETY: the closure calls two of this module's `unsafe fn`s — `enable_inc_base_priority`
|
|
||||||
// (adjusts the current-process token; it has no caller precondition and builds all its FFI args
|
|
||||||
// locally) and `d3dkmt_set_scheduling_priority_class` (loads gdi32 by name and calls the export).
|
|
||||||
// The latter requires `process` to be a valid process handle; `GetCurrentProcess()` returns the
|
|
||||||
// current-process pseudo-handle, which is always valid and needs no close. Runs once via
|
|
||||||
// `Once::call_once`; no raw pointers are dereferenced here.
|
|
||||||
ONCE.call_once(|| unsafe {
|
|
||||||
use windows::Win32::System::Threading::GetCurrentProcess;
|
|
||||||
let Some(prio) = configured_gpu_priority_class() else {
|
|
||||||
tracing::info!("GPU process scheduling priority class left at default (off)");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
enable_inc_base_priority();
|
|
||||||
match d3dkmt_set_scheduling_priority_class(GetCurrentProcess(), prio) {
|
|
||||||
Some(0) => tracing::info!(
|
|
||||||
priority_class = prio,
|
|
||||||
"GPU process scheduling priority class set (2=normal 4=high 5=realtime)"
|
|
||||||
),
|
|
||||||
Some(st) => tracing::warn!(
|
|
||||||
status = format!("0x{st:08X}"),
|
|
||||||
"D3DKMTSetProcessSchedulingPriorityClass failed (run as admin/SYSTEM for GPU priority)"
|
|
||||||
),
|
|
||||||
None => tracing::warn!("D3DKMTSetProcessSchedulingPriorityClass export not found"),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// How many times DXGI has actually called our hooked `NtGdiDdDDIGetCachedHybridQueryValue`. If this
|
/// How many times DXGI has actually called our hooked `NtGdiDdDDIGetCachedHybridQueryValue`. If this
|
||||||
/// stays 0 while DDA churns with ACCESS_LOST, the hook is NOT on DXGI's GPU-preference path on this
|
/// stays 0 while DDA churns with ACCESS_LOST, the hook is NOT on DXGI's GPU-preference path on this
|
||||||
|
|||||||
@@ -1514,7 +1514,7 @@ impl Capturer for IddPushCapturer {
|
|||||||
// PQ VUI; pair that with a mastering-display SEI so any decoder tone-maps from a real grade. The
|
// PQ VUI; pair that with a mastering-display SEI so any decoder tone-maps from a real grade. The
|
||||||
// driver doesn't (yet) forward the OS's IDDCX_HDR10_METADATA, so use the generic HDR10 baseline
|
// driver doesn't (yet) forward the OS's IDDCX_HDR10_METADATA, so use the generic HDR10 baseline
|
||||||
// (the same metadata the native HDR path sends on the 0xCE datagram).
|
// (the same metadata the native HDR path sends on the 0xCE datagram).
|
||||||
self.display_hdr.then(crate::hdr::generic_hdr10)
|
self.display_hdr.then(pf_frame::hdr::generic_hdr10)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pipeline_depth(&self) -> usize {
|
fn pipeline_depth(&self) -> usize {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ pub(super) struct Stall {
|
|||||||
/// How long the hole lasted (last fresh frame → the frame that ended it).
|
/// How long the hole lasted (last fresh frame → the frame that ended it).
|
||||||
pub(super) gap: Duration,
|
pub(super) gap: Duration,
|
||||||
/// `Some(mean period)` when this stall completes a metronomic cycle (see
|
/// `Some(mean period)` when this stall completes a metronomic cycle (see
|
||||||
/// [`crate::metronome::Metronome`]).
|
/// [`pf_frame::metronome::Metronome`]).
|
||||||
pub(super) metronomic: Option<Duration>,
|
pub(super) metronomic: Option<Duration>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,14 +23,14 @@ pub(super) struct Stall {
|
|||||||
/// On a damage-driven capture an idle desktop legitimately goes quiet (no damage → no frames), so a
|
/// On a damage-driven capture an idle desktop legitimately goes quiet (no damage → no frames), so a
|
||||||
/// gap only counts as a stall when the [`Self::RECENT`] frames before it all arrived within
|
/// gap only counts as a stall when the [`Self::RECENT`] frames before it all arrived within
|
||||||
/// [`Self::ACTIVE_SPAN`] — sustained ≥ ~20 fps flow (a game or video), not a blinking caret or a
|
/// [`Self::ACTIVE_SPAN`] — sustained ≥ ~20 fps flow (a game or video), not a blinking caret or a
|
||||||
/// mouse twitch. Each stall feeds a [`crate::metronome::Metronome`], so periodic stalls self-diagnose
|
/// mouse twitch. Each stall feeds a [`pf_frame::metronome::Metronome`], so periodic stalls self-diagnose
|
||||||
/// in the log WITHOUT needing any client keyframe request — discriminating "DWM stopped composing"
|
/// in the log WITHOUT needing any client keyframe request — discriminating "DWM stopped composing"
|
||||||
/// from encode/network causes that the recovery-cadence detector covers. Pure logic — unit-tested
|
/// from encode/network causes that the recovery-cadence detector covers. Pure logic — unit-tested
|
||||||
/// below; the caller does the logging.
|
/// below; the caller does the logging.
|
||||||
pub(super) struct StallWatch {
|
pub(super) struct StallWatch {
|
||||||
/// The last [`Self::RECENT`] fresh-frame instants (pre-gap history for the activity gate).
|
/// The last [`Self::RECENT`] fresh-frame instants (pre-gap history for the activity gate).
|
||||||
recent: std::collections::VecDeque<Instant>,
|
recent: std::collections::VecDeque<Instant>,
|
||||||
cadence: crate::metronome::Metronome,
|
cadence: pf_frame::metronome::Metronome,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StallWatch {
|
impl StallWatch {
|
||||||
@@ -47,7 +47,7 @@ impl StallWatch {
|
|||||||
pub(super) fn new() -> Self {
|
pub(super) fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
recent: std::collections::VecDeque::with_capacity(Self::RECENT + 1),
|
recent: std::collections::VecDeque::with_capacity(Self::RECENT + 1),
|
||||||
cadence: crate::metronome::Metronome::new(),
|
cadence: pf_frame::metronome::Metronome::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1010,23 +1010,23 @@ impl Encoder for NvencCudaEncoder {
|
|||||||
let is_idr = flags != 0 || opening;
|
let is_idr = flags != 0 || opening;
|
||||||
let mastering_sei = self
|
let mastering_sei = self
|
||||||
.hdr_meta
|
.hdr_meta
|
||||||
.map(|m| crate::hdr::hevc_mastering_display_sei(&m));
|
.map(|m| pf_frame::hdr::hevc_mastering_display_sei(&m));
|
||||||
let cll_sei = self
|
let cll_sei = self
|
||||||
.hdr_meta
|
.hdr_meta
|
||||||
.map(|m| crate::hdr::hevc_content_light_level_sei(&m));
|
.map(|m| pf_frame::hdr::hevc_content_light_level_sei(&m));
|
||||||
let mut sei: Vec<nv::NV_ENC_SEI_PAYLOAD> = Vec::new();
|
let mut sei: Vec<nv::NV_ENC_SEI_PAYLOAD> = Vec::new();
|
||||||
if is_idr && self.hdr {
|
if is_idr && self.hdr {
|
||||||
if let Some(p) = mastering_sei.as_ref() {
|
if let Some(p) = mastering_sei.as_ref() {
|
||||||
sei.push(nv::NV_ENC_SEI_PAYLOAD {
|
sei.push(nv::NV_ENC_SEI_PAYLOAD {
|
||||||
payloadSize: p.len() as u32,
|
payloadSize: p.len() as u32,
|
||||||
payloadType: crate::hdr::SEI_TYPE_MASTERING_DISPLAY_COLOUR_VOLUME,
|
payloadType: pf_frame::hdr::SEI_TYPE_MASTERING_DISPLAY_COLOUR_VOLUME,
|
||||||
payload: p.as_ptr() as *mut u8,
|
payload: p.as_ptr() as *mut u8,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if let Some(p) = cll_sei.as_ref() {
|
if let Some(p) = cll_sei.as_ref() {
|
||||||
sei.push(nv::NV_ENC_SEI_PAYLOAD {
|
sei.push(nv::NV_ENC_SEI_PAYLOAD {
|
||||||
payloadSize: p.len() as u32,
|
payloadSize: p.len() as u32,
|
||||||
payloadType: crate::hdr::SEI_TYPE_CONTENT_LIGHT_LEVEL_INFO,
|
payloadType: pf_frame::hdr::SEI_TYPE_CONTENT_LIGHT_LEVEL_INFO,
|
||||||
payload: p.as_ptr() as *mut u8,
|
payload: p.as_ptr() as *mut u8,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -321,7 +321,7 @@ fn retrieve_loop(
|
|||||||
work_rx: mpsc::Receiver<RetrieveJob>,
|
work_rx: mpsc::Receiver<RetrieveJob>,
|
||||||
done_tx: mpsc::Sender<RetrieveDone>,
|
done_tx: mpsc::Sender<RetrieveDone>,
|
||||||
) {
|
) {
|
||||||
crate::native::boost_thread_priority(false);
|
pf_frame::thread_qos::boost_thread_priority(false);
|
||||||
while let Ok(job) = work_rx.recv() {
|
while let Ok(job) = work_rx.recv() {
|
||||||
// SAFETY: `job.event` is one of the auto-reset events `init_session` created and
|
// SAFETY: `job.event` is one of the auto-reset events `init_session` created and
|
||||||
// registered for exactly this session, and `job.bs` one of its pool bitstreams; both stay
|
// registered for exactly this session, and `job.bs` one of its pool bitstreams; both stay
|
||||||
@@ -1250,23 +1250,23 @@ impl Encoder for NvencD3d11Encoder {
|
|||||||
let is_idr = flags != 0 || opening;
|
let is_idr = flags != 0 || opening;
|
||||||
let mastering_sei = self
|
let mastering_sei = self
|
||||||
.hdr_meta
|
.hdr_meta
|
||||||
.map(|m| crate::hdr::hevc_mastering_display_sei(&m));
|
.map(|m| pf_frame::hdr::hevc_mastering_display_sei(&m));
|
||||||
let cll_sei = self
|
let cll_sei = self
|
||||||
.hdr_meta
|
.hdr_meta
|
||||||
.map(|m| crate::hdr::hevc_content_light_level_sei(&m));
|
.map(|m| pf_frame::hdr::hevc_content_light_level_sei(&m));
|
||||||
let mut sei: Vec<nv::NV_ENC_SEI_PAYLOAD> = Vec::new();
|
let mut sei: Vec<nv::NV_ENC_SEI_PAYLOAD> = Vec::new();
|
||||||
if is_idr && self.hdr {
|
if is_idr && self.hdr {
|
||||||
if let Some(p) = mastering_sei.as_ref() {
|
if let Some(p) = mastering_sei.as_ref() {
|
||||||
sei.push(nv::NV_ENC_SEI_PAYLOAD {
|
sei.push(nv::NV_ENC_SEI_PAYLOAD {
|
||||||
payloadSize: p.len() as u32,
|
payloadSize: p.len() as u32,
|
||||||
payloadType: crate::hdr::SEI_TYPE_MASTERING_DISPLAY_COLOUR_VOLUME,
|
payloadType: pf_frame::hdr::SEI_TYPE_MASTERING_DISPLAY_COLOUR_VOLUME,
|
||||||
payload: p.as_ptr() as *mut u8,
|
payload: p.as_ptr() as *mut u8,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if let Some(p) = cll_sei.as_ref() {
|
if let Some(p) = cll_sei.as_ref() {
|
||||||
sei.push(nv::NV_ENC_SEI_PAYLOAD {
|
sei.push(nv::NV_ENC_SEI_PAYLOAD {
|
||||||
payloadSize: p.len() as u32,
|
payloadSize: p.len() as u32,
|
||||||
payloadType: crate::hdr::SEI_TYPE_CONTENT_LIGHT_LEVEL_INFO,
|
payloadType: pf_frame::hdr::SEI_TYPE_CONTENT_LIGHT_LEVEL_INFO,
|
||||||
payload: p.as_ptr() as *mut u8,
|
payload: p.as_ptr() as *mut u8,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ fn run(
|
|||||||
stats: &Arc<crate::stats_recorder::StatsRecorder>,
|
stats: &Arc<crate::stats_recorder::StatsRecorder>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
// GameStream capture/encode thread: apply Windows session tuning (no-op off Windows).
|
// GameStream capture/encode thread: apply Windows session tuning (no-op off Windows).
|
||||||
crate::session_tuning::on_hot_thread();
|
pf_frame::session_tuning::on_hot_thread();
|
||||||
// Reject an out-of-range client mode before allocating capture/encode buffers.
|
// Reject an out-of-range client mode before allocating capture/encode buffers.
|
||||||
encode::validate_dimensions(cfg.codec, cfg.width, cfg.height)
|
encode::validate_dimensions(cfg.codec, cfg.width, cfg.height)
|
||||||
.context("client-requested video mode")?;
|
.context("client-requested video mode")?;
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ mod gamestream;
|
|||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
#[path = "linux/gpuclocks.rs"]
|
#[path = "linux/gpuclocks.rs"]
|
||||||
mod gpuclocks;
|
mod gpuclocks;
|
||||||
mod hdr;
|
|
||||||
mod hooks;
|
mod hooks;
|
||||||
mod inject;
|
mod inject;
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
@@ -55,7 +54,6 @@ mod install;
|
|||||||
mod interactive;
|
mod interactive;
|
||||||
mod library;
|
mod library;
|
||||||
mod log_capture;
|
mod log_capture;
|
||||||
mod metronome;
|
|
||||||
mod mgmt;
|
mod mgmt;
|
||||||
mod mgmt_token;
|
mod mgmt_token;
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
@@ -71,7 +69,6 @@ mod send_pacing;
|
|||||||
mod service;
|
mod service;
|
||||||
mod session_plan;
|
mod session_plan;
|
||||||
mod session_status;
|
mod session_status;
|
||||||
mod session_tuning;
|
|
||||||
mod spike;
|
mod spike;
|
||||||
mod stats_recorder;
|
mod stats_recorder;
|
||||||
mod stream_marker;
|
mod stream_marker;
|
||||||
|
|||||||
@@ -42,11 +42,10 @@ use rand::RngCore;
|
|||||||
use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicU8, Ordering};
|
use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicU8, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
/// Per-thread OS scheduling QoS lives in its own module (plan §W1); re-exported so
|
/// Per-thread OS scheduling QoS lives in the shared `pf-frame` leaf crate (plan §W1/§W6);
|
||||||
/// `crate::native::boost_thread_priority` stays stable (the GameStream path and the direct-NVENC
|
/// re-exported so `crate::native::boost_thread_priority` stays stable (the GameStream path and the
|
||||||
/// send thread reach it there).
|
/// native data plane reach it there).
|
||||||
mod thread_qos;
|
pub(crate) use pf_frame::thread_qos::boost_thread_priority;
|
||||||
pub(crate) use thread_qos::boost_thread_priority;
|
|
||||||
|
|
||||||
/// Compositor-preference resolution (plan §W1); `serve_session` reaches `resolve_compositor` here.
|
/// Compositor-preference resolution (plan §W1); `serve_session` reaches `resolve_compositor` here.
|
||||||
mod compositor;
|
mod compositor;
|
||||||
@@ -1029,7 +1028,9 @@ async fn serve_session(
|
|||||||
// Prefer the CLIENT's own display volume (Hello::display_hdr): the virtual display's EDID
|
// Prefer the CLIENT's own display volume (Hello::display_hdr): the virtual display's EDID
|
||||||
// now advertises it, so host apps tone-map to exactly that volume — echoing it here keeps
|
// now advertises it, so host apps tone-map to exactly that volume — echoing it here keeps
|
||||||
// the mastering metadata honest end-to-end. Generic HDR10 only for older clients.
|
// the mastering metadata honest end-to-end. Generic HDR10 only for older clients.
|
||||||
let meta = hello.display_hdr.unwrap_or_else(crate::hdr::generic_hdr10);
|
let meta = hello
|
||||||
|
.display_hdr
|
||||||
|
.unwrap_or_else(pf_frame::hdr::generic_hdr10);
|
||||||
let _ = conn.send_datagram(punktfunk_core::quic::encode_hdr_meta_datagram(&meta).into());
|
let _ = conn.send_datagram(punktfunk_core::quic::encode_hdr_meta_datagram(&meta).into());
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
client_volume = hello.display_hdr.is_some(),
|
client_volume = hello.display_hdr.is_some(),
|
||||||
|
|||||||
@@ -1020,8 +1020,8 @@ pub(super) fn virtual_stream(ctx: SessionContext) -> Result<()> {
|
|||||||
// opening GOP, instead of answering it with a redundant second IDR.
|
// opening GOP, instead of answering it with a redundant second IDR.
|
||||||
let mut last_forced_idr: Option<std::time::Instant> = Some(std::time::Instant::now());
|
let mut last_forced_idr: Option<std::time::Instant> = Some(std::time::Instant::now());
|
||||||
// Self-diagnosis for the periodic-stutter class: warns when the served recovery IDRs settle
|
// Self-diagnosis for the periodic-stutter class: warns when the served recovery IDRs settle
|
||||||
// into a stable multi-second rhythm (see [`crate::metronome::Metronome`]).
|
// into a stable multi-second rhythm (see [`pf_frame::metronome::Metronome`]).
|
||||||
let mut recovery_cadence = crate::metronome::Metronome::new();
|
let mut recovery_cadence = pf_frame::metronome::Metronome::new();
|
||||||
// Position within the current intra-refresh wave (frames since the last IDR/wave start). Only
|
// Position within the current intra-refresh wave (frames since the last IDR/wave start). Only
|
||||||
// meaningful on a `caps().intra_refresh_recovery` encoder; the pump tags every wave-boundary AU
|
// meaningful on a `caps().intra_refresh_recovery` encoder; the pump tags every wave-boundary AU
|
||||||
// with `USER_FLAG_RECOVERY_POINT` so the client can lift its post-loss freeze on a clean
|
// with `USER_FLAG_RECOVERY_POINT` so the client can lift its post-loss freeze on a clean
|
||||||
|
|||||||
@@ -443,7 +443,7 @@ impl VdisplayDriver for PfVdisplayDriver {
|
|||||||
// unknown → the driver keeps its built-in defaults (also what an un-upgraded driver, which
|
// unknown → the driver keeps its built-in defaults (also what an un-upgraded driver, which
|
||||||
// reads only the legacy 24-byte prefix, does).
|
// reads only the legacy 24-byte prefix, does).
|
||||||
let (max_luminance_nits, max_frame_avg_nits, min_luminance_millinits) = client_hdr
|
let (max_luminance_nits, max_frame_avg_nits, min_luminance_millinits) = client_hdr
|
||||||
.map(|m| crate::hdr::vdisplay_luminance_fields(&m))
|
.map(|m| pf_frame::hdr::vdisplay_luminance_fields(&m))
|
||||||
.unwrap_or((0, 0, 0));
|
.unwrap_or((0, 0, 0));
|
||||||
if max_luminance_nits > 0 {
|
if max_luminance_nits > 0 {
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
|
|||||||
Reference in New Issue
Block a user