/* 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::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(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(frame_number % 256) / 255.0f, static_cast((frame_number + 63) % 256) / 255.0f, static_cast((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(half_width) / 2, static_cast(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 = ¤t_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: // // // // 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 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 SwapchainRecreation::create_device(vkb::PhysicalDevice &gpu) { std::unique_ptr 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(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(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 create_swapchain_recreation() { return std::make_unique(); }