Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement timers for the X11 platform #1096

Merged
merged 9 commits into from
Jul 21, 2020
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ You can find its changes [documented below](#060---2020-06-01).
- X11: Support idle callbacks. ([#1072] by [@jneem])
- GTK: Don't interrupt `KeyEvent.repeat` when releasing another key. ([#1081] by [@raphlinus])
- X11: Set some more common window properties. ([#1097] by [@psychon])
- X11: Support timers. ([#1096] by [@psychon])

### Visual

Expand Down Expand Up @@ -361,6 +362,7 @@ Last release without a changelog :(
[#1062]: https://github.com/linebender/druid/pull/1062
[#1072]: https://github.com/linebender/druid/pull/1072
[#1081]: https://github.com/linebender/druid/pull/1081
[#1096]: https://github.com/linebender/druid/pull/1096
[#1097]: https://github.com/linebender/druid/pull/1097

[Unreleased]: https://github.com/linebender/druid/compare/v0.6.0...master
Expand Down
68 changes: 58 additions & 10 deletions druid-shell/src/platform/x11/application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,18 @@ impl Application {
let timeout = Duration::from_millis((1000.0 / refresh_rate) as u64);
let mut last_idle_time = Instant::now();
loop {
// Figure out when the next wakeup needs to happen
let next_timeout = if let Ok(state) = self.state.try_borrow() {
state.windows
.values()
.filter_map(|w| w.next_timeout())
.min()
} else {
log::error!("Getting next timeout, application state already borrowed");
None
};
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
Expand All @@ -390,7 +401,7 @@ impl Application {
let mut event = self.connection.poll_for_event()?;

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

Expand All @@ -409,6 +420,17 @@ impl Application {
}

let now = Instant::now();
if let Some(timeout) = next_timeout {
if timeout <= now {
if let Ok(state) = self.state.try_borrow() {
for w in state.windows.values() {
w.run_timers(now);
}
} else {
log::error!("In timer loop, application state already borrowed");
}
}
}
if now >= next_idle_time {
last_idle_time = now;
drain_idle_pipe(self.idle_read)?;
Expand Down Expand Up @@ -510,11 +532,13 @@ fn drain_idle_pipe(idle_read: RawFd) -> Result<(), Error> {
/// 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> {
fn poll_with_timeout(conn: &Rc<XCBConnection>, idle: RawFd, timer_timeout: Option<Instant>, idle_timeout: Instant) -> Result<(), Error> {
use nix::poll::{poll, PollFd, PollFlags};
use std::os::raw::c_int;
use std::os::unix::io::AsRawFd;

let mut now = Instant::now();
let earliest_timeout = idle_timeout.min(timer_timeout.unwrap_or(idle_timeout));
let fd = conn.as_raw_fd();
let mut both_poll_fds = [
PollFd::new(fd, PollFlags::POLLIN),
Expand All @@ -525,36 +549,60 @@ fn poll_with_timeout(conn: &Rc<XCBConnection>, idle: RawFd, timeout: Instant) ->

// 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;
let mut honor_idle_timeout = false;
loop {
fn readable(p: PollFd) -> bool {
p.revents()
.unwrap_or_else(PollFlags::empty)
.contains(PollFlags::POLLIN)
};

// Compute the deadline for when poll() has to wakeup
let deadline = if honor_idle_timeout {
Some(earliest_timeout)
} else {
timer_timeout
};
// ...and convert the deadline into an argument for poll()
let poll_timeout = if let Some(deadline) = deadline {
if deadline <= now {
break;
} else {
let millis = c_int::try_from(deadline.duration_since(now).as_millis())
.unwrap_or(c_int::max_value() - 1);
// The above .as_millis() rounds down. This means we would wake up before the
// deadline is reached. Add one to 'simulate' rounding up instead.
millis + 1
}
} else {
// No timeout
-1
};

match poll(poll_fds, poll_timeout) {
Ok(_) => {
if readable(poll_fds[0]) {
// There is an X11 event ready to be handled.
break;
}
now = Instant::now();
if timer_timeout.is_some() && now >= timer_timeout.unwrap() {
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 {
honor_idle_timeout = true;
if now >= idle_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(nix::Error::Sys(nix::errno::Errno::EINTR)) => {
now = Instant::now();
}
Err(e) => return Err(e.into()),
}
}
Expand Down
41 changes: 41 additions & 0 deletions druid-shell/src/platform/x11/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,17 @@

//! Miscellaneous utility functions for working with X11.

use std::cmp::Ordering;
use std::rc::Rc;
use std::time::Instant;

use anyhow::{anyhow, Error};
use x11rb::protocol::randr::{ConnectionExt, ModeFlag};
use x11rb::protocol::xproto::{Screen, Visualtype, Window};
use x11rb::xcb_ffi::XCBConnection;

use crate::window::TimerToken;

// See: https://github.com/rtbo/rust-xcb/blob/master/examples/randr_screen_modes.rs
pub fn refresh_rate(conn: &Rc<XCBConnection>, window_id: Window) -> Option<f64> {
let try_refresh_rate = || -> Result<f64, Error> {
Expand Down Expand Up @@ -87,3 +91,40 @@ macro_rules! log_x11 {
}
};
}

/// A timer is a deadline (`std::Time::Instant`) and a `TimerToken`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Timer {
deadline: Instant,
token: TimerToken,
}

impl Timer {
pub(crate) fn new(deadline: Instant) -> Self {
let token = TimerToken::next();
Self { deadline, token }
}

pub(crate) fn deadline(&self) -> Instant {
self.deadline
}

pub(crate) fn token(&self) -> TimerToken {
self.token
}
}

impl Ord for Timer {
/// Ordering is so that earliest deadline sorts first
// "Earliest deadline first" that a std::collections::BinaryHeap will have the earliest timer
// at its head, which is just what is needed for timer management.
fn cmp(&self, other: &Self) -> Ordering {
self.deadline.cmp(&other.deadline).reverse()
}
}

impl PartialOrd for Timer {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
42 changes: 35 additions & 7 deletions druid-shell/src/platform/x11/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@
use std::any::Any;
use std::cell::RefCell;
use std::convert::{TryFrom, TryInto};
use std::collections::BinaryHeap;
use std::os::unix::io::RawFd;
use std::rc::{Rc, Weak};
use std::sync::{Arc, Mutex};
use std::time::Instant;

use anyhow::{anyhow, Context, Error};
use cairo::{XCBConnection as CairoXCBConnection, XCBDrawable, XCBSurface, XCBVisualType};
Expand Down Expand Up @@ -47,7 +49,7 @@ use crate::window::{IdleToken, Text, TimerToken, WinHandler};
use super::application::Application;
use super::keycodes;
use super::menu::Menu;
use super::util;
use super::util::{self, Timer};

/// A version of XCB's `xcb_visualtype_t` struct. This was copied from the [example] in x11rb; it
/// is used to interoperate with cairo.
Expand Down Expand Up @@ -296,6 +298,7 @@ impl WindowBuilder {
cairo_surface,
atoms,
state,
timer_queue: Mutex::new(BinaryHeap::new()),
idle_queue: Arc::new(Mutex::new(Vec::new())),
idle_pipe: self.app.idle_pipe(),
present_data: RefCell::new(present_data),
Expand Down Expand Up @@ -350,6 +353,8 @@ pub(crate) struct Window {
cairo_surface: RefCell<XCBSurface>,
atoms: WindowAtoms,
state: RefCell<WindowState>,
/// Timers, sorted by "earliest deadline first"
timer_queue: Mutex<BinaryHeap<Timer>>,
idle_queue: Arc<Mutex<Vec<IdleKind>>>,
// Writing to this wakes up the event loop, so that it can run idle handlers.
idle_pipe: RawFd,
Expand Down Expand Up @@ -1004,6 +1009,27 @@ impl Window {
}
}
}

pub(crate) fn next_timeout(&self) -> Option<Instant> {
if let Some(timer) = self.timer_queue.lock().unwrap().peek() {
Some(timer.deadline())
} else {
None
}
}

pub(crate) fn run_timers(&self, now: Instant) {
while let Some(deadline) = self.next_timeout() {
if deadline > now {
break;
}
// Remove the timer and get the token
let token = self.timer_queue.lock().unwrap().pop().unwrap().token();
if let Ok(mut handler_borrow) = self.handler.try_borrow_mut() {
handler_borrow.timer(token);
}
}
}
}

impl Buffers {
Expand Down Expand Up @@ -1339,12 +1365,14 @@ impl WindowHandle {
Text::new()
}

pub fn request_timer(&self, _deadline: std::time::Instant) -> TimerToken {
// TODO(x11/timers): implement WindowHandle::request_timer
// This one might be tricky, since there's not really any timers to hook into in X11.
// Might have to code up our own Timer struct, running in its own thread?
log::warn!("WindowHandle::resizeable is currently unimplemented for X11 platforms.");
TimerToken::INVALID
pub fn request_timer(&self, deadline: Instant) -> TimerToken {
if let Some(w) = self.window.upgrade() {
let timer = Timer::new(deadline);
w.timer_queue.lock().unwrap().push(timer);
timer.token()
} else {
TimerToken::INVALID
}
}

pub fn set_cursor(&mut self, _cursor: &Cursor) {
Expand Down