From c6caeca5bcb92375612fb9e35380779172064fb8 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Mon, 20 Jul 2026 23:19:46 +0200 Subject: [PATCH] =?UTF-8?q?feat(encode):=20RGB-direct=20(EFC)=20is=20now?= =?UTF-8?q?=20the=20DEFAULT=20on=20capable=20hosts=20=E2=80=94=20gated=20b?= =?UTF-8?q?y=20a=20session=20cursor-blend=20hint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit B2 default-on (design/vulkan-rgb-direct-encode.md): wherever the probe passes, Vulkan Video sessions now take the EFC RGB source by default — direct import on aligned modes, padded-copy staging on unaligned ones (1080p). PUNKTFUNK_VULKAN_RGB_DIRECT becomes the override: =0 disables, =1 forces, unset = the default below. The one session class that must NOT default on: cursor-as-metadata captures (every non-gamescope compositor), where the CSC shader's blend IS the visible pointer — the EFC cannot composite, and defaulting there would silently drop the cursor from the stream. The hint rides the existing plumbing: - SessionPlan gains cursor_blend, resolved once where the compositor is known (gamescope embeds the pointer itself → false; kwin/mutter/ wlroots/hyprland → true), and shows up in the logged plan line. - open_video/open_video_backend thread it through (native pump: all three encoder-open sites read plan.cursor_blend; GameStream monitor capture: true — it negotiates metadata cursor; spike: false). - VulkanVideoEncoder::open resolves: env override, else ON iff the session never hands us cursor bitmaps. The warn-once for a cursor on an RGB session (forced via =1) stays. Verified on-hw box (Linux): pf-encode + punktfunk-host compile, clippy clean, unit suite green. The GPU paths themselves are unchanged from the smoke-validated 96e19986 — this commit only changes which sessions select them by default. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../pf-encode/src/enc/linux/vulkan_video.rs | 41 ++++++++++++++----- crates/pf-encode/src/lib.rs | 27 ++++++++++-- .../punktfunk-host/src/gamestream/stream.rs | 4 ++ crates/punktfunk-host/src/native/stream.rs | 17 +++++++- crates/punktfunk-host/src/session_plan.rs | 7 ++++ crates/punktfunk-host/src/spike.rs | 1 + 6 files changed, 81 insertions(+), 16 deletions(-) diff --git a/crates/pf-encode/src/enc/linux/vulkan_video.rs b/crates/pf-encode/src/enc/linux/vulkan_video.rs index 6d052c69..79cd9834 100644 --- a/crates/pf-encode/src/enc/linux/vulkan_video.rs +++ b/crates/pf-encode/src/enc/linux/vulkan_video.rs @@ -69,12 +69,19 @@ fn quality_request() -> u32 { .unwrap_or(0) } -/// `PUNKTFUNK_VULKAN_RGB_DIRECT=1` opts into the RGB-direct encode source (B1, -/// design/vulkan-rgb-direct-encode.md): the captured RGB frame is handed to the encoder as-is -/// and the VCN EFC front-end does the 709-narrow CSC inline — no compute CSC, no plane copies, -/// one queue submit per frame. Default OFF until the color/latency A/B graduates it (B2). -fn rgb_request() -> bool { - std::env::var("PUNKTFUNK_VULKAN_RGB_DIRECT").is_ok_and(|v| v == "1") +/// `PUNKTFUNK_VULKAN_RGB_DIRECT` override for the RGB-direct encode source +/// (design/vulkan-rgb-direct-encode.md): the captured RGB frame is the encode source and the +/// VCN EFC front-end does the 709-narrow CSC inline — no compute CSC, no plane copies, one +/// queue submit per frame (unaligned modes go through the padded-copy staging blit). B2 +/// default: ON wherever the probe passes, EXCEPT sessions that may need the CSC's cursor +/// blend (see [`VulkanVideoEncoder::open`]). `=0` disables outright; `=1` forces it even on +/// cursor-blend sessions (the pointer will be missing from the stream); unset = the default. +fn rgb_request() -> Option { + match std::env::var("PUNKTFUNK_VULKAN_RGB_DIRECT") { + Ok(v) if v == "0" => Some(false), + Ok(_) => Some(true), + Err(_) => None, + } } /// Live RGB-direct session config: the chroma-siting bits the session was created with @@ -408,9 +415,21 @@ pub struct VulkanVideoEncoder { unsafe impl Send for VulkanVideoEncoder {} impl VulkanVideoEncoder { - /// Signature mirrors the other Linux backends' `open` (see `nvenc_cuda::NvencCudaEncoder::open`). - pub fn open(codec: Codec, width: u32, height: u32, fps: u32, bitrate_bps: u64) -> Result { - Self::open_opts(codec, width, height, fps, bitrate_bps, rgb_request()) + /// Signature mirrors the other Linux backends' `open` plus `cursor_blend`: the session may + /// hand this encoder cursor bitmaps to composite (cursor-as-metadata captures — every + /// non-gamescope compositor). The EFC cannot blend, so such sessions default to the CSC + /// path; everywhere else the RGB-direct source is the DEFAULT wherever the probe passes + /// (B2). `PUNKTFUNK_VULKAN_RGB_DIRECT` overrides both ways (see [`rgb_request`]). + pub fn open( + codec: Codec, + width: u32, + height: u32, + fps: u32, + bitrate_bps: u64, + cursor_blend: bool, + ) -> Result { + let want_rgb = rgb_request().unwrap_or(!cursor_blend); + Self::open_opts(codec, width, height, fps, bitrate_bps, want_rgb) } /// `open` with the RGB-direct request explicit instead of read from the env — the smoke @@ -563,7 +582,9 @@ impl VulkanVideoEncoder { (_, _, Some(RgbDirect { padded: true, .. })) => "active(padded-copy: mode is not 64x16-aligned — staging blit + edge \ duplication instead of the direct import)", - (Ok(_), false, None) => "available(off; set PUNKTFUNK_VULKAN_RGB_DIRECT=1)", + (Ok(_), false, None) => + "available(off: PUNKTFUNK_VULKAN_RGB_DIRECT=0, or a cursor-blend session \ + — =1 forces)", (Err(e), _, None) => e, // (Ok, wanted) always builds Some above. (Ok(_), true, None) => unreachable!("rgb gate and cfg disagree"), diff --git a/crates/pf-encode/src/lib.rs b/crates/pf-encode/src/lib.rs index 2f4a624d..11cf364a 100644 --- a/crates/pf-encode/src/lib.rs +++ b/crates/pf-encode/src/lib.rs @@ -129,6 +129,9 @@ pub fn open_video( cuda: bool, bit_depth: u8, chroma: ChromaFormat, + // The session may hand this encoder cursor bitmaps to composite (cursor-as-metadata + // captures). Backends whose fast path can't blend (Vulkan EFC RGB-direct) key off it. + cursor_blend: bool, ) -> Result> { let (inner, backend) = open_video_backend( codec, @@ -140,6 +143,7 @@ pub fn open_video( cuda, bit_depth, chroma, + cursor_blend, )?; // Record what this session encodes on (the mgmt API's "currently used GPU"): the backend label // is reported by `open_video_backend` from the branch that ACTUALLY opened — not re-derived by @@ -264,7 +268,9 @@ fn open_video_backend( cuda: bool, bit_depth: u8, chroma: ChromaFormat, + cursor_blend: bool, ) -> Result<(Box, &'static str)> { + let _ = cursor_blend; // consumed only by the Linux vulkan-encode arm below validate_dimensions(codec, width, height)?; // Refresh/fps must be positive and sane: fps feeds the encoder time_base (`Rational(1, fps)`) // and the pts→ns conversion (`pts * 1e9 / fps`), so 0 builds a 1/0 rational / divides by zero. @@ -322,8 +328,14 @@ fn open_video_backend( && vulkan_encode_enabled() && !(bit_depth == 10 && format.is_hdr_rgb10()) { - match vulkan_video::VulkanVideoEncoder::open(codec, width, height, fps, bitrate_bps) - { + match vulkan_video::VulkanVideoEncoder::open( + codec, + width, + height, + fps, + bitrate_bps, + cursor_blend, + ) { Ok(e) => { tracing::info!( codec = ?codec, @@ -376,8 +388,15 @@ fn open_video_backend( "the Vulkan Video encoder supports HEVC + AV1; the session negotiated {codec:?}" ); } - vulkan_video::VulkanVideoEncoder::open(codec, width, height, fps, bitrate_bps) - .map(|e| (Box::new(e) as Box, "vulkan")) + vulkan_video::VulkanVideoEncoder::open( + codec, + width, + height, + fps, + bitrate_bps, + cursor_blend, + ) + .map(|e| (Box::new(e) as Box, "vulkan")) } #[cfg(not(feature = "vulkan-encode"))] { diff --git a/crates/punktfunk-host/src/gamestream/stream.rs b/crates/punktfunk-host/src/gamestream/stream.rs index fe266580..a43a6eb0 100644 --- a/crates/punktfunk-host/src/gamestream/stream.rs +++ b/crates/punktfunk-host/src/gamestream/stream.rs @@ -658,6 +658,9 @@ fn stream_body( // GameStream/Moonlight stays 4:2:0 — stock Moonlight clients can't decode 4:4:4, and the // Windows IDD-push capturer can't yet deliver full-chroma frames. 4:4:4 is punktfunk/1-native only. encode::ChromaFormat::Yuv420, + // Desktop monitor capture negotiates cursor-as-metadata where available — the encoder + // may be handed cursor bitmaps to composite. + true, ) .context("open video encoder for stream")?; // FEC overhead percent (Sunshine default 20). Override with PUNKTFUNK_FEC_PCT (0 = data-only). @@ -811,6 +814,7 @@ fn stream_body( frame.is_cuda(), gs_bit_depth(frame.format), encode::ChromaFormat::Yuv420, // GameStream stays 4:2:0 + true, // metadata-cursor capture — see the first open ) .context("reopen encoder after rebuild")?; supports_rfi = enc.caps().supports_rfi; diff --git a/crates/punktfunk-host/src/native/stream.rs b/crates/punktfunk-host/src/native/stream.rs index 4f4986ce..9c5ce530 100644 --- a/crates/punktfunk-host/src/native/stream.rs +++ b/crates/punktfunk-host/src/native/stream.rs @@ -983,7 +983,12 @@ pub(super) fn virtual_stream(ctx: SessionContext, prepared: Option { tracing::info!( @@ -2551,6 +2557,7 @@ fn try_inplace_resize( new_frame.is_cuda(), bit_depth, plan.chroma, + plan.cursor_blend, ) { Ok(e) => e, Err(e) => { @@ -2619,7 +2626,12 @@ pub(super) fn prepare_display( // Same plan resolution as `virtual_stream` (pure in these inputs + host config), including // PyroWave's datagram-aligned wire mode — `Session::shard_payload()` echoes the negotiated // Welcome value passed here. - let mut plan = crate::session_plan::SessionPlan::resolve(bit_depth, chroma, codec); + let mut plan = crate::session_plan::SessionPlan::resolve( + bit_depth, + chroma, + codec, + compositor != pf_vdisplay::Compositor::Gamescope, + ); if codec == crate::encode::Codec::PyroWave { plan.wire_chunk = Some(shard_payload as usize); } @@ -2871,6 +2883,7 @@ fn build_pipeline( frame.is_cuda(), bit_depth, plan.chroma, + plan.cursor_blend, ) .context("open video encoder")?; if let Some(t) = trace { diff --git a/crates/punktfunk-host/src/session_plan.rs b/crates/punktfunk-host/src/session_plan.rs index 07471e3b..8873953f 100644 --- a/crates/punktfunk-host/src/session_plan.rs +++ b/crates/punktfunk-host/src/session_plan.rs @@ -103,6 +103,11 @@ pub struct SessionPlan { /// PyroWave session — applied to EVERY encoder this plan opens (initial + all rebuilds) so /// AUs stay shard-aligned across mode/bitrate/stall rebuilds. `None` for the H.26x codecs. pub wire_chunk: Option, + /// The session may hand the encoder cursor bitmaps to composite (cursor-as-metadata + /// captures — every non-gamescope compositor; gamescope embeds the pointer itself). + /// Encoders whose fast path cannot blend (the Vulkan EFC RGB-direct source) stay on their + /// blending path when this is set, so the pointer never silently vanishes from the stream. + pub cursor_blend: bool, } impl SessionPlan { @@ -112,6 +117,7 @@ impl SessionPlan { bit_depth: u8, chroma: crate::encode::ChromaFormat, codec: crate::encode::Codec, + cursor_blend: bool, ) -> Self { SessionPlan { capture: CaptureBackend::resolve(), @@ -122,6 +128,7 @@ impl SessionPlan { chroma, codec, wire_chunk: None, + cursor_blend, } } diff --git a/crates/punktfunk-host/src/spike.rs b/crates/punktfunk-host/src/spike.rs index 369ca24d..1fed9270 100644 --- a/crates/punktfunk-host/src/spike.rs +++ b/crates/punktfunk-host/src/spike.rs @@ -147,6 +147,7 @@ pub fn run(opts: Options) -> Result<()> { first.is_cuda(), 8, // spike synthetic harness: 8-bit encode::ChromaFormat::Yuv420, // ...and 4:2:0 + false, // synthetic frames carry no cursor ) .context("open encoder")?;