Table of Contents
Raw FFI Layer
cl-vk/ffi is the Vulkan C API transcribed into Lisp, generated from the
registry: every type, constant and command of Vulkan 1.0 to 1.4 and of
every extension, except those for platforms other than Wayland, X11
(xcb, Xlib, Xlib-XRandR) and Windows, and provisional ones. That is
about 700 commands and 1,500 structs and unions.
It does no error checking beyond argument types and no memory management. Use it for what the idiomatic layer does not cover yet, usually on cl-vk's objects; Recipes shows how the two mix.
(asdf:load-system :cl-vk/ffi) ; or :cl-vk, which loads it
(defpackage #:example
(:use #:cl)
(:local-nicknames (#:vk-ffi #:cl-vk/ffi)))
(in-package #:example)
(let ((global (vk-ffi:load-global-commands)))
(cl-ffi:with-foreign-object (version :uint32)
(vk-ffi:vk-enumerate-instance-version global version)
(let ((version (cl-ffi:mem-ref version :uint32)))
(list (vk-ffi:vk-api-version-major version)
(vk-ffi:vk-api-version-minor version)))))
;; => (1 4)
Names
| C | Lisp |
|---|---|
vkCreateInstance |
function vk-create-instance |
VkApplicationInfo |
struct vk-application-info, as (:struct vk-application-info) |
pNext, a member |
slot :p-next |
VK_SUCCESS, VK_WHOLE_SIZE |
constants +vk-success+, +vk-whole-size+ |
VK_API_VERSION_1_3 |
constant +vk-api-version-1-3+ |
VkResult, VkQueueFlags |
foreign types vk-result, vk-queue-flags |
PFN_vkAllocationFunction |
foreign type pfn-vk-allocation-function |
VK_MAKE_API_VERSION |
function vk-make-api-version |
Words of camel-case names are separated with hyphens; runs of capitals
and digits stay with their word (VkPhysicalDeviceVulkan13Features is
vk-physical-device-vulkan13-features, VkExtent2D is vk-extent2d).
The package uses no other package, not even common-lisp, so that it can
define names such as type and format of its own. Refer to it through
a package-local nickname. Slots are keywords, so bind them with explicit
pairs:
(cl-ffi:with-foreign-slots ((s-type :s-type) (p-next :p-next)) info
(:struct vk-ffi:vk-application-info)
...)
Command tables
Commands are called through function pointers, never through the loader's exported symbols. A command table holds the pointers of one level of the API, and every command takes its table first:
| Function | Table | Commands |
|---|---|---|
load-global-commands |
global-commands |
vkCreateInstance, vkEnumerateInstanceVersion, vkEnumerateInstanceExtensionProperties, vkEnumerateInstanceLayerProperties |
load-instance-commands instance |
instance-commands |
those of a VkInstance and its physical devices |
load-device-commands instance device |
device-commands |
those of a VkDevice, its queues and command buffers |
The device table's pointers come from vkGetDeviceProcAddr, so calls
go straight to the driver.
With the idiomatic layer, you never load tables: vk:instance-commands
and vk:device-commands return those of cl-vk's objects, and signal
vk:destroyed-object-error once the object is destroyed.
instance-commands-instance and device-commands-device return the
handle a table was loaded for. command-table is the type of all three.
Tables never change once loaded, and may be shared between threads.
Their pointers belong to the process that loaded them: a program that
keeps a table in an image it saves loads it again when the image starts,
as cl-vk does for the global table it keeps, through
cl-ffi:add-image-start-hook.
Missing commands
A table has no pointer for a command the instance or device does not
provide: one of an API version or extension that was not enabled, or that
the driver lacks. Calling it signals command-unavailable, whose
command-unavailable-name is the command's C name:
vkCmdDrawMeshTasksEXT is not available: the device did not provide it. Enable
the API version or extension that provides it when creating the device.
command-available-p asks first, by C name, aliases included:
(vk-ffi:command-available-p (vk:device-commands device) "vkCmdDrawMeshTasksEXT")
;; => NIL
Commands
Each command is a function that takes the table, then the C parameters in order:
(vk-ffi:vk-create-fence (vk:device-commands device) (vk:raw-handle device)
info (cl-ffi:null-pointer) fence-pointer)
- Values are passed as they are in C: integers, floats and foreign pointers. Enums and flags are integers; nothing is translated.
- Dispatchable handles (
VkInstance,VkPhysicalDevice,VkDevice,VkQueue,VkCommandBuffer) are foreign pointers; the others are 64-bit integers. - The result is returned as is: a
VkResultinteger, to give tovk:check, or whatever else the command returns. - Each argument's type is checked, so a wrong one signals a
type-errorrather than crashing. - Commands are inline, and a call allocates nothing when its pointers are on the stack or preallocated.
Each command's docstring gives its C prototype, its success codes and its error codes:
(documentation 'vk-ffi:vk-create-instance 'function)
;; => "Call vkCreateInstance through COMMANDS, a GLOBAL-COMMANDS table:
;;
;; VkResult vkCreateInstance(
;; const VkInstanceCreateInfo* pCreateInfo,
;; const VkAllocationCallbacks* pAllocator,
;; VkInstance* pInstance)
;;
;; Success codes: VK_SUCCESS.
;;
;; Error codes: VK_ERROR_OUT_OF_HOST_MEMORY, ..."
Structs
Structs and unions are cl-ffi structs, (:struct vk-ffi:vk-application-info).
Their layouts are checked against the C compiler's, member by member,
by a test (see Generator and Registry).
structure-type returns the value a struct's sType must hold, or
nil if it has none:
(vk-ffi:structure-type 'vk-ffi:vk-fence-create-info)
;; => 8 ; VK_STRUCTURE_TYPE_FENCE_CREATE_INFO
The idiomatic layer's with-structs, set-struct and struct-ref work
on these same structs and are far easier; see Structs.
Bit-fields
cl-ffi has no bit-fields, so each storage unit of C bit-fields is one
integer slot, :bit-field-0 and so on. bit-field and
(setf bit-field) read and write a field by name:
(setf (vk-ffi:bit-field instance 'vk-ffi:vk-acceleration-structure-instance-khr :mask) #xff)
(vk-ffi:bit-field instance 'vk-ffi:vk-acceleration-structure-instance-khr :mask)
;; => 255
(setf bit-field) signals a type-error for a value that does not fit,
expecting (unsigned-byte width). Both signal a type-error for a struct
without bit-fields or a field it has not, its expected type listing the
valid ones. The struct macros of the idiomatic layer take bit-fields by
name too.
Versions
The registry's version macros are functions:
| Function | C macro |
|---|---|
vk-make-api-version variant major minor patch |
VK_MAKE_API_VERSION |
vk-api-version-variant, -major, -minor, -patch version |
VK_API_VERSION_VARIANT and so on |
vk-make-version, vk-version-major, -minor, -patch |
the deprecated VK_MAKE_VERSION and so on |
vk-make-video-std-version major minor patch |
VK_MAKE_VIDEO_STD_VERSION |
(= (vk-ffi:vk-make-api-version 0 1 3 0) vk-ffi:+vk-api-version-1-3+)
;; => T
The loader
libvulkan is the cl-ffi foreign library for the Vulkan loader:
libvulkan.so.1 (or libvulkan.so) on Unix, vulkan-1.dll on Windows.
It is loaded when cl-vk/ffi is. Only vkGetInstanceProcAddr is looked
up by symbol; every other command comes from it.
Packages
cl-vk/ffi is generated, in registry/vulkan.lisp. The hand-written
runtime under it is cl-vk/ffi-support: the loader, the command tables,
the version functions and the macros the generated definitions expand
into (define-constant, define-ctype, define-enum, define-struct,
define-struct-alias, define-command, define-command-names).
cl-vk/ffi re-exports everything a user needs from it, so use
cl-vk/ffi.