Structs
Nearly every Vulkan command takes a struct, and filling structs is most
of the code of calling Vulkan. cl-vk fills them by field name, from Lisp
values, with sType set, strings and lists copied to foreign memory, and
array counts filled in:
(vk:with-structs ((app vks:application-info
:application-name "example"
:api-version vk-ffi:+vk-api-version-1-4+)
(info vks:instance-create-info
:application-info app
:enabled-extension-names '("VK_KHR_surface")))
(list (vk:struct-ref info vks:instance-create-info :enabled-extension-count)
(vk:struct-ref app vks:application-info :application-name)))
;; => (1 "example")
Every struct of the raw layer can be filled this way, those of extensions and video included.
Names
Structs are named by the symbols of the package cl-vk/structs: the
C name without Vk, in Lisp spelling. VkInstanceCreateInfo is
vks:instance-create-info, VkPhysicalDeviceVulkan13Features is
vks:physical-device-vulkan13-features, VkSwapchainCreateInfoKHR is
vks:swapchain-create-info-khr. Your editor completes them.
Enums are named the same way, each the type of its keywords:
(typep :fifo 'vks:present-mode-khr) is true, and cl-vk's functions
declare their enum arguments with these types. A name that would be a
symbol of Common Lisp keeps its vk-, so the type of VkFormat is
vks:vk-format. A name that is neither a struct nor an enum does not
even read: the package exports only those.
Fields are keywords named after the C members, without the p and
pp of pointers: pApplicationName is :application-name,
ppEnabledExtensionNames is :enabled-extension-names. The pNext
member is :next. The sType member is not a field: cl-vk sets it.
What each field takes
| Field | Value |
|---|---|
| integer, float | the number, of the field's C type |
VkBool32 |
a generalized boolean: nil is false, anything else true |
| enum | a keyword (:b8g8r8a8-srgb, :fifo) or an integer |
| flags | a keyword, a list of keywords ('(:color-attachment :transfer-dst)) or an integer |
| handle | a cl-vk object, a raw handle, or nil for none |
| string | a string, a foreign pointer, or nil for null |
fixed-size string (char[N]) |
a string shorter than N bytes |
| fixed-size array | a list or vector of at most N elements |
| array with a count field | a list or vector; the count field is set from its length |
| struct held by value | a field list, (:width 800 :height 600), or a struct to copy |
| pointer to a struct | a struct bound earlier, or nil |
| array of structs | a struct array bound earlier, or a list of structs bound earlier |
:next |
a struct, a list of structs to chain in order, or nil |
| any other pointer | a foreign pointer, or nil |
| C bit-field | an integer that fits its width |
Enum and flag keywords drop the type's prefix, _BIT and the type's own
vendor tag: VK_PRESENT_MODE_FIFO_KHR is :fifo,
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT is :color-attachment. A vendor tag
that is not the type's own stays: VK_IMAGE_LAYOUT_PRESENT_SRC_KHR is
:present-src-khr, because VkImageLayout is a core type. A name that
would start with a digit keeps the last word of the prefix:
VK_IMAGE_VIEW_TYPE_2D is :type-2d. The full rules are in
Concepts.
Fields not given are zero, which in Vulkan usually means "none" or the default.
with-structs
(with-structs (binding ...) body ...)
binds each variable to a struct in foreign memory for the extent of the
body: zeroed, with its sType set, and with the sType of every struct
embedded in it set too (such as VkComputePipelineCreateInfo's stage).
Bindings are made in order, so a later one may refer to an earlier one. A
binding is one of:
(variable name field value ...) ; one struct
(variable name (field value ...) ...) ; an array, one struct per field list
(variable (name count)) ; an array of COUNT zeroed structs
Strings and lists given as values are copied to foreign memory that also lasts for the extent of the body. Nothing outlives the body: pass the structs to Vulkan within it.
Pointers to structs and chains
A field that points to a struct takes a variable bound earlier:
(vk:with-structs ((app vks:application-info :application-name "game")
(info vks:instance-create-info :application-info app))
...)
:next takes a struct or a list of structs, which are chained in order
through their own :next:
(vk:with-structs ((v13 vks:physical-device-vulkan13-features :dynamic-rendering t)
(v12 vks:physical-device-vulkan12-features :timeline-semaphore t)
(features vks:physical-device-features2 :next (list v13 v12)))
...) ; features -> v13 -> v12
Structs held by value
A struct member held by value takes a field list, written literally in the form:
(vk:with-structs ((info vks:rendering-info
:render-area (:offset (:x 0 :y 0) :extent (:width 800 :height 600))
:layer-count 1))
...)
or a struct bound earlier, which is copied in:
(vk:with-structs ((extent vks:extent2d :width 800 :height 600)
(info vks:swapchain-create-info-khr :image-extent extent))
...)
Arrays
A list or vector fills an array field and sets its count:
(vk:with-structs ((info vks:submit-info
:wait-semaphores (list semaphore-a semaphore-b)
:wait-dst-stage-mask '((:color-attachment-output) (:transfer))
:command-buffers (list command-buffer)))
...) ; wait-semaphore-count = 2, command-buffer-count = 1
Arrays that share a count, such as :wait-semaphores and
:wait-dst-stage-mask of vks:submit-info, must have the same length;
otherwise a struct-value-error says so:
1 is not a valid value for the field :WAIT-DST-STAGE-MASK of VKS:SUBMIT-INFO:
expected 2 elements, like the other arrays counted by :WAIT-SEMAPHORE-COUNT.
An array of structs is a binding with several field lists. Given to an array field, it sets the count:
(vk:with-structs ((attachments vks:rendering-attachment-info
(:image-view view-a :image-layout :color-attachment-optimal
:load-op :clear :store-op :store)
(:image-view view-b :image-layout :color-attachment-optimal
:load-op :load :store-op :store))
(info vks:rendering-info :color-attachments attachments :layer-count 1))
...) ; color-attachment-count = 2
When the number of elements is known only at run time, bind
(variable (name count)) and fill each element with set-struct and
struct-aref:
(vk:with-structs ((viewports (vks:viewport (length sizes)))
(state vks:pipeline-viewport-state-create-info :viewports viewports))
(loop for (width height) in sizes
for index from 0
do (vk:set-struct (vk:struct-aref viewports vks:viewport index) vks:viewport
:width (float width) :height (float height) :max-depth 1.0))
...)
A list of separately bound structs works too: they are copied into a new array.
An array may also be given as a foreign pointer, with its count given explicitly in the count field.
Unions
A union takes exactly one field:
(vk:with-structs ((color vks:clear-color-value :float32 '(0.1 0.2 0.3 1.0)))
...)
struct-ref
(struct-ref pointer name field)
reads a field back as a Lisp value:
| Field | Returns |
|---|---|
VkBool32 |
t or nil |
| enum | a keyword, or the integer for a value newer than the bindings |
| flags | a list of keywords, in bit order, followed by one integer holding any bits newer than the bindings |
| string, fixed-size string | a string, or nil for a null pointer |
| struct held by value, fixed-size array | a pointer to it, inside the struct |
any other pointer, :next included |
the foreign pointer |
| anything else | the number or handle |
(vk:with-structs ((range vks:image-subresource-range
:aspect-mask '(:color :depth) :level-count 1 :layer-count 1))
(vk:struct-ref range vks:image-subresource-range :aspect-mask))
;; => (:COLOR :DEPTH)
A struct held by value reads as a pointer, so nested reads chain:
(vk:struct-ref (vk:struct-ref info vks:swapchain-create-info-khr :image-extent)
vks:extent2d :width)
(setf struct-ref) writes one field, as set-struct does.
Structs reused every frame
with-structs copies strings and lists to foreign memory, which
allocates. For structs filled every frame, allocate them once and fill
them with set-struct, which allocates nothing:
(defvar *submit* (vk:allocate-struct 'vks:submit-info2))
(defvar *wait* (vk:allocate-struct 'vks:semaphore-submit-info))
(defvar *buffer* (vk:allocate-struct 'vks:command-buffer-submit-info))
(defun fill-submit (semaphore command-buffer)
(vk:set-struct *wait* vks:semaphore-submit-info
:semaphore semaphore :stage-mask :color-attachment-output)
(vk:set-struct *buffer* vks:command-buffer-submit-info :command-buffer command-buffer)
(vk:set-struct *submit* vks:submit-info2
:wait-semaphore-info-count 1 :wait-semaphore-infos *wait*
:command-buffer-info-count 1 :command-buffer-infos *buffer*))
;; and when done:
(mapc #'vk:free-struct (list *submit* *wait* *buffer*))
allocate-struct name &key (count 1)returns foreign memory forcountstructs, zeroed and with theirsTypeset. It lasts untilfree-struct.set-struct pointer name field value ...stores the fields given and leaves the others as they are. Numbers, booleans, handles, keywords and pointers cost nothing. Strings and arrays must be foreign pointers, with their counts given; a list signals astruct-value-errorthat says to usewith-structs.- A field that points to untyped memory, such as the
:dataofvks:specialization-info, takes a foreign pointer and its size inwith-structstoo, since a list says nothing of its layout. struct-aref pointer name indexreturns a pointer to elementindexof an array of structs.
In the example above, filling the submit info a thousand times conses zero bytes.
Mistakes are caught early
The macros know the struct and its fields when they expand, so a wrong
field name, or a wrong constant keyword, is a vk-error at compile time
that lists what is valid:
VKS:SAMPLER-CREATE-INFO has no field :MAGFILTER. Its fields are :NEXT, :FLAGS,
:MAG-FILTER, :MIN-FILTER, :MIPMAP-MODE, ...
:BILINEAR is not a valid value for the field :MAG-FILTER of VKS:SAMPLER-CREATE-INFO:
expected one of :NEAREST, :LINEAR, :CUBIC-EXT, :CUBIC-IMG.
A field given twice is an error too. A value known only at run time that
is of the wrong kind signals a struct-value-error, which names the
struct, the field, the value and what the field takes:
"linear" is not a valid value for the field :MAG-FILTER of VKS:SAMPLER-CREATE-INFO:
expected a keyword or an integer.
Constant keywords are converted to integers when the macro expands, and cost nothing at run time.
Thread rules
The structs with-structs binds are local to the thread running its
body. Memory from allocate-struct may be passed between threads, but
not written by two at once.
Under the hood
What cl-vk knows about each struct is generated from the registry into
registry/structs.lisp: each field's keyword, the raw slot that holds
it, its kind of value, and which field counts an array. struct-ref and
the others read it when they expand. The raw cl-ffi structs are those of
cl-vk/ffi, such as (:struct vk-ffi:vk-instance-create-info); see Raw FFI
Layer.