diff --git a/tests/Input/CMakeLists.txt b/tests/Input/CMakeLists.txt new file mode 100644 index 00000000..d6be6e53 --- /dev/null +++ b/tests/Input/CMakeLists.txt @@ -0,0 +1,29 @@ +# ============================================================ +# Input Module Tests +# ============================================================ + +set(INPUT_TEST_SOURCES + test_input_manager.cpp +) + +add_executable(xcengine_input_tests ${INPUT_TEST_SOURCES}) + +if(MSVC) + set_target_properties(xcengine_input_tests PROPERTIES + LINK_FLAGS "/NODEFAULTLIB:libcpmt.lib /NODEFAULTLIB:libcmt.lib" + ) +endif() + +target_link_libraries(xcengine_input_tests + PRIVATE + XCEngine + GTest::gtest + GTest::gtest_main +) + +target_include_directories(xcengine_input_tests PRIVATE + ${CMAKE_SOURCE_DIR}/engine/include + ${CMAKE_SOURCE_DIR}/tests/fixtures +) + +add_test(NAME InputTests COMMAND xcengine_input_tests) diff --git a/tests/Input/test_input_manager.cpp b/tests/Input/test_input_manager.cpp new file mode 100644 index 00000000..2b08ba4e --- /dev/null +++ b/tests/Input/test_input_manager.cpp @@ -0,0 +1,423 @@ +#include +#include +#include +#include +#include +#include +#include + +using namespace XCEngine::Input; +using namespace XCEngine::Math; +using namespace XCEngine::Containers; + +namespace { + +TEST(InputManager, Singleton) { + InputManager& mgr1 = InputManager::Get(); + InputManager& mgr2 = InputManager::Get(); + EXPECT_EQ(&mgr1, &mgr2); +} + +TEST(InputManager, InitializeShutdown) { + InputManager& mgr = InputManager::Get(); + mgr.Initialize(nullptr); + mgr.Shutdown(); +} + +TEST(InputManager, IsKeyDown) { + InputManager& mgr = InputManager::Get(); + mgr.Initialize(nullptr); + + EXPECT_FALSE(mgr.IsKeyDown(KeyCode::A)); + + mgr.ProcessKeyDown(KeyCode::A, false, false, false, false, false); + EXPECT_TRUE(mgr.IsKeyDown(KeyCode::A)); + + mgr.ProcessKeyUp(KeyCode::A, false, false, false, false); + EXPECT_FALSE(mgr.IsKeyDown(KeyCode::A)); + + mgr.Shutdown(); +} + +TEST(InputManager, IsKeyPressed) { + InputManager& mgr = InputManager::Get(); + mgr.Initialize(nullptr); + + EXPECT_FALSE(mgr.IsKeyPressed(KeyCode::A)); + + mgr.ProcessKeyDown(KeyCode::A, false, false, false, false, false); + EXPECT_TRUE(mgr.IsKeyPressed(KeyCode::A)); + + mgr.Update(0.016f); + EXPECT_FALSE(mgr.IsKeyPressed(KeyCode::A)); + + mgr.Shutdown(); +} + +TEST(InputManager, IsKeyUp) { + InputManager& mgr = InputManager::Get(); + mgr.Initialize(nullptr); + + EXPECT_TRUE(mgr.IsKeyUp(KeyCode::A)); + + mgr.ProcessKeyDown(KeyCode::A, false, false, false, false, false); + EXPECT_FALSE(mgr.IsKeyUp(KeyCode::A)); + + mgr.ProcessKeyUp(KeyCode::A, false, false, false, false); + EXPECT_TRUE(mgr.IsKeyUp(KeyCode::A)); + + mgr.Shutdown(); +} + +TEST(InputManager, KeyEventModifiers) { + InputManager& mgr = InputManager::Get(); + mgr.Initialize(nullptr); + + bool eventFired = false; + KeyEvent capturedEvent{}; + + uint64_t id = mgr.OnKeyEvent().Subscribe([&eventFired, &capturedEvent](const KeyEvent& e) { + eventFired = true; + capturedEvent = e; + }); + + mgr.ProcessKeyDown(KeyCode::A, false, true, true, false, false); + EXPECT_TRUE(eventFired); + EXPECT_EQ(capturedEvent.keyCode, KeyCode::A); + EXPECT_TRUE(capturedEvent.alt); + EXPECT_TRUE(capturedEvent.ctrl); + EXPECT_FALSE(capturedEvent.shift); + EXPECT_FALSE(capturedEvent.meta); + EXPECT_EQ(capturedEvent.type, KeyEvent::Type::Down); + + mgr.OnKeyEvent().Unsubscribe(id); + mgr.Shutdown(); +} + +TEST(InputManager, KeyEventRepeat) { + InputManager& mgr = InputManager::Get(); + mgr.Initialize(nullptr); + + bool eventFired = false; + KeyEvent capturedEvent{}; + + uint64_t id = mgr.OnKeyEvent().Subscribe([&eventFired, &capturedEvent](const KeyEvent& e) { + eventFired = true; + capturedEvent = e; + }); + + mgr.ProcessKeyDown(KeyCode::A, true, false, false, false, false); + EXPECT_TRUE(eventFired); + EXPECT_EQ(capturedEvent.type, KeyEvent::Type::Repeat); + + mgr.OnKeyEvent().Unsubscribe(id); + mgr.Shutdown(); +} + +TEST(InputManager, MousePosition) { + InputManager& mgr = InputManager::Get(); + mgr.Initialize(nullptr); + + mgr.ProcessMouseMove(100, 200, 10, -5); + + Vector2 pos = mgr.GetMousePosition(); + EXPECT_EQ(pos.x, 100.0f); + EXPECT_EQ(pos.y, 200.0f); + + Vector2 delta = mgr.GetMouseDelta(); + EXPECT_EQ(delta.x, 10.0f); + EXPECT_EQ(delta.y, -5.0f); + + mgr.Shutdown(); +} + +TEST(InputManager, MouseButton) { + InputManager& mgr = InputManager::Get(); + mgr.Initialize(nullptr); + + EXPECT_FALSE(mgr.IsMouseButtonDown(MouseButton::Left)); + + mgr.ProcessMouseButton(MouseButton::Left, true, 100, 200); + EXPECT_TRUE(mgr.IsMouseButtonDown(MouseButton::Left)); + + mgr.ProcessMouseButton(MouseButton::Left, false, 100, 200); + EXPECT_FALSE(mgr.IsMouseButtonDown(MouseButton::Left)); + + mgr.Shutdown(); +} + +TEST(InputManager, MouseButtonClicked) { + InputManager& mgr = InputManager::Get(); + mgr.Initialize(nullptr); + + EXPECT_FALSE(mgr.IsMouseButtonClicked(MouseButton::Left)); + + mgr.ProcessMouseButton(MouseButton::Left, true, 100, 200); + EXPECT_TRUE(mgr.IsMouseButtonClicked(MouseButton::Left)); + + mgr.Update(0.016f); + EXPECT_FALSE(mgr.IsMouseButtonClicked(MouseButton::Left)); + + mgr.Shutdown(); +} + +TEST(InputManager, MouseButtonEvent) { + InputManager& mgr = InputManager::Get(); + mgr.Initialize(nullptr); + + bool eventFired = false; + MouseButtonEvent capturedEvent{}; + + uint64_t id = mgr.OnMouseButton().Subscribe([&eventFired, &capturedEvent](const MouseButtonEvent& e) { + eventFired = true; + capturedEvent = e; + }); + + mgr.ProcessMouseButton(MouseButton::Left, true, 100, 200); + EXPECT_TRUE(eventFired); + EXPECT_EQ(capturedEvent.button, MouseButton::Left); + EXPECT_EQ(capturedEvent.position.x, 100.0f); + EXPECT_EQ(capturedEvent.position.y, 200.0f); + EXPECT_EQ(capturedEvent.type, MouseButtonEvent::Pressed); + + mgr.OnMouseButton().Unsubscribe(id); + mgr.Shutdown(); +} + +TEST(InputManager, MouseMoveEvent) { + InputManager& mgr = InputManager::Get(); + mgr.Initialize(nullptr); + + bool eventFired = false; + MouseMoveEvent capturedEvent{}; + + uint64_t id = mgr.OnMouseMove().Subscribe([&eventFired, &capturedEvent](const MouseMoveEvent& e) { + eventFired = true; + capturedEvent = e; + }); + + mgr.ProcessMouseMove(100, 200, 10, -5); + EXPECT_TRUE(eventFired); + EXPECT_EQ(capturedEvent.position.x, 100.0f); + EXPECT_EQ(capturedEvent.position.y, 200.0f); + EXPECT_EQ(capturedEvent.delta.x, 10.0f); + EXPECT_EQ(capturedEvent.delta.y, -5.0f); + + mgr.OnMouseMove().Unsubscribe(id); + mgr.Shutdown(); +} + +TEST(InputManager, MouseWheelEvent) { + InputManager& mgr = InputManager::Get(); + mgr.Initialize(nullptr); + + bool eventFired = false; + MouseWheelEvent capturedEvent{}; + + uint64_t id = mgr.OnMouseWheel().Subscribe([&eventFired, &capturedEvent](const MouseWheelEvent& e) { + eventFired = true; + capturedEvent = e; + }); + + mgr.ProcessMouseWheel(1.0f, 100, 200); + EXPECT_TRUE(eventFired); + EXPECT_EQ(capturedEvent.delta, 1.0f); + + mgr.OnMouseWheel().Unsubscribe(id); + mgr.Shutdown(); +} + +TEST(InputManager, DefaultAxes) { + InputManager& mgr = InputManager::Get(); + mgr.Initialize(nullptr); + + float h = mgr.GetAxis("Horizontal"); + EXPECT_GE(h, -1.0f); + EXPECT_LE(h, 1.0f); + + float v = mgr.GetAxis("Vertical"); + EXPECT_GE(v, -1.0f); + EXPECT_LE(v, 1.0f); + + mgr.Shutdown(); +} + +TEST(InputManager, DefaultButtons) { + InputManager& mgr = InputManager::Get(); + mgr.Initialize(nullptr); + + EXPECT_FALSE(mgr.GetButton("Jump")); + + mgr.ProcessKeyDown(KeyCode::Space, false, false, false, false, false); + EXPECT_TRUE(mgr.GetButton("Jump")); + + mgr.ProcessKeyUp(KeyCode::Space, false, false, false, false); + EXPECT_FALSE(mgr.GetButton("Jump")); + + mgr.Shutdown(); +} + +TEST(InputManager, GetButtonDown) { + InputManager& mgr = InputManager::Get(); + mgr.Initialize(nullptr); + + EXPECT_FALSE(mgr.GetButtonDown("Fire1")); + + mgr.ProcessKeyDown(KeyCode::LeftCtrl, false, false, false, false, false); + EXPECT_TRUE(mgr.GetButtonDown("Fire1")); + + mgr.Update(0.016f); + EXPECT_FALSE(mgr.GetButtonDown("Fire1")); + + mgr.Shutdown(); +} + +TEST(InputManager, GetButtonUp) { + InputManager& mgr = InputManager::Get(); + mgr.Initialize(nullptr); + + mgr.ProcessKeyDown(KeyCode::LeftCtrl, false, false, false, false, false); + EXPECT_TRUE(mgr.GetButton("Fire1")); + + mgr.ProcessKeyUp(KeyCode::LeftCtrl, false, false, false, false); + EXPECT_TRUE(mgr.GetButtonUp("Fire1")); + + mgr.Shutdown(); +} + +TEST(InputManager, RegisterAxis) { + InputManager& mgr = InputManager::Get(); + mgr.Initialize(nullptr); + + mgr.ClearAxes(); + + InputAxis axis("TestAxis", KeyCode::W, KeyCode::S); + mgr.RegisterAxis(axis); + + EXPECT_EQ(mgr.GetAxis("TestAxis"), 0.0f); + + mgr.ProcessKeyDown(KeyCode::W, false, false, false, false, false); + EXPECT_EQ(mgr.GetAxis("TestAxis"), 1.0f); + + mgr.ProcessKeyUp(KeyCode::W, false, false, false, false); + EXPECT_EQ(mgr.GetAxis("TestAxis"), 0.0f); + + mgr.ProcessKeyDown(KeyCode::S, false, false, false, false, false); + EXPECT_EQ(mgr.GetAxis("TestAxis"), -1.0f); + + mgr.Shutdown(); +} + +TEST(InputManager, GetAxisRaw) { + InputManager& mgr = InputManager::Get(); + mgr.Initialize(nullptr); + + mgr.ClearAxes(); + InputAxis axis("RawTest", KeyCode::D, KeyCode::A); + mgr.RegisterAxis(axis); + + mgr.ProcessKeyDown(KeyCode::D, false, false, false, false, false); + EXPECT_EQ(mgr.GetAxisRaw("RawTest"), 1.0f); + + mgr.Update(0.016f); + EXPECT_EQ(mgr.GetAxisRaw("RawTest"), 0.0f); + + mgr.Shutdown(); +} + +TEST(InputManager, TextInputEvent) { + InputManager& mgr = InputManager::Get(); + mgr.Initialize(nullptr); + + bool eventFired = false; + TextInputEvent capturedEvent{}; + + uint64_t id = mgr.OnTextInput().Subscribe([&eventFired, &capturedEvent](const TextInputEvent& e) { + eventFired = true; + capturedEvent = e; + }); + + mgr.ProcessTextInput('A'); + EXPECT_TRUE(eventFired); + EXPECT_EQ(capturedEvent.character, 'A'); + + mgr.OnTextInput().Unsubscribe(id); + mgr.Shutdown(); +} + +TEST(InputAxis, DefaultConstruction) { + InputAxis axis; + EXPECT_EQ(axis.GetValue(), 0.0f); + EXPECT_EQ(axis.GetPositiveKey(), KeyCode::None); + EXPECT_EQ(axis.GetNegativeKey(), KeyCode::None); +} + +TEST(InputAxis, PositiveNegativeKeys) { + InputAxis axis("Test", KeyCode::W, KeyCode::S); + EXPECT_EQ(axis.GetPositiveKey(), KeyCode::W); + EXPECT_EQ(axis.GetNegativeKey(), KeyCode::S); +} + +TEST(InputAxis, SetValue) { + InputAxis axis; + axis.SetValue(0.5f); + EXPECT_EQ(axis.GetValue(), 0.5f); +} + +TEST(InputEvent, KeyEventConstruction) { + KeyEvent event; + event.keyCode = KeyCode::A; + event.alt = false; + event.ctrl = false; + event.shift = false; + event.meta = false; + event.type = KeyEvent::Type::Down; + EXPECT_EQ(event.keyCode, KeyCode::A); + EXPECT_FALSE(event.alt); + EXPECT_FALSE(event.ctrl); + EXPECT_FALSE(event.shift); + EXPECT_FALSE(event.meta); + EXPECT_EQ(event.type, KeyEvent::Type::Down); +} + +TEST(InputEvent, MouseButtonEventConstruction) { + MouseButtonEvent event; + event.button = MouseButton::Left; + event.position.x = 100.0f; + event.position.y = 200.0f; + event.type = MouseButtonEvent::Pressed; + EXPECT_EQ(event.button, MouseButton::Left); + EXPECT_EQ(event.position.x, 100.0f); + EXPECT_EQ(event.position.y, 200.0f); + EXPECT_EQ(event.type, MouseButtonEvent::Pressed); +} + +TEST(InputEvent, MouseMoveEventConstruction) { + MouseMoveEvent event; + event.position.x = 100.0f; + event.position.y = 200.0f; + event.delta.x = 10.0f; + event.delta.y = -5.0f; + EXPECT_EQ(event.position.x, 100.0f); + EXPECT_EQ(event.position.y, 200.0f); + EXPECT_EQ(event.delta.x, 10.0f); + EXPECT_EQ(event.delta.y, -5.0f); +} + +TEST(InputEvent, MouseWheelEventConstruction) { + MouseWheelEvent event; + event.position.x = 100.0f; + event.position.y = 200.0f; + event.delta = 1.5f; + EXPECT_EQ(event.position.x, 100.0f); + EXPECT_EQ(event.position.y, 200.0f); + EXPECT_EQ(event.delta, 1.5f); +} + +TEST(InputEvent, TextInputEventConstruction) { + TextInputEvent event; + event.character = 'X'; + EXPECT_EQ(event.character, 'X'); +} + +} // namespace diff --git a/tests/RHI/TEST_ISSUES.md b/tests/RHI/TEST_ISSUES.md new file mode 100644 index 00000000..46b94fc5 --- /dev/null +++ b/tests/RHI/TEST_ISSUES.md @@ -0,0 +1,260 @@ +# RHI 抽象层测试遗留问题报告 + +## 测试概述 + +RHI 抽象层单元测试通过 `RHI_BACKEND` 环境变量选择后端(D3D12/OpenGL),一次编译后可测试两个后端的抽象接口一致性。 + +```bash +# 运行测试 +RHI_BACKEND=D3D12 ./rhi_unit_tests.exe +RHI_BACKEND=OpenGL ./rhi_unit_tests.exe +``` + +## 测试结果汇总 + +| 后端 | 通过 | 失败 | 总计 | +|------|------|------|------| +| D3D12 | 21 | 48 | 69 | +| OpenGL | 未测试 | - | - | + +--- + +## 问题清单 + +### 1. D3D12CommandQueue::CreateCommandQueue 未实现 + +**严重程度**: 高 +**接口**: `RHIDevice::CreateCommandQueue` +**现象**: 返回 `nullptr` +**位置**: `engine/src/RHI/D3D12/D3D12Device.cpp:288` + +```cpp +RHICommandQueue* D3D12Device::CreateCommandQueue(const CommandQueueDesc& desc) { + return nullptr; // 未实现 +} +``` + +**影响测试**: +- `CommandQueue_ExecuteCommandLists` +- `CommandQueue_SignalWaitFence` +- `CommandQueue_GetCompletedValue` +- `CommandQueue_WaitForIdle` +- `CommandQueue_GetType` +- `CommandQueue_GetTimestampFrequency` +- `Device_CreateCommandQueue_ReturnsValid` + +--- + +### 2. D3D12CommandList::CreateCommandList 未实现 + +**严重程度**: 高 +**接口**: `RHIDevice::CreateCommandList` +**现象**: 返回 `nullptr` +**位置**: `engine/src/RHI/D3D12/D3D12Device.cpp:300` + +```cpp +D3D12CommandList* D3D12Device::CreateCommandListImpl(const CommandListDesc& desc) { + return nullptr; // 未实现 +} +``` + +**影响测试**: +- `CommandList_Reset_Close` +- `CommandList_SetPrimitiveTopology` +- `CommandList_SetViewport` +- `CommandList_SetViewports` +- `CommandList_SetScissorRect` +- `CommandList_Draw` +- `CommandList_DrawIndexed` +- `CommandList_ClearRenderTarget` +- `CommandList_SetDepthStencilState` +- `CommandList_SetBlendState` +- `CommandList_SetStencilRef` +- `CommandList_TransitionBarrier` +- `Device_CreateCommandList_ReturnsValid` + +--- + +### 3. Texture 枚举值不匹配 + +**严重程度**: 高 +**接口**: `RHIDevice::CreateTexture` +**现象**: 返回 `nullptr` +**位置**: `engine/src/RHI/D3D12/D3D12Device.cpp:220` + +**原因**: RHI `TextureType` 枚举值与 D3D12 `D3D12_RESOURCE_DIMENSION` 枚举值不匹配 + +| RHI TextureType | 值 | D3D12 D3D12_RESOURCE_DIMENSION | 值 | +|-----------------|-----|----------------------------------|-----| +| Texture1D | 0 | D3D12_RESOURCE_DIMENSION_BUFFER | 0 | +| Texture2D | 1 | D3D12_RESOURCE_DIMENSION_TEXTURE1D | 1 | +| Texture2DArray | 2 | D3D12_RESOURCE_DIMENSION_TEXTURE2D | 2 | +| Texture3D | 3 | D3D12_RESOURCE_DIMENSION_TEXTURE3D | 3 | +| TextureCube | 4 | D3D12_RESOURCE_DIMENSION_TEXTURECUBE | 4 | +| TextureCubeArray | 5 | (无对应) | - | + +**影响测试**: +- `Texture_Create_Texture2D` +- `Texture_Create_Texture3D` +- `Texture_StateManagement` +- `Texture_Naming` +- `Texture_GetNativeHandle` +- `Device_CreateTexture_ReturnsValid` + +--- + +### 4. Shader 编译返回 nullptr + +**严重程度**: 中 +**接口**: `RHIDevice::CompileShader` +**现象**: 返回 `nullptr` +**可能原因**: +- Shader 编译参数不正确 +- 需要有效的 shader 源码或文件路径 + +**影响测试**: +- `Shader_Compile_FromSource` +- `Shader_GetType` +- `Shader_IsValid` +- `Shader_Bind_Unbind` +- `Shader_SetInt` +- `Shader_SetFloat` +- `Shader_SetVec3` +- `Shader_SetVec4` +- `Shader_SetMat4` +- `Shader_GetNativeHandle` + +--- + +### 5. SwapChain 需要窗口句柄 + +**严重程度**: 中 +**接口**: `RHIDevice::CreateSwapChain` +**现象**: 返回 `nullptr` +**原因**: `RHIDeviceDesc` 需要有效的 `windowHandle` + +**影响测试**: +- `SwapChain_Create` +- `SwapChain_GetCurrentBackBufferIndex` +- `SwapChain_GetCurrentBackBuffer` +- `SwapChain_Resize` +- `SwapChain_FullscreenState` +- `SwapChain_ShouldClose` + +--- + +### 6. Buffer::Map 对某些类型返回 nullptr + +**严重程度**: 中 +**接口**: `RHIBuffer::Map` +**现象**: D3D12 Constant Buffer 类型可能无法 Map +**位置**: `tests/RHI/unit/test_buffer.cpp:16` + +```cpp +RHIBuffer* buffer = GetDevice()->CreateBuffer(desc); // desc.bufferType = Vertex +void* data = buffer->Map(); // 返回 nullptr +``` + +**影响测试**: +- `Buffer_Map_Unmap` +- `Buffer_SetData` + +--- + +### 7. RHICapabilities 未填充 + +**严重程度**: 低 +**接口**: `RHIDevice::GetCapabilities` +**现象**: 所有 capability 值为 0 +**位置**: `engine/src/RHI/D3D12/D3D12Device.cpp` + +**影响测试**: +- `Device_GetCapabilities_ReturnsValid` - 断言 `caps.maxRenderTargets >= 1` 等 + +--- + +### 8. RHIDeviceInfo 未填充 + +**严重程度**: 低 +**接口**: `RHIDevice::GetDeviceInfo` +**现象**: `vendor` 和 `renderer` 字符串为空 +**位置**: `engine/src/RHI/D3D12/D3D12Device.cpp:QueryAdapterInfo` + +**影响测试**: +- `Device_GetDeviceInfo_ReturnsValid` + +--- + +### 9. Fence::IsSignaled 逻辑问题 + +**严重程度**: 低 +**接口**: `RHIFence::IsSignaled` +**现象**: 初始值应该为 `false`,但返回 `true` +**位置**: `tests/RHI/unit/test_fence.cpp:80` + +**影响测试**: +- `Fence_IsSignaled` + +--- + +### 10. Sampler::GetNativeHandle 返回 nullptr + +**严重程度**: 低 +**接口**: `RHISampler::GetNativeHandle` +**现象**: 返回 nullptr 但 Sampler 创建成功 +**位置**: `engine/src/RHI/D3D12/D3D12Sampler.cpp` 或 `engine/src/RHI/OpenGL/OpenGLSampler.cpp` + +**影响测试**: +- `Sampler_GetNativeHandle` + +--- + +## 优先级建议 + +### P0 - 必须修复 +1. D3D12CommandQueue::CreateCommandQueue 实现 +2. D3D12CommandList::CreateCommandList 实现 +3. Texture 枚举值对齐 + +### P1 - 应该修复 +4. Shader 编译逻辑 +5. SwapChain 窗口支持 + +### P2 - 可以修复 +6. Buffer Map 行为确认 +7. Capabilities/DeviceInfo 填充 +8. Fence::IsSignaled 逻辑 +9. Sampler::GetNativeHandle + +--- + +## 测试文件位置 + +``` +tests/RHI/unit/ +├── fixtures/RHITestFixture.h/.cpp # 测试框架 +├── test_device.cpp # 设备相关测试 +├── test_buffer.cpp # Buffer 相关测试 +├── test_texture.cpp # Texture 相关测试 +├── test_swap_chain.cpp # SwapChain 相关测试 +├── test_command_list.cpp # CommandList 相关测试 +├── test_command_queue.cpp # CommandQueue 相关测试 +├── test_shader.cpp # Shader 相关测试 +├── test_fence.cpp # Fence 相关测试 +└── test_sampler.cpp # Sampler 相关测试 +``` + +--- + +## 运行测试 + +```bash +# 编译 +cmake --build build --config Debug + +# D3D12 后端测试 +RHI_BACKEND=D3D12 ./build/tests/RHI/unit/Debug/rhi_unit_tests.exe + +# OpenGL 后端测试 +RHI_BACKEND=OpenGL ./build/tests/RHI/unit/Debug/rhi_unit_tests.exe +```