diff --git a/tests/RHI/CMakeLists.txt b/tests/RHI/CMakeLists.txt index 3557e405..071431f9 100644 --- a/tests/RHI/CMakeLists.txt +++ b/tests/RHI/CMakeLists.txt @@ -50,6 +50,7 @@ add_custom_target(rhi_backend_integration_tests opengl_sphere_test vulkan_minimal_test vulkan_triangle_test + vulkan_quad_test ) add_custom_target(rhi_backend_tests diff --git a/tests/RHI/Vulkan/TEST_SPEC.md b/tests/RHI/Vulkan/TEST_SPEC.md index 784ae65b..d5de8853 100644 --- a/tests/RHI/Vulkan/TEST_SPEC.md +++ b/tests/RHI/Vulkan/TEST_SPEC.md @@ -10,6 +10,11 @@ tests/RHI/Vulkan/ |- TEST_SPEC.md \- integration/ | |- CMakeLists.txt +| |- quad/ +| | |- CMakeLists.txt +| | |- GT.ppm +| | |- main.cpp +| | \- Res/ | \- minimal/ | |- CMakeLists.txt | |- GT.ppm @@ -32,7 +37,7 @@ tests/RHI/Vulkan/ | 类别 | target | | --- | --- | | Vulkan 后端单元测试 | `rhi_vulkan_tests` | -| Vulkan 后端集成测试 | `vulkan_minimal_test` `vulkan_triangle_test` | +| Vulkan 后端集成测试 | `vulkan_minimal_test` `vulkan_triangle_test` `vulkan_quad_test` | ## 3. 当前覆盖 @@ -54,7 +59,7 @@ tests/RHI/Vulkan/ - `tests/RHI/unit/` 继续只承载 RHI 抽象层统一语义测试。 - `tests/RHI/Vulkan/unit/` 承载 Vulkan 专属断言、原生对象检查和需要直接调用 Vulkan API 的测试。 -- `tests/RHI/Vulkan/integration/` 承载 Vulkan 后端直连场景,当前已经落地 `minimal / triangle`,后续再按需要扩到 quad / sphere。 +- `tests/RHI/Vulkan/integration/` 承载 Vulkan 后端直连场景,当前已经落地 `minimal / triangle / quad`,后续再按需要扩到 `sphere`。 ## 5. 推荐执行方式 @@ -62,6 +67,6 @@ tests/RHI/Vulkan/ cmake --build build --config Debug --target rhi_vulkan_tests build\tests\RHI\Vulkan\unit\Debug\rhi_vulkan_tests.exe --gtest_brief=1 -cmake --build build --config Debug --target vulkan_minimal_test -ctest -C Debug -R vulkan_minimal_test --test-dir build +cmake --build build --config Debug --target vulkan_quad_test +ctest -C Debug -R "vulkan_(minimal|triangle|quad)_test" --test-dir build ``` diff --git a/tests/RHI/Vulkan/integration/CMakeLists.txt b/tests/RHI/Vulkan/integration/CMakeLists.txt index e36f96e1..659ccd00 100644 --- a/tests/RHI/Vulkan/integration/CMakeLists.txt +++ b/tests/RHI/Vulkan/integration/CMakeLists.txt @@ -6,3 +6,4 @@ find_package(Python3 REQUIRED) add_subdirectory(minimal) add_subdirectory(triangle) +add_subdirectory(quad) diff --git a/tests/RHI/Vulkan/integration/quad/CMakeLists.txt b/tests/RHI/Vulkan/integration/quad/CMakeLists.txt new file mode 100644 index 00000000..5eda33f8 --- /dev/null +++ b/tests/RHI/Vulkan/integration/quad/CMakeLists.txt @@ -0,0 +1,59 @@ +cmake_minimum_required(VERSION 3.15) + +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) + +project(vulkan_quad_test) + +set(ENGINE_ROOT_DIR ${CMAKE_SOURCE_DIR}/engine) + +if(NOT TARGET Vulkan::Vulkan) + find_package(Vulkan REQUIRED) +endif() + +add_executable(vulkan_quad_test + WIN32 + main.cpp +) + +target_include_directories(vulkan_quad_test PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${ENGINE_ROOT_DIR}/include + ${ENGINE_ROOT_DIR} +) + +target_compile_definitions(vulkan_quad_test PRIVATE + UNICODE + _UNICODE + XCENGINE_SUPPORT_VULKAN +) + +target_link_libraries(vulkan_quad_test PRIVATE + winmm + Vulkan::Vulkan + XCEngine +) + +add_custom_command(TARGET vulkan_quad_test POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_SOURCE_DIR}/tests/RHI/integration/compare_ppm.py + $/ + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_SOURCE_DIR}/tests/RHI/integration/run_integration_test.py + $/ + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_CURRENT_SOURCE_DIR}/GT.ppm + $/ + COMMAND ${CMAKE_COMMAND} -E copy_directory + ${CMAKE_CURRENT_SOURCE_DIR}/Res + $/Res +) + +add_test(NAME vulkan_quad_test + COMMAND ${Python3_EXECUTABLE} $/run_integration_test.py + $ + quad.ppm + ${CMAKE_CURRENT_SOURCE_DIR}/GT.ppm + 0 + Vulkan + WORKING_DIRECTORY $ +) diff --git a/tests/RHI/Vulkan/integration/quad/GT.ppm b/tests/RHI/Vulkan/integration/quad/GT.ppm new file mode 100644 index 00000000..a8920605 Binary files /dev/null and b/tests/RHI/Vulkan/integration/quad/GT.ppm differ diff --git a/tests/RHI/Vulkan/integration/quad/Res/Image/earth.png b/tests/RHI/Vulkan/integration/quad/Res/Image/earth.png new file mode 100644 index 00000000..663081b5 Binary files /dev/null and b/tests/RHI/Vulkan/integration/quad/Res/Image/earth.png differ diff --git a/tests/RHI/Vulkan/integration/quad/main.cpp b/tests/RHI/Vulkan/integration/quad/main.cpp new file mode 100644 index 00000000..3812a8be --- /dev/null +++ b/tests/RHI/Vulkan/integration/quad/main.cpp @@ -0,0 +1,611 @@ +#include + +#include +#include +#include +#include +#include +#include + +#include "XCEngine/Core/Containers/String.h" +#include "XCEngine/Debug/ConsoleLogSink.h" +#include "XCEngine/Debug/Logger.h" +#include "XCEngine/RHI/RHIBuffer.h" +#include "XCEngine/RHI/RHIDescriptorPool.h" +#include "XCEngine/RHI/RHIDescriptorSet.h" +#include "XCEngine/RHI/RHIEnums.h" +#include "XCEngine/RHI/RHIPipelineLayout.h" +#include "XCEngine/RHI/RHIPipelineState.h" +#include "XCEngine/RHI/RHIResourceView.h" +#include "XCEngine/RHI/RHISampler.h" +#include "XCEngine/RHI/RHITexture.h" +#include "XCEngine/RHI/Vulkan/VulkanCommandList.h" +#include "XCEngine/RHI/Vulkan/VulkanCommandQueue.h" +#include "XCEngine/RHI/Vulkan/VulkanDevice.h" +#include "XCEngine/RHI/Vulkan/VulkanScreenshot.h" +#include "XCEngine/RHI/Vulkan/VulkanSwapChain.h" +#include "XCEngine/RHI/Vulkan/VulkanTexture.h" +#include "third_party/stb/stb_image.h" + +using namespace XCEngine::Containers; +using namespace XCEngine::Debug; +using namespace XCEngine::RHI; + +namespace { + +constexpr int kWidth = 1280; +constexpr int kHeight = 720; +constexpr int kTargetFrameCount = 30; + +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 kQuadVertexShader[] = 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 kQuadFragmentShader[] = R"(#version 450 +layout(set = 0, binding = 0) uniform texture2D uTexture; +layout(set = 1, 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); +} +)"; + +VulkanDevice g_device; +VulkanCommandQueue g_commandQueue; +VulkanSwapChain g_swapChain; +VulkanCommandList g_commandList; +VulkanScreenshot g_screenshot; +std::vector g_backBufferViews; + +RHIBuffer* g_vertexBuffer = nullptr; +RHIResourceView* g_vertexBufferView = nullptr; +RHIBuffer* g_indexBuffer = nullptr; +RHIResourceView* g_indexBufferView = nullptr; +RHITexture* g_texture = nullptr; +RHIResourceView* g_textureView = nullptr; +RHISampler* g_sampler = nullptr; +RHIDescriptorPool* g_texturePool = nullptr; +RHIDescriptorSet* g_textureSet = nullptr; +RHIDescriptorPool* g_samplerPool = nullptr; +RHIDescriptorSet* g_samplerSet = nullptr; +RHIPipelineLayout* g_pipelineLayout = nullptr; +RHIPipelineState* g_pipelineState = nullptr; +HWND g_window = nullptr; + +template +void ShutdownAndDelete(T*& object) { + if (object != nullptr) { + object->Shutdown(); + delete object; + object = nullptr; + } +} + +void Log(const char* format, ...) { + char buffer[1024] = {}; + va_list args; + va_start(args, format); + vsnprintf(buffer, sizeof(buffer), format, args); + va_end(args); + Logger::Get().Debug(LogCategory::Rendering, String(buffer)); +} + +std::filesystem::path GetExecutableDirectory() { + char exePath[MAX_PATH] = {}; + const DWORD length = GetModuleFileNameA(nullptr, exePath, MAX_PATH); + if (length == 0 || length >= MAX_PATH) { + return std::filesystem::current_path(); + } + + return std::filesystem::path(exePath).parent_path(); +} + +std::filesystem::path ResolveRuntimePath(const char* relativePath) { + return GetExecutableDirectory() / relativePath; +} + +LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { + switch (msg) { + case WM_CLOSE: + PostQuitMessage(0); + return 0; + default: + return DefWindowProc(hwnd, msg, wParam, lParam); + } +} + +GraphicsPipelineDesc CreateQuadPipelineDesc() { + GraphicsPipelineDesc desc = {}; + desc.pipelineLayout = g_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(kQuadVertexShader, kQuadVertexShader + std::strlen(kQuadVertexShader)); + desc.vertexShader.sourceLanguage = ShaderLanguage::GLSL; + desc.vertexShader.profile = L"vs"; + + desc.fragmentShader.source.assign(kQuadFragmentShader, kQuadFragmentShader + std::strlen(kQuadFragmentShader)); + desc.fragmentShader.sourceLanguage = ShaderLanguage::GLSL; + desc.fragmentShader.profile = L"fs"; + + return desc; +} + +void ShutdownViews() { + for (RHIResourceView* view : g_backBufferViews) { + if (view != nullptr) { + view->Shutdown(); + delete view; + } + } + g_backBufferViews.clear(); +} + +void ShutdownQuadResources() { + ShutdownAndDelete(g_pipelineState); + ShutdownAndDelete(g_pipelineLayout); + ShutdownAndDelete(g_textureSet); + ShutdownAndDelete(g_samplerSet); + ShutdownAndDelete(g_texturePool); + ShutdownAndDelete(g_samplerPool); + ShutdownAndDelete(g_sampler); + ShutdownAndDelete(g_textureView); + ShutdownAndDelete(g_texture); + ShutdownAndDelete(g_vertexBufferView); + ShutdownAndDelete(g_indexBufferView); + ShutdownAndDelete(g_vertexBuffer); + ShutdownAndDelete(g_indexBuffer); +} + +void ShutdownVulkan() { + ShutdownQuadResources(); + ShutdownViews(); + g_commandList.Shutdown(); + g_swapChain.Shutdown(); + g_commandQueue.Shutdown(); + g_device.Shutdown(); +} + +bool InitVulkan() { + RHIDeviceDesc deviceDesc = {}; + deviceDesc.adapterIndex = 0; + deviceDesc.enableDebugLayer = false; + deviceDesc.enableGPUValidation = false; + + if (!g_device.Initialize(deviceDesc)) { + Log("[ERROR] Failed to initialize Vulkan device"); + return false; + } + + if (!g_commandQueue.Initialize(&g_device, CommandQueueType::Direct)) { + Log("[ERROR] Failed to initialize Vulkan command queue"); + return false; + } + + if (!g_swapChain.Initialize(&g_device, &g_commandQueue, g_window, kWidth, kHeight)) { + Log("[ERROR] Failed to initialize Vulkan swap chain"); + return false; + } + + if (!g_commandList.Initialize(&g_device)) { + Log("[ERROR] Failed to initialize Vulkan command list"); + return false; + } + + Log("[INFO] Vulkan initialized successfully"); + return true; +} + +bool LoadQuadTexture() { + const std::filesystem::path texturePath = ResolveRuntimePath("Res/Image/earth.png"); + const std::string texturePathString = texturePath.string(); + + stbi_set_flip_vertically_on_load(0); + + int width = 0; + int height = 0; + int channels = 0; + stbi_uc* pixels = stbi_load(texturePathString.c_str(), &width, &height, &channels, STBI_rgb_alpha); + if (pixels == nullptr) { + Log("[ERROR] Failed to load texture: %s", texturePathString.c_str()); + return false; + } + + TextureDesc textureDesc = {}; + textureDesc.width = static_cast(width); + textureDesc.height = static_cast(height); + textureDesc.depth = 1; + textureDesc.mipLevels = 1; + textureDesc.arraySize = 1; + textureDesc.format = static_cast(Format::R8G8B8A8_UNorm); + textureDesc.textureType = static_cast(TextureType::Texture2D); + textureDesc.sampleCount = 1; + textureDesc.sampleQuality = 0; + textureDesc.flags = 0; + + g_texture = g_device.CreateTexture( + textureDesc, + pixels, + static_cast(width) * static_cast(height) * 4, + static_cast(width) * 4); + stbi_image_free(pixels); + + if (g_texture == nullptr) { + Log("[ERROR] Failed to create texture"); + return false; + } + + ResourceViewDesc textureViewDesc = {}; + textureViewDesc.format = static_cast(Format::R8G8B8A8_UNorm); + textureViewDesc.dimension = ResourceViewDimension::Texture2D; + textureViewDesc.mipLevel = 0; + textureViewDesc.arraySize = 1; + g_textureView = g_device.CreateShaderResourceView(g_texture, textureViewDesc); + if (g_textureView == nullptr) { + Log("[ERROR] Failed to create texture view"); + return false; + } + + 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.mipLodBias = 0.0f; + samplerDesc.maxAnisotropy = 1; + samplerDesc.comparisonFunc = static_cast(ComparisonFunc::Always); + samplerDesc.borderColorR = 0.0f; + samplerDesc.borderColorG = 0.0f; + samplerDesc.borderColorB = 0.0f; + samplerDesc.borderColorA = 0.0f; + samplerDesc.minLod = 0.0f; + samplerDesc.maxLod = 1000.0f; + g_sampler = g_device.CreateSampler(samplerDesc); + if (g_sampler == nullptr) { + Log("[ERROR] Failed to create sampler"); + return false; + } + + DescriptorSetLayoutBinding textureBinding = {}; + textureBinding.binding = 0; + textureBinding.type = static_cast(DescriptorType::SRV); + textureBinding.count = 1; + textureBinding.visibility = static_cast(ShaderVisibility::Pixel); + + DescriptorSetLayoutDesc textureSetLayout = {}; + textureSetLayout.bindings = &textureBinding; + textureSetLayout.bindingCount = 1; + + DescriptorPoolDesc texturePoolDesc = {}; + texturePoolDesc.type = DescriptorHeapType::CBV_SRV_UAV; + texturePoolDesc.descriptorCount = 1; + texturePoolDesc.shaderVisible = true; + g_texturePool = g_device.CreateDescriptorPool(texturePoolDesc); + if (g_texturePool == nullptr) { + Log("[ERROR] Failed to create texture descriptor pool"); + return false; + } + + g_textureSet = g_texturePool->AllocateSet(textureSetLayout); + if (g_textureSet == nullptr) { + Log("[ERROR] Failed to allocate texture descriptor set"); + return false; + } + g_textureSet->Update(0, g_textureView); + + DescriptorSetLayoutBinding samplerBinding = {}; + samplerBinding.binding = 0; + samplerBinding.type = static_cast(DescriptorType::Sampler); + samplerBinding.count = 1; + samplerBinding.visibility = static_cast(ShaderVisibility::Pixel); + + DescriptorSetLayoutDesc samplerSetLayout = {}; + samplerSetLayout.bindings = &samplerBinding; + samplerSetLayout.bindingCount = 1; + + DescriptorPoolDesc samplerPoolDesc = {}; + samplerPoolDesc.type = DescriptorHeapType::Sampler; + samplerPoolDesc.descriptorCount = 1; + samplerPoolDesc.shaderVisible = true; + g_samplerPool = g_device.CreateDescriptorPool(samplerPoolDesc); + if (g_samplerPool == nullptr) { + Log("[ERROR] Failed to create sampler descriptor pool"); + return false; + } + + g_samplerSet = g_samplerPool->AllocateSet(samplerSetLayout); + if (g_samplerSet == nullptr) { + Log("[ERROR] Failed to allocate sampler descriptor set"); + return false; + } + g_samplerSet->UpdateSampler(0, g_sampler); + + DescriptorSetLayoutDesc setLayouts[] = { + textureSetLayout, + samplerSetLayout, + }; + + RHIPipelineLayoutDesc pipelineLayoutDesc = {}; + pipelineLayoutDesc.setLayouts = setLayouts; + pipelineLayoutDesc.setLayoutCount = static_cast(std::size(setLayouts)); + g_pipelineLayout = g_device.CreatePipelineLayout(pipelineLayoutDesc); + if (g_pipelineLayout == nullptr) { + Log("[ERROR] Failed to create pipeline layout"); + return false; + } + + return true; +} + +bool InitQuadResources() { + BufferDesc vertexBufferDesc = {}; + vertexBufferDesc.size = sizeof(kQuadVertices); + vertexBufferDesc.stride = sizeof(Vertex); + vertexBufferDesc.bufferType = static_cast(BufferType::Vertex); + + g_vertexBuffer = g_device.CreateBuffer(vertexBufferDesc); + if (g_vertexBuffer == nullptr) { + Log("[ERROR] Failed to create vertex buffer"); + return false; + } + g_vertexBuffer->SetData(kQuadVertices, sizeof(kQuadVertices)); + g_vertexBuffer->SetStride(sizeof(Vertex)); + g_vertexBuffer->SetBufferType(BufferType::Vertex); + + ResourceViewDesc vertexViewDesc = {}; + vertexViewDesc.dimension = ResourceViewDimension::Buffer; + vertexViewDesc.structureByteStride = sizeof(Vertex); + g_vertexBufferView = g_device.CreateVertexBufferView(g_vertexBuffer, vertexViewDesc); + if (g_vertexBufferView == nullptr) { + Log("[ERROR] Failed to create vertex buffer view"); + return false; + } + + BufferDesc indexBufferDesc = {}; + indexBufferDesc.size = sizeof(kQuadIndices); + indexBufferDesc.stride = sizeof(uint32_t); + indexBufferDesc.bufferType = static_cast(BufferType::Index); + + g_indexBuffer = g_device.CreateBuffer(indexBufferDesc); + if (g_indexBuffer == nullptr) { + Log("[ERROR] Failed to create index buffer"); + return false; + } + g_indexBuffer->SetData(kQuadIndices, sizeof(kQuadIndices)); + g_indexBuffer->SetStride(sizeof(uint32_t)); + g_indexBuffer->SetBufferType(BufferType::Index); + + ResourceViewDesc indexViewDesc = {}; + indexViewDesc.dimension = ResourceViewDimension::Buffer; + indexViewDesc.format = static_cast(Format::R32_UInt); + g_indexBufferView = g_device.CreateIndexBufferView(g_indexBuffer, indexViewDesc); + if (g_indexBufferView == nullptr) { + Log("[ERROR] Failed to create index buffer view"); + return false; + } + + if (!LoadQuadTexture()) { + return false; + } + + GraphicsPipelineDesc pipelineDesc = CreateQuadPipelineDesc(); + g_pipelineState = g_device.CreatePipelineState(pipelineDesc); + if (g_pipelineState == nullptr || !g_pipelineState->IsValid()) { + Log("[ERROR] Failed to create quad pipeline state"); + return false; + } + + Log("[INFO] Quad resources initialized successfully"); + return true; +} + +RHIResourceView* GetCurrentBackBufferView() { + const uint32_t backBufferIndex = g_swapChain.GetCurrentBackBufferIndex(); + if (g_backBufferViews.size() <= backBufferIndex) { + g_backBufferViews.resize(backBufferIndex + 1, nullptr); + } + + if (g_backBufferViews[backBufferIndex] == nullptr) { + ResourceViewDesc viewDesc = {}; + viewDesc.dimension = ResourceViewDimension::Texture2D; + viewDesc.format = static_cast(Format::R8G8B8A8_UNorm); + viewDesc.arraySize = 1; + g_backBufferViews[backBufferIndex] = g_device.CreateRenderTargetView( + g_swapChain.GetCurrentBackBuffer(), + viewDesc); + if (g_backBufferViews[backBufferIndex] == nullptr) { + Log("[ERROR] Failed to create render target view for swap chain image %u", backBufferIndex); + } + } + + return g_backBufferViews[backBufferIndex]; +} + +bool RenderFrame() { + if (!g_swapChain.AcquireNextImage()) { + Log("[ERROR] Failed to acquire next swap chain image"); + return false; + } + + RHIResourceView* renderTargetView = GetCurrentBackBufferView(); + if (renderTargetView == nullptr) { + return false; + } + + g_commandList.Reset(); + g_commandList.SetRenderTargets(1, &renderTargetView, nullptr); + + Viewport viewport = {0.0f, 0.0f, static_cast(kWidth), static_cast(kHeight), 0.0f, 1.0f}; + Rect scissorRect = {0, 0, kWidth, kHeight}; + g_commandList.SetViewport(viewport); + g_commandList.SetScissorRect(scissorRect); + g_commandList.Clear(0.0f, 0.0f, 1.0f, 1.0f, 1); + + g_commandList.SetPipelineState(g_pipelineState); + RHIDescriptorSet* descriptorSets[] = {g_textureSet, g_samplerSet}; + g_commandList.SetGraphicsDescriptorSets(0, static_cast(std::size(descriptorSets)), descriptorSets, g_pipelineLayout); + g_commandList.SetPrimitiveTopology(PrimitiveTopology::TriangleList); + + RHIResourceView* vertexBuffers[] = {g_vertexBufferView}; + uint64_t offsets[] = {0}; + uint32_t strides[] = {sizeof(Vertex)}; + g_commandList.SetVertexBuffers(0, 1, vertexBuffers, offsets, strides); + g_commandList.SetIndexBuffer(g_indexBufferView, 0); + g_commandList.DrawIndexed(static_cast(std::size(kQuadIndices))); + g_commandList.Close(); + + void* commandLists[] = {&g_commandList}; + g_commandQueue.ExecuteCommandLists(1, commandLists); + return true; +} + +} // namespace + +int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int nShowCmd) { + Logger::Get().Initialize(); + Logger::Get().AddSink(std::make_unique()); + Logger::Get().SetMinimumLevel(LogLevel::Debug); + + WNDCLASSEXW wc = {}; + wc.cbSize = sizeof(WNDCLASSEXW); + wc.style = CS_HREDRAW | CS_VREDRAW; + wc.lpfnWndProc = WindowProc; + wc.hInstance = hInstance; + wc.lpszClassName = L"XCEngine_Vulkan_Quad_Test"; + + if (!RegisterClassExW(&wc)) { + Log("[ERROR] Failed to register window class"); + Logger::Get().Shutdown(); + return -1; + } + + RECT rect = {0, 0, kWidth, kHeight}; + AdjustWindowRect(&rect, WS_OVERLAPPEDWINDOW, FALSE); + + g_window = CreateWindowExW( + 0, + L"XCEngine_Vulkan_Quad_Test", + L"Vulkan Quad Integration Test", + WS_OVERLAPPEDWINDOW, + CW_USEDEFAULT, + CW_USEDEFAULT, + rect.right - rect.left, + rect.bottom - rect.top, + nullptr, + nullptr, + hInstance, + nullptr); + + if (g_window == nullptr) { + Log("[ERROR] Failed to create window"); + Logger::Get().Shutdown(); + return -1; + } + + if (!InitVulkan() || !InitQuadResources()) { + ShutdownVulkan(); + DestroyWindow(g_window); + g_window = nullptr; + Logger::Get().Shutdown(); + return -1; + } + + ShowWindow(g_window, nShowCmd); + UpdateWindow(g_window); + + MSG msg = {}; + int frameCount = 0; + int exitCode = 0; + + while (true) { + if (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) { + if (msg.message == WM_QUIT) { + break; + } + TranslateMessage(&msg); + DispatchMessageW(&msg); + continue; + } + + if (!RenderFrame()) { + exitCode = -1; + break; + } + + ++frameCount; + Log("[INFO] Rendered frame %d", frameCount); + + if (frameCount >= kTargetFrameCount) { + g_commandQueue.WaitForIdle(); + if (!g_screenshot.Capture(&g_device, &g_swapChain, "quad.ppm")) { + Log("[ERROR] Failed to capture screenshot"); + exitCode = -1; + } + break; + } + + g_swapChain.Present(0, 0); + } + + ShutdownVulkan(); + + if (g_window != nullptr) { + DestroyWindow(g_window); + g_window = nullptr; + } + + Logger::Get().Shutdown(); + return exitCode; +} diff --git a/tests/TEST_SPEC.md b/tests/TEST_SPEC.md index 7a32a725..2714debf 100644 --- a/tests/TEST_SPEC.md +++ b/tests/TEST_SPEC.md @@ -119,8 +119,8 @@ RHI 当前分为四层测试: - Vulkan 现在已经拥有独立的 `tests/RHI/Vulkan/` 子树。 - `tests/RHI/unit/` 继续只保留三后端参数化的抽象层统一语义测试。 - Vulkan 专属断言、原生句柄检查与直接依赖 Vulkan API 的测试,统一收敛到 `tests/RHI/Vulkan/unit/`。 -- Vulkan 现在已经建立独立的后端 integration 子树,当前先覆盖 `tests/RHI/Vulkan/integration/minimal/`。 -- Vulkan 后端更复杂的 triangle / quad / sphere backend integration 仍可继续追加,但不应再回流到 abstraction suite。 +- Vulkan 现在已经建立独立的后端 integration 子树,当前已覆盖 `tests/RHI/Vulkan/integration/minimal/`、`triangle/`、`quad/`。 +- Vulkan 后端后续仍应继续补齐 `sphere` 等 backend integration,但不应再回流到 abstraction suite。 设计边界: @@ -150,7 +150,7 @@ RHI 当前分为四层测试: | Vulkan 后端单元测试 | `rhi_vulkan_tests` | | D3D12 后端集成测试 | `d3d12_minimal_test` `d3d12_triangle_test` `d3d12_quad_test` `d3d12_sphere_test` | | OpenGL 后端集成测试 | `opengl_minimal_test` `opengl_triangle_test` `opengl_quad_test` `opengl_sphere_test` | -| Vulkan 后端集成测试 | `vulkan_minimal_test` `vulkan_triangle_test` | +| Vulkan 后端集成测试 | `vulkan_minimal_test` `vulkan_triangle_test` `vulkan_quad_test` | ### 5.3 聚合 target @@ -254,7 +254,7 @@ build\tests\RHI\integration\backpack\Debug\rhi_integration_backpack.exe --gtest_ 仍需继续完善: -- 扩展 Vulkan 后端 integration 场景覆盖 +- 继续补齐 Vulkan 后端 integration 的 `sphere` 与更工程化场景覆盖 - 把仍然合理存在的后端专属断言与 skip 场景继续收敛 - 补充 `resize / swapchain 重建 / 长时间 soak / 多线程录制 / validation layer 负例` 等更工程化的测试 - 保持文档、CMake target 与实际测试状态同步