9 Structs
dxtr edited this page 2026-10-18 17:10:33 +02:00

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 for count structs, zeroed and with their sType set. It lasts until free-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 a struct-value-error that says to use with-structs.
  • A field that points to untyped memory, such as the :data of vks:specialization-info, takes a foreign pointer and its size in with-structs too, since a list says nothing of its layout.
  • struct-aref pointer name index returns a pointer to element index of 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.