Lifetimes and Threads
What outlives what
Vulkan objects are made from other objects, and must be destroyed before them:
| Object | Destroy before | Destroyed by |
|---|---|---|
instance |
destroy-instance, or leaving with-instance |
|
physical-device |
(never destroyed; valid while its instance is) | |
surface |
its instance | destroy-surface |
device |
its instance | destroy-device, or leaving with-device |
queue |
(goes with its device) | |
swapchain |
its device and its surface | destroy-swapchain |
fence, binary-semaphore |
their device | destroy-fence, destroy-binary-semaphore |
cl-vk enforces this order. Each object remembers the live objects made
from it, and destroying it while any is alive signals
live-children-error, which names them, and destroys nothing. Once an
object is destroyed, using it, or destroying it again, signals
destroyed-object-error; its readers still work, so it can be printed
and inspected.
with-instance and with-device destroy their object when their body
is left, normally or by a non-local exit, unless it was destroyed
already. with-device waits for the device's work to finish first.
Swapchains, surfaces, fences and semaphores have no with- macros: they
usually live across frames, so their lifetimes are rarely a dynamic
extent.
A typical order of tear-down:
(vk:device-wait-idle device) ; no work in flight
;; ... destroy what you made through the raw layer: views, pools, pipelines ...
(mapc #'vk:destroy-binary-semaphore semaphores)
(vk:destroy-fence fence)
(vk:destroy-swapchain swapchain)
(vk:destroy-device device)
(vk:destroy-surface surface)
(vk:destroy-instance instance)
Objects made through the raw layer
Objects you create with raw commands, such as command pools, image views and pipelines, are not tracked. Destroy them yourself, before the device or instance they were made from; Vulkan does not check, and destroying a device with live children is undefined behaviour. The validation layer reports it.
The GPU still using an object
cl-vk tracks what objects exist, not what the GPU is doing. Destroying a
fence, semaphore, swapchain or device while submitted work still uses it
is an error Vulkan does not catch. Wait first: wait-for-fence for the
work that uses it, or device-wait-idle.
Surfaces from other libraries
adopt-surface takes ownership of a VkSurfaceKHR created elsewhere:
cl-vk destroys it in destroy-surface. Destroy the surface before the
window it was made for.
Memory of the idiomatic layer
The objects are Lisp structs. Their Vulkan resources are released by the
destroy- functions only, never by the garbage collector: dropping the
last reference to an undestroyed device leaks it.
with-structs memory lasts for the extent of its body. allocate-struct
memory lasts until free-struct.
Threads
Vulkan's own rules decide what may be shared: some objects are externally synchronized, which means the program must not use them from two threads at once. cl-vk follows those rules and states them in each object's documentation:
| Object | May be shared between threads | Must be one thread at a time |
|---|---|---|
instance |
using it, from several threads at once | creating and destroying the objects made from it; destroying it while another thread uses it is an error |
physical-device |
everything (immutable) | |
device |
using it, from several threads at once | creating and destroying the objects made from it; destroying it while another thread uses it is an error |
queue |
different queues on different threads | submitting to and presenting on one queue |
surface |
querying it | creating a swapchain for it, destroying it |
swapchain |
acquiring, presenting, destroying: in practice the render thread owns it | |
fence |
waiting for it, from several threads at once | resetting and destroying it |
binary-semaphore |
destroying it | |
debug-message, queue-family, surface-capabilities |
everything (immutable) | |
| command tables | everything (immutable once loaded) | |
with-structs structs |
local to the thread running the body | |
allocate-struct memory |
passing it between threads | writing it |
take-debug-messages may be called from any thread: the debug messenger
queues messages under a lock, from whatever thread the driver calls it
on.
The debug messenger is a foreign callback. It only queues the message and returns; it never signals or unwinds through the driver's frames.
Allocation
Code that runs every frame should not allocate, so that the garbage collector does not pause the render thread. These allocate nothing:
acquire-next-image,presentwait-for-fence,reset-fence,fence-signaled-pcheckset-struct,struct-refof numbers, keywords and pointers,struct-aref- every raw command of
cl-vk/ffi, called with stack or preallocated memory
with-structs allocates foreign memory for strings and lists given as
values, and struct-ref of a string or of flags makes a Lisp string or
list. For structs filled every frame, use allocate-struct once and
set-struct each frame; see Recipes.
Creating and destroying objects, physical-devices, the surface-
queries and take-debug-messages allocate. Call them at setup, on a
resize, or at most once a frame.