linux/x11: Custom run loop with mio instead of calloop (#13646)

This changes the implementation of the X11 client to use `mio`, as a
polling mechanism, and a custom run loop instead of `calloop` and its
callback-based approach.

We're doing this for one big reason: more control over how we handle
events.

With `calloop` we don't have any control over which events are processed
when and how long they're processes for. For example: we could be
blasted with 150 input events from X11 and miss a frame while processing
them, but instead of then drawing a new frame, calloop could decide to
work off the runnables that were generated from application-level code,
which would then again cause us to be behind.

We kinda worked around some of that in
https://github.com/zed-industries/zed/pull/12839 but the problem still
persists.

So what we're doing here is to use `mio` as a polling-mechanism. `mio`
notifies us if there are X11 on the XCB connection socket to be
processed. We also use its timeout mechanism to make sure that we don't
wait for events when we should render frames.

On top of `mio` we now have a custom run loop that allows us to decide
how much time to spend on what — input events, rendering windows, XDG
events, runnables — and in what order we work things off.

This custom run loop is consciously "dumb": we render all windows at the
highest frame rate right now, because we want to keep things predictable
for now while we test this approach more. We can then always switch to
more granular timings. But considering that our loop runs and checks for
windows to be redrawn whenever there's an event, this is more an
optimization than a requirement.

One reason for why we're doing this for X11 but not for Wayland is due
to how peculiar X11's event handling is: it's asynchronous and by
default X11 generates synthetic events when a key is held down. That can
lead to us being flooded with input events if someone keeps a key
pressed.

So another optimization that's in here is inspired by [GLFW's X11 input
handling](b35641f4a3/src/x11_window.c (L1321-L1349)):
based on a heuristic we detect whether a `KeyRelease` event was
auto-generated and if so, we drop it. That essentially halves the amount
of events we have to process when someone keeps a key pressed.

Release Notes:

- N/A

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Conrad <conrad@zed.dev>
This commit is contained in:
Thorsten Ball 2024-07-03 17:05:26 +02:00 committed by GitHub
parent 3348c3ab4c
commit 64755a7aea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 423 additions and 237 deletions

28
Cargo.lock generated
View file

@ -4889,6 +4889,7 @@ dependencies = [
"log", "log",
"media", "media",
"metal", "metal",
"mio 1.0.0",
"num_cpus", "num_cpus",
"objc", "objc",
"oo7", "oo7",
@ -5143,9 +5144,9 @@ dependencies = [
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.3.3" version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
[[package]] [[package]]
name = "hex" name = "hex"
@ -5659,7 +5660,7 @@ version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2"
dependencies = [ dependencies = [
"hermit-abi 0.3.3", "hermit-abi 0.3.9",
"libc", "libc",
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
@ -5690,7 +5691,7 @@ dependencies = [
"fnv", "fnv",
"lazy_static", "lazy_static",
"libc", "libc",
"mio", "mio 0.8.11",
"rand 0.8.5", "rand 0.8.5",
"serde", "serde",
"tempfile", "tempfile",
@ -6639,6 +6640,19 @@ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
[[package]]
name = "mio"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4929e1f84c5e54c3ec6141cd5d8b5a5c055f031f80cf78f2072920173cb4d880"
dependencies = [
"hermit-abi 0.3.9",
"libc",
"log",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "miow" name = "miow"
version = "0.6.0" version = "0.6.0"
@ -6877,7 +6891,7 @@ dependencies = [
"kqueue", "kqueue",
"libc", "libc",
"log", "log",
"mio", "mio 0.8.11",
"walkdir", "walkdir",
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
@ -7057,7 +7071,7 @@ version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
dependencies = [ dependencies = [
"hermit-abi 0.3.3", "hermit-abi 0.3.9",
"libc", "libc",
] ]
@ -11098,7 +11112,7 @@ dependencies = [
"backtrace", "backtrace",
"bytes 1.5.0", "bytes 1.5.0",
"libc", "libc",
"mio", "mio 0.8.11",
"num_cpus", "num_cpus",
"parking_lot", "parking_lot",
"pin-project-lite", "pin-project-lite",

View file

@ -141,6 +141,7 @@ xim = { git = "https://github.com/npmania/xim-rs", rev = "27132caffc5b9bc9c432ca
] } ] }
font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "5a5c4d4", features = ["source-fontconfig-dlopen"] } font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "5a5c4d4", features = ["source-fontconfig-dlopen"] }
x11-clipboard = "0.9.2" x11-clipboard = "0.9.2"
mio = { version = "1.0.0", features = ["os-poll", "os-ext"] }
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
windows.workspace = true windows.workspace = true

View file

@ -5,9 +5,10 @@ use calloop::{
timer::TimeoutAction, timer::TimeoutAction,
EventLoop, EventLoop,
}; };
use mio::Waker;
use parking::{Parker, Unparker}; use parking::{Parker, Unparker};
use parking_lot::Mutex; use parking_lot::Mutex;
use std::{thread, time::Duration}; use std::{sync::Arc, thread, time::Duration};
use util::ResultExt; use util::ResultExt;
struct TimerAfter { struct TimerAfter {
@ -18,6 +19,7 @@ struct TimerAfter {
pub(crate) struct LinuxDispatcher { pub(crate) struct LinuxDispatcher {
parker: Mutex<Parker>, parker: Mutex<Parker>,
main_sender: Sender<Runnable>, main_sender: Sender<Runnable>,
main_waker: Option<Arc<Waker>>,
timer_sender: Sender<TimerAfter>, timer_sender: Sender<TimerAfter>,
background_sender: flume::Sender<Runnable>, background_sender: flume::Sender<Runnable>,
_background_threads: Vec<thread::JoinHandle<()>>, _background_threads: Vec<thread::JoinHandle<()>>,
@ -25,7 +27,7 @@ pub(crate) struct LinuxDispatcher {
} }
impl LinuxDispatcher { impl LinuxDispatcher {
pub fn new(main_sender: Sender<Runnable>) -> Self { pub fn new(main_sender: Sender<Runnable>, main_waker: Option<Arc<Waker>>) -> Self {
let (background_sender, background_receiver) = flume::unbounded::<Runnable>(); let (background_sender, background_receiver) = flume::unbounded::<Runnable>();
let thread_count = std::thread::available_parallelism() let thread_count = std::thread::available_parallelism()
.map(|i| i.get()) .map(|i| i.get())
@ -77,6 +79,7 @@ impl LinuxDispatcher {
Self { Self {
parker: Mutex::new(Parker::new()), parker: Mutex::new(Parker::new()),
main_sender, main_sender,
main_waker,
timer_sender, timer_sender,
background_sender, background_sender,
_background_threads: background_threads, _background_threads: background_threads,
@ -96,6 +99,9 @@ impl PlatformDispatcher for LinuxDispatcher {
fn dispatch_on_main_thread(&self, runnable: Runnable) { fn dispatch_on_main_thread(&self, runnable: Runnable) {
self.main_sender.send(runnable).ok(); self.main_sender.send(runnable).ok();
if let Some(main_waker) = self.main_waker.as_ref() {
main_waker.wake().ok();
}
} }
fn dispatch_after(&self, duration: Duration, runnable: Runnable) { fn dispatch_after(&self, duration: Duration, runnable: Runnable) {

View file

@ -22,7 +22,7 @@ impl HeadlessClient {
pub(crate) fn new() -> Self { pub(crate) fn new() -> Self {
let event_loop = EventLoop::try_new().unwrap(); let event_loop = EventLoop::try_new().unwrap();
let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal()); let (common, main_receiver) = LinuxCommon::new(Box::new(event_loop.get_signal()), None);
let handle = event_loop.handle(); let handle = event_loop.handle();

View file

@ -26,6 +26,7 @@ use calloop::{EventLoop, LoopHandle, LoopSignal};
use filedescriptor::FileDescriptor; use filedescriptor::FileDescriptor;
use flume::{Receiver, Sender}; use flume::{Receiver, Sender};
use futures::channel::oneshot; use futures::channel::oneshot;
use mio::Waker;
use parking_lot::Mutex; use parking_lot::Mutex;
use time::UtcOffset; use time::UtcOffset;
use util::ResultExt; use util::ResultExt;
@ -84,6 +85,16 @@ pub(crate) struct PlatformHandlers {
pub(crate) validate_app_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>, pub(crate) validate_app_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
} }
pub trait QuitSignal {
fn quit(&mut self);
}
impl QuitSignal for LoopSignal {
fn quit(&mut self) {
self.stop();
}
}
pub(crate) struct LinuxCommon { pub(crate) struct LinuxCommon {
pub(crate) background_executor: BackgroundExecutor, pub(crate) background_executor: BackgroundExecutor,
pub(crate) foreground_executor: ForegroundExecutor, pub(crate) foreground_executor: ForegroundExecutor,
@ -91,17 +102,20 @@ pub(crate) struct LinuxCommon {
pub(crate) appearance: WindowAppearance, pub(crate) appearance: WindowAppearance,
pub(crate) auto_hide_scrollbars: bool, pub(crate) auto_hide_scrollbars: bool,
pub(crate) callbacks: PlatformHandlers, pub(crate) callbacks: PlatformHandlers,
pub(crate) signal: LoopSignal, pub(crate) quit_signal: Box<dyn QuitSignal>,
pub(crate) menus: Vec<OwnedMenu>, pub(crate) menus: Vec<OwnedMenu>,
} }
impl LinuxCommon { impl LinuxCommon {
pub fn new(signal: LoopSignal) -> (Self, Channel<Runnable>) { pub fn new(
quit_signal: Box<dyn QuitSignal>,
main_waker: Option<Arc<Waker>>,
) -> (Self, Channel<Runnable>) {
let (main_sender, main_receiver) = calloop::channel::channel::<Runnable>(); let (main_sender, main_receiver) = calloop::channel::channel::<Runnable>();
let text_system = Arc::new(CosmicTextSystem::new()); let text_system = Arc::new(CosmicTextSystem::new());
let callbacks = PlatformHandlers::default(); let callbacks = PlatformHandlers::default();
let dispatcher = Arc::new(LinuxDispatcher::new(main_sender.clone())); let dispatcher = Arc::new(LinuxDispatcher::new(main_sender.clone(), main_waker));
let background_executor = BackgroundExecutor::new(dispatcher.clone()); let background_executor = BackgroundExecutor::new(dispatcher.clone());
@ -112,7 +126,7 @@ impl LinuxCommon {
appearance: WindowAppearance::Light, appearance: WindowAppearance::Light,
auto_hide_scrollbars: false, auto_hide_scrollbars: false,
callbacks, callbacks,
signal, quit_signal,
menus: Vec::new(), menus: Vec::new(),
}; };
@ -146,7 +160,7 @@ impl<P: LinuxClient + 'static> Platform for P {
} }
fn quit(&self) { fn quit(&self) {
self.with_common(|common| common.signal.stop()); self.with_common(|common| common.quit_signal.quit());
} }
fn compositor_name(&self) -> &'static str { fn compositor_name(&self) -> &'static str {

View file

@ -310,7 +310,7 @@ impl WaylandClientStatePtr {
} }
} }
if state.windows.is_empty() { if state.windows.is_empty() {
state.common.signal.stop(); state.common.quit_signal.quit();
} }
} }
} }
@ -406,7 +406,7 @@ impl WaylandClient {
let event_loop = EventLoop::<WaylandClientStatePtr>::try_new().unwrap(); let event_loop = EventLoop::<WaylandClientStatePtr>::try_new().unwrap();
let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal()); let (common, main_receiver) = LinuxCommon::new(Box::new(event_loop.get_signal()), None);
let handle = event_loop.handle(); let handle = event_loop.handle();
handle handle
@ -443,7 +443,7 @@ impl WaylandClient {
let mut cursor = Cursor::new(&conn, &globals, 24); let mut cursor = Cursor::new(&conn, &globals, 24);
handle handle
.insert_source(XDPEventSource::new(&common.background_executor), { .insert_source(XDPEventSource::new(&common.background_executor, None), {
move |event, _, client| match event { move |event, _, client| match event {
XDPEvent::WindowAppearance(appearance) => { XDPEvent::WindowAppearance(appearance) => {
if let Some(client) = client.0.upgrade() { if let Some(client) = client.0.upgrade() {

View file

@ -1,19 +1,23 @@
use std::cell::RefCell; use std::cell::RefCell;
use std::collections::HashSet; use std::collections::HashSet;
use std::ops::Deref; use std::ops::Deref;
use std::os::fd::AsRawFd;
use std::rc::{Rc, Weak}; use std::rc::{Rc, Weak};
use std::sync::Arc;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use calloop::generic::{FdWrapper, Generic}; use anyhow::Context;
use calloop::{EventLoop, LoopHandle, RegistrationToken}; use async_task::Runnable;
use calloop::channel::Channel;
use collections::HashMap; use collections::HashMap;
use util::ResultExt;
use futures::channel::oneshot;
use mio::{Interest, Token, Waker};
use util::ResultExt;
use x11rb::connection::{Connection, RequestConnection}; use x11rb::connection::{Connection, RequestConnection};
use x11rb::cursor; use x11rb::cursor;
use x11rb::errors::ConnectionError; use x11rb::errors::ConnectionError;
use x11rb::protocol::randr::ConnectionExt as _;
use x11rb::protocol::xinput::ConnectionExt; use x11rb::protocol::xinput::ConnectionExt;
use x11rb::protocol::xkb::ConnectionExt as _; use x11rb::protocol::xkb::ConnectionExt as _;
use x11rb::protocol::xproto::{ChangeWindowAttributesAux, ConnectionExt as _}; use x11rb::protocol::xproto::{ChangeWindowAttributesAux, ConnectionExt as _};
@ -30,7 +34,7 @@ use crate::platform::{LinuxCommon, PlatformWindow};
use crate::{ use crate::{
modifiers_from_xinput_info, point, px, AnyWindowHandle, Bounds, ClipboardItem, CursorStyle, modifiers_from_xinput_info, point, px, AnyWindowHandle, Bounds, ClipboardItem, CursorStyle,
DisplayId, Keystroke, Modifiers, ModifiersChangedEvent, Pixels, PlatformDisplay, PlatformInput, DisplayId, Keystroke, Modifiers, ModifiersChangedEvent, Pixels, PlatformDisplay, PlatformInput,
Point, ScrollDelta, Size, TouchPhase, WindowParams, X11Window, Point, QuitSignal, ScrollDelta, Size, TouchPhase, WindowParams, X11Window,
}; };
use super::{ use super::{
@ -47,7 +51,6 @@ pub(super) const XINPUT_MASTER_DEVICE: u16 = 1;
pub(crate) struct WindowRef { pub(crate) struct WindowRef {
window: X11WindowStatePtr, window: X11WindowStatePtr,
refresh_event_token: RegistrationToken,
} }
impl WindowRef { impl WindowRef {
@ -95,15 +98,18 @@ impl From<xim::ClientError> for EventHandlerError {
} }
pub struct X11ClientState { pub struct X11ClientState {
pub(crate) loop_handle: LoopHandle<'static, X11Client>, /// poll is in an Option so we can take it out in `run()` without
pub(crate) event_loop: Option<calloop::EventLoop<'static, X11Client>>, /// mutating self.
poll: Option<mio::Poll>,
quit_signal_rx: oneshot::Receiver<()>,
runnables: Channel<Runnable>,
xdp_event_source: XDPEventSource,
pub(crate) last_click: Instant, pub(crate) last_click: Instant,
pub(crate) last_location: Point<Pixels>, pub(crate) last_location: Point<Pixels>,
pub(crate) current_count: usize, pub(crate) current_count: usize,
pub(crate) scale_factor: f32, pub(crate) scale_factor: f32,
pub(crate) xcb_connection: Rc<XCBConnection>, pub(crate) xcb_connection: Rc<XCBConnection>,
pub(crate) x_root_index: usize, pub(crate) x_root_index: usize,
pub(crate) _resource_database: Database, pub(crate) _resource_database: Database,
@ -139,14 +145,46 @@ impl X11ClientStatePtr {
let client = X11Client(self.0.upgrade().expect("client already dropped")); let client = X11Client(self.0.upgrade().expect("client already dropped"));
let mut state = client.0.borrow_mut(); let mut state = client.0.borrow_mut();
if let Some(window_ref) = state.windows.remove(&x_window) { if state.windows.remove(&x_window).is_none() {
state.loop_handle.remove(window_ref.refresh_event_token); log::warn!(
"failed to remove X window {} from client state, does not exist",
x_window
);
} }
state.cursor_styles.remove(&x_window); state.cursor_styles.remove(&x_window);
if state.windows.is_empty() { if state.windows.is_empty() {
state.common.signal.stop(); state.common.quit_signal.quit();
}
}
}
struct ChannelQuitSignal {
tx: Option<oneshot::Sender<()>>,
waker: Option<Arc<Waker>>,
}
impl ChannelQuitSignal {
fn new(waker: Option<Arc<Waker>>) -> (Self, oneshot::Receiver<()>) {
let (tx, rx) = oneshot::channel::<()>();
let quit_signal = ChannelQuitSignal {
tx: Some(tx),
waker,
};
(quit_signal, rx)
}
}
impl QuitSignal for ChannelQuitSignal {
fn quit(&mut self) {
if let Some(tx) = self.tx.take() {
tx.send(()).log_err();
if let Some(waker) = self.waker.as_ref() {
waker.wake().ok();
}
} }
} }
} }
@ -156,27 +194,12 @@ pub(crate) struct X11Client(Rc<RefCell<X11ClientState>>);
impl X11Client { impl X11Client {
pub(crate) fn new() -> Self { pub(crate) fn new() -> Self {
let event_loop = EventLoop::try_new().unwrap(); let mut poll = mio::Poll::new().unwrap();
let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal()); let waker = Arc::new(Waker::new(poll.registry(), WAKER_TOKEN).unwrap());
let handle = event_loop.handle(); let (quit_signal, quit_signal_rx) = ChannelQuitSignal::new(Some(waker.clone()));
let (common, runnables) = LinuxCommon::new(Box::new(quit_signal), Some(waker.clone()));
handle
.insert_source(main_receiver, {
let handle = handle.clone();
move |event, _, _: &mut X11Client| {
if let calloop::channel::Event::Msg(runnable) = event {
// Insert the runnables as idle callbacks, so we make sure that user-input and X11
// events have higher priority and runnables are only worked off after the event
// callbacks.
handle.insert_idle(|_| {
runnable.run();
});
}
}
})
.unwrap();
let (xcb_connection, x_root_index) = XCBConnection::connect(None).unwrap(); let (xcb_connection, x_root_index) = XCBConnection::connect(None).unwrap();
xcb_connection xcb_connection
@ -275,105 +298,18 @@ impl X11Client {
None None
}; };
// Safety: Safe if xcb::Connection always returns a valid fd let xdp_event_source =
let fd = unsafe { FdWrapper::new(Rc::clone(&xcb_connection)) }; XDPEventSource::new(&common.background_executor, Some(waker.clone()));
handle
.insert_source(
Generic::new_with_error::<EventHandlerError>(
fd,
calloop::Interest::READ,
calloop::Mode::Level,
),
{
let xcb_connection = xcb_connection.clone();
move |_readiness, _, client| {
let mut events = Vec::new();
let mut windows_to_refresh = HashSet::new();
while let Some(event) = xcb_connection.poll_for_event()? {
if let Event::Expose(event) = event {
windows_to_refresh.insert(event.window);
} else {
events.push(event);
}
}
for window in windows_to_refresh.into_iter() {
if let Some(window) = client.get_window(window) {
window.refresh();
}
}
for event in events.into_iter() {
let mut state = client.0.borrow_mut();
if state.ximc.is_none() || state.xim_handler.is_none() {
drop(state);
client.handle_event(event);
continue;
}
let mut ximc = state.ximc.take().unwrap();
let mut xim_handler = state.xim_handler.take().unwrap();
let xim_connected = xim_handler.connected;
drop(state);
let xim_filtered = match ximc.filter_event(&event, &mut xim_handler) {
Ok(handled) => handled,
Err(err) => {
log::error!("XIMClientError: {}", err);
false
}
};
let xim_callback_event = xim_handler.last_callback_event.take();
let mut state = client.0.borrow_mut();
state.ximc = Some(ximc);
state.xim_handler = Some(xim_handler);
drop(state);
if let Some(event) = xim_callback_event {
client.handle_xim_callback_event(event);
}
if xim_filtered {
continue;
}
if xim_connected {
client.xim_handle_event(event);
} else {
client.handle_event(event);
}
}
Ok(calloop::PostAction::Continue)
}
},
)
.expect("Failed to initialize x11 event source");
handle
.insert_source(XDPEventSource::new(&common.background_executor), {
move |event, _, client| match event {
XDPEvent::WindowAppearance(appearance) => {
client.with_common(|common| common.appearance = appearance);
for (_, window) in &mut client.0.borrow_mut().windows {
window.window.set_appearance(appearance);
}
}
XDPEvent::CursorTheme(_) | XDPEvent::CursorSize(_) => {
// noop, X11 manages this for us.
}
}
})
.unwrap();
X11Client(Rc::new(RefCell::new(X11ClientState { X11Client(Rc::new(RefCell::new(X11ClientState {
modifiers: Modifiers::default(), poll: Some(poll),
event_loop: Some(event_loop), runnables,
loop_handle: handle,
xdp_event_source,
quit_signal_rx,
common, common,
modifiers: Modifiers::default(),
last_click: Instant::now(), last_click: Instant::now(),
last_location: Point::new(px(0.0), px(0.0)), last_location: Point::new(px(0.0), px(0.0)),
current_count: 0, current_count: 0,
@ -468,6 +404,110 @@ impl X11Client {
.map(|window_reference| window_reference.window.clone()) .map(|window_reference| window_reference.window.clone())
} }
fn read_x11_events(&self) -> (HashSet<u32>, Vec<Event>) {
let mut events = Vec::new();
let mut windows_to_refresh = HashSet::new();
let mut state = self.0.borrow_mut();
let mut last_key_release: Option<Event> = None;
loop {
match state.xcb_connection.poll_for_event() {
Ok(Some(event)) => {
if let Event::Expose(expose_event) = event {
windows_to_refresh.insert(expose_event.window);
} else {
match event {
Event::KeyRelease(_) => {
last_key_release = Some(event);
}
Event::KeyPress(key_press) => {
if let Some(Event::KeyRelease(key_release)) =
last_key_release.take()
{
// We ignore that last KeyRelease if it's too close to this KeyPress,
// suggesting that it's auto-generated by X11 as a key-repeat event.
if key_release.detail != key_press.detail
|| key_press.time.wrapping_sub(key_release.time) > 20
{
events.push(Event::KeyRelease(key_release));
}
}
events.push(Event::KeyPress(key_press));
}
_ => {
if let Some(release_event) = last_key_release.take() {
events.push(release_event);
}
events.push(event);
}
}
}
}
Ok(None) => {
// Add any remaining stored KeyRelease event
if let Some(release_event) = last_key_release.take() {
events.push(release_event);
}
break;
}
Err(e) => {
log::warn!("error polling for X11 events: {e:?}");
break;
}
}
}
(windows_to_refresh, events)
}
fn process_x11_events(&self, events: Vec<Event>) {
for event in events.into_iter() {
let mut state = self.0.borrow_mut();
if state.ximc.is_none() || state.xim_handler.is_none() {
drop(state);
self.handle_event(event);
continue;
}
let mut ximc = state.ximc.take().unwrap();
let mut xim_handler = state.xim_handler.take().unwrap();
let xim_connected = xim_handler.connected;
drop(state);
// let xim_filtered = false;
let xim_filtered = match ximc.filter_event(&event, &mut xim_handler) {
Ok(handled) => handled,
Err(err) => {
log::error!("XIMClientError: {}", err);
false
}
};
let xim_callback_event = xim_handler.last_callback_event.take();
let mut state = self.0.borrow_mut();
state.ximc = Some(ximc);
state.xim_handler = Some(xim_handler);
if let Some(event) = xim_callback_event {
drop(state);
self.handle_xim_callback_event(event);
} else {
drop(state);
}
if xim_filtered {
continue;
}
if xim_connected {
self.xim_handle_event(event);
} else {
self.handle_event(event);
}
}
}
fn handle_event(&self, event: Event) -> Option<()> { fn handle_event(&self, event: Event) -> Option<()> {
match event { match event {
Event::ClientMessage(event) => { Event::ClientMessage(event) => {
@ -902,11 +942,13 @@ impl X11Client {
} }
} }
const XCB_CONNECTION_TOKEN: Token = Token(0);
const WAKER_TOKEN: Token = Token(1);
impl LinuxClient for X11Client { impl LinuxClient for X11Client {
fn compositor_name(&self) -> &'static str { fn compositor_name(&self) -> &'static str {
"X11" "X11"
} }
fn with_common<R>(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R { fn with_common<R>(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R {
f(&mut self.0.borrow_mut().common) f(&mut self.0.borrow_mut().common)
} }
@ -972,69 +1014,8 @@ impl LinuxClient for X11Client {
state.common.appearance, state.common.appearance,
)?; )?;
let screen_resources = state
.xcb_connection
.randr_get_screen_resources(x_window)
.unwrap()
.reply()
.expect("Could not find available screens");
let mode = screen_resources
.crtcs
.iter()
.find_map(|crtc| {
let crtc_info = state
.xcb_connection
.randr_get_crtc_info(*crtc, x11rb::CURRENT_TIME)
.ok()?
.reply()
.ok()?;
screen_resources
.modes
.iter()
.find(|m| m.id == crtc_info.mode)
})
.expect("Unable to find screen refresh rate");
let refresh_event_token = state
.loop_handle
.insert_source(calloop::timer::Timer::immediate(), {
let refresh_duration = mode_refresh_rate(mode);
move |mut instant, (), client| {
let state = client.0.borrow_mut();
state
.xcb_connection
.send_event(
false,
x_window,
xproto::EventMask::EXPOSURE,
xproto::ExposeEvent {
response_type: xproto::EXPOSE_EVENT,
sequence: 0,
window: x_window,
x: 0,
y: 0,
width: 0,
height: 0,
count: 1,
},
)
.unwrap();
let _ = state.xcb_connection.flush().unwrap();
// Take into account that some frames have been skipped
let now = Instant::now();
while instant < now {
instant += refresh_duration;
}
calloop::timer::TimeoutAction::ToInstant(instant)
}
})
.expect("Failed to initialize refresh timer");
let window_ref = WindowRef { let window_ref = WindowRef {
window: window.0.clone(), window: window.0.clone(),
refresh_event_token,
}; };
state.windows.insert(x_window, window_ref); state.windows.insert(x_window, window_ref);
@ -1157,14 +1138,123 @@ impl LinuxClient for X11Client {
} }
fn run(&self) { fn run(&self) {
let mut event_loop = self let mut poll = self
.0 .0
.borrow_mut() .borrow_mut()
.event_loop .poll
.take() .take()
.expect("App is already running"); .context("no poll set on X11Client. calling run more than once is not possible")
.unwrap();
event_loop.run(None, &mut self.clone(), |_| {}).log_err(); let xcb_fd = self.0.borrow().xcb_connection.as_raw_fd();
let mut xcb_source = mio::unix::SourceFd(&xcb_fd);
poll.registry()
.register(&mut xcb_source, XCB_CONNECTION_TOKEN, Interest::READABLE)
.unwrap();
let mut events = mio::Events::with_capacity(1024);
let mut next_refresh_needed = Instant::now();
'run_loop: loop {
let poll_timeout = next_refresh_needed - Instant::now();
// We rounding the poll_timeout down so `mio` doesn't round it up to the next higher milliseconds
let poll_timeout = Duration::from_millis(poll_timeout.as_millis() as u64);
if poll_timeout >= Duration::from_millis(1) {
let _ = poll.poll(&mut events, Some(poll_timeout));
};
let mut state = self.0.borrow_mut();
// Check if we need to quit
if let Ok(Some(())) = state.quit_signal_rx.try_recv() {
return;
}
// Redraw windows
let now = Instant::now();
if now > next_refresh_needed {
// This will be pulled down to 16ms (or less) if a window is open
let mut frame_length = Duration::from_millis(100);
let mut windows = vec![];
for (_, window_ref) in state.windows.iter() {
if !window_ref.window.state.borrow().destroyed {
frame_length = frame_length.min(window_ref.window.refresh_rate());
windows.push(window_ref.window.clone());
}
}
drop(state);
for window in windows {
window.refresh();
}
state = self.0.borrow_mut();
// In the case that we're looping a bit too fast, slow down
next_refresh_needed = now.max(next_refresh_needed) + frame_length;
}
// X11 events
drop(state);
loop {
let (x_windows, events) = self.read_x11_events();
for x_window in x_windows {
if let Some(window) = self.get_window(x_window) {
window.refresh();
}
}
if events.len() == 0 {
break;
}
self.process_x11_events(events);
// When X11 is sending us events faster than we can handle we'll
// let the frame rate drop to 10fps to try and avoid getting too behind.
if Instant::now() > next_refresh_needed + Duration::from_millis(80) {
continue 'run_loop;
}
}
state = self.0.borrow_mut();
// Runnables
while let Ok(runnable) = state.runnables.try_recv() {
drop(state);
runnable.run();
state = self.0.borrow_mut();
if Instant::now() + Duration::from_millis(1) >= next_refresh_needed {
continue 'run_loop;
}
}
// XDG events
if let Ok(event) = state.xdp_event_source.try_recv() {
match event {
XDPEvent::WindowAppearance(appearance) => {
let mut windows = state
.windows
.values()
.map(|window| window.window.clone())
.collect::<Vec<_>>();
drop(state);
self.with_common(|common| common.appearance = appearance);
for mut window in windows {
window.set_appearance(appearance);
}
}
XDPEvent::CursorTheme(_) | XDPEvent::CursorSize(_) => {
// noop, X11 manages this for us.
}
};
};
}
} }
fn active_window(&self) -> Option<AnyWindowHandle> { fn active_window(&self) -> Option<AnyWindowHandle> {
@ -1178,19 +1268,6 @@ impl LinuxClient for X11Client {
} }
} }
// Adatpted from:
// https://docs.rs/winit/0.29.11/src/winit/platform_impl/linux/x11/monitor.rs.html#103-111
pub fn mode_refresh_rate(mode: &randr::ModeInfo) -> Duration {
if mode.dot_clock == 0 || mode.htotal == 0 || mode.vtotal == 0 {
return Duration::from_millis(16);
}
let millihertz = mode.dot_clock as u64 * 1_000 / (mode.htotal as u64 * mode.vtotal as u64);
let micros = 1_000_000_000 / millihertz;
log::info!("Refreshing at {} micros", micros);
Duration::from_micros(micros)
}
fn fp3232_to_f32(value: xinput::Fp3232) -> f32 { fn fp3232_to_f32(value: xinput::Fp3232) -> f32 {
value.integral as f32 + value.frac as f32 / u32::MAX as f32 value.integral as f32 + value.frac as f32 / u32::MAX as f32
} }

View file

@ -14,6 +14,7 @@ use util::{maybe, ResultExt};
use x11rb::{ use x11rb::{
connection::Connection, connection::Connection,
protocol::{ protocol::{
randr::{self, ConnectionExt as _},
xinput::{self, ConnectionExt as _}, xinput::{self, ConnectionExt as _},
xproto::{ xproto::{
self, ClientMessageEvent, ConnectionExt as _, EventMask, TranslateCoordinatesReply, self, ClientMessageEvent, ConnectionExt as _, EventMask, TranslateCoordinatesReply,
@ -31,6 +32,7 @@ use std::{
ptr::NonNull, ptr::NonNull,
rc::Rc, rc::Rc,
sync::{self, Arc}, sync::{self, Arc},
time::Duration,
}; };
use super::{X11Display, XINPUT_MASTER_DEVICE}; use super::{X11Display, XINPUT_MASTER_DEVICE};
@ -159,6 +161,7 @@ pub struct Callbacks {
pub struct X11WindowState { pub struct X11WindowState {
pub destroyed: bool, pub destroyed: bool,
refresh_rate: Duration,
client: X11ClientStatePtr, client: X11ClientStatePtr,
executor: ForegroundExecutor, executor: ForegroundExecutor,
atoms: XcbAtoms, atoms: XcbAtoms,
@ -178,7 +181,7 @@ pub(crate) struct X11WindowStatePtr {
pub state: Rc<RefCell<X11WindowState>>, pub state: Rc<RefCell<X11WindowState>>,
pub(crate) callbacks: Rc<RefCell<Callbacks>>, pub(crate) callbacks: Rc<RefCell<Callbacks>>,
xcb_connection: Rc<XCBConnection>, xcb_connection: Rc<XCBConnection>,
x_window: xproto::Window, pub x_window: xproto::Window,
} }
impl rwh::HasWindowHandle for RawWindow { impl rwh::HasWindowHandle for RawWindow {
@ -397,6 +400,31 @@ impl X11WindowState {
}; };
xcb_connection.map_window(x_window).unwrap(); xcb_connection.map_window(x_window).unwrap();
let screen_resources = xcb_connection
.randr_get_screen_resources(x_window)
.unwrap()
.reply()
.expect("Could not find available screens");
let mode = screen_resources
.crtcs
.iter()
.find_map(|crtc| {
let crtc_info = xcb_connection
.randr_get_crtc_info(*crtc, x11rb::CURRENT_TIME)
.ok()?
.reply()
.ok()?;
screen_resources
.modes
.iter()
.find(|m| m.id == crtc_info.mode)
})
.expect("Unable to find screen refresh rate");
let refresh_rate = mode_refresh_rate(&mode);
Ok(Self { Ok(Self {
client, client,
executor, executor,
@ -413,6 +441,7 @@ impl X11WindowState {
appearance, appearance,
handle, handle,
destroyed: false, destroyed: false,
refresh_rate,
}) })
} }
@ -715,6 +744,10 @@ impl X11WindowStatePtr {
(fun)() (fun)()
} }
} }
pub fn refresh_rate(&self) -> Duration {
self.state.borrow().refresh_rate
}
} }
impl PlatformWindow for X11Window { impl PlatformWindow for X11Window {
@ -1039,3 +1072,16 @@ impl PlatformWindow for X11Window {
false false
} }
} }
// Adapted from:
// https://docs.rs/winit/0.29.11/src/winit/platform_impl/linux/x11/monitor.rs.html#103-111
pub fn mode_refresh_rate(mode: &randr::ModeInfo) -> Duration {
if mode.dot_clock == 0 || mode.htotal == 0 || mode.vtotal == 0 {
return Duration::from_millis(16);
}
let millihertz = mode.dot_clock as u64 * 1_000 / (mode.htotal as u64 * mode.vtotal as u64);
let micros = 1_000_000_000 / millihertz;
log::info!("Refreshing at {} micros", micros);
Duration::from_micros(micros)
}

View file

@ -2,9 +2,13 @@
//! //!
//! This module uses the [ashpd] crate //! This module uses the [ashpd] crate
use std::sync::Arc;
use anyhow::anyhow;
use ashpd::desktop::settings::{ColorScheme, Settings}; use ashpd::desktop::settings::{ColorScheme, Settings};
use calloop::channel::Channel; use calloop::channel::{Channel, Sender};
use calloop::{EventSource, Poll, PostAction, Readiness, Token, TokenFactory}; use calloop::{EventSource, Poll, PostAction, Readiness, Token, TokenFactory};
use mio::Waker;
use smol::stream::StreamExt; use smol::stream::StreamExt;
use crate::{BackgroundExecutor, WindowAppearance}; use crate::{BackgroundExecutor, WindowAppearance};
@ -20,31 +24,45 @@ pub struct XDPEventSource {
} }
impl XDPEventSource { impl XDPEventSource {
pub fn new(executor: &BackgroundExecutor) -> Self { pub fn new(executor: &BackgroundExecutor, waker: Option<Arc<Waker>>) -> Self {
let (sender, channel) = calloop::channel::channel(); let (sender, channel) = calloop::channel::channel();
let background = executor.clone(); let background = executor.clone();
executor executor
.spawn(async move { .spawn(async move {
fn send_event<T>(
sender: &Sender<T>,
waker: &Option<Arc<Waker>>,
event: T,
) -> Result<(), std::sync::mpsc::SendError<T>> {
sender.send(event)?;
if let Some(waker) = waker {
waker.wake().ok();
};
Ok(())
}
let settings = Settings::new().await?; let settings = Settings::new().await?;
if let Ok(initial_appearance) = settings.color_scheme().await { if let Ok(initial_appearance) = settings.color_scheme().await {
sender.send(Event::WindowAppearance(WindowAppearance::from_native( send_event(
initial_appearance, &sender,
)))?; &waker,
Event::WindowAppearance(WindowAppearance::from_native(initial_appearance)),
)?;
} }
if let Ok(initial_theme) = settings if let Ok(initial_theme) = settings
.read::<String>("org.gnome.desktop.interface", "cursor-theme") .read::<String>("org.gnome.desktop.interface", "cursor-theme")
.await .await
{ {
sender.send(Event::CursorTheme(initial_theme))?; send_event(&sender, &waker, Event::CursorTheme(initial_theme))?;
} }
if let Ok(initial_size) = settings if let Ok(initial_size) = settings
.read::<u32>("org.gnome.desktop.interface", "cursor-size") .read::<u32>("org.gnome.desktop.interface", "cursor-size")
.await .await
{ {
sender.send(Event::CursorSize(initial_size))?; send_event(&sender, &waker, Event::CursorSize(initial_size))?;
} }
if let Ok(mut cursor_theme_changed) = settings if let Ok(mut cursor_theme_changed) = settings
@ -55,11 +73,12 @@ impl XDPEventSource {
.await .await
{ {
let sender = sender.clone(); let sender = sender.clone();
let waker = waker.clone();
background background
.spawn(async move { .spawn(async move {
while let Some(theme) = cursor_theme_changed.next().await { while let Some(theme) = cursor_theme_changed.next().await {
let theme = theme?; let theme = theme?;
sender.send(Event::CursorTheme(theme))?; send_event(&sender, &waker, Event::CursorTheme(theme))?;
} }
anyhow::Ok(()) anyhow::Ok(())
}) })
@ -74,11 +93,12 @@ impl XDPEventSource {
.await .await
{ {
let sender = sender.clone(); let sender = sender.clone();
let waker = waker.clone();
background background
.spawn(async move { .spawn(async move {
while let Some(size) = cursor_size_changed.next().await { while let Some(size) = cursor_size_changed.next().await {
let size = size?; let size = size?;
sender.send(Event::CursorSize(size))?; send_event(&sender, &waker, Event::CursorSize(size))?;
} }
anyhow::Ok(()) anyhow::Ok(())
}) })
@ -87,9 +107,11 @@ impl XDPEventSource {
let mut appearance_changed = settings.receive_color_scheme_changed().await?; let mut appearance_changed = settings.receive_color_scheme_changed().await?;
while let Some(scheme) = appearance_changed.next().await { while let Some(scheme) = appearance_changed.next().await {
sender.send(Event::WindowAppearance(WindowAppearance::from_native( send_event(
scheme, &sender,
)))?; &waker,
Event::WindowAppearance(WindowAppearance::from_native(scheme)),
)?;
} }
anyhow::Ok(()) anyhow::Ok(())
@ -98,6 +120,12 @@ impl XDPEventSource {
Self { channel } Self { channel }
} }
pub fn try_recv(&self) -> anyhow::Result<Event> {
self.channel
.try_recv()
.map_err(|error| anyhow!("{}", error))
}
} }
impl EventSource for XDPEventSource { impl EventSource for XDPEventSource {