From 3d2fd5c8ee2c9029a647d828682cf73a664664a9 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Sat, 28 Mar 2026 00:17:16 +0800 Subject: [PATCH] Add Vulkan graphics descriptor offset unit test --- tests/RHI/Vulkan/TEST_SPEC.md | 1 + tests/RHI/Vulkan/unit/CMakeLists.txt | 1 + tests/RHI/Vulkan/unit/test_graphics.cpp | 309 ++++++++++++++++++++++++ 3 files changed, 311 insertions(+) create mode 100644 tests/RHI/Vulkan/unit/test_graphics.cpp diff --git a/tests/RHI/Vulkan/TEST_SPEC.md b/tests/RHI/Vulkan/TEST_SPEC.md index 944c2c04..76078249 100644 --- a/tests/RHI/Vulkan/TEST_SPEC.md +++ b/tests/RHI/Vulkan/TEST_SPEC.md @@ -56,6 +56,7 @@ tests/RHI/Vulkan/ - RTV / DSV / SRV 视图创建与原生 image-view 句柄有效性 - `Sampler` 创建与原生 sampler 句柄有效性 - `DescriptorSet` 的原生句柄、绑定布局元数据与常量缓冲脏标记行为 +- graphics draw 到离屏纹理后的像素回读,以及 `SetGraphicsDescriptorSets(firstSet != 0)` 绑定路径 - `PipelineLayout` 的显式 set-layout 聚合与 flat-count 合成逻辑 当前这些 Vulkan backend unit 用例已经按职责拆分为 fixture / render-pass / shader / pipeline / compute 几个文件,避免后续继续堆到单个巨型测试文件中。 diff --git a/tests/RHI/Vulkan/unit/CMakeLists.txt b/tests/RHI/Vulkan/unit/CMakeLists.txt index fffc2d7a..bce6e93e 100644 --- a/tests/RHI/Vulkan/unit/CMakeLists.txt +++ b/tests/RHI/Vulkan/unit/CMakeLists.txt @@ -12,6 +12,7 @@ set(TEST_SOURCES fixtures/VulkanTestFixture.cpp test_compute.cpp test_descriptor_set.cpp + test_graphics.cpp test_pipeline_state.cpp test_pipeline_layout.cpp test_render_pass.cpp diff --git a/tests/RHI/Vulkan/unit/test_graphics.cpp b/tests/RHI/Vulkan/unit/test_graphics.cpp new file mode 100644 index 00000000..f84f20dc --- /dev/null +++ b/tests/RHI/Vulkan/unit/test_graphics.cpp @@ -0,0 +1,309 @@ +#if defined(XCENGINE_SUPPORT_VULKAN) + +#include "fixtures/VulkanTestFixture.h" +#include "XCEngine/RHI/RHIBuffer.h" +#include "XCEngine/RHI/RHIDescriptorPool.h" +#include "XCEngine/RHI/RHIDescriptorSet.h" +#include "XCEngine/RHI/RHIPipelineLayout.h" +#include "XCEngine/RHI/RHIPipelineState.h" +#include "XCEngine/RHI/RHIResourceView.h" +#include "XCEngine/RHI/RHISampler.h" +#include "XCEngine/RHI/Vulkan/VulkanTexture.h" + +#include +#include +#include + +using namespace XCEngine::RHI; + +namespace { + +struct Vertex { + float pos[4]; + float uv[2]; +}; + +constexpr Vertex kQuadVertices[] = { + {{-0.5f, -0.5f, 0.0f, 1.0f}, {0.0f, 1.0f}}, + {{-0.5f, 0.5f, 0.0f, 1.0f}, {0.0f, 0.0f}}, + {{ 0.5f, -0.5f, 0.0f, 1.0f}, {1.0f, 1.0f}}, + {{ 0.5f, 0.5f, 0.0f, 1.0f}, {1.0f, 0.0f}}, +}; + +constexpr uint32_t kQuadIndices[] = {0, 1, 2, 2, 1, 3}; + +const char kVertexShader[] = R"(#version 450 +layout(location = 0) in vec4 aPosition; +layout(location = 1) in vec2 aTexCoord; + +layout(location = 0) out vec2 vTexCoord; + +void main() { + gl_Position = aPosition; + vTexCoord = aTexCoord; +} +)"; + +const char kFragmentShader[] = R"(#version 450 +layout(set = 1, binding = 0) uniform texture2D uTexture; +layout(set = 2, binding = 0) uniform sampler uSampler; + +layout(location = 0) in vec2 vTexCoord; +layout(location = 0) out vec4 fragColor; + +void main() { + fragColor = texture(sampler2D(uTexture, uSampler), vTexCoord); +} +)"; + +GraphicsPipelineDesc CreatePipelineDesc(RHIPipelineLayout* pipelineLayout) { + GraphicsPipelineDesc desc = {}; + desc.pipelineLayout = pipelineLayout; + desc.topologyType = static_cast(PrimitiveTopologyType::Triangle); + desc.renderTargetFormats[0] = static_cast(Format::R8G8B8A8_UNorm); + desc.depthStencilFormat = static_cast(Format::Unknown); + desc.sampleCount = 1; + + desc.rasterizerState.fillMode = static_cast(FillMode::Solid); + desc.rasterizerState.cullMode = static_cast(CullMode::None); + desc.rasterizerState.frontFace = static_cast(FrontFace::CounterClockwise); + desc.rasterizerState.depthClipEnable = true; + + desc.depthStencilState.depthTestEnable = false; + desc.depthStencilState.depthWriteEnable = false; + desc.depthStencilState.stencilEnable = false; + + InputElementDesc position = {}; + position.semanticName = "POSITION"; + position.semanticIndex = 0; + position.format = static_cast(Format::R32G32B32A32_Float); + position.inputSlot = 0; + position.alignedByteOffset = 0; + desc.inputLayout.elements.push_back(position); + + InputElementDesc texcoord = {}; + texcoord.semanticName = "TEXCOORD"; + texcoord.semanticIndex = 0; + texcoord.format = static_cast(Format::R32G32_Float); + texcoord.inputSlot = 0; + texcoord.alignedByteOffset = sizeof(float) * 4; + desc.inputLayout.elements.push_back(texcoord); + + desc.vertexShader.source.assign(kVertexShader, kVertexShader + std::strlen(kVertexShader)); + desc.vertexShader.sourceLanguage = ShaderLanguage::GLSL; + desc.vertexShader.profile = L"vs"; + + desc.fragmentShader.source.assign(kFragmentShader, kFragmentShader + std::strlen(kFragmentShader)); + desc.fragmentShader.sourceLanguage = ShaderLanguage::GLSL; + desc.fragmentShader.profile = L"fs"; + return desc; +} + +TEST_F(VulkanGraphicsFixture, DrawIndexedTexturedQuadWithFirstSetOffsetWritesExpectedPixels) { + constexpr uint32_t kWidth = 64; + constexpr uint32_t kHeight = 64; + + TextureDesc colorDesc = CreateColorTextureDesc(kWidth, kHeight); + RHITexture* colorTexture = m_device->CreateTexture(colorDesc); + ASSERT_NE(colorTexture, nullptr); + + RHIResourceView* renderTargetView = m_device->CreateRenderTargetView(colorTexture, {}); + ASSERT_NE(renderTargetView, nullptr); + + BufferDesc vertexBufferDesc = {}; + vertexBufferDesc.size = sizeof(kQuadVertices); + vertexBufferDesc.stride = sizeof(Vertex); + vertexBufferDesc.bufferType = static_cast(BufferType::Vertex); + RHIBuffer* vertexBuffer = m_device->CreateBuffer(vertexBufferDesc); + ASSERT_NE(vertexBuffer, nullptr); + vertexBuffer->SetData(kQuadVertices, sizeof(kQuadVertices)); + vertexBuffer->SetStride(sizeof(Vertex)); + vertexBuffer->SetBufferType(BufferType::Vertex); + + ResourceViewDesc vertexViewDesc = {}; + vertexViewDesc.dimension = ResourceViewDimension::Buffer; + vertexViewDesc.structureByteStride = sizeof(Vertex); + RHIResourceView* vertexView = m_device->CreateVertexBufferView(vertexBuffer, vertexViewDesc); + ASSERT_NE(vertexView, nullptr); + + BufferDesc indexBufferDesc = {}; + indexBufferDesc.size = sizeof(kQuadIndices); + indexBufferDesc.stride = sizeof(uint32_t); + indexBufferDesc.bufferType = static_cast(BufferType::Index); + RHIBuffer* indexBuffer = m_device->CreateBuffer(indexBufferDesc); + ASSERT_NE(indexBuffer, nullptr); + indexBuffer->SetData(kQuadIndices, sizeof(kQuadIndices)); + indexBuffer->SetStride(sizeof(uint32_t)); + indexBuffer->SetBufferType(BufferType::Index); + + ResourceViewDesc indexViewDesc = {}; + indexViewDesc.dimension = ResourceViewDimension::Buffer; + indexViewDesc.format = static_cast(Format::R32_UInt); + RHIResourceView* indexView = m_device->CreateIndexBufferView(indexBuffer, indexViewDesc); + ASSERT_NE(indexView, nullptr); + + const uint8_t texturePixel[] = {255, 0, 0, 255}; + TextureDesc textureDesc = CreateColorTextureDesc(1, 1); + RHITexture* texture = m_device->CreateTexture(textureDesc, texturePixel, sizeof(texturePixel), 4); + ASSERT_NE(texture, nullptr); + + RHIResourceView* shaderResourceView = m_device->CreateShaderResourceView(texture, {}); + ASSERT_NE(shaderResourceView, nullptr); + + SamplerDesc samplerDesc = {}; + samplerDesc.filter = static_cast(FilterMode::Linear); + samplerDesc.addressU = static_cast(TextureAddressMode::Clamp); + samplerDesc.addressV = static_cast(TextureAddressMode::Clamp); + samplerDesc.addressW = static_cast(TextureAddressMode::Clamp); + samplerDesc.comparisonFunc = static_cast(ComparisonFunc::Always); + samplerDesc.maxAnisotropy = 1; + samplerDesc.minLod = 0.0f; + samplerDesc.maxLod = 1000.0f; + RHISampler* sampler = m_device->CreateSampler(samplerDesc); + ASSERT_NE(sampler, nullptr); + + DescriptorPoolDesc texturePoolDesc = {}; + texturePoolDesc.type = DescriptorHeapType::CBV_SRV_UAV; + texturePoolDesc.descriptorCount = 1; + texturePoolDesc.shaderVisible = true; + RHIDescriptorPool* texturePool = m_device->CreateDescriptorPool(texturePoolDesc); + ASSERT_NE(texturePool, nullptr); + + DescriptorSetLayoutBinding textureBinding = {}; + textureBinding.binding = 0; + textureBinding.type = static_cast(DescriptorType::SRV); + textureBinding.count = 1; + textureBinding.visibility = static_cast(ShaderVisibility::Pixel); + + DescriptorSetLayoutDesc textureLayoutDesc = {}; + textureLayoutDesc.bindings = &textureBinding; + textureLayoutDesc.bindingCount = 1; + + RHIDescriptorSet* textureSet = texturePool->AllocateSet(textureLayoutDesc); + ASSERT_NE(textureSet, nullptr); + textureSet->Update(0, shaderResourceView); + + DescriptorPoolDesc samplerPoolDesc = {}; + samplerPoolDesc.type = DescriptorHeapType::Sampler; + samplerPoolDesc.descriptorCount = 1; + samplerPoolDesc.shaderVisible = true; + RHIDescriptorPool* samplerPool = m_device->CreateDescriptorPool(samplerPoolDesc); + ASSERT_NE(samplerPool, nullptr); + + DescriptorSetLayoutBinding samplerBinding = {}; + samplerBinding.binding = 0; + samplerBinding.type = static_cast(DescriptorType::Sampler); + samplerBinding.count = 1; + samplerBinding.visibility = static_cast(ShaderVisibility::Pixel); + + DescriptorSetLayoutDesc samplerLayoutDesc = {}; + samplerLayoutDesc.bindings = &samplerBinding; + samplerLayoutDesc.bindingCount = 1; + + RHIDescriptorSet* samplerSet = samplerPool->AllocateSet(samplerLayoutDesc); + ASSERT_NE(samplerSet, nullptr); + samplerSet->UpdateSampler(0, sampler); + + DescriptorSetLayoutBinding reservedBinding = {}; + reservedBinding.binding = 0; + reservedBinding.type = static_cast(DescriptorType::CBV); + reservedBinding.count = 1; + reservedBinding.visibility = static_cast(ShaderVisibility::Vertex); + + DescriptorSetLayoutDesc reservedLayoutDesc = {}; + reservedLayoutDesc.bindings = &reservedBinding; + reservedLayoutDesc.bindingCount = 1; + + DescriptorSetLayoutDesc setLayouts[3] = {}; + setLayouts[0] = reservedLayoutDesc; + setLayouts[1] = textureLayoutDesc; + setLayouts[2] = samplerLayoutDesc; + + RHIPipelineLayoutDesc pipelineLayoutDesc = {}; + pipelineLayoutDesc.setLayouts = setLayouts; + pipelineLayoutDesc.setLayoutCount = 3; + RHIPipelineLayout* pipelineLayout = m_device->CreatePipelineLayout(pipelineLayoutDesc); + ASSERT_NE(pipelineLayout, nullptr); + + RHIPipelineState* pipelineState = m_device->CreatePipelineState(CreatePipelineDesc(pipelineLayout)); + ASSERT_NE(pipelineState, nullptr); + ASSERT_TRUE(pipelineState->IsValid()); + + RHICommandList* commandList = CreateCommandList(); + ASSERT_NE(commandList, nullptr); + + commandList->Reset(); + commandList->SetRenderTargets(1, &renderTargetView, nullptr); + commandList->SetViewport({0.0f, 0.0f, static_cast(kWidth), static_cast(kHeight), 0.0f, 1.0f}); + commandList->SetScissorRect({0, 0, static_cast(kWidth), static_cast(kHeight)}); + commandList->Clear(0.0f, 0.0f, 1.0f, 1.0f, 1); + commandList->SetPipelineState(pipelineState); + + RHIDescriptorSet* descriptorSets[] = {textureSet, samplerSet}; + commandList->SetGraphicsDescriptorSets(1, 2, descriptorSets, pipelineLayout); + commandList->SetPrimitiveTopology(PrimitiveTopology::TriangleList); + + RHIResourceView* vertexViews[] = {vertexView}; + uint64_t offsets[] = {0}; + uint32_t strides[] = {sizeof(Vertex)}; + commandList->SetVertexBuffers(0, 1, vertexViews, offsets, strides); + commandList->SetIndexBuffer(indexView, 0); + commandList->DrawIndexed(static_cast(sizeof(kQuadIndices) / sizeof(kQuadIndices[0]))); + SubmitAndWait(commandList); + + const std::vector pixels = ReadTextureRgba8(static_cast(colorTexture)); + ASSERT_EQ(pixels.size(), static_cast(kWidth * kHeight * 4)); + + auto PixelAt = [&](uint32_t x, uint32_t y) -> const uint8_t* { + return pixels.data() + (static_cast(y) * kWidth + x) * 4; + }; + + const uint8_t* centerPixel = PixelAt(kWidth / 2, kHeight / 2); + EXPECT_EQ(centerPixel[0], 255u); + EXPECT_EQ(centerPixel[1], 0u); + EXPECT_EQ(centerPixel[2], 0u); + EXPECT_EQ(centerPixel[3], 255u); + + const uint8_t* cornerPixel = PixelAt(2, 2); + EXPECT_EQ(cornerPixel[0], 0u); + EXPECT_EQ(cornerPixel[1], 0u); + EXPECT_EQ(cornerPixel[2], 255u); + EXPECT_EQ(cornerPixel[3], 255u); + + commandList->Shutdown(); + delete commandList; + pipelineState->Shutdown(); + delete pipelineState; + pipelineLayout->Shutdown(); + delete pipelineLayout; + textureSet->Shutdown(); + delete textureSet; + samplerSet->Shutdown(); + delete samplerSet; + texturePool->Shutdown(); + delete texturePool; + samplerPool->Shutdown(); + delete samplerPool; + sampler->Shutdown(); + delete sampler; + shaderResourceView->Shutdown(); + delete shaderResourceView; + texture->Shutdown(); + delete texture; + indexView->Shutdown(); + delete indexView; + vertexView->Shutdown(); + delete vertexView; + indexBuffer->Shutdown(); + delete indexBuffer; + vertexBuffer->Shutdown(); + delete vertexBuffer; + renderTargetView->Shutdown(); + delete renderTargetView; + colorTexture->Shutdown(); + delete colorTexture; +} + +} // namespace + +#endif