feat(capture): gamescope cursor via XFixes shape + QueryPointer position (Phase C)

gamescope excludes its pointer from the PipeWire node it feeds us and can't embed
one either (`set_hw_cursor` is inert), so every gamescope stream was cursorless.
Read the pointer from gamescope's nested Xwayland instead — XFixesGetCursorImage
for shape/hotspot/visibility, core QueryPointer for position — and publish a
CursorOverlay into the capturer's existing `cursor_live` slot, so the encoder
blend composites it into the video exactly like the SPA_META_Cursor path.

- pf-capture/src/linux/xfixes_cursor.rs (new): the XFixes reader. Connects to
  EVERY nested Xwayland (Gaming Mode runs one per --xwayland-count) and follows
  the focused one each tick (the display whose pointer moves), reading that
  display's own cursor shape. Un-premultiplies ARGB -> straight RGBA. Drop stops
  the thread.
- Capturer::attach_gamescope_cursor + the PortalCapturer override spawn it into
  the same `cursor_live` slot; pf_vdisplay::gamescope_xwayland_cursor_targets
  discovers the (DISPLAY, XAUTHORITY) pairs via the GAMESCOPE_WAYLAND_DISPLAY scan.
- host: SessionPlan.gamescope_cursor (set from the compositor at both resolve
  sites AND on a mid-stream Desktop->Gaming switch); the blend gate now builds the
  encoder blend for gamescope; a sibling composite arm attaches capturer.cursor()
  per tick for capture-mode clients (no channel needed). native NV12 is disabled
  for these sessions — that encode path can't blend the cursor (it assumes
  gamescope embeds the pointer), so we capture RGB and route to the proven CUDA
  VkSlotBlend / compute-CSC blend.
- the pipewire thread no longer clobbers `cursor_live` with None on a buffer that
  carries no SPA_META_Cursor (gamescope) — that raced the XFixes writer and
  strobed the composited pointer on/off.

On-glass (home-bazzite-2, RTX 5070 Ti, Gaming Mode): cursor visible + steady in
Big Picture and CS2 menus, follows focus between the two Xwaylands, hidden in-game.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-22 23:47:53 +02:00
co-authored by Claude Opus 4.8
parent 925130d1f9
commit d5ae8dcc3e
10 changed files with 614 additions and 31 deletions
+16 -2
View File
@@ -112,6 +112,12 @@ pub struct SessionPlan {
/// locally, so `cursor_blend` is off AND (on Windows) the capturer sets the driver's
/// hardware cursor up via [`OutputFormat::hw_cursor`](pf_frame::OutputFormat).
pub cursor_forward: bool,
/// This is a gamescope session and its cursor comes from the XFixes source, NOT the
/// (absent) `SPA_META_Cursor` (remote-desktop-sweep Phase C). Distinct from `cursor_forward`:
/// gamescope can't embed the pointer OR carry the channel for a plain capture-mode client, so
/// the host ALWAYS composites the XFixes-sourced cursor into the video (`cursor_blend` is set
/// too). `build_pipeline` reads this to attach the XFixes reader to the capturer.
pub gamescope_cursor: bool,
}
impl SessionPlan {
@@ -135,6 +141,9 @@ impl SessionPlan {
wire_chunk: None,
cursor_blend,
cursor_forward,
// Set by the resolve callers (they know the compositor); default off keeps every
// non-gamescope plan unchanged.
gamescope_cursor: false,
}
}
@@ -188,9 +197,14 @@ impl SessionPlan {
pyrowave: self.codec == crate::encode::Codec::PyroWave,
// Producer-native NV12 (gamescope) is consumable only by the Linux Vulkan Video
// backend — resolved HERE from the plan's codec so the capturer never reaches back
// into encode (the same one-way edge as `gpu` above).
// into encode (the same one-way edge as `gpu` above). BUT the native-NV12 encode path
// has no CSC stage to fold the cursor into (it assumes gamescope embeds its pointer,
// which it does NOT into the PipeWire node) — so a gamescope-cursor session (Phase C)
// must capture RGB instead, routing to the compute-CSC / VkSlotBlend blend that draws
// `frame.cursor`. Costs the RGB→NV12 CSC we'd otherwise skip; the native-NV12 cursor
// blend is the perf-preserving follow-up.
#[cfg(target_os = "linux")]
nv12_native: crate::encode::linux_native_nv12_ok(self.codec),
nv12_native: crate::encode::linux_native_nv12_ok(self.codec) && !self.gamescope_cursor,
#[cfg(not(target_os = "linux"))]
nv12_native: false,
}