summaryrefslogtreecommitdiff
path: root/hk21/vulkan
diff options
context:
space:
mode:
Diffstat (limited to 'hk21/vulkan')
-rw-r--r--hk21/vulkan/BUILD.bazel13
-rw-r--r--hk21/vulkan/loader/BUILD.bazel2
-rw-r--r--hk21/vulkan/loader/loader.hpp3
-rw-r--r--hk21/vulkan/sync/BUILD.bazel33
-rw-r--r--hk21/vulkan/sync/sync.cpp252
-rw-r--r--hk21/vulkan/sync/sync.hpp64
-rw-r--r--hk21/vulkan/sync/sync_tests.cpp162
-rw-r--r--hk21/vulkan/vulkan.hpp12
8 files changed, 514 insertions, 27 deletions
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>