From 2ffbd38a7dfd552311266606c754fe916815222c Mon Sep 17 00:00:00 2001 From: joe chu Date: Fri, 13 Nov 2026 02:24:45 -0800 Subject: [PATCH] feature(vulkan): back transient attachments with lazily allocated memory Co-Authored-By: Claude Opus 5.5 --- examples/vulkan/CarControlDemo/main.cpp | 5 +- examples/vulkan/FPSShooterDemo/main.cpp | 5 +- examples/vulkan/MultiSamplingDemo/main.cpp | 6 +- examples/vulkan/PhysicsDemo/main.cpp | 5 +- tests/vulkan/CMakeLists.txt | 3 + tests/vulkan/TransientAttachment.frag | 10 + tests/vulkan/TransientAttachment.vert | 11 + .../vulkan/VulkanTransientAttachmentTest.cpp | 385 ++++++++++++++++++ .../TransientAttachmentBenchmark.cpp | 239 +++++++++++ vulkan/include/VulkanImage.h | 3 + vulkan/src/VulkanImage.cpp | 23 +- vulkan/src/VulkanSwapChain.cpp | 4 +- 12 files changed, 687 insertions(+), 12 deletions(-) create mode 100644 tests/vulkan/TransientAttachment.frag create mode 100644 tests/vulkan/TransientAttachment.vert create mode 100644 tests/vulkan/VulkanTransientAttachmentTest.cpp create mode 100644 tests/vulkan/benchmarks/TransientAttachmentBenchmark.cpp diff --git a/examples/vulkan/CarControlDemo/main.cpp b/examples/vulkan/CarControlDemo/main.cpp index df7e778..2fa7e2f 100644 --- a/examples/vulkan/CarControlDemo/main.cpp +++ b/examples/vulkan/CarControlDemo/main.cpp @@ -91,8 +91,9 @@ int main() { // Manually-managed depth image for dynamic rendering. core::vulkan::VulkanImage depth_image( &context, swap_chain->swapchain_extent.width, swap_chain->swapchain_extent.height, - kDepthFormat, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, VK_IMAGE_ASPECT_DEPTH_BIT, - VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, VK_IMAGE_TILING_OPTIMAL); + kDepthFormat, + VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT | VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, + VK_IMAGE_ASPECT_DEPTH_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, VK_IMAGE_TILING_OPTIMAL); depth_image.TransitionDepthImageLayout( VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, kDepthFormat); diff --git a/examples/vulkan/FPSShooterDemo/main.cpp b/examples/vulkan/FPSShooterDemo/main.cpp index 42eebfb..db4d9a2 100644 --- a/examples/vulkan/FPSShooterDemo/main.cpp +++ b/examples/vulkan/FPSShooterDemo/main.cpp @@ -95,8 +95,9 @@ int main() { // Manually-managed depth image for the dynamic rendering path. core::vulkan::VulkanImage depth_image( &context, swap_chain->swapchain_extent.width, swap_chain->swapchain_extent.height, - kDepthFormat, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, VK_IMAGE_ASPECT_DEPTH_BIT, - VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, VK_IMAGE_TILING_OPTIMAL); + kDepthFormat, + VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT | VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, + VK_IMAGE_ASPECT_DEPTH_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, VK_IMAGE_TILING_OPTIMAL); depth_image.TransitionDepthImageLayout( VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, kDepthFormat); diff --git a/examples/vulkan/MultiSamplingDemo/main.cpp b/examples/vulkan/MultiSamplingDemo/main.cpp index e136b6e..8d9a6be 100644 --- a/examples/vulkan/MultiSamplingDemo/main.cpp +++ b/examples/vulkan/MultiSamplingDemo/main.cpp @@ -60,8 +60,10 @@ int main() { core::vulkan::VulkanImage depth_image( &context, swap_chain->swapchain_extent.width, swap_chain->swapchain_extent.height, - kDepthFormat, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, VK_IMAGE_ASPECT_DEPTH_BIT, - VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, VK_IMAGE_TILING_OPTIMAL, 1, msaa_samples); + kDepthFormat, + VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT | VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, + VK_IMAGE_ASPECT_DEPTH_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, VK_IMAGE_TILING_OPTIMAL, 1, + msaa_samples); depth_image.TransitionDepthImageLayout( VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, kDepthFormat); diff --git a/examples/vulkan/PhysicsDemo/main.cpp b/examples/vulkan/PhysicsDemo/main.cpp index 9655033..2674305 100644 --- a/examples/vulkan/PhysicsDemo/main.cpp +++ b/examples/vulkan/PhysicsDemo/main.cpp @@ -79,8 +79,9 @@ int main() { // rendering we own the depth attachment here. core::vulkan::VulkanImage depth_image( &context, swap_chain->swapchain_extent.width, swap_chain->swapchain_extent.height, - kDepthFormat, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, VK_IMAGE_ASPECT_DEPTH_BIT, - VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, VK_IMAGE_TILING_OPTIMAL); + kDepthFormat, + VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT | VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, + VK_IMAGE_ASPECT_DEPTH_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, VK_IMAGE_TILING_OPTIMAL); depth_image.TransitionDepthImageLayout( VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, kDepthFormat); diff --git a/tests/vulkan/CMakeLists.txt b/tests/vulkan/CMakeLists.txt index b5bf10e..1f5efe7 100644 --- a/tests/vulkan/CMakeLists.txt +++ b/tests/vulkan/CMakeLists.txt @@ -48,3 +48,6 @@ target_include_directories(vulkan_gaussian_benchmark PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/shaders) add_dependencies(vulkan_gaussian_benchmark vulkan_shaders) target_link_libraries(vulkan_gaussian_benchmark PRIVATE mat vulkan) + +add_executable(vulkan_transient_attachment_benchmark benchmarks/TransientAttachmentBenchmark.cpp) +target_link_libraries(vulkan_transient_attachment_benchmark PRIVATE vulkan) diff --git a/tests/vulkan/TransientAttachment.frag b/tests/vulkan/TransientAttachment.frag new file mode 100644 index 0000000..e201266 --- /dev/null +++ b/tests/vulkan/TransientAttachment.frag @@ -0,0 +1,10 @@ +#version 450 + +layout(push_constant) uniform Draw { + vec4 color; + float depth; +} draw; + +layout(location = 0) out vec4 out_color; + +void main() { out_color = draw.color; } diff --git a/tests/vulkan/TransientAttachment.vert b/tests/vulkan/TransientAttachment.vert new file mode 100644 index 0000000..cc629ef --- /dev/null +++ b/tests/vulkan/TransientAttachment.vert @@ -0,0 +1,11 @@ +#version 450 + +layout(push_constant) uniform Draw { + vec4 color; + float depth; +} draw; + +void main() { + const vec2 positions[3] = vec2[](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0)); + gl_Position = vec4(positions[gl_VertexIndex], draw.depth, 1.0); +} diff --git a/tests/vulkan/VulkanTransientAttachmentTest.cpp b/tests/vulkan/VulkanTransientAttachmentTest.cpp new file mode 100644 index 0000000..6c63917 --- /dev/null +++ b/tests/vulkan/VulkanTransientAttachmentTest.cpp @@ -0,0 +1,385 @@ +#include + +#include +#include +#include +#include +#include + +#include "VulkanBuffer.h" +#include "VulkanCommandBuffer.h" +#include "VulkanContext.h" +#include "VulkanDynamicRendering.h" +#include "VulkanImage.h" +#include "VulkanSync.h" + +namespace core { +namespace test { +namespace { + +constexpr VkFormat kColorFormat = VK_FORMAT_R8G8B8A8_UNORM; +constexpr VkFormat kDepthFormat = VK_FORMAT_D16_UNORM; +constexpr VkSampleCountFlagBits kSamples = VK_SAMPLE_COUNT_4_BIT; +constexpr uint32_t kExtent = 32; +constexpr uint32_t kRed = 0xff0000ffU; +constexpr uint32_t kGreen = 0xff00ff00U; +constexpr uint32_t kBlue = 0xffff0000U; + +struct DrawConstants { + float color[4]; + float depth; +}; + +struct GraphicsPipeline { + VkDevice device = VK_NULL_HANDLE; + VkShaderModule vertex = VK_NULL_HANDLE; + VkShaderModule fragment = VK_NULL_HANDLE; + VkPipelineLayout layout = VK_NULL_HANDLE; + VkPipeline pipeline = VK_NULL_HANDLE; + + ~GraphicsPipeline() { + vkDestroyPipeline(device, pipeline, nullptr); + vkDestroyPipelineLayout(device, layout, nullptr); + vkDestroyShaderModule(device, fragment, nullptr); + vkDestroyShaderModule(device, vertex, nullptr); + } +}; + +class VulkanTransientAttachmentTest : public testing::Test { + protected: + void SetUp() override { context_.Init(); } + + bool SupportsGraphics() const { + uint32_t count = 0; + vkGetPhysicalDeviceQueueFamilyProperties(context_.physical_device, &count, nullptr); + std::vector families(count); + vkGetPhysicalDeviceQueueFamilyProperties(context_.physical_device, &count, families.data()); + const uint32_t family = context_.GetQueueFamilyIndices().compute_family.value(); + return (families[family].queueFlags & VK_QUEUE_GRAPHICS_BIT) != 0U; + } + + bool HasLazyMemoryType(const vulkan::VulkanImage& image) const { + VkMemoryRequirements requirements{}; + vkGetImageMemoryRequirements(context_.logical_device, image.image, &requirements); + VkPhysicalDeviceMemoryProperties memory{}; + vkGetPhysicalDeviceMemoryProperties(context_.physical_device, &memory); + const VkMemoryPropertyFlags lazy = + VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT; + for (uint32_t i = 0; i < memory.memoryTypeCount; ++i) { + if ((requirements.memoryTypeBits & (1U << i)) != 0U && + (memory.memoryTypes[i].propertyFlags & lazy) == lazy) { + return true; + } + } + return false; + } + + vulkan::VulkanImage CreateColor(VkImageUsageFlags usage) { + return vulkan::VulkanImage(&context_, kExtent, kExtent, kColorFormat, usage, + VK_IMAGE_ASPECT_COLOR_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, + VK_IMAGE_TILING_OPTIMAL, 1, kSamples); + } + + vulkan::VulkanImage CreateDepth(VkImageUsageFlags usage) { + return vulkan::VulkanImage(&context_, kExtent, kExtent, kDepthFormat, usage, + VK_IMAGE_ASPECT_DEPTH_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, + VK_IMAGE_TILING_OPTIMAL, 1, kSamples); + } + + void ExpectMemory(const vulkan::VulkanImage& image, bool transient) { + const VkMemoryPropertyFlags properties = image.memory_properties(); + const bool lazy = (properties & VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT) != 0U; + EXPECT_NE(properties & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, 0U); + EXPECT_EQ(lazy, transient && HasLazyMemoryType(image)); + VkDeviceSize committed = 0; + if (lazy) { + vkGetDeviceMemoryCommitment(context_.logical_device, image.image_memory, &committed); + } + std::printf("transient=%d lazy=%d properties=0x%x committed=%llu\n", transient ? 1 : 0, + lazy ? 1 : 0, static_cast(properties), + static_cast(committed)); + } + + void CreatePipeline(GraphicsPipeline& pipeline) { + static const std::vector vertex_code = +#include "TransientAttachment.vert.spv" + ; + static const std::vector fragment_code = +#include "TransientAttachment.frag.spv" + ; + pipeline.device = context_.logical_device; + const VkShaderModuleCreateInfo vertex_info{ + .sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .codeSize = vertex_code.size() * sizeof(uint32_t), + .pCode = vertex_code.data(), + }; + ASSERT_EQ(vkCreateShaderModule(pipeline.device, &vertex_info, nullptr, &pipeline.vertex), + VK_SUCCESS); + const VkShaderModuleCreateInfo fragment_info{ + .sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .codeSize = fragment_code.size() * sizeof(uint32_t), + .pCode = fragment_code.data(), + }; + ASSERT_EQ(vkCreateShaderModule(pipeline.device, &fragment_info, nullptr, &pipeline.fragment), + VK_SUCCESS); + + const VkPushConstantRange range{ + .stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, + .offset = 0, + .size = sizeof(DrawConstants), + }; + const VkPipelineLayoutCreateInfo layout_info{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .setLayoutCount = 0, + .pSetLayouts = nullptr, + .pushConstantRangeCount = 1, + .pPushConstantRanges = &range, + }; + ASSERT_EQ(vkCreatePipelineLayout(pipeline.device, &layout_info, nullptr, &pipeline.layout), + VK_SUCCESS); + + const VkPipelineShaderStageCreateInfo stages[2] = { + {.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .stage = VK_SHADER_STAGE_VERTEX_BIT, + .module = pipeline.vertex, + .pName = "main", + .pSpecializationInfo = nullptr}, + {.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .stage = VK_SHADER_STAGE_FRAGMENT_BIT, + .module = pipeline.fragment, + .pName = "main", + .pSpecializationInfo = nullptr}, + }; + VkPipelineVertexInputStateCreateInfo vertex_input{}; + vertex_input.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO; + VkPipelineInputAssemblyStateCreateInfo input_assembly{}; + input_assembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; + input_assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + VkPipelineViewportStateCreateInfo viewport{}; + viewport.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; + viewport.viewportCount = 1; + viewport.scissorCount = 1; + VkPipelineRasterizationStateCreateInfo rasterization{}; + rasterization.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; + rasterization.polygonMode = VK_POLYGON_MODE_FILL; + rasterization.cullMode = VK_CULL_MODE_NONE; + rasterization.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + rasterization.lineWidth = 1.0f; + VkPipelineMultisampleStateCreateInfo multisample{}; + multisample.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; + multisample.rasterizationSamples = kSamples; + VkPipelineDepthStencilStateCreateInfo depth_stencil{}; + depth_stencil.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO; + depth_stencil.depthTestEnable = VK_TRUE; + depth_stencil.depthWriteEnable = VK_TRUE; + depth_stencil.depthCompareOp = VK_COMPARE_OP_LESS; + VkPipelineColorBlendAttachmentState blend_attachment{}; + blend_attachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | + VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; + VkPipelineColorBlendStateCreateInfo blend{}; + blend.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; + blend.attachmentCount = 1; + blend.pAttachments = &blend_attachment; + const VkDynamicState dynamic_states[2] = {VK_DYNAMIC_STATE_VIEWPORT, + VK_DYNAMIC_STATE_SCISSOR}; + VkPipelineDynamicStateCreateInfo dynamic{}; + dynamic.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO; + dynamic.dynamicStateCount = 2; + dynamic.pDynamicStates = dynamic_states; + const VkPipelineRenderingCreateInfo rendering{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO, + .pNext = nullptr, + .viewMask = 0, + .colorAttachmentCount = 1, + .pColorAttachmentFormats = &kColorFormat, + .depthAttachmentFormat = kDepthFormat, + .stencilAttachmentFormat = VK_FORMAT_UNDEFINED, + }; + VkGraphicsPipelineCreateInfo pipeline_info{}; + pipeline_info.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; + pipeline_info.pNext = &rendering; + pipeline_info.stageCount = 2; + pipeline_info.pStages = stages; + pipeline_info.pVertexInputState = &vertex_input; + pipeline_info.pInputAssemblyState = &input_assembly; + pipeline_info.pViewportState = &viewport; + pipeline_info.pRasterizationState = &rasterization; + pipeline_info.pMultisampleState = &multisample; + pipeline_info.pDepthStencilState = &depth_stencil; + pipeline_info.pColorBlendState = &blend; + pipeline_info.pDynamicState = &dynamic; + pipeline_info.layout = pipeline.layout; + ASSERT_EQ(vkCreateGraphicsPipelines(pipeline.device, VK_NULL_HANDLE, 1, &pipeline_info, + nullptr, &pipeline.pipeline), + VK_SUCCESS); + } + + void Draw(VkCommandBuffer command, const GraphicsPipeline& pipeline, uint32_t color, + float depth, uint32_t width) { + const DrawConstants constants{ + .color = {static_cast(color & 0xffU) / 255.0f, + static_cast((color >> 8U) & 0xffU) / 255.0f, + static_cast((color >> 16U) & 0xffU) / 255.0f, 1.0f}, + .depth = depth, + }; + const VkRect2D scissor{.offset = {.x = 0, .y = 0}, + .extent = {.width = width, .height = kExtent}}; + vkCmdSetScissor(command, 0, 1, &scissor); + vkCmdPushConstants(command, pipeline.layout, + VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, + sizeof(DrawConstants), &constants); + vkCmdDraw(command, 3, 1, 0, 0); + } + + template + std::vector RenderFrame(const GraphicsPipeline& pipeline, + const vulkan::VulkanDynamicRendering& rendering, + vulkan::VulkanImage& color, vulkan::VulkanImage& depth, + vulkan::VulkanImage& resolve, Record record) { + const VkDeviceSize bytes = static_cast(kExtent) * kExtent * sizeof(uint32_t); + vulkan::VulkanBuffer readback( + &context_, bytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); + const vulkan::VulkanCommandBuffer command = + vulkan::VulkanCommandBuffer::BeginOneTimeCommands(&context_); + resolve.TransitionImageLayout(command.buffer(), VK_IMAGE_LAYOUT_UNDEFINED, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, kColorFormat); + const VkClearValue color_clear{.color = {.float32 = {0.0f, 0.0f, 0.0f, 1.0f}}}; + const VkClearValue depth_clear{.depthStencil = {.depth = 1.0f, .stencil = 0}}; + const VkExtent2D extent{.width = kExtent, .height = kExtent}; + rendering.BeginDynamicRendering(command.buffer(), color.image_view, resolve.image_view, extent, + color_clear, depth.image_view, depth_clear); + vkCmdBindPipeline(command.buffer(), VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline.pipeline); + const VkViewport viewport{.x = 0.0f, + .y = 0.0f, + .width = static_cast(kExtent), + .height = static_cast(kExtent), + .minDepth = 0.0f, + .maxDepth = 1.0f}; + vkCmdSetViewport(command.buffer(), 0, 1, &viewport); + record(command.buffer()); + rendering.EndDynamicRendering(command.buffer()); + + VkImageMemoryBarrier barrier{}; + barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + barrier.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + barrier.oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = resolve.image; + barrier.subresourceRange = {.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1}; + vkCmdPipelineBarrier(command.buffer(), VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); + VkBufferImageCopy region{}; + region.imageSubresource = {.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .mipLevel = 0, + .baseArrayLayer = 0, + .layerCount = 1}; + region.imageExtent = {.width = kExtent, .height = kExtent, .depth = 1}; + vkCmdCopyImageToBuffer(command.buffer(), resolve.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + readback.buffer, 1, ®ion); + const VkMemoryBarrier host_barrier{ + .sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER, + .pNext = nullptr, + .srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT, + .dstAccessMask = VK_ACCESS_HOST_READ_BIT, + }; + vkCmdPipelineBarrier(command.buffer(), VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_HOST_BIT, 0, 1, &host_barrier, 0, nullptr, 0, nullptr); + + vulkan::VulkanFence fence(&context_); + fence.Reset(); + command.Submit(fence.fence); + EXPECT_EQ(vkWaitForFences(context_.logical_device, 1, &fence.fence, VK_TRUE, UINT64_MAX), + VK_SUCCESS); + std::vector pixels(static_cast(kExtent) * kExtent); + readback.MapData([&](void* data) { std::memcpy(pixels.data(), data, bytes); }); + return pixels; + } + + vulkan::VulkanContext context_{true}; +}; + +TEST_F(VulkanTransientAttachmentTest, TransientAttachmentsPreferLazilyAllocatedMemory) { + vulkan::VulkanImage color = + CreateColor(VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT); + vulkan::VulkanImage depth = CreateDepth(VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT | + VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT); + ExpectMemory(color, true); + ExpectMemory(depth, true); + + const VkMemoryPropertyFlags properties = color.memory_properties(); + vulkan::VulkanImage moved; + moved = std::move(color); + EXPECT_EQ(moved.memory_properties(), properties); +} + +TEST_F(VulkanTransientAttachmentTest, PersistentImagesKeepRequestedMemory) { + vulkan::VulkanImage color = + CreateColor(VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT); + vulkan::VulkanImage depth = CreateDepth(VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT); + ExpectMemory(color, false); + ExpectMemory(depth, false); +} + +TEST_F(VulkanTransientAttachmentTest, RendersThroughTransientMultisampledAttachments) { + if (!SupportsGraphics()) { + GTEST_SKIP() << "Selected queue family does not support graphics"; + } + vulkan::VulkanImage color = + CreateColor(VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT); + vulkan::VulkanImage depth = CreateDepth(VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT | + VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT); + vulkan::VulkanImage resolve( + &context_, kExtent, kExtent, kColorFormat, + VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT, + VK_IMAGE_ASPECT_COLOR_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + color.TransitionImageLayout(VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + kColorFormat); + depth.TransitionDepthImageLayout(VK_IMAGE_LAYOUT_UNDEFINED, + VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, kDepthFormat); + GraphicsPipeline pipeline; + ASSERT_NO_FATAL_FAILURE(CreatePipeline(pipeline)); + const vulkan::VulkanDynamicRendering rendering(&context_); + + const std::vector first = + RenderFrame(pipeline, rendering, color, depth, resolve, [&](VkCommandBuffer command) { + Draw(command, pipeline, kRed, 0.25f, kExtent); + Draw(command, pipeline, kGreen, 0.75f, kExtent); + Draw(command, pipeline, kBlue, 0.1f, kExtent / 2); + }); + for (uint32_t y = 0; y < kExtent; ++y) { + for (uint32_t x = 0; x < kExtent; ++x) { + ASSERT_EQ(first[static_cast(y) * kExtent + x], x < kExtent / 2 ? kBlue : kRed) + << "x=" << x << " y=" << y; + } + } + + const std::vector second = + RenderFrame(pipeline, rendering, color, depth, resolve, [&](VkCommandBuffer command) { + Draw(command, pipeline, kGreen, 0.9f, kExtent); + }); + for (size_t index = 0; index < second.size(); ++index) { + ASSERT_EQ(second[index], kGreen) << "index=" << index; + } +} + +} +} +} diff --git a/tests/vulkan/benchmarks/TransientAttachmentBenchmark.cpp b/tests/vulkan/benchmarks/TransientAttachmentBenchmark.cpp new file mode 100644 index 0000000..66613c8 --- /dev/null +++ b/tests/vulkan/benchmarks/TransientAttachmentBenchmark.cpp @@ -0,0 +1,239 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(__ANDROID__) +#include +#endif + +#include "VulkanCommandBuffer.h" +#include "VulkanContext.h" +#include "VulkanDynamicRendering.h" +#include "VulkanImage.h" +#include "VulkanSync.h" + +namespace { + +constexpr VkFormat kColorFormat = VK_FORMAT_R8G8B8A8_UNORM; +constexpr VkFormat kDepthFormat = VK_FORMAT_D32_SFLOAT; +constexpr VkSampleCountFlagBits kSamples = VK_SAMPLE_COUNT_4_BIT; +constexpr int kSets = 4; +constexpr int kRounds = 7; + +struct Sample { + int64_t allocated = 0; + int64_t usage = 0; +}; + +bool SupportsMemoryBudget(VkPhysicalDevice device) { + uint32_t count = 0; + vkEnumerateDeviceExtensionProperties(device, nullptr, &count, nullptr); + std::vector extensions(count); + vkEnumerateDeviceExtensionProperties(device, nullptr, &count, extensions.data()); + for (const VkExtensionProperties& extension : extensions) { + if (std::strcmp(extension.extensionName, VK_EXT_MEMORY_BUDGET_EXTENSION_NAME) == 0) { + return true; + } + } + return false; +} + +int64_t ProcessGpuMemory() { +#if defined(__ANDROID__) + FILE* pipe = popen("dumpsys gpu --gpumem", "r"); + if (pipe == nullptr) { + return -1; + } + const std::string prefix = "Proc " + std::to_string(getpid()) + " total: "; + int64_t total = -1; + char line[256]; + while (std::fgets(line, sizeof(line), pipe) != nullptr) { + if (std::strncmp(line, prefix.c_str(), prefix.size()) == 0) { + char* end = nullptr; + const long long value = std::strtoll(line + prefix.size(), &end, 10); + if (end != line + prefix.size() && value >= 0) { + total = static_cast(value); + } + } + } + if (pclose(pipe) != 0) { + return -1; + } + return total; +#else + return -1; +#endif +} + +int64_t DeviceMemoryUsage(VkPhysicalDevice device, bool budget_supported) { + if (!budget_supported) { + return ProcessGpuMemory(); + } + VkPhysicalDeviceMemoryBudgetPropertiesEXT budget{}; + budget.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT; + VkPhysicalDeviceMemoryProperties2 properties{}; + properties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PROPERTIES_2; + properties.pNext = &budget; + vkGetPhysicalDeviceMemoryProperties2(device, &properties); + int64_t usage = 0; + for (uint32_t i = 0; i < properties.memoryProperties.memoryHeapCount; ++i) { + if ((properties.memoryProperties.memoryHeaps[i].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) != + 0U) { + usage += static_cast(budget.heapUsage[i]); + } + } + return usage; +} + +Sample Measure(core::vulkan::VulkanContext& context, + const core::vulkan::VulkanDynamicRendering& rendering, + core::vulkan::VulkanImage& resolve, uint32_t width, uint32_t height, + bool transient, bool budget_supported) { + const VkImageUsageFlags transient_bit = transient ? VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT : 0U; + const int64_t usage_before = DeviceMemoryUsage(context.physical_device, budget_supported); + std::vector> images; + Sample sample{}; + for (int set = 0; set < kSets; ++set) { + images.push_back(std::make_unique( + &context, width, height, kColorFormat, + transient_bit | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, VK_IMAGE_ASPECT_COLOR_BIT, + VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, VK_IMAGE_TILING_OPTIMAL, 1, kSamples)); + core::vulkan::VulkanImage& color = *images.back(); + images.push_back(std::make_unique( + &context, width, height, kDepthFormat, + transient_bit | VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, VK_IMAGE_ASPECT_DEPTH_BIT, + VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, VK_IMAGE_TILING_OPTIMAL, 1, kSamples)); + core::vulkan::VulkanImage& depth = *images.back(); + for (const core::vulkan::VulkanImage* image : {&color, &depth}) { + VkMemoryRequirements requirements{}; + vkGetImageMemoryRequirements(context.logical_device, image->image, &requirements); + sample.allocated += static_cast(requirements.size); + } + + const core::vulkan::VulkanCommandBuffer command = + core::vulkan::VulkanCommandBuffer::BeginOneTimeCommands(&context); + color.TransitionImageLayout(command.buffer(), VK_IMAGE_LAYOUT_UNDEFINED, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, kColorFormat); + resolve.TransitionImageLayout(command.buffer(), VK_IMAGE_LAYOUT_UNDEFINED, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, kColorFormat); + VkImageMemoryBarrier barrier{}; + barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + barrier.srcAccessMask = 0; + barrier.dstAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + barrier.newLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = depth.image; + barrier.subresourceRange = {.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1}; + vkCmdPipelineBarrier(command.buffer(), VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT, 0, 0, nullptr, 0, nullptr, + 1, &barrier); + const VkClearValue color_clear{.color = {.float32 = {0.0f, 0.0f, 0.0f, 1.0f}}}; + const VkClearValue depth_clear{.depthStencil = {.depth = 1.0f, .stencil = 0}}; + rendering.BeginDynamicRendering(command.buffer(), color.image_view, resolve.image_view, + VkExtent2D{.width = width, .height = height}, color_clear, + depth.image_view, depth_clear); + rendering.EndDynamicRendering(command.buffer()); + core::vulkan::VulkanFence fence(&context); + fence.Reset(); + command.Submit(fence.fence); + VK_CHECK(vkWaitForFences(context.logical_device, 1, &fence.fence, VK_TRUE, UINT64_MAX)); + } + const int64_t usage_after = DeviceMemoryUsage(context.physical_device, budget_supported); + sample.usage = usage_before < 0 || usage_after < 0 ? -1 : usage_after - usage_before; + if (transient) { + for (const std::unique_ptr& image : images) { + if ((image->memory_properties() & VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT) == 0U) { + std::printf("warning: transient image did not receive lazily allocated memory\n"); + } + } + } + return sample; +} + +int64_t Median(std::vector values) { + std::sort(values.begin(), values.end()); + return values[values.size() / 2]; +} + +double MiB(int64_t bytes) { return static_cast(bytes) / (1024.0 * 1024.0); } + +} + +int main() { + core::vulkan::VulkanContext context(false); + context.Init(); + uint32_t family_count = 0; + vkGetPhysicalDeviceQueueFamilyProperties(context.physical_device, &family_count, nullptr); + std::vector families(family_count); + vkGetPhysicalDeviceQueueFamilyProperties(context.physical_device, &family_count, + families.data()); + const uint32_t family = context.GetQueueFamilyIndices().compute_family.value(); + if ((families[family].queueFlags & VK_QUEUE_GRAPHICS_BIT) == 0U) { + std::printf("unsupported: selected queue family does not support graphics\n"); + return 1; + } + for (const VkImageUsageFlags usage : + {static_cast(VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT), + static_cast(VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT | + VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT)}) { + VkImageFormatProperties format_properties{}; + if (vkGetPhysicalDeviceImageFormatProperties(context.physical_device, kDepthFormat, + VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage, + 0, &format_properties) != VK_SUCCESS || + (format_properties.sampleCounts & kSamples) == 0U) { + std::printf("unsupported: 4x D32 depth attachments are not available\n"); + return 1; + } + } + const bool budget_supported = SupportsMemoryBudget(context.physical_device); + const core::vulkan::VulkanDynamicRendering rendering(&context); + std::printf("sets=%d rounds=%d samples=4 color=RGBA8 depth=D32 usage_source=%s\n", kSets, + kRounds, budget_supported ? "VK_EXT_memory_budget" : "dumpsys_gpu_gpumem"); + + const VkExtent2D extents[3] = {{.width = 1280, .height = 720}, + {.width = 1920, .height = 1080}, + {.width = 2560, .height = 1440}}; + for (const VkExtent2D& extent : extents) { + core::vulkan::VulkanImage resolve( + &context, extent.width, extent.height, kColorFormat, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, + VK_IMAGE_ASPECT_COLOR_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + Measure(context, rendering, resolve, extent.width, extent.height, false, budget_supported); + Measure(context, rendering, resolve, extent.width, extent.height, true, budget_supported); + + std::vector usage[2]; + int64_t allocated[2] = {0, 0}; + for (int round = 0; round < kRounds; ++round) { + for (int order = 0; order < 2; ++order) { + const int mode = (round + order) % 2; + const Sample sample = Measure(context, rendering, resolve, extent.width, extent.height, + mode == 1, budget_supported); + usage[mode].push_back(sample.usage); + allocated[mode] = sample.allocated; + } + } + for (int mode = 0; mode < 2; ++mode) { + if (std::find(usage[mode].begin(), usage[mode].end(), -1) != usage[mode].end()) { + std::printf("%ux%u %s allocated=%.2fMiB usage unavailable\n", extent.width, + extent.height, mode == 1 ? "transient " : "persistent", MiB(allocated[mode])); + continue; + } + std::printf( + "%ux%u %s allocated=%.2fMiB usage median=%.2fMiB min=%.2f max=%.2f\n", extent.width, + extent.height, mode == 1 ? "transient " : "persistent", MiB(allocated[mode]), + MiB(Median(usage[mode])), MiB(*std::min_element(usage[mode].begin(), usage[mode].end())), + MiB(*std::max_element(usage[mode].begin(), usage[mode].end()))); + } + } + return 0; +} diff --git a/vulkan/include/VulkanImage.h b/vulkan/include/VulkanImage.h index 5c6a8da..da0a792 100644 --- a/vulkan/include/VulkanImage.h +++ b/vulkan/include/VulkanImage.h @@ -38,6 +38,8 @@ class VulkanImage { void GenerateMipmaps(VkCommandBuffer command_buffer); + VkMemoryPropertyFlags memory_properties() const { return memory_properties_; } + // void CreateTextureImage(const std::string& image_path); private: @@ -48,6 +50,7 @@ class VulkanImage { VkSampleCountFlagBits samples_ = VK_SAMPLE_COUNT_1_BIT; VkImageTiling tiling_ = VK_IMAGE_TILING_OPTIMAL; VkImageUsageFlags usage_ = 0; + VkMemoryPropertyFlags memory_properties_ = 0; public: uint32_t image_width; diff --git a/vulkan/src/VulkanImage.cpp b/vulkan/src/VulkanImage.cpp index bd77ef0..7f921cd 100644 --- a/vulkan/src/VulkanImage.cpp +++ b/vulkan/src/VulkanImage.cpp @@ -44,11 +44,29 @@ VulkanImage::VulkanImage(VulkanContext* context, const uint32_t width, const uin VkMemoryRequirements mem_requirements; vkGetImageMemoryRequirements(context_->logical_device, image, &mem_requirements); + VkPhysicalDeviceMemoryProperties device_memory{}; + vkGetPhysicalDeviceMemoryProperties(context_->physical_device, &device_memory); + uint32_t memory_type_index = device_memory.memoryTypeCount; + if ((usage & VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT) != 0U) { + const VkMemoryPropertyFlags lazy_properties = + properties | VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT; + for (uint32_t i = 0; i < device_memory.memoryTypeCount; ++i) { + if ((mem_requirements.memoryTypeBits & (1U << i)) != 0U && + (device_memory.memoryTypes[i].propertyFlags & lazy_properties) == lazy_properties) { + memory_type_index = i; + break; + } + } + } + if (memory_type_index == device_memory.memoryTypeCount) { + memory_type_index = context_->FindMemoryType(mem_requirements.memoryTypeBits, properties); + } + memory_properties_ = device_memory.memoryTypes[memory_type_index].propertyFlags; + VkMemoryAllocateInfo alloc_info{}; alloc_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; alloc_info.allocationSize = mem_requirements.size; - alloc_info.memoryTypeIndex = - context_->FindMemoryType(mem_requirements.memoryTypeBits, properties); + alloc_info.memoryTypeIndex = memory_type_index; VK_CHECK(vkAllocateMemory(context_->logical_device, &alloc_info, nullptr, &image_memory)); @@ -114,6 +132,7 @@ VulkanImage& VulkanImage::operator=(VulkanImage&& rhs) { samples_ = rhs.samples_; tiling_ = rhs.tiling_; usage_ = rhs.usage_; + memory_properties_ = rhs.memory_properties_; image = rhs.image; rhs.image = VK_NULL_HANDLE; diff --git a/vulkan/src/VulkanSwapChain.cpp b/vulkan/src/VulkanSwapChain.cpp index b26f49b..96d8d2f 100644 --- a/vulkan/src/VulkanSwapChain.cpp +++ b/vulkan/src/VulkanSwapChain.cpp @@ -185,8 +185,8 @@ void VulkanSwapChain::CreateImageViews() { void VulkanSwapChain::CreateDepthResources(VkFormat depth_format) { depth_image_ = core::vulkan::VulkanImage( context_, swapchain_extent.width, swapchain_extent.height, depth_format, - VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, VK_IMAGE_ASPECT_DEPTH_BIT, - VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, VK_IMAGE_TILING_OPTIMAL); + VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT | VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, + VK_IMAGE_ASPECT_DEPTH_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, VK_IMAGE_TILING_OPTIMAL); depth_image_.TransitionDepthImageLayout( VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, depth_format);