diff --git a/tests/RHI/integration/CMakeLists.txt b/tests/RHI/integration/CMakeLists.txt index cab68661..862922b8 100644 --- a/tests/RHI/integration/CMakeLists.txt +++ b/tests/RHI/integration/CMakeLists.txt @@ -4,4 +4,5 @@ find_package(GTest REQUIRED) enable_testing() -add_subdirectory(minimal) \ No newline at end of file +add_subdirectory(minimal) +add_subdirectory(triangle) diff --git a/tests/RHI/integration/triangle/CMakeLists.txt b/tests/RHI/integration/triangle/CMakeLists.txt new file mode 100644 index 00000000..e4693a38 --- /dev/null +++ b/tests/RHI/integration/triangle/CMakeLists.txt @@ -0,0 +1,56 @@ +cmake_minimum_required(VERSION 3.15) + +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) + +project(rhi_integration_triangle) + +set(ENGINE_ROOT_DIR ${CMAKE_SOURCE_DIR}/engine) +set(PACKAGE_DIR ${CMAKE_SOURCE_DIR}/tests/opengl/package) + +get_filename_component(PROJECT_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../.. ABSOLUTE) + +add_executable(rhi_integration_triangle + main.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../fixtures/RHIIntegrationFixture.cpp + ${PACKAGE_DIR}/src/glad.c +) + +target_include_directories(rhi_integration_triangle PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/../fixtures + ${ENGINE_ROOT_DIR}/include + ${PACKAGE_DIR}/include + ${PROJECT_ROOT_DIR}/engine/src +) + +target_link_libraries(rhi_integration_triangle PRIVATE + d3d12 + dxgi + d3dcompiler + winmm + opengl32 + XCEngine + GTest::gtest +) + +target_compile_definitions(rhi_integration_triangle PRIVATE + UNICODE + _UNICODE + XCENGINE_SUPPORT_OPENGL + XCENGINE_SUPPORT_D3D12 +) + +add_custom_command(TARGET rhi_integration_triangle 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_CURRENT_SOURCE_DIR}/GT.ppm + $/GT.ppm + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${ENGINE_ROOT_DIR}/third_party/renderdoc/renderdoc.dll + $/ +) + +include(GoogleTest) +gtest_discover_tests(rhi_integration_triangle) diff --git a/tests/RHI/integration/triangle/GT.ppm b/tests/RHI/integration/triangle/GT.ppm new file mode 100644 index 00000000..6fd275e4 Binary files /dev/null and b/tests/RHI/integration/triangle/GT.ppm differ diff --git a/tests/RHI/integration/triangle/main.cpp b/tests/RHI/integration/triangle/main.cpp new file mode 100644 index 00000000..a15fc4de --- /dev/null +++ b/tests/RHI/integration/triangle/main.cpp @@ -0,0 +1,286 @@ +#include +#include +#include +#include + +#include + +#include "../fixtures/RHIIntegrationFixture.h" +#include "XCEngine/Debug/ConsoleLogSink.h" +#include "XCEngine/Debug/Logger.h" +#include "XCEngine/RHI/RHIBuffer.h" +#include "XCEngine/RHI/RHIPipelineState.h" +#include "XCEngine/RHI/RHIResourceView.h" + +using namespace XCEngine::Debug; +using namespace XCEngine::RHI; +using namespace XCEngine::RHI::Integration; + +namespace { + +struct Vertex { + float pos[4]; + float col[4]; +}; + +constexpr Vertex kTriangleVertices[] = { + { { 0.0f, 0.5f, 0.0f, 1.0f }, { 1.0f, 0.0f, 0.0f, 1.0f } }, + { { -0.5f, -0.5f, 0.0f, 1.0f }, { 0.0f, 1.0f, 0.0f, 1.0f } }, + { { 0.5f, -0.5f, 0.0f, 1.0f }, { 0.0f, 0.0f, 1.0f, 1.0f } }, +}; + +const char kTriangleHlsl[] = R"( +struct VSInput { + float4 position : POSITION; + float4 color : COLOR; +}; + +struct PSInput { + float4 position : SV_POSITION; + float4 color : COLOR; +}; + +PSInput MainVS(VSInput input) { + PSInput output; + output.position = input.position; + output.color = input.color; + return output; +} + +float4 MainPS(PSInput input) : SV_TARGET { + return input.color; +} +)"; + +const char kTriangleVertexShader[] = R"(#version 430 +layout(location = 0) in vec4 aPosition; +layout(location = 1) in vec4 aColor; + +out vec4 vColor; + +void main() { + gl_Position = aPosition; + vColor = aColor; +} +)"; + +const char kTriangleFragmentShader[] = R"(#version 430 +in vec4 vColor; + +layout(location = 0) out vec4 fragColor; + +void main() { + fragColor = vColor; +} +)"; + +const char* GetScreenshotFilename(RHIType type) { + return type == RHIType::D3D12 ? "triangle_d3d12.ppm" : "triangle_opengl.ppm"; +} + +int GetComparisonThreshold(RHIType type) { + return type == RHIType::OpenGL ? 5 : 0; +} + +GraphicsPipelineDesc CreateTrianglePipelineDesc(RHIType type) { + GraphicsPipelineDesc desc = {}; + 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 color = {}; + color.semanticName = "COLOR"; + color.semanticIndex = 0; + color.format = static_cast(Format::R32G32B32A32_Float); + color.inputSlot = 0; + color.alignedByteOffset = sizeof(float) * 4; + desc.inputLayout.elements.push_back(color); + + if (type == RHIType::D3D12) { + desc.vertexShader.source.assign(kTriangleHlsl, kTriangleHlsl + strlen(kTriangleHlsl)); + desc.vertexShader.sourceLanguage = ShaderLanguage::HLSL; + desc.vertexShader.entryPoint = L"MainVS"; + desc.vertexShader.profile = L"vs_5_0"; + + desc.fragmentShader.source.assign(kTriangleHlsl, kTriangleHlsl + strlen(kTriangleHlsl)); + desc.fragmentShader.sourceLanguage = ShaderLanguage::HLSL; + desc.fragmentShader.entryPoint = L"MainPS"; + desc.fragmentShader.profile = L"ps_5_0"; + } else { + desc.vertexShader.source.assign(kTriangleVertexShader, kTriangleVertexShader + strlen(kTriangleVertexShader)); + desc.vertexShader.sourceLanguage = ShaderLanguage::GLSL; + desc.vertexShader.profile = L"vs_4_30"; + + desc.fragmentShader.source.assign(kTriangleFragmentShader, kTriangleFragmentShader + strlen(kTriangleFragmentShader)); + desc.fragmentShader.sourceLanguage = ShaderLanguage::GLSL; + desc.fragmentShader.profile = L"fs_4_30"; + } + + return desc; +} + +class TriangleTest : public RHIIntegrationFixture { +protected: + void SetUp() override; + void TearDown() override; + void RenderFrame() override; + +private: + void InitializeTriangleResources(); + void ShutdownTriangleResources(); + + RHIBuffer* mVertexBuffer = nullptr; + RHIResourceView* mVertexBufferView = nullptr; + RHIPipelineState* mPipelineState = nullptr; +}; + +void TriangleTest::SetUp() { + RHIIntegrationFixture::SetUp(); + InitializeTriangleResources(); +} + +void TriangleTest::TearDown() { + ShutdownTriangleResources(); + RHIIntegrationFixture::TearDown(); +} + +void TriangleTest::InitializeTriangleResources() { + BufferDesc bufferDesc = {}; + bufferDesc.size = sizeof(kTriangleVertices); + bufferDesc.stride = sizeof(Vertex); + bufferDesc.bufferType = static_cast(BufferType::Vertex); + + mVertexBuffer = GetDevice()->CreateBuffer(bufferDesc); + ASSERT_NE(mVertexBuffer, nullptr); + mVertexBuffer->SetData(kTriangleVertices, sizeof(kTriangleVertices)); + mVertexBuffer->SetStride(sizeof(Vertex)); + mVertexBuffer->SetBufferType(BufferType::Vertex); + + ResourceViewDesc viewDesc = {}; + viewDesc.dimension = ResourceViewDimension::Buffer; + viewDesc.structureByteStride = sizeof(Vertex); + mVertexBufferView = GetDevice()->CreateVertexBufferView(mVertexBuffer, viewDesc); + ASSERT_NE(mVertexBufferView, nullptr); + + GraphicsPipelineDesc pipelineDesc = CreateTrianglePipelineDesc(GetBackendType()); + mPipelineState = GetDevice()->CreatePipelineState(pipelineDesc); + ASSERT_NE(mPipelineState, nullptr); + ASSERT_TRUE(mPipelineState->IsValid()); + + Log("[TEST] Triangle resources initialized for backend %d", static_cast(GetBackendType())); +} + +void TriangleTest::ShutdownTriangleResources() { + if (mPipelineState != nullptr) { + mPipelineState->Shutdown(); + delete mPipelineState; + mPipelineState = nullptr; + } + + if (mVertexBufferView != nullptr) { + mVertexBufferView->Shutdown(); + delete mVertexBufferView; + mVertexBufferView = nullptr; + } + + if (mVertexBuffer != nullptr) { + mVertexBuffer->Shutdown(); + delete mVertexBuffer; + mVertexBuffer = nullptr; + } +} + +void TriangleTest::RenderFrame() { + ASSERT_NE(mPipelineState, nullptr); + ASSERT_NE(mVertexBufferView, nullptr); + + RHICommandList* cmdList = GetCommandList(); + RHICommandQueue* cmdQueue = GetCommandQueue(); + ASSERT_NE(cmdList, nullptr); + ASSERT_NE(cmdQueue, nullptr); + + cmdList->Reset(); + SetRenderTargetForClear(); + + Viewport viewport = { 0.0f, 0.0f, 1280.0f, 720.0f, 0.0f, 1.0f }; + Rect scissorRect = { 0, 0, 1280, 720 }; + cmdList->SetViewport(viewport); + cmdList->SetScissorRect(scissorRect); + cmdList->Clear(0.0f, 0.0f, 1.0f, 1.0f, 1); + + cmdList->SetPipelineState(mPipelineState); + cmdList->SetPrimitiveTopology(PrimitiveTopology::TriangleList); + + RHIResourceView* vertexBuffers[] = { mVertexBufferView }; + uint64_t offsets[] = { 0 }; + uint32_t strides[] = { sizeof(Vertex) }; + cmdList->SetVertexBuffers(0, 1, vertexBuffers, offsets, strides); + cmdList->Draw(3); + + EndRender(); + + cmdList->Close(); + void* cmdLists[] = { cmdList }; + cmdQueue->ExecuteCommandLists(1, cmdLists); +} + +TEST_P(TriangleTest, RenderTriangle) { + RHICommandQueue* cmdQueue = GetCommandQueue(); + RHISwapChain* swapChain = GetSwapChain(); + const int targetFrameCount = 30; + const char* screenshotFilename = GetScreenshotFilename(GetBackendType()); + const int comparisonThreshold = GetComparisonThreshold(GetBackendType()); + + for (int frameCount = 0; frameCount <= targetFrameCount; ++frameCount) { + if (frameCount > 0) { + cmdQueue->WaitForPreviousFrame(); + } + + Log("[TEST] Triangle MainLoop: frame %d", frameCount); + BeginRender(); + RenderFrame(); + + if (frameCount >= targetFrameCount) { + cmdQueue->WaitForIdle(); + Log("[TEST] Triangle MainLoop: frame %d reached, capturing %s", frameCount, screenshotFilename); + ASSERT_TRUE(TakeScreenshot(screenshotFilename)); + ASSERT_TRUE(CompareWithGoldenTemplate(screenshotFilename, "GT.ppm", static_cast(comparisonThreshold))); + Log("[TEST] Triangle MainLoop: frame %d compare passed", frameCount); + break; + } + + swapChain->Present(0, 0); + } +} + +} // namespace + +INSTANTIATE_TEST_SUITE_P(D3D12, TriangleTest, ::testing::Values(RHIType::D3D12)); +INSTANTIATE_TEST_SUITE_P(OpenGL, TriangleTest, ::testing::Values(RHIType::OpenGL)); + +GTEST_API_ int main(int argc, char** argv) { + Logger::Get().Initialize(); + Logger::Get().AddSink(std::make_unique()); + Logger::Get().SetMinimumLevel(LogLevel::Debug); + + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +}