fix(host/encode): negotiate the cursor around what the encoder can blend
EncoderCaps::blends_cursor's contract said the HOST must fall back to capturer-side compositing when a cursor-as-metadata session lands on an encoder that can't composite — but that host half was never built: open_video warned and the session streamed WITHOUT a pointer (confirmed on the VAAPI dmabuf and libav-NVENC CUDA paths; latent on vulkan RGB-direct/native-NV12). The negotiation is now caps-aware, ahead of capture, on both planes: * pf-encode grows cursor_blend_capable() — the pre-open dispatch mirror (sibling of linux_native_nv12_ok) answering whether the resolved backend composites frame.cursor; its pure core is test-pinned arm by arm. * Native plane: handshake::cursor_forward grants the cursor channel only where the resolved backend can blend (the capture-mouse flip makes the host draw the pointer on demand); denied sessions keep the pre-channel path — the compositor EMBEDS the pointer, never cursorless, never doubled. The Welcome's HOST_CAP_CURSOR bit is computed once and read back at both session-wiring sites instead of recomputed. SessionPlan::output_format additionally keeps every cursor-blend session off producer-native NV12 (the arm with no CSC to fold a cursor into), and vulkan RGB-direct now yields to a cursor-blend session even when pinned (EFC cannot composite; the open logs the override). Windows plans cursor_blend=false via the new shared cursor_blend_for() rule — the IDD capturer composites the pointer itself, and asking the encoder anyway fired the blends-cursor warn spuriously on every cursor-channel session. * GameStream plane: the hardcoded cursor_blend=true is gone. The portal source asks for cursor-as-metadata only when the resolved backend blends, otherwise negotiates an Embedded pointer (choose_cursor_mode's new ladder); the capturer pool now also keys on that mode. The virtual-output source passes false — its capture embeds the pointer where it can. The per-arm warns in vulkan_video (RGB-direct, native-NV12) are now structurally unreachable and removed. open_video's post-open check stays as the single backstop for what planning cannot see: a Vulkan-open falling back to VAAPI mid-session, and the gamescope residual (no embedded mode exists there, so a never-blending backend — H.264-on-AMD VAAPI, software — still streams cursorless; fixing that needs a compositing stage, deliberately not built in this pass). Zero-copy is preserved throughout — every fallback is a capture-negotiation change, never a readback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -307,8 +307,11 @@ pub struct EncoderCaps {
|
||||
///
|
||||
/// This makes the answer queryable instead of assumed. It is deliberately a plain fact about the
|
||||
/// encoder, not a policy: what to DO when a session wants blending and the backend cannot is the
|
||||
/// host's call, since only the host can re-plan capture (fall back to capturer-side compositing).
|
||||
/// `open_video` can only warn, which it does.
|
||||
/// host's call, since only the host can re-plan capture. That call is wired now — the
|
||||
/// negotiation consults the pre-open mirror ([`cursor_blend_capable`](crate::cursor_blend_capable))
|
||||
/// to gate the cursor channel and to keep capture on embedded-cursor / CSC-capable shapes for
|
||||
/// any backend that can't blend; `open_video`'s post-open check remains as the backstop for
|
||||
/// open-time fallbacks the plan can't see.
|
||||
pub blends_cursor: bool,
|
||||
}
|
||||
|
||||
|
||||
@@ -76,9 +76,11 @@ fn quality_request() -> u32 {
|
||||
/// (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.
|
||||
/// default: ON wherever the probe passes, EXCEPT sessions that need the CSC's cursor blend
|
||||
/// (see [`VulkanVideoEncoder::open`]). `=0` disables outright; `=1` forces it on non-cursor
|
||||
/// sessions; unset = the default. A cursor-blend session IGNORES `=1` — EFC cannot composite
|
||||
/// the pointer, and the caps-aware negotiation promised the client a composited one, so the
|
||||
/// pointer outranks the lab pin (the open logs the override).
|
||||
///
|
||||
/// Parses like every sibling knob (`matches!(v.trim(), …)`): anything unrecognised — including
|
||||
/// an empty value and a value that is only whitespace — falls back to the default rather than
|
||||
@@ -614,10 +616,6 @@ pub struct VulkanVideoEncoder {
|
||||
/// GPU reset (those paths keep their aligned-size sources/staging).
|
||||
native_nv12: bool,
|
||||
|
||||
/// One-shot warning latch: a cursor bitmap arrived on an RGB-direct or native-NV12 session
|
||||
/// (neither has a compositing stage — the cursor will be missing from the stream until the
|
||||
/// CSC path is used).
|
||||
warned_cursor: bool,
|
||||
/// A [`reconfigure_bitrate`](Encoder::reconfigure_bitrate) rate not yet installed in the video
|
||||
/// session. The next `record_submit` emits an `ENCODE_RATE_CONTROL` control command carrying it
|
||||
/// (mid-stream) or folds it into the first frame's RESET+RC install, then promotes it into
|
||||
@@ -671,7 +669,16 @@ impl VulkanVideoEncoder {
|
||||
cursor_blend: bool,
|
||||
) -> Result<Self> {
|
||||
let native_nv12 = format == PixelFormat::Nv12;
|
||||
let want_rgb = !native_nv12 && rgb_request().unwrap_or(!cursor_blend);
|
||||
// A cursor-blend session must keep the compute-CSC path — the only arm with the cursor
|
||||
// blend — so it outranks an explicit RGB-direct pin (EFC cannot composite; the
|
||||
// negotiation promised the client a composited pointer).
|
||||
if cursor_blend && rgb_request() == Some(true) {
|
||||
tracing::info!(
|
||||
"PUNKTFUNK_VULKAN_RGB_DIRECT=1 ignored for this session — it composites the \
|
||||
pointer, which the EFC front-end cannot; using the compute-CSC path"
|
||||
);
|
||||
}
|
||||
let want_rgb = !native_nv12 && !cursor_blend && rgb_request().unwrap_or(true);
|
||||
Self::open_opts_inner(
|
||||
codec,
|
||||
width,
|
||||
@@ -1448,7 +1455,6 @@ impl VulkanVideoEncoder {
|
||||
cpu_expand: Vec::new(),
|
||||
rgb: rgb_cfg,
|
||||
native_nv12,
|
||||
warned_cursor: false,
|
||||
pending_bitrate: None,
|
||||
width: w,
|
||||
height: h,
|
||||
@@ -2621,17 +2627,10 @@ impl VulkanVideoEncoder {
|
||||
d.modifier
|
||||
);
|
||||
}
|
||||
// No compositing stage exists here (like RGB-direct/EFC): gamescope embeds its pointer
|
||||
// in the produced pixels, but any other NV12 producer's metadata cursor would be lost —
|
||||
// say so once instead of silently.
|
||||
if frame.cursor.is_some() && !self.warned_cursor {
|
||||
self.warned_cursor = true;
|
||||
tracing::warn!(
|
||||
"cursor bitmap on a native-NV12 session — nothing composites it; the cursor \
|
||||
will be missing from the stream (unset PUNKTFUNK_PIPEWIRE_NV12 for \
|
||||
metadata-cursor captures)"
|
||||
);
|
||||
}
|
||||
// No compositing stage exists here (like RGB-direct/EFC) — and none is needed: the
|
||||
// session plan negotiates native NV12 only for a non-cursor-blend session
|
||||
// (`SessionPlan::output_format` gates `nv12_native` on `!cursor_blend`), so no cursor
|
||||
// bitmap ever reaches this arm. Gamescope embeds its pointer in the produced pixels.
|
||||
let dev = self.device.clone();
|
||||
let cmd = self.frames[slot].cmd;
|
||||
let fence = self.frames[slot].fence;
|
||||
@@ -2691,16 +2690,9 @@ impl VulkanVideoEncoder {
|
||||
let query_pool = self.frames[slot].query_pool;
|
||||
let bs_buf = self.frames[slot].bs_buf;
|
||||
let ts_pool = self.frames[slot].ts_pool;
|
||||
// EFC cannot composite the cursor bitmap the metadata-cursor captures hand us — say so
|
||||
// once instead of silently losing the pointer (gamescope, the flagship, embeds it).
|
||||
if frame.cursor.is_some() && !self.warned_cursor {
|
||||
self.warned_cursor = true;
|
||||
tracing::warn!(
|
||||
"cursor bitmap on an RGB-direct session — EFC cannot composite it; the cursor \
|
||||
will be missing from the stream (unset PUNKTFUNK_VULKAN_RGB_DIRECT for \
|
||||
metadata-cursor captures)"
|
||||
);
|
||||
}
|
||||
// EFC cannot composite a cursor bitmap — and never has to: `open` refuses the RGB-direct
|
||||
// shape for a cursor-blend session (the pin override above), so no cursor bitmap ever
|
||||
// reaches this arm. Gamescope, the flagship, embeds its pointer in the produced pixels.
|
||||
let padded = self.rgb.as_ref().is_some_and(|r| r.padded);
|
||||
// Only the padded Dmabuf arm below records timestamps (via `record_pad_blit`); the
|
||||
// CPU-upload arm records its own command buffer and writes none. Default to "not written"
|
||||
|
||||
Reference in New Issue
Block a user