diff --git a/samples/api/swapchain_recreation/swapchain_recreation.cpp b/samples/api/swapchain_recreation/swapchain_recreation.cpp index 45800c0c2..98f040196 100644 --- a/samples/api/swapchain_recreation/swapchain_recreation.cpp +++ b/samples/api/swapchain_recreation/swapchain_recreation.cpp @@ -64,31 +64,8 @@ void SwapchainRecreation::query_compatible_present_modes(VkPresentModeKHR presen { // 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)); + compatible_modes.resize(1); + compatible_modes[0] = present_mode; } void SwapchainRecreation::adjust_desired_present_mode() @@ -236,22 +213,6 @@ void SwapchainRecreation::init_swapchain() 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)); @@ -277,14 +238,11 @@ void SwapchainRecreation::init_swapchain() 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) { - // 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); - } + init_swapchain_image(index); } } @@ -529,36 +487,22 @@ VkResult SwapchainRecreation::acquire_next_image(uint32_t *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(); + VkFence acquire_fence = 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)) + if (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 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; } - 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); - } + associate_fence_with_present_history(*index, acquire_fence); return ignore_suboptimal_due_to_rotation(result); } @@ -584,30 +528,6 @@ VkResult SwapchainRecreation::present_image(uint32_t index) 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); @@ -627,18 +547,10 @@ void SwapchainRecreation::add_present_to_history(uint32_t index, VkFence present 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; - } + // 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() @@ -892,19 +804,6 @@ VkDevice SwapchainRecreation::get_device_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"); - } } SwapchainRecreation::~SwapchainRecreation() @@ -977,20 +876,12 @@ std::unique_ptr SwapchainRecreation::create_device(vkb::PhysicalDev { 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_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; diff --git a/samples/api/swapchain_recreation/swapchain_recreation.h b/samples/api/swapchain_recreation/swapchain_recreation.h index 3adb2ac5d..a5e4ff9e6 100644 --- a/samples/api/swapchain_recreation/swapchain_recreation.h +++ b/samples/api/swapchain_recreation/swapchain_recreation.h @@ -1,4 +1,4 @@ -/* Copyright (c) 2023-2024, Google +/* Copyright (c) 2023-2025, Google * * SPDX-License-Identifier: Apache-2.0 * @@ -112,10 +112,6 @@ class SwapchainRecreation : public vkb::VulkanSampleC /// Submission and present queue. const vkb::Queue *queue = nullptr; - /// Whether the VK_EXT_surface_maintenance1 and VK_EXT_swapchain_maintenance1 extensions are - /// to be used. - bool has_maintenance1 = false; - /// Surface data. VkSurfaceFormatKHR surface_format = {}; std::vector present_modes = {}; @@ -168,6 +164,7 @@ class SwapchainRecreation : public vkb::VulkanSampleC // User toggles. bool recreate_swapchain_on_present_mode_change = false; + // from vkb::VulkanSample std::unique_ptr create_device(vkb::PhysicalDevice &gpu) override; void get_queue(); diff --git a/samples/api/swapchain_recreation_maintenance1/CMakeLists.txt b/samples/api/swapchain_recreation_maintenance1/CMakeLists.txt new file mode 100644 index 000000000..f706f4cdb --- /dev/null +++ b/samples/api/swapchain_recreation_maintenance1/CMakeLists.txt @@ -0,0 +1,27 @@ +# Copyright (c) 2023, 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. +# + +get_filename_component(FOLDER_NAME ${CMAKE_CURRENT_LIST_DIR} NAME) +get_filename_component(PARENT_DIR ${CMAKE_CURRENT_LIST_DIR} PATH) +get_filename_component(CATEGORY_NAME ${PARENT_DIR} NAME) + +add_sample( + ID ${FOLDER_NAME} + CATEGORY ${CATEGORY_NAME} + AUTHOR "Google" + NAME "Swapchain Recreation - maintenance 1" + DESCRIPTION "Best practices when dealing with Vulkan swapchain recreation (using VK_EXT_swapchain_maintenance1).") diff --git a/samples/api/swapchain_recreation_maintenance1/README.adoc b/samples/api/swapchain_recreation_maintenance1/README.adoc new file mode 100644 index 000000000..3ab78c0c3 --- /dev/null +++ b/samples/api/swapchain_recreation_maintenance1/README.adoc @@ -0,0 +1,92 @@ +//// +- Copyright (c) 2023-2024, The Khronos Group +- +- 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. +- +//// + += Swapchain Recreation + +ifdef::site-gen-antora[] +TIP: The source for this sample can be found in the https://github.com/KhronosGroup/Vulkan-Samples/tree/main/samples/api/swapchain_recreation[Khronos Vulkan samples github repository]. +endif::[] + + +A sample that implements best practices in handling present resources and swapchain recreation, for example due to window resizing or present mode changes. + +Before VK_EXT_swapchain_maintenance1, there is no straightforward way to tell when a semaphore associated with a present operation can be recycled, or when a retired swapchain can be destroyed. +Both these operations depend on knowing when the presentation engine has acquired a reference to these resources as part of the present job, for which there is no indicator. + +In this sample, a workaround is implemented where a fence signaled by vkAcquireNextImageKHR is used to determine when the _previous_ present job involving the same image index has been completed. +This is often much later than the point where the present resources can be freed. + +Take the following shorthand notation: + +* PE: Presentation Engine +* ANI: vkAcquireNextImageKHR +* QS: vkQueueSubmit +* QP: vkQueuePresentKHR +* W: Wait +* S: Signal +* R: Render +* P: Present +* SN: Semaphore N +* IN: Swapchain image N +* FN: Fence N + +Assuming both ANI calls below return the same index: + + CPU: ANI ... QS ... QP ANI ... QS ... QP + S:S1 W:S1 W:S2 S:S3 W:S3 W:S4 + S:F1 S:S2 S:F2 S:S4 + GPU: <------ R ------> <------ R ------> + PE: <-- P --> <-- P --> + +The following holds: + + F2 is signaled + => The PE has handed the image to the application + => The PE is no longer presenting the image (the first P operation is finished) + => The PE is done waiting on S2 + +At this point, we can destroy or recycle S2. +To implement this, a history of present operations is maintained, which includes the wait semaphore used with that presentation. +Associated with each present operation, is a fence that is used to determine when that semaphore can be destroyed. + +Since the fence is not actually known at present time (QP), the present operation is kept in history without an associated fence. +Once ANI returns the same index, the fence given to ANI is associated with the previous QP of that index. + +After each present call, the present history is inspected. +Any present operation whose fence is signaled is cleaned up. + +== Swapchain recreation + +When recreating the swapchain, all images are eventually freed and new ones are created, possibly with a different count and present mode. +For the old swapchain, we can no longer rely on a future ANI to know when a previous presentation's semaphore can be destroyed, as there won't be any more acquisitions from the old swapchain. +Similarly, we cannot know when the old swapchain itself can be destroyed. + +This issue is resolved by deferring the destruction of the old swapchain and its remaining present semaphores to the time when the semaphore corresponding to the first present of the new swapchain can be destroyed. +Because once the first present semaphore of the new swapchain can be destroyed, the first present operation of the new swapchain is done, which means the old swapchain is no longer being presented. + +Note that the swapchain may be recreated without a second acquire. +This means that the swapchain could be recreated while there are pending old swapchains to be destroyed. +The destruction of both old swapchains must now be deferred to when the first QP of the new swapchain has been processed. +If an application resizes the window constantly and at a high rate, we would keep accumulating old swapchains and not free them until it stops. + +== VK_EXT_swapchain_maintenance1 + +With the VK_EXT_swapchain_maintenance1, all the above is unnecessary. +Each QP operation can have an associated fence, which can be used to know when the semaphore associated with it can be recycled. +The old swapchains can be destroyed at the same time as before. diff --git a/samples/api/swapchain_recreation_maintenance1/swapchain_recreation_maintenance1.cpp b/samples/api/swapchain_recreation_maintenance1/swapchain_recreation_maintenance1.cpp new file mode 100644 index 000000000..35a5ccdfd --- /dev/null +++ b/samples/api/swapchain_recreation_maintenance1/swapchain_recreation_maintenance1.cpp @@ -0,0 +1,1073 @@ +/* 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_maintenance1.h" + +#include "common/vk_common.h" +#include "core/util/logging.hpp" +#include "filesystem/legacy.h" +#include "glsl_compiler.h" + +static constexpr uint32_t INVALID_IMAGE_INDEX = std::numeric_limits::max(); + +void SwapchainRecreationMaintenance1::get_queue() +{ + queue = &get_device().get_suitable_graphics_queue(); + + // 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 SwapchainRecreationMaintenance1::query_surface_format() +{ + surface_format = vkb::select_surface_format(get_gpu_handle(), get_surface()); +} + +void SwapchainRecreationMaintenance1::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 SwapchainRecreationMaintenance1::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 (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 SwapchainRecreationMaintenance1::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 SwapchainRecreationMaintenance1::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 SwapchainRecreationMaintenance1::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 SwapchainRecreationMaintenance1::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}; + // 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())); +} + +/** + * @brief Called to initialize resources for a swapchain image. + */ +void SwapchainRecreationMaintenance1::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 SwapchainRecreationMaintenance1::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 SwapchainRecreationMaintenance1::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 SwapchainRecreationMaintenance1::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 SwapchainRecreationMaintenance1::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 SwapchainRecreationMaintenance1::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. + + VkResult result = vkAcquireNextImageKHR(get_device_handle(), swapchain, UINT64_MAX, frame.acquire_semaphore, VK_NULL_HANDLE, index); + + if (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); + } + } + + 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 SwapchainRecreationMaintenance1::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}; + 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 SwapchainRecreationMaintenance1::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; + + present_history.back().image_index = INVALID_IMAGE_INDEX; + present_history.back().cleanup_fence = present_fence; +} + +void SwapchainRecreationMaintenance1::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 SwapchainRecreationMaintenance1::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 SwapchainRecreationMaintenance1::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 SwapchainRecreationMaintenance1::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 SwapchainRecreationMaintenance1::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 SwapchainRecreationMaintenance1::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 SwapchainRecreationMaintenance1::recycle_semaphore(VkSemaphore semaphore) +{ + semaphore_pool.push_back(semaphore); +} + +VkFence SwapchainRecreationMaintenance1::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 SwapchainRecreationMaintenance1::recycle_fence(VkFence fence) +{ + fence_pool.push_back(fence); + + VK_CHECK(vkResetFences(get_device_handle(), 1, &fence)); +} + +VkPhysicalDevice SwapchainRecreationMaintenance1::get_gpu_handle() +{ + return get_device().get_gpu().get_handle(); +} + +VkDevice SwapchainRecreationMaintenance1::get_device_handle() +{ + if (!has_device()) + { + return VK_NULL_HANDLE; + } + return get_device().get_handle(); +} + +SwapchainRecreationMaintenance1::SwapchainRecreationMaintenance1() +{ + add_instance_extension(VK_KHR_GET_SURFACE_CAPABILITIES_2_EXTENSION_NAME, false); + add_instance_extension(VK_EXT_SURFACE_MAINTENANCE_1_EXTENSION_NAME, false); + add_device_extension(VK_EXT_SWAPCHAIN_MAINTENANCE_1_EXTENSION_NAME, false); +} + +SwapchainRecreationMaintenance1::~SwapchainRecreationMaintenance1() +{ + 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 SwapchainRecreationMaintenance1::request_gpu_features(vkb::PhysicalDevice &gpu) +{ + REQUEST_REQUIRED_FEATURE(gpu, + VkPhysicalDeviceSwapchainMaintenance1FeaturesEXT, + VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SWAPCHAIN_MAINTENANCE_1_FEATURES_EXT, + swapchainMaintenance1); +} + +std::unique_ptr SwapchainRecreationMaintenance1::create_device(vkb::PhysicalDevice &gpu) +{ + std::unique_ptr device = vkb::VulkanSampleC::create_device(gpu); + + 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"); + LOGI("------------------------------------"); + + return device; +} + +void SwapchainRecreationMaintenance1::create_render_context() +{ + get_queue(); + query_surface_format(); + create_render_pass(); + init_swapchain(); +} + +void SwapchainRecreationMaintenance1::prepare_render_context() +{ + // Nothing to do +} + +void SwapchainRecreationMaintenance1::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 SwapchainRecreationMaintenance1::resize(const uint32_t, const uint32_t) +{ + if (get_device_handle() == VK_NULL_HANDLE) + { + return false; + } + + return recreate_swapchain(); +} + +void SwapchainRecreationMaintenance1::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_maintenance1() +{ + return std::make_unique(); +} diff --git a/samples/api/swapchain_recreation_maintenance1/swapchain_recreation_maintenance1.h b/samples/api/swapchain_recreation_maintenance1/swapchain_recreation_maintenance1.h new file mode 100644 index 000000000..c00de6cb0 --- /dev/null +++ b/samples/api/swapchain_recreation_maintenance1/swapchain_recreation_maintenance1.h @@ -0,0 +1,208 @@ +/* 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. + */ + +#pragma once + +#include "common/vk_common.h" +#include "platform/application.h" +#include "vulkan_sample.h" + +/** + * @brief A sample that implements best practices in handling present resources and swapchain + * recreation, for example due to window resizing or present mode changes. + */ +class SwapchainRecreationMaintenance1 : public vkb::VulkanSampleC +{ + struct SwapchainObjects + { + std::vector images; + std::vector views; + std::vector framebuffers; + }; + + /** + * @brief Per-frame data. This is not per swapchain image! + * A queue of this data structure is used to remember the history of submissions. To avoid + * the CPU getting too far ahead of the GPU, the sample paces itself by waiting for the + * submission before last to finish before recording commands for the new frame. This means + * that frame N+1 doesn't start recording until frame N-1 finishes executing on the GPU (and + * likely frame N starts). In a real application, this minimizes latency from input to + * screen. + */ + struct PerFrame + { + VkFence submit_fence = VK_NULL_HANDLE; + VkCommandPool command_pool = VK_NULL_HANDLE; + VkCommandBuffer command_buffer = VK_NULL_HANDLE; + VkSemaphore acquire_semaphore = VK_NULL_HANDLE; + VkSemaphore present_semaphore = VK_NULL_HANDLE; + + // Garbage to clean up once the submit_fence is signaled, if any. + std::vector swapchain_garbage; + }; + + struct SwapchainCleanupData + { + /// The old swapchain to be destroyed. + VkSwapchainKHR swapchain = VK_NULL_HANDLE; + + /** + * @brief Any present semaphores that were pending recycle at the time the swapchain + * was recreated will be scheduled for recycling at the same time as the swapchain's + * destruction. + */ + std::vector semaphores; + }; + + struct PresentOperationInfo + { + /** + * @brief Fence that tells when the present semaphore can be destroyed. Without + * VK_EXT_swapchain_maintenance1, the fence used with the vkAcquireNextImageKHR that + * returns the same image index in the future is used to know when the semaphore can + * be recycled. + */ + VkFence cleanup_fence = VK_NULL_HANDLE; + VkSemaphore present_semaphore = VK_NULL_HANDLE; + + /** + * @brief Old swapchains are scheduled to be destroyed at the same time as the last + * wait semaphore used to present an image to the old swapchains can be recycled. + */ + std::vector old_swapchains; + + /** + * @brief Used to associate an acquire fence with the previous present operation of + * the image. Only relevant when VK_EXT_swapchain_maintenance1 is not supported; + * otherwise a fence is always associated with the present operation. + */ + uint32_t image_index = std::numeric_limits::max(); + }; + + public: + SwapchainRecreationMaintenance1(); + + virtual ~SwapchainRecreationMaintenance1() override; + + void create_render_context() override; + + void prepare_render_context() override; + + void update(float delta_time) override; + + bool resize(uint32_t width, uint32_t height) override; + + void input_event(const vkb::InputEvent &input_event) override; + + private: + /// Submission and present queue. + const vkb::Queue *queue = nullptr; + + /// Surface data. + VkSurfaceFormatKHR surface_format = {}; + std::vector present_modes = {}; + std::vector compatible_modes = {}; + VkExtent2D swapchain_extents = {}; + + /// The swapchain. + VkSwapchainKHR swapchain = VK_NULL_HANDLE; + + /// Swapchain data. + VkPresentModeKHR current_present_mode = VK_PRESENT_MODE_FIFO_KHR; + VkPresentModeKHR desired_present_mode = VK_PRESENT_MODE_FIFO_KHR; + SwapchainObjects swapchain_objects; + + /// The render pass used for rendering. + VkRenderPass render_pass = VK_NULL_HANDLE; + + /// The submission history. This is a fixed-size queue, implemented as a circular buffer. + std::array submit_history = {}; + size_t submit_history_index = 0; + + /// The present operation history. This is used to clean up present semaphores and old swapchains. + std::deque present_history; + + /** + * @brief The previous swapchain which needs to be scheduled for destruction when + * appropriate. This will be done when the first image of the current swapchain is + * presented. If there were older swapchains pending destruction when the swapchain is + * recreated, they will accumulate and be destroyed with the previous swapchain. + * + * Note that if the user resizes the window such that the swapchain is recreated every + * frame, this array can go grow indefinitely. + */ + std::vector old_swapchains; + + /// Resource pools. + std::vector semaphore_pool; + std::vector fence_pool; + + /// Time. + uint32_t frame_number = 0; + + // FPS log. + float fps_timer = 0; + uint32_t fps_last_logged_frame_number = 0; + + // Other statistics + uint32_t swapchain_creation_count = 0; + + // User toggles. + bool recreate_swapchain_on_present_mode_change = false; + + // from vkb::VulkanSample + void request_gpu_features(vkb::PhysicalDevice &gpu) override; + std::unique_ptr create_device(vkb::PhysicalDevice &gpu) override; + + void get_queue(); + void query_surface_format(); + void query_present_modes(); + void query_compatible_present_modes(VkPresentModeKHR present_mode); + void adjust_desired_present_mode(); + void create_render_pass(); + + bool are_present_modes_compatible(); + + void init_swapchain(); + void init_swapchain_image(uint32_t index); + void cleanup_swapchain_objects(SwapchainObjects &garbage); + bool recreate_swapchain(); + + void setup_frame(); + void render(uint32_t index); + + VkResult acquire_next_image(uint32_t *index); + VkResult present_image(uint32_t index); + void add_present_to_history(uint32_t index, VkFence present_fence); + void cleanup_present_history(); + void cleanup_present_info(PresentOperationInfo &present_info); + void cleanup_old_swapchain(SwapchainCleanupData &old_swapchain); + + void associate_fence_with_present_history(uint32_t index, VkFence acquire_fence); + void schedule_old_swapchain_for_destruction(VkSwapchainKHR old_swapchain); + + VkSemaphore get_semaphore(); + void recycle_semaphore(VkSemaphore semaphore); + + VkFence get_fence(); + void recycle_fence(VkFence fence); + + VkPhysicalDevice get_gpu_handle(); + VkDevice get_device_handle(); +}; + +std::unique_ptr create_swapchain_recreation_maintenance1();