From 6884804b87b93c228b97ba683ddcc7d0acb4d023 Mon Sep 17 00:00:00 2001 From: Thanos Apollo Date: Fri, 2 Oct 2026 08:23:09 +0300 Subject: [PATCH] Allow explicit owned Wayland connections for deferred startup --- winit-wayland/src/event_loop/mod.rs | 32 ++++++++- winit-wayland/src/lib.rs | 16 +++++ winit/src/event_loop.rs | 9 +++ winit/src/platform_impl/linux/mod.rs | 35 +++++++-- winit/tests/wayland_connection.rs | 103 +++++++++++++++++++++++++++ 5 files changed, 190 insertions(+), 5 deletions(-) create mode 100644 winit/tests/wayland_connection.rs diff --git a/winit-wayland/src/event_loop/mod.rs b/winit-wayland/src/event_loop/mod.rs index 27fc123be6..e38b895819 100644 --- a/winit-wayland/src/event_loop/mod.rs +++ b/winit-wayland/src/event_loop/mod.rs @@ -56,6 +56,17 @@ use super::{WindowId, logical_to_physical_rounded}; type WaylandDispatcher = calloop::Dispatcher<'static, WaylandSource, WinitState>; +static EVENT_LOOP_CREATED: AtomicBool = AtomicBool::new(false); + +/// Release an explicit-connection creation reservation unless initialization completes. +struct CreationReservation; + +impl Drop for CreationReservation { + fn drop(&mut self) { + EVENT_LOOP_CREATED.store(false, Ordering::Relaxed); + } +} + #[derive(Debug)] pub(crate) enum Event { WindowEvent { window_id: WindowId, event: WindowEvent }, @@ -91,7 +102,6 @@ pub struct EventLoop { impl EventLoop { pub fn new() -> Result { - static EVENT_LOOP_CREATED: AtomicBool = AtomicBool::new(false); if EVENT_LOOP_CREATED.swap(true, Ordering::Relaxed) { // For better cross-platformness. return Err(EventLoopError::RecreationAttempt); @@ -99,6 +109,26 @@ impl EventLoop { let connection = Connection::connect_to_env().map_err(|err| os_error!(err))?; + Self::from_connection(connection) + } + + /// Create an event loop using an owned connection without consulting the environment. + /// + /// A fresh event queue is created on the supplied connection. Other queues remain + /// the caller's responsibility. Failed initialization releases the creation reservation + /// so another explicit connection can be tried. Successful creation permanently consumes + /// the one-event-loop allowance, including after the event loop is dropped. + pub fn new_with_connection(connection: Connection) -> Result { + if EVENT_LOOP_CREATED.swap(true, Ordering::Relaxed) { + return Err(EventLoopError::RecreationAttempt); + } + let reservation = CreationReservation; + let event_loop = Self::from_connection(connection)?; + mem::forget(reservation); + Ok(event_loop) + } + + fn from_connection(connection: Connection) -> Result { let (globals, mut event_queue) = globals::registry_queue_init(&connection).map_err(|err| os_error!(err))?; let queue_handle = event_queue.handle(); diff --git a/winit-wayland/src/lib.rs b/winit-wayland/src/lib.rs index 113e2b074c..0a9aee154b 100644 --- a/winit-wayland/src/lib.rs +++ b/winit-wayland/src/lib.rs @@ -51,6 +51,8 @@ pub use self::dnd::{DataOffer, DragSource, MimeData, MimeType}; pub use self::event_loop::{ActiveEventLoop, EventLoop}; pub use self::popup::Popup; pub use self::window::Window; +/// An owned connection to a Wayland compositor. +pub use wayland_client::Connection as WaylandConnection; /// Additional methods on [`ActiveEventLoop`] that are specific to Wayland. pub trait ActiveEventLoopExtWayland { @@ -76,6 +78,20 @@ pub trait EventLoopBuilderExtWayland { /// Force using Wayland. fn with_wayland(&mut self) -> &mut Self; + /// Use an owned Wayland connection instead of connecting through the environment. + /// + /// This also selects the Wayland backend. The connection is retained by the builder; + /// the event loop holds its own clone and creates its own event queue. Existing queues + /// on this connection remain owned by their callers. + /// + /// For an explicit socket, construct a [`WaylandConnection`] with + /// [`WaylandConnection::from_socket`] and a connected `std::os::unix::net::UnixStream`. + /// Neither `WAYLAND_DISPLAY`, `WAYLAND_SOCKET` nor `XDG_RUNTIME_DIR` is consulted. + /// Main-thread requirements are unchanged. Failed initialization with an explicit + /// connection permits retry; successful creation still forbids another event loop, + /// even after it is dropped. A later backend-selection method can override Wayland. + fn with_wayland_connection(&mut self, connection: WaylandConnection) -> &mut Self; + /// Whether to allow the event loop to be created off of the main thread. /// /// By default, the window is only allowed to be created on the main diff --git a/winit/src/event_loop.rs b/winit/src/event_loop.rs index ff9ee6cc6c..f1db6ef9fe 100644 --- a/winit/src/event_loop.rs +++ b/winit/src/event_loop.rs @@ -377,6 +377,15 @@ impl winit_wayland::EventLoopBuilderExtWayland for EventLoopBuilder { self } + #[inline] + fn with_wayland_connection( + &mut self, + connection: winit_wayland::WaylandConnection, + ) -> &mut Self { + self.platform_specific.wayland_connection = Some(connection); + self.with_wayland() + } + #[inline] fn with_any_thread(&mut self, any_thread: bool) -> &mut Self { self.platform_specific.any_thread = any_thread; diff --git a/winit/src/platform_impl/linux/mod.rs b/winit/src/platform_impl/linux/mod.rs index 79a8a86d77..b8526c2a59 100644 --- a/winit/src/platform_impl/linux/mod.rs +++ b/winit/src/platform_impl/linux/mod.rs @@ -29,10 +29,24 @@ pub(crate) enum Backend { Wayland, } -#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Default, Clone, PartialEq, Eq)] pub(crate) struct PlatformSpecificEventLoopAttributes { pub(crate) forced_backend: Option, pub(crate) any_thread: bool, + #[cfg(wayland_platform)] + pub(crate) wayland_connection: Option, +} + +impl std::hash::Hash for PlatformSpecificEventLoopAttributes { + fn hash(&self, state: &mut H) { + self.forced_backend.hash(state); + self.any_thread.hash(state); + #[cfg(wayland_platform)] + self.wayland_connection + .as_ref() + .map(|connection| connection.backend().display_id()) + .hash(state); + } } /// `x11_or_wayland!(match expr; Enum(foo) => foo.something())` @@ -86,6 +100,13 @@ impl EventLoop { ); } + #[cfg(wayland_platform)] + if attributes.forced_backend == Some(Backend::Wayland) + && attributes.wayland_connection.is_some() + { + return Self::new_wayland_any_thread(attributes); + } + // NOTE: Wayland first because of X11 could be present under Wayland as well. Empty // variables are also treated as not set. let backend = match ( @@ -124,15 +145,21 @@ impl EventLoop { // Create the display based on the backend. match backend { #[cfg(wayland_platform)] - Backend::Wayland => EventLoop::new_wayland_any_thread(), + Backend::Wayland => EventLoop::new_wayland_any_thread(attributes), #[cfg(x11_platform)] Backend::X => EventLoop::new_x11_any_thread(), } } #[cfg(wayland_platform)] - fn new_wayland_any_thread() -> Result { - wayland::EventLoop::new().map(|evlp| EventLoop::Wayland(Box::new(evlp))) + fn new_wayland_any_thread( + attributes: &PlatformSpecificEventLoopAttributes, + ) -> Result { + let event_loop = match &attributes.wayland_connection { + Some(connection) => wayland::EventLoop::new_with_connection(connection.clone()), + None => wayland::EventLoop::new(), + }; + event_loop.map(|evlp| EventLoop::Wayland(Box::new(evlp))) } #[cfg(x11_platform)] diff --git a/winit/tests/wayland_connection.rs b/winit/tests/wayland_connection.rs new file mode 100644 index 0000000000..7a8ce54bf2 --- /dev/null +++ b/winit/tests/wayland_connection.rs @@ -0,0 +1,103 @@ +//! Explicit-connection selection and failed-initialization recovery. +#![cfg(all(target_os = "linux", feature = "wayland"))] + +use std::collections::hash_map::DefaultHasher; +use std::hash::{Hash, Hasher}; +use std::os::unix::net::UnixStream; + +use winit::error::EventLoopError; +use winit::event_loop::{EventLoop, EventLoopBuilder}; +use winit::platform::wayland::{ + EventLoopBuilderExtWayland, EventLoopExtWayland, WaylandConnection, +}; + +fn disconnected_connection() -> WaylandConnection { + let (client, server) = UnixStream::pair().unwrap(); + drop(server); + WaylandConnection::from_socket(client).unwrap() +} + +fn hash(builder: &EventLoopBuilder) -> u64 { + let mut state = DefaultHasher::new(); + builder.hash(&mut state); + state.finish() +} + +#[test] +fn builder_connection_identity_preserves_equality_and_hashing() { + let connection = disconnected_connection(); + let mut first = EventLoop::builder(); + let mut second = EventLoop::builder(); + first.with_wayland_connection(connection.clone()); + second.with_wayland_connection(connection); + assert_eq!(first, second); + assert_eq!(hash(&first), hash(&second)); + second.with_wayland_connection(disconnected_connection()); + assert_ne!(first, second); + assert_ne!(first, EventLoop::builder()); +} + +#[test] +fn failed_explicit_initialization_can_retry_without_relaxing_thread_policy() { + let mut builder = EventLoop::builder(); + builder.with_wayland_connection(disconnected_connection()); + // Rust test functions run off the OS main thread. Connection injection must + // not bypass the ordinary platform check, or consume the creation allowance. + assert!(std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| builder.build())).is_err()); + builder.with_any_thread(true); + for _ in 0..2 { + let error = builder.build().unwrap_err(); + assert!(matches!(error, EventLoopError::Os(_)), "unexpected error: {error}"); + builder.with_wayland_connection(disconnected_connection()); + } +} + +#[test] +fn environment_connection_keeps_native_one_shot_failure_policy() { + const CHILD: &str = "WINIT_TEST_DEFAULT_CONNECTION_CHILD"; + if std::env::var_os(CHILD).is_none() { + let status = std::process::Command::new(std::env::current_exe().unwrap()) + .args(["--exact", "environment_connection_keeps_native_one_shot_failure_policy"]) + .env(CHILD, "1") + .env("WAYLAND_DISPLAY", "/nonexistent/winit-test-wayland") + .env_remove("WAYLAND_SOCKET") + .env_remove("DISPLAY") + .status() + .unwrap(); + assert!(status.success()); + return; + } + let mut builder = EventLoop::builder(); + builder.with_wayland().with_any_thread(true); + assert!(matches!(builder.build(), Err(EventLoopError::Os(_)))); + assert!(matches!(builder.build(), Err(EventLoopError::RecreationAttempt))); + builder.with_wayland_connection(disconnected_connection()); + assert!(matches!(builder.build(), Err(EventLoopError::RecreationAttempt))); +} + +#[test] +#[ignore = "requires a private compositor socket in WINIT_TEST_WAYLAND_SOCKET"] +fn failed_connection_then_real_explicit_display_and_permanent_success_guard() { + use winit::raw_window_handle::{HasDisplayHandle, RawDisplayHandle}; + + let socket = std::env::var_os("WINIT_TEST_WAYLAND_SOCKET") + .expect("select an isolated compositor explicitly"); + let mut builder = EventLoop::builder(); + builder.with_any_thread(true).with_wayland_connection(disconnected_connection()); + assert!(matches!(builder.build(), Err(EventLoopError::Os(_)))); + + let connection = WaylandConnection::from_socket(UnixStream::connect(socket).unwrap()).unwrap(); + let display = connection.backend().display_ptr(); + builder.with_wayland_connection(connection); + let event_loop = builder.build().unwrap(); + assert!(event_loop.is_wayland()); + let handle = event_loop.owned_display_handle(); + let RawDisplayHandle::Wayland(raw) = handle.display_handle().unwrap().as_raw() else { + panic!("explicit connection selected another backend"); + }; + assert_eq!(raw.display.as_ptr(), display.cast()); + assert!(matches!(builder.build(), Err(EventLoopError::RecreationAttempt))); + drop(handle); + drop(event_loop); + assert!(matches!(builder.build(), Err(EventLoopError::RecreationAttempt))); +}