5 Surfaces and Swapchains
dxtr edited this page 2026-10-18 17:12:34 +02:00

Surfaces and Swapchains

A surface is where Vulkan presents: a window, or nowhere for tests. A swapchain is the set of images presented to a surface in turn: each frame you acquire one, render to it, and present it.

Surfaces

cl-vk knows no window system. The library that owns the window creates the VkSurfaceKHR and gives it to cl-vk with adopt-surface. From then on the surface belongs to cl-vk: destroy it with destroy-surface.

With cl-windowing, its optional system cl-windowing/vulkan does this for you:

(vk:with-instance (instance :extensions (cl-windowing/vulkan:vulkan-instance-extensions
                                         window-system))
  (let ((surface (cl-windowing/vulkan:create-vulkan-surface window instance)))
    ...
    (vk:destroy-surface surface)))

To adopt a surface yourself, create it through the raw layer and pass the handle. For a Wayland window, given its wl_display and wl_surface pointers, with the instance made with "VK_KHR_surface" and "VK_KHR_wayland_surface":

(defun create-wayland-surface (instance display wl-surface)
  (vk:with-structs ((info vks:wayland-surface-create-info-khr
                          :display display :surface wl-surface))
    (cl-ffi:with-foreign-object (handle :uint64)
      (vk:check (vk-ffi:vk-create-wayland-surface-khr (vk:instance-commands instance)
                                                       (vk:raw-handle instance)
                                                       info (cl-ffi:null-pointer) handle)
                "vkCreateWaylandSurfaceKHR")
      (vk:adopt-surface instance (cl-ffi:mem-ref handle :uint64)))))

For tests and off-screen work, create-headless-surface makes a surface that presents nowhere. The instance needs the extensions "VK_KHR_surface" and "VK_EXT_headless_surface"; without them it signals an unsupported-error that says which to add.

Asking what a surface supports

What a surface supports depends on the physical device:

(vk:surface-formats gpu surface)
;; => ((:R8G8B8A8-UNORM :SRGB-NONLINEAR) (:B8G8R8A8-UNORM :SRGB-NONLINEAR)
;;     (:R8G8B8A8-SRGB :SRGB-NONLINEAR) (:B8G8R8A8-SRGB :SRGB-NONLINEAR))

(vk:surface-present-modes gpu surface)
;; => (:MAILBOX :FIFO)

(vk:surface-supported-p gpu 0 surface)   ; can queue family 0 present to it?
;; => T

surface-capabilities returns a surface-capabilities object:

Reader Value
surface-capabilities-min-image-count The fewest swapchain images.
surface-capabilities-max-image-count The most, or nil for no limit.
surface-capabilities-current-extent The surface's size (WIDTH HEIGHT), or nil when the swapchain decides it, as on Wayland and for headless surfaces.
surface-capabilities-min-extent, surface-capabilities-max-extent The bounds of the size.
surface-capabilities-max-array-layers The most image layers.
surface-capabilities-supported-transforms, surface-capabilities-current-transform Rotations and mirrors, as keywords.
surface-capabilities-supported-composite-alpha How alpha blends with the desktop, as keywords.
surface-capabilities-supported-usage Image usages a swapchain may have, such as :color-attachment and :transfer-dst.

Creating a swapchain

(vk:create-swapchain device surface :extent '(800 600))
;; => #<SWAPCHAIN 800x600 b8g8r8a8-srgb fifo>

The device needs the extension "VK_KHR_swapchain", and one of its queues must be able to present to the surface. create-swapchain takes:

Argument Default Meaning
:extent none The size (WIDTH HEIGHT), for a surface that leaves it to the swapchain. Ignored when the surface has a size of its own. Clamped to what the surface allows.
:format an sRGB format The image format, a vks:vk-format keyword. By default :b8g8r8a8-srgb, else :r8g8b8a8-srgb, else the first supported.
:color-space :srgb-nonlinear The colour space, a vks:color-space-khr keyword, used with :format.
:present-mode :fifo A vks:present-mode-khr keyword: :fifo waits for vertical blank and is always supported; :mailbox replaces the queued image; :immediate tears; :fifo-relaxed tears when late.
:image-count one more than the minimum The number of images.
:usage '(:color-attachment) Image usage flags. Add :transfer-dst to clear or copy into the images.
:old-swapchain nil The swapchain this one replaces; see below.

The other settings are fixed: one array layer, exclusive sharing, the surface's current transform, opaque composite alpha when supported, and clipping on.

Anything the surface does not support signals an unsupported-error before anything is created, naming what it does support:

The surface does not support the present mode :IMMEDIATE.  It supports :MAILBOX, :FIFO.

When the surface leaves the size to the swapchain and no :extent is given, create-swapchain signals a vk-error that asks for one.

The swapchain's images

Reader Value
swapchain-images The raw VkImage handles, in index order: an escape hatch to the raw layer.
swapchain-format, swapchain-color-space Keywords, a vks:vk-format and a vks:color-space-khr.
swapchain-extent (WIDTH HEIGHT).
swapchain-present-mode A vks:present-mode-khr keyword.
swapchain-usage A list of keywords.
swapchain-device, swapchain-surface What it was made from.

The images belong to the swapchain: do not destroy them. cl-vk does not wrap them, so swapchain-images is an escape hatch, as raw-handle is: image views for them are made through the raw layer, and are yours to destroy before the swapchain. Once the swapchain is destroyed, swapchain-images signals destroyed-object-error.

Each frame

(multiple-value-bind (index status)
    (vk:acquire-next-image swapchain :semaphore image-available)
  (case status
    ((:success :suboptimal)
     ;; Record and submit work that renders to (nth index (vk:swapchain-images swapchain)),
     ;; waiting on IMAGE-AVAILABLE and signalling RENDER-FINISHED.
     (case (vk:present queue swapchain index :wait-semaphore render-finished)
       ((:suboptimal :out-of-date) (recreate))))
    (:out-of-date (recreate))))

acquire-next-image asks for the next image to render to. It signals the given :semaphore, :fence, or both, when the image is ready; at least one is needed. It waits at most :timeout nanoseconds, forever by default, and returns two values, the image's index and a status:

Status Index Meaning
:success an integer Render to it.
:suboptimal an integer Usable, but the swapchain no longer matches the surface exactly: recreate it soon.
:out-of-date nil The swapchain no longer matches the surface: recreate it, then acquire again.
:timeout nil No image within :timeout.
:not-ready nil No image yet, with :timeout 0.

present queues the image for presentation on a queue, after :wait-semaphore is signaled, and returns :success, :suboptimal (presented, but recreate the swapchain soon) or :out-of-date (recreate the swapchain).

Neither function signals for these statuses, since they are part of normal operation; other failures, such as a lost device or surface, signal a result-error. Neither allocates.

Recipes has a complete frame loop, with the command buffer, the barriers and the submission, that runs clean under synchronization validation.

Recreating a swapchain

After a resize, the surface and swapchain no longer match. Wait for the device to finish with the old images, then call recreate-swapchain:

(vk:device-wait-idle device)
(setf swapchain (vk:recreate-swapchain swapchain))
;; or, for a surface that leaves its size to the swapchain:
(setf swapchain (vk:recreate-swapchain swapchain :extent (list new-width new-height)))

It creates a swapchain with the same settings for the surface's current size, or :extent, passing the old one as :old-swapchain so the driver can reuse its resources, and destroys the old one. Without :extent and without a size from the surface, the old extent is kept. Image views and framebuffers of the old images must be recreated too.

Destroying

destroy-swapchain destroys a swapchain; its images must no longer be in use. destroy-surface destroys a surface; the swapchains made for it must be destroyed first, or it signals live-children-error.

Thread rules

A surface may be queried from several threads at once; creating a swapchain for it or destroying it must not overlap any other use. Acquiring, presenting and destroying a swapchain must happen on one thread at a time: in practice the render thread owns it.