Skip to content

WIP: Experiment: Split swapchain recreation #1329

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 20 additions & 129 deletions samples/api/swapchain_recreation/swapchain_recreation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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<uint32_t>(compatible_modes.size());
compatible_modes_info.pPresentModes = compatible_modes.data();
info.pNext = &compatible_modes_info;
}
}

LOGI("Creating new swapchain");
VK_CHECK(vkCreateSwapchainKHR(get_device_handle(), &info, nullptr, &swapchain));
Expand All @@ -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);
}
}

Expand Down Expand Up @@ -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);
}
Expand All @@ -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 = &current_present_mode;

fence_info.pNext = &present_mode;
}
}

VkResult result = vkQueuePresentKHR(queue->get_handle(), &present);

Expand All @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -977,20 +876,12 @@ std::unique_ptr<vkb::Device> SwapchainRecreation::create_device(vkb::PhysicalDev
{
std::unique_ptr<vkb::Device> 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;
Expand Down
7 changes: 2 additions & 5 deletions samples/api/swapchain_recreation/swapchain_recreation.h
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* Copyright (c) 2023-2024, Google
/* Copyright (c) 2023-2025, Google
*
* SPDX-License-Identifier: Apache-2.0
*
Expand Down Expand Up @@ -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<VkPresentModeKHR> present_modes = {};
Expand Down Expand Up @@ -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<vkb::Device> create_device(vkb::PhysicalDevice &gpu) override;

void get_queue();
Expand Down
27 changes: 27 additions & 0 deletions samples/api/swapchain_recreation_maintenance1/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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).")
92 changes: 92 additions & 0 deletions samples/api/swapchain_recreation_maintenance1/README.adoc
Original file line number Diff line number Diff line change
@@ -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.
Loading
Loading