4 Threads
dxtr edited this page 2026-10-14 04:26:09 +02:00

Threads

The rules

  • process-events runs 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-events runs on another. That covers creating and destroying windows, setting titles, the size functions, adopt-window-size, poll-event and 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-events with a short timeout, 0.05 seconds here, so it notices within that time when it should stop. Held keys still repeat on time: process-events wakes up for them.
  • The render thread calls nothing on the window system directly. cl-gfx's surface calls window-pixel-size, window-pending-pixel-size and adopt-window-size from 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-window destroys the window. Destroy what draws into a window before the window.
  • When something must be redrawn because of an event, such as a :resize in 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 with copy-event: the struct is reused.

A real render loop also handles its errors, and tells the event thread to stop when it fails.