Skip to content

Commit

Permalink
Adds an idle loop to the x11 backend, and uses it for repainting. (#1072
Browse files Browse the repository at this point in the history
)

Co-authored-by: Leopold Luley <[email protected]>
  • Loading branch information
jneem and luleyleo authored Jul 18, 2020
1 parent 5ec6ca4 commit 5afab1b
Show file tree
Hide file tree
Showing 4 changed files with 307 additions and 70 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ You can find its changes [documented below](#060---2020-06-01).
- GTK: Don't crash when receiving an external command while a file dialog is visible. ([#1043] by [@jneem])
- Fix derive `Data` when type param bounds are defined ([#1058] by [@chris-zen])
- Ensure that `update` is called after all commands. ([#1062] by [@jneem])
- X11: Support idle callbacks. ([#1072] by [@jneem])
- GTK: Don't interrupt `KeyEvent.repeat` when releasing another key. ([#1081] by [@raphlinus])

### Visual
Expand Down Expand Up @@ -356,6 +357,7 @@ Last release without a changelog :(
[#1058]: https://github.com/linebender/druid/pull/1058
[#1075]: https://github.com/linebender/druid/pull/1075
[#1062]: https://github.com/linebender/druid/pull/1062
[#1072]: https://github.com/linebender/druid/pull/1072
[#1081]: https://github.com/linebender/druid/pull/1081

[Unreleased]: https://github.com/linebender/druid/compare/v0.6.0...master
Expand Down
3 changes: 2 additions & 1 deletion druid-shell/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ rustdoc-args = ["--cfg", "docsrs"]
default-target = "x86_64-pc-windows-msvc"

[features]
x11 = ["x11rb", "cairo-sys-rs"]
x11 = ["x11rb", "nix", "cairo-sys-rs"]

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

[target.'cfg(target_os="windows")'.dependencies]
Expand Down
168 changes: 162 additions & 6 deletions druid-shell/src/platform/x11/application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@
use std::cell::RefCell;
use std::collections::HashMap;
use std::convert::TryInto;
use std::convert::{TryFrom, TryInto};
use std::os::unix::io::RawFd;
use std::rc::Rc;
use std::time::{Duration, Instant};

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

use super::clipboard::Clipboard;
use super::util;
use super::window::Window;

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

let (idle_read, idle_write) = nix::unistd::pipe2(nix::fcntl::OFlag::O_NONBLOCK)?;

let present_opcode = if std::env::var_os("DRUID_SHELL_DISABLE_X11_PRESENT").is_some() {
// Allow disabling Present with an environment variable.
None
Expand All @@ -110,7 +126,10 @@ impl Application {
screen_num: screen_num as i32,
window_id,
state,
idle_read,
idle_write,
present_opcode,
marker: std::marker::PhantomData,
})
}

Expand Down Expand Up @@ -350,23 +369,67 @@ impl Application {
Ok(false)
}

pub fn run(self, _handler: Option<Box<dyn AppHandler>>) {
fn run_inner(self) -> Result<(), Error> {
// Try to figure out the refresh rate of the current screen. We run the idle loop at that
// rate. The rate-limiting of the idle loop has two purposes:
// - When the present extension is disabled, we paint in the idle loop. By limiting the
// idle loop to the monitor's refresh rate, we aren't painting unnecessarily.
// - By running idle commands at a limited rate, we limit spurious wake-ups: if the X11
// connection is otherwise idle, we'll wake up at most once per frame, run *all* the
// pending idle commands, and then go back to sleep.
let refresh_rate = util::refresh_rate(self.connection(), self.window_id).unwrap_or(60.0);
let timeout = Duration::from_millis((1000.0 / refresh_rate) as u64);
let mut last_idle_time = Instant::now();
loop {
if let Ok(ev) = self.connection.wait_for_event() {
let next_idle_time = last_idle_time + timeout;
self.connection.flush()?;

// Before we poll on the connection's file descriptor, check whether there are any
// events ready. It could be that XCB has some events in its internal buffers because
// of something that happened during the idle loop.
let mut event = self.connection.poll_for_event()?;

if event.is_none() {
poll_with_timeout(&self.connection, self.idle_read, next_idle_time)
.context("Error while waiting for X11 connection")?;
}

while let Some(ev) = event {
match self.handle_event(&ev) {
Ok(quit) => {
if quit {
break;
return Ok(());
}
}
Err(e) => {
log::error!("Error handling event: {:#}", e);
}
}
event = self.connection.poll_for_event()?;
}

let now = Instant::now();
if now >= next_idle_time {
last_idle_time = now;
drain_idle_pipe(self.idle_read)?;

if let Ok(state) = self.state.try_borrow() {
for w in state.windows.values() {
w.run_idle();
}
} else {
log::error!("In idle loop, application state already borrowed");
}
}
}
}

pub fn run(self, _handler: Option<Box<dyn AppHandler>>) {
if let Err(e) = self.run_inner() {
log::error!("{}", e);
}
}

pub fn quit(&self) {
if let Ok(mut state) = self.state.try_borrow_mut() {
if !state.quitting {
Expand All @@ -380,7 +443,6 @@ impl Application {
for window in state.windows.values() {
window.destroy();
}
log_x11!(self.connection.flush());
}
}
} else {
Expand All @@ -390,7 +452,12 @@ impl Application {

fn finalize_quit(&self) {
log_x11!(self.connection.destroy_window(self.window_id));
log_x11!(self.connection.flush());
if let Err(e) = nix::unistd::close(self.idle_read) {
log::error!("Error closing idle_read: {}", e);
}
if let Err(e) = nix::unistd::close(self.idle_write) {
log::error!("Error closing idle_write: {}", e);
}
}

pub fn clipboard(&self) -> Clipboard {
Expand All @@ -404,4 +471,93 @@ impl Application {
log::warn!("Application::get_locale is currently unimplemented for X11 platforms. (defaulting to en-US)");
"en-US".into()
}

pub(crate) fn idle_pipe(&self) -> RawFd {
self.idle_write
}
}

/// Clears out our idle pipe; `idle_read` should be the reading end of a pipe that was opened with
/// O_NONBLOCK.
fn drain_idle_pipe(idle_read: RawFd) -> Result<(), Error> {
// Each write to the idle pipe adds one byte; it's unlikely that there will be much in it, but
// read it 16 bytes at a time just in case.
let mut read_buf = [0u8; 16];
loop {
match nix::unistd::read(idle_read, &mut read_buf[..]) {
Err(nix::Error::Sys(nix::errno::Errno::EINTR)) => {}
// According to write(2), this is the outcome of reading an empty, O_NONBLOCK
// pipe.
Err(nix::Error::Sys(nix::errno::Errno::EAGAIN)) => {
break;
}
Err(e) => {
return Err(e).context("Failed to read from idle pipe");
}
// According to write(2), this is the outcome of reading an O_NONBLOCK pipe
// when the other end has been closed. This shouldn't happen to us because we
// own both ends, but just in case.
Ok(0) => {
break;
}
Ok(_) => {}
}
}
Ok(())
}

/// Returns when there is an event ready to read from `conn`, or we got signalled by another thread
/// writing into our idle pipe and the `timeout` has passed.
// This was taken, with minor modifications, from the xclock_utc example in the x11rb crate.
// https://github.com/psychon/x11rb/blob/a6bd1453fd8e931394b9b1f2185fad48b7cca5fe/examples/xclock_utc.rs
fn poll_with_timeout(conn: &Rc<XCBConnection>, idle: RawFd, timeout: Instant) -> Result<(), Error> {
use nix::poll::{poll, PollFd, PollFlags};
use std::os::raw::c_int;
use std::os::unix::io::AsRawFd;

let fd = conn.as_raw_fd();
let mut both_poll_fds = [
PollFd::new(fd, PollFlags::POLLIN),
PollFd::new(idle, PollFlags::POLLIN),
];
let mut just_connection = [PollFd::new(fd, PollFlags::POLLIN)];
let mut poll_fds = &mut both_poll_fds[..];

// We start with no timeout in the poll call. If we get something from the idle handler, we'll
// start setting one.
let mut poll_timeout = -1;
loop {
fn readable(p: PollFd) -> bool {
p.revents()
.unwrap_or_else(PollFlags::empty)
.contains(PollFlags::POLLIN)
};

match poll(poll_fds, poll_timeout) {
Ok(_) => {
if readable(poll_fds[0]) {
// There is an X11 event ready to be handled.
break;
}
if poll_fds.len() == 1 || readable(poll_fds[1]) {
// Now that we got signalled, stop polling from the idle pipe and use a timeout
// instead.
poll_fds = &mut just_connection;

let now = Instant::now();
if now >= timeout {
break;
} else {
poll_timeout = c_int::try_from(timeout.duration_since(now).as_millis())
.unwrap_or(c_int::max_value())
}
}
}

Err(nix::Error::Sys(nix::errno::Errno::EINTR)) => {}
Err(e) => return Err(e.into()),
}
}

Ok(())
}
Loading

0 comments on commit 5afab1b

Please sign in to comment.