From b168790e0a6e900543fd67e21da9ed37fa3e7730 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Fri, 17 Jul 2026 10:03:56 +0200 Subject: [PATCH] refactor(host/W6.2): extract the shared frame/format vocabulary into the pf-frame leaf crate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- Cargo.lock | 13 + Cargo.toml | 1 + crates/pf-frame/Cargo.toml | 37 +++ crates/pf-frame/src/dxgi.rs | 217 +++++++++++++++++ .../{punktfunk-host => pf-frame}/src/hdr.rs | 0 crates/pf-frame/src/lib.rs | 230 ++++++++++++++++++ .../src/metronome.rs | 7 +- .../src/session_tuning.rs | 0 .../src/native => pf-frame/src}/thread_qos.rs | 10 +- crates/punktfunk-host/Cargo.toml | 3 + crates/punktfunk-host/src/capture.rs | 211 +--------------- .../src/capture/windows/dxgi.rs | 220 ++--------------- .../src/capture/windows/idd_push.rs | 2 +- .../src/capture/windows/idd_push/stall.rs | 8 +- .../src/encode/linux/nvenc_cuda.rs | 8 +- .../src/encode/windows/nvenc.rs | 10 +- .../punktfunk-host/src/gamestream/stream.rs | 2 +- crates/punktfunk-host/src/main.rs | 3 - crates/punktfunk-host/src/native.rs | 13 +- crates/punktfunk-host/src/native/stream.rs | 4 +- .../src/vdisplay/windows/pf_vdisplay.rs | 2 +- 21 files changed, 555 insertions(+), 446 deletions(-) create mode 100644 crates/pf-frame/Cargo.toml create mode 100644 crates/pf-frame/src/dxgi.rs rename crates/{punktfunk-host => pf-frame}/src/hdr.rs (100%) create mode 100644 crates/pf-frame/src/lib.rs rename crates/{punktfunk-host => pf-frame}/src/metronome.rs (97%) rename crates/{punktfunk-host => pf-frame}/src/session_tuning.rs (100%) rename crates/{punktfunk-host/src/native => pf-frame/src}/thread_qos.rs (90%) diff --git a/Cargo.lock b/Cargo.lock index 7636fa50..37e0ea4e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2808,6 +2808,18 @@ dependencies = [ "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]] name = "pf-gpu" version = "0.12.0" @@ -3161,6 +3173,7 @@ dependencies = [ "opus", "parking_lot", "pf-driver-proto", + "pf-frame", "pf-gpu", "pf-host-config", "pf-paths", diff --git a/Cargo.toml b/Cargo.toml index 2cd5170b..926c8d6e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ members = [ "crates/pf-host-config", "crates/pf-gpu", "crates/pf-zerocopy", + "crates/pf-frame", "crates/pyrowave-sys", "clients/probe", "clients/linux", diff --git a/crates/pf-frame/Cargo.toml b/crates/pf-frame/Cargo.toml new file mode 100644 index 00000000..0084883f --- /dev/null +++ b/crates/pf-frame/Cargo.toml @@ -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", +] } diff --git a/crates/pf-frame/src/dxgi.rs b/crates/pf-frame/src/dxgi.rs new file mode 100644 index 00000000..7d0993a3 --- /dev/null +++ b/crates/pf-frame/src/dxgi.rs @@ -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 = None; + let mut context: Option = 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::() { + // 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::() { + 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 { + 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 { + 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"), + } + }); +} diff --git a/crates/punktfunk-host/src/hdr.rs b/crates/pf-frame/src/hdr.rs similarity index 100% rename from crates/punktfunk-host/src/hdr.rs rename to crates/pf-frame/src/hdr.rs diff --git a/crates/pf-frame/src/lib.rs b/crates/pf-frame/src/lib.rs new file mode 100644 index 00000000..0162e4c9 --- /dev/null +++ b/crates/pf-frame/src/lib.rs @@ -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 { + 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>, + /// 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, +} + +/// 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), + /// 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 + } + } +} diff --git a/crates/punktfunk-host/src/metronome.rs b/crates/pf-frame/src/metronome.rs similarity index 97% rename from crates/punktfunk-host/src/metronome.rs rename to crates/pf-frame/src/metronome.rs index c51a5d46..332cb834 100644 --- a/crates/punktfunk-host/src/metronome.rs +++ b/crates/pf-frame/src/metronome.rs @@ -17,7 +17,8 @@ use std::time::{Duration, Instant}; /// 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 { +#[derive(Default)] +pub struct Metronome { events: VecDeque, last_warn: Option, } @@ -32,7 +33,7 @@ impl Metronome { /// Once warned, re-warn at most this often while the cycle persists. const REWARN: Duration = Duration::from_secs(30); - pub(crate) fn new() -> Self { + pub fn new() -> Self { Self { events: VecDeque::new(), last_warn: None, @@ -41,7 +42,7 @@ impl Metronome { /// 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 { + pub fn note(&mut self, now: Instant) -> Option { if self .events .back() diff --git a/crates/punktfunk-host/src/session_tuning.rs b/crates/pf-frame/src/session_tuning.rs similarity index 100% rename from crates/punktfunk-host/src/session_tuning.rs rename to crates/pf-frame/src/session_tuning.rs diff --git a/crates/punktfunk-host/src/native/thread_qos.rs b/crates/pf-frame/src/thread_qos.rs similarity index 90% rename from crates/punktfunk-host/src/native/thread_qos.rs rename to crates/pf-frame/src/thread_qos.rs index b4cf0d1c..bc7ba85c 100644 --- a/crates/punktfunk-host/src/native/thread_qos.rs +++ b/crates/pf-frame/src/thread_qos.rs @@ -1,7 +1,7 @@ -//! 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`). +//! 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)] @@ -14,7 +14,7 @@ /// 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) { +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). diff --git a/crates/punktfunk-host/Cargo.toml b/crates/punktfunk-host/Cargo.toml index 3e85f8fb..7586f12c 100644 --- a/crates/punktfunk-host/Cargo.toml +++ b/crates/punktfunk-host/Cargo.toml @@ -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" diff --git a/crates/punktfunk-host/src/capture.rs b/crates/punktfunk-host/src/capture.rs index 1f226618..e5351f09 100644 --- a/crates/punktfunk-host/src/capture.rs +++ b/crates/punktfunk-host/src/capture.rs @@ -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 { - 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>, - /// 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, -} - -/// 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), - /// 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). diff --git a/crates/punktfunk-host/src/capture/windows/dxgi.rs b/crates/punktfunk-host/src/capture/windows/dxgi.rs index 3b2c2057..6629d606 100644 --- a/crates/punktfunk-host/src/capture/windows/dxgi.rs +++ b/crates/punktfunk-host/src/capture/windows/dxgi.rs @@ -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 = None; - let mut context: Option = 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::() { - // 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::() { - 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 { - 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 { - 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 diff --git a/crates/punktfunk-host/src/capture/windows/idd_push.rs b/crates/punktfunk-host/src/capture/windows/idd_push.rs index f81fcddc..b878720d 100644 --- a/crates/punktfunk-host/src/capture/windows/idd_push.rs +++ b/crates/punktfunk-host/src/capture/windows/idd_push.rs @@ -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 { diff --git a/crates/punktfunk-host/src/capture/windows/idd_push/stall.rs b/crates/punktfunk-host/src/capture/windows/idd_push/stall.rs index 42e16fd6..8591e1be 100644 --- a/crates/punktfunk-host/src/capture/windows/idd_push/stall.rs +++ b/crates/punktfunk-host/src/capture/windows/idd_push/stall.rs @@ -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, } @@ -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, - 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(), } } diff --git a/crates/punktfunk-host/src/encode/linux/nvenc_cuda.rs b/crates/punktfunk-host/src/encode/linux/nvenc_cuda.rs index 16765cf7..dd35c0ea 100644 --- a/crates/punktfunk-host/src/encode/linux/nvenc_cuda.rs +++ b/crates/punktfunk-host/src/encode/linux/nvenc_cuda.rs @@ -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 = 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, }); } diff --git a/crates/punktfunk-host/src/encode/windows/nvenc.rs b/crates/punktfunk-host/src/encode/windows/nvenc.rs index e130aad5..a53e96b8 100644 --- a/crates/punktfunk-host/src/encode/windows/nvenc.rs +++ b/crates/punktfunk-host/src/encode/windows/nvenc.rs @@ -321,7 +321,7 @@ fn retrieve_loop( work_rx: mpsc::Receiver, done_tx: mpsc::Sender, ) { - 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 = 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, }); } diff --git a/crates/punktfunk-host/src/gamestream/stream.rs b/crates/punktfunk-host/src/gamestream/stream.rs index 3731d2fa..0a15b979 100644 --- a/crates/punktfunk-host/src/gamestream/stream.rs +++ b/crates/punktfunk-host/src/gamestream/stream.rs @@ -126,7 +126,7 @@ fn run( stats: &Arc, ) -> 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")?; diff --git a/crates/punktfunk-host/src/main.rs b/crates/punktfunk-host/src/main.rs index 68865a81..189bf1e4 100644 --- a/crates/punktfunk-host/src/main.rs +++ b/crates/punktfunk-host/src/main.rs @@ -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; diff --git a/crates/punktfunk-host/src/native.rs b/crates/punktfunk-host/src/native.rs index 537c5423..5982a3c2 100644 --- a/crates/punktfunk-host/src/native.rs +++ b/crates/punktfunk-host/src/native.rs @@ -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(), diff --git a/crates/punktfunk-host/src/native/stream.rs b/crates/punktfunk-host/src/native/stream.rs index 1cb08db5..f1132a06 100644 --- a/crates/punktfunk-host/src/native/stream.rs +++ b/crates/punktfunk-host/src/native/stream.rs @@ -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 = 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 diff --git a/crates/punktfunk-host/src/vdisplay/windows/pf_vdisplay.rs b/crates/punktfunk-host/src/vdisplay/windows/pf_vdisplay.rs index 601ae1cd..a34026dc 100644 --- a/crates/punktfunk-host/src/vdisplay/windows/pf_vdisplay.rs +++ b/crates/punktfunk-host/src/vdisplay/windows/pf_vdisplay.rs @@ -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!(