Commit 8e7f3950 authored by Niklas Haas's avatar Niklas Haas

vulkan: add swapchain implementation

Mostly ported from mpv, but some parts were slightly rewritten. Notably,
the way window resizing is handled. Instead of having two different
sources of window size information and having to argue about which one
to take as authoritative, we only use the current extent as reported by
the vulkan implementation.

Apparently, not all platforms report valid values here - particularly,
wayland apparently doesn't. If somebody has an idea for how to figure
out the size of a wayland window, feel free to tell me; because the
vulkan wayland extension doesn't seem to consider it being possible.
parent 19242bed
......@@ -119,4 +119,40 @@ const struct pl_vulkan *pl_vulkan_create(struct pl_context *ctx,
// destroyed by the user before calling this.
void pl_vulkan_destroy(const struct pl_vulkan **vk);
struct pl_vulkan_swapchain_params {
// The surface to use for rendering. Required, the user is in charge of
// creating this. Must belong to the same VkInstance as `vk->instance`.
VkSurfaceKHR surface;
// The image format and colorspace we should be using. Optional, if left
// as {0}, libplacebo will pick the best surface format based on what the
// GPU/surface seems to support.
VkSurfaceFormatKHR surface_format;
// The preferred presentation mode. See the vulkan documentation for more
// information about these. If the device/surface combination does not
// support this mode, libplacebo will fall back to VK_PRESENT_MODE_FIFO_KHR.
// Warning: Leaving this zero-initialized is the same as having specified
// VK_PRESENT_MODE_IMMEDIATE_KHR, which is probably not what the user
// wants!
VkPresentModeKHR present_mode;
// Allow up to N in-flight frames. This essentially controls how many
// rendering commands may be queued up at the same time. See the
// documentation for `ra_swapchain_get_latency` for more information. For
// vulkan specifically, we are only able to wait until the GPU has finished
// rendering a frame - we are unable to wait until the display has actually
// finished displaying it. So this only provides a rough guideline.
// Optional, defaults to 3.
int swapchain_depth;
// Creates a new vulkan swapchain based on an existing VkSurfaceKHR. Using this
// function requires that the vulkan device was created with the
// VK_KHR_swapchain extension. The easiest way of accomplishing this is to set
// the `pl_vulkan_params.surface` explicitly at creation time.
const struct ra_swapchain *pl_vulkan_create_swapchain(const struct pl_vulkan *vk,
const struct pl_vulkan_swapchain_params *params);
......@@ -75,6 +75,7 @@ components = [
......@@ -256,10 +256,8 @@ static bool device_init(struct vk_ctx *vk, const struct pl_vulkan_params *params
const char **exts = NULL;
int num_exts = 0;
if (params->surface) {
if (params->surface)
// Add extra user extensions
for (int i = 0; i < params->num_extensions; i++)
* This file is part of libplacebo.
* libplacebo is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
* libplacebo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* GNU Lesser General Public License for more details.
* You should have received a copy of the GNU Lesser General Public
* License along with libplacebo. If not, see <>.
#include "common.h"
#include "command.h"
#include "formats.h"
#include "utils.h"
#include "ra_vk.h"
#include "swapchain.h"
struct priv {
struct vk_ctx *vk;
VkSurfaceKHR surf;
// current swapchain and metadata:
VkSwapchainCreateInfoKHR protoInfo; // partially filled-in prototype
VkSwapchainKHR swapchain;
VkSwapchainKHR old_swapchain;
int swapchain_depth;
int frames_in_flight; // number of frames currently queued
struct pl_color_repr color_repr;
struct pl_color_space color_space;
// state of the images:
const struct ra_tex **images; // ra_tex wrappers for the VkImages
int num_images; // size of `images`
VkSemaphore *sems_in; // pool of semaphores used to synchronize images
VkSemaphore *sems_out; // outgoing semaphores (rendering complete)
int num_sems; // size of `sems_in` / `sems_out`
int idx_sems; // index of next free semaphore pair
int last_imgidx; // the image index last acquired (for submit)
static const struct ra_sw vulkan_swapchain;
static bool pick_surf_format(const struct ra *ra, const struct vk_ctx *vk,
VkSurfaceKHR surf, VkSurfaceFormatKHR *out_format)
VkSurfaceFormatKHR *formats = NULL;
int num = 0;
VK(vkGetPhysicalDeviceSurfaceFormatsKHR(vk->physd, surf, &num, NULL));
formats = talloc_array(NULL, VkSurfaceFormatKHR, num);
VK(vkGetPhysicalDeviceSurfaceFormatsKHR(vk->physd, surf, &num, formats));
for (int i = 0; i < num; i++) {
// A value of VK_FORMAT_UNDEFINED means we can pick anything we want
if (formats[i].format == VK_FORMAT_UNDEFINED) {
*out_format = (VkSurfaceFormatKHR) {
.format = VK_FORMAT_R16G16B16A16_UNORM,
return true;
// Try and avoid exotic color spaces like HDR output for now
// TODO: support these and map the correct `pl_color_space`
if (formats[i].colorSpace != VK_COLOR_SPACE_SRGB_NONLINEAR_KHR)
// Format whitelist, since we want only >= 8 bit _UNORM formats
switch (formats[i].format) {
case VK_FORMAT_R16G16B16A16_UNORM:
break; // accept
default: continue;
// Make sure we can wrap this format to a meaningful, renderable ra_format
for (int n = 0; n < ra->num_formats; n++) {
const struct ra_fmt *rafmt = ra->formats[n];
const struct vk_format *vkfmt = rafmt->priv;
if (vkfmt->ifmt != formats[i].format)
if (!(rafmt->caps & RA_FMT_CAP_RENDERABLE))
// format valid, use it
*out_format = formats[i];
return true;
// fall through
PL_FATAL(vk, "Failed picking any valid, renderable surface format!");
return false;
const struct ra_swapchain *pl_vulkan_create_swapchain(const struct pl_vulkan *plvk,
const struct pl_vulkan_swapchain_params *params)
struct vk_ctx *vk = plvk->priv;
const struct ra *ra = plvk->ra;
VkSurfaceFormatKHR sfmt = params->surface_format;
if (!sfmt.format && !pick_surf_format(ra, vk, params->surface, &sfmt))
return NULL;
struct ra_swapchain *sw = talloc_zero(NULL, struct ra_swapchain);
sw->impl = &vulkan_swapchain;
sw->ctx = vk->ctx;
sw->ra = ra;
struct priv *p = sw->priv = talloc_zero(sw, struct priv);
p->vk = vk;
p->surf = params->surface;
p->swapchain_depth = PL_DEF(params->swapchain_depth, 3);
pl_assert(p->swapchain_depth > 0);
p->protoInfo = (VkSwapchainCreateInfoKHR) {
.surface = p->surf,
.imageFormat = sfmt.format,
.imageColorSpace = sfmt.colorSpace,
.imageArrayLayers = 1, // non-stereoscopic
.minImageCount = p->swapchain_depth + 1, // +1 for the FB
.presentMode = params->present_mode,
.clipped = true,
p->color_space = pl_color_space_monitor;
p->color_repr = (struct pl_color_repr) {
.alpha = PL_ALPHA_UNKNOWN, // will be updated by vk_sw_recreate
// Make sure the swapchain present mode is supported
VkPresentModeKHR *modes = NULL;
int num_modes;
VK(vkGetPhysicalDeviceSurfacePresentModesKHR(vk->physd, p->surf,
&num_modes, NULL));
modes = talloc_array(NULL, VkPresentModeKHR, num_modes);
VK(vkGetPhysicalDeviceSurfacePresentModesKHR(vk->physd, p->surf,
&num_modes, modes));
bool supported = false;
for (int i = 0; i < num_modes; i++)
supported |= (modes[i] == p->protoInfo.presentMode);
if (!supported) {
PL_WARN(vk, "Requested swap mode unsupported by this device, falling "
p->protoInfo.presentMode = VK_PRESENT_MODE_FIFO_KHR;
return sw;
return NULL;
static void vk_sw_destroy(const struct ra_swapchain *sw)
const struct ra *ra = sw->ra;
struct priv *p = sw->priv;
struct vk_ctx *vk = p->vk;
for (int i = 0; i < p->num_images; i++)
ra_tex_destroy(ra, &p->images[i]);
for (int i = 0; i < p->num_sems; i++) {
vkDestroySemaphore(vk->dev, p->sems_in[i], VK_ALLOC);
vkDestroySemaphore(vk->dev, p->sems_out[i], VK_ALLOC);
vkDestroySwapchainKHR(vk->dev, p->swapchain, VK_ALLOC);
talloc_free((void *) sw);
static int vk_sw_latency(const struct ra_swapchain *sw)
struct priv *p = sw->priv;
return p->swapchain_depth;
static bool update_swapchain_info(struct priv *p, VkSwapchainCreateInfoKHR *info)
struct vk_ctx *vk = p->vk;
// Query the supported capabilities and update this struct as needed
VkSurfaceCapabilitiesKHR caps;
VK(vkGetPhysicalDeviceSurfaceCapabilitiesKHR(vk->physd, p->surf, &caps));
// Sorted by preference
static const struct { VkCompositeAlphaFlagsKHR vk_mode;
enum pl_alpha_mode pl_mode;
} alphaModes[] = {
for (int i = 0; i < PL_ARRAY_SIZE(alphaModes); i++) {
if (caps.supportedCompositeAlpha & alphaModes[i].vk_mode) {
info->compositeAlpha = alphaModes[i].vk_mode;
p->color_repr.alpha = alphaModes[i].pl_mode;
if (!info->compositeAlpha) {
PL_ERR(vk, "Failed picking alpha compositing mode (caps: 0x%x)",
goto error;
// Note: We could probably also allow picking a surface transform that
// flips the framebuffer and set `ra_swapchain_frame.flipped`, but this
// doesn't appear to be necessary for any vulkan implementations.
static const VkSurfaceTransformFlagsKHR rotModes[] = {
for (int i = 0; i < PL_ARRAY_SIZE(rotModes); i++) {
if (caps.supportedTransforms & rotModes[i]) {
info->preTransform = rotModes[i];
if (!info->preTransform) {
PL_ERR(vk, "Failed picking surface transform mode (caps: 0x%x)",
goto error;
// Image count as required
PL_DEBUG(vk, "Requested image count: %d (min %d max %d)\n",
(int) info->minImageCount, (int) caps.minImageCount,
(int) caps.maxImageCount);
info->minImageCount = PL_MAX(info->minImageCount, caps.minImageCount);
if (caps.maxImageCount)
info->minImageCount = PL_MIN(info->minImageCount, caps.maxImageCount);
// This seems to be an obscure case, and doesn't make sense anyway. So just
// ignore it and assume we're using a sane environment where the current
// window size is known.
if (caps.currentExtent.width == 0xFFFFFFFF ||
caps.currentExtent.height == 0xFFFFFFFF)
PL_ERR(vk, "The swapchain's current extent is reported as unknown. "
"In other words, we don't know the size of the window. Giving up!");
goto error;
// This seems to be an obscure case that should technically violate the spec
// anyway, but better safe than sorry..
if (!caps.currentExtent.width || !caps.currentExtent.height) {
PL_WARN(vk, "Unable to recreate swapchain: image extent is 0, possibly "
"the window is minimized or hidden?");
goto error;
info->imageExtent = caps.currentExtent;
// We just request whatever usage we can, and let the ra_vk decide what
// ra_tex_params that translates to. This makes the images as flexible
// as possible.
info->imageUsage = caps.supportedUsageFlags;
return true;
return false;
static void destroy_swapchain(struct vk_ctx *vk, struct priv *p)
vkDestroySwapchainKHR(vk->dev, p->old_swapchain, VK_ALLOC);
p->old_swapchain = NULL;
static bool vk_sw_recreate(const struct ra_swapchain *sw)
const struct ra *ra = sw->ra;
struct priv *p = sw->priv;
struct vk_ctx *vk = p->vk;
VkImage *vkimages = NULL;
int num_images = 0;
// It's invalid to trigger another swapchain recreation while there's more
// than one swapchain already active, so we need to flush any pending
// asynchronous swapchain release operations that may be ongoing
while (p->old_swapchain)
vk_poll_commands(vk, 1000000); // 1 ms
VkSwapchainCreateInfoKHR sinfo = p->protoInfo;
sinfo.oldSwapchain = p->swapchain;
if (!update_swapchain_info(p, &sinfo))
goto error;
VK(vkCreateSwapchainKHR(vk->dev, &sinfo, VK_ALLOC, &p->swapchain));
// Freeing the old swapchain while it's still in use is an error, so do it
// asynchronously once the device is idle
if (sinfo.oldSwapchain) {
p->old_swapchain = sinfo.oldSwapchain;
vk_dev_callback(vk, (vk_cb) destroy_swapchain, vk, p);
// Get the new swapchain images
VK(vkGetSwapchainImagesKHR(vk->dev, p->swapchain, &num_images, NULL));
vkimages = talloc_array(NULL, VkImage, num_images);
VK(vkGetSwapchainImagesKHR(vk->dev, p->swapchain, &num_images, vkimages));
// If needed, allocate some more semaphores
while (num_images > p->num_sems) {
VkSemaphore sem_in, sem_out;
static const VkSemaphoreCreateInfo seminfo = {
VK(vkCreateSemaphore(vk->dev, &seminfo, VK_ALLOC, &sem_in));
VK(vkCreateSemaphore(vk->dev, &seminfo, VK_ALLOC, &sem_out));
int idx = p->num_sems++;
TARRAY_GROW(p, p->sems_in, idx);
TARRAY_GROW(p, p->sems_out, idx);
p->sems_in[idx] = sem_in;
p->sems_out[idx] = sem_out;
// Recreate the ra_tex wrappers
for (int i = 0; i < p->num_images; i++)
ra_tex_destroy(ra, &p->images[i]);
p->num_images = num_images;
TARRAY_GROW(p, p->images, num_images);
for (int i = 0; i < num_images; i++) {
p->images[i] = ra_vk_wrap_swapchain_img(ra, vkimages[i], sinfo);
if (!p->images[i])
goto error;
pl_assert(num_images > 0);
int bits = 0;
// The channel with the most bits is probably the most authoritative about
// the actual color information (consider e.g. a2bgr10). Slight downside
// in that it results in rounding r/b for e.g. rgb565, but we don't pick
// surfaces with fewer than 8 bits anyway, so let's not care for now.
const struct ra_fmt *fmt = p->images[0]->params.format;
for (int i = 0; i < fmt->num_components; i++)
bits = PL_MAX(bits, fmt->component_depth[i]);
p->color_repr.bits.sample_depth = bits;
p->color_repr.bits.color_depth = bits;
return true;
PL_ERR(vk, "Failed (re)creating swapchain!");
vkDestroySwapchainKHR(vk->dev, p->swapchain, VK_ALLOC);
p->swapchain = NULL;
return false;
static bool vk_sw_start_frame(const struct ra_swapchain *sw,
struct ra_swapchain_frame *out_frame)
struct priv *p = sw->priv;
struct vk_ctx *vk = p->vk;
if (!p->swapchain && !vk_sw_recreate(sw))
return false;
VkSemaphore sem_in = p->sems_in[p->idx_sems];
PL_TRACE(vk, "vkAcquireNextImageKHR signals %p", (void *) sem_in);
for (int attempts = 0; attempts < 2; attempts++) {
uint32_t imgidx = 0;
VkResult res = vkAcquireNextImageKHR(vk->dev, p->swapchain, UINT64_MAX,
sem_in, NULL, &imgidx);
switch (res) {
p->last_imgidx = imgidx;
*out_frame = (struct ra_swapchain_frame) {
.fbo = p->images[imgidx],
.flipped = false,
.color_repr = p->color_repr,
.color_space = p->color_space,
ra_tex_vk_external_dep(sw->ra, out_frame->fbo, sem_in);
return true;
// In these cases try recreating the swapchain
if (!vk_sw_recreate(sw))
return false;
PL_ERR(vk, "Failed acquiring swapchain image: %s", vk_res_str(res));
return false;
// If we've exhausted the number of attempts to recreate the swapchain,
// just give up silently and let the user retry some time later.
return false;
static void present_cb(struct priv *p, void *arg)
static bool vk_sw_submit_frame(const struct ra_swapchain *sw)
const struct ra *ra = sw->ra;
struct priv *p = sw->priv;
struct vk_ctx *vk = p->vk;
if (!p->swapchain)
return false;
struct vk_cmd *cmd = ra_vk_finish_frame(ra, p->images[p->last_imgidx]);
if (!cmd)
return false;
VkSemaphore sem_out = p->sems_out[p->idx_sems++];
p->idx_sems %= p->num_sems;
vk_cmd_sig(cmd, sem_out);
vk_cmd_callback(cmd, (vk_cb) present_cb, p, NULL);
vk_cmd_queue(vk, cmd);
if (!vk_flush_commands(vk))
return false;
// Older nvidia drivers can spontaneously combust when submitting to the
// same queue as we're rendering from, in a multi-queue scenario. Safest
// option is to flush the commands first and then submit to the next queue.
// We can drop this hack in the future, I suppose.
struct vk_cmdpool *pool = vk->pool_graphics;
VkQueue queue = pool->queues[pool->idx_queues];
VkPresentInfoKHR pinfo = {
.waitSemaphoreCount = 1,
.pWaitSemaphores = &sem_out,
.swapchainCount = 1,
.pSwapchains = &p->swapchain,
.pImageIndices = &p->last_imgidx,
PL_TRACE(vk, "vkQueuePresentKHR waits on %p", (void *) sem_out);
VkResult res = vkQueuePresentKHR(queue, &pinfo);
switch (res) {
return true;
// We can silently ignore this error, since the next start_frame will
// recreate the swapchain automatically.
return true;
PL_ERR(vk, "Failed presenting to queue %p: %s", (void *) queue,
return false;
static void vk_sw_swap_buffers(const struct ra_swapchain *sw)
struct priv *p = sw->priv;
while (p->frames_in_flight >= p->swapchain_depth)
vk_poll_commands(p->vk, 1000000); // 1 ms
static const struct ra_sw vulkan_swapchain = {
.destroy = vk_sw_destroy,
.latency = vk_sw_latency,
.start_frame = vk_sw_start_frame,
.submit_frame = vk_sw_submit_frame,
.swap_buffers = vk_sw_swap_buffers,
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment