linux/x11: Reduce input latency and ensure rerender priority (#13355)

This change ensures that we always render a window according to its
refresh rate, even if there are a lot of X11 events.

We're working around some limitations of `calloop`. In the future, we
think we should revisit how the event loop is implemented on X11, so
that we can ensure proper prioritization of input events vs. rendering.

Release Notes:

- N/A

Co-authored-by: Antonio <me@as-cii.com>
This commit is contained in:
Thorsten Ball 2024-06-21 12:14:55 +02:00 committed by GitHub
parent 04a79780d8
commit f69c8ca74e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 79 additions and 60 deletions

View file

@ -13,7 +13,6 @@ 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 _};
@ -46,7 +45,7 @@ use crate::platform::linux::xdg_desktop_portal::{Event as XDPEvent, XDPEventSour
pub(super) const XINPUT_MASTER_DEVICE: u16 = 1; pub(super) const XINPUT_MASTER_DEVICE: u16 = 1;
pub(crate) struct WindowRef { pub(crate) struct WindowRef {
window: X11WindowStatePtr, pub window: X11WindowStatePtr,
refresh_event_token: RegistrationToken, refresh_event_token: RegistrationToken,
} }
@ -292,13 +291,39 @@ impl X11Client {
.insert_source( .insert_source(
Generic::new_with_error::<EventHandlerError>( Generic::new_with_error::<EventHandlerError>(
fd, fd,
calloop::Interest::READ, calloop::Interest::BOTH,
calloop::Mode::Level, calloop::Mode::Level,
), ),
{ {
let xcb_connection = xcb_connection.clone(); let xcb_connection = xcb_connection.clone();
move |_readiness, _, client| { move |_readiness, _, client| {
let windows = client
.0
.borrow()
.windows
.values()
.map(|window_ref| window_ref.window.clone())
.collect::<Vec<_>>();
while let Some(event) = xcb_connection.poll_for_event()? { while let Some(event) = xcb_connection.poll_for_event()? {
for window in &windows {
let last_render_at;
let refresh_rate;
{
let window_state = window.state.borrow();
last_render_at = window_state.last_render_at;
refresh_rate = window_state.refresh_rate;
}
if let Some(last_render_at) = last_render_at {
if last_render_at.elapsed() >= refresh_rate {
window.refresh();
}
} else {
window.refresh();
}
}
let mut state = client.0.borrow_mut(); let mut state = client.0.borrow_mut();
if state.ximc.is_none() || state.xim_handler.is_none() { if state.ximc.is_none() || state.xim_handler.is_none() {
drop(state); drop(state);
@ -955,60 +980,18 @@ 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 let refresh_event_token = state
.loop_handle .loop_handle
.insert_source(calloop::timer::Timer::immediate(), { .insert_source(calloop::timer::Timer::immediate(), {
let refresh_duration = mode_refresh_rate(mode); let window = window.0.clone();
move |mut instant, (), client| { let refresh_rate = window.state.borrow().refresh_rate;
let state = client.0.borrow_mut(); move |mut instant, (), _| {
state window.refresh();
.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 // Take into account that some frames have been skipped
let now = Instant::now(); let now = Instant::now();
while instant < now { while instant < now {
instant += refresh_duration; instant += refresh_rate;
} }
calloop::timer::TimeoutAction::ToInstant(instant) calloop::timer::TimeoutAction::ToInstant(instant)
} }
@ -1146,15 +1129,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 {
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, Instant},
}; };
use super::{X11Display, XINPUT_MASTER_DEVICE}; use super::{X11Display, XINPUT_MASTER_DEVICE};
@ -159,6 +161,8 @@ pub struct Callbacks {
pub struct X11WindowState { pub struct X11WindowState {
pub destroyed: bool, pub destroyed: bool,
pub last_render_at: Option<Instant>,
pub refresh_rate: Duration,
client: X11ClientStatePtr, client: X11ClientStatePtr,
executor: ForegroundExecutor, executor: ForegroundExecutor,
atoms: XcbAtoms, atoms: XcbAtoms,
@ -389,6 +393,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,
@ -405,6 +434,8 @@ impl X11WindowState {
appearance, appearance,
handle, handle,
destroyed: false, destroyed: false,
last_render_at: None,
refresh_rate,
}) })
} }
@ -574,6 +605,11 @@ impl X11WindowStatePtr {
let mut cb = self.callbacks.borrow_mut(); let mut cb = self.callbacks.borrow_mut();
if let Some(ref mut fun) = cb.request_frame { if let Some(ref mut fun) = cb.request_frame {
fun(); fun();
self.state
.borrow_mut()
.last_render_at
.replace(Instant::now());
} }
} }
@ -1020,3 +1056,12 @@ impl PlatformWindow for X11Window {
false false
} }
} }
// 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 {
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)
}