Rebuild bootstrap and active Xwayland state after compositor replacement, rediscover the native pointer surface, fail unavailable during recovery, and retry with bounded backoff.
- commit
- 190136519a33c2cd68e0318a70b89dcc5dcf8b69
- parent
- dc5e2187012b2e649adbe3d4cc773d0a84b09819
- author
- Dami <dami.etoile@gmail.com>
| M | crates/ | 261 | +++++++++++++++++++++++++----- |
@@ -12,6 +12,10 @@ use sigil_protocol::{PointerPosition, PointerSurfaceDimensions};
1212 1313 const POINTER_POLL_INTERVAL: std::time::Duration = std::time::Duration::from_micros(16_667);1414 15+const BOOTSTRAP_RECONNECT_INITIAL: Duration = Duration::from_millis(100);16+17+const BOOTSTRAP_RECONNECT_MAXIMUM: Duration = Duration::from_secs(2);18+1519 const POINTER_ACTIVITY_VISIBLE_TIMEOUT: Duration = Duration::from_secs(3);1620 1721 const FOCUS_DISPLAY_PROPERTY: &[u8] = b"GAMESCOPE_MOUSE_FOCUS_DISPLAY";@@ -84,6 +88,41 @@ impl PointerActivityVisibility {
8488 }8589 }8690 91+92+93+struct ReconnectBackoff {94+ delay: Duration,95+ retry_not_before: Instant,96+}97+98+99+impl ReconnectBackoff {100+ fn new(now: Instant) -> Self {101+ Self {102+ delay: BOOTSTRAP_RECONNECT_INITIAL,103+ retry_not_before: now,104+ }105+ }106+107+ fn ready(&self, now: Instant) -> bool {108+ now >= self.retry_not_before109+ }110+111+ fn failed(&mut self, now: Instant) {112+ self.retry_not_before = now.checked_add(self.delay).unwrap_or(now);113+ self.delay = self114+ .delay115+ .checked_mul(2)116+ .unwrap_or(BOOTSTRAP_RECONNECT_MAXIMUM)117+ .min(BOOTSTRAP_RECONNECT_MAXIMUM);118+ }119+120+ fn recovered(&mut self, now: Instant) {121+ self.delay = BOOTSTRAP_RECONNECT_INITIAL;122+ self.retry_not_before = now;123+ }124+}125+87126 88127 pub struct PointerPositionTracker {89128 inner: Arc<TrackerInner>,@@ -117,29 +156,32 @@ impl PointerPositionTracker {
117156 configured_display: Option<&str>,118157 pointer_surface_dimensions: PointerSurfaceDimensions,119158 ) -> Result<Self> {120- use x11rb::protocol::xproto::ConnectionExt;121-122159 let bootstrap_display = match configured_display {123160 Some(display) => display.to_owned(),124161 None => std::env::var("DISPLAY")125162 .context("neither gamescope_pipewire.xwayland_display nor DISPLAY is configured")?,126163 };127164 validate_display_name(&bootstrap_display)?;128- let (bootstrap_connection, bootstrap_root) = connect_display(&bootstrap_display)129- .context("connecting to bootstrap Gamescope Xwayland")?;130- let focus_atom = bootstrap_connection131- .intern_atom(false, FOCUS_DISPLAY_PROPERTY)132- .context("interning Gamescope mouse-focus property")?133- .reply()134- .context("receiving Gamescope mouse-focus atom")?135- .atom;136- let focus_display = query_focus_display(&bootstrap_connection, bootstrap_root, focus_atom)137- .context("querying initial Gamescope mouse-focus display")?;165+ let bootstrap = connect_bootstrap_display(&bootstrap_display)?;166+ anyhow::ensure!(167+ bootstrap.pointer_surface_dimensions == pointer_surface_dimensions,168+ "bootstrap Gamescope pointer surface is {}x{}, but capture preflight resolved {}x{}",169+ bootstrap.pointer_surface_dimensions.width,170+ bootstrap.pointer_surface_dimensions.height,171+ pointer_surface_dimensions.width,172+ pointer_surface_dimensions.height173+ );174+ let focus_display =175+ query_focus_display(&bootstrap.connection, bootstrap.root, bootstrap.focus_atom)176+ .context("querying initial Gamescope mouse-focus display")?;138177 let active = connect_active_display(&focus_display)139178 .context("connecting to active Gamescope Xwayland")?;140- let initial_position =141- query_pointer_position(&active.connection, active.root, pointer_surface_dimensions)142- .context("querying initial Gamescope Xwayland pointer position")?;179+ let initial_position = query_pointer_position(180+ &active.connection,181+ active.root,182+ bootstrap.pointer_surface_dimensions,183+ )184+ .context("querying initial Gamescope Xwayland pointer position")?;143185 let initial = available_pointer_state(initial_position, query_cursor_visible(&active).ok());144186 let (latest, _) = tokio::sync::watch::channel(initial);145187 let stop = Arc::new(AtomicBool::new(false));@@ -148,27 +190,102 @@ impl PointerPositionTracker {
148190 std::thread::Builder::new()149191 .name("sigil-xwayland-pointer".into())150192 .spawn(move || {193+ let mut bootstrap = Some(bootstrap);151194 let mut active_display = focus_display;152195 let mut active_connection = Some(active);153196 let mut activity_visibility = PointerActivityVisibility::new(initial_position);197+ let mut reconnect_backoff = ReconnectBackoff::new(Instant::now());154198 while !thread_stop.load(Ordering::Acquire) {155199 std::thread::sleep(POINTER_POLL_INTERVAL);200+ if bootstrap.is_none() {201+ let now = Instant::now();202+ if !reconnect_backoff.ready(now) {203+ continue;204+ }205+ match connect_bootstrap_display(&bootstrap_display) {206+ Ok(reconnected) => {207+ let focus_display = query_focus_display(208+ &reconnected.connection,209+ reconnected.root,210+ reconnected.focus_atom,211+ );212+ let recovered = focus_display.and_then(|focus_display| {213+ let active = connect_active_display(&focus_display)?;214+ let position = query_pointer_position(215+ &active.connection,216+ active.root,217+ reconnected.pointer_surface_dimensions,218+ )?;219+ Ok((focus_display, active, position))220+ });221+ match recovered {222+ Ok((focus_display, active, position)) => {223+ let visible = query_cursor_visible(&active).ok();224+ let pointer_surface_dimensions =225+ reconnected.pointer_surface_dimensions;226+ bootstrap = Some(reconnected);227+ active_display = focus_display;228+ active_connection = Some(active);229+ activity_visibility =230+ PointerActivityVisibility::new(position);231+ reconnect_backoff.recovered(now);232+ let state = available_pointer_state(position, visible);233+ thread_latest.send_if_modified(|latest| {234+ if *latest == state {235+ false236+ } else {237+ *latest = state;238+ true239+ }240+ });241+ tracing::info!(242+ display = %bootstrap_display,243+ pointer_surface_width = pointer_surface_dimensions.width,244+ pointer_surface_height = pointer_surface_dimensions.height,245+ "Gamescope Xwayland pointer feedback reconnected"246+ );247+ }248+ Err(error) => {249+ reconnect_backoff.failed(now);250+ tracing::warn!(251+ display = %bootstrap_display,252+ error = %error,253+ "Gamescope Xwayland pointer feedback reconnect incomplete"254+ );255+ }256+ }257+ }258+ Err(error) => {259+ reconnect_backoff.failed(now);260+ tracing::warn!(261+ display = %bootstrap_display,262+ error = %error,263+ "Gamescope Xwayland pointer feedback reconnect failed"264+ );265+ }266+ }267+ continue;268+ }269+ let bootstrap_connection = bootstrap270+ .as_ref()271+ .expect("bootstrap connection was checked above");156272 let focus_display = match query_focus_display(157- &bootstrap_connection,158- bootstrap_root,159- focus_atom,273+ &bootstrap_connection.connection,274+ bootstrap_connection.root,275+ bootstrap_connection.focus_atom,160276 ) {161277 Ok(display) => display,162- Err(_) => {278+ Err(error) => {279+ tracing::warn!(280+ display = %bootstrap_display,281+ error = %error,282+ "Gamescope bootstrap Xwayland connection was lost"283+ );284+ bootstrap = None;285+ active_connection = None;163286 activity_visibility.reset();164- thread_latest.send_if_modified(|latest| {165- if *latest == PointerState::UNAVAILABLE {166- false167- } else {168- *latest = PointerState::UNAVAILABLE;169- true170- }171- });287+ reconnect_backoff.failed(Instant::now());288+ publish_unavailable(&thread_latest);172289 continue;173290 }174291 };@@ -178,20 +295,13 @@ impl PointerPositionTracker {
178295 activity_visibility.reset();179296 }180297 let Some(active) = active_connection.as_ref() else {181- thread_latest.send_if_modified(|latest| {182- if *latest == PointerState::UNAVAILABLE {183- false184- } else {185- *latest = PointerState::UNAVAILABLE;186- true187- }188- });298+ publish_unavailable(&thread_latest);189299 continue;190300 };191301 match query_pointer_position(192302 &active.connection,193303 active.root,194- pointer_surface_dimensions,304+ bootstrap_connection.pointer_surface_dimensions,195305 ) {196306 Ok(position) => {197307 let visible = activity_visibility.resolve(@@ -212,14 +322,7 @@ impl PointerPositionTracker {
212322 Err(_) => {213323 active_connection = None;214324 activity_visibility.reset();215- thread_latest.send_if_modified(|latest| {216- if *latest == PointerState::UNAVAILABLE {217- false218- } else {219- *latest = PointerState::UNAVAILABLE;220- true221- }222- });325+ publish_unavailable(&thread_latest);223326 }224327 }225328 }@@ -262,6 +365,53 @@ fn connect_display(display: &str) -> Result<(x11rb::rust_connection::RustConnect
262365 Ok((connection, root))263366 }264367 368+369+struct BootstrapXwayland {370+ connection: x11rb::rust_connection::RustConnection,371+ root: u32,372+ focus_atom: u32,373+ pointer_surface_dimensions: PointerSurfaceDimensions,374+}375+376+377+fn connect_bootstrap_display(display: &str) -> Result<BootstrapXwayland> {378+ use x11rb::protocol::xproto::ConnectionExt;379+380+ let (connection, root) =381+ connect_display(display).context("connecting to bootstrap Gamescope Xwayland")?;382+ let focus_atom = connection383+ .intern_atom(false, FOCUS_DISPLAY_PROPERTY)384+ .context("interning Gamescope mouse-focus property")?385+ .reply()386+ .context("receiving Gamescope mouse-focus atom")?387+ .atom;388+ let geometry = connection389+ .get_geometry(root)390+ .context("sending bootstrap X11 GetGeometry")?391+ .reply()392+ .context("receiving bootstrap X11 GetGeometry reply")?;393+ let pointer_surface_dimensions = PointerSurfaceDimensions::new(geometry.width, geometry.height)394+ .context("validating bootstrap Gamescope pointer surface dimensions")?;395+ Ok(BootstrapXwayland {396+ connection,397+ root,398+ focus_atom,399+ pointer_surface_dimensions,400+ })401+}402+403+404+fn publish_unavailable(latest: &tokio::sync::watch::Sender<PointerState>) {405+ latest.send_if_modified(|latest| {406+ if *latest == PointerState::UNAVAILABLE {407+ false408+ } else {409+ *latest = PointerState::UNAVAILABLE;410+ true411+ }412+ });413+}414+265415 266416 struct ActiveXwayland {267417 connection: x11rb::rust_connection::RustConnection,@@ -608,4 +758,27 @@ mod tests {
608758 visibility.reset();609759 assert!(!visibility.resolve(moved, None, sampled_at));610760 }761+762+ 763+ fn bootstrap_reconnect_backoff_is_bounded_and_resets_after_recovery() {764+ let started_at = Instant::now();765+ let mut backoff = ReconnectBackoff::new(started_at);766+767+ assert!(backoff.ready(started_at));768+ backoff.failed(started_at);769+ assert!(!backoff.ready(started_at));770+ assert!(backoff.ready(started_at + BOOTSTRAP_RECONNECT_INITIAL));771+772+ let mut now = started_at + BOOTSTRAP_RECONNECT_INITIAL;773+ for _ in 0..16 {774+ backoff.failed(now);775+ assert!(backoff.delay <= BOOTSTRAP_RECONNECT_MAXIMUM);776+ now += BOOTSTRAP_RECONNECT_MAXIMUM;777+ }778+ assert_eq!(backoff.delay, BOOTSTRAP_RECONNECT_MAXIMUM);779+780+ backoff.recovered(now);781+ assert!(backoff.ready(now));782+ assert_eq!(backoff.delay, BOOTSTRAP_RECONNECT_INITIAL);783+ }611784 }