Skip to content

Commit 5afab1b

Browse files
jneemluleyleo
andauthored
Adds an idle loop to the x11 backend, and uses it for repainting. (#1072)
Co-authored-by: Leopold Luley <git@leopoldluley.de>
1 parent 5ec6ca4 commit 5afab1b

File tree

4 files changed

+307
-70
lines changed

4 files changed

+307
-70
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ You can find its changes [documented below](#060---2020-06-01).
3333
- GTK: Don't crash when receiving an external command while a file dialog is visible. ([#1043] by [@jneem])
3434
- Fix derive `Data` when type param bounds are defined ([#1058] by [@chris-zen])
3535
- Ensure that `update` is called after all commands. ([#1062] by [@jneem])
36+
- X11: Support idle callbacks. ([#1072] by [@jneem])
3637
- GTK: Don't interrupt `KeyEvent.repeat` when releasing another key. ([#1081] by [@raphlinus])
3738

3839
### Visual
@@ -356,6 +357,7 @@ Last release without a changelog :(
356357
[#1058]: https://github.com/linebender/druid/pull/1058
357358
[#1075]: https://github.com/linebender/druid/pull/1075
358359
[#1062]: https://github.com/linebender/druid/pull/1062
360+
[#1072]: https://github.com/linebender/druid/pull/1072
359361
[#1081]: https://github.com/linebender/druid/pull/1081
360362

361363
[Unreleased]: https://github.com/linebender/druid/compare/v0.6.0...master

druid-shell/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ rustdoc-args = ["--cfg", "docsrs"]
1414
default-target = "x86_64-pc-windows-msvc"
1515

1616
[features]
17-
x11 = ["x11rb", "cairo-sys-rs"]
17+
x11 = ["x11rb", "nix", "cairo-sys-rs"]
1818

1919
[dependencies]
2020
# NOTE: When changing the piet or kurbo versions, ensure that
@@ -40,6 +40,7 @@ gtk = { version = "0.8.1", optional = true }
4040
glib = { version = "0.9.3", optional = true }
4141
glib-sys = { version = "0.9.1", optional = true }
4242
gtk-sys = { version = "0.9.2", optional = true }
43+
nix = { version = "0.17.0", optional = true }
4344
x11rb = { version = "0.6.0", features = ["allow-unsafe-code", "present", "randr", "xfixes"], optional = true }
4445

4546
[target.'cfg(target_os="windows")'.dependencies]

druid-shell/src/platform/x11/application.rs

Lines changed: 162 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@
1616
1717
use std::cell::RefCell;
1818
use std::collections::HashMap;
19-
use std::convert::TryInto;
19+
use std::convert::{TryFrom, TryInto};
20+
use std::os::unix::io::RawFd;
2021
use std::rc::Rc;
22+
use std::time::{Duration, Instant};
2123

2224
use anyhow::{anyhow, Context, Error};
2325
use x11rb::connection::Connection;
@@ -30,6 +32,7 @@ use x11rb::xcb_ffi::XCBConnection;
3032
use crate::application::AppHandler;
3133

3234
use super::clipboard::Clipboard;
35+
use super::util;
3336
use super::window::Window;
3437

3538
#[derive(Clone)]
@@ -41,6 +44,11 @@ pub(crate) struct Application {
4144
///
4245
/// A display is a collection of screens.
4346
connection: Rc<XCBConnection>,
47+
/// An `XCBConnection` is *technically* safe to use from other threads, but there are
48+
/// subtleties; see https://github.com/psychon/x11rb/blob/41ab6610f44f5041e112569684fc58cd6d690e57/src/event_loop_integration.rs.
49+
/// Let's just avoid the issue altogether. As far as public API is concerned, this causes
50+
/// `druid_shell::WindowHandle` to be `!Send` and `!Sync`.
51+
marker: std::marker::PhantomData<*mut XCBConnection>,
4452
/// The default screen of the connected display.
4553
///
4654
/// The connected display may also have additional screens.
@@ -62,6 +70,12 @@ pub(crate) struct Application {
6270
window_id: u32,
6371
/// The mutable `Application` state.
6472
state: Rc<RefCell<State>>,
73+
/// The read end of the "idle pipe", a pipe that allows the event loop to be woken up from
74+
/// other threads.
75+
idle_read: RawFd,
76+
/// The write end of the "idle pipe", a pipe that allows the event loop to be woken up from
77+
/// other threads.
78+
idle_write: RawFd,
6579
/// The major opcode of the Present extension, if it is supported.
6680
present_opcode: Option<u8>,
6781
}
@@ -92,6 +106,8 @@ impl Application {
92106
windows: HashMap::new(),
93107
}));
94108

109+
let (idle_read, idle_write) = nix::unistd::pipe2(nix::fcntl::OFlag::O_NONBLOCK)?;
110+
95111
let present_opcode = if std::env::var_os("DRUID_SHELL_DISABLE_X11_PRESENT").is_some() {
96112
// Allow disabling Present with an environment variable.
97113
None
@@ -110,7 +126,10 @@ impl Application {
110126
screen_num: screen_num as i32,
111127
window_id,
112128
state,
129+
idle_read,
130+
idle_write,
113131
present_opcode,
132+
marker: std::marker::PhantomData,
114133
})
115134
}
116135

@@ -350,23 +369,67 @@ impl Application {
350369
Ok(false)
351370
}
352371

353-
pub fn run(self, _handler: Option<Box<dyn AppHandler>>) {
372+
fn run_inner(self) -> Result<(), Error> {
373+
// Try to figure out the refresh rate of the current screen. We run the idle loop at that
374+
// rate. The rate-limiting of the idle loop has two purposes:
375+
// - When the present extension is disabled, we paint in the idle loop. By limiting the
376+
// idle loop to the monitor's refresh rate, we aren't painting unnecessarily.
377+
// - By running idle commands at a limited rate, we limit spurious wake-ups: if the X11
378+
// connection is otherwise idle, we'll wake up at most once per frame, run *all* the
379+
// pending idle commands, and then go back to sleep.
380+
let refresh_rate = util::refresh_rate(self.connection(), self.window_id).unwrap_or(60.0);
381+
let timeout = Duration::from_millis((1000.0 / refresh_rate) as u64);
382+
let mut last_idle_time = Instant::now();
354383
loop {
355-
if let Ok(ev) = self.connection.wait_for_event() {
384+
let next_idle_time = last_idle_time + timeout;
385+
self.connection.flush()?;
386+
387+
// Before we poll on the connection's file descriptor, check whether there are any
388+
// events ready. It could be that XCB has some events in its internal buffers because
389+
// of something that happened during the idle loop.
390+
let mut event = self.connection.poll_for_event()?;
391+
392+
if event.is_none() {
393+
poll_with_timeout(&self.connection, self.idle_read, next_idle_time)
394+
.context("Error while waiting for X11 connection")?;
395+
}
396+
397+
while let Some(ev) = event {
356398
match self.handle_event(&ev) {
357399
Ok(quit) => {
358400
if quit {
359-
break;
401+
return Ok(());
360402
}
361403
}
362404
Err(e) => {
363405
log::error!("Error handling event: {:#}", e);
364406
}
365407
}
408+
event = self.connection.poll_for_event()?;
409+
}
410+
411+
let now = Instant::now();
412+
if now >= next_idle_time {
413+
last_idle_time = now;
414+
drain_idle_pipe(self.idle_read)?;
415+
416+
if let Ok(state) = self.state.try_borrow() {
417+
for w in state.windows.values() {
418+
w.run_idle();
419+
}
420+
} else {
421+
log::error!("In idle loop, application state already borrowed");
422+
}
366423
}
367424
}
368425
}
369426

427+
pub fn run(self, _handler: Option<Box<dyn AppHandler>>) {
428+
if let Err(e) = self.run_inner() {
429+
log::error!("{}", e);
430+
}
431+
}
432+
370433
pub fn quit(&self) {
371434
if let Ok(mut state) = self.state.try_borrow_mut() {
372435
if !state.quitting {
@@ -380,7 +443,6 @@ impl Application {
380443
for window in state.windows.values() {
381444
window.destroy();
382445
}
383-
log_x11!(self.connection.flush());
384446
}
385447
}
386448
} else {
@@ -390,7 +452,12 @@ impl Application {
390452

391453
fn finalize_quit(&self) {
392454
log_x11!(self.connection.destroy_window(self.window_id));
393-
log_x11!(self.connection.flush());
455+
if let Err(e) = nix::unistd::close(self.idle_read) {
456+
log::error!("Error closing idle_read: {}", e);
457+
}
458+
if let Err(e) = nix::unistd::close(self.idle_write) {
459+
log::error!("Error closing idle_write: {}", e);
460+
}
394461
}
395462

396463
pub fn clipboard(&self) -> Clipboard {
@@ -404,4 +471,93 @@ impl Application {
404471
log::warn!("Application::get_locale is currently unimplemented for X11 platforms. (defaulting to en-US)");
405472
"en-US".into()
406473
}
474+
475+
pub(crate) fn idle_pipe(&self) -> RawFd {
476+
self.idle_write
477+
}
478+
}
479+
480+
/// Clears out our idle pipe; `idle_read` should be the reading end of a pipe that was opened with
481+
/// O_NONBLOCK.
482+
fn drain_idle_pipe(idle_read: RawFd) -> Result<(), Error> {
483+
// Each write to the idle pipe adds one byte; it's unlikely that there will be much in it, but
484+
// read it 16 bytes at a time just in case.
485+
let mut read_buf = [0u8; 16];
486+
loop {
487+
match nix::unistd::read(idle_read, &mut read_buf[..]) {
488+
Err(nix::Error::Sys(nix::errno::Errno::EINTR)) => {}
489+
// According to write(2), this is the outcome of reading an empty, O_NONBLOCK
490+
// pipe.
491+
Err(nix::Error::Sys(nix::errno::Errno::EAGAIN)) => {
492+
break;
493+
}
494+
Err(e) => {
495+
return Err(e).context("Failed to read from idle pipe");
496+
}
497+
// According to write(2), this is the outcome of reading an O_NONBLOCK pipe
498+
// when the other end has been closed. This shouldn't happen to us because we
499+
// own both ends, but just in case.
500+
Ok(0) => {
501+
break;
502+
}
503+
Ok(_) => {}
504+
}
505+
}
506+
Ok(())
507+
}
508+
509+
/// Returns when there is an event ready to read from `conn`, or we got signalled by another thread
510+
/// writing into our idle pipe and the `timeout` has passed.
511+
// This was taken, with minor modifications, from the xclock_utc example in the x11rb crate.
512+
// https://github.com/psychon/x11rb/blob/a6bd1453fd8e931394b9b1f2185fad48b7cca5fe/examples/xclock_utc.rs
513+
fn poll_with_timeout(conn: &Rc<XCBConnection>, idle: RawFd, timeout: Instant) -> Result<(), Error> {
514+
use nix::poll::{poll, PollFd, PollFlags};
515+
use std::os::raw::c_int;
516+
use std::os::unix::io::AsRawFd;
517+
518+
let fd = conn.as_raw_fd();
519+
let mut both_poll_fds = [
520+
PollFd::new(fd, PollFlags::POLLIN),
521+
PollFd::new(idle, PollFlags::POLLIN),
522+
];
523+
let mut just_connection = [PollFd::new(fd, PollFlags::POLLIN)];
524+
let mut poll_fds = &mut both_poll_fds[..];
525+
526+
// We start with no timeout in the poll call. If we get something from the idle handler, we'll
527+
// start setting one.
528+
let mut poll_timeout = -1;
529+
loop {
530+
fn readable(p: PollFd) -> bool {
531+
p.revents()
532+
.unwrap_or_else(PollFlags::empty)
533+
.contains(PollFlags::POLLIN)
534+
};
535+
536+
match poll(poll_fds, poll_timeout) {
537+
Ok(_) => {
538+
if readable(poll_fds[0]) {
539+
// There is an X11 event ready to be handled.
540+
break;
541+
}
542+
if poll_fds.len() == 1 || readable(poll_fds[1]) {
543+
// Now that we got signalled, stop polling from the idle pipe and use a timeout
544+
// instead.
545+
poll_fds = &mut just_connection;
546+
547+
let now = Instant::now();
548+
if now >= timeout {
549+
break;
550+
} else {
551+
poll_timeout = c_int::try_from(timeout.duration_since(now).as_millis())
552+
.unwrap_or(c_int::max_value())
553+
}
554+
}
555+
}
556+
557+
Err(nix::Error::Sys(nix::errno::Errno::EINTR)) => {}
558+
Err(e) => return Err(e.into()),
559+
}
560+
}
561+
562+
Ok(())
407563
}

0 commit comments

Comments
 (0)