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:
2026-07-17 10:03:56 +02:00
parent 6824c1cc0c
commit b168790e0a
21 changed files with 555 additions and 446 deletions
+73
View File
@@ -0,0 +1,73 @@
//! Per-thread OS scheduling QoS for the data plane (plan §W1/§W6 — now in the shared `pf-frame`
//! leaf). The capture/encode and send threads raise their own priority so a CPU-saturating game
//! can't deschedule them; the native, GameStream, and direct-NVENC send threads all reach this the
//! same way (`pf_frame::thread_qos::boost_thread_priority`).
// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program).
#![deny(clippy::undocumented_unsafe_blocks)]
/// Raise the current thread's OS scheduling priority so a CPU-heavy game can't deschedule our
/// capture/encode/send threads. This matters even though our GPU work is already HIGH priority: the
/// GPU scheduler can only favour commands we've actually SUBMITTED, so if a normal-priority thread is
/// descheduled by the game it submits the convert/encode late and the GPU priority never bites. Apollo
/// does the same (capture thread CRITICAL, encoder ABOVE_NORMAL). The Linux host needs this too: an
/// 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
/// (the capture+encode loop); otherwise above-normal (the send/relay thread).
pub fn boost_thread_priority(critical: bool) {
// 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
// capture/encode (critical) and send (non-critical).
crate::session_tuning::on_hot_thread();
#[cfg(target_os = "windows")]
// SAFETY: `GetCurrentThread()` returns the constant pseudo-handle for the calling thread — always
// valid, thread-local in meaning, and never closed (no leak/double-close). `SetThreadPriority`
// takes that handle plus a `THREAD_PRIORITY_*` value the windows crate defines (HIGHEST or
// ABOVE_NORMAL here); it only reprioritizes this OS thread, borrows no Rust memory, and its
// `Result` is matched (a failure is logged, never UB). No pointers, lifetimes, or aliasing.
unsafe {
use windows::Win32::System::Threading::{
GetCurrentThread, SetThreadPriority, THREAD_PRIORITY_ABOVE_NORMAL,
THREAD_PRIORITY_HIGHEST,
};
let prio = if critical {
THREAD_PRIORITY_HIGHEST
} else {
THREAD_PRIORITY_ABOVE_NORMAL
};
match SetThreadPriority(GetCurrentThread(), prio) {
Ok(()) => tracing::debug!(critical, "thread priority raised"),
Err(e) => {
tracing::debug!(critical, error = ?e, "SetThreadPriority failed")
}
}
}
#[cfg(target_os = "linux")]
{
// Best-effort nice of the CALLING thread. On Linux `setpriority(PRIO_PROCESS, 0, …)` acts on
// the calling thread (the kernel resolves who==0 to the current task/tid), and both call
// sites run inside their worker thread — so this nices exactly the capture/encode (critical)
// and send (non-critical) threads, nothing else. Silently no-ops without CAP_SYS_NICE / a
// raised RLIMIT_NICE, which is fine. We deliberately do NOT use SCHED_RR/FIFO by default: a
// realtime CPU class can preempt the compositor AND the game's own render thread, adding the
// very frame-time we refuse to add (opt-in only — see PUNKTFUNK_SCHED_RR).
let nice = if critical { -10 } else { -5 };
// SAFETY: `setpriority` takes three by-value integers and no pointers, so there is nothing to
// alias or outlive. `PRIO_PROCESS` with `who == 0` targets the calling task on Linux and
// `nice` is in range; the call only adjusts this thread's scheduling nice value and returns an
// `int` we inspect. No memory is touched.
let rc = unsafe { libc::setpriority(libc::PRIO_PROCESS, 0, nice) };
if rc == 0 {
tracing::debug!(critical, nice, "thread nice raised");
} else {
tracing::debug!(
critical,
"setpriority(nice) no-op (needs CAP_SYS_NICE / RLIMIT_NICE)"
);
}
}
#[cfg(not(any(target_os = "windows", target_os = "linux")))]
{
let _ = critical;
}
}