diff options
author | Steven Le Rouzic <steven.lerouzic@gmail.com> | 2025-05-09 00:32:33 +0200 |
---|---|---|
committer | Steven Le Rouzic <steven.lerouzic@gmail.com> | 2025-05-09 00:37:56 +0200 |
commit | 7ec394db8961009e6ac23fea909f8353d865f7a3 (patch) | |
tree | 6104b92652953ee84c3d6c4386a7cfd6f133bcd9 /hk21 | |
parent | 69d33476b825a1f2deb4f9fb38a199c59d356701 (diff) |
Add and use Vulkan synchronization library
Diffstat (limited to 'hk21')
-rw-r--r-- | hk21/game/BUILD.bazel | 1 | ||||
-rw-r--r-- | hk21/game/gpu.cpp | 170 | ||||
-rw-r--r-- | hk21/vulkan/BUILD.bazel | 13 | ||||
-rw-r--r-- | hk21/vulkan/loader/BUILD.bazel | 2 | ||||
-rw-r--r-- | hk21/vulkan/loader/loader.hpp | 3 | ||||
-rw-r--r-- | hk21/vulkan/sync/BUILD.bazel | 33 | ||||
-rw-r--r-- | hk21/vulkan/sync/sync.cpp | 252 | ||||
-rw-r--r-- | hk21/vulkan/sync/sync.hpp | 64 | ||||
-rw-r--r-- | hk21/vulkan/sync/sync_tests.cpp | 162 | ||||
-rw-r--r-- | hk21/vulkan/vulkan.hpp | 12 |
10 files changed, 608 insertions, 104 deletions
diff --git a/hk21/game/BUILD.bazel b/hk21/game/BUILD.bazel index 0a571f3..218d1f0 100644 --- a/hk21/game/BUILD.bazel +++ b/hk21/game/BUILD.bazel @@ -18,6 +18,7 @@ cc_binary( "@asl//asl/containers:buffer",
"@sdl3_windows//:sdl3",
"//hk21/vulkan/loader",
+ "//hk21/vulkan/sync",
],
applicable_licenses = ["//:license"],
)
diff --git a/hk21/game/gpu.cpp b/hk21/game/gpu.cpp index 0d74559..a929e1a 100644 --- a/hk21/game/gpu.cpp +++ b/hk21/game/gpu.cpp @@ -14,11 +14,11 @@ #include <SDL3/SDL_vulkan.h>
#include "hk21/vulkan/loader/loader.hpp"
+#include "hk21/vulkan/sync/sync.hpp"
// @Todo Make fences recyclable
// @Todo Make command pool recyclable
// @Todo Make frame structure recyclable
-// @Todo Auto barriers for images
#define VK_ALLOCATOR nullptr
@@ -288,8 +288,53 @@ static asl::status_or<VkDevice> create_device(VkPhysicalDevice physical_device, struct FrameResources : asl::intrusive_list_node<FrameResources>
{
- VkFence complete_fence = VK_NULL_HANDLE;
- VkCommandPool command_pool = VK_NULL_HANDLE;
+ VkFence complete_fence = VK_NULL_HANDLE;
+ VkCommandPool command_pool = VK_NULL_HANDLE;
+};
+
+class DependencyInfoBuilder : public vulkan_sync::DependencyInfoBuilder
+{
+ // @Todo Configure allocator
+
+ asl::buffer<VkImageMemoryBarrier2> m_image_barriers;
+ asl::buffer<VkBufferMemoryBarrier2> m_buffer_barriers;
+
+public:
+ void add_image_barrier(const VkImageMemoryBarrier2& barrier) override
+ {
+ m_image_barriers.push(barrier);
+ }
+
+ void add_buffer_barrier(const VkBufferMemoryBarrier2& barrier) override
+ {
+ m_buffer_barriers.push(barrier);
+ }
+
+ void apply(VkCommandBuffer command_buffer)
+ {
+ if (m_image_barriers.is_empty() &&
+ m_buffer_barriers.is_empty())
+ {
+ return;
+ }
+
+ VkDependencyInfo dependency_info{
+ .sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO,
+ .pNext = nullptr,
+ .dependencyFlags = 0,
+ .memoryBarrierCount = 0,
+ .pMemoryBarriers = nullptr,
+ .bufferMemoryBarrierCount = static_cast<uint32_t>(m_buffer_barriers.size()),
+ .pBufferMemoryBarriers = m_buffer_barriers.data(),
+ .imageMemoryBarrierCount = static_cast<uint32_t>(m_image_barriers.size()),
+ .pImageMemoryBarriers = m_image_barriers.data(),
+ };
+
+ vkCmdPipelineBarrier2(command_buffer, &dependency_info);
+
+ m_image_barriers.clear();
+ m_buffer_barriers.clear();
+ }
};
class GpuImpl : public Gpu
@@ -303,8 +348,14 @@ class GpuImpl : public Gpu VkDevice m_device;
VkQueue m_queue;
+ struct Image
+ {
+ vulkan_sync::ImageState state;
+ VkImage image{};
+ };
+
asl::option<VkSwapchainKHR> m_swapchain;
- asl::buffer<VkImage> m_swapchain_images;
+ asl::buffer<Image> m_swapchain_images;
VkSemaphore m_swapchain_image_acquire_semaphore = VK_NULL_HANDLE;
VkSemaphore m_queue_complete_semaphore = VK_NULL_HANDLE;
@@ -312,6 +363,8 @@ class GpuImpl : public Gpu asl::GlobalHeap m_allocator; // @Todo Make this configurable
asl::IntrusiveList<FrameResources> m_in_flight_frames;
+ DependencyInfoBuilder m_dependency_builder;
+
public:
GpuImpl(
VkInstance instance,
@@ -451,11 +504,24 @@ public: m_swapchain = swapchain;
vkGetSwapchainImagesKHR(m_device, m_swapchain.value(), &count, nullptr);
- m_swapchain_images.resize_zero(count);
- res = vkGetSwapchainImagesKHR(m_device, m_swapchain.value(), &count, m_swapchain_images.data());
- if (res != VK_SUCCESS)
+
{
- return asl::runtime_error("Couldn't retrieve Vulkan swapchain images: {}", res);
+ m_swapchain_images.resize(count);
+
+ // @Todo Good candidate for temporary allocation
+ asl::buffer<VkImage> images;
+ images.resize_zero(count);
+
+ res = vkGetSwapchainImagesKHR(m_device, m_swapchain.value(), &count, images.data());
+ if (res != VK_SUCCESS)
+ {
+ return asl::runtime_error("Couldn't retrieve Vulkan swapchain images: {}", res);
+ }
+
+ for (int64_t i = 0; i < count; ++i)
+ {
+ m_swapchain_images[i].image = images[i];
+ }
}
ASL_LOG_INFO("Vulkan swapchain created ({}x{} with {} images)",
@@ -518,6 +584,8 @@ public: {
return asl::runtime_error("Couldn't acquire swapchain image: {}", res);
}
+
+ auto& swapchain_image = m_swapchain_images[image_index];
VkCommandPoolCreateInfo command_pool_create_info{
.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO,
@@ -561,40 +629,12 @@ public: return asl::runtime_error("Couldn't begin command buffer: {}", res);
}
- VkImageMemoryBarrier2 barrier1{
- .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2,
- .pNext = nullptr,
- .srcStageMask = VK_PIPELINE_STAGE_2_BOTTOM_OF_PIPE_BIT,
- .srcAccessMask = VK_ACCESS_2_NONE,
- .dstStageMask = VK_PIPELINE_STAGE_2_CLEAR_BIT,
- .dstAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT,
- .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED,
- .newLayout = VK_IMAGE_LAYOUT_GENERAL,
- .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
- .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
- .image = m_swapchain_images[image_index],
- .subresourceRange = {
- .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
- .baseMipLevel = 0,
- .levelCount = 1,
- .baseArrayLayer = 0,
- .layerCount = 1,
- },
- };
-
- VkDependencyInfo dependency_info1{
- .sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO,
- .pNext = nullptr,
- .dependencyFlags = 0,
- .memoryBarrierCount = 0,
- .pMemoryBarriers = nullptr,
- .bufferMemoryBarrierCount = 0,
- .pBufferMemoryBarriers = nullptr,
- .imageMemoryBarrierCount = 1,
- .pImageMemoryBarriers = &barrier1,
- };
-
- vkCmdPipelineBarrier2(command_buffer, &dependency_info1);
+ vulkan_sync::synchronize_resource(
+ swapchain_image.image,
+ VK_IMAGE_ASPECT_COLOR_BIT,
+ &swapchain_image.state,
+ vulkan_sync::Usage::kImageClear,
+ &m_dependency_builder);
VkClearColorValue clear_color{
.float32 = { 0.0F, 0.137F, 0.4F, 1.0F },
@@ -603,47 +643,23 @@ public: VkImageSubresourceRange range{
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
.baseMipLevel = 0,
- .levelCount = 1,
+ .levelCount = VK_REMAINING_MIP_LEVELS,
.baseArrayLayer = 0,
- .layerCount = 1,
+ .layerCount = VK_REMAINING_ARRAY_LAYERS,
};
- vkCmdClearColorImage(command_buffer, m_swapchain_images[image_index], VK_IMAGE_LAYOUT_GENERAL, &clear_color, 1, &range);
+ m_dependency_builder.apply(command_buffer);
+ vkCmdClearColorImage(command_buffer, swapchain_image.image, swapchain_image.state.current_layout, &clear_color, 1, &range);
- VkImageMemoryBarrier2 barrier2{
- .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2,
- .pNext = nullptr,
- .srcStageMask = VK_PIPELINE_STAGE_2_CLEAR_BIT,
- .srcAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT,
- .dstStageMask = VK_PIPELINE_STAGE_2_BOTTOM_OF_PIPE_BIT,
- .dstAccessMask = VK_ACCESS_2_NONE,
- .oldLayout = VK_IMAGE_LAYOUT_GENERAL,
- .newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
- .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
- .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
- .image = m_swapchain_images[image_index],
- .subresourceRange = {
- .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
- .baseMipLevel = 0,
- .levelCount = 1,
- .baseArrayLayer = 0,
- .layerCount = 1,
- },
- };
+ vulkan_sync::synchronize_resource(
+ swapchain_image.image,
+ VK_IMAGE_ASPECT_COLOR_BIT,
+ &swapchain_image.state,
+ vulkan_sync::Usage::kImagePresent,
+ &m_dependency_builder);
- VkDependencyInfo dependency_info2{
- .sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO,
- .pNext = nullptr,
- .dependencyFlags = 0,
- .memoryBarrierCount = 0,
- .pMemoryBarriers = nullptr,
- .bufferMemoryBarrierCount = 0,
- .pBufferMemoryBarriers = nullptr,
- .imageMemoryBarrierCount = 1,
- .pImageMemoryBarriers = &barrier2,
- };
+ m_dependency_builder.apply(command_buffer);
- vkCmdPipelineBarrier2(command_buffer, &dependency_info2);
res = vkEndCommandBuffer(command_buffer);
if (res != VK_SUCCESS)
diff --git a/hk21/vulkan/BUILD.bazel b/hk21/vulkan/BUILD.bazel index 25cc0d6..3bbac5a 100644 --- a/hk21/vulkan/BUILD.bazel +++ b/hk21/vulkan/BUILD.bazel @@ -1,16 +1,3 @@ # Copyright 2025 Steven Le Rouzic
#
# SPDX-License-Identifier: BSD-3-Clause
-
-cc_library(
- name = "vulkan",
- hdrs = [
- "vulkan.hpp",
- ],
- deps = [
- "//vendor/vulkan",
- "@asl//asl/base",
- ],
- visibility = ["//:__subpackages__"],
- applicable_licenses = ["//:license"],
-)
diff --git a/hk21/vulkan/loader/BUILD.bazel b/hk21/vulkan/loader/BUILD.bazel index 32e8f03..db49ee6 100644 --- a/hk21/vulkan/loader/BUILD.bazel +++ b/hk21/vulkan/loader/BUILD.bazel @@ -12,7 +12,7 @@ cc_library( "fns.hpp",
],
deps = [
- "//hk21/vulkan",
+ "//vendor/vulkan",
"@asl//asl/base",
"@asl//asl/types:status",
],
diff --git a/hk21/vulkan/loader/loader.hpp b/hk21/vulkan/loader/loader.hpp index e1147f4..3461c99 100644 --- a/hk21/vulkan/loader/loader.hpp +++ b/hk21/vulkan/loader/loader.hpp @@ -7,7 +7,8 @@ #include <asl/base/integers.hpp>
#include <asl/types/status.hpp>
-#include "hk21/vulkan/vulkan.hpp"
+#include <vulkan.h>
+
#include "hk21/vulkan/loader/fns.hpp"
#define FN(NAME) extern PFN_##NAME NAME;
diff --git a/hk21/vulkan/sync/BUILD.bazel b/hk21/vulkan/sync/BUILD.bazel new file mode 100644 index 0000000..fc74cad --- /dev/null +++ b/hk21/vulkan/sync/BUILD.bazel @@ -0,0 +1,33 @@ +# Copyright 2025 Steven Le Rouzic
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+cc_library(
+ name = "sync",
+ hdrs = [
+ "sync.hpp",
+ ],
+ srcs = [
+ "sync.cpp",
+ ],
+ deps = [
+ "//vendor/vulkan",
+ "@asl//asl/base",
+ "@asl//asl/types:option",
+ "@asl//asl/types:span",
+ ],
+ visibility = ["//:__subpackages__"],
+ applicable_licenses = ["//:license"],
+)
+
+cc_test(
+ name = "tests",
+ srcs = [
+ "sync_tests.cpp",
+ ],
+ deps = [
+ ":sync",
+ "@asl//asl/containers:buffer",
+ "@asl//asl/testing",
+ ],
+)
diff --git a/hk21/vulkan/sync/sync.cpp b/hk21/vulkan/sync/sync.cpp new file mode 100644 index 0000000..1c81ce3 --- /dev/null +++ b/hk21/vulkan/sync/sync.cpp @@ -0,0 +1,252 @@ +// Copyright 2025 Steven Le Rouzic +// +// SPDX-License-Identifier: BSD-3-Clause + +#include "hk21/vulkan/sync/sync.hpp" + +#include <asl/types/option.hpp> + +// All of this is largely inspired by nicegraf's synchronization utility. +// See https://github.com/nicebyte/nicegraf/blob/3a291433fdb4fd9cf38356f297ff1d851617f0f5/source/ngf-vk/impl.c#L2718 + +namespace +{ + +enum StageAccess : uint32_t +{ + kClearStageTransferWrite = 0x0000'0001U, + kFragmentStageShaderSampled = 0x0000'0002U, + kVertexStageShaderSampled = 0x0000'0004U, + kColorAttachmentWrite = 0x0000'0008U, +}; + +constexpr VkAccessFlags kWriteAccessMask = + VK_ACCESS_2_SHADER_WRITE_BIT + | VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT + | VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT + | VK_ACCESS_2_TRANSFER_WRITE_BIT + | VK_ACCESS_2_HOST_WRITE_BIT + | VK_ACCESS_2_MEMORY_WRITE_BIT + | VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT; + +struct UsageInfo +{ + uint32_t stage_access_mask{}; + VkAccessFlags2 access_flags{}; + VkPipelineStageFlags2 pipeline_stage_flags{}; + VkImageLayout image_layout{}; +}; + +const auto kUsageInfos = ([]() static { + using namespace vulkan_sync; + + static UsageInfo info[asl::to_underlying(Usage::kCount_)]{}; + + info[asl::to_underlying(Usage::kImageClear)] = UsageInfo{ + .stage_access_mask = StageAccess::kClearStageTransferWrite, + .access_flags = VK_ACCESS_2_TRANSFER_WRITE_BIT, + .pipeline_stage_flags = VK_PIPELINE_STAGE_2_CLEAR_BIT, + .image_layout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + }; + + info[asl::to_underlying(Usage::kImagePresent)] = UsageInfo{ + .stage_access_mask = 0, + .access_flags = VK_ACCESS_2_NONE, + .pipeline_stage_flags = VK_PIPELINE_STAGE_2_NONE, + .image_layout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, + }; + + info[asl::to_underlying(Usage::kImageSampledInFragmentShader)] = UsageInfo{ + .stage_access_mask = StageAccess::kFragmentStageShaderSampled, + .access_flags = VK_ACCESS_2_SHADER_SAMPLED_READ_BIT, + .pipeline_stage_flags = VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT, + .image_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + }; + + info[asl::to_underlying(Usage::kImageSampledInVertexShader)] = UsageInfo{ + .stage_access_mask = StageAccess::kVertexStageShaderSampled, + .access_flags = VK_ACCESS_2_SHADER_SAMPLED_READ_BIT, + .pipeline_stage_flags = VK_PIPELINE_STAGE_2_VERTEX_SHADER_BIT, + .image_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + }; + + info[asl::to_underlying(Usage::kImageColorWriteAttachment)] = UsageInfo{ + .stage_access_mask = StageAccess::kColorAttachmentWrite, + .access_flags = VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT, + .pipeline_stage_flags = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT, + .image_layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + }; + + return asl::span(info); +})(); + + + +// We use an image barrier as a common structure for image and buffer barriers. +// The only differences are the resource, the subresource range, and the image layouts. +// We just discard whatever we don't need and fill the more specific fields outside. +asl::option<VkImageMemoryBarrier2> synchronize_resource_( + vulkan_sync::ResourceState* state, + VkImageLayout* state_layout, + vulkan_sync::Usage new_usage) +{ + const UsageInfo& usage_info = kUsageInfos[asl::to_underlying(new_usage)]; + + const bool is_read_only_access = (usage_info.access_flags & kWriteAccessMask) == 0U; + + const bool needs_layout_transition = *state_layout != usage_info.image_layout; + + const bool needs_write = needs_layout_transition || !is_read_only_access; + + VkImageMemoryBarrier2 barrier{ + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, + .pNext = nullptr, + .srcStageMask = 0, + .srcAccessMask = 0, + .dstStageMask = 0, + .dstAccessMask = 0, + .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED, + .newLayout = VK_IMAGE_LAYOUT_UNDEFINED, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = VK_NULL_HANDLE, + .subresourceRange = {}, + }; + + if (needs_write) + { + barrier.srcStageMask |= asl::exchange(state->active_readers_pipeline_stage_mask, VK_PIPELINE_STAGE_2_NONE); + barrier.srcAccessMask |= asl::exchange(state->active_readers_access_mask, VK_ACCESS_2_NONE); + state->has_seen_last_write = 0; + + // If there was to read since last write, but there was a write, + // synchronize with last write instead. + if (barrier.srcStageMask == VK_PIPELINE_STAGE_2_NONE && + state->last_writer_pipeline_stage_mask != VK_PIPELINE_STAGE_2_NONE) + { + barrier.srcStageMask |= state->last_writer_pipeline_stage_mask; + barrier.srcAccessMask |= state->last_writer_access_mask; + } + + // Last write is now the new usage. + state->last_writer_pipeline_stage_mask = usage_info.pipeline_stage_flags; + state->last_writer_access_mask = usage_info.access_flags; + + // If this is a read-only that is considered a write (layout transition) + // we also record the reader info, because it acts as if this read has been + // synchronized. + if (is_read_only_access) + { + state->has_seen_last_write |= usage_info.stage_access_mask; + state->active_readers_access_mask |= usage_info.access_flags; + state->active_readers_pipeline_stage_mask |= usage_info.pipeline_stage_flags; + } + } + else + { + // If there was a previous write we need to synchronize with, and this + // access has not been synchronized with it yet, synchronize. + if (state->last_writer_pipeline_stage_mask != VK_PIPELINE_STAGE_2_NONE + && (state->has_seen_last_write & usage_info.stage_access_mask) != usage_info.stage_access_mask) + { + barrier.srcStageMask |= state->last_writer_pipeline_stage_mask; + barrier.srcAccessMask |= state->last_writer_access_mask; + } + + // Record this reader info. + state->has_seen_last_write |= usage_info.stage_access_mask; + state->active_readers_access_mask |= usage_info.access_flags; + state->active_readers_pipeline_stage_mask |= usage_info.pipeline_stage_flags; + } + + // If the barrier has been filled or we need a layout transition, emit a barrier. + if (barrier.srcStageMask != VK_PIPELINE_STAGE_2_NONE || needs_layout_transition) + { + barrier.dstStageMask |= usage_info.pipeline_stage_flags; + barrier.dstAccessMask |= usage_info.access_flags; + + if (barrier.dstStageMask == VK_PIPELINE_STAGE_2_NONE) + { + barrier.dstStageMask = VK_PIPELINE_STAGE_2_BOTTOM_OF_PIPE_BIT; + } + + if (barrier.srcStageMask == VK_PIPELINE_STAGE_2_NONE) + { + barrier.srcStageMask = VK_PIPELINE_STAGE_2_BOTTOM_OF_PIPE_BIT; + } + + if (needs_layout_transition) + { + barrier.oldLayout = asl::exchange(*state_layout, usage_info.image_layout); + barrier.newLayout = usage_info.image_layout; + } + + return barrier; + } + + return asl::nullopt; +} + +} // anonymous namespace + +namespace vulkan_sync +{ + +void synchronize_resource( + VkImage image, VkImageAspectFlags aspects, + ImageState* state, Usage new_usage, DependencyInfoBuilder* builder) +{ + const UsageInfo& usage_info = kUsageInfos[asl::to_underlying(new_usage)]; + ASL_ASSERT(usage_info.image_layout != VK_IMAGE_LAYOUT_UNDEFINED); + + auto barrier_opt = synchronize_resource_(state, &state->current_layout, new_usage); + if (barrier_opt.has_value()) + { + auto& barrier = barrier_opt.value(); + + barrier.image = image; + barrier.subresourceRange = { + .aspectMask = aspects, + .baseMipLevel = 0, + .levelCount = VK_REMAINING_MIP_LEVELS, + .baseArrayLayer = 0, + .layerCount = VK_REMAINING_ARRAY_LAYERS, + }; + + builder->add_image_barrier(barrier); + } +} + +void synchronize_resource( + VkBuffer buffer, BufferState* state, + Usage new_usage, DependencyInfoBuilder* builder) +{ + const UsageInfo& usage_info = kUsageInfos[asl::to_underlying(new_usage)]; + ASL_ASSERT(usage_info.image_layout == VK_IMAGE_LAYOUT_UNDEFINED); + + VkImageLayout dummy_layout = VK_IMAGE_LAYOUT_UNDEFINED; + auto barrier_opt = synchronize_resource_(state, &dummy_layout, new_usage); + if (barrier_opt.has_value()) + { + const auto& image_barrier = barrier_opt.value(); + + VkBufferMemoryBarrier2 barrier{ + .sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER_2, + .pNext = nullptr, + .srcStageMask = image_barrier.srcStageMask, + .srcAccessMask = image_barrier.srcAccessMask, + .dstStageMask = image_barrier.dstStageMask, + .dstAccessMask = image_barrier.dstAccessMask, + .srcQueueFamilyIndex = image_barrier.srcQueueFamilyIndex, + .dstQueueFamilyIndex = image_barrier.dstQueueFamilyIndex, + .buffer = buffer, + .offset = 0, + .size = VK_WHOLE_SIZE, + }; + + builder->add_buffer_barrier(barrier); + } +} + +} // namespace vulkan_sync + diff --git a/hk21/vulkan/sync/sync.hpp b/hk21/vulkan/sync/sync.hpp new file mode 100644 index 0000000..b89b625 --- /dev/null +++ b/hk21/vulkan/sync/sync.hpp @@ -0,0 +1,64 @@ +// Copyright 2025 Steven Le Rouzic +// +// SPDX-License-Identifier: BSD-3-Clause + +#pragma once + +#include <asl/base/integers.hpp> +#include <asl/base/utility.hpp> +#include <asl/types/span.hpp> +#include <vulkan.h> + +namespace vulkan_sync +{ + +class DependencyInfoBuilder +{ +public: + DependencyInfoBuilder() = default; + ASL_DEFAULT_COPY_MOVE(DependencyInfoBuilder); + virtual ~DependencyInfoBuilder() = default; + + virtual void add_image_barrier(const VkImageMemoryBarrier2&) = 0; + virtual void add_buffer_barrier(const VkBufferMemoryBarrier2&) = 0; +}; + +struct ResourceState +{ + VkAccessFlags2 last_writer_access_mask{}; + VkPipelineStageFlags2 last_writer_pipeline_stage_mask{}; + + VkAccessFlags2 active_readers_access_mask{}; + VkPipelineStageFlags2 active_readers_pipeline_stage_mask{}; + + // Which StageAccess-es have seen the previous write. + uint32_t has_seen_last_write{}; +}; + +struct BufferState : public ResourceState {}; + +struct ImageState : public ResourceState +{ + VkImageLayout current_layout = VK_IMAGE_LAYOUT_UNDEFINED; +}; + +enum class Usage : uint32_t +{ + kImageClear, + kImagePresent, + kImageSampledInFragmentShader, + kImageSampledInVertexShader, + kImageColorWriteAttachment, + + kCount_, +}; + +void synchronize_resource( + VkImage, VkImageAspectFlags, + ImageState*, Usage new_usage, DependencyInfoBuilder*); + +void synchronize_resource( + VkBuffer, BufferState*, + Usage new_usage, DependencyInfoBuilder*); + +} // namespace vulkan_sync diff --git a/hk21/vulkan/sync/sync_tests.cpp b/hk21/vulkan/sync/sync_tests.cpp new file mode 100644 index 0000000..ff6bfd0 --- /dev/null +++ b/hk21/vulkan/sync/sync_tests.cpp @@ -0,0 +1,162 @@ +// Copyright 2025 Steven Le Rouzic +// +// SPDX-License-Identifier: BSD-3-Clause + +#include "hk21/vulkan/sync/sync.hpp" + +#include <asl/containers/buffer.hpp> +#include <asl/testing/testing.hpp> + +class DependencyInfoBuilder : public vulkan_sync::DependencyInfoBuilder +{ + asl::buffer<VkImageMemoryBarrier2> m_image_barriers; + asl::buffer<VkBufferMemoryBarrier2> m_buffer_barriers; + +public: + void add_image_barrier(const VkImageMemoryBarrier2& barrier) override + { + m_image_barriers.push(barrier); + } + + void add_buffer_barrier(const VkBufferMemoryBarrier2& barrier) override + { + m_buffer_barriers.push(barrier); + } + + void reset() + { + m_image_barriers.clear(); + m_buffer_barriers.clear(); + } + + asl::span<const VkImageMemoryBarrier2> image_barriers() const + { + return m_image_barriers; + } + + asl::span<const VkBufferMemoryBarrier2> buffer_barriers() const + { + return m_buffer_barriers; + } +}; + +ASL_TEST(clear_and_present) +{ + DependencyInfoBuilder builder; + vulkan_sync::ImageState state{}; + + synchronize_resource(VK_NULL_HANDLE, {}, &state, vulkan_sync::Usage::kImageClear, &builder); + ASL_TEST_ASSERT(builder.buffer_barriers().size() == 0); + ASL_TEST_ASSERT(builder.image_barriers().size() == 1); + auto barrier = builder.image_barriers()[0]; + ASL_TEST_EXPECT(barrier.srcStageMask == VK_PIPELINE_STAGE_2_BOTTOM_OF_PIPE_BIT); + ASL_TEST_EXPECT(barrier.srcAccessMask == VK_ACCESS_2_NONE); + ASL_TEST_EXPECT(barrier.dstStageMask == VK_PIPELINE_STAGE_2_CLEAR_BIT); + ASL_TEST_EXPECT(barrier.dstAccessMask == VK_ACCESS_2_TRANSFER_WRITE_BIT); + ASL_TEST_EXPECT(barrier.oldLayout == VK_IMAGE_LAYOUT_UNDEFINED); + ASL_TEST_EXPECT(barrier.newLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + ASL_TEST_EXPECT(state.current_layout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + + builder.reset(); + synchronize_resource(VK_NULL_HANDLE, {}, &state, vulkan_sync::Usage::kImageClear, &builder); + ASL_TEST_ASSERT(builder.buffer_barriers().size() == 0); + ASL_TEST_ASSERT(builder.image_barriers().size() == 1); + barrier = builder.image_barriers()[0]; + ASL_TEST_EXPECT(barrier.srcStageMask == VK_PIPELINE_STAGE_2_CLEAR_BIT); + ASL_TEST_EXPECT(barrier.srcAccessMask == VK_ACCESS_2_TRANSFER_WRITE_BIT); + ASL_TEST_EXPECT(barrier.dstStageMask == VK_PIPELINE_STAGE_2_CLEAR_BIT); + ASL_TEST_EXPECT(barrier.dstAccessMask == VK_ACCESS_2_TRANSFER_WRITE_BIT); + ASL_TEST_EXPECT(barrier.oldLayout == VK_IMAGE_LAYOUT_UNDEFINED); + ASL_TEST_EXPECT(barrier.newLayout == VK_IMAGE_LAYOUT_UNDEFINED); + ASL_TEST_EXPECT(state.current_layout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + + builder.reset(); + synchronize_resource(VK_NULL_HANDLE, {}, &state, vulkan_sync::Usage::kImagePresent, &builder); + ASL_TEST_ASSERT(builder.buffer_barriers().size() == 0); + ASL_TEST_ASSERT(builder.image_barriers().size() == 1); + barrier = builder.image_barriers()[0]; + ASL_TEST_EXPECT(barrier.srcStageMask == VK_PIPELINE_STAGE_2_CLEAR_BIT); + ASL_TEST_EXPECT(barrier.srcAccessMask == VK_ACCESS_2_TRANSFER_WRITE_BIT); + ASL_TEST_EXPECT(barrier.dstStageMask == VK_PIPELINE_STAGE_2_BOTTOM_OF_PIPE_BIT); + ASL_TEST_EXPECT(barrier.dstAccessMask == VK_ACCESS_2_NONE); + ASL_TEST_EXPECT(barrier.oldLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + ASL_TEST_EXPECT(barrier.newLayout == VK_IMAGE_LAYOUT_PRESENT_SRC_KHR); + ASL_TEST_EXPECT(state.current_layout == VK_IMAGE_LAYOUT_PRESENT_SRC_KHR); + + builder.reset(); + synchronize_resource(VK_NULL_HANDLE, {}, &state, vulkan_sync::Usage::kImageClear, &builder); + ASL_TEST_ASSERT(builder.buffer_barriers().size() == 0); + ASL_TEST_ASSERT(builder.image_barriers().size() == 1); + barrier = builder.image_barriers()[0]; + ASL_TEST_EXPECT(barrier.srcStageMask == VK_PIPELINE_STAGE_2_BOTTOM_OF_PIPE_BIT); + ASL_TEST_EXPECT(barrier.srcAccessMask == VK_ACCESS_2_NONE); + ASL_TEST_EXPECT(barrier.dstStageMask == VK_PIPELINE_STAGE_2_CLEAR_BIT); + ASL_TEST_EXPECT(barrier.dstAccessMask == VK_ACCESS_2_TRANSFER_WRITE_BIT); + ASL_TEST_EXPECT(barrier.oldLayout == VK_IMAGE_LAYOUT_PRESENT_SRC_KHR); + ASL_TEST_EXPECT(barrier.newLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + ASL_TEST_EXPECT(state.current_layout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); +} + +ASL_TEST(clear_and_draw) +{ + DependencyInfoBuilder builder; + vulkan_sync::ImageState state{}; + + synchronize_resource(VK_NULL_HANDLE, {}, &state, vulkan_sync::Usage::kImageColorWriteAttachment, &builder); + ASL_TEST_ASSERT(builder.buffer_barriers().size() == 0); + ASL_TEST_ASSERT(builder.image_barriers().size() == 1); + auto barrier = builder.image_barriers()[0]; + ASL_TEST_EXPECT(barrier.srcStageMask == VK_PIPELINE_STAGE_2_BOTTOM_OF_PIPE_BIT); + ASL_TEST_EXPECT(barrier.srcAccessMask == VK_ACCESS_2_NONE); + ASL_TEST_EXPECT(barrier.dstStageMask == VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT); + ASL_TEST_EXPECT(barrier.dstAccessMask == VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT); + ASL_TEST_EXPECT(barrier.oldLayout == VK_IMAGE_LAYOUT_UNDEFINED); + ASL_TEST_EXPECT(barrier.newLayout == VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); + ASL_TEST_EXPECT(state.current_layout == VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); + + builder.reset(); + synchronize_resource(VK_NULL_HANDLE, {}, &state, vulkan_sync::Usage::kImageSampledInVertexShader, &builder); + ASL_TEST_ASSERT(builder.buffer_barriers().size() == 0); + ASL_TEST_ASSERT(builder.image_barriers().size() == 1); + barrier = builder.image_barriers()[0]; + ASL_TEST_EXPECT(barrier.srcStageMask == VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT); + ASL_TEST_EXPECT(barrier.srcAccessMask == VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT); + ASL_TEST_EXPECT(barrier.dstStageMask == VK_PIPELINE_STAGE_2_VERTEX_SHADER_BIT); + ASL_TEST_EXPECT(barrier.dstAccessMask == VK_ACCESS_2_SHADER_SAMPLED_READ_BIT); + ASL_TEST_EXPECT(barrier.oldLayout == VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); + ASL_TEST_EXPECT(barrier.newLayout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + ASL_TEST_EXPECT(state.current_layout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + + builder.reset(); + synchronize_resource(VK_NULL_HANDLE, {}, &state, vulkan_sync::Usage::kImageSampledInFragmentShader, &builder); + ASL_TEST_ASSERT(builder.buffer_barriers().size() == 0); + ASL_TEST_ASSERT(builder.image_barriers().size() == 1); + barrier = builder.image_barriers()[0]; + ASL_TEST_EXPECT(barrier.srcStageMask == VK_PIPELINE_STAGE_2_VERTEX_SHADER_BIT); + ASL_TEST_EXPECT(barrier.srcAccessMask == VK_ACCESS_2_SHADER_SAMPLED_READ_BIT); + ASL_TEST_EXPECT(barrier.dstStageMask == VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT); + ASL_TEST_EXPECT(barrier.dstAccessMask == VK_ACCESS_2_SHADER_SAMPLED_READ_BIT); + ASL_TEST_EXPECT(barrier.oldLayout == VK_IMAGE_LAYOUT_UNDEFINED); + ASL_TEST_EXPECT(barrier.newLayout == VK_IMAGE_LAYOUT_UNDEFINED); + ASL_TEST_EXPECT(state.current_layout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + + builder.reset(); + synchronize_resource(VK_NULL_HANDLE, {}, &state, vulkan_sync::Usage::kImageSampledInVertexShader, &builder); + ASL_TEST_ASSERT(builder.buffer_barriers().size() == 0); + ASL_TEST_ASSERT(builder.image_barriers().size() == 0); + ASL_TEST_EXPECT(state.current_layout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + + builder.reset(); + synchronize_resource(VK_NULL_HANDLE, {}, &state, vulkan_sync::Usage::kImageClear, &builder); + ASL_TEST_ASSERT(builder.buffer_barriers().size() == 0); + ASL_TEST_ASSERT(builder.image_barriers().size() == 1); + barrier = builder.image_barriers()[0]; + ASL_TEST_EXPECT(barrier.srcStageMask == (VK_PIPELINE_STAGE_2_VERTEX_SHADER_BIT | VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT)); + ASL_TEST_EXPECT(barrier.srcAccessMask == VK_ACCESS_2_SHADER_SAMPLED_READ_BIT); + ASL_TEST_EXPECT(barrier.dstStageMask == VK_PIPELINE_STAGE_2_CLEAR_BIT); + ASL_TEST_EXPECT(barrier.dstAccessMask == VK_ACCESS_2_TRANSFER_WRITE_BIT); + ASL_TEST_EXPECT(barrier.oldLayout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + ASL_TEST_EXPECT(barrier.newLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + ASL_TEST_EXPECT(state.current_layout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); +} + diff --git a/hk21/vulkan/vulkan.hpp b/hk21/vulkan/vulkan.hpp deleted file mode 100644 index 0e3242b..0000000 --- a/hk21/vulkan/vulkan.hpp +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2025 Steven Le Rouzic
-//
-// SPDX-License-Identifier: BSD-3-Clause
-
-#pragma once
-
-#include <asl/base/integers.hpp>
-
-#define VK_NO_STDDEF_H
-#define VK_NO_STDINT_H
-#define VK_NO_PROTOTYPES
-#include <vulkan.h>
|