From 2656b0bfc1f1ac639fd509de1a8edab87ed0d5ba Mon Sep 17 00:00:00 2001 From: joe chu Date: Sat, 14 Nov 2026 02:21:43 -0800 Subject: [PATCH] feature(vulkan): support depth-only render pipelines Allow dynamic-rendering pipelines with no color attachment when a depth or stencil format is set, make the fragment stage optional for them, and add configurable depth bias plus VulkanDynamicRendering::BeginDepthRendering. CarControlDemo uses this to render the car into a shadow map and shade the ground with comparison-sampled shadows. Co-Authored-By: Claude Opus 5.5 --- examples/vulkan/CarControlDemo/CarShadow.vert | 11 + examples/vulkan/CarControlDemo/Ground.frag | 19 + examples/vulkan/CarControlDemo/Ground.vert | 3 + examples/vulkan/CarControlDemo/RenderCar.cpp | 3 +- examples/vulkan/CarControlDemo/RenderCar.h | 5 + .../vulkan/CarControlDemo/RenderCarShadow.cpp | 184 ++++++++++ .../vulkan/CarControlDemo/RenderCarShadow.h | 56 +++ .../vulkan/CarControlDemo/RenderGround.cpp | 24 +- examples/vulkan/CarControlDemo/RenderGround.h | 8 +- examples/vulkan/CarControlDemo/main.cpp | 21 +- tests/vulkan/DepthOnly.vert | 13 + tests/vulkan/VulkanDepthOnlyRenderTest.cpp | 347 ++++++++++++++++++ vulkan/include/VulkanDynamicRendering.h | 2 + vulkan/include/VulkanRender.h | 7 + vulkan/src/VulkanDynamicRendering.cpp | 39 ++ vulkan/src/VulkanRender.cpp | 68 +++- 16 files changed, 782 insertions(+), 28 deletions(-) create mode 100644 examples/vulkan/CarControlDemo/CarShadow.vert create mode 100644 examples/vulkan/CarControlDemo/RenderCarShadow.cpp create mode 100644 examples/vulkan/CarControlDemo/RenderCarShadow.h create mode 100644 tests/vulkan/DepthOnly.vert create mode 100644 tests/vulkan/VulkanDepthOnlyRenderTest.cpp diff --git a/examples/vulkan/CarControlDemo/CarShadow.vert b/examples/vulkan/CarControlDemo/CarShadow.vert new file mode 100644 index 0000000..82ed58c --- /dev/null +++ b/examples/vulkan/CarControlDemo/CarShadow.vert @@ -0,0 +1,11 @@ +#version 450 + +layout(push_constant) uniform ShadowTransform { + mat4 light_model_view_projection; +} transform; + +layout(location = 0) in vec3 inPos; + +void main() { + gl_Position = transform.light_model_view_projection * vec4(inPos, 1.0); +} diff --git a/examples/vulkan/CarControlDemo/Ground.frag b/examples/vulkan/CarControlDemo/Ground.frag index 03c2f3e..92bf9f4 100644 --- a/examples/vulkan/CarControlDemo/Ground.frag +++ b/examples/vulkan/CarControlDemo/Ground.frag @@ -2,13 +2,32 @@ layout(location = 0) in vec2 fragUV; layout(location = 1) in vec2 fragWorldXZ; +layout(location = 2) in vec4 fragLightClip; layout(binding = 1) uniform sampler2D groundTex; +layout(binding = 2) uniform sampler2DShadow shadowMap; layout(location = 0) out vec4 outColor; +float ShadowVisibility() { + vec3 light = fragLightClip.xyz / fragLightClip.w; + vec2 uv = light.xy * 0.5 + 0.5; + if (light.z <= 0.0 || light.z >= 1.0) { + return 1.0; + } + vec2 texel = 1.0 / vec2(textureSize(shadowMap, 0)); + float visibility = 0.0; + for (int y = -1; y <= 1; ++y) { + for (int x = -1; x <= 1; ++x) { + visibility += texture(shadowMap, vec3(uv + vec2(x, y) * texel, light.z)); + } + } + return visibility / 9.0; +} + void main() { vec3 sampled = texture(groundTex, fragUV).rgb; + sampled *= mix(0.45, 1.0, ShadowVisibility()); // Fade the textured plane toward the horizon color so its edge doesn't // read as a hard seam against the skybox. diff --git a/examples/vulkan/CarControlDemo/Ground.vert b/examples/vulkan/CarControlDemo/Ground.vert index d937cb1..5dcdbfd 100644 --- a/examples/vulkan/CarControlDemo/Ground.vert +++ b/examples/vulkan/CarControlDemo/Ground.vert @@ -3,17 +3,20 @@ layout(binding = 0) uniform UniformBufferObject { mat4 view; mat4 project; + mat4 light_view_projection; } ubo; layout(location = 0) in vec3 inPos; layout(location = 0) out vec2 fragUV; layout(location = 1) out vec2 fragWorldXZ; +layout(location = 2) out vec4 fragLightClip; void main() { // One texture tile every 4 world units, so the asphalt feels grounded in // real scale. fragUV = inPos.xz * 0.25; fragWorldXZ = inPos.xz; + fragLightClip = ubo.light_view_projection * vec4(inPos, 1.0); gl_Position = ubo.project * ubo.view * vec4(inPos, 1.0); } diff --git a/examples/vulkan/CarControlDemo/RenderCar.cpp b/examples/vulkan/CarControlDemo/RenderCar.cpp index 04f78a0..08df440 100644 --- a/examples/vulkan/CarControlDemo/RenderCar.cpp +++ b/examples/vulkan/CarControlDemo/RenderCar.cpp @@ -87,7 +87,8 @@ const std::vector RenderCar::LoadFragmentShader() const { std::vector RenderCar::GetVertexBindingDescriptions() const { VkVertexInputBindingDescription b{}; b.binding = 0; - b.stride = sizeof(Vertex); + static_assert(sizeof(Vertex) == kVertexStride); + b.stride = kVertexStride; b.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; return {b}; } diff --git a/examples/vulkan/CarControlDemo/RenderCar.h b/examples/vulkan/CarControlDemo/RenderCar.h index 8504da0..d1e15d4 100644 --- a/examples/vulkan/CarControlDemo/RenderCar.h +++ b/examples/vulkan/CarControlDemo/RenderCar.h @@ -25,6 +25,11 @@ class RenderCar : public core::vulkan::VulkanRender { void UpdateUniformBuffer(const glm::mat4& model, const glm::mat4& view, const glm::mat4& project); + const core::vulkan::VulkanBuffer& vertex_buffer() const { return vertex_buffer_local_; } + const core::vulkan::VulkanBuffer& index_buffer() const { return index_buffer_local_; } + uint32_t index_count() const { return static_cast(indices_.size()); } + static constexpr uint32_t kVertexStride = 9 * sizeof(float); + protected: VkCullModeFlags SetCullMode() const override { return VK_CULL_MODE_BACK_BIT; } VkFrontFace SetFrontFace() const override { return VK_FRONT_FACE_COUNTER_CLOCKWISE; } diff --git a/examples/vulkan/CarControlDemo/RenderCarShadow.cpp b/examples/vulkan/CarControlDemo/RenderCarShadow.cpp new file mode 100644 index 0000000..1f61f72 --- /dev/null +++ b/examples/vulkan/CarControlDemo/RenderCarShadow.cpp @@ -0,0 +1,184 @@ +#include "RenderCarShadow.h" + +#include + +namespace core { + +RenderCarShadow::RenderCarShadow(core::vulkan::VulkanContext* context, VkFormat depth_format, + uint32_t size) + : core::vulkan::VulkanRender(context, DepthOnlyInfo(depth_format)), + depth_format_(depth_format), + size_(size), + dynamic_rendering_(context) { + if (size_ == 0) { + throw std::invalid_argument("Shadow map size must be positive"); + } + VkFormatProperties properties{}; + vkGetPhysicalDeviceFormatProperties(context_->physical_device, depth_format_, &properties); + const VkFormatFeatureFlags required = + VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT; + if ((properties.optimalTilingFeatures & required) != required) { + throw std::runtime_error("Shadow map format cannot be rendered and sampled"); + } + const bool linear_filter = + (properties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT) != 0; + + shadow_map_ = core::vulkan::VulkanImage( + context_, size_, size_, depth_format_, + VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, + VK_IMAGE_ASPECT_DEPTH_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + + const VkFilter filter = linear_filter ? VK_FILTER_LINEAR : VK_FILTER_NEAREST; + const VkSamplerCreateInfo sampler_info{ + .sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .magFilter = filter, + .minFilter = filter, + .mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST, + .addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER, + .addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER, + .addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER, + .mipLodBias = 0.0f, + .anisotropyEnable = VK_FALSE, + .maxAnisotropy = 1.0f, + .compareEnable = VK_TRUE, + .compareOp = VK_COMPARE_OP_LESS_OR_EQUAL, + .minLod = 0.0f, + .maxLod = 0.0f, + .borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE, + .unnormalizedCoordinates = VK_FALSE, + }; + VK_CHECK(vkCreateSampler(context_->logical_device, &sampler_info, nullptr, &sampler_)); +} + +RenderCarShadow::~RenderCarShadow() { + if (sampler_ != VK_NULL_HANDLE) { + vkDestroySampler(context_->logical_device, sampler_, nullptr); + } +} + +void RenderCarShadow::Init() { + core::vulkan::VulkanRender::Init(); + + const core::vulkan::VulkanCommandBuffer command = + core::vulkan::VulkanCommandBuffer::BeginOneTimeCommands(context_); + const VkImageMemoryBarrier to_shader_read{ + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .pNext = nullptr, + .srcAccessMask = 0, + .dstAccessMask = VK_ACCESS_SHADER_READ_BIT, + .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED, + .newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = shadow_map_.image, + .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_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, + &to_shader_read); + command.EndOneTimeCommands(); +} + +void RenderCarShadow::Record(VkCommandBuffer command_buffer, const RenderCar& car, + const glm::mat4& light_model_view_projection) const { + const VkImageSubresourceRange range{.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1}; + const VkImageMemoryBarrier to_attachment{ + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .pNext = nullptr, + .srcAccessMask = 0, + .dstAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | + VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT, + .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED, + .newLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = shadow_map_.image, + .subresourceRange = range, + }; + vkCmdPipelineBarrier( + command_buffer, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, + VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT, 0, 0, + nullptr, 0, nullptr, 1, &to_attachment); + + const VkExtent2D extent{.width = size_, .height = size_}; + const VkClearValue clear{.depthStencil = {.depth = 1.0f, .stencil = 0}}; + dynamic_rendering_.BeginDepthRendering(command_buffer, shadow_map_.image_view, extent, clear); + vkCmdBindPipeline(command_buffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); + const VkViewport viewport{.x = 0.0f, + .y = 0.0f, + .width = static_cast(size_), + .height = static_cast(size_), + .minDepth = 0.0f, + .maxDepth = 1.0f}; + vkCmdSetViewport(command_buffer, 0, 1, &viewport); + const VkRect2D scissor{.offset = {.x = 0, .y = 0}, .extent = extent}; + vkCmdSetScissor(command_buffer, 0, 1, &scissor); + vkCmdPushConstants(command_buffer, pipeline_layout, VK_SHADER_STAGE_VERTEX_BIT, 0, + sizeof(glm::mat4), &light_model_view_projection); + const VkBuffer vertex_buffer = car.vertex_buffer().buffer; + const VkDeviceSize offset = 0; + vkCmdBindVertexBuffers(command_buffer, 0, 1, &vertex_buffer, &offset); + vkCmdBindIndexBuffer(command_buffer, car.index_buffer().buffer, 0, VK_INDEX_TYPE_UINT32); + vkCmdDrawIndexed(command_buffer, car.index_count(), 1, 0, 0, 0); + dynamic_rendering_.EndDynamicRendering(command_buffer); + + const VkImageMemoryBarrier to_shader_read{ + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .pNext = nullptr, + .srcAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT, + .dstAccessMask = VK_ACCESS_SHADER_READ_BIT, + .oldLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, + .newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = shadow_map_.image, + .subresourceRange = range, + }; + vkCmdPipelineBarrier( + command_buffer, + VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &to_shader_read); +} + +std::vector RenderCarShadow::GetPushConstantRanges() const { + return {VkPushConstantRange{ + .stageFlags = VK_SHADER_STAGE_VERTEX_BIT, .offset = 0, .size = sizeof(glm::mat4)}}; +} + +const std::vector RenderCarShadow::LoadVertexShader() const { + static const std::vector shader_code = +#include "CarShadow.vert.spv" + ; + return shader_code; +} + +std::vector RenderCarShadow::GetVertexBindingDescriptions() + const { + return {VkVertexInputBindingDescription{.binding = 0, + .stride = RenderCar::kVertexStride, + .inputRate = VK_VERTEX_INPUT_RATE_VERTEX}}; +} + +std::vector RenderCarShadow::GetVertexAttributeDescriptions() + const { + return {VkVertexInputAttributeDescription{ + .location = 0, .binding = 0, .format = VK_FORMAT_R32G32B32_SFLOAT, .offset = 0}}; +} + +core::vulkan::DynamicRenderingInfo RenderCarShadow::DepthOnlyInfo(VkFormat depth_format) { + core::vulkan::DynamicRenderingInfo info{}; + info.depth_format = depth_format; + return info; +} + +} diff --git a/examples/vulkan/CarControlDemo/RenderCarShadow.h b/examples/vulkan/CarControlDemo/RenderCarShadow.h new file mode 100644 index 0000000..95bdc86 --- /dev/null +++ b/examples/vulkan/CarControlDemo/RenderCarShadow.h @@ -0,0 +1,56 @@ +#pragma once + +#include +#include + +#include "RenderCar.h" +#include "VulkanDynamicRendering.h" +#include "VulkanImage.h" +#include "VulkanRender.h" + +#define GLM_FORCE_RADIANS +#define GLM_FORCE_DEPTH_ZERO_TO_ONE +#include "glm/glm.hpp" + +namespace core { + +class RenderCarShadow : public core::vulkan::VulkanRender { + public: + RenderCarShadow(core::vulkan::VulkanContext* context, VkFormat depth_format, uint32_t size); + ~RenderCarShadow() override; + RenderCarShadow(const RenderCarShadow&) = delete; + RenderCarShadow& operator=(const RenderCarShadow&) = delete; + + void Init() override; + void Record(VkCommandBuffer command_buffer, const RenderCar& car, + const glm::mat4& light_model_view_projection) const; + + VkImageView shadow_map_view() const { return shadow_map_.image_view; } + VkSampler shadow_sampler() const { return sampler_; } + + protected: + VkCullModeFlags SetCullMode() const override { return VK_CULL_MODE_NONE; } + VkBool32 SetDepthTesting() const override { return VK_TRUE; } + VkBool32 SetDepthWriting() const override { return VK_TRUE; } + std::optional SetDepthBias() const override { + return core::vulkan::DepthBiasInfo{.constant_factor = 1.25f, .slope_factor = 1.75f}; + } + + std::vector GetBindingInfo() const override { return {}; } + std::vector GetPushConstantRanges() const override; + const std::vector LoadVertexShader() const override; + const std::vector LoadFragmentShader() const override { return {}; } + std::vector GetVertexBindingDescriptions() const override; + std::vector GetVertexAttributeDescriptions() const override; + + private: + static core::vulkan::DynamicRenderingInfo DepthOnlyInfo(VkFormat depth_format); + + VkFormat depth_format_; + uint32_t size_; + core::vulkan::VulkanDynamicRendering dynamic_rendering_; + core::vulkan::VulkanImage shadow_map_; + VkSampler sampler_ = VK_NULL_HANDLE; +}; + +} diff --git a/examples/vulkan/CarControlDemo/RenderGround.cpp b/examples/vulkan/CarControlDemo/RenderGround.cpp index 1933174..001ff6c 100644 --- a/examples/vulkan/CarControlDemo/RenderGround.cpp +++ b/examples/vulkan/CarControlDemo/RenderGround.cpp @@ -18,6 +18,7 @@ void RenderGround::Init() { CreateUniformBufferDescriptorSet(0, uniform_buffer_); CreateCombinedImageSamplerDescriptorSet(1, texture_image_.image_view, sampler_.sampler); + CreateCombinedImageSamplerDescriptorSet(2, shadow_map_view_, shadow_sampler_); vkUpdateDescriptorSets(context_->logical_device, writes_.size(), writes_.data(), 0, nullptr); vertex_buffer_staging_.MapData([this](void* data) { @@ -29,12 +30,20 @@ void RenderGround::Init() { index_buffer_staging_.CopyToBuffer(index_buffer_local_); uniform_buffer_.MapData([](void* data) { - UniformBufferObject init{glm::mat4(1.0f), glm::mat4(1.0f)}; + UniformBufferObject init{.view = glm::mat4(1.0f), + .project = glm::mat4(1.0f), + .light_view_projection = glm::mat4(1.0f)}; memcpy(data, &init, sizeof(UniformBufferObject)); }); } -void RenderGround::Init(const std::string& texture_path) { +void RenderGround::Init(const std::string& texture_path, VkImageView shadow_map_view, + VkSampler shadow_sampler) { + if (shadow_map_view == VK_NULL_HANDLE || shadow_sampler == VK_NULL_HANDLE) { + throw std::invalid_argument("Ground rendering requires a shadow map and sampler"); + } + shadow_map_view_ = shadow_map_view; + shadow_sampler_ = shadow_sampler; CreateTextureImage(texture_path); CreateBuffers(); Init(); @@ -66,9 +75,11 @@ void RenderGround::Render(VkCommandBuffer command_buffer, VkExtent2D extent) { vkCmdDrawIndexed(command_buffer, static_cast(indices_.size()), 1, 0, 0, 0); } -void RenderGround::UpdateUniformBuffer(const glm::mat4& view, const glm::mat4& project) { +void RenderGround::UpdateUniformBuffer(const glm::mat4& view, const glm::mat4& project, + const glm::mat4& light_view_projection) { uniform_buffer_.MapData([&](void* data) { - UniformBufferObject ubo{view, project}; + UniformBufferObject ubo{ + .view = view, .project = project, .light_view_projection = light_view_projection}; ubo.project[1][1] *= -1; // Vulkan Y-flip memcpy(data, &ubo, sizeof(UniformBufferObject)); }); @@ -76,7 +87,10 @@ void RenderGround::UpdateUniformBuffer(const glm::mat4& view, const glm::mat4& p std::vector RenderGround::GetBindingInfo() const { return {{0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_VERTEX_BIT}, - {1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT}}; + {1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT}, + {.binding = 2, + .descriptor_type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, + .shader_stage = VK_SHADER_STAGE_FRAGMENT_BIT}}; } const std::vector RenderGround::LoadVertexShader() const { diff --git a/examples/vulkan/CarControlDemo/RenderGround.h b/examples/vulkan/CarControlDemo/RenderGround.h index 33ca3e5..7bca2e3 100644 --- a/examples/vulkan/CarControlDemo/RenderGround.h +++ b/examples/vulkan/CarControlDemo/RenderGround.h @@ -24,10 +24,11 @@ class RenderGround : public core::vulkan::VulkanRender { const core::vulkan::DynamicRenderingInfo& dynamic_rendering_info); void Init() override; - void Init(const std::string& texture_path); + void Init(const std::string& texture_path, VkImageView shadow_map_view, VkSampler shadow_sampler); void Render(VkCommandBuffer command_buffer, VkExtent2D extent); - void UpdateUniformBuffer(const glm::mat4& view, const glm::mat4& project); + void UpdateUniformBuffer(const glm::mat4& view, const glm::mat4& project, + const glm::mat4& light_view_projection); protected: VkCullModeFlags SetCullMode() const override { return VK_CULL_MODE_NONE; } @@ -45,6 +46,7 @@ class RenderGround : public core::vulkan::VulkanRender { struct UniformBufferObject { glm::mat4 view; glm::mat4 project; + glm::mat4 light_view_projection; }; void CreateTextureImage(const std::string& texture_path); @@ -67,6 +69,8 @@ class RenderGround : public core::vulkan::VulkanRender { core::vulkan::VulkanImage texture_image_; core::vulkan::VulkanSampler sampler_; + VkImageView shadow_map_view_ = VK_NULL_HANDLE; + VkSampler shadow_sampler_ = VK_NULL_HANDLE; }; } // namespace core diff --git a/examples/vulkan/CarControlDemo/main.cpp b/examples/vulkan/CarControlDemo/main.cpp index df7e778..71603ad 100644 --- a/examples/vulkan/CarControlDemo/main.cpp +++ b/examples/vulkan/CarControlDemo/main.cpp @@ -10,6 +10,7 @@ #include #include "RenderCar.h" +#include "RenderCarShadow.h" #include "RenderGround.h" #include "RenderSkybox.h" #include "VulkanCommandBuffer.h" @@ -43,6 +44,11 @@ constexpr float kDriveSpeed = 8.0f; // world units per second constexpr float kTurnRate = glm::radians(110.0f); // radians per second while driving constexpr float kMouseSensitivity = 0.15f; +constexpr uint32_t kShadowMapSize = 2048; +constexpr float kShadowHalfExtent = 7.0f; +constexpr float kLightDistance = 20.0f; +const glm::vec3 kDirectionToLight = glm::normalize(glm::vec3(0.4f, 0.85f, 0.3f)); + // Camera orbit parameters. constexpr float kCameraDistance = 12.0f; constexpr float kCameraHeight = 1.8f; // height of the orbit target above the floor @@ -109,6 +115,7 @@ int main() { auto skybox = std::make_unique(&context, dynamic_rendering_info); auto ground = std::make_unique(&context, dynamic_rendering_info); auto car = std::make_unique(&context, dynamic_rendering_info); + auto car_shadow = std::make_unique(&context, kDepthFormat, kShadowMapSize); const VkClearValue color_clear_value{ .color = { @@ -127,8 +134,9 @@ int main() { } skybox->Init(kSkyboxFacePaths); - ground->Init(kGroundTexturePath); car->Init(); + car_shadow->Init(); + ground->Init(kGroundTexturePath, car_shadow->shadow_map_view(), car_shadow->shadow_sampler()); auto last_time = std::chrono::high_resolution_clock::now(); @@ -180,6 +188,13 @@ int main() { glm::mat4 model = glm::translate(glm::mat4(1.0f), car_pos); model = glm::rotate(model, car_yaw, glm::vec3(0.0f, 1.0f, 0.0f)); + const glm::mat4 light_view = glm::lookAt(car_pos + kDirectionToLight * kLightDistance, car_pos, + glm::vec3(0.0f, 1.0f, 0.0f)); + const glm::mat4 light_view_projection = + glm::orthoRH_ZO(-kShadowHalfExtent, kShadowHalfExtent, -kShadowHalfExtent, + kShadowHalfExtent, 0.1f, 2.0f * kLightDistance) * + light_view; + vkWaitForFences(context.logical_device, 1, &(in_flight_fence.fence), VK_TRUE, UINT64_MAX); in_flight_fence.Reset(); @@ -188,11 +203,13 @@ int main() { image_available_semaphore.semaphore, VK_NULL_HANDLE, &image_index); skybox->UpdateUniformBuffer(view, project); - ground->UpdateUniformBuffer(view, project); + ground->UpdateUniformBuffer(view, project, light_view_projection); car->UpdateUniformBuffer(model, view, project); command_buffer.BeginCommandBufferWithReset(); + car_shadow->Record(command_buffer.buffer(), *car, light_view_projection * model); + swap_chain->TransitionImageLayout(command_buffer.buffer(), image_index, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); diff --git a/tests/vulkan/DepthOnly.vert b/tests/vulkan/DepthOnly.vert new file mode 100644 index 0000000..6516ce8 --- /dev/null +++ b/tests/vulkan/DepthOnly.vert @@ -0,0 +1,13 @@ +#version 450 + +layout(push_constant) uniform DepthRamp { + float left; + float right; +} ramp; + +void main() { + const vec2 positions[3] = vec2[](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0)); + const vec2 position = positions[gl_VertexIndex]; + const float depth = mix(ramp.left, ramp.right, (position.x + 1.0) * 0.5); + gl_Position = vec4(position, depth, 1.0); +} diff --git a/tests/vulkan/VulkanDepthOnlyRenderTest.cpp b/tests/vulkan/VulkanDepthOnlyRenderTest.cpp new file mode 100644 index 0000000..089bc80 --- /dev/null +++ b/tests/vulkan/VulkanDepthOnlyRenderTest.cpp @@ -0,0 +1,347 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "VulkanBuffer.h" +#include "VulkanCommandBuffer.h" +#include "VulkanContext.h" +#include "VulkanDynamicRendering.h" +#include "VulkanImage.h" +#include "VulkanRender.h" +#include "VulkanSync.h" + +namespace core { +namespace test { +namespace { + +constexpr uint32_t kSize = 32; +constexpr VkFormat kDepthFormat = VK_FORMAT_D32_SFLOAT; + +struct DepthRamp { + float left; + float right; +}; + +std::vector DepthOnlyVertexShader() { + static const std::vector code = +#include "DepthOnly.vert.spv" + ; + return code; +} + +class DepthOnlyPipeline : public vulkan::VulkanRender { + public: + DepthOnlyPipeline(vulkan::VulkanContext* context, const vulkan::DynamicRenderingInfo& info, + std::optional depth_bias, + std::vector vertex_shader = DepthOnlyVertexShader(), + std::vector fragment_shader = {}) + : VulkanRender(context, info), + depth_bias_(depth_bias), + vertex_shader_(std::move(vertex_shader)), + fragment_shader_(std::move(fragment_shader)) {} + + void Draw(VkCommandBuffer command_buffer, const DepthRamp& ramp, const VkRect2D& scissor) { + vkCmdBindPipeline(command_buffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); + const VkViewport viewport{.x = 0.0f, + .y = 0.0f, + .width = static_cast(kSize), + .height = static_cast(kSize), + .minDepth = 0.0f, + .maxDepth = 1.0f}; + vkCmdSetViewport(command_buffer, 0, 1, &viewport); + vkCmdSetScissor(command_buffer, 0, 1, &scissor); + vkCmdPushConstants(command_buffer, pipeline_layout, VK_SHADER_STAGE_VERTEX_BIT, 0, + sizeof(DepthRamp), &ramp); + vkCmdDraw(command_buffer, 3, 1, 0, 0); + } + + private: + VkCullModeFlags SetCullMode() const override { return VK_CULL_MODE_NONE; } + VkBool32 SetDepthTesting() const override { return VK_TRUE; } + VkBool32 SetDepthWriting() const override { return VK_TRUE; } + std::optional SetDepthBias() const override { return depth_bias_; } + + std::vector GetBindingInfo() const override { return {}; } + std::vector GetPushConstantRanges() const override { + return {VkPushConstantRange{ + .stageFlags = VK_SHADER_STAGE_VERTEX_BIT, .offset = 0, .size = sizeof(DepthRamp)}}; + } + const std::vector LoadVertexShader() const override { return vertex_shader_; } + const std::vector LoadFragmentShader() const override { return fragment_shader_; } + std::vector GetVertexBindingDescriptions() const override { + return {}; + } + std::vector GetVertexAttributeDescriptions() const override { + return {}; + } + + std::optional depth_bias_; + std::vector vertex_shader_; + std::vector fragment_shader_; +}; + +struct DepthDraw { + DepthOnlyPipeline* pipeline; + DepthRamp ramp; + VkRect2D scissor; +}; + +class VulkanDepthOnlyRenderTest : public testing::Test { + protected: + void SetUp() override { + context_.Init(); + rendering_.emplace(&context_); + 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(); + if ((families[family].queueFlags & VK_QUEUE_GRAPHICS_BIT) == 0) { + GTEST_SKIP() << "Compute queue family cannot record graphics work"; + } + VkFormatProperties properties{}; + vkGetPhysicalDeviceFormatProperties(context_.physical_device, kDepthFormat, &properties); + const VkFormatFeatureFlags required = + VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_FORMAT_FEATURE_TRANSFER_SRC_BIT; + if ((properties.optimalTilingFeatures & required) != required) { + GTEST_SKIP() << "D32_SFLOAT depth attachments cannot be copied on this device"; + } + } + + std::vector Render(const std::vector& draws) { + vulkan::VulkanImage depth(&context_, kSize, kSize, kDepthFormat, + VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | + VK_IMAGE_USAGE_TRANSFER_SRC_BIT, + VK_IMAGE_ASPECT_DEPTH_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + const VkDeviceSize bytes = static_cast(kSize) * kSize * sizeof(float); + vulkan::VulkanBuffer readback( + &context_, bytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); + + vulkan::VulkanCommandBuffer command(&context_); + command.BeginCommandBuffer(); + const VkImageSubresourceRange range{.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1}; + const VkImageMemoryBarrier to_attachment{ + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .pNext = nullptr, + .srcAccessMask = 0, + .dstAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | + VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT, + .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED, + .newLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = depth.image, + .subresourceRange = range}; + vkCmdPipelineBarrier(command.buffer(), VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | + VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT, + 0, 0, nullptr, 0, nullptr, 1, &to_attachment); + + const VkClearValue clear{.depthStencil = {.depth = 1.0f, .stencil = 0}}; + rendering_->BeginDepthRendering(command.buffer(), depth.image_view, + VkExtent2D{.width = kSize, .height = kSize}, clear); + for (const DepthDraw& draw : draws) { + draw.pipeline->Draw(command.buffer(), draw.ramp, draw.scissor); + } + rendering_->EndDynamicRendering(command.buffer()); + + const VkImageMemoryBarrier to_transfer{ + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .pNext = nullptr, + .srcAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT, + .dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT, + .oldLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, + .newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = depth.image, + .subresourceRange = range}; + vkCmdPipelineBarrier(command.buffer(), + VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | + VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, + &to_transfer); + const VkBufferImageCopy region{ + .bufferOffset = 0, + .bufferRowLength = 0, + .bufferImageHeight = 0, + .imageSubresource = {.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT, + .mipLevel = 0, + .baseArrayLayer = 0, + .layerCount = 1}, + .imageOffset = {.x = 0, .y = 0, .z = 0}, + .imageExtent = {.width = kSize, .height = kSize, .depth = 1}}; + vkCmdCopyImageToBuffer(command.buffer(), depth.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + readback.buffer, 1, ®ion); + const VkMemoryBarrier to_host{.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, &to_host, 0, nullptr, 0, nullptr); + + vulkan::VulkanFence fence(&context_); + fence.Reset(); + command.Submit(fence.fence); + VK_CHECK(vkWaitForFences(context_.logical_device, 1, &fence.fence, VK_TRUE, UINT64_MAX)); + + std::vector texels(static_cast(kSize) * kSize); + readback.MapData([&](void* data) { std::memcpy(texels.data(), data, bytes); }); + return texels; + } + + static VkRect2D Scissor(uint32_t x, uint32_t width) { + return VkRect2D{.offset = {.x = static_cast(x), .y = 0}, + .extent = {.width = width, .height = kSize}}; + } + + static vulkan::DynamicRenderingInfo DepthOnlyInfo() { + vulkan::DynamicRenderingInfo info{}; + info.depth_format = kDepthFormat; + return info; + } + + vulkan::VulkanContext context_{true, vulkan::QueueFamilyType::Compute, VK_NULL_HANDLE}; + std::optional rendering_; +}; + +TEST_F(VulkanDepthOnlyRenderTest, WritesAndTestsDepthWithoutColorOrFragmentStage) { + DepthOnlyPipeline pipeline(&context_, DepthOnlyInfo(), std::nullopt); + pipeline.Init(); + + const std::vector depth = + Render({DepthDraw{.pipeline = &pipeline, + .ramp = {.left = 0.25f, .right = 0.25f}, + .scissor = Scissor(0, kSize / 2)}, + DepthDraw{.pipeline = &pipeline, + .ramp = {.left = 0.5f, .right = 0.5f}, + .scissor = Scissor(0, kSize)}, + DepthDraw{.pipeline = &pipeline, + .ramp = {.left = 0.75f, .right = 0.75f}, + .scissor = Scissor(kSize / 4, kSize / 2)}}); + + for (uint32_t y = 0; y < kSize; ++y) { + for (uint32_t x = 0; x < kSize; ++x) { + const float expected = x < kSize / 2 ? 0.25f : 0.5f; + ASSERT_EQ(depth[static_cast(y) * kSize + x], expected) << x << "," << y; + } + } + + const std::vector cleared = + Render({DepthDraw{.pipeline = &pipeline, + .ramp = {.left = 0.25f, .right = 0.25f}, + .scissor = Scissor(kSize - 4, 4)}}); + for (uint32_t y = 0; y < kSize; ++y) { + for (uint32_t x = 0; x < kSize; ++x) { + const float expected = x >= kSize - 4 ? 0.25f : 1.0f; + ASSERT_EQ(cleared[static_cast(y) * kSize + x], expected) << x << "," << y; + } + } +} + +TEST_F(VulkanDepthOnlyRenderTest, AppliesConstantAndSlopeDepthBias) { + DepthOnlyPipeline unbiased(&context_, DepthOnlyInfo(), std::nullopt); + DepthOnlyPipeline constant(&context_, DepthOnlyInfo(), + vulkan::DepthBiasInfo{.constant_factor = 4096.0f, + .slope_factor = 0.0f}); + DepthOnlyPipeline slope(&context_, DepthOnlyInfo(), + vulkan::DepthBiasInfo{.constant_factor = 0.0f, .slope_factor = 2.0f}); + unbiased.Init(); + constant.Init(); + slope.Init(); + + const DepthRamp flat{.left = 0.5f, .right = 0.5f}; + const DepthRamp ramp{.left = 0.2f, .right = 0.6f}; + const std::vector flat_reference = + Render({DepthDraw{.pipeline = &unbiased, .ramp = flat, .scissor = Scissor(0, kSize)}}); + const std::vector flat_biased = + Render({DepthDraw{.pipeline = &constant, .ramp = flat, .scissor = Scissor(0, kSize)}}); + const std::vector ramp_reference = + Render({DepthDraw{.pipeline = &unbiased, .ramp = ramp, .scissor = Scissor(0, kSize)}}); + const std::vector ramp_biased = + Render({DepthDraw{.pipeline = &slope, .ramp = ramp, .scissor = Scissor(0, kSize)}}); + + const float slope_per_pixel = (ramp.right - ramp.left) / static_cast(kSize); + const float expected_slope_bias = 2.0f * slope_per_pixel; + const float constant_upper_bound = 4096.0f * std::ldexp(1.0f, -22); + const float constant_bias = flat_biased[0] - flat_reference[0]; + EXPECT_GT(constant_bias, 0.0f); + EXPECT_LE(constant_bias, constant_upper_bound); + for (size_t i = 0; i < flat_reference.size(); ++i) { + ASSERT_EQ(flat_reference[i], 0.5f) << i; + ASSERT_EQ(flat_biased[i], flat_biased[0]) << i; + } + for (uint32_t y = 0; y < kSize; ++y) { + for (uint32_t x = 0; x < kSize; ++x) { + const size_t index = static_cast(y) * kSize + x; + const float center = ramp.left + slope_per_pixel * (static_cast(x) + 0.5f); + ASSERT_NEAR(ramp_reference[index], center, 1e-5f) << x << "," << y; + ASSERT_NEAR(ramp_biased[index] - ramp_reference[index], expected_slope_bias, + expected_slope_bias * 0.05f) + << x << "," << y; + } + } +} + +TEST_F(VulkanDepthOnlyRenderTest, RejectsInvalidDepthOnlyConfigurations) { + DepthOnlyPipeline no_attachments(&context_, vulkan::DynamicRenderingInfo{}, std::nullopt); + EXPECT_THROW(no_attachments.Init(), std::invalid_argument); + + vulkan::DynamicRenderingInfo color_info = DepthOnlyInfo(); + color_info.color_formats = {VK_FORMAT_R8G8B8A8_UNORM}; + DepthOnlyPipeline missing_fragment(&context_, color_info, std::nullopt); + EXPECT_THROW(missing_fragment.Init(), std::invalid_argument); + + DepthOnlyPipeline missing_vertex(&context_, DepthOnlyInfo(), std::nullopt, {}); + EXPECT_THROW(missing_vertex.Init(), std::invalid_argument); + + DepthOnlyPipeline infinite_bias( + &context_, DepthOnlyInfo(), + vulkan::DepthBiasInfo{.constant_factor = std::numeric_limits::infinity(), + .slope_factor = 0.0f}); + EXPECT_THROW(infinite_bias.Init(), std::invalid_argument); + + DepthOnlyPipeline nan_bias( + &context_, DepthOnlyInfo(), + vulkan::DepthBiasInfo{.constant_factor = 0.0f, + .slope_factor = std::numeric_limits::quiet_NaN()}); + EXPECT_THROW(nan_bias.Init(), std::invalid_argument); + + vulkan::DynamicRenderingInfo two_colors = DepthOnlyInfo(); + two_colors.color_formats = {VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_R8G8B8A8_UNORM}; + DepthOnlyPipeline too_many_colors(&context_, two_colors, std::nullopt); + EXPECT_THROW(too_many_colors.Init(), std::runtime_error); + + vulkan::VulkanImage depth(&context_, kSize, kSize, kDepthFormat, + VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, VK_IMAGE_ASPECT_DEPTH_BIT, + VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + vulkan::VulkanCommandBuffer command(&context_); + const VkClearValue clear{.depthStencil = {.depth = 1.0f, .stencil = 0}}; + const VkExtent2D extent{.width = kSize, .height = kSize}; + EXPECT_THROW(rendering_->BeginDepthRendering(command.buffer(), VK_NULL_HANDLE, extent, clear), + std::invalid_argument); + EXPECT_THROW(rendering_->BeginDepthRendering(VK_NULL_HANDLE, depth.image_view, extent, clear), + std::invalid_argument); + EXPECT_THROW(rendering_->BeginDepthRendering(command.buffer(), depth.image_view, + VkExtent2D{.width = 0, .height = kSize}, clear), + std::invalid_argument); + EXPECT_THROW(rendering_->BeginDepthRendering(command.buffer(), depth.image_view, + VkExtent2D{.width = kSize, .height = 0}, clear), + std::invalid_argument); +} + +} +} +} diff --git a/vulkan/include/VulkanDynamicRendering.h b/vulkan/include/VulkanDynamicRendering.h index 8adf2cd..5472c84 100644 --- a/vulkan/include/VulkanDynamicRendering.h +++ b/vulkan/include/VulkanDynamicRendering.h @@ -19,6 +19,8 @@ class VulkanDynamicRendering { VkClearValue clear_value, VkImageView depth_image_view = VK_NULL_HANDLE, VkClearValue depth_clear_value = {}) const; + void BeginDepthRendering(VkCommandBuffer command_buffer, VkImageView depth_image_view, + VkExtent2D extent, VkClearValue depth_clear_value) const; void EndDynamicRendering(VkCommandBuffer command_buffer) const; private: diff --git a/vulkan/include/VulkanRender.h b/vulkan/include/VulkanRender.h index d345e96..12fd7e2 100644 --- a/vulkan/include/VulkanRender.h +++ b/vulkan/include/VulkanRender.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include "VulkanBase.h" @@ -17,6 +18,11 @@ struct DynamicRenderingInfo { VkFormat stencil_format = VK_FORMAT_UNDEFINED; }; +struct DepthBiasInfo { + float constant_factor = 0.0f; + float slope_factor = 0.0f; +}; + class VulkanRender : public VulkanBase { public: VulkanRender(VulkanContext* context, VulkanRenderPass* render_pass); @@ -32,6 +38,7 @@ class VulkanRender : public VulkanBase { virtual VkFrontFace SetFrontFace() const { return VK_FRONT_FACE_CLOCKWISE; } virtual VkBool32 SetDepthTesting() const { return VK_FALSE; } virtual VkBool32 SetDepthWriting() const { return VK_FALSE; } + virtual std::optional SetDepthBias() const { return std::nullopt; } virtual VkPipelineColorBlendAttachmentState SetColorBlendAttachment() const { VkPipelineColorBlendAttachmentState s{}; s.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | diff --git a/vulkan/src/VulkanDynamicRendering.cpp b/vulkan/src/VulkanDynamicRendering.cpp index b626029..02305de 100644 --- a/vulkan/src/VulkanDynamicRendering.cpp +++ b/vulkan/src/VulkanDynamicRendering.cpp @@ -1,5 +1,7 @@ #include "VulkanDynamicRendering.h" +#include + #include "VulkanUtils.h" namespace core { @@ -65,6 +67,43 @@ void VulkanDynamicRendering::BeginDynamicRendering(VkCommandBuffer command_buffe vk_cmd_begin_rendering_(command_buffer, &rendering_info); } +void VulkanDynamicRendering::BeginDepthRendering(VkCommandBuffer command_buffer, + VkImageView depth_image_view, VkExtent2D extent, + VkClearValue depth_clear_value) const { + if (command_buffer == VK_NULL_HANDLE || depth_image_view == VK_NULL_HANDLE) { + throw std::invalid_argument("Depth rendering requires a command buffer and depth image view"); + } + if (extent.width == 0 || extent.height == 0) { + throw std::invalid_argument("Depth rendering requires a non-empty extent"); + } + + const VkRenderingAttachmentInfo depth_attachment{ + .sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO, + .pNext = nullptr, + .imageView = depth_image_view, + .imageLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, + .resolveMode = VK_RESOLVE_MODE_NONE, + .resolveImageView = VK_NULL_HANDLE, + .resolveImageLayout = VK_IMAGE_LAYOUT_UNDEFINED, + .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR, + .storeOp = VK_ATTACHMENT_STORE_OP_STORE, + .clearValue = depth_clear_value, + }; + const VkRenderingInfo rendering_info{ + .sType = VK_STRUCTURE_TYPE_RENDERING_INFO, + .pNext = nullptr, + .flags = 0, + .renderArea = {.offset = {.x = 0, .y = 0}, .extent = extent}, + .layerCount = 1, + .viewMask = 0, + .colorAttachmentCount = 0, + .pColorAttachments = nullptr, + .pDepthAttachment = &depth_attachment, + .pStencilAttachment = nullptr, + }; + vk_cmd_begin_rendering_(command_buffer, &rendering_info); +} + void VulkanDynamicRendering::EndDynamicRendering(VkCommandBuffer command_buffer) const { vk_cmd_end_rendering_(command_buffer); } diff --git a/vulkan/src/VulkanRender.cpp b/vulkan/src/VulkanRender.cpp index 944dc90..de47b9b 100644 --- a/vulkan/src/VulkanRender.cpp +++ b/vulkan/src/VulkanRender.cpp @@ -1,5 +1,8 @@ #include "VulkanRender.h" +#include +#include + namespace core { namespace vulkan { @@ -15,9 +18,40 @@ VulkanRender::VulkanRender(VulkanContext* context, msaa_samples_(msaa_samples) {} void VulkanRender::CreatePipeline() { + const bool has_color_attachment = + render_pass_ != nullptr || !dynamic_rendering_info_.color_formats.empty(); + if (render_pass_ == nullptr) { + if (dynamic_rendering_info_.color_formats.size() > 1) { + throw std::runtime_error("Dynamic rendering currently supports at most one color format"); + } + if (!has_color_attachment && dynamic_rendering_info_.depth_format == VK_FORMAT_UNDEFINED && + dynamic_rendering_info_.stencil_format == VK_FORMAT_UNDEFINED) { + throw std::invalid_argument("Dynamic rendering requires a color, depth or stencil format"); + } + } + + const std::optional depth_bias = SetDepthBias(); + if (depth_bias.has_value() && + (!std::isfinite(depth_bias->constant_factor) || !std::isfinite(depth_bias->slope_factor))) { + throw std::invalid_argument("Depth bias factors must be finite"); + } + + const std::vector vertex_shader_code = LoadVertexShader(); + const std::vector fragment_shader_code = LoadFragmentShader(); + if (vertex_shader_code.empty()) { + throw std::invalid_argument("Graphics pipelines require a vertex shader"); + } + if (fragment_shader_code.empty() && has_color_attachment) { + throw std::invalid_argument( + "Only pipelines without color attachments may omit fragment shaders"); + } + // 1. shader stage - const auto vertex_shader_module = CreateShaderModule(LoadVertexShader()); - const auto fragment_shader_module = CreateShaderModule(LoadFragmentShader()); + const VkShaderModule vertex_shader_module = CreateShaderModule(vertex_shader_code); + VkShaderModule fragment_shader_module = VK_NULL_HANDLE; + if (!fragment_shader_code.empty()) { + fragment_shader_module = CreateShaderModule(fragment_shader_code); + } VkPipelineShaderStageCreateInfo vert_info{}; vert_info.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; @@ -31,7 +65,10 @@ void VulkanRender::CreatePipeline() { frag_info.module = fragment_shader_module; frag_info.pName = "main"; - std::vector shader_stages = {vert_info, frag_info}; + std::vector shader_stages = {vert_info}; + if (fragment_shader_module != VK_NULL_HANDLE) { + shader_stages.push_back(frag_info); + } // 2. vertex input state const auto vertex_bindings = GetVertexBindingDescriptions(); @@ -80,10 +117,11 @@ void VulkanRender::CreatePipeline() { rasterize_state.cullMode = SetCullMode(); rasterize_state.frontFace = SetFrontFace(); - rasterize_state.depthBiasEnable = VK_FALSE; - rasterize_state.depthBiasConstantFactor = 0.0f; + rasterize_state.depthBiasEnable = depth_bias.has_value() ? VK_TRUE : VK_FALSE; + rasterize_state.depthBiasConstantFactor = + depth_bias.has_value() ? depth_bias->constant_factor : 0.0f; rasterize_state.depthBiasClamp = 0.0f; - rasterize_state.depthBiasSlopeFactor = 0.0f; + rasterize_state.depthBiasSlopeFactor = depth_bias.has_value() ? depth_bias->slope_factor : 0.0f; // 6. multisample state VkPipelineMultisampleStateCreateInfo multisample_state{}; @@ -123,8 +161,8 @@ void VulkanRender::CreatePipeline() { color_blending_state.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; color_blending_state.logicOpEnable = VK_FALSE; color_blending_state.logicOp = VK_LOGIC_OP_COPY; - color_blending_state.attachmentCount = 1; - color_blending_state.pAttachments = &color_blend_attachment; + color_blending_state.attachmentCount = has_color_attachment ? 1 : 0; + color_blending_state.pAttachments = has_color_attachment ? &color_blend_attachment : nullptr; color_blending_state.blendConstants[0] = 0.0f; color_blending_state.blendConstants[1] = 0.0f; color_blending_state.blendConstants[2] = 0.0f; @@ -155,19 +193,13 @@ void VulkanRender::CreatePipeline() { pipeline_info.subpass = 0; } else { // Dynamic rendering path - if (dynamic_rendering_info_.color_formats.empty()) { - throw std::runtime_error("Dynamic rendering requires at least one color format"); - } - - if (dynamic_rendering_info_.color_formats.size() != 1) { - throw std::runtime_error("Dynamic rendering currently supports exactly one color format"); - } - rendering_ci.sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO; rendering_ci.pNext = nullptr; rendering_ci.viewMask = 0; - rendering_ci.colorAttachmentCount = 1; - rendering_ci.pColorAttachmentFormats = dynamic_rendering_info_.color_formats.data(); + rendering_ci.colorAttachmentCount = + static_cast(dynamic_rendering_info_.color_formats.size()); + rendering_ci.pColorAttachmentFormats = + has_color_attachment ? dynamic_rendering_info_.color_formats.data() : nullptr; rendering_ci.depthAttachmentFormat = dynamic_rendering_info_.depth_format; rendering_ci.stencilAttachmentFormat = dynamic_rendering_info_.stencil_format;