Files
Vulkan-Samples/samples/api/swapchain_recreation/swapchain_recreation.cpp
T
2025-09-04 10:54:47 +08:00

1138 lines
38 KiB
C++

/* Copyright (c) 2023-2025, Google
*
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 the "License";
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "swapchain_recreation.h"
#include "common/vk_common.h"
#include "core/util/logging.hpp"
#include "filesystem/legacy.h"
static constexpr uint32_t INVALID_IMAGE_INDEX = std::numeric_limits<uint32_t>::max();
void SwapchainRecreation::get_queue()
{
queue = &get_device().get_queue_by_flags(VK_QUEUE_GRAPHICS_BIT, 0);
// Make sure presentation is supported on this queue. This is practically always the case;
// if a platform/driver is found where this is not true, all queues supporting
// VK_QUEUE_GRAPHICS_BIT need to be queried and one that supports presentation picked.
VkBool32 supports_present = VK_FALSE;
vkGetPhysicalDeviceSurfaceSupportKHR(get_gpu_handle(), queue->get_family_index(), get_surface(), &supports_present);
if (!supports_present)
{
throw std::runtime_error("Default graphics queue does not support present.");
}
}
void SwapchainRecreation::query_surface_format()
{
surface_format = vkb::select_surface_format(get_gpu_handle(), get_surface());
}
void SwapchainRecreation::query_present_modes()
{
uint32_t present_mode_count = 0;
VK_CHECK(vkGetPhysicalDeviceSurfacePresentModesKHR(get_gpu_handle(), get_surface(), &present_mode_count, nullptr));
present_modes.resize(present_mode_count);
VK_CHECK(vkGetPhysicalDeviceSurfacePresentModesKHR(get_gpu_handle(), get_surface(), &present_mode_count, present_modes.data()));
adjust_desired_present_mode();
}
/**
* @brief Get the list of present modes compatible with the current mode. If present mode is
* changed and the two modes are compatible, swapchain is not recreated.
*/
void SwapchainRecreation::query_compatible_present_modes(VkPresentModeKHR present_mode)
{
// If manually overriden, or if VK_EXT_surface_maintenance1 is not supported, assume no
// compatible present modes.
if (!has_maintenance1 || recreate_swapchain_on_present_mode_change)
{
compatible_modes.resize(1);
compatible_modes[0] = present_mode;
return;
}
VkPhysicalDeviceSurfaceInfo2KHR surface_info{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SURFACE_INFO_2_KHR};
surface_info.surface = get_surface();
VkSurfacePresentModeEXT surface_present_mode{VK_STRUCTURE_TYPE_SURFACE_PRESENT_MODE_EXT};
surface_present_mode.presentMode = present_mode;
surface_info.pNext = &surface_present_mode;
VkSurfaceCapabilities2KHR surface_caps{VK_STRUCTURE_TYPE_SURFACE_CAPABILITIES_2_KHR};
VkSurfacePresentModeCompatibilityEXT modes{VK_STRUCTURE_TYPE_SURFACE_PRESENT_MODE_COMPATIBILITY_EXT};
modes.presentModeCount = 0;
surface_caps.pNext = &modes;
VK_CHECK(vkGetPhysicalDeviceSurfaceCapabilities2KHR(get_gpu_handle(), &surface_info, &surface_caps));
compatible_modes.resize(modes.presentModeCount);
modes.pPresentModes = compatible_modes.data();
VK_CHECK(vkGetPhysicalDeviceSurfaceCapabilities2KHR(get_gpu_handle(), &surface_info, &surface_caps));
}
void SwapchainRecreation::adjust_desired_present_mode()
{
// The FIFO present mode is guaranteed to be present.
if (desired_present_mode == VK_PRESENT_MODE_FIFO_KHR)
{
return;
}
// When switching to MAILBOX, fallback to IMMEDIATE if not available and back to FIFO if
// neither are available.
if (desired_present_mode == VK_PRESENT_MODE_MAILBOX_KHR && std::ranges::find(present_modes, desired_present_mode) != present_modes.end())
{
return;
}
desired_present_mode = VK_PRESENT_MODE_IMMEDIATE_KHR;
if (std::ranges::find(present_modes, desired_present_mode) == present_modes.end())
{
LOGW("Neither MAILBOX nor IMMEDIATE are supported, falling back to FIFO");
desired_present_mode = VK_PRESENT_MODE_FIFO_KHR;
}
}
void SwapchainRecreation::create_render_pass()
{
VkAttachmentDescription attachment = {0};
attachment.format = surface_format.format;
attachment.samples = VK_SAMPLE_COUNT_1_BIT;
attachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
attachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
attachment.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
VkAttachmentReference color_ref = {0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL};
VkSubpassDescription subpass = {0};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = 1;
subpass.pColorAttachments = &color_ref;
// Create a dependency from external such that srcStageMask matches WSI semaphore wait stage
// (pWaitDstStageMask)
VkSubpassDependency dependency = {0};
dependency.srcSubpass = VK_SUBPASS_EXTERNAL;
dependency.dstSubpass = 0;
dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dependency.srcAccessMask = 0;
dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
dependency.dependencyFlags = 0;
VkRenderPassCreateInfo rp_info = {VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO};
rp_info.attachmentCount = 1;
rp_info.pAttachments = &attachment;
rp_info.subpassCount = 1;
rp_info.pSubpasses = &subpass;
rp_info.dependencyCount = 1;
rp_info.pDependencies = &dependency;
VK_CHECK(vkCreateRenderPass(get_device_handle(), &rp_info, nullptr, &render_pass));
}
bool SwapchainRecreation::are_present_modes_compatible()
{
// Look in the list of compatible present modes (which was created for
// current_present_mode). If desired_present_mode is in that list, then there's no need to
// recreate the swapchain. Note that current_present_mode is in this list as well.
//
// While this functionality was introduced by VK_EXT_surface_maintenance1, compatible_modes
// is always set up such that every present mode is assumed to be compatible only with
// itself; there is no need for an extension check here.
return std::ranges::find(compatible_modes, desired_present_mode) != compatible_modes.end();
}
/**
* @brief Initializes the Vulkan swapchain.
*/
void SwapchainRecreation::init_swapchain()
{
VkSurfaceCapabilitiesKHR surface_properties;
VK_CHECK(vkGetPhysicalDeviceSurfaceCapabilitiesKHR(get_gpu_handle(), get_surface(), &surface_properties));
if (surface_properties.currentExtent.width == 0xFFFFFFFF)
{
swapchain_extents = VkExtent2D{400, 300};
}
else
{
swapchain_extents = surface_properties.currentExtent;
}
// Do triple-buffering when possible. This is clamped to the min and max image count limits.
uint32_t desired_swapchain_images = std::max(surface_properties.minImageCount, 3u);
if (surface_properties.maxImageCount > 0)
{
desired_swapchain_images = std::min(desired_swapchain_images, surface_properties.maxImageCount);
}
// Find a supported composite type.
VkCompositeAlphaFlagBitsKHR composite = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
if (surface_properties.supportedCompositeAlpha & VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR)
{
composite = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
}
else if (surface_properties.supportedCompositeAlpha & VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR)
{
composite = VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR;
}
else if (surface_properties.supportedCompositeAlpha & VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR)
{
composite = VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR;
}
else if (surface_properties.supportedCompositeAlpha & VK_COMPOSITE_ALPHA_POST_MULTIPLIED_BIT_KHR)
{
composite = VK_COMPOSITE_ALPHA_POST_MULTIPLIED_BIT_KHR;
}
VkSwapchainKHR old_swapchain = swapchain;
VkSwapchainCreateInfoKHR info{VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR};
info.surface = get_surface();
info.minImageCount = desired_swapchain_images;
info.imageFormat = surface_format.format;
info.imageColorSpace = surface_format.colorSpace;
info.imageExtent = swapchain_extents;
info.imageArrayLayers = 1;
info.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
info.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
info.preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
info.compositeAlpha = composite;
info.presentMode = desired_present_mode;
info.clipped = true;
info.oldSwapchain = old_swapchain;
// Note: the above info sets preTransform to `VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR`. This
// is **not optimal** on devices that support rotation and will lead to measurable
// performance loss. It is strongly recommended that `surface_properties.currentTransform`
// be used instead. However, the application is required to handle preTransform elsewhere
// accordingly.
query_compatible_present_modes(desired_present_mode);
VkSwapchainPresentModesCreateInfoEXT compatible_modes_info{VK_STRUCTURE_TYPE_SWAPCHAIN_PRESENT_MODES_CREATE_INFO_EXT};
if (has_maintenance1)
{
// When VK_EXT_swapchain_maintenance1 is available, you can optionally amortize the
// cost of swapchain image allocations over multiple frames.
info.flags |= VK_SWAPCHAIN_CREATE_DEFERRED_MEMORY_ALLOCATION_BIT_EXT;
// If there are multiple present modes that are compatible, give that list to create
// info. When switching present modes between compatible ones, swapchain doesn't
// need to be recreated.
if (compatible_modes.size() > 1)
{
compatible_modes_info.presentModeCount = static_cast<uint32_t>(compatible_modes.size());
compatible_modes_info.pPresentModes = compatible_modes.data();
info.pNext = &compatible_modes_info;
}
}
LOGI("Creating new swapchain");
VK_CHECK(vkCreateSwapchainKHR(get_device_handle(), &info, nullptr, &swapchain));
++swapchain_creation_count;
current_present_mode = desired_present_mode;
// Schedule destruction of the old swapchain resources once this frame's submission is finished.
submit_history[submit_history_index].swapchain_garbage.push_back(std::move(swapchain_objects));
// Schedule destruction of the old swapchain itself once its last presentation is finished.
if (old_swapchain != VK_NULL_HANDLE)
{
schedule_old_swapchain_for_destruction(old_swapchain);
}
// Get the swapchain images.
uint32_t image_count;
VK_CHECK(vkGetSwapchainImagesKHR(get_device_handle(), swapchain, &image_count, nullptr));
swapchain_objects.images.resize(image_count, VK_NULL_HANDLE);
swapchain_objects.views.resize(image_count, VK_NULL_HANDLE);
swapchain_objects.framebuffers.resize(image_count, VK_NULL_HANDLE);
VK_CHECK(vkGetSwapchainImagesKHR(get_device_handle(), swapchain, &image_count, swapchain_objects.images.data()));
if (!has_maintenance1)
{
// When VK_SWAPCHAIN_CREATE_DEFERRED_MEMORY_ALLOCATION_BIT_EXT is used, image views
// cannot be created until the first time the image is acquired.
for (uint32_t index = 0; index < image_count; ++index)
{
init_swapchain_image(index);
}
}
}
/**
* @brief Called to initialize resources for a swapchain image.
*/
void SwapchainRecreation::init_swapchain_image(uint32_t index)
{
assert(swapchain_objects.views[index] == VK_NULL_HANDLE);
VkImageViewCreateInfo view_info{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
view_info.viewType = VK_IMAGE_VIEW_TYPE_2D;
view_info.format = surface_format.format;
view_info.image = swapchain_objects.images[index];
view_info.subresourceRange.levelCount = 1;
view_info.subresourceRange.layerCount = 1;
view_info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
view_info.components.r = VK_COMPONENT_SWIZZLE_R;
view_info.components.g = VK_COMPONENT_SWIZZLE_G;
view_info.components.b = VK_COMPONENT_SWIZZLE_B;
view_info.components.a = VK_COMPONENT_SWIZZLE_A;
VK_CHECK(vkCreateImageView(get_device_handle(), &view_info, nullptr, &swapchain_objects.views[index]));
VkFramebufferCreateInfo fb_info{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
fb_info.renderPass = render_pass;
fb_info.attachmentCount = 1;
fb_info.pAttachments = &swapchain_objects.views[index];
fb_info.width = swapchain_extents.width;
fb_info.height = swapchain_extents.height;
fb_info.layers = 1;
VK_CHECK(vkCreateFramebuffer(get_device_handle(), &fb_info, nullptr, &swapchain_objects.framebuffers[index]));
}
/**
* @brief When a swapchain is retired, the resources associated with its images are scheduled to be
* cleaned up as soon as the last submission using those images is complete. This function is
* called at such a moment.
*
* The swapchain itself is not destroyed until known safe.
*/
void SwapchainRecreation::cleanup_swapchain_objects(SwapchainObjects &garbage)
{
for (VkImageView view : garbage.views)
{
vkDestroyImageView(get_device_handle(), view, nullptr);
}
for (VkFramebuffer framebuffer : garbage.framebuffers)
{
vkDestroyFramebuffer(get_device_handle(), framebuffer, nullptr);
}
garbage = {};
}
bool SwapchainRecreation::recreate_swapchain()
{
VkSurfaceCapabilitiesKHR surface_properties;
VK_CHECK(vkGetPhysicalDeviceSurfaceCapabilitiesKHR(get_gpu_handle(), get_surface(), &surface_properties));
// Only rebuild the swapchain if the dimensions have changed
if (surface_properties.currentExtent.width == swapchain_extents.width &&
surface_properties.currentExtent.height == swapchain_extents.height &&
are_present_modes_compatible())
{
return false;
}
init_swapchain();
return true;
}
void SwapchainRecreation::setup_frame()
{
// For the frame we need:
// - A fence for the submission
// - A semaphore for image acquire
// - A semaphore for image present
// But first, pace the CPU. Wait for frame N-2 to finish before starting recording of frame N.
submit_history_index = (submit_history_index + 1) % submit_history.size();
PerFrame &frame = submit_history[submit_history_index];
if (frame.submit_fence != VK_NULL_HANDLE)
{
vkWaitForFences(get_device_handle(), 1, &frame.submit_fence, true, UINT64_MAX);
// Reset/recycle resources, they are no longer in use.
recycle_fence(frame.submit_fence);
recycle_semaphore(frame.acquire_semaphore);
vkResetCommandPool(get_device_handle(), frame.command_pool, 0);
// Destroy any garbage that's associated with this submission.
for (SwapchainObjects &garbage : frame.swapchain_garbage)
{
cleanup_swapchain_objects(garbage);
}
frame.swapchain_garbage.clear();
// Note that while the submission fence, the semaphore it waited on and the command
// pool its command was allocated from are guaranteed to have finished execution,
// there is no guarantee that the present semaphore is not in use.
//
// This is because the fence wait above ensures that the submission _before_ present
// is finished, but makes no guarantees as to the state of the present operation
// that follows. The present semaphore is queued for garbage collection when
// possible after present, and is not kept as part of the submit history.
assert(frame.present_semaphore == VK_NULL_HANDLE);
}
frame.submit_fence = get_fence();
frame.acquire_semaphore = get_semaphore();
frame.present_semaphore = get_semaphore();
if (frame.command_pool == VK_NULL_HANDLE)
{
VkCommandPoolCreateInfo cmd_pool_info{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
cmd_pool_info.flags = VK_COMMAND_POOL_CREATE_TRANSIENT_BIT;
cmd_pool_info.queueFamilyIndex = queue->get_family_index();
VK_CHECK(vkCreateCommandPool(get_device_handle(), &cmd_pool_info, nullptr, &frame.command_pool));
VkCommandBufferAllocateInfo cmd_buf_info{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
cmd_buf_info.commandPool = frame.command_pool;
cmd_buf_info.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
cmd_buf_info.commandBufferCount = 1;
VK_CHECK(vkAllocateCommandBuffers(get_device_handle(), &cmd_buf_info, &frame.command_buffer));
}
}
void SwapchainRecreation::render(uint32_t index)
{
PerFrame &frame = submit_history[submit_history_index];
VkCommandBufferBeginInfo begin_info{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
begin_info.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
vkBeginCommandBuffer(frame.command_buffer, &begin_info);
// Render the following with basic vkCmdClearAttachments calls:
// - A gray rectangle that scales with the size of the extent
// - A fixed size square with changing color based on FPS
VkClearValue black;
black.color = {{0, 0, 0, 1.0f}};
VkClearValue gray;
gray.color = {{0.5f, 0.5f, 0.5f, 1.0f}};
VkClearValue colorful;
colorful.color = {{
static_cast<float>(frame_number % 256) / 255.0f,
static_cast<float>((frame_number + 63) % 256) / 255.0f,
static_cast<float>((frame_number + 128) % 256) / 255.0f,
1.0f,
}};
VkRenderPassBeginInfo rp_begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO};
rp_begin.renderPass = render_pass;
rp_begin.framebuffer = swapchain_objects.framebuffers[index];
rp_begin.renderArea.extent = swapchain_extents;
rp_begin.clearValueCount = 1;
rp_begin.pClearValues = &black;
vkCmdBeginRenderPass(frame.command_buffer, &rp_begin, VK_SUBPASS_CONTENTS_INLINE);
VkClearAttachment gray_clear;
gray_clear.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
gray_clear.colorAttachment = 0;
gray_clear.clearValue = gray;
VkClearAttachment colorful_clear = gray_clear;
colorful_clear.clearValue = colorful;
const uint32_t half_width = swapchain_extents.width / 2;
const uint32_t half_height = swapchain_extents.height / 2;
VkClearRect gray_rect;
gray_rect.rect.offset = {static_cast<int32_t>(half_width) / 2, static_cast<int32_t>(half_height)};
gray_rect.rect.extent = {std::max(half_width, 1u), std::max(half_height / 2, 1u)};
gray_rect.baseArrayLayer = 0;
gray_rect.layerCount = 1;
constexpr int32_t colorful_rect_x = 250;
constexpr int32_t colorful_rect_y = 150;
constexpr uint32_t colorful_rect_width = 300;
constexpr uint32_t colorful_rect_height = 350;
VkClearRect colorful_rect = gray_rect;
colorful_rect.rect.offset = {colorful_rect_x, colorful_rect_y};
colorful_rect.rect.extent = {colorful_rect_width, colorful_rect_height};
// Draw two rectangles via vkCmdClearAttachments. The gray rectangle scales with the
// window, but the colorful one has fixed size, and it's skipped if the window is too small.
vkCmdClearAttachments(frame.command_buffer, 1, &gray_clear, 1, &gray_rect);
if (colorful_rect_x + colorful_rect_width <= swapchain_extents.width &&
colorful_rect_y + colorful_rect_height <= swapchain_extents.height)
{
vkCmdClearAttachments(frame.command_buffer, 1, &colorful_clear, 1, &colorful_rect);
}
vkCmdEndRenderPass(frame.command_buffer);
VK_CHECK(vkEndCommandBuffer(frame.command_buffer));
// Make a submission. Wait on the acquire semaphore and signal the present semaphore.
VkPipelineStageFlags wait_stage{VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT};
VkSubmitInfo info{VK_STRUCTURE_TYPE_SUBMIT_INFO};
info.commandBufferCount = 1;
info.pCommandBuffers = &frame.command_buffer;
info.waitSemaphoreCount = 1;
info.pWaitSemaphores = &frame.acquire_semaphore;
info.pWaitDstStageMask = &wait_stage;
info.signalSemaphoreCount = 1;
info.pSignalSemaphores = &frame.present_semaphore;
VK_CHECK(vkQueueSubmit(queue->get_handle(), 1, &info, frame.submit_fence));
}
static VkResult ignore_suboptimal_due_to_rotation(VkResult result)
{
// Because preTransform is not respected in this sample, VK_SUBOPTIMAL_KHR is returned if
// the device is rotated. Handling preTransform optimally is out of scope for this sample,
// so VK_SUBOPTIMAL_KHR is ignored in that case.
//
// Note that on Android VK_SUBOPTIMAL_KHR is only returned when there is a mismatch between
// the device rotation and the specified preTransform.
#if defined(ANDROID)
if (result == VK_SUBOPTIMAL_KHR)
{
result = VK_SUCCESS;
}
#endif
return result;
}
/**
* @brief Acquires an image from the swapchain.
* @param[out] index The swapchain index for the acquired image.
* @returns Vulkan result code
*/
VkResult SwapchainRecreation::acquire_next_image(uint32_t *index)
{
PerFrame &frame = submit_history[submit_history_index];
// Use a fence to know when acquire is done. Without VK_EXT_swapchain_maintenance1, this
// fence is used to infer when the _previous_ present to this image index has finished.
// There is no need for this with VK_EXT_swapchain_maintenance1.
VkFence acquire_fence = has_maintenance1 ? VK_NULL_HANDLE : get_fence();
VkResult result = vkAcquireNextImageKHR(get_device_handle(), swapchain, UINT64_MAX, frame.acquire_semaphore, acquire_fence, index);
if (has_maintenance1 && (result == VK_SUCCESS || result == VK_SUBOPTIMAL_KHR))
{
// When VK_SWAPCHAIN_CREATE_DEFERRED_MEMORY_ALLOCATION_BIT_EXT is specified, image
// views must be created after the first time the image is acquired.
assert(*index < swapchain_objects.views.size());
if (swapchain_objects.views[*index] == VK_NULL_HANDLE)
{
init_swapchain_image(*index);
}
}
if (!has_maintenance1)
{
if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR)
{
// If failed, fence is untouched, recycle it.
//
// The semaphore is also untouched, but it may be used in the retry of
// vkAcquireNextImageKHR. It is nevertheless cleaned up after cpu
// throttling automatically.
recycle_fence(acquire_fence);
return result;
}
associate_fence_with_present_history(*index, acquire_fence);
}
return ignore_suboptimal_due_to_rotation(result);
}
/**
* @brief Presents an image to the swapchain.
* @param index The swapchain index previously obtained from @ref acquire_next_image.
* @returns Vulkan result code
*/
VkResult SwapchainRecreation::present_image(uint32_t index)
{
PerFrame &frame = submit_history[submit_history_index];
VkPresentInfoKHR present{VK_STRUCTURE_TYPE_PRESENT_INFO_KHR};
present.swapchainCount = 1;
present.pSwapchains = &swapchain;
present.pImageIndices = &index;
present.waitSemaphoreCount = 1;
present.pWaitSemaphores = &frame.present_semaphore;
// When VK_EXT_swapchain_maintenance1 is enabled, add a fence to the present operation,
// which is signaled when the resources associated with present operation can be freed.
VkFence present_fence = VK_NULL_HANDLE;
VkSwapchainPresentFenceInfoEXT fence_info{VK_STRUCTURE_TYPE_SWAPCHAIN_PRESENT_FENCE_INFO_EXT};
VkSwapchainPresentModeInfoEXT present_mode{VK_STRUCTURE_TYPE_SWAPCHAIN_PRESENT_MODE_INFO_EXT};
if (has_maintenance1)
{
present_fence = get_fence();
fence_info.swapchainCount = 1;
fence_info.pFences = &present_fence;
present.pNext = &fence_info;
// If present mode has changed but the two modes are compatible, change the present
// mode at present time.
if (current_present_mode != desired_present_mode)
{
// Can't reach here if the modes are not compatible.
assert(are_present_modes_compatible());
current_present_mode = desired_present_mode;
present_mode.swapchainCount = 1;
present_mode.pPresentModes = &current_present_mode;
fence_info.pNext = &present_mode;
}
}
VkResult result = vkQueuePresentKHR(queue->get_handle(), &present);
add_present_to_history(index, present_fence);
cleanup_present_history();
return ignore_suboptimal_due_to_rotation(result);
}
void SwapchainRecreation::add_present_to_history(uint32_t index, VkFence present_fence)
{
PerFrame &frame = submit_history[submit_history_index];
present_history.emplace_back();
present_history.back().present_semaphore = frame.present_semaphore;
present_history.back().old_swapchains = std::move(old_swapchains);
frame.present_semaphore = VK_NULL_HANDLE;
if (has_maintenance1)
{
present_history.back().image_index = INVALID_IMAGE_INDEX;
present_history.back().cleanup_fence = present_fence;
}
else
{
// The fence needed to know when the semaphore can be recycled will be one that is
// passed to vkAcquireNextImageKHR that returns the same image index. That is why
// the image index needs to be tracked in this case.
present_history.back().image_index = index;
}
}
void SwapchainRecreation::cleanup_present_history()
{
while (!present_history.empty())
{
PresentOperationInfo &present_info = present_history.front();
// If there is no fence associated with the history, it can't be cleaned up yet.
if (present_info.cleanup_fence == VK_NULL_HANDLE)
{
// Can't have an old present operation without a fence that doesn't have an
// image index used to later associate a fence with it.
assert(present_info.image_index != INVALID_IMAGE_INDEX);
break;
}
// Otherwise check to see if the fence is signaled.
VkResult result = vkGetFenceStatus(get_device_handle(), present_info.cleanup_fence);
if (result == VK_NOT_READY)
{
// Not yet
break;
}
VK_CHECK(result);
cleanup_present_info(present_info);
present_history.pop_front();
}
// The present history can grow indefinitely if a present operation is done on an index
// that's never acquired in the future. In that case, there's no fence associated with that
// present operation. Move the offending entry to last, so the resources associated with
// the rest of the present operations can be duly freed.
if (present_history.size() > swapchain_objects.images.size() * 2 && present_history.front().cleanup_fence == VK_NULL_HANDLE)
{
PresentOperationInfo present_info = std::move(present_history.front());
present_history.pop_front();
// We can't be stuck on a presentation to an old swapchain without a fence.
assert(present_info.image_index != INVALID_IMAGE_INDEX);
// Move clean up data to the next (now first) present operation, if any. Note that
// there cannot be any clean up data on the rest of the present operations, because
// the first present already gathers every old swapchain to clean up.
assert(std::ranges::all_of(present_history, [](const PresentOperationInfo &op) {
return op.old_swapchains.empty();
}));
present_history.front().old_swapchains = std::move(present_info.old_swapchains);
// Put the present operation at the end of the queue, so it's revisited after the
// rest of the present operations are cleaned up.
present_history.push_back(std::move(present_info));
}
}
void SwapchainRecreation::cleanup_present_info(PresentOperationInfo &present_info)
{
// Called when it's safe to destroy resources associated with a present operation.
if (present_info.cleanup_fence != VK_NULL_HANDLE)
{
recycle_fence(present_info.cleanup_fence);
}
// On the first acquire of the image, a fence is used but there is no present semaphore to
// clean up. That fence is placed in the present history just for clean up purposes.
if (present_info.present_semaphore != VK_NULL_HANDLE)
{
recycle_semaphore(present_info.present_semaphore);
}
// Destroy old swapchains
for (SwapchainCleanupData &old_swapchain : present_info.old_swapchains)
{
cleanup_old_swapchain(old_swapchain);
}
present_info = {};
}
void SwapchainRecreation::cleanup_old_swapchain(SwapchainCleanupData &old_swapchain)
{
if (old_swapchain.swapchain != VK_NULL_HANDLE)
{
vkDestroySwapchainKHR(get_device_handle(), old_swapchain.swapchain, nullptr);
}
for (VkSemaphore semaphore : old_swapchain.semaphores)
{
recycle_semaphore(semaphore);
}
old_swapchain = {};
}
void SwapchainRecreation::associate_fence_with_present_history(uint32_t index, VkFence acquire_fence)
{
// The history looks like this:
//
// <entries for old swapchains, imageIndex == UINT32_MAX> <entries for this swapchain>
//
// Walk the list backwards and find the entry for the given image index. That's the last
// present with that image. Associate the fence with that present operation.
for (size_t history_index = 0; history_index < present_history.size(); ++history_index)
{
PresentOperationInfo &present_info =
present_history[present_history.size() - history_index - 1];
if (present_info.image_index == INVALID_IMAGE_INDEX)
{
// No previous presentation with this index.
break;
}
if (present_info.image_index == index)
{
assert(present_info.cleanup_fence == VK_NULL_HANDLE);
present_info.cleanup_fence = acquire_fence;
return;
}
}
// If no previous presentation with this index, add an empty entry just so the fence can be
// cleaned up.
present_history.emplace_back();
present_history.back().cleanup_fence = acquire_fence;
present_history.back().image_index = index;
}
void SwapchainRecreation::schedule_old_swapchain_for_destruction(VkSwapchainKHR old_swapchain)
{
// If no presentation is done on the swapchain, destroy it right away.
if (!present_history.empty() && present_history.back().image_index == INVALID_IMAGE_INDEX)
{
vkDestroySwapchainKHR(get_device_handle(), old_swapchain, nullptr);
return;
}
SwapchainCleanupData cleanup;
cleanup.swapchain = old_swapchain;
// Place any present operation that's not associated with a fence into old_swapchains. That
// gets scheduled for destruction when the semaphore of the first image of the next
// swapchain can be recycled.
std::vector<PresentOperationInfo> history_to_keep;
while (!present_history.empty())
{
PresentOperationInfo &present_info = present_history.back();
// If this is about an older swapchain, let it be.
if (present_info.image_index == INVALID_IMAGE_INDEX)
{
assert(present_info.cleanup_fence != VK_NULL_HANDLE);
break;
}
// Reset the index, so it's not processed in the future.
present_info.image_index = INVALID_IMAGE_INDEX;
if (present_info.cleanup_fence != VK_NULL_HANDLE)
{
// If there is already a fence associated with it, let it be cleaned up once
// the fence is signaled.
history_to_keep.push_back(std::move(present_info));
}
else
{
assert(present_info.present_semaphore != VK_NULL_HANDLE);
// Otherwise accumulate it in cleanup data.
cleanup.semaphores.push_back(present_info.present_semaphore);
// Accumulate any previous swapchains that are pending destruction too.
for (SwapchainCleanupData &swapchain : present_info.old_swapchains)
{
old_swapchains.emplace_back(swapchain);
}
present_info.old_swapchains.clear();
}
present_history.pop_back();
}
std::move(history_to_keep.begin(), history_to_keep.end(), std::back_inserter(present_history));
if (cleanup.swapchain != VK_NULL_HANDLE || !cleanup.semaphores.empty())
{
old_swapchains.emplace_back(std::move(cleanup));
}
}
VkSemaphore SwapchainRecreation::get_semaphore()
{
// If there is a free semaphore, return it
if (!semaphore_pool.empty())
{
VkSemaphore semaphore = semaphore_pool.back();
semaphore_pool.pop_back();
return semaphore;
}
VkSemaphore semaphore = VK_NULL_HANDLE;
VkSemaphoreCreateInfo create_info{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO};
VK_CHECK(vkCreateSemaphore(get_device_handle(), &create_info, nullptr, &semaphore));
return semaphore;
}
void SwapchainRecreation::recycle_semaphore(VkSemaphore semaphore)
{
semaphore_pool.push_back(semaphore);
}
VkFence SwapchainRecreation::get_fence()
{
// If there is a free fence, return it
if (!fence_pool.empty())
{
VkFence fence = fence_pool.back();
fence_pool.pop_back();
return fence;
}
VkFence fence = VK_NULL_HANDLE;
VkFenceCreateInfo create_info{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO};
VK_CHECK(vkCreateFence(get_device_handle(), &create_info, nullptr, &fence));
return fence;
}
void SwapchainRecreation::recycle_fence(VkFence fence)
{
fence_pool.push_back(fence);
VK_CHECK(vkResetFences(get_device_handle(), 1, &fence));
}
VkPhysicalDevice SwapchainRecreation::get_gpu_handle()
{
return get_device().get_gpu().get_handle();
}
VkDevice SwapchainRecreation::get_device_handle()
{
if (!has_device())
{
return VK_NULL_HANDLE;
}
return get_device().get_handle();
}
SwapchainRecreation::SwapchainRecreation()
{
const char *use_maintenance1 = std::getenv("USE_MAINTENANCE1");
if ((use_maintenance1 == nullptr) || (strcmp(use_maintenance1, "no") != 0))
{
// Request sample-specific extensions as optional
add_instance_extension(VK_KHR_GET_SURFACE_CAPABILITIES_2_EXTENSION_NAME, true);
add_instance_extension(VK_EXT_SURFACE_MAINTENANCE_1_EXTENSION_NAME, true);
add_device_extension(VK_EXT_SWAPCHAIN_MAINTENANCE_1_EXTENSION_NAME, true);
}
else
{
LOGI("Disabling usage of VK_EXT_surface_maintenance1 due to USE_MAINTENANCE1=no");
allow_maintenance1 = false;
}
}
SwapchainRecreation::~SwapchainRecreation()
{
if (get_device_handle() == VK_NULL_HANDLE)
{
// No device, VK_EXT_swapchain_maintenance1 may not be available. Resources will not be created.
return;
}
// Wait for device to be idle and clean up everything.
vkDeviceWaitIdle(get_device_handle());
for (PerFrame &frame : submit_history)
{
recycle_fence(frame.submit_fence);
recycle_semaphore(frame.acquire_semaphore);
vkDestroyCommandPool(get_device_handle(), frame.command_pool, nullptr);
for (SwapchainObjects &garbage : frame.swapchain_garbage)
{
cleanup_swapchain_objects(garbage);
}
frame.swapchain_garbage.clear();
assert(frame.present_semaphore == VK_NULL_HANDLE);
}
for (PresentOperationInfo &present_info : present_history)
{
if (present_info.cleanup_fence != VK_NULL_HANDLE)
{
vkWaitForFences(get_device_handle(), 1, &present_info.cleanup_fence, true, UINT64_MAX);
}
cleanup_present_info(present_info);
}
LOGI("During the lifetime of this sample, {} swapchains were created", swapchain_creation_count);
LOGI("Old swapchain count at destruction: {}", old_swapchains.size());
for (SwapchainCleanupData &old_swapchain : old_swapchains)
{
cleanup_old_swapchain(old_swapchain);
}
LOGI("Semaphore pool size at destruction: {}", semaphore_pool.size());
LOGI("Fence pool size at destruction: {}", fence_pool.size());
for (VkSemaphore semaphore : semaphore_pool)
{
vkDestroySemaphore(get_device_handle(), semaphore, nullptr);
}
for (VkFence fence : fence_pool)
{
vkDestroyFence(get_device_handle(), fence, nullptr);
}
cleanup_swapchain_objects(swapchain_objects);
if (swapchain != VK_NULL_HANDLE)
{
vkDestroySwapchainKHR(get_device_handle(), swapchain, nullptr);
}
if (render_pass != VK_NULL_HANDLE)
{
vkDestroyRenderPass(get_device_handle(), render_pass, nullptr);
}
}
void SwapchainRecreation::request_gpu_features(vkb::PhysicalDevice &gpu)
{
if (allow_maintenance1)
{
REQUEST_OPTIONAL_FEATURE(gpu,
VkPhysicalDeviceSwapchainMaintenance1FeaturesEXT,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SWAPCHAIN_MAINTENANCE_1_FEATURES_EXT,
swapchainMaintenance1);
}
}
std::unique_ptr<vkb::core::DeviceC> SwapchainRecreation::create_device(vkb::PhysicalDevice &gpu)
{
std::unique_ptr<vkb::core::DeviceC> device = vkb::VulkanSampleC::create_device(gpu);
has_maintenance1 = get_instance().is_enabled(VK_KHR_GET_SURFACE_CAPABILITIES_2_EXTENSION_NAME) &&
get_instance().is_enabled(VK_EXT_SURFACE_MAINTENANCE_1_EXTENSION_NAME) &&
device->is_extension_enabled(VK_EXT_SWAPCHAIN_MAINTENANCE_1_EXTENSION_NAME);
LOGI("------------------------------------");
LOGI("USAGE:");
LOGI(" - Press v to enable v-sync (default)");
LOGI(" - Press n to disable v-sync");
LOGI(" - Press p to enable switching between compatible present modes (default)");
LOGI(" - Press r to disable switching between compatible present modes");
if (has_maintenance1)
{
LOGI("Set environment variable USE_MAINTENANCE1=no to avoid VK_EXT_surface_maintenance1");
}
LOGI("------------------------------------");
return device;
}
void SwapchainRecreation::create_render_context()
{
get_queue();
query_surface_format();
create_render_pass();
init_swapchain();
}
void SwapchainRecreation::prepare_render_context()
{
// Nothing to do
}
void SwapchainRecreation::update(float delta_time)
{
fps_timer += delta_time;
if (fps_timer > 1.0f)
{
LOGI("FPS: {}", static_cast<float>(frame_number - fps_last_logged_frame_number) / fps_timer);
fps_timer -= 1.0f;
fps_last_logged_frame_number = frame_number;
}
++frame_number;
setup_frame();
if (!are_present_modes_compatible())
{
recreate_swapchain();
}
uint32_t index;
auto res = acquire_next_image(&index);
// Handle outdated error in acquire.
if (res == VK_SUBOPTIMAL_KHR || res == VK_ERROR_OUT_OF_DATE_KHR)
{
recreate_swapchain();
res = acquire_next_image(&index);
}
if (res != VK_SUBOPTIMAL_KHR)
{
VK_CHECK(res);
}
render(index);
res = present_image(index);
// Handle Outdated error in present.
if (res == VK_SUBOPTIMAL_KHR || res == VK_ERROR_OUT_OF_DATE_KHR)
{
recreate_swapchain();
}
else
{
VK_CHECK(res);
}
}
bool SwapchainRecreation::resize(const uint32_t, const uint32_t)
{
if (get_device_handle() == VK_NULL_HANDLE)
{
return false;
}
return recreate_swapchain();
}
void SwapchainRecreation::input_event(const vkb::InputEvent &input_event)
{
if (input_event.get_source() != vkb::EventSource::Keyboard)
{
return;
}
const auto &key_button = static_cast<const vkb::KeyInputEvent &>(input_event);
if (key_button.get_action() != vkb::KeyAction::Up)
{
return;
}
switch (key_button.get_code())
{
case vkb::KeyCode::V:
// Note: events are being double-sent, avoid double logging with this check
// as a workaround.
if (current_present_mode != VK_PRESENT_MODE_FIFO_KHR)
{
LOGI("Enabling V-Sync");
desired_present_mode = VK_PRESENT_MODE_FIFO_KHR;
}
break;
case vkb::KeyCode::N:
if (current_present_mode == VK_PRESENT_MODE_FIFO_KHR)
{
LOGI("Disabling V-Sync");
desired_present_mode = VK_PRESENT_MODE_MAILBOX_KHR;
}
break;
case vkb::KeyCode::P:
if (recreate_swapchain_on_present_mode_change)
{
LOGI("Switch between compatible present modes: Enabled");
recreate_swapchain_on_present_mode_change = false;
compatible_modes.clear();
}
break;
case vkb::KeyCode::R:
if (!recreate_swapchain_on_present_mode_change)
{
LOGI("Switch between compatible present modes: Disabled");
recreate_swapchain_on_present_mode_change = true;
compatible_modes.clear();
}
break;
default:
break;
}
query_present_modes();
}
std::unique_ptr<vkb::Application> create_swapchain_recreation()
{
return std::make_unique<SwapchainRecreation>();
}