7 Devices and Frames
dxtr edited this page 2026-10-18 15:59:12 +02:00

Devices and Frames

Opening a device

(gfx:with-device (device)                 ; offscreen: render to textures, compute
  ...)

(gfx:with-device (device :present-to window-system :validation t)
  ...)                                    ; can present to that window system's windows

with-device opens a device with open-device and closes it with close-device however its body is left. Without the macro:

(let ((device (gfx:open-device :present-to window-system)))
  (unwind-protect (run device)
    (gfx:close-device device)))

open-device options

Key Default Meaning
:backend :vulkan A backend keyword. The backend's system must be loaded, as cl-gfx/vulkan for :vulkan.
:present-to nil What the device's surfaces will present to, such as a cl-windowing window system. Leave it out for a device that only renders offscreen. A device opened without it cannot make surfaces.
:validation nil Check every use of the graphics API through the Vulkan validation layer. take-validation-messages returns what it found. See Errors and Validation.
:validation-checks () Extra checks: :synchronization (barriers between commands) and :best-practices (valid but slow or fragile uses). Giving any turns :validation on.
:frames-in-flight 2 How many frames may be recorded before the GPU finishes the oldest, from 1 to 8.
:application-name "" Passed to the driver.

open-device signals no-backend-error if the backend asked for, by default :vulkan, is not loaded; and unsupported-error if no GPU fits.

Which GPU

The Vulkan backend picks a GPU that supports Vulkan 1.4, has a queue for both graphics and compute, and, with :present-to, can present. Among those it prefers a discrete GPU, then an integrated one, then a virtual one, then a software renderer.

Every Vulkan 1.4 device has the features cl-gfx enables on all devices, so none of them is checked one by one: dynamic rendering, synchronization2, demoting fragments to helper invocations (which a shader's discard does), zero-initialized workgroup memory (which compute shaders' shared variables start from), writing storage buffers and textures from fragment shaders, cube map arrays, shading each sample, gathering with offsets, and anisotropic filtering.

(gfx:device-name device)               ; => "llvmpipe (LLVM 21.1.8, 256 bits)"
(gfx:device-backend device)            ; => :VULKAN
(gfx:device-frames-in-flight device)   ; => 2

Device features

Some GPUs lack some features, which Vulkan 1.4 leaves optional. device-features lists those the device has:

(gfx:device-features device)
;; => (:GEOMETRY-SHADERS :TESSELLATION-SHADERS :FLOAT64 :MULTISAMPLE-STORAGE-TEXTURES
;;     :VERTEX-STORES :ROUNDING-MODE-RTE-FLOAT32 :ROUNDING-MODE-RTE-FLOAT64
;;     :SIGNED-ZERO-INF-NAN-PRESERVE-FLOAT32 :SIGNED-ZERO-INF-NAN-PRESERVE-FLOAT64)
Feature Needed for
:geometry-shaders :geometry in create-render-pipeline
:tessellation-shaders :tessellation-control and :tessellation-evaluation
:float64 doubles in shaders, and :float64… vertex attributes
:multisample-storage-textures multisampled textures with :storage usage
:vertex-stores writing storage buffers and textures from the stages before the fragment shader
:rounding-mode-rte-float32, :rounding-mode-rte-float64 a shader whose floats, or doubles, round to the nearest, halves to even: SPIR-V's RoundingModeRTE
:signed-zero-inf-nan-preserve-float32, :signed-zero-inf-nan-preserve-float64 a shader that keeps the signed zeros, infinities and NaNs of floats, or doubles, which a GPU may otherwise assume there are none of: SPIR-V's SignedZeroInfNanPreserve

Every device has the rest: :cube-array textures, samplers with a :max-anisotropy, fragment shaders that run once per sample, and writing storage buffers and textures from fragment shaders need no feature.

Asking for something that needs a feature the device lacks signals unsupported-error, whose message names the feature. Check first when you have a fallback:

(if (member :geometry-shaders (gfx:device-features device))
    (make-pipeline-with-geometry-shader device)
    (make-plain-pipeline device))

Closing

close-device waits for the GPU to finish, destroys every object made from the device that is still alive, and closes it. It signals an error while a frame is being recorded. A closed device, and every object made from it, signals destroyed-error when used.

Frames

Everything the GPU does in a frame is recorded between begin-frame and end-frame, or inside with-frame:

(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))))
        ...))))
  • begin-frame waits if frames-in-flight frames are still being drawn, that is, for the frame that last used this slot. It then releases what was destroyed while that slot was in flight and returns the frame.
  • end-frame submits what the frame recorded and presents the surface textures it acquired. It signals an error if a pass is still open.
  • with-frame ends the frame however its body is left. After an error and a restart, what was recorded is submitted, and the next frame can begin.
  • Only one frame is recorded at a time. begin-frame during a frame signals an error.

frame-number counts the device's frames from 0, as begin-frame numbers them. frame-slot is the frame's slot among the frames in flight, from 0 below device-frames-in-flight. Use it to pick a per-frame region of an :upload buffer:

(gfx:write-buffer uniforms data :offset (* 256 (gfx:frame-slot frame)))

Frame objects are reused, one per slot of the frames in flight. Don't keep a frame after it ends.

A render loop

(win:with-window-system (window-system)
  (win:with-window (window window-system :title "Game" :width 1280 :height 720)
    (gfx:with-device (device :present-to window-system)
      (let ((surface (gfx:create-surface device window))
            (event (win:make-event))
            (running t))
        ;; ... create pipelines, buffers, textures ...
        (loop while running
              do (win:process-events window-system)
                 (loop while (win:poll-event window-system event)
                       when (eq (win:event-type event) :close)
                         do (setf running nil))
                 (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))))
                         ...)))))))))

win:make-event makes one event object, which win:poll-event fills, so the loop allocates nothing. acquire-surface-texture returns nil until the window has its first size and while it is minimised, so the frame records nothing and still ends cleanly. With the default :fifo present mode, the loop runs at the display's refresh rate.

Thread rules

A device and every object made from it belong to the thread that opened it. Make every cl-gfx call on that thread.