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-framewaits ifframes-in-flightframes 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-framesubmits what the frame recorded and presents the surface textures it acquired. It signals an error if a pass is still open.with-frameends 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-frameduring 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.