Table of Contents
Threads
The rules
process-eventsruns on one thread at a time. That thread is the event thread. It may be any thread, including the one that renders.- Everything else may be called from any thread, including while
process-eventsruns on another. That covers creating and destroying windows, setting titles, the size functions,adopt-window-size,poll-eventand creating Vulkan surfaces. - An event struct belongs to the thread that polls into it. Give each
thread that polls its own, made with
make-event.
A window system serialises access to the platform with a lock. While
process-events waits for the window system to send something, it does
not hold the lock. So a render thread can call adopt-window-size or
window-pending-pixel-size while the event thread waits, and neither
blocks the other for longer than a call takes.
On Windows
A Win32 window gets its messages only on the thread that created it. So
each window system runs a thread of its own, which creates the windows and
handles their messages, and create-window and destroy-window are handed
to it from whatever thread calls them. Its events wait for
process-events as on Wayland, so the rules above hold unchanged. While
the user drags a window's border, Windows runs a loop of its own in that
thread, and events keep coming. See Win32 Backend.
One thread
The simplest program handles events and renders on one thread, as in
Getting Started: each frame, it
calls process-events with a timeout of 0, polls the events, and draws.
The display paces the loop.
This is fine for many programs. Its weakness: while a frame takes long, nobody answers the window system, and input waits for the next frame.
An event thread and a render thread
A game engine usually separates them. The event thread waits in
process-events and reacts at once. The render thread draws at the
display's pace. This is what Pyssel does.
The example uses cl-threads for threads and a semaphore. Any threading library will do.
(asdf:load-system :cl-windowing/wayland)
(asdf:load-system :cl-gfx/windowing)
(asdf:load-system :cl-threads)
(defpackage #:example
(:use #:cl)
(:local-nicknames (#:win #:cl-windowing)
(#:gfx #:cl-gfx)
(#:th #:cl-threads)))
(in-package #:example)
(defun render (window-system window stop)
"On the render thread: draw WINDOW until STOP is signalled."
(gfx:with-device (device :present-to window-system)
(let ((surface (gfx:create-surface device window)))
(loop until (th:wait-on-semaphore stop :timeout 0)
do (gfx:with-frame (frame device)
(let ((texture (gfx:acquire-surface-texture surface frame)))
(when texture
(gfx:with-render-pass (pass frame :color ((texture :clear '(0 0 0 1))))
;; Draw here.
))))))))
(defun run ()
(win:with-window-system (window-system)
(win:with-window (window window-system :title "Threads" :width 800 :height 600)
(let* ((stop (th:make-semaphore))
(renderer (th:make-thread (lambda () (render window-system window stop))
:name "render"))
(event (win:make-event)))
;; This thread is the event thread.
(unwind-protect
(loop with closed = nil
until closed
do (win:process-events window-system :timeout 0.05)
(loop while (win:poll-event window-system event)
do (case (win:event-type event)
(:close (setf closed t))
(:key-press (when (eq (win:event-key event) :escape)
(setf closed t))))))
(th:signal-semaphore stop)
(th:join-thread renderer))))))
Points to note:
- The event thread waits in
process-eventswith a short timeout, 0.05 seconds here, so it notices within that time when it should stop. Held keys still repeat on time:process-eventswakes up for them. - The render thread calls nothing on the window system directly. cl-gfx's
surface calls
window-pixel-size,window-pending-pixel-sizeandadopt-window-sizefrom it, which the rules allow. Until the event thread has received the window's first size, the surface has nothing to draw into and the render thread draws nothing. - The window outlives the render thread: the event thread joins the render
thread before
with-windowdestroys the window. Destroy what draws into a window before the window. - When something must be redrawn because of an event, such as a
:resizein a program that draws on demand, the event thread wakes the render thread. - Game input usually goes from the event thread to the game through a
queue of your own. Copy what you need out of the event struct, including
(copy-seq (win:event-text event)), or the whole event into one of the queue's withcopy-event: the struct is reused.
A real render loop also handles its errors, and tells the event thread to stop when it fails.