diff --git a/tests/RHI/CMakeLists.txt b/tests/RHI/CMakeLists.txt index b33e861e..51fe44de 100644 --- a/tests/RHI/CMakeLists.txt +++ b/tests/RHI/CMakeLists.txt @@ -48,6 +48,7 @@ add_custom_target(rhi_backend_integration_tests opengl_triangle_test opengl_quad_test opengl_sphere_test + vulkan_minimal_test ) add_custom_target(rhi_backend_tests diff --git a/tests/RHI/Vulkan/CMakeLists.txt b/tests/RHI/Vulkan/CMakeLists.txt index e42efb92..93165c27 100644 --- a/tests/RHI/Vulkan/CMakeLists.txt +++ b/tests/RHI/Vulkan/CMakeLists.txt @@ -6,3 +6,4 @@ set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) add_subdirectory(unit) +add_subdirectory(integration) diff --git a/tests/RHI/Vulkan/TEST_SPEC.md b/tests/RHI/Vulkan/TEST_SPEC.md index 9cf4f385..22330407 100644 --- a/tests/RHI/Vulkan/TEST_SPEC.md +++ b/tests/RHI/Vulkan/TEST_SPEC.md @@ -8,12 +8,20 @@ tests/RHI/Vulkan/ |- CMakeLists.txt |- TEST_SPEC.md +\- integration/ +| |- CMakeLists.txt +| \- minimal/ +| |- CMakeLists.txt +| |- GT.ppm +| \- main.cpp \- unit/ |- CMakeLists.txt |- fixtures/ | |- VulkanTestFixture.cpp | \- VulkanTestFixture.h |- test_compute.cpp + |- test_descriptor_set.cpp + |- test_pipeline_layout.cpp |- test_pipeline_state.cpp |- test_render_pass.cpp \- test_shader.cpp @@ -24,6 +32,7 @@ tests/RHI/Vulkan/ | 类别 | target | | --- | --- | | Vulkan 后端单元测试 | `rhi_vulkan_tests` | +| Vulkan 后端集成测试 | `vulkan_minimal_test` | ## 3. 当前覆盖 @@ -45,11 +54,14 @@ tests/RHI/Vulkan/ - `tests/RHI/unit/` 继续只承载 RHI 抽象层统一语义测试。 - `tests/RHI/Vulkan/unit/` 承载 Vulkan 专属断言、原生对象检查和需要直接调用 Vulkan API 的测试。 -- 如果后续增加 Vulkan 后端集成测试,应在 `tests/RHI/Vulkan/integration/` 下单独建树,而不是继续堆进 generic abstraction suite。 +- `tests/RHI/Vulkan/integration/` 承载 Vulkan 后端直连场景,当前先落地 `minimal`,后续再按需要扩到 triangle / quad / sphere。 ## 5. 推荐执行方式 ```bash 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 ``` diff --git a/tests/RHI/Vulkan/integration/CMakeLists.txt b/tests/RHI/Vulkan/integration/CMakeLists.txt new file mode 100644 index 00000000..ff3b1814 --- /dev/null +++ b/tests/RHI/Vulkan/integration/CMakeLists.txt @@ -0,0 +1,7 @@ +cmake_minimum_required(VERSION 3.15) + +project(vulkan_integration_tests) + +find_package(Python3 REQUIRED) + +add_subdirectory(minimal) diff --git a/tests/RHI/Vulkan/integration/minimal/CMakeLists.txt b/tests/RHI/Vulkan/integration/minimal/CMakeLists.txt new file mode 100644 index 00000000..bc412935 --- /dev/null +++ b/tests/RHI/Vulkan/integration/minimal/CMakeLists.txt @@ -0,0 +1,55 @@ +cmake_minimum_required(VERSION 3.15) + +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) + +project(vulkan_minimal_test) + +set(ENGINE_ROOT_DIR ${CMAKE_SOURCE_DIR}/engine) + +if(NOT TARGET Vulkan::Vulkan) + find_package(Vulkan REQUIRED) +endif() + +add_executable(vulkan_minimal_test + WIN32 + main.cpp +) + +target_include_directories(vulkan_minimal_test PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${ENGINE_ROOT_DIR}/include +) + +target_compile_definitions(vulkan_minimal_test PRIVATE + UNICODE + _UNICODE + XCENGINE_SUPPORT_VULKAN +) + +target_link_libraries(vulkan_minimal_test PRIVATE + winmm + Vulkan::Vulkan + XCEngine +) + +add_custom_command(TARGET vulkan_minimal_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 + $/ +) + +add_test(NAME vulkan_minimal_test + COMMAND ${Python3_EXECUTABLE} $/run_integration_test.py + $ + minimal.ppm + ${CMAKE_CURRENT_SOURCE_DIR}/GT.ppm + 0 + Vulkan + WORKING_DIRECTORY $ +) diff --git a/tests/RHI/Vulkan/integration/minimal/GT.ppm b/tests/RHI/Vulkan/integration/minimal/GT.ppm new file mode 100644 index 00000000..631172b1 Binary files /dev/null and b/tests/RHI/Vulkan/integration/minimal/GT.ppm differ diff --git a/tests/RHI/Vulkan/integration/minimal/main.cpp b/tests/RHI/Vulkan/integration/minimal/main.cpp new file mode 100644 index 00000000..32a0aa04 --- /dev/null +++ b/tests/RHI/Vulkan/integration/minimal/main.cpp @@ -0,0 +1,248 @@ +#include + +#include +#include +#include +#include + +#include "XCEngine/Core/Containers/String.h" +#include "XCEngine/Debug/ConsoleLogSink.h" +#include "XCEngine/Debug/Logger.h" +#include "XCEngine/RHI/RHIEnums.h" +#include "XCEngine/RHI/RHIResourceView.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" + +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; + +VulkanDevice g_device; +VulkanCommandQueue g_commandQueue; +VulkanSwapChain g_swapChain; +VulkanCommandList g_commandList; +VulkanScreenshot g_screenshot; +std::vector g_backBufferViews; +HWND g_window = 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)); +} + +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); + } +} + +void ShutdownViews() { + for (RHIResourceView* view : g_backBufferViews) { + if (view != nullptr) { + view->Shutdown(); + delete view; + } + } + g_backBufferViews.clear(); +} + +void ShutdownVulkan() { + 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; +} + +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(1.0f, 0.0f, 0.0f, 1.0f, 1); + 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_Minimal_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_Minimal_Test", + L"Vulkan Minimal 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()) { + 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, "minimal.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 0fb4b160..c56c1aba 100644 --- a/tests/TEST_SPEC.md +++ b/tests/TEST_SPEC.md @@ -119,7 +119,8 @@ RHI 当前分为四层测试: - Vulkan 现在已经拥有独立的 `tests/RHI/Vulkan/` 子树。 - `tests/RHI/unit/` 继续只保留三后端参数化的抽象层统一语义测试。 - Vulkan 专属断言、原生句柄检查与直接依赖 Vulkan API 的测试,统一收敛到 `tests/RHI/Vulkan/unit/`。 -- Vulkan 目前尚未建立独立的后端 integration 子树;如后续需要,应在 `tests/RHI/Vulkan/integration/` 下继续扩展,而不是再回流到 abstraction suite。 +- Vulkan 现在已经建立独立的后端 integration 子树,当前先覆盖 `tests/RHI/Vulkan/integration/minimal/`。 +- Vulkan 后端更复杂的 triangle / quad / sphere backend integration 仍可继续追加,但不应再回流到 abstraction suite。 设计边界: @@ -149,6 +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` | ### 5.3 聚合 target @@ -252,7 +254,7 @@ build\tests\RHI\integration\backpack\Debug\rhi_integration_backpack.exe --gtest_ 仍需继续完善: -- 为 Vulkan 增加后端 integration 测试树 +- 扩展 Vulkan 后端 integration 场景覆盖 - 把仍然合理存在的后端专属断言与 skip 场景继续收敛 - 补充 `resize / swapchain 重建 / 长时间 soak / 多线程录制 / validation layer 负例` 等更工程化的测试 - 保持文档、CMake target 与实际测试状态同步