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-errorthat names the command and explains theVkResult. A request the system cannot meet, such as a missing extension, signals anunsupported-errorbefore anything is created. See Errors. - Structs by field name:
with-structs,set-structandstruct-reffill and read any Vulkan struct, withsTypeset 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:
vkCreateInstanceand thevkEnumerateInstance*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_ofVkPresentModeKHR,VK_IMAGE_USAGE_ofVkImageUsageFlagBits,VK_PIPELINE_STAGE_2_ofVkPipelineStageFlagBits2. _BITgoes:VK_IMAGE_USAGE_COLOR_ATTACHMENT_BITis:color-attachment.- The type's own vendor tag goes:
VK_PRESENT_MODE_FIFO_KHRis:fifo. A tag that is not the type's own stays:VK_IMAGE_LAYOUT_PRESENT_SRC_KHRof the core typeVkImageLayoutis:present-src-khr. - A name that would start with a digit keeps the last word of the prefix:
VK_IMAGE_VIEW_TYPE_2Dis:type-2d,VK_SAMPLE_COUNT_4_BITis:count-4. VkResultvalues share onlyVK_::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.