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:
@@ -19,6 +19,9 @@ pf-gpu = { path = "../pf-gpu" }
|
||||
# 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.
|
||||
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).
|
||||
quinn = "0.11"
|
||||
anyhow = "1"
|
||||
|
||||
@@ -8,212 +8,13 @@
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
/// 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"`).
|
||||
// The shared frame vocabulary lives in the `pf-frame` leaf crate (plan §W6); re-export it here so
|
||||
// every existing `crate::capture::{PixelFormat, CapturedFrame, …}` path stays valid.
|
||||
pub use pf_frame::{CapturedFrame, FramePayload, OutputFormat, PixelFormat};
|
||||
// `CursorOverlay` (cursor-as-metadata) and the dmabuf vocabulary are named only by the Linux
|
||||
// capture/encode paths; gate the re-exports so the Windows build doesn't flag them unused.
|
||||
#[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 [`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
|
||||
}
|
||||
}
|
||||
}
|
||||
pub use pf_frame::{drm_fourcc, CursorOverlay, DmabufFrame};
|
||||
|
||||
/// Produces frames from a captured output. Lives on its own thread, feeding the encoder
|
||||
/// 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,
|
||||
//! HLSL shader compilation, HDR FP16→P010 conversion ([`HdrP010Converter`]), video-engine
|
||||
//! colour conversion ([`VideoConverter`]), and the IDD-push capture identity
|
||||
//! ([`WinCaptureTarget`], [`pack_luid`]). Consumed by [`super::idd_push`].
|
||||
//! DXGI Desktop Duplication has been removed; this module contains no capturer.
|
||||
//! Windows capture GPU mechanics — the win32u GPU-preference hook, HLSL shader compilation, HDR
|
||||
//! FP16→P010 conversion ([`HdrP010Converter`]), video-engine colour conversion ([`VideoConverter`]),
|
||||
//! and the P010 self-test. Consumed by [`super::idd_push`].
|
||||
//!
|
||||
//! 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).
|
||||
#![deny(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
pub use pf_frame::dxgi::{make_device, pack_luid, D3d11Frame, WinCaptureTarget};
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use std::ffi::c_void;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
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::{
|
||||
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::{
|
||||
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_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
|
||||
/// 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
|
||||
// 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).
|
||||
self.display_hdr.then(crate::hdr::generic_hdr10)
|
||||
self.display_hdr.then(pf_frame::hdr::generic_hdr10)
|
||||
}
|
||||
|
||||
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).
|
||||
pub(super) gap: Duration,
|
||||
/// `Some(mean period)` when this stall completes a metronomic cycle (see
|
||||
/// [`crate::metronome::Metronome`]).
|
||||
/// [`pf_frame::metronome::Metronome`]).
|
||||
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
|
||||
/// 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
|
||||
/// 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"
|
||||
/// from encode/network causes that the recovery-cadence detector covers. Pure logic — unit-tested
|
||||
/// below; the caller does the logging.
|
||||
pub(super) struct StallWatch {
|
||||
/// The last [`Self::RECENT`] fresh-frame instants (pre-gap history for the activity gate).
|
||||
recent: std::collections::VecDeque<Instant>,
|
||||
cadence: crate::metronome::Metronome,
|
||||
cadence: pf_frame::metronome::Metronome,
|
||||
}
|
||||
|
||||
impl StallWatch {
|
||||
@@ -47,7 +47,7 @@ impl StallWatch {
|
||||
pub(super) fn new() -> Self {
|
||||
Self {
|
||||
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 mastering_sei = self
|
||||
.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
|
||||
.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();
|
||||
if is_idr && self.hdr {
|
||||
if let Some(p) = mastering_sei.as_ref() {
|
||||
sei.push(nv::NV_ENC_SEI_PAYLOAD {
|
||||
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,
|
||||
});
|
||||
}
|
||||
if let Some(p) = cll_sei.as_ref() {
|
||||
sei.push(nv::NV_ENC_SEI_PAYLOAD {
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -321,7 +321,7 @@ fn retrieve_loop(
|
||||
work_rx: mpsc::Receiver<RetrieveJob>,
|
||||
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() {
|
||||
// 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
|
||||
@@ -1250,23 +1250,23 @@ impl Encoder for NvencD3d11Encoder {
|
||||
let is_idr = flags != 0 || opening;
|
||||
let mastering_sei = self
|
||||
.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
|
||||
.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();
|
||||
if is_idr && self.hdr {
|
||||
if let Some(p) = mastering_sei.as_ref() {
|
||||
sei.push(nv::NV_ENC_SEI_PAYLOAD {
|
||||
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,
|
||||
});
|
||||
}
|
||||
if let Some(p) = cll_sei.as_ref() {
|
||||
sei.push(nv::NV_ENC_SEI_PAYLOAD {
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ fn run(
|
||||
stats: &Arc<crate::stats_recorder::StatsRecorder>,
|
||||
) -> Result<()> {
|
||||
// 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.
|
||||
encode::validate_dimensions(cfg.codec, cfg.width, cfg.height)
|
||||
.context("client-requested video mode")?;
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
//! Pure HDR static-metadata helpers shared by the capture (source mastering metadata) and encode
|
||||
//! (in-band SEI) paths — kept platform-independent and unit-tested here so the byte-level logic is
|
||||
//! verified on every target, even though the only *callers* of the SEI builders are the Windows
|
||||
//! NVENC path (`encode/nvenc.rs`) and of the display conversion the Windows DXGI/WGC capturers.
|
||||
//!
|
||||
//! Units follow the HDR10 standards so the values pass straight through:
|
||||
//! - chromaticities in 1/50000 increments (SMPTE ST.2086 / DXGI `DXGI_HDR_METADATA_HDR10`),
|
||||
//! - mastering luminance in 0.0001 cd/m²,
|
||||
//! - content light level (MaxCLL/MaxFALL) in cd/m² (nits).
|
||||
|
||||
use punktfunk_core::quic::HdrMeta;
|
||||
|
||||
/// HEVC/H.264 SEI payload type for `mastering_display_colour_volume` (SMPTE ST.2086). Same code
|
||||
/// point in AVC and HEVC.
|
||||
pub const SEI_TYPE_MASTERING_DISPLAY_COLOUR_VOLUME: u32 = 137;
|
||||
/// HEVC/H.264 SEI payload type for `content_light_level_info` (CEA-861.3 MaxCLL/MaxFALL).
|
||||
pub const SEI_TYPE_CONTENT_LIGHT_LEVEL_INFO: u32 = 144;
|
||||
|
||||
/// Quantize a CIE xy chromaticity coordinate (0.0..=1.0) to ST.2086 1/50000 units.
|
||||
fn xy_to_2086(v: f32) -> u16 {
|
||||
(v * 50000.0).round().clamp(0.0, 65535.0) as u16
|
||||
}
|
||||
|
||||
/// Build an [`HdrMeta`] from a source display's measured colour volume — the chromaticities (CIE xy)
|
||||
/// and luminances (cd/m²) reported by e.g. Windows `IDXGIOutput6::GetDesc1`. `max_cll`/`max_fall`
|
||||
/// are content light levels in nits; pass `0` when unknown (GetDesc1 doesn't expose them — Apollo
|
||||
/// zeroes them too, and a `0` lets the display fall back to the mastering luminance).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn hdr_meta_from_display(
|
||||
red: (f32, f32),
|
||||
green: (f32, f32),
|
||||
blue: (f32, f32),
|
||||
white: (f32, f32),
|
||||
max_mastering_nits: f32,
|
||||
min_mastering_nits: f32,
|
||||
max_cll: u16,
|
||||
max_fall: u16,
|
||||
) -> HdrMeta {
|
||||
HdrMeta {
|
||||
// ST.2086 stores primaries in G, B, R order.
|
||||
display_primaries: [
|
||||
[xy_to_2086(green.0), xy_to_2086(green.1)],
|
||||
[xy_to_2086(blue.0), xy_to_2086(blue.1)],
|
||||
[xy_to_2086(red.0), xy_to_2086(red.1)],
|
||||
],
|
||||
white_point: [xy_to_2086(white.0), xy_to_2086(white.1)],
|
||||
max_display_mastering_luminance: (max_mastering_nits.max(0.0) * 10_000.0).round() as u32,
|
||||
min_display_mastering_luminance: (min_mastering_nits.max(0.0) * 10_000.0).round() as u32,
|
||||
max_cll,
|
||||
max_fall,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert an [`HdrMeta`] display volume into the pf-vdisplay `AddRequest` luminance fields —
|
||||
/// `(max nits, max frame-average nits, min MILLI-nits)` — which the driver codes into the virtual
|
||||
/// monitor's EDID CTA-861.3 HDR block. Pure unit conversion: mastering luminance is 0.0001 cd/m²
|
||||
/// (so nits = /10 000, milli-nits = /10); MaxFALL is already nits and doubles as the display's
|
||||
/// frame-average ceiling.
|
||||
pub fn vdisplay_luminance_fields(m: &HdrMeta) -> (u32, u32, u32) {
|
||||
(
|
||||
m.max_display_mastering_luminance / 10_000,
|
||||
m.max_fall as u32,
|
||||
m.min_display_mastering_luminance / 10,
|
||||
)
|
||||
}
|
||||
|
||||
/// A generic HDR10 default (BT.2020 primaries, D65 white, 1000-nit mastering, MaxCLL 1000 /
|
||||
/// MaxFALL 400) — the baseline a host sends until it reads the source display's real mastering
|
||||
/// metadata, and the values clients used to hardcode.
|
||||
pub fn generic_hdr10() -> HdrMeta {
|
||||
HdrMeta {
|
||||
display_primaries: [[8500, 39850], [6550, 2300], [35400, 14600]], // BT.2020 G, B, R
|
||||
white_point: [15635, 16450], // D65
|
||||
max_display_mastering_luminance: 10_000_000, // 1000 nits
|
||||
min_display_mastering_luminance: 1, // 0.0001 nits
|
||||
max_cll: 1000,
|
||||
max_fall: 400,
|
||||
}
|
||||
}
|
||||
|
||||
/// The `mastering_display_colour_volume` SEI payload (HEVC/H.264 type
|
||||
/// [`SEI_TYPE_MASTERING_DISPLAY_COLOUR_VOLUME`]) — 24 bytes, big-endian (SEI RBSP order), in G/B/R
|
||||
/// primary order per ST.2086. Pass this raw payload to NVENC's `NV_ENC_SEI_PAYLOAD` (NVENC wraps it
|
||||
/// in the SEI NAL).
|
||||
pub fn hevc_mastering_display_sei(m: &HdrMeta) -> [u8; 24] {
|
||||
let mut b = [0u8; 24];
|
||||
let mut o = 0;
|
||||
let mut put16 = |v: u16| {
|
||||
b[o..o + 2].copy_from_slice(&v.to_be_bytes());
|
||||
o += 2;
|
||||
};
|
||||
for p in m.display_primaries.iter() {
|
||||
put16(p[0]);
|
||||
put16(p[1]);
|
||||
}
|
||||
put16(m.white_point[0]);
|
||||
put16(m.white_point[1]);
|
||||
let mut put32 = |v: u32| {
|
||||
b[o..o + 4].copy_from_slice(&v.to_be_bytes());
|
||||
o += 4;
|
||||
};
|
||||
put32(m.max_display_mastering_luminance);
|
||||
put32(m.min_display_mastering_luminance);
|
||||
debug_assert_eq!(o, 24);
|
||||
b
|
||||
}
|
||||
|
||||
/// The `content_light_level_info` SEI payload (HEVC/H.264 type
|
||||
/// [`SEI_TYPE_CONTENT_LIGHT_LEVEL_INFO`]) — 4 bytes, big-endian: MaxCLL then MaxFALL.
|
||||
pub fn hevc_content_light_level_sei(m: &HdrMeta) -> [u8; 4] {
|
||||
let mut b = [0u8; 4];
|
||||
b[0..2].copy_from_slice(&m.max_cll.to_be_bytes());
|
||||
b[2..4].copy_from_slice(&m.max_fall.to_be_bytes());
|
||||
b
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn display_conversion_bt2020_1000nit() {
|
||||
// BT.2020 primaries + D65 white, a 1000-nit / 0.0001-nit mastering display.
|
||||
let m = hdr_meta_from_display(
|
||||
(0.708, 0.292), // red
|
||||
(0.170, 0.797), // green
|
||||
(0.131, 0.046), // blue
|
||||
(0.3127, 0.3290), // D65
|
||||
1000.0,
|
||||
0.0001,
|
||||
0,
|
||||
0,
|
||||
);
|
||||
// ST.2086 G, B, R order, 1/50000 units.
|
||||
assert_eq!(
|
||||
m.display_primaries,
|
||||
[[8500, 39850], [6550, 2300], [35400, 14600]]
|
||||
);
|
||||
assert_eq!(m.white_point, [15635, 16450]);
|
||||
assert_eq!(m.max_display_mastering_luminance, 10_000_000); // 1000 * 10000
|
||||
assert_eq!(m.min_display_mastering_luminance, 1); // 0.0001 * 10000
|
||||
assert_eq!((m.max_cll, m.max_fall), (0, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mastering_sei_is_24_bytes_big_endian_gbr() {
|
||||
let m = generic_hdr10();
|
||||
let p = hevc_mastering_display_sei(&m);
|
||||
assert_eq!(p.len(), 24);
|
||||
// First field = green.x = 8500 = 0x2134, big-endian.
|
||||
assert_eq!(&p[0..2], &8500u16.to_be_bytes());
|
||||
assert_eq!(&p[2..4], &39850u16.to_be_bytes()); // green.y
|
||||
assert_eq!(&p[4..6], &6550u16.to_be_bytes()); // blue.x
|
||||
assert_eq!(&p[12..14], &15635u16.to_be_bytes()); // white.x
|
||||
assert_eq!(&p[16..20], &10_000_000u32.to_be_bytes()); // max lum
|
||||
assert_eq!(&p[20..24], &1u32.to_be_bytes()); // min lum
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cll_sei_is_4_bytes_big_endian() {
|
||||
let m = generic_hdr10();
|
||||
let p = hevc_content_light_level_sei(&m);
|
||||
assert_eq!(p, [0x03, 0xE8, 0x01, 0x90]); // 1000, 400 big-endian
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vdisplay_luminance_fields_convert_units() {
|
||||
// An 800-nit / 0.05-nit panel with a 400-nit frame-average ceiling: the AddRequest fields
|
||||
// come out as whole nits / nits / MILLI-nits.
|
||||
let m = hdr_meta_from_display(
|
||||
(0.680, 0.320),
|
||||
(0.265, 0.690),
|
||||
(0.150, 0.060),
|
||||
(0.3127, 0.3290),
|
||||
800.0,
|
||||
0.05,
|
||||
0,
|
||||
400,
|
||||
);
|
||||
assert_eq!(vdisplay_luminance_fields(&m), (800, 400, 50));
|
||||
// The all-zero (unknown) volume stays all-zero — the driver keeps its EDID defaults.
|
||||
assert_eq!(vdisplay_luminance_fields(&HdrMeta::default()), (0, 0, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clamps_out_of_range() {
|
||||
let m = hdr_meta_from_display(
|
||||
(2.0, 2.0),
|
||||
(0.0, 0.0),
|
||||
(0.0, 0.0),
|
||||
(0.5, 0.5),
|
||||
-5.0,
|
||||
0.0,
|
||||
0,
|
||||
0,
|
||||
);
|
||||
assert_eq!(m.display_primaries[2], [65535, 65535]); // red clamped
|
||||
assert_eq!(m.max_display_mastering_luminance, 0); // negative → 0
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,6 @@ mod gamestream;
|
||||
#[cfg(target_os = "linux")]
|
||||
#[path = "linux/gpuclocks.rs"]
|
||||
mod gpuclocks;
|
||||
mod hdr;
|
||||
mod hooks;
|
||||
mod inject;
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -55,7 +54,6 @@ mod install;
|
||||
mod interactive;
|
||||
mod library;
|
||||
mod log_capture;
|
||||
mod metronome;
|
||||
mod mgmt;
|
||||
mod mgmt_token;
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -71,7 +69,6 @@ mod send_pacing;
|
||||
mod service;
|
||||
mod session_plan;
|
||||
mod session_status;
|
||||
mod session_tuning;
|
||||
mod spike;
|
||||
mod stats_recorder;
|
||||
mod stream_marker;
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
//! Detector for METRONOMIC event cycles — evenly-spaced disturbances repeating every few seconds.
|
||||
//!
|
||||
//! The "periodic double-jolt" symptom class field reports keep describing is a host/display-side
|
||||
//! disturbance on a stable multi-second period (display-topology churn, display-poller software,
|
||||
//! virtual-display present timing). Random network loss is bursty and irregular; a stable period is
|
||||
//! a machine, and saying so in the host log turns a "nothing in the logs :/" report into a
|
||||
//! self-diagnosis. Two feeds today: served client-recovery IDRs (`native`) and IDD-push capture
|
||||
//! stalls (`capture::windows::idd_push`).
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// Pure evenly-spaced-events detector (unit-tested below).
|
||||
///
|
||||
/// Events within [`Self::COALESCE`] count as ONE (a double-jolt's paired disturbances — e.g. the
|
||||
/// cooldown re-issue of a lost keyframe ~0.7 s after the first — are one user-visible cycle). When
|
||||
/// 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
|
||||
/// [`Self::REWARN`] while the cycle persists.
|
||||
pub(crate) struct Metronome {
|
||||
events: VecDeque<Instant>,
|
||||
last_warn: Option<Instant>,
|
||||
}
|
||||
|
||||
impl Metronome {
|
||||
/// Events closer together than this are the same user-visible disturbance.
|
||||
const COALESCE: Duration = Duration::from_millis(1500);
|
||||
/// Consecutive evenly-spaced events before the cycle counts as metronomic.
|
||||
const STREAK: usize = 4;
|
||||
/// "Evenly spaced" = every gap within this fraction of the mean gap.
|
||||
const TOLERANCE: f64 = 0.2;
|
||||
/// Once warned, re-warn at most this often while the cycle persists.
|
||||
const REWARN: Duration = Duration::from_secs(30);
|
||||
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
events: VecDeque::new(),
|
||||
last_warn: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Record a disturbance at `now`; `Some(mean period)` exactly when the metronomic-cycle
|
||||
/// warning should fire.
|
||||
pub(crate) fn note(&mut self, now: Instant) -> Option<Duration> {
|
||||
if self
|
||||
.events
|
||||
.back()
|
||||
.is_some_and(|last| now.duration_since(*last) < Self::COALESCE)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
self.events.push_back(now);
|
||||
if self.events.len() > Self::STREAK {
|
||||
self.events.pop_front();
|
||||
}
|
||||
if self.events.len() < Self::STREAK {
|
||||
return None;
|
||||
}
|
||||
let gaps: Vec<f64> = self
|
||||
.events
|
||||
.iter()
|
||||
.zip(self.events.iter().skip(1))
|
||||
.map(|(a, b)| b.duration_since(*a).as_secs_f64())
|
||||
.collect();
|
||||
let mean = gaps.iter().sum::<f64>() / gaps.len() as f64;
|
||||
if mean <= 0.0
|
||||
|| gaps
|
||||
.iter()
|
||||
.any(|g| (g - mean).abs() > mean * Self::TOLERANCE)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
if self
|
||||
.last_warn
|
||||
.is_some_and(|t| now.duration_since(t) < Self::REWARN)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
self.last_warn = Some(now);
|
||||
Some(Duration::from_secs_f64(mean))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Feed a [`Metronome`] a schedule of event offsets (ms from a common origin) and return
|
||||
/// what each `note` produced.
|
||||
fn cadence_run(offsets_ms: &[u64]) -> Vec<Option<Duration>> {
|
||||
let base = Instant::now();
|
||||
let mut c = Metronome::new();
|
||||
offsets_ms
|
||||
.iter()
|
||||
.map(|ms| c.note(base + Duration::from_millis(*ms)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cadence_detects_metronomic_events() {
|
||||
// Four events ~4 s apart (±5%) → the fourth trips the detector at ~4 s.
|
||||
let out = cadence_run(&[0, 4_000, 8_100, 11_950]);
|
||||
assert_eq!(out[..3], [None, None, None]);
|
||||
let period = out[3].expect("metronomic series must be detected");
|
||||
assert!(
|
||||
(period.as_secs_f64() - 3.98).abs() < 0.2,
|
||||
"period={period:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cadence_coalesces_double_jolt_pairs() {
|
||||
// The field signature: a jolt pair (second event ~0.7 s after the first, e.g. the IDR
|
||||
// cooldown re-issue) every ~4 s. Each pair is ONE event; detection still lands on the
|
||||
// ~4 s cycle.
|
||||
let out = cadence_run(&[
|
||||
0, 700, // pair 1
|
||||
4_000, 4_700, // pair 2
|
||||
8_000, 8_650, // pair 3
|
||||
12_000, // pair 4 (first event trips it)
|
||||
]);
|
||||
assert!(out[..6].iter().all(Option::is_none));
|
||||
let period = out[6].expect("coalesced pairs must still read as a 4 s cycle");
|
||||
assert!(
|
||||
(period.as_secs_f64() - 4.0).abs() < 0.2,
|
||||
"period={period:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cadence_ignores_irregular_bursts() {
|
||||
// Genuine Wi-Fi-style loss: irregular gaps → never flagged.
|
||||
assert!(cadence_run(&[0, 2_000, 9_000, 12_500, 21_000])
|
||||
.iter()
|
||||
.all(Option::is_none));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cadence_rewarns_at_most_every_30s() {
|
||||
// A persisting 4 s cycle: warn on the 4th event (t=12 s), then stay quiet until ≥30 s
|
||||
// past the warn — the t=44 s event (index 11) is the first at or beyond t=42 s.
|
||||
let offsets: Vec<u64> = (0..12).map(|i| i * 4_000).collect();
|
||||
let out = cadence_run(&offsets);
|
||||
let warned: Vec<usize> = out
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, o)| o.map(|_| i))
|
||||
.collect();
|
||||
assert_eq!(warned, vec![3, 11], "warn indices");
|
||||
}
|
||||
}
|
||||
@@ -42,11 +42,10 @@ use rand::RngCore;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicU8, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Per-thread OS scheduling QoS lives in its own module (plan §W1); re-exported so
|
||||
/// `crate::native::boost_thread_priority` stays stable (the GameStream path and the direct-NVENC
|
||||
/// send thread reach it there).
|
||||
mod thread_qos;
|
||||
pub(crate) use thread_qos::boost_thread_priority;
|
||||
/// Per-thread OS scheduling QoS lives in the shared `pf-frame` leaf crate (plan §W1/§W6);
|
||||
/// re-exported so `crate::native::boost_thread_priority` stays stable (the GameStream path and the
|
||||
/// native data plane reach it there).
|
||||
pub(crate) use pf_frame::thread_qos::boost_thread_priority;
|
||||
|
||||
/// Compositor-preference resolution (plan §W1); `serve_session` reaches `resolve_compositor` here.
|
||||
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
|
||||
// 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.
|
||||
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());
|
||||
tracing::info!(
|
||||
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.
|
||||
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
|
||||
// into a stable multi-second rhythm (see [`crate::metronome::Metronome`]).
|
||||
let mut recovery_cadence = crate::metronome::Metronome::new();
|
||||
// into a stable multi-second rhythm (see [`pf_frame::metronome::Metronome`]).
|
||||
let mut recovery_cadence = pf_frame::metronome::Metronome::new();
|
||||
// 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
|
||||
// with `USER_FLAG_RECOVERY_POINT` so the client can lift its post-loss freeze on a clean
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
//! Per-thread OS scheduling QoS for the native data plane (plan §W1 — carved out of the [`super`]
|
||||
//! module). 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
|
||||
//! way (`crate::native::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(crate) 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;
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
//! Windows host-process session tuning — parity with Apollo/Sunshine `streaming_will_start`.
|
||||
//!
|
||||
//! The default Windows process runs at NORMAL priority and ~15.6 ms timer granularity, and lets the
|
||||
//! GPU/display idle. Under a GPU-saturating game that starves our capture/encode/send threads (the
|
||||
//! "240→40 fps collapse"), and the coarse timer floors any precise frame pacing. This raises the
|
||||
//! process out of the default scheduling class, gives DWM and our hot threads MMCSS priority, drops
|
||||
//! the timer to 1 ms, and keeps the (virtual) display awake for the session.
|
||||
//!
|
||||
//! Raw C-ABI FFI (winmm/kernel32/dwmapi/avrt) rather than the `windows` crate so it builds without
|
||||
//! pulling new windows-rs features. No-op on non-Windows. Per-thread effects (MMCSS, execution
|
||||
//! state) auto-revert at thread exit (= session end); the process-wide bits revert at process exit.
|
||||
//! See `design/host-latency-plan.md` Tier 3A.
|
||||
|
||||
// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program).
|
||||
#![deny(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
mod imp {
|
||||
#![allow(non_snake_case)]
|
||||
use std::ffi::c_void;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
type Handle = *mut c_void;
|
||||
type Bool = i32;
|
||||
|
||||
#[link(name = "winmm")]
|
||||
extern "system" {
|
||||
fn timeBeginPeriod(uPeriod: u32) -> u32;
|
||||
}
|
||||
#[link(name = "kernel32")]
|
||||
extern "system" {
|
||||
fn GetCurrentProcess() -> Handle;
|
||||
fn SetPriorityClass(hProcess: Handle, dwPriorityClass: u32) -> Bool;
|
||||
fn SetThreadExecutionState(esFlags: u32) -> u32;
|
||||
}
|
||||
#[link(name = "dwmapi")]
|
||||
extern "system" {
|
||||
fn DwmEnableMMCSS(fEnableMMCSS: Bool) -> i32; // HRESULT
|
||||
}
|
||||
#[link(name = "avrt")]
|
||||
extern "system" {
|
||||
fn AvSetMmThreadCharacteristicsW(TaskName: *const u16, TaskIndex: *mut u32) -> Handle;
|
||||
}
|
||||
|
||||
const HIGH_PRIORITY_CLASS: u32 = 0x0000_0080;
|
||||
const ES_CONTINUOUS: u32 = 0x8000_0000;
|
||||
const ES_SYSTEM_REQUIRED: u32 = 0x0000_0001;
|
||||
const ES_DISPLAY_REQUIRED: u32 = 0x0000_0002;
|
||||
|
||||
static PROCESS_TUNED: OnceLock<()> = OnceLock::new();
|
||||
|
||||
/// Process-wide tuning, applied exactly once. Reverts at process exit. Best-effort: each call is
|
||||
/// independent and a failure is ignored (e.g. a non-elevated host may not get HIGH class).
|
||||
fn tune_process_once() {
|
||||
// SAFETY: each call is a C-ABI FFI into winmm/kernel32/dwmapi declared with a matching
|
||||
// `extern "system"` signature; every argument is a plain integer (no pointers/buffers escape),
|
||||
// and `GetCurrentProcess()` returns the current-process pseudo-handle (a constant, always valid,
|
||||
// never closed). The body runs inside `get_or_init`, so it executes exactly once per process.
|
||||
PROCESS_TUNED.get_or_init(|| unsafe {
|
||||
// 1 ms timer granularity (default ~15.6 ms) — the floor for precise frame pacing and the
|
||||
// encode|send split's sub-ms sleeps.
|
||||
timeBeginPeriod(1);
|
||||
// Run DWM's compositor work at MMCSS priority — helps the compose-rate ceiling hold up
|
||||
// under a saturating game (capture is bounded by how often DWM composes).
|
||||
DwmEnableMMCSS(1);
|
||||
// Lift the whole host above NORMAL so a CPU-saturating game can't deschedule our
|
||||
// control/capture/encode/send threads on the CPU (Apollo does the same).
|
||||
SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS);
|
||||
tracing::info!("windows session tuning applied (timer 1ms, DWM MMCSS, HIGH priority)");
|
||||
});
|
||||
}
|
||||
|
||||
/// Call at the start of each capture/encode/send (hot stream) thread. Applies the process-wide
|
||||
/// tuning once, registers the calling thread with MMCSS ("Games"), and asserts the display/system
|
||||
/// must stay awake for as long as this thread lives. The MMCSS handle is intentionally leaked and
|
||||
/// the execution-state assertion is bound to this thread — both are reverted by the OS when the
|
||||
/// thread exits, so a session that ends tears them down without explicit bookkeeping.
|
||||
pub fn on_hot_thread() {
|
||||
tune_process_once();
|
||||
// SAFETY: C-ABI FFI declared with matching `extern "system"` signatures. SetThreadExecutionState
|
||||
// takes only flag bits. `task` is a local NUL-terminated UTF-16 buffer ("Games\0") alive for the
|
||||
// whole block, so `task.as_ptr()` is a valid LPCWSTR for the call, and `&mut idx` is a live local
|
||||
// u32 the call writes the task index into. The returned MMCSS handle is intentionally leaked (the
|
||||
// OS reverts the characteristics at thread exit), so there is nothing to free or double-free.
|
||||
unsafe {
|
||||
SetThreadExecutionState(ES_CONTINUOUS | ES_DISPLAY_REQUIRED | ES_SYSTEM_REQUIRED);
|
||||
let task: Vec<u16> = "Games\0".encode_utf16().collect();
|
||||
let mut idx: u32 = 0;
|
||||
// Leak the handle: these are session/process-lifetime worker threads; the OS reverts the
|
||||
// MMCSS characteristics at thread exit.
|
||||
let _ = AvSetMmThreadCharacteristicsW(task.as_ptr(), &mut idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use imp::on_hot_thread;
|
||||
|
||||
/// No-op on non-Windows (Linux uses `setpriority` nice + CUDA stream priority instead — see
|
||||
/// `native::boost_thread_priority` and `zerocopy::cuda`).
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub fn on_hot_thread() {}
|
||||
@@ -443,7 +443,7 @@ impl VdisplayDriver for PfVdisplayDriver {
|
||||
// unknown → the driver keeps its built-in defaults (also what an un-upgraded driver, which
|
||||
// reads only the legacy 24-byte prefix, does).
|
||||
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));
|
||||
if max_luminance_nits > 0 {
|
||||
tracing::info!(
|
||||
|
||||
Reference in New Issue
Block a user