A common need when using x11 is to wait for an event but with a timeout. For example in sxcs I need to magnify the cursor position when it moves. But even when the cursor isn't moving I still need to redraw periodically so that the magnifier doesn't appear to be "hung".
In this case, what I need is something like XNextEventTimeout
which allows me
to set a timeout in case no events occur within that time-frame.
Unfortunately no such functionality exists, and some of the common ways people
achieve this goal aren't really correct.
Thankfully the relatively unknown X Synchronization Extension Protocol
provides alarm facility which can be used to simulate a timeout when using
XNextEvent
.
The goal of this article is to shine some spotlight so that more people are
aware of this functionality and to share some hiccups that I ran into when
trying to switch sxcs
to use XSyncAlarm so that you can avoid them.
But before showing the XSyncAlarm method, I'd like to briefly mentions some of the common methods and their downfalls.
This seems like a common approach a lot of people make; which is to set up a
signal (typically SIGALRM
) and then generate some XEvent inside the signal
handler.
I've seen at least 3 open-source projects which try this, and all 3 of them have
user reports on the application randomly freezing.
Signal handlers are special functions with very strict constrains. A signal handler cannot call any function which isn't async-signal-safe. And guess what, almost none of the Xlib functions are async-signal-safe.
So definitely don't call any Xlib functions (or any function which isn't explicitly declared as async-signal-safe) inside a sig-handler. Doing so is just asking for defect reports.
I haven't yet seen any real world project use this approach. But when searching online, a good amount of examples try using a separate thread to generate a dummy event. This method suffers from similar problem as the signal method. That is, XLib isn't thread safe, so you cannot just call XLib functions from another thread an expect it to work properly and consistently.
This seems to be the most common approach towards solving the problem.
First we grab the X server's fd using ConnectionNumber
and then we poll on
that fd.
Seems simple enough.
while (1) { /* main event loop */
struct pollfd pfd = {
.fd = ConnectionNumber(x11_display),
.events = POLLIN,
};
Bool pending = XPending(x11_display) > 0 || poll(&pfd, 1, TIMEOUT) > 0;
if (!pending) {
continue;
}
XNextEvent(...);
/* process the event */
}
One thing to note here is that we need to call XPending
before calling poll
.
This is because if there's events already in the queue, then poll would just go
to sleep.
One more thing to note is the if (!pending) continue
part, poll can wake up
for a number of reasons, one of them is interrupts.
Waking up due to interrupts may very well be desirable but if that's not the
case then you'll need to deal with this somehow.
In this case I'm just continuing the loop.
But you may want to readjust the timeout before polling again.
And while this method works fine in practice, it's actually subtly incorrect. As noted by u/skeeto
My concern is that it's relying on Xlib implementation details, and there's no documented guarantee that
XNextEvent
won't block on its internal socketread
because, say, it's trying to pre-read an extra event.
So while this seems to work, it's actually case of Hyrum's law.
While there still might be a couple cases where using this method might be necessary (more on this later), suffice to say, we should try to avoid depending on internal implementation details as they're the cause of many real-world bugs.
Conceptually this is as simple as "set an alarm right before calling
XNextEvent
which will act as the timeout."
Unfortunately, but perhaps not surprisingly since X is involved, you'll have to
go through a bunch of mundane rituals before you get to the point of being able
to set an alarm.
First we need to query for support and initialize the library.
int sync_event, sync_error, tmp;
if (!XSyncQueryExtension(x11.dpy, &sync_event, &sync_error))
fatal("XSync extension not available");
if (!XSyncInitialize(x11.dpy, &tmp, &tmp))
fatal("failed to initialize XSync extension");
We'll need sync_event
for later use.
The major/minor version returned by XSyncInitialize
is not that important for
me, so I've stuffed them into a tmp var.
For sxcs, the alarm will be critical for functioning, so I've decided to fatally error out in case any of those 2 calls fail. You may wish to deal with the error differently depending on your needs.
One interesting thing to note here is that almost all XSync functions seems to
become no-op if XSync support isn't available.
So you probably won't need to manually track weather XSync was enabled or not
and guard any XSync calls under something like if (xsync_available) { ... }
.
Other thing to note here is calling XSyncInitialize
is very important,
as the library documentation states:
The only XSync function that may be called before this function is XSyncQueryExtension. If a client violates this rule, the effects of all XSync calls that it makes are undefined.
In order to set an alarm we'll need a counter to set the alarm against.
SERVERTIME
counter, which counts milliseconds from some arbitrary starting
point, is probably what you want .
It's also the only counter guaranteed to exist by the XSync extension protocol.
int ncounter;
XSyncSystemCounter *counters;
XSyncCounter servertime = None;
if ((counters = XSyncListSystemCounters(x11.dpy, &ncounter)) != NULL) {
for (int i = 0; i < ncounter; i++) {
if (strcmp(counters[i].name, "SERVERTIME") == 0) {
servertime = counter[i].counter;
break;
}
}
XSyncFreeSystemCounterList(counters);
}
if (servertime == None)
fatal("SERVERTIME counter not found");
We'll have to loop over all the available system counters and test their .name
against "SERVERTIME"
to grab the right one.
There's also an "IDLETIME" counter which seems interesting and might be useful for certain use-cases. But I wasn't able to find any documentation of what exactly it counts.
After all that ritual, we're finally at a point where we can set an alarm.
XSyncAlarmAttributes attr;
unsigned long flags = 0;
First we declare a XSyncAlarmAttributes
, which we'll need for filling in
information about the alarm.
The flag
variable is a bitmask which tells the function which values we have
explicitly set.
attr.trigger.counter = servertime;
flags |= XSyncCACounter;
First we set the counter to SERVERTIME
and accordingly set the
XSyncCACounter
bit in flags
.
XSyncIntToValue(&attr.trigger.wait_value, 16);
flags |= XSyncCAValue;
attr.trigger.value_type = XSyncRelative;
flags |= XSyncCAValueType;
attr.trigger.test_type = XSyncPositiveComparison;
flags |= XSyncCATestType;
Since I want the timeout to be 16ms, I'm setting the wait_value
to 16.
The value_type
is set to XSyncRelative
since my wait_value
is relative to
the counter and not absolute.
XSyncPositiveComparison
should already be the default, so it's likely
redundant.
XSyncIntToValue(&attr.delta, 0);
flags |= XSyncCADelta;
For now let's set the delta to 0, this will deactivate the alarm after firing once. We'll come back to this in a bit.
And now with everything in place, we can create the alarm right before entering
the main event loop, and then reset the alarm after processing each event.
You could also move the call to XSyncChangeAlarm
right before calling
XNextEvent
.
XSyncAlarm alarm = XSyncCreateAlarm(x11.dpy, sync.flags, &sync.attr);
for (XEvent ev; !XNextEvent(x11.dpy, &ev); /* no-op */) {
switch (ev.type) {
case MotionNotify:
redraw();
break;
/* process other events... */
default:
if (ev.type == (sync_event + XSyncAlarmNotify)) {
/* got an alarm */
redraw();
}
break;
}
XSyncChangeAlarm(x11.dpy, alarm, flags, &attr);
}
And that's more or less it, now you've got yourself a nice way to poll for events with a timeout 🎉.
While the above method "works", it wasn't exactly correct for sxcs. The goal here was that we don't go more than 16ms without doing a redraw. So we don't want to set the alarm after processing each event, instead we want to set it only after doing a redraw.
In sxcs, we're redrawing only on MotionNotify
and XSyncAlarmNotify
, so we
can simply remove the XSyncChangeAlarm
and call it only if we get these two
events.
case MotionNotify:
redraw();
XSyncChangeAlarm(...);
break;
This takes care of MotionNotify
, however in case of an alarm, instead of
calling XSyncChangeAlarm
manually we can just set a delta instead.
When a delta is set to non-zero value, instead of deactivating the alarm after
firing, it will set another alarm according to the delta.
So instead of setting the delta to 0
above in attr.delta
we can just set it
to 16
and that'll automatically take care of the XSyncAlarmNotify
case for
us.
Unfortunately there are a couple cases where using XSyncAlarm won't work.
For example if you're trying to immediately deal with interrupts then XSyncAlarm isn't very useful since XNextEvent will just go back to blocking.
Another case when XSyncAlarm isn't useful is when you want to poll other things (e.g an inotify fd) as well.
For these cases, you'll unfortunately need to go back to using the poll method even though it's not entirely correct.
There are a couple other nuances which I've omitted because they're not related to XSyncAlarm specifically, but are a general problem when dealing with time.
Things such as missed deadlines, timer resolution or simply setting the timeout at the wrong time can end up giving unwanted results. There are ways to deal with these of course, such as:
nanosleep
).I won't go into details on these as they are out of scope of this article; and for sxcs I didn't think slightly missing a deadline would be too noticeable so I've decided not to complicate things and simply not dealing with these issues.
And that's more or less it. Hopefully this article introduced you to a x11 feature which might be useful to you in the future (maybe even for purposes other than setting timeouts).
Due to the code-size and inefficiency, I've decided to keep using the poll
method in sxcs
despite the XSyncAlarm method being more "correct" in theory.
Tags: [ xorg ]