fix(pf-capture): gamescope cursor — auth without setenv, frame-space coords, live targets (2.5)
**2.5a (L4) — stop `set_var`ing `XAUTHORITY` from a live multithreaded host.** The old connect swapped the process-global var around each `RustConnection::connect` under a mutex. That lock serialises this source against itself and nothing else: `getenv` takes no lock, so every OTHER thread's read raced it — and the concurrency is by construction, since `attach_gamescope_cursor` runs while the PipeWire thread is starting up (libspa plugin load, EGL/CUDA init). The primary path now parses the MIT-MAGIC-COOKIE-1 entry out of the given file itself and hands it to `DefaultStream::connect` + `RustConnection::connect_to_stream_with_auth_info` — the same two steps `RustConnection::connect` performs internally, minus its env-derived auth lookup — so nothing in this process touches the environment. The env swap survives only as a fallback for a file we cannot parse, and a wrong cookie pick cannot do damage: the server rejects it and the fallback (which does libxcb's full family/address match) takes over. Sharing `pf_vdisplay`'s process-wide env lock was not the fix — wrong layer, and it would still not fix `getenv`. **2.5b (L6) — publish the pointer in FRAME coordinates.** `QueryPointer` answers in the nested root's space, but `CursorOverlay::x/y`'s contract is frame pixels, and gamescope's `-w/-h` (nested root) and `-W/-H` (output + PipeWire node) are independent knobs — at `-W 1280 -H 720 -w 640 -h 360` they differ by 2×, so the pointer drew at a fraction of its real position. The root size comes free off the setup reply we already parse; the negotiated frame size arrives from the PipeWire thread's `param_changed` through a new `Arc<AtomicU64>` (`0` = not negotiated ⇒ pass through, as before). Scaling is computed in `i64` — a 5K coordinate times a 5K width overflows `i32`. Position only: the bitmap stays at root scale, warned once so a mismatched session is visible. **2.5c (L7) — the targets are a PROVIDER, not a snapshot.** The list was discovered once, before the game launched. gamescope creates the game's Xwayland at launch and advertises only the FIRST in any child's environ (verified on this box: `--xwayland-count 2` makes `:2` and `:3`, only `:2` is advertised), so the game's display was invisible — and when the connected Big Picture display then reported "gamescope is not drawing the pointer here", the source blanked the cursor for the whole game session, which is the exact regression the module doc says it fixed. The worker now re-runs the provider every 2 s: it adopts new Xwaylands, reconnects dead ones, and `spawn` no longer returns `None` on an empty list — a stream that starts before the game converges instead of staying cursorless. The provider is a host-facade closure, same one-way-edge shape as `FrameChannelSender`. **2.5d (L8) — bound the teardown join.** `Drop` joined the worker unbounded while the worker blocks in `RustConnection` replies with NO read timeout, so a peer that stops answering but keeps its socket open hung capturer teardown — on the session path. Now: a completion channel, `recv_timeout(250 ms)`, then detach with a warning (the thread only touches its own X connections and an `Arc`'d slot). 7 new tests, all host-runnable: the cookie reader against a hostile `.Xauthority` (wrong protocol, wrong display, wildcard entry, truncation mid-length-prefix, an over-long length prefix, a missing file), display-string parsing, and the root→frame mapping incl. the 5K overflow. pf-capture 20/20; workspace clippy --all-targets clean on Linux and windows-msvc. Phase 7.2 owes the on-glass nested-gamescope validation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3248,20 +3248,23 @@ fn build_pipeline(
|
||||
let mut capturer =
|
||||
crate::capture::capture_virtual_output(vout, plan.output_format(), plan.capture)
|
||||
.context("capture virtual output")?;
|
||||
// gamescope (Phase C): gamescope paints no `SPA_META_Cursor`, so hand the capturer gamescope's
|
||||
// nested Xwayland — it reads the pointer over X11 (XFixes shape + QueryPointer position) and
|
||||
// feeds `cursor()`, which the encode loop composites. A failed discovery/connect leaves the
|
||||
// stream cursorless (today's behaviour); non-gamescope plans skip this entirely.
|
||||
// gamescope (Phase C): gamescope paints no `SPA_META_Cursor`, so hand the capturer a way to
|
||||
// reach gamescope's nested Xwaylands — it reads the pointer over X11 (XFixes shape +
|
||||
// QueryPointer position) and feeds `cursor()`, which the encode loop composites.
|
||||
// Non-gamescope plans skip this entirely.
|
||||
//
|
||||
// A PROVIDER, not the discovered list: gamescope creates the game's Xwayland when the game
|
||||
// launches and advertises only the FIRST in any child's environ, so a list captured here misses
|
||||
// it — and the cursor source would then blank the pointer for the whole game session (it asks
|
||||
// the connected display "are you drawing the pointer?" and gets "no"). The source re-runs this
|
||||
// every couple of seconds, so a stream that starts before the game converges, and a display
|
||||
// that dies is retried. Same one-way-edge shape as the Windows channel senders: the closure
|
||||
// wraps the host's discovery, and pf-capture never reaches back into pf-vdisplay.
|
||||
#[cfg(target_os = "linux")]
|
||||
if plan.gamescope_cursor {
|
||||
let targets = pf_vdisplay::gamescope_xwayland_cursor_targets();
|
||||
if targets.is_empty() {
|
||||
tracing::warn!(
|
||||
"gamescope cursor: no nested Xwayland discovered — no in-video pointer this session"
|
||||
);
|
||||
} else {
|
||||
capturer.attach_gamescope_cursor(targets);
|
||||
}
|
||||
capturer.attach_gamescope_cursor(std::sync::Arc::new(
|
||||
pf_vdisplay::gamescope_xwayland_cursor_targets,
|
||||
));
|
||||
}
|
||||
if let Some(t) = trace {
|
||||
t.mark("capture_attached");
|
||||
|
||||
Reference in New Issue
Block a user