Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
549a831
Prepare sample base class for new sync
SaschaWillems Apr 23, 2026
97701fe
Prepare sample base class for new sync
SaschaWillems Apr 23, 2026
3e1a307
Prepare sample base class for new sync
SaschaWillems Apr 23, 2026
df82914
Update first sample to use new (proper) sync
SaschaWillems Apr 23, 2026
bed7aa0
Prepare sample base classes for new sync
SaschaWillems Apr 24, 2026
646d5c3
Update first sample to use new (proper) sync
SaschaWillems Apr 24, 2026
f4cdc97
Format and copyright
SaschaWillems Apr 24, 2026
0d30069
Prepare GUI overlay for new sync
SaschaWillems Apr 25, 2026
06f9104
Comments
SaschaWillems Apr 25, 2026
40a9b31
Update samples to use new sync
SaschaWillems Apr 26, 2026
33c9a64
Update samples to use new sync
SaschaWillems Apr 26, 2026
b608000
Clang format
SaschaWillems Apr 26, 2026
62b682f
Framework adjustments for new sync
SaschaWillems Apr 30, 2026
a445d42
Document new parameter
SaschaWillems Apr 30, 2026
fa4c2aa
Temp fix for GUI issues with dynamic rendering samples
SaschaWillems May 2, 2026
3173529
Update samples to use new sync
SaschaWillems May 2, 2026
0a458d8
Clang format
SaschaWillems May 2, 2026
166d701
Trying to fix Clang format
SaschaWillems May 2, 2026
d5d5e49
Update samples to use new sync
SaschaWillems May 2, 2026
b86eaa8
Simplify render/draw functions
SaschaWillems May 2, 2026
3102e02
Update samples to use new sync
SaschaWillems May 2, 2026
6374dca
Update samples to use new sync
SaschaWillems May 3, 2026
b3e2987
Update samples to use new sync
SaschaWillems May 3, 2026
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
159 changes: 131 additions & 28 deletions framework/api_vulkan_sample.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ bool ApiVulkanSample::prepare(const vkb::ApplicationOptions &options)

void ApiVulkanSample::prepare_gui()
{
create_gui(*window, nullptr, 15.0f, true);
create_gui(*window, nullptr, 15.0f, true, use_new_sync);

std::vector<VkPipelineShaderStageCreateInfo> shader_stages = {
load_shader("uioverlay/uioverlay.vert.spv", VK_SHADER_STAGE_VERTEX_BIT),
Expand Down Expand Up @@ -408,8 +408,15 @@ bool ApiVulkanSample::check_command_buffers()

void ApiVulkanSample::create_command_buffers()
{
// Create one command buffer for each swap chain image and reuse for rendering
draw_cmd_buffers.resize(get_render_context().get_render_frames().size());
if (use_new_sync)
{
draw_cmd_buffers.resize(max_concurrent_frames);
}
else
{
// Create one command buffer for each swap chain image and reuse for rendering
draw_cmd_buffers.resize(get_render_context().get_render_frames().size());
}

VkCommandBufferAllocateInfo allocate_info =
vkb::initializers::command_buffer_allocate_info(
Expand Down Expand Up @@ -517,33 +524,69 @@ void ApiVulkanSample::draw_ui(const VkCommandBuffer command_buffer, uint32_t swa
vkCmdSetViewport(command_buffer, 0, 1, &viewport);
vkCmdSetScissor(command_buffer, 0, 1, &scissor);

get_gui().draw(command_buffer, swapchain_buffers[swapchain_image_index].view, width, height);
get_gui().draw(command_buffer, swapchain_buffers[swapchain_image_index].view, width, height, current_buffer);
}
}

void ApiVulkanSample::prepare_frame()
{
if (get_render_context().has_swapchain())
{
handle_surface_changes();
// Acquire the next image from the swap chain
VkResult result = get_render_context().get_swapchain().acquire_next_image(current_buffer, semaphores.acquired_image_ready, VK_NULL_HANDLE);
// Recreate the swapchain if it's no longer compatible with the surface (OUT_OF_DATE)
if (result == VK_ERROR_OUT_OF_DATE_KHR)
if (use_new_sync)
{
resize(width, height);
// Ensure command buffer execution has finished
VK_CHECK(vkWaitForFences(get_device().get_handle(), 1, &wait_fences[current_buffer], VK_TRUE, UINT64_MAX));
VK_CHECK(vkResetFences(get_device().get_handle(), 1, &wait_fences[current_buffer]));
handle_surface_changes();
// Acquire the next image from the swap chain
VkResult result = get_render_context().get_swapchain().acquire_next_image(current_image_index, acquired_image_ready_semaphores[current_buffer], VK_NULL_HANDLE);
// Recreate the swapchain if it's no longer compatible with the surface (OUT_OF_DATE)
if (result == VK_ERROR_OUT_OF_DATE_KHR)
{
resize(width, height);
}
// VK_SUBOPTIMAL_KHR means that acquire was successful and semaphore is signaled but image is suboptimal
// allow rendering frame to suboptimal swapchain as otherwise we would have to manually unsignal semaphore and acquire image again
else if (result != VK_SUBOPTIMAL_KHR)
{
VK_CHECK(result);
}
}
// VK_SUBOPTIMAL_KHR means that acquire was successful and semaphore is signaled but image is suboptimal
// allow rendering frame to suboptimal swapchain as otherwise we would have to manually unsignal semaphore and acquire image again
else if (result != VK_SUBOPTIMAL_KHR)
else
{
VK_CHECK(result);
handle_surface_changes();
// Acquire the next image from the swap chain
VkResult result = get_render_context().get_swapchain().acquire_next_image(current_buffer, semaphores.acquired_image_ready, VK_NULL_HANDLE);
// Recreate the swapchain if it's no longer compatible with the surface (OUT_OF_DATE)
if (result == VK_ERROR_OUT_OF_DATE_KHR)
{
resize(width, height);
}
// VK_SUBOPTIMAL_KHR means that acquire was successful and semaphore is signaled but image is suboptimal
// allow rendering frame to suboptimal swapchain as otherwise we would have to manually unsignal semaphore and acquire image again
else if (result != VK_SUBOPTIMAL_KHR)
{
VK_CHECK(result);
}
}
}
}

void ApiVulkanSample::submit_frame()
{
if (use_new_sync)
{
VkSubmitInfo submit_info{
.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
.waitSemaphoreCount = 1,
.pWaitSemaphores = &acquired_image_ready_semaphores[current_buffer],
.pWaitDstStageMask = &submit_pipeline_stages,
.commandBufferCount = 1,
.pCommandBuffers = &draw_cmd_buffers[current_buffer],
.signalSemaphoreCount = 1,
.pSignalSemaphores = &render_complete_semaphores[current_image_index]};
VK_CHECK(vkQueueSubmit(queue, 1, &submit_info, wait_fences[current_buffer]));
}
if (get_render_context().has_swapchain())
{
const auto &queue = get_device().get_queue_by_present(0);
Expand All @@ -555,7 +598,7 @@ void ApiVulkanSample::submit_frame()
present_info.pNext = NULL;
present_info.swapchainCount = 1;
present_info.pSwapchains = &sc;
present_info.pImageIndices = &current_buffer;
present_info.pImageIndices = use_new_sync ? &current_image_index : &current_buffer;

VkDisplayPresentInfoKHR disp_present_info{};
if (get_device().get_gpu().is_extension_supported(VK_KHR_DISPLAY_SWAPCHAIN_EXTENSION_NAME) &&
Expand All @@ -566,11 +609,19 @@ void ApiVulkanSample::submit_frame()
}

// Check if a wait semaphore has been specified to wait for before presenting the image
if (semaphores.render_complete != VK_NULL_HANDLE)
if (use_new_sync)
{
present_info.pWaitSemaphores = &semaphores.render_complete;
present_info.pWaitSemaphores = &render_complete_semaphores[current_image_index];
present_info.waitSemaphoreCount = 1;
}
else
{
if (semaphores.render_complete != VK_NULL_HANDLE)
{
present_info.pWaitSemaphores = &semaphores.render_complete;
present_info.waitSemaphoreCount = 1;
}
}

VkResult present_result = queue.present(present_info);

Expand All @@ -589,10 +640,18 @@ void ApiVulkanSample::submit_frame()
}
}

// DO NOT USE
// vkDeviceWaitIdle and vkQueueWaitIdle are extremely expensive functions, and are used here purely for demonstrating the vulkan API
// without having to concern ourselves with proper syncronization. These functions should NEVER be used inside the render loop like this (every frame).
VK_CHECK(get_device().get_queue_by_present(0).wait_idle());
if (use_new_sync)
{
// Select the next frame to render to, based on the max. no. of concurrent frames
current_buffer = (current_buffer + 1) % max_concurrent_frames;
}
else
{
// DO NOT USE
// vkDeviceWaitIdle and vkQueueWaitIdle are extremely expensive functions, and are used here purely for demonstrating the vulkan API
// without having to concern ourselves with proper syncronization. These functions should NEVER be used inside the render loop like this (every frame).
VK_CHECK(get_device().get_queue_by_present(0).wait_idle());
}
}

ApiVulkanSample::~ApiVulkanSample()
Expand Down Expand Up @@ -639,6 +698,18 @@ ApiVulkanSample::~ApiVulkanSample()
{
vkDestroyFence(get_device().get_handle(), fence, nullptr);
}

if (use_new_sync)
{
for (auto &semaphore : acquired_image_ready_semaphores)
{
vkDestroySemaphore(get_device().get_handle(), semaphore, nullptr);
}
for (auto &semaphore : render_complete_semaphores)
{
vkDestroySemaphore(get_device().get_handle(), semaphore, nullptr);
}
}
}
}

Expand All @@ -650,18 +721,46 @@ void ApiVulkanSample::build_command_buffers()

void ApiVulkanSample::rebuild_command_buffers()
{
vkResetCommandPool(get_device().get_handle(), cmd_pool, 0);
build_command_buffers();
if (!use_new_sync)
{
vkResetCommandPool(get_device().get_handle(), cmd_pool, 0);
build_command_buffers();
}
}

void ApiVulkanSample::create_synchronization_primitives()
{
// Wait fences to sync command buffer access
VkFenceCreateInfo fence_create_info = vkb::initializers::fence_create_info(VK_FENCE_CREATE_SIGNALED_BIT);
wait_fences.resize(draw_cmd_buffers.size());
for (auto &fence : wait_fences)
if (use_new_sync)
{
VK_CHECK(vkCreateFence(get_device().get_handle(), &fence_create_info, nullptr, &fence));
// Wait fences to sync command buffer access
wait_fences.resize(max_concurrent_frames);
VkFenceCreateInfo fence_create_info{.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO, .flags = VK_FENCE_CREATE_SIGNALED_BIT};
for (auto &fence : wait_fences)
{
VK_CHECK(vkCreateFence(get_device().get_handle(), &fence_create_info, nullptr, &fence));
}
VkSemaphoreCreateInfo semaphore_create_info{.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO};
// Used to ensure that image presentation is complete before starting to submit again
for (auto &semaphore : acquired_image_ready_semaphores)
{
VK_CHECK(vkCreateSemaphore(get_device().get_handle(), &semaphore_create_info, nullptr, &semaphore));
}
// Semaphore used to ensure that all commands submitted have been finished before submitting the image to the queue
render_complete_semaphores.resize(swapchain_buffers.size());
for (auto &semaphore : render_complete_semaphores)
{
VK_CHECK(vkCreateSemaphore(get_device().get_handle(), &semaphore_create_info, nullptr, &semaphore));
}
}
else
{
// Wait fences to sync command buffer access
VkFenceCreateInfo fence_create_info = vkb::initializers::fence_create_info(VK_FENCE_CREATE_SIGNALED_BIT);
wait_fences.resize(draw_cmd_buffers.size());
for (auto &fence : wait_fences)
{
VK_CHECK(vkCreateFence(get_device().get_handle(), &fence_create_info, nullptr, &fence));
}
}
}

Expand All @@ -670,6 +769,10 @@ void ApiVulkanSample::create_command_pool()
VkCommandPoolCreateInfo command_pool_info = {};
command_pool_info.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
command_pool_info.queueFamilyIndex = get_device().get_queue_by_flags(VK_QUEUE_GRAPHICS_BIT | VK_QUEUE_COMPUTE_BIT, 0).get_family_index();
if (use_new_sync)
{
command_pool_info.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
}
VK_CHECK(vkCreateCommandPool(get_device().get_handle(), &command_pool_info, nullptr, &cmd_pool));
}

Expand Down
16 changes: 14 additions & 2 deletions framework/api_vulkan_sample.h
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,10 @@ class ApiVulkanSample : public vkb::VulkanSampleC
};

protected:
/// Stores the swapchain image buffers
// @todo
bool use_new_sync = false;

// Stores the swapchain image buffers
std::vector<SwapchainBuffer> swapchain_buffers;

virtual void create_render_context() override;
Expand Down Expand Up @@ -159,6 +162,9 @@ class ApiVulkanSample : public vkb::VulkanSampleC
// List of available frame buffers (same as number of swap chain images)
std::vector<VkFramebuffer> framebuffers;

// @todo
uint32_t current_image_index = 0;

// Active frame buffer index
uint32_t current_buffer = 0;

Expand All @@ -184,6 +190,12 @@ class ApiVulkanSample : public vkb::VulkanSampleC
// Synchronization fences
std::vector<VkFence> wait_fences;

// Synchronization primitives
std::array<VkSemaphore, max_concurrent_frames> acquired_image_ready_semaphores{};
std::vector<VkSemaphore> render_complete_semaphores{};
// @todo: use this
// std::array<VkFence, max_concurrent_frames> wait_fences;

/**
* @brief Populates the swapchain_buffers vector with the image and imageviews
*/
Expand Down Expand Up @@ -284,7 +296,7 @@ class ApiVulkanSample : public vkb::VulkanSampleC
* @brief To be overridden by the derived class. Records the relevant commands to the rendering command buffers
* Called when the framebuffers need to be rebuilt
*/
virtual void build_command_buffers() = 0;
virtual void build_command_buffers();

/**
* @brief Rebuild the command buffers by first resetting the corresponding command pool and then building the command buffers.
Expand Down
3 changes: 3 additions & 0 deletions framework/common/vk_common.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@

#define DEFAULT_FENCE_TIMEOUT 100000000000 // Default fence timeout in nanoseconds

// Max. number of frames in floight
constexpr uint32_t max_concurrent_frames = 2;

template <class T>
using ShaderStageMap = std::map<VkShaderStageFlagBits, T>;

Expand Down
Loading
Loading