This commit is contained in:
xsl
2025-09-04 10:54:47 +08:00
commit 6bc8f61b18
1808 changed files with 208268 additions and 0 deletions
@@ -0,0 +1,33 @@
# Copyright (c) 2021-2024, Arm Limited and Contributors
#
# 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_with_tags(
ID ${FOLDER_NAME}
CATEGORY ${CATEGORY_NAME}
AUTHOR "Hans-Kristian Arntzen"
NAME "Timeline semaphore"
DESCRIPTION "Demonstrates use of timeline semaphores to express complex queue dependency graphs"
SHADER_FILES_GLSL
"timeline_semaphore/game_of_life_update.comp"
"timeline_semaphore/game_of_life_mutate.comp"
"timeline_semaphore/game_of_life_init.comp"
"timeline_semaphore/render.vert"
"timeline_semaphore/render.frag")
@@ -0,0 +1,365 @@
////
- Copyright (c) 2021-2024, Arm Limited and Contributors
-
- 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.
-
////
= Timeline semaphore
ifdef::site-gen-antora[]
TIP: The source for this sample can be found in the https://github.com/KhronosGroup/Vulkan-Samples/tree/main/samples/extensions/timeline_semaphore[Khronos Vulkan samples github repository].
endif::[]
== Overview
In Vulkan 1.0, we were introduced to `VkSemaphore` which is able to synchronize work between Vulkan queues.
It has some peculiar behavior which makes it somewhat difficult to use in practice.
The timeline semaphore is designed to solve these problems and it also makes the queue synchronization model closer to what we see in D3D12.
The existing semaphore as-is works fine in normal situations, but as applications learn to take advantage of async compute, async transfer, and other advanced synchronization use cases, there are problems which are hard to ignore.
=== The binary semaphore problems
The existing semaphore type is now called a `BINARY` semaphore, as signals and waits must always happen in 1:1 pairs.
Completing a wait for a semaphore on the `VkQueue` also *unsignals it*.
This is problematic for more advanced use cases where we wish to create a single producer, multiple consumers scenario.
To make binary semaphores work, we would have to signal multiple semaphores in a single `vkQueueSubmit`, and then assign one semaphore to each waiting queue.
This is rather awkward, since it might not be obvious at signal time how this scenario will play out, and juggling N semaphores just for this case is not fun.
When juggling N semaphores, it might also happen that a semaphore was not required after all, and we are now sitting with a signalled semaphore which cannot be recycled and signalled again unless we wait for it first.
The solution here is to just destroy such "hung" semaphores, which is unfortunate.
Ideally we would be able to reset semaphores on the host as well, but no such API exists and submitting a wait to GPU just for the purpose of unsignalling a semaphore is silly.
There is also an object bloat problem.
Usually, there are many submissions in flight on a GPU, and to be able to synchronize with each submission, we must keep track of a certain number of semaphores which are in-flight at any one time.
This is doable, but inelegant.
There is a similar problem for `VkFence` as well.
The final problem is a lack of out-of-order signal and wait.
This is a somewhat of a niche problem, but in a world with free threaded task graphs, it could make sense to be able to submit work out of order and let synchronization objects take care of synchronization on the GPU.
With binary semaphores, a signal must be submitted before the wait, which guarantees forward progress, but guarantees jank in the engine.
There are certainly good reasons for this restriction, but it removes some flexibility.
=== Viewing a `VkQueue` as a sequence - thinking in terms of counters
In order to signal on a `VkQueue`, we wait for everything that happened before we signal anything.
This also means that future signal operations will wait for a superset of the operations in the signal that came before.
In this sense, instead of thinking of synchronizing against individual submissions, we can think about things like "Wait for submission #134 on compute queue to complete", i.e.
we just associate a single monotonically increasing number to a queue.
Submitting to a `VkQueue` can now be considered a simple increment of the monotonically increasing number.
This is the foundation of timeline semaphores.
A `VkSemaphore` can have a 64-bit counter associated with it and there are two new operations we can do:
* As a signal semaphore, wait for everything to complete in queue, then *monotonically* bump counter value to `$old_value + $increment`, where `$increment` is usually 1.
* As a wait semaphore, wait for the counter of the semaphore to reach *at least* the wait count value.
From an application point of view, there is no longer a need to own synchronization objects and applications can instead agree on 64-bit counters.
=== Out-of-order signal and wait
Timeline semaphores also adds support for submitting waits before the corresponding signal operation.
This hands over the burden to the driver, where it will need to either hold back submissions on its own, or defer this work to the kernel driver.
Either way, the application no longer needs to hold back submissions.
This can be quite useful when applications have multiple threads which perform queue submission, since ensuring ordering otherwise would require a lot of careful thread synchronization.
=== Single producer, multiple consumers
There is no unsignal operation with timeline semaphores, so it's perfectly fine to do something like:
* Signal graphics queue, value 40
* Wait async compute queue 0, value 40
* Wait async compute queue 1, value 39
* Wait async compute queue 2, value 36
Once the counter reaches 40, it will always be at least 40, and we can keep waiting for this counter as long as we wish.
=== Integration of host signal and wait, good night sweet `VkFence`
VkFence is somewhat redundant when we have timeline semaphores, since we can now wait for counter values on CPU as well.
There is not even a requirement to externally synchronize `VkSemaphore` objects when doing so, which is very nice!
To synchronize GPU work with CPU, we just need to know the timeline value we signalled with.
== Using timeline semaphores
First, we need to create a `VkSemaphore` with `TIMELINE` type.
[,cpp]
----
// A timeline semaphore is still a semaphore, but it is of TIMELINE type rather than BINARY.
VkSemaphoreCreateInfo create_info = vkb::initializers::semaphore_create_info();
VkSemaphoreTypeCreateInfoKHR type_create_info{VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO_KHR};
type_create_info.semaphoreType = VK_SEMAPHORE_TYPE_TIMELINE_KHR;
type_create_info.initialValue = 0;
create_info.pNext = &type_create_info;
VK_CHECK(vkCreateSemaphore(get_device().get_handle(), &create_info, nullptr, &timeline.semaphore));
----
We can signal the timeline in `vkQueueSubmit`.
[,cpp]
----
VkSubmitInfo submit = vkb::initializers::submit_info();
submit.pSignalSemaphores = &timeline.semaphore;
submit.signalSemaphoreCount = 1;
submit.pCommandBuffers = &cmd;
submit.commandBufferCount = 1;
// For every timeline semaphore we signal, we give an auxillary timeline value.
VkTimelineSemaphoreSubmitInfoKHR timeline_info{VK_STRUCTURE_TYPE_TIMELINE_SEMAPHORE_SUBMIT_INFO_KHR};
timeline_info.signalSemaphoreValueCount = 1;
timeline_info.pSignalSemaphoreValues = &timeline.timeline;
submit.pNext = &timeline_info;
VK_CHECK(vkQueueSubmit(signal_queue, 1, &submit, VK_NULL_HANDLE));
----
Similarly, we can wait in `vkQueueSubmit`.
[,cpp]
----
const VkPipelineStageFlags wait_stages = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
VkSubmitInfo submit = vkb::initializers::submit_info();
submit.pWaitSemaphores = &timeline.semaphore;
submit.pWaitDstStageMask = &wait_stages;
submit.waitSemaphoreCount = 1;
submit.pCommandBuffers = &cmd;
submit.commandBufferCount = 1;
VkTimelineSemaphoreSubmitInfoKHR timeline_info{VK_STRUCTURE_TYPE_TIMELINE_SEMAPHORE_SUBMIT_INFO_KHR};
timeline_info.waitSemaphoreValueCount = 1;
timeline_info.pWaitSemaphoreValues = &timeline.timeline;
submit.pNext = &timeline_info;
VK_CHECK(vkQueueSubmit(wait_queue, 1, &submit, VK_NULL_HANDLE));
----
We can wait for one or more semaphores on host as well!
[,cpp]
----
VkSemaphoreWaitInfoKHR wait_info{VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO_KHR};
wait_info.pSemaphores = &semaphore;
wait_info.semaphoreCount = 1;
wait_info.pValues = &value;
VK_CHECK(vkWaitSemaphoresKHR(device->get_handle(), &wait_info, UINT64_MAX));
----
A somewhat esoteric feature is to signal a timeline on host, this can be used to "kick" the GPU.
[,cpp]
----
VkSemaphoreSignalInfoKHR signal_info{VK_STRUCTURE_TYPE_SEMAPHORE_SIGNAL_INFO_KHR};
signal_info.semaphore = semaphore;
signal_info.value = value;
VK_CHECK(vkSignalSemaphoreKHR(device->get_handle(), &signal_info));
----
== The sample
image::./images/sample.png[Sample]
This sample demonstrates an esoteric way of implementing the well-known "Game of Life".
Through this sample we end up using all the core features of timeline semaphores.
=== The queues
In this sample, we make use of two `VkQueues`, an async compute queue which performs simulation, and the main graphics queue which blits to swapchain and presents the results.
The two queues need to carefully synchronize with each other.
This sample could trivially be done with binary semaphores of course, so in this sample we implement it in a difficult way to demonstrate the full API capabilities.
=== Async worker thread - out-of-order submission
The key aspects we use to demonstrate out of order submission are dedicated workers thread which perform all work related to simulation on the async compute queue, and drawing on the graphics queue.
They never synchronize with the main thread except at teardown, so the only way to synchronize them is through timeline semaphores.
To avoid issues when running the sample on Windows platforms (particularly when resizing the window), forward progress in the queues is throttled by the main thread (i.e. only allowing the timeline to advance
when a render call is active).
=== Data flow
To simulate "Game of Life", we allocate two images of 64x64 RGBA8.
First, one image is initialized with initial state, and from here there is a ping-pong where image N is updated, while reading from image 1 - N.
After updating image N, the main thread will sample from image N.
The sequential flow of the rendering is something like:
* Compute: wait for "submit"
* Graphics: wait for "submit"
* Main: acquires the swapchain image
* Main: signal "submit"
* Main: wait for "present"
* Compute: wait for "image_acquired" (binary semaphore)
* Graphics: wait for "draw"
* Compute: write image
* Compute: signal "draw"
* Compute: wait for "end of frame"
* Graphics: read image
* Graphics: signal "present"
* Graphics: wait for "end of frame"
* Main: present swapchain
* Main: signals "end of frame"
* Compute: wait for "submit"
* Graphics: wait for "submit"
And so on ...
With out of order signal, we can end up observing this order of submissions instead.
* Compute: wait for "submit"
* Graphics: wait for "submit"
* Main: acquires the swapchain image
* Main: signal "submit"
* Graphics: wait for "draw"
* Compute: wait for "image_acquired" (binary semaphore)
* Compute: write image
* Compute: signal "draw"
* Graphics: read image
* Graphics: signal "present"
* Main: wait for "present"
* Main: present swapchain
* Compute: wait for "end of frame"
* Main: signals "end of frame"
* Graphics: wait for "end of frame"
* Compute: wait for "submit"
* Graphics: wait for "submit"
When submitting out of order, it is important that you don't just submit work way ahead of where the GPU actually is, since the latency becomes extremely large.
The natural place to keep submission explosion under control here is the place where we wait for the timeline on host, since we need to re-record command buffers anyways.
=== Avoiding deadlocks in `vkDeviceWaitIdle`
When submitting out-of-order we end up in a situation where a queue cannot see any forward progress until another queue submits.
Calling `vkDeviceWaitIdle` at this point triggers a deadlock of the application since `vkDeviceWaitIdle` will never finish, as there is one queue which cannot make forward progress.
While calling `vkDeviceWaitIdle`, you cannot call `vkQueueSubmit` due to external synchronization rules.
Instead, just wait for timeline semaphores on host to "drain" the GPU, or if you must use API calls, use `vkQueueWaitIdle` and only wait on queues which you need.
=== Avoiding deadlocks when tearing down worker thread
Similar to `vkDeviceWaitIdle`, when tearing down the application, an out-of-order submission might be waiting on work which never comes, and that queue becomes deadlocked.
To alleviate this, we can make use of host signalling of timeline semaphores to unblock everything in one fell swoop.
From `TimelineSemaphore::finish_timeline_workers()`:
[,cpp]
----
graphics_worker.alive = false;
compute_worker.alive = false;
signal_timeline(Timeline::MAX_STAGES);
if (graphics_worker.thread.joinable())
{
graphics_worker.thread.join();
}
if (compute_worker.thread.joinable())
{
compute_worker.thread.join();
}
----
From `TimelineSemaphore::finish_timeline_workers()`:
[,cpp]
----
graphics_worker.alive = false;
compute_worker.alive = false;
signal_timeline(Timeline::MAX_STAGES);
if (graphics_worker.thread.joinable())
{
graphics_worker.thread.join();
}
if (compute_worker.thread.joinable())
{
compute_worker.thread.join();
}
----
=== Out-of-order submission fallbacks for single queue implementations
Since this sample needs to run on all implementations which support timeline semaphores, the sample also demonstrates the limitations of out-of-order queue submissions.
It's easy to land in a situation where you deadlock the GPU or driver which only happens on single queue Vulkan implementations.
There are two fixes we need to make this work.
==== Holding back submissions
This workaround ensures that submissions happen in-order, where forward progress can always be made.
Since we are using multiple submission threads this sample uses a condition variable to only allow a wait to be submitted if it ensures forward progress.
This is handled by `TimelineSemaphore::update_pending()`:
[,cpp]
----
std::lock_guard<std::mutex> holder{lock.lock};
lock.pending_timeline = timeline;
lock.cond.notify_one();
----
and `TimelineSemaphore::wait_pending()`:
[,cpp]
----
std::unique_lock<std::mutex> holder{lock.lock};
lock.cond.wait(holder, [&lock, timeline]() -> bool {
return lock.pending_timeline >= timeline;
});
----
Blocking like this only works when multiple threads can submit, but that's what this sample is doing, so it is a simple fix.
The most robust workaround is probably to not lean too heavily on out-of-order submission unless you know you have all the `VkQueues` you need to resolve the dependencies properly.
==== Locking `vkQueueSubmit`
If two threads end up submitting to the same queue at the same time, we need to add locks due to external synchronization requirement of the `VkQueue`.
In this sample, we only add the locks if we're applying workarounds.
== API limitations
Currently, the Vulkan WSI swapchain does not support timeline semaphores.
In practice, this isn't too big of a deal as swapchain integration tends to be a "special case" either way in most rendering backends.
The acquire and release semaphores have no analog in other modern APIs.
Another related issue with WSI swapchains is that when using binary semaphores, it is not possible to use wait-before-signal.
The specification states that in order to submit a wait on a binary semaphore all dependencies for that semaphore wait must have been submitted already.
This means that we need to potentially block a bit on host before we can call vkQueuePresentKHR.
The sample does this right before calling `ApiVulkanSample::submit_frame()`.
[,cpp]
----
// Before we call present, which uses a binary semaphore, we must ensure that all dependent submissions
// have been submitted, so that the presenting queue is unblocked at the time of calling.
wait_pending(async_compute_timeline_lock, main_thread_timeline.timeline);
ApiVulkanSample::submit_frame();
----
== Conclusion
Timeline semaphores grants a lot of flexibility to applications.
With modern approaches of task graphs, many threads and free flowing synchronization, timeline semaphores simplify a lot of things and removes the need for emulating a similar concept with binary semaphores and fences.
Be careful with out-of-order submissions.
There are various pitfalls with this approach which have been outlined in this sample.
Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

@@ -0,0 +1,668 @@
/* Copyright (c) 2021-2025, Arm Limited and Contributors
*
* 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 "timeline_semaphore.h"
#include "common/vk_initializers.h"
// What we're trying to demonstrate here is:
// - Out-of-order submission using threads which synchronize GPU work with each other using timeline semaphores.
// In this sample we have a dedicated worker threads for submitting work to the compute and graphics pipelines respectively,
// and the only synchronization with main thread happens via timeline semaphores.
// - Waiting for timeline semaphore on CPU to replace redundant fence objects.
// - Multiple waits on the same timeline. We don't need to worry about allocating and managing binary semaphores in complex scenarios.
// We can wait on the same timeline values as many times as we want, and we avoid all resource management problems that binary semaphores have.
namespace
{
static constexpr unsigned grid_width = 64;
static constexpr unsigned grid_height = 64;
} // namespace
TimelineSemaphore::TimelineSemaphore()
{
title = "Timeline Semaphore";
add_device_extension(VK_KHR_TIMELINE_SEMAPHORE_EXTENSION_NAME);
add_instance_extension(VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME);
}
TimelineSemaphore::~TimelineSemaphore()
{
if (prepared)
{
finish_timeline_workers();
}
if (has_device())
{
VkDevice vk_device = get_device().get_handle();
vkDestroyCommandPool(vk_device, graphics.command_pool, nullptr);
vkDestroyPipelineLayout(vk_device, graphics.pipeline_layout, nullptr);
vkDestroyPipeline(vk_device, graphics.pipeline, nullptr);
vkDestroyCommandPool(vk_device, compute.command_pool, nullptr);
vkDestroyPipelineLayout(vk_device, compute.pipeline_layout, nullptr);
vkDestroyPipeline(vk_device, compute.update_pipeline, nullptr);
vkDestroyPipeline(vk_device, compute.mutate_pipeline, nullptr);
vkDestroyPipeline(vk_device, compute.init_pipeline, nullptr);
vkDestroyDescriptorSetLayout(vk_device, shared.storage_layout, nullptr);
vkDestroyDescriptorSetLayout(vk_device, shared.sampled_layout, nullptr);
vkDestroyDescriptorPool(vk_device, shared.descriptor_pool, nullptr);
vkDestroySemaphore(vk_device, timeline.semaphore, nullptr);
}
}
void TimelineSemaphore::setup_shared_resources()
{
// Descriptor pool
{
VkDescriptorPoolSize pool_sizes[2] = {
{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, NumAsyncFrames},
{VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, NumAsyncFrames},
};
VkDescriptorPoolCreateInfo pool_info = vkb::initializers::descriptor_pool_create_info(2, pool_sizes, 2 * NumAsyncFrames);
VK_CHECK(vkCreateDescriptorPool(get_device().get_handle(), &pool_info, nullptr, &shared.descriptor_pool));
}
// Sampler
{
auto sampler_create_info = vkb::initializers::sampler_create_info();
sampler_create_info.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT;
sampler_create_info.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT;
sampler_create_info.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT;
sampler_create_info.minFilter = VK_FILTER_NEAREST;
sampler_create_info.magFilter = VK_FILTER_NEAREST;
sampler_create_info.maxLod = VK_LOD_CLAMP_NONE;
sampler_create_info.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
shared.immutable_sampler = std::make_unique<vkb::core::Sampler>(get_device(), sampler_create_info);
}
// Images and image views
{
const auto present_index = get_device().get_queue_by_present(0).get_family_index();
auto sharing_mode = VK_SHARING_MODE_CONCURRENT;
std::vector<uint32_t> queue_families{compute.queue_family_index};
if (graphics.queue_family_index != compute.queue_family_index)
{
queue_families.push_back(graphics.queue_family_index);
}
if (compute.queue_family_index != present_index && graphics.queue_family_index != present_index)
{
queue_families.push_back(present_index);
}
if (queue_families.size() <= 1)
{
sharing_mode = VK_SHARING_MODE_EXCLUSIVE;
}
for (int i = 0; i < NumAsyncFrames; ++i)
{
// Need CONCURRENT usage here since we will sample from the image in both graphics and compute queues.
shared.images[i] = std::make_unique<vkb::core::Image>(get_device(), vkb::core::ImageBuilder(VkExtent3D{grid_width, grid_height, 1})
.with_format(VK_FORMAT_R8G8B8A8_UNORM)
.with_usage(VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT)
.with_vma_usage(VMA_MEMORY_USAGE_GPU_ONLY)
.with_sample_count(VK_SAMPLE_COUNT_1_BIT)
.with_mip_levels(1)
.with_array_layers(1)
.with_tiling(VK_IMAGE_TILING_OPTIMAL)
.with_queue_families(static_cast<uint32_t>(queue_families.size()), queue_families.data())
.with_sharing_mode(sharing_mode));
shared.image_views[i] = std::make_unique<vkb::core::ImageView>(*shared.images[i], VK_IMAGE_VIEW_TYPE_2D, VK_FORMAT_R8G8B8A8_UNORM);
}
}
// Descriptor layouts
{
VkDescriptorSetLayoutBinding storage_binding = vkb::initializers::descriptor_set_layout_binding(VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, VK_SHADER_STAGE_COMPUTE_BIT, 0);
VkDescriptorSetLayoutBinding sampled_binding = vkb::initializers::descriptor_set_layout_binding(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_ALL, 0);
VkSampler vk_immutable_sampler = shared.immutable_sampler->get_handle();
sampled_binding.pImmutableSamplers = &vk_immutable_sampler;
VkDescriptorSetLayoutCreateInfo storage_set_layout_info = vkb::initializers::descriptor_set_layout_create_info(&storage_binding, 1);
VkDescriptorSetLayoutCreateInfo sampled_set_layout_info = vkb::initializers::descriptor_set_layout_create_info(&sampled_binding, 1);
VK_CHECK(vkCreateDescriptorSetLayout(get_device().get_handle(), &storage_set_layout_info, nullptr, &shared.storage_layout));
VK_CHECK(vkCreateDescriptorSetLayout(get_device().get_handle(), &sampled_set_layout_info, nullptr, &shared.sampled_layout));
}
// Descriptor sets
{
VkDescriptorSetAllocateInfo storage_alloc_info = vkb::initializers::descriptor_set_allocate_info(shared.descriptor_pool, &shared.storage_layout, 1);
VkDescriptorSetAllocateInfo sampled_alloc_info = vkb::initializers::descriptor_set_allocate_info(shared.descriptor_pool, &shared.sampled_layout, 1);
for (int i = 0; i < NumAsyncFrames; ++i)
{
VK_CHECK(vkAllocateDescriptorSets(get_device().get_handle(), &storage_alloc_info, &shared.storage_descriptor_sets[i]));
VK_CHECK(vkAllocateDescriptorSets(get_device().get_handle(), &sampled_alloc_info, &shared.sampled_descriptor_sets[i]));
auto general_info = vkb::initializers::descriptor_image_info(VK_NULL_HANDLE, shared.image_views[i]->get_handle(), VK_IMAGE_LAYOUT_GENERAL);
auto readonly_info = vkb::initializers::descriptor_image_info(VK_NULL_HANDLE, shared.image_views[i]->get_handle(), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
const VkWriteDescriptorSet writes[2] = {
vkb::initializers::write_descriptor_set(shared.storage_descriptor_sets[i], VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 0, &general_info),
vkb::initializers::write_descriptor_set(shared.sampled_descriptor_sets[i], VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 0, &readonly_info),
};
vkUpdateDescriptorSets(get_device().get_handle(), 2, writes, 0, nullptr);
}
}
}
void TimelineSemaphore::build_command_buffers()
{
// Unused, but required to resolve pure virtual function inherited from ApiVulkanSample
}
void TimelineSemaphore::create_timeline_semaphore()
{
// A timeline semaphore is still a semaphore, but it is of TIMELINE type rather than BINARY.
VkSemaphoreCreateInfo create_info = vkb::initializers::semaphore_create_info();
VkSemaphoreTypeCreateInfoKHR type_create_info{VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO_KHR};
type_create_info.semaphoreType = VK_SEMAPHORE_TYPE_TIMELINE_KHR;
type_create_info.initialValue = 0;
create_info.pNext = &type_create_info;
VK_CHECK(vkCreateSemaphore(get_device().get_handle(), &create_info, nullptr, &timeline.semaphore));
timeline.frame = 0;
}
void TimelineSemaphore::start_timeline_workers()
{
graphics_worker.alive = true;
graphics_worker.thread = std::thread([this]() { do_graphics_work(); });
compute_worker.alive = true;
compute_worker.thread = std::thread([this]() { do_compute_work(); });
}
void TimelineSemaphore::finish_timeline_workers()
{
graphics_worker.alive = false;
compute_worker.alive = false;
// The MAX_STAGES value is used to unblock all threads that are waiting on a timeline stage
signal_timeline(Timeline::MAX_STAGES);
if (graphics_worker.thread.joinable())
{
graphics_worker.thread.join();
}
if (compute_worker.thread.joinable())
{
compute_worker.thread.join();
}
}
// Signal the timeline from the host.
void TimelineSemaphore::signal_timeline(const Timeline::Stages stage)
{
VkSemaphoreSignalInfo signalInfo;
signalInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SIGNAL_INFO;
signalInfo.pNext = NULL;
signalInfo.semaphore = timeline.semaphore;
signalInfo.value = get_timeline_stage_value(stage);
VK_CHECK(vkSignalSemaphoreKHR(get_device().get_handle(), &signalInfo));
}
// Wait on the timeline from the host.
void TimelineSemaphore::wait_on_timeline(const Timeline::Stages stage)
{
const uint64_t waitValue = get_timeline_stage_value(stage);
VkSemaphoreWaitInfo waitInfo;
waitInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO;
waitInfo.pNext = NULL;
waitInfo.flags = 0;
waitInfo.semaphoreCount = 1;
waitInfo.pSemaphores = &timeline.semaphore;
waitInfo.pValues = &waitValue;
VK_CHECK(vkWaitSemaphoresKHR(get_device().get_handle(), &waitInfo, UINT64_MAX));
}
// Sends the MAX_STAGES signal for the current frame, then increments the frame counter
void TimelineSemaphore::signal_next_frame()
{
VkSemaphoreSignalInfo signalInfo;
signalInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SIGNAL_INFO;
signalInfo.pNext = NULL;
signalInfo.semaphore = timeline.semaphore;
signalInfo.value = get_timeline_stage_value(Timeline::MAX_STAGES);
timeline.frame++;
VK_CHECK(vkSignalSemaphoreKHR(get_device().get_handle(), &signalInfo));
}
// Waits for the timeline to reach MAX_STAGES for the current frame
void TimelineSemaphore::wait_for_next_frame()
{
// MAX_STAGES is used as it provides a boundary value between the stages of this frame and the next
const uint64_t waitValue = (timeline.frame + 1) * Timeline::MAX_STAGES;
VkSemaphoreWaitInfo waitInfo;
waitInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO;
waitInfo.pNext = NULL;
waitInfo.flags = 0;
waitInfo.semaphoreCount = 1;
waitInfo.pSemaphores = &timeline.semaphore;
waitInfo.pValues = &waitValue;
VK_CHECK(vkWaitSemaphoresKHR(get_device().get_handle(), &waitInfo, UINT64_MAX));
}
// Calculates the timeline value for the specified stage in the current frame
uint64_t TimelineSemaphore::get_timeline_stage_value(const Timeline::Stages stage)
{
return (timeline.frame * Timeline::MAX_STAGES) + stage;
}
void TimelineSemaphore::do_compute_work()
{
compute.timer.start();
while (compute_worker.alive)
{
// Wait for the main thread to signal that the workers can prepare and submit their work
wait_on_timeline(Timeline::submit);
auto elapsed = static_cast<float>(compute.timer.elapsed());
build_compute_command_buffers(elapsed);
uint64_t signal_value = get_timeline_stage_value(Timeline::draw);
VkTimelineSemaphoreSubmitInfo timeline_info = vkb::initializers::timeline_semaphore_submit_info(0, nullptr, 1, &signal_value);
VkSubmitInfo submit_info = vkb::initializers::submit_info();
submit_info.pNext = &timeline_info;
submit_info.commandBufferCount = 1;
submit_info.pCommandBuffers = &compute.command_buffer;
submit_info.signalSemaphoreCount = 1;
submit_info.pSignalSemaphores = &timeline.semaphore;
// If the threads are being killed, we need to skip the queue submission to allow the program to exit gracefully
if (compute_worker.alive)
{
VK_CHECK(vkQueueSubmit(compute.queue, 1, &submit_info, VK_NULL_HANDLE));
}
wait_for_next_frame();
}
}
void TimelineSemaphore::setup_compute_pipeline()
{
VkDescriptorSetLayout layouts[2] = {shared.storage_layout, shared.sampled_layout};
auto layout_info = vkb::initializers::pipeline_layout_create_info(layouts, 2);
VkPushConstantRange range = vkb::initializers::push_constant_range(VK_SHADER_STAGE_COMPUTE_BIT, sizeof(float), 0);
layout_info.pushConstantRangeCount = 1;
layout_info.pPushConstantRanges = &range;
VK_CHECK(vkCreatePipelineLayout(get_device().get_handle(), &layout_info, nullptr, &compute.pipeline_layout));
VkComputePipelineCreateInfo info = vkb::initializers::compute_pipeline_create_info(compute.pipeline_layout);
info.stage = load_shader("timeline_semaphore/game_of_life_update.comp.spv", VK_SHADER_STAGE_COMPUTE_BIT);
VK_CHECK(vkCreateComputePipelines(get_device().get_handle(), VK_NULL_HANDLE, 1, &info, nullptr, &compute.update_pipeline));
info.stage = load_shader("timeline_semaphore/game_of_life_mutate.comp.spv", VK_SHADER_STAGE_COMPUTE_BIT);
VK_CHECK(vkCreateComputePipelines(get_device().get_handle(), VK_NULL_HANDLE, 1, &info, nullptr, &compute.mutate_pipeline));
info.stage = load_shader("timeline_semaphore/game_of_life_init.comp.spv", VK_SHADER_STAGE_COMPUTE_BIT);
VK_CHECK(vkCreateComputePipelines(get_device().get_handle(), VK_NULL_HANDLE, 1, &info, nullptr, &compute.init_pipeline));
}
void TimelineSemaphore::setup_compute_resources()
{
// Get compute queue
compute.queue_family_index = vkb::get_queue_family_index(get_device().get_gpu().get_queue_family_properties(), VK_QUEUE_COMPUTE_BIT);
vkGetDeviceQueue(get_device().get_handle(), compute.queue_family_index, 0, &compute.queue);
compute.command_pool = get_device().create_command_pool(compute.queue_family_index, VK_COMMAND_POOL_CREATE_TRANSIENT_BIT | VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT);
VkCommandBufferAllocateInfo alloc_info =
vkb::initializers::command_buffer_allocate_info(compute.command_pool, VK_COMMAND_BUFFER_LEVEL_PRIMARY, 1);
VK_CHECK(vkAllocateCommandBuffers(get_device().get_handle(), &alloc_info, &compute.command_buffer));
}
void TimelineSemaphore::setup_game_of_life()
{
auto begin_info = vkb::initializers::command_buffer_begin_info();
begin_info.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
VK_CHECK(vkResetCommandBuffer(compute.command_buffer, 0));
VK_CHECK(vkBeginCommandBuffer(compute.command_buffer, &begin_info));
for (int i = 0; i < NumAsyncFrames; ++i)
{
vkCmdBindDescriptorSets(compute.command_buffer, VK_PIPELINE_BIND_POINT_COMPUTE, compute.pipeline_layout, 0, 1, &shared.storage_descriptor_sets[i], 0, nullptr);
// On the first iteration, we initialize the game of life.
vkCmdBindPipeline(compute.command_buffer, VK_PIPELINE_BIND_POINT_COMPUTE, compute.init_pipeline);
VkImageMemoryBarrier image_barrier = vkb::initializers::image_memory_barrier();
image_barrier.srcAccessMask = 0;
image_barrier.dstAccessMask = VK_ACCESS_SHADER_WRITE_BIT;
image_barrier.image = shared.images[i]->get_handle();
image_barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
image_barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
image_barrier.newLayout = VK_IMAGE_LAYOUT_GENERAL;
// The semaphore takes care of srcStageMask.
vkCmdPipelineBarrier(compute.command_buffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &image_barrier);
vkCmdDispatch(compute.command_buffer, grid_width / 8, grid_height / 8, 1);
image_barrier.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT;
image_barrier.dstAccessMask = 0;
image_barrier.oldLayout = VK_IMAGE_LAYOUT_GENERAL;
image_barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
// The semaphore takes care of dstStageMask.
vkCmdPipelineBarrier(compute.command_buffer, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, nullptr, 0, nullptr, 1, &image_barrier);
}
VK_CHECK(vkEndCommandBuffer(compute.command_buffer));
VkSubmitInfo submit_info = vkb::initializers::submit_info();
submit_info.commandBufferCount = 1;
submit_info.pCommandBuffers = &compute.command_buffer;
VK_CHECK(vkQueueSubmit(compute.queue, 1, &submit_info, VK_NULL_HANDLE));
get_device().wait_idle();
}
void TimelineSemaphore::build_compute_command_buffers(const float elapsed)
{
auto begin_info = vkb::initializers::command_buffer_begin_info();
begin_info.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
VK_CHECK(vkResetCommandBuffer(compute.command_buffer, 0));
VK_CHECK(vkBeginCommandBuffer(compute.command_buffer, &begin_info));
auto frame_index = timeline.frame % NumAsyncFrames;
auto prev_index = (timeline.frame - 1) % NumAsyncFrames;
vkCmdBindDescriptorSets(compute.command_buffer, VK_PIPELINE_BIND_POINT_COMPUTE, compute.pipeline_layout, 0, 1, &shared.storage_descriptor_sets[frame_index], 0, nullptr);
if (elapsed > 1.0f)
{
vkCmdBindPipeline(compute.command_buffer, VK_PIPELINE_BIND_POINT_COMPUTE, compute.update_pipeline);
compute.timer.lap();
}
else
{
vkCmdBindPipeline(compute.command_buffer, VK_PIPELINE_BIND_POINT_COMPUTE, compute.mutate_pipeline);
vkCmdPushConstants(compute.command_buffer, compute.pipeline_layout, VK_SHADER_STAGE_COMPUTE_BIT,
0, sizeof(elapsed), &elapsed);
}
// Bind previous iteration's texture.
vkCmdBindDescriptorSets(compute.command_buffer, VK_PIPELINE_BIND_POINT_COMPUTE, compute.pipeline_layout, 1, 1, &shared.sampled_descriptor_sets[prev_index], 0, nullptr);
VkImageMemoryBarrier image_barrier = vkb::initializers::image_memory_barrier();
image_barrier.srcAccessMask = 0;
image_barrier.dstAccessMask = VK_ACCESS_SHADER_WRITE_BIT;
image_barrier.image = shared.images[frame_index]->get_handle();
image_barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
image_barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
image_barrier.newLayout = VK_IMAGE_LAYOUT_GENERAL;
// The semaphore takes care of srcStageMask.
vkCmdPipelineBarrier(compute.command_buffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &image_barrier);
vkCmdDispatch(compute.command_buffer, grid_width / 8, grid_height / 8, 1);
image_barrier.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT;
image_barrier.dstAccessMask = 0;
image_barrier.oldLayout = VK_IMAGE_LAYOUT_GENERAL;
image_barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
// The semaphore takes care of dstStageMask.
vkCmdPipelineBarrier(compute.command_buffer, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, nullptr, 0, nullptr, 1, &image_barrier);
VK_CHECK(vkEndCommandBuffer(compute.command_buffer));
}
void TimelineSemaphore::do_graphics_work()
{
while (graphics_worker.alive)
{
// Wait for the main thread to signal that the workers can prepare and submit their work
wait_on_timeline(Timeline::submit);
build_graphics_command_buffer();
uint64_t wait_values[] = {get_timeline_stage_value(Timeline::draw), 0};
VkPipelineStageFlags wait_stage_masks[] = {VK_PIPELINE_STAGE_VERTEX_INPUT_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT};
VkSemaphore wait_semaphores[] = {timeline.semaphore, semaphores.acquired_image_ready};
uint64_t signal_values[] = {get_timeline_stage_value(Timeline::present), 0};
VkSemaphore signal_semaphores[] = {timeline.semaphore, semaphores.render_complete};
VkTimelineSemaphoreSubmitInfo timeline_info = vkb::initializers::timeline_semaphore_submit_info(2, wait_values, 2, signal_values);
VkSubmitInfo submit_info = vkb::initializers::submit_info();
submit_info.pNext = &timeline_info;
submit_info.waitSemaphoreCount = 2;
submit_info.pWaitSemaphores = wait_semaphores;
submit_info.pWaitDstStageMask = wait_stage_masks;
submit_info.signalSemaphoreCount = 2;
submit_info.pSignalSemaphores = signal_semaphores;
submit_info.commandBufferCount = 1;
submit_info.pCommandBuffers = &graphics.command_buffer;
if (compute.queue == graphics.queue)
{
// If compute.queue == queue, we need synchronise access to the queue AND ensure that submissions are made in order
// (otherwise the queue will deadlock itself). So we wait for the "draw" stage to be signalled on the host, before
// submitting the work.
wait_on_timeline(Timeline::draw);
}
// If the threads are being killed, we need to skip the queue submission to allow the program to exit gracefully
if (graphics_worker.alive)
{
VK_CHECK(vkQueueSubmit(graphics.queue, 1, &submit_info, VK_NULL_HANDLE));
}
wait_for_next_frame();
}
}
void TimelineSemaphore::setup_graphics_resources()
{
graphics.queue_family_index = vkb::get_queue_family_index(get_device().get_gpu().get_queue_family_properties(), VK_QUEUE_GRAPHICS_BIT);
graphics.queue = queue;
graphics.command_pool = get_device().create_command_pool(graphics.queue_family_index, VK_COMMAND_POOL_CREATE_TRANSIENT_BIT | VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT);
VkCommandBufferAllocateInfo alloc_info =
vkb::initializers::command_buffer_allocate_info(graphics.command_pool, VK_COMMAND_BUFFER_LEVEL_PRIMARY, 1);
VK_CHECK(vkAllocateCommandBuffers(get_device().get_handle(), &alloc_info, &graphics.command_buffer));
}
void TimelineSemaphore::setup_graphics_pipeline()
{
auto layout_info = vkb::initializers::pipeline_layout_create_info(&shared.sampled_layout, 1);
VK_CHECK(vkCreatePipelineLayout(get_device().get_handle(), &layout_info, nullptr, &graphics.pipeline_layout));
VkGraphicsPipelineCreateInfo info = vkb::initializers::pipeline_create_info(graphics.pipeline_layout, render_pass);
VkPipelineVertexInputStateCreateInfo vertex_input_state = vkb::initializers::pipeline_vertex_input_state_create_info();
VkPipelineInputAssemblyStateCreateInfo input_assembly_state =
vkb::initializers::pipeline_input_assembly_state_create_info(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, 0, VK_FALSE);
VkPipelineRasterizationStateCreateInfo rasterization_state =
vkb::initializers::pipeline_rasterization_state_create_info(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE, VK_FRONT_FACE_CLOCKWISE, 0);
VkPipelineColorBlendAttachmentState blend_attachment_state =
vkb::initializers::pipeline_color_blend_attachment_state(0xf, VK_FALSE);
VkPipelineColorBlendStateCreateInfo color_blend_state =
vkb::initializers::pipeline_color_blend_state_create_info(1, &blend_attachment_state);
VkPipelineDepthStencilStateCreateInfo depth_stencil_state =
vkb::initializers::pipeline_depth_stencil_state_create_info(VK_FALSE, VK_FALSE, VK_COMPARE_OP_GREATER);
VkPipelineViewportStateCreateInfo viewport_state =
vkb::initializers::pipeline_viewport_state_create_info(1, 1, 0);
VkPipelineMultisampleStateCreateInfo multisample_state =
vkb::initializers::pipeline_multisample_state_create_info(VK_SAMPLE_COUNT_1_BIT, 0);
std::vector<VkDynamicState> dynamic_state_enables = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR};
VkPipelineDynamicStateCreateInfo dynamic_state =
vkb::initializers::pipeline_dynamic_state_create_info(dynamic_state_enables);
info.pVertexInputState = &vertex_input_state;
info.pInputAssemblyState = &input_assembly_state;
info.pRasterizationState = &rasterization_state;
info.pColorBlendState = &color_blend_state;
info.pDepthStencilState = &depth_stencil_state;
info.pViewportState = &viewport_state;
info.pMultisampleState = &multisample_state;
info.pDynamicState = &dynamic_state;
VkPipelineShaderStageCreateInfo stages[2];
info.pStages = stages;
info.stageCount = 2;
stages[0] = load_shader("timeline_semaphore/render.vert.spv", VK_SHADER_STAGE_VERTEX_BIT);
stages[1] = load_shader("timeline_semaphore/render.frag.spv", VK_SHADER_STAGE_FRAGMENT_BIT);
VK_CHECK(vkCreateGraphicsPipelines(get_device().get_handle(), VK_NULL_HANDLE, 1, &info, nullptr, &graphics.pipeline));
}
void TimelineSemaphore::build_graphics_command_buffer()
{
auto frame_index = timeline.frame % NumAsyncFrames;
VkViewport viewport = {0.0f, 0.0f, static_cast<float>(width), static_cast<float>(height), 0.0f, 1.0f};
VkRect2D scissor = {{0, 0}, {width, height}};
// Simple fix for 1:1 pixel aspect ratio.
if (viewport.width > viewport.height)
{
viewport.x += 0.5f * (viewport.width - viewport.height);
viewport.width = viewport.height;
}
else if (viewport.height > viewport.width)
{
viewport.y += 0.5f * (viewport.height - viewport.width);
viewport.height = viewport.width;
}
VK_CHECK(vkResetCommandBuffer(graphics.command_buffer, 0));
auto begin_info = vkb::initializers::command_buffer_begin_info();
begin_info.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
VK_CHECK(vkBeginCommandBuffer(graphics.command_buffer, &begin_info));
VkRenderPassBeginInfo render_pass_begin = vkb::initializers::render_pass_begin_info();
render_pass_begin.renderPass = render_pass;
render_pass_begin.renderArea.extent.width = width;
render_pass_begin.renderArea.extent.height = height;
render_pass_begin.clearValueCount = 2;
VkClearValue clears[2] = {};
clears[0].color.float32[0] = 0.033f;
clears[0].color.float32[1] = 0.073f;
clears[0].color.float32[2] = 0.133f;
render_pass_begin.pClearValues = clears;
render_pass_begin.framebuffer = framebuffers[current_buffer];
vkCmdBeginRenderPass(graphics.command_buffer, &render_pass_begin, VK_SUBPASS_CONTENTS_INLINE);
vkCmdBindPipeline(graphics.command_buffer, VK_PIPELINE_BIND_POINT_GRAPHICS, graphics.pipeline);
vkCmdSetViewport(graphics.command_buffer, 0, 1, &viewport);
vkCmdSetScissor(graphics.command_buffer, 0, 1, &scissor);
vkCmdBindDescriptorSets(graphics.command_buffer, VK_PIPELINE_BIND_POINT_GRAPHICS, graphics.pipeline_layout, 0, 1, &shared.sampled_descriptor_sets[frame_index], 0, nullptr);
vkCmdDraw(graphics.command_buffer, 3, 1, 0, 0);
draw_ui(graphics.command_buffer);
vkCmdEndRenderPass(graphics.command_buffer);
VK_CHECK(vkEndCommandBuffer(graphics.command_buffer));
}
void TimelineSemaphore::request_gpu_features(vkb::PhysicalDevice &gpu)
{
// Need to enable the timelineSemaphore feature.
REQUEST_REQUIRED_FEATURE(gpu,
VkPhysicalDeviceTimelineSemaphoreFeaturesKHR,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_TIMELINE_SEMAPHORE_FEATURES_KHR,
timelineSemaphore);
}
bool TimelineSemaphore::prepare(const vkb::ApplicationOptions &options)
{
if (!ApiVulkanSample::prepare(options))
{
return false;
}
setup_compute_resources();
setup_graphics_resources();
setup_shared_resources();
setup_compute_pipeline();
setup_graphics_pipeline();
setup_game_of_life();
create_timeline_semaphore();
start_timeline_workers();
prepared = true;
return true;
}
void TimelineSemaphore::render(float delta_time)
{
if (!prepared)
{
return;
}
ApiVulkanSample::prepare_frame();
// Signal to the worker threads that they can submit their work
signal_timeline(Timeline::submit);
// Wait for the worker threads to signal that the frame is ready to present
wait_on_timeline(Timeline::present);
ApiVulkanSample::submit_frame();
// Signal to the worker threads that they can proceed to the next frame's work
signal_next_frame();
}
std::unique_ptr<vkb::Application> create_timeline_semaphore()
{
return std::make_unique<TimelineSemaphore>();
}
@@ -0,0 +1,128 @@
/* Copyright (c) 2021-2024, Arm Limited and Contributors
*
* 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 "api_vulkan_sample.h"
class TimelineSemaphore : public ApiVulkanSample
{
public:
static const uint32_t NumAsyncFrames = 2;
// Resources for the graphics worker
struct GraphicsResources
{
VkQueue queue;
VkCommandPool command_pool;
VkCommandBuffer command_buffer;
VkPipelineLayout pipeline_layout;
VkPipeline pipeline;
uint32_t queue_family_index;
} graphics;
// Resources for the compute worker
struct ComputeResources
{
VkQueue queue;
VkCommandPool command_pool;
VkCommandBuffer command_buffer;
VkPipelineLayout pipeline_layout;
VkPipeline init_pipeline;
VkPipeline update_pipeline;
VkPipeline mutate_pipeline;
vkb::Timer timer;
uint32_t queue_family_index;
} compute;
// Resources used by both workers for storing/sampling images
struct SharedResources
{
VkDescriptorSetLayout storage_layout;
VkDescriptorSetLayout sampled_layout;
VkDescriptorSet storage_descriptor_sets[NumAsyncFrames];
VkDescriptorSet sampled_descriptor_sets[NumAsyncFrames];
VkDescriptorPool descriptor_pool;
std::unique_ptr<vkb::core::Sampler> immutable_sampler;
std::unique_ptr<vkb::core::Image> images[NumAsyncFrames];
std::unique_ptr<vkb::core::ImageView> image_views[NumAsyncFrames];
} shared;
struct Timeline
{
// The stages of the timeline are enumerated, to make it easier to read which stage we are signalling/waiting on, and to allow
// the stages to be reused without needing to recreate the semaphore.
enum Stages
{
submit = 1, // Worker threads can create and submit their command buffers,
draw, // The graphics worker can draw the current frame
present, // The main thread can present the frame to the display
MAX_STAGES
};
VkSemaphore semaphore;
uint64_t frame; // Number of iterations through the timeline stages
} timeline;
struct TimelineWorker
{
std::thread thread;
std::atomic_bool alive;
};
TimelineWorker graphics_worker, compute_worker;
TimelineSemaphore();
~TimelineSemaphore();
void setup_shared_resources();
void build_command_buffers() override;
// Timeline operations
void create_timeline_semaphore();
void start_timeline_workers();
void finish_timeline_workers();
void signal_timeline(const Timeline::Stages stage);
void wait_on_timeline(const Timeline::Stages stage);
void signal_next_frame();
void wait_for_next_frame();
uint64_t get_timeline_stage_value(const Timeline::Stages stage);
// Compute Work
void do_compute_work();
void setup_compute_pipeline();
void setup_compute_resources();
void setup_game_of_life();
void build_compute_command_buffers(const float elapsed = 0.0f);
// Graphics Work
void do_graphics_work();
void setup_graphics_resources();
void setup_graphics_pipeline();
void build_graphics_command_buffer();
virtual void request_gpu_features(vkb::PhysicalDevice &gpu) override;
virtual bool prepare(const vkb::ApplicationOptions &options) override;
virtual void render(float delta_time) override;
};
std::unique_ptr<vkb::Application> create_timeline_semaphore();