6 Concepts
dxtr edited this page 2026-10-18 16:58:25 +02:00

Concepts

Two layers

cl-vk is two layers over the Vulkan C API.

The raw layer, cl-vk/ffi, is the C API transcribed: every command, struct, enum, constant and function pointer type of Vulkan 1.0 to 1.4 and of the included extensions, under the C names spelled the Lisp way. Commands take integers and pointers and return the VkResult as an integer. Nothing is checked beyond argument types and nothing is allocated or freed for you. See Raw FFI Layer.

The idiomatic layer, cl-vk, is what you use:

  • Objects for instances, physical devices, devices, queues, surfaces, swapchains, fences and binary semaphores. They remember what made them, refuse to be used after they are destroyed, and refuse to be destroyed while objects made from them are alive.
  • Lisp values: names are strings, versions are lists (1 3 0), enums are keywords (:fifo), flags are lists of keywords ((:graphics :compute)), extents are lists (800 600).
  • Conditions: a failing command signals a result-error that names the command and explains the VkResult. A request the system cannot meet, such as a missing extension, signals an unsupported-error before anything is created. See Errors.
  • Structs by field name: with-structs, set-struct and struct-ref fill and read any Vulkan struct, with sType set for you, strings and lists converted, and array counts filled in. See Structs.

The two layers mix. raw-handle returns the Vulkan handle of any cl-vk object, swapchain-images the raw handles of a swapchain's images, and instance-commands and device-commands return the command tables the raw commands are called through. Anything the idiomatic layer does not cover yet, such as command buffers, is called through the raw layer on cl-vk's objects. Recipes shows it.

Objects and what made them

instance ─┬─ physical-device ── device ─┬─ queue
          │                             ├─ fence
          │                             ├─ binary-semaphore
          │                             └─ swapchain ──┐
          └─ surface ──────────────────────────────────┘
Object Made by Destroyed by Made from
instance create-instance, with-instance destroy-instance
physical-device physical-devices (not destroyed) instance
device create-device, with-device destroy-device physical device
queue create-device (with its device) device
surface adopt-surface, create-headless-surface destroy-surface instance
swapchain create-swapchain, recreate-swapchain destroy-swapchain device and surface
fence create-fence destroy-fence device
binary-semaphore create-binary-semaphore destroy-binary-semaphore device

An object must be destroyed before the object it was made from. cl-vk tracks this: destroying an instance with a live device, or a device with a live fence, signals live-children-error naming them. See Lifetimes and Threads.

Command tables

Vulkan commands are reached through function pointers, at three levels:

  • Global commands need no instance: vkCreateInstance and the vkEnumerateInstance* commands.
  • Instance commands belong to an instance and its physical devices.
  • Device commands belong to a device, its queues and its command buffers. Their pointers come from vkGetDeviceProcAddr, so calls go straight to the driver, without the loader's dispatch.

A command table holds the pointers of one level. cl-vk loads them for you: each instance and device has its own table, and instance-commands and device-commands return them. A command that the instance or device does not provide, because the API version or extension was not enabled, is missing from the table, and calling it signals command-unavailable instead of crashing.

The global table, which cl-vk keeps from its first use, holds pointers into the Vulkan loader as this process mapped it. An image saved with save-lisp-and-die, as an exported game is, starts without it: cl-ffi loads the loader again when the image starts, cl-vk then forgets the table, and its next use loads it afresh. Vulkan objects do not outlive their process, so make instances and devices after the image starts.

Names

The raw layer keeps the C names in Lisp spelling:

C Raw layer (cl-vk/ffi) Idiomatic layer
vkCreateInstance vk-create-instance create-instance
VkApplicationInfo (:struct vk-application-info) vks:application-info
member pApplicationName slot :p-application-name field :application-name
VK_SUCCESS +vk-success+ (an integer) :success
VK_PRESENT_MODE_FIFO_KHR +vk-present-mode-fifo-khr+ :fifo
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT +vk-image-usage-color-attachment-bit+ :color-attachment

The idiomatic names of struct fields drop the p and pp of pointers.

Enum and flag keywords are the value's name without what every value of the type shares:

  • The type's prefix goes: VK_PRESENT_MODE_ of VkPresentModeKHR, VK_IMAGE_USAGE_ of VkImageUsageFlagBits, VK_PIPELINE_STAGE_2_ of VkPipelineStageFlagBits2.
  • _BIT goes: VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT is :color-attachment.
  • The type's own vendor tag goes: VK_PRESENT_MODE_FIFO_KHR is :fifo. A tag that is not the type's own stays: VK_IMAGE_LAYOUT_PRESENT_SRC_KHR of the core type VkImageLayout is :present-src-khr.
  • A name that would start with a digit keeps the last word of the prefix: VK_IMAGE_VIEW_TYPE_2D is :type-2d, VK_SAMPLE_COUNT_4_BIT is :count-4.
  • VkResult values share only VK_: :success, :suboptimal-khr, :error-device-lost.

When unsure, give any keyword: a wrong one is a compile-time error that lists the valid ones.

Versions

A Vulkan version is a list (MAJOR MINOR) or (MAJOR MINOR PATCH), the type vk:version. create-instance takes :api-version '(1 4), and instance-version, instance-api-version and physical-device-api-version return lists such as (1 4 354). The raw layer's packed integers are made and taken apart with vk-ffi:vk-make-api-version and vk-ffi:vk-api-version-major and its siblings.

Handles

A Vulkan handle is a foreign pointer when it is dispatchable (VkInstance, VkPhysicalDevice, VkDevice, VkQueue, VkCommandBuffer) and a 64-bit integer otherwise (VkFence, VkImage, VkSwapchainKHR and the rest). raw-handle returns the handle of a cl-vk object, and returns a raw handle unchanged, so a struct field that holds a handle takes either: :surface surface and :image image both work.

Thread rules and allocation in brief

  • Instances and devices may be used from several threads; creating and destroying their children happens on one thread at a time.
  • A queue and a swapchain belong to one thread at a time, in practice the render thread.
  • The per-frame functions (acquire-next-image, present, wait-for-fence, reset-fence, check, set-struct, and every raw command) allocate nothing.

The details are in Lifetimes and Threads.