From 139bbc455628e30a52e927149651d5adbc721d3a Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Wed, 12 Aug 2026 01:39:54 +0200 Subject: [PATCH 1/3] wip --- src/platform/x11/event_loop.rs | 88 +++++++++++++++++-------------- src/platform/x11/window_shared.rs | 8 ++- src/platform/x11/xcb_window.rs | 35 ++++++------ 3 files changed, 73 insertions(+), 58 deletions(-) diff --git a/src/platform/x11/event_loop.rs b/src/platform/x11/event_loop.rs index 14f777b5..1ef8f7ed 100644 --- a/src/platform/x11/event_loop.rs +++ b/src/platform/x11/event_loop.rs @@ -12,13 +12,11 @@ use crate::warn; use crate::wrappers::xkbcommon::XkbcommonState; use crate::{Event, MouseButton, MouseEvent, ScrollDelta, WindowEvent, WindowHandler, WindowSize}; use calloop::generic::Generic; -use calloop::timer::{TimeoutAction, Timer}; use calloop::{Interest, LoopSignal, Mode, PostAction}; use dpi::{PhysicalPosition, PhysicalSize}; use std::rc::Rc; use std::sync::mpsc; use std::sync::mpsc::Receiver; -use std::time::{Duration, Instant}; use x11rb::connection::Connection; use x11rb::errors::ConnectionError; use x11rb::protocol::Event as XEvent; @@ -52,6 +50,7 @@ pub(crate) struct EventLoop { window: Rc, new_physical_size: Option>, + exposed: bool, loop_signal: LoopSignal, @@ -64,8 +63,6 @@ pub(crate) struct EventLoop { main_thread: Option, } -const FRAME_INTERVAL: Duration = Duration::from_millis(15); - impl EventLoop { pub fn new( window: Rc, handler: Box, @@ -75,10 +72,6 @@ impl EventLoop { ) -> Result { let loop_handle = inner.handle(); - loop_handle - .insert_source(Timer::from_duration(FRAME_INTERVAL), |i, _, e| e.handle_frame(i)) - .map_err(|e| e.error)?; - loop_handle .insert_source( Generic::new_with_error(window.connection.conn.clone(), Interest::READ, Mode::Edge), @@ -94,6 +87,7 @@ impl EventLoop { loop_signal: inner.get_signal(), handler, new_physical_size: None, + exposed: false, drag_n_drop: DragNDropState::NoCurrentSession, xkb_state: XkbcommonState::new(&window.connection), run_error: None, @@ -106,16 +100,32 @@ impl EventLoop { #[inline] fn drain_xcb_events(&mut self) -> Result<(), FatalError> { - // the X server has a tendency to send spurious/extraneous configure notify events when a - // window is resized, and we need to batch those together and just send one resize event - // when they've all been coalesced. - self.new_physical_size = None; - while let Some(event) = self.window.connection.conn.poll_for_event()? { self.handle_xcb_event(event)?; } - self.handle_coalesced_resize_events() + Ok(()) + } + + fn handle_coalesced_expose_events(&mut self) -> Result<(), FatalError> { + if !self.exposed { + return Ok(()); + } + self.exposed = false; + + if !self.window.is_mapped.get() { + return Ok(()); + } + + let _ = self.window.xcb_window.trigger_expose()?; + + if let Err(e) = self.handler.on_frame() { + self.run_error = Some(e.into()); + self.stop_now(); + return Ok(()); + } + + Ok(()) } fn handle_coalesced_resize_events(&mut self) -> Result<(), FatalError> { @@ -143,6 +153,9 @@ impl EventLoop { previous: WindowSize::from_physical(previous.cast(), scale_factor), })?; } + + // Immediately schedule a redraw, do not wait for an "expose" event + self.exposed = true; } Ok(()) @@ -223,30 +236,14 @@ impl EventLoop { Ok(PostAction::Continue) } - fn handle_frame(&mut self, previous_deadline: Instant) -> TimeoutAction { - if let Err(e) = self.handler.on_frame() { - self.run_error = Some(e.into()); - self.stop_now(); - return TimeoutAction::Drop; - } - - // We'll try to keep a consistent frame pace. If the last frame couldn't be processed in - // the expected frame time, this will throttle down to prevent multiple frames from - // being queued up. - - let now = Instant::now(); - let next_deadline = if previous_deadline + FRAME_INTERVAL >= now { - now + FRAME_INTERVAL - } else { - previous_deadline + FRAME_INTERVAL - }; - - TimeoutAction::ToInstant(next_deadline) - } - fn handle_idle(&mut self) { // Check for any events in the internal buffers before going to sleep: - let _ = self.drain_xcb_events(); + let _ = self.drain_xcb_events(); // TODO: handle error + + self.handle_coalesced_resize_events().unwrap(); // TODO: handle error + self.handle_coalesced_expose_events().unwrap(); + + self.window.connection.conn.flush().unwrap(); // TODO: handle error } pub fn run(mut self, mut inner: calloop::EventLoop) -> Result<(), Error> { @@ -329,14 +326,17 @@ impl EventLoop { } } - XEvent::ConfigureNotify(event) => { - let new_physical_size = PhysicalSize::new(event.width, event.height); + XEvent::Error(e) => { + warn!("Received leftover X11 error: {:?}", e); + } - if self.new_physical_size.is_some() || new_physical_size != self.window.get_size() { - self.new_physical_size = Some(new_physical_size); - } + XEvent::ConfigureNotify(event) => { + // These are coalesced and then handled asynchronously at the end of the event loop + self.new_physical_size = Some(PhysicalSize::new(event.width, event.height)); } + XEvent::Expose(_) => self.exposed = true, + //// // mouse //// @@ -416,6 +416,12 @@ impl EventLoop { self.handle_event(Event::Window(WindowEvent::Unfocused)); } + XEvent::MapNotify(_) => { + self.window.is_mapped.set(true); + self.window.xcb_window.trigger_expose()?; + } + XEvent::UnmapNotify(_) => self.window.is_mapped.set(false), + _ => {} } diff --git a/src/platform/x11/window_shared.rs b/src/platform/x11/window_shared.rs index 81582180..bc74b7a4 100644 --- a/src/platform/x11/window_shared.rs +++ b/src/platform/x11/window_shared.rs @@ -56,6 +56,7 @@ pub(crate) struct WindowInner { pub(crate) visual_id: Visualid, pub(crate) is_focused: Cell, + pub(crate) is_mapped: Cell, pub(crate) loop_signal: LoopSignal, pub(crate) main_thread_shared: Arc, @@ -134,6 +135,7 @@ impl WindowInner { loop_signal: ev_loop.get_signal(), is_focused: false.into(), + is_mapped: false.into(), main_thread_shared: shared, #[cfg(feature = "opengl")] @@ -165,7 +167,11 @@ impl WindowInner { pub fn store_size(&self, size: PhysicalSize) -> PhysicalSize { let previous = self.window_size.replace(size); - self.main_thread_shared.set_size(size); + + if previous != size { + self.main_thread_shared.set_size(size); + } + previous } diff --git a/src/platform/x11/xcb_window.rs b/src/platform/x11/xcb_window.rs index 686eaa82..36b894c8 100644 --- a/src/platform/x11/xcb_window.rs +++ b/src/platform/x11/xcb_window.rs @@ -62,12 +62,16 @@ impl XcbWindow { Ok(Self { window_id, connection }) } - pub fn map_window(&self) -> Result, ReplyOrIdError> { - Ok(self.connection.conn.map_window(self.window_id.get())?) + pub fn map_window(&self) -> Result, ConnectionError> { + self.connection.conn.map_window(self.window_id.get()) } - pub fn unmap_window(&self) -> Result, ReplyOrIdError> { - Ok(self.connection.conn.unmap_window(self.window_id.get())?) + pub fn unmap_window(&self) -> Result, ConnectionError> { + self.connection.conn.unmap_window(self.window_id.get()) + } + + pub fn trigger_expose(&self) -> Result, ConnectionError> { + self.connection.conn.clear_area(true, self.window_id.get(), 0, 0, 0, 0) } pub fn resize( @@ -90,41 +94,40 @@ impl XcbWindow { ) } - pub fn set_title(&self, title: &str) -> Result, ReplyOrIdError> { - Ok(self.connection.conn.change_property8( + pub fn set_title(&self, title: &str) -> Result, ConnectionError> { + self.connection.conn.change_property8( PropMode::REPLACE, self.window_id.get(), AtomEnum::WM_NAME, AtomEnum::STRING, title.as_bytes(), - )?) + ) } - pub fn enable_wm_protocols(&self) -> Result, ReplyOrIdError> { - Ok(self.connection.conn.change_property32( + pub fn enable_wm_protocols(&self) -> Result, ConnectionError> { + self.connection.conn.change_property32( PropMode::REPLACE, self.window_id.get(), self.connection.atoms.WM_PROTOCOLS, AtomEnum::ATOM, &[self.connection.atoms.WM_DELETE_WINDOW], - )?) + ) } - pub fn enable_dnd_protocols(&self) -> Result, ReplyOrIdError> { - Ok(self.connection.conn.change_property32( + pub fn enable_dnd_protocols(&self) -> Result, ConnectionError> { + self.connection.conn.change_property32( PropMode::REPLACE, self.window_id.get(), self.connection.atoms.XdndAware, AtomEnum::ATOM, &[5u32], // Latest version; hasn't changed since 2002 - )?) + ) } pub fn set_size_hints( &self, size_hints: WmSizeHints, - ) -> Result, ReplyOrIdError> { - Ok(size_hints - .set_normal_hints(&self.connection.conn as &XCBConnection, self.window_id.get())?) + ) -> Result, ConnectionError> { + size_hints.set_normal_hints(&self.connection.conn as &XCBConnection, self.window_id.get()) } #[inline] From efd0a6942425d15d7bd147b2e6a8cb5247553498 Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Wed, 12 Aug 2026 02:15:40 +0200 Subject: [PATCH 2/3] wip --- src/platform/x11/error.rs | 11 +++++ src/platform/x11/event_loop.rs | 77 +++++++++++++++++++++++++++------- src/platform/x11/xcb_window.rs | 4 -- 3 files changed, 74 insertions(+), 18 deletions(-) diff --git a/src/platform/x11/error.rs b/src/platform/x11/error.rs index debf1678..1c1586cc 100644 --- a/src/platform/x11/error.rs +++ b/src/platform/x11/error.rs @@ -54,6 +54,7 @@ pub enum Error { MainThreadRecvResult, Calloop(calloop::Error), RequestFromMainThreadFailed(RequestFailed), + SendMainThread, #[cfg(feature = "opengl")] XLib(crate::wrappers::xlib::XLibError), #[cfg(feature = "opengl")] @@ -81,6 +82,7 @@ impl Display for Error { } Error::Calloop(e) => e.fmt(f), Error::RequestFromMainThreadFailed(e) => e.fmt(f), + Error::SendMainThread => FatalError::SendMainThread.fmt(f), #[cfg(feature = "opengl")] Error::XLib(e) => e.fmt(f), #[cfg(feature = "opengl")] @@ -157,6 +159,15 @@ impl From for Error { } } +impl From for Error { + fn from(value: FatalError) -> Self { + match value { + FatalError::Connection(e) => Self::Connection(e), + FatalError::SendMainThread => Self::SendMainThread, + } + } +} + #[cfg(feature = "opengl")] impl From for Error { fn from(value: crate::wrappers::xlib::XLibError) -> Self { diff --git a/src/platform/x11/event_loop.rs b/src/platform/x11/event_loop.rs index 1ef8f7ed..421691d0 100644 --- a/src/platform/x11/event_loop.rs +++ b/src/platform/x11/event_loop.rs @@ -12,11 +12,13 @@ use crate::warn; use crate::wrappers::xkbcommon::XkbcommonState; use crate::{Event, MouseButton, MouseEvent, ScrollDelta, WindowEvent, WindowHandler, WindowSize}; use calloop::generic::Generic; -use calloop::{Interest, LoopSignal, Mode, PostAction}; +use calloop::timer::{TimeoutAction, Timer}; +use calloop::{Interest, LoopHandle, LoopSignal, Mode, PostAction}; use dpi::{PhysicalPosition, PhysicalSize}; use std::rc::Rc; use std::sync::mpsc; use std::sync::mpsc::Receiver; +use std::time::{Duration, Instant}; use x11rb::connection::Connection; use x11rb::errors::ConnectionError; use x11rb::protocol::Event as XEvent; @@ -72,6 +74,8 @@ impl EventLoop { ) -> Result { let loop_handle = inner.handle(); + Self::setup_fallback_frame_timer(&loop_handle)?; + loop_handle .insert_source( Generic::new_with_error(window.connection.conn.clone(), Interest::READ, Mode::Edge), @@ -99,33 +103,63 @@ impl EventLoop { } #[inline] - fn drain_xcb_events(&mut self) -> Result<(), FatalError> { + fn drain_xcb_events(&mut self) -> Result { + let mut event_received = false; while let Some(event) = self.window.connection.conn.poll_for_event()? { + event_received = true; self.handle_xcb_event(event)?; } + Ok(event_received) + } + + fn setup_fallback_frame_timer( + loop_handle: &LoopHandle<'_, Self>, + ) -> Result<(), calloop::Error> { + const FRAME_INTERVAL: Duration = Duration::from_millis(15); + + fn handle_frame(evloop: &mut EventLoop, previous_deadline: Instant) -> TimeoutAction { + evloop.exposed = true; + + // We'll try to keep a consistent frame pace. If the last frame couldn't be processed in + // the expected frame time, this will throttle down to prevent multiple frames from + // being queued up. + + let now = Instant::now(); + let next_deadline = if previous_deadline + FRAME_INTERVAL >= now { + now + FRAME_INTERVAL + } else { + previous_deadline + FRAME_INTERVAL + }; + + TimeoutAction::ToInstant(next_deadline) + } + + loop_handle + .insert_source(Timer::from_duration(FRAME_INTERVAL), |i, _, e| handle_frame(e, i)) + .map_err(|e| e.error)?; + Ok(()) } - fn handle_coalesced_expose_events(&mut self) -> Result<(), FatalError> { + fn handle_redraw(&mut self) { if !self.exposed { - return Ok(()); + return; } self.exposed = false; if !self.window.is_mapped.get() { - return Ok(()); + return; } - let _ = self.window.xcb_window.trigger_expose()?; - if let Err(e) = self.handler.on_frame() { self.run_error = Some(e.into()); self.stop_now(); - return Ok(()); + return; } - Ok(()) + // Any socket error will be handled in the next poll + let _ = self.window.connection.conn.flush(); } fn handle_coalesced_resize_events(&mut self) -> Result<(), FatalError> { @@ -237,13 +271,28 @@ impl EventLoop { } fn handle_idle(&mut self) { + if let Err(e) = self.try_handle_idle() { + self.run_error = Some(e.into()); + self.stop_now(); + } + } + + fn try_handle_idle(&mut self) -> Result<(), FatalError> { // Check for any events in the internal buffers before going to sleep: - let _ = self.drain_xcb_events(); // TODO: handle error + self.drain_xcb_events()?; - self.handle_coalesced_resize_events().unwrap(); // TODO: handle error - self.handle_coalesced_expose_events().unwrap(); + loop { + self.handle_coalesced_resize_events()?; + self.handle_redraw(); - self.window.connection.conn.flush().unwrap(); // TODO: handle error + if !self.drain_xcb_events()? { + break; + } + } + + self.window.connection.conn.flush()?; + + Ok(()) } pub fn run(mut self, mut inner: calloop::EventLoop) -> Result<(), Error> { @@ -418,7 +467,7 @@ impl EventLoop { XEvent::MapNotify(_) => { self.window.is_mapped.set(true); - self.window.xcb_window.trigger_expose()?; + self.exposed = true; } XEvent::UnmapNotify(_) => self.window.is_mapped.set(false), diff --git a/src/platform/x11/xcb_window.rs b/src/platform/x11/xcb_window.rs index 36b894c8..95086067 100644 --- a/src/platform/x11/xcb_window.rs +++ b/src/platform/x11/xcb_window.rs @@ -70,10 +70,6 @@ impl XcbWindow { self.connection.conn.unmap_window(self.window_id.get()) } - pub fn trigger_expose(&self) -> Result, ConnectionError> { - self.connection.conn.clear_area(true, self.window_id.get(), 0, 0, 0, 0) - } - pub fn resize( &self, size: PhysicalSize, ) -> Result, ConnectionError> { From b198604725bdc238dcda96f08d579f4976065d3e Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Wed, 12 Aug 2026 06:22:37 +0200 Subject: [PATCH 3/3] Improve error management --- src/platform/x11/event_loop.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/platform/x11/event_loop.rs b/src/platform/x11/event_loop.rs index 421691d0..4a2e22dc 100644 --- a/src/platform/x11/event_loop.rs +++ b/src/platform/x11/event_loop.rs @@ -153,8 +153,7 @@ impl EventLoop { } if let Err(e) = self.handler.on_frame() { - self.run_error = Some(e.into()); - self.stop_now(); + self.trigger_fatal_error(e.into()); return; } @@ -225,6 +224,13 @@ impl EventLoop { self.loop_signal.wakeup(); } + fn trigger_fatal_error(&mut self, error: Error) { + if self.run_error.is_none() { + self.run_error = Some(error); + } + self.stop_now(); + } + fn handle_request(&mut self, req: WindowThreadRequest) -> Result<(), Error> { match req { WindowThreadRequest::Resize(new_size) => { @@ -272,8 +278,7 @@ impl EventLoop { fn handle_idle(&mut self) { if let Err(e) = self.try_handle_idle() { - self.run_error = Some(e.into()); - self.stop_now(); + self.trigger_fatal_error(e.into()); } }