Event Loops
cl-wl does nothing in the background. Events are read and handled only when you call one of the dispatch functions, in the thread that calls it.
The dispatch functions
| Function | Sends buffered requests | Waits | Reads | Runs handlers | Returns |
|---|---|---|---|---|---|
(dispatch display &key timeout) |
yes | until events arrive or timeout |
yes | yes | events delivered |
(dispatch-pending display) |
no | no | no | yes, for events already read | events delivered |
(roundtrip display) |
yes | until the compositor has handled every request so far | yes | yes | events delivered |
(read-events display &key timeout) |
yes | until events arrive or timeout |
yes | no | true if there may be events to deliver |
(flush display) |
yes | no | no | no | nothing |
timeout is in seconds and may be any non-negative real. Without a
timeout the function waits indefinitely, and a timeout of 0 does not
wait at all. A wait interrupted by a signal, as SBCL uses to stop threads
for garbage collection, resumes with the time that is left.
A simple loop
(loop until *quit*
do (wl:dispatch display))
This loop blocks until the compositor sends something. A game or any program that draws continuously either uses frame callbacks (see Recipes) or polls without waiting:
(loop until *quit*
do (wl:dispatch display :timeout 0) ; handle what has arrived, don't wait
(update-and-draw))
Waiting for an answer: roundtrip
roundtrip sends everything and blocks until the compositor has processed
it, delivering every event that arrives in the meantime. Use it during
setup, when you need the compositor's answer before going on. For
example, wl_seat's capabilities arrive right after binding:
(let ((seat (wl:bind display 'wl-seat :version 7))
(capabilities '()))
(wl:set-event-handlers seat
:capabilities (lambda (seat caps)
(declare (ignore seat))
(setf capabilities caps)))
(wl:roundtrip display)
capabilities) ; => (:POINTER :KEYBOARD)
Do not use roundtrip every frame. It costs a full trip to the
compositor and back.
wl_display.sync is the explicit form of the same idea. It creates a
wl_callback whose :done event arrives once everything before it has
been handled. :done ends the callback, so cl-wl destroys it after the
handler:
(let ((done nil))
(wl:set-event-handlers (wl-display-sync display)
:done (lambda (callback serial)
(declare (ignore callback serial))
(setf done t)))
(loop until done do (wl:dispatch display)))
Integrating with another event loop: display-fd
display-fd returns the connection's file descriptor. Wait on it with
poll, epoll or your toolkit's loop, then call dispatch with a
timeout of 0:
(wl:flush display) ; send requests before sleeping
;; … your loop waits until (wl:display-fd display) is readable …
(wl:dispatch display :timeout 0)
The fd belongs to the display. Do not close it or read from it.
If you wait on the fd yourself, flush before you sleep. Otherwise
requests may sit in libwayland's buffer while the compositor waits for
them. dispatch and read-events flush on their own.
An event thread: read-events and dispatch-pending
A program can have an event thread that waits for the compositor while
other threads, such as a render thread, use the same display. For that
program, dispatch splits in two:
read-eventswaits for events and reads them into libwayland's queue, without running handlers. It is the one function that may run while another thread uses the display.dispatch-pendingruns the handlers for the events that have been read.
Guard the display with a lock. Call read-events without holding the
lock, and call everything else, including dispatch-pending, every
request and flush, while holding it:
(asdf:load-system :cl-threads)
(defvar *lock* (cl-threads:make-lock "wayland display"))
(defun event-thread (display)
(loop until *quit*
do (when (wl:read-events display :timeout 0.1)
(cl-threads:with-lock (*lock*)
(wl:dispatch-pending display)
(wl:flush display)))))
;; Any other thread:
(cl-threads:with-lock (*lock*)
(wl-surface-commit surface)
(wl:flush display))
read-events returns true if there may be events to deliver. That is
either because it read some, or because some had already been read
before it was called; in the second case it returns at once without
waiting. It returns NIL when the timeout ran out.
display-error and flush may also be called while another thread waits
in read-events.
Errors in the loop
Every dispatch function, read-events and flush signal
compositor-error or connection-error once the connection has failed.
A failed connection stays failed, so catch the error outside the loop
rather than retrying:
(handler-case
(loop until *quit* do (wl:dispatch display))
(wl:compositor-error (e)
(format *error-output* "Protocol error: ~A~%" e))
(wl:connection-error (e)
(format *error-output* "Lost the compositor: ~A~%" e)))
A dispatch function also signals event-error when an event could not be
decoded. The event is dropped, but the connection is still fine, so you
may continue dispatching. See Errors.
Cost
Dispatching allocates nothing in steady state. That covers waiting for events, however briefly, queueing an event, calling its handler with up to ten arguments, and sending a request. The exceptions are values that must be new Lisp objects: strings, arrays, new proxies, and the first occurrence of each bitfield value. See Lifetimes and Threads.