Files
pixelpass/src/host/x11.rs
T
molluskandClaude Opus 4.8 b0ff20fe3f host/x11: default to XDamage capture; drop --untimed from viewers
X11 full-desktop capture used `ximagesrc use-damage=false`, which copies
the whole root window every frame. On servers without working MIT-SHM
(and CPU-bound everywhere else) this collapses to ~1 fps — a field test
over an xlibre host played back at roughly one frame per minute. Default
to `use-damage=true` (XDamage re-grabs only changed regions); keep
`PIXELPASS_X11_NO_DAMAGE=1` as an escape hatch for driver artifacts.

Also drop `--untimed` from both mpv invocations (viewer banner + the
interactive launcher). `--untimed` displays each frame as it decodes and
ignores audio timestamps, which drifts a shared *video* progressively
out of sync with its audio. Pacing to the audio clock keeps A/V synced
at a negligible latency cost.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 20:29:44 -04:00

114 lines
4.5 KiB
Rust

//! X11 capture: `ximagesrc` → the shared encode/mux tail in [`super::pipeline`].
//! Unlike Wayland there's no portal and no fd hand-off — `ximagesrc` opens its
//! own X connection from `$DISPLAY`. The whole root window is captured by
//! default; `--window` resolves a single window's XID via an `xwininfo`
//! click-picker. The ticket is the access control, so capture starts silently
//! when the first viewer connects (no host-side consent prompt).
use anyhow::{Context, Result, bail};
use tokio::process::Command;
use x11rb::connection::Connection;
use x11rb::protocol::xproto::ConnectionExt;
use super::pipeline::{self, CaptureHandle};
use super::quality::EffectiveQuality;
use crate::cli::HostOpts;
pub async fn start(opts: &HostOpts, quality: &EffectiveQuality) -> Result<CaptureHandle> {
let xid = if opts.window {
Some(pick_window().await?)
} else {
None
};
// Geometry mirrors Wayland's portal-handshake log line and feeds the
// downscale presets (so they can compute an exact target size). A failure
// here shouldn't abort capture — ximagesrc will surface a real error if the
// X connection is genuinely unusable, and the scaler falls back to a
// height-only negotiation when dims are unknown.
let source_dims = match read_geometry(xid) {
Ok((w, h)) => {
tracing::info!(width = w, height = h, xid = ?xid, "X11 capture geometry");
Some((w as u32, h as u32))
}
Err(e) => {
tracing::warn!("could not read X11 geometry (capture will still try): {e:#}");
None
}
};
// XDamage capture (`use-damage=true`) only re-grabs changed screen
// regions instead of copying the whole root window every frame. On a busy
// desktop that is the difference between a usable framerate and ~1 fps —
// `use-damage=false` does a full XGetImage per frame, which collapses on
// servers without working MIT-SHM (and pins the CPU everywhere else).
// Kept as the default; `PIXELPASS_X11_NO_DAMAGE=1` restores full-frame
// capture if a driver produces partial-update artifacts with damage on.
let use_damage = if std::env::var_os("PIXELPASS_X11_NO_DAMAGE").is_some() {
"use-damage=false"
} else {
"use-damage=true"
};
let mut source_args = vec![
"ximagesrc".to_string(),
// show-pointer matches Wayland's CursorMode::Embedded.
use_damage.to_string(),
"show-pointer=true".to_string(),
];
if let Some(xid) = xid {
source_args.push(format!("xid={xid}"));
}
// X11 has no leaked fd to clean up, so the post-spawn hook is a no-op.
pipeline::spawn(opts, quality, source_dims, source_args, || {}).await
}
/// Run `xwininfo` and let the user click the window they want to share, then
/// parse the `Window id: 0x…` line out of its output. Returns the numeric XID.
async fn pick_window() -> Result<u32> {
eprintln!("[pixelpass] click the window you want to share…");
let output = Command::new("xwininfo")
.output()
.await
.context("failed to run `xwininfo` (install xorg-xwininfo)")?;
if !output.status.success() {
bail!(
"xwininfo failed: {}",
String::from_utf8_lossy(&output.stderr).trim()
);
}
let text = String::from_utf8_lossy(&output.stdout);
for line in text.lines() {
// e.g. "xwininfo: Window id: 0x3a00007 \"xterm\""
if let Some((_, rest)) = line.split_once("Window id: ") {
let token = rest.split_whitespace().next().unwrap_or("");
let hex = token.strip_prefix("0x").unwrap_or(token);
if let Ok(xid) = u32::from_str_radix(hex, 16) {
return Ok(xid);
}
}
}
bail!("could not parse a window id from xwininfo output");
}
/// Read pixel dimensions: the selected window's geometry when `--window` was
/// used, otherwise the root window of the screen named by `$DISPLAY`.
fn read_geometry(xid: Option<u32>) -> Result<(u16, u16)> {
let (conn, screen_num) =
x11rb::connect(None).context("could not connect to the X server (is DISPLAY set?)")?;
match xid {
Some(id) => {
let geo = conn
.get_geometry(id)
.context("GetGeometry request failed")?
.reply()
.context("GetGeometry reply failed")?;
Ok((geo.width, geo.height))
}
None => {
let screen = &conn.setup().roots[screen_num];
Ok((screen.width_in_pixels, screen.height_in_pixels))
}
}
}