From 0ccd0fe6767f93798558accdb0205ea3d8d8beec Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Thu, 25 Jun 2026 21:27:20 +0000 Subject: [PATCH] =?UTF-8?q?feat(windows-host):=20EncoderCaps=20=E2=80=94?= =?UTF-8?q?=20query=20RFI/HDR-SEI=20caps=20(Goal-1=20stage=205,=20tighteni?= =?UTF-8?q?ng=203)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The last §2.3 seam-trait tightening: give `Encoder` a `caps() -> EncoderCaps` so the session glue routes by *query* instead of relying on the no-op/`false` defaults of `invalidate_ref_frames`/`set_hdr_meta`. `EncoderCaps { supports_rfi, supports_hdr_metadata }` is a cheap `Copy` struct. The trait gains a default `caps()` returning `EncoderCaps::default()` (all false) — correct for every SDR/libavcodec backend (Linux NVENC, VAAPI, AMF/QSV, software openh264), so they need no change. Only the Windows direct-NVENC path (`NvencD3d11Encoder`) overrides it, reporting the real `rfi_supported` (probed once at open via `nvEncGetEncodeCaps`) and `hdr` (HDR-SEI on keyframes). Consumer: the GameStream encode loop (`gamestream/stream.rs`) hoists `supports_rfi` once before the loop and gates the loss-recovery path on it — `!(supports_rfi && enc.invalidate_ref_frames(..))` forces a keyframe directly on non-RFI encoders instead of making an always-`false` call every loss event. Behaviour-preserving (same keyframe/RFI outcome), one fewer no-op call, intent explicit. The native host (punktfunk1) uses FEC+keyframes, no RFI consumer. Linux `cargo clippy -p punktfunk-host --all-targets -D warnings` clean; the three edited files are rustfmt-clean. The NVENC override is Windows-only (1:1 with the existing impl style) → CI/on-glass gate. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/punktfunk-host/src/encode.rs | 25 +++++++++++++++++++ .../src/encode/windows/nvenc.rs | 11 +++++++- .../punktfunk-host/src/gamestream/stream.rs | 8 +++++- 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/crates/punktfunk-host/src/encode.rs b/crates/punktfunk-host/src/encode.rs index a493b14..f0e1a5e 100644 --- a/crates/punktfunk-host/src/encode.rs +++ b/crates/punktfunk-host/src/encode.rs @@ -71,9 +71,34 @@ impl Codec { } } +/// Static capabilities an [`Encoder`] declares so the session glue routes loss-recovery and HDR +/// plumbing by *query* rather than relying on a method's no-op/`false` default. Cheap `Copy`; fixed +/// for the session (an HDR toggle re-initialises the encoder — re-query if that matters). +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct EncoderCaps { + /// The encoder can perform real reference-frame invalidation — i.e. + /// [`invalidate_ref_frames`](Encoder::invalidate_ref_frames) can return `true`. When `false` + /// the caller skips that always-`false` call and forces a keyframe directly on loss recovery. + /// Only the Windows direct-NVENC path implements RFI; libavcodec (Linux NVENC), VAAPI and + /// AMF/QSV always keyframe. + pub supports_rfi: bool, + /// The encoder emits in-band HDR mastering/CLL SEI from [`set_hdr_meta`](Encoder::set_hdr_meta). + /// When `false`, `set_hdr_meta` is a no-op and no in-band grade reaches the client. Only the + /// Windows direct-NVENC path attaches it today. + pub supports_hdr_metadata: bool, +} + /// A hardware encoder. One per session; runs on the encode thread. pub trait Encoder: Send { fn submit(&mut self, frame: &CapturedFrame) -> Result<()>; + /// This encoder's static [capabilities](EncoderCaps) (RFI, HDR SEI), so the session glue can + /// route by query rather than rely on the no-op/`false` defaults of + /// [`invalidate_ref_frames`](Self::invalidate_ref_frames) / [`set_hdr_meta`](Self::set_hdr_meta). + /// Default: no optional capabilities (the SDR / libavcodec backends) — only the direct-NVENC + /// path overrides it. + fn caps(&self) -> EncoderCaps { + EncoderCaps::default() + } /// Force the next submitted frame to be an IDR keyframe (e.g. after a client /// reference-frame-invalidation request). Default: no-op. fn request_keyframe(&mut self) {} diff --git a/crates/punktfunk-host/src/encode/windows/nvenc.rs b/crates/punktfunk-host/src/encode/windows/nvenc.rs index 8876a61..6103be9 100644 --- a/crates/punktfunk-host/src/encode/windows/nvenc.rs +++ b/crates/punktfunk-host/src/encode/windows/nvenc.rs @@ -13,7 +13,7 @@ //! Needs a real NVIDIA GPU at runtime (session creation fails otherwise) — compiles GPU-less, but //! `open`/`submit` only succeed on a GPU box. The software encoder (`super::sw`) is the fallback. -use super::{Codec, EncodedFrame, Encoder}; +use super::{Codec, EncodedFrame, Encoder, EncoderCaps}; use crate::capture::{CapturedFrame, FramePayload, PixelFormat}; use anyhow::{anyhow, bail, Context, Result}; use std::collections::{HashMap, VecDeque}; @@ -732,6 +732,15 @@ impl Encoder for NvencD3d11Encoder { self.force_kf = true; } + fn caps(&self) -> EncoderCaps { + // RFI is probed once at open (`rfi_supported`); HDR SEI rides keyframes whenever the + // session is in HDR mode. Both are the real capabilities the session glue routes on. + EncoderCaps { + supports_rfi: self.rfi_supported, + supports_hdr_metadata: self.hdr, + } + } + fn set_hdr_meta(&mut self, meta: Option) { // Stored and emitted as in-band SEI on the next keyframe (see `submit`). Cheap to call every // frame; only changes when the source is regraded or HDR toggles. diff --git a/crates/punktfunk-host/src/gamestream/stream.rs b/crates/punktfunk-host/src/gamestream/stream.rs index a4bb1c5..eb41bcd 100644 --- a/crates/punktfunk-host/src/gamestream/stream.rs +++ b/crates/punktfunk-host/src/gamestream/stream.rs @@ -367,6 +367,10 @@ fn stream_body( (0u128, 0u128, 0u128, 0u128, 0usize, 0u32); // Absolute next-frame deadline — the single pacing clock for the loop. let mut next_frame = Instant::now(); + // RFI capability is fixed for the session (probed at encoder open). Query it once so the + // recovery path skips the always-`false` invalidate call on encoders without NVENC RFI and + // forces a keyframe directly instead. + let supports_rfi = enc.caps().supports_rfi; while running.load(Ordering::SeqCst) { let tick = Instant::now(); @@ -380,7 +384,9 @@ fn stream_body( // re-references an older still-valid frame — no costly IDR spike); if the encoder can't // invalidate (range too old, or no NVENC RFI) it returns false and we force a keyframe. if let Some((first, last)) = rfi_range.lock().unwrap().take() { - if !enc.invalidate_ref_frames(first, last) { + // Prefer reference-frame invalidation when the encoder supports it (no costly IDR + // spike); otherwise — or if the range is too old to invalidate — force a keyframe. + if !(supports_rfi && enc.invalidate_ref_frames(first, last)) { enc.request_keyframe(); } }