diff --git a/docs/plan/Shader与Material系统下一阶段计划.md b/docs/plan/Shader与Material系统下一阶段计划.md index cff645fc..3a55c7e5 100644 --- a/docs/plan/Shader与Material系统下一阶段计划.md +++ b/docs/plan/Shader与Material系统下一阶段计划.md @@ -393,7 +393,15 @@ Unity-like Shader Authoring (.shader) - D3D12:通过 - OpenGL:通过 - Vulkan:通过 -- 下一步:补 `rendering_integration_object_id_scene` 或等价的 object-id integration coverage,完成本阶段收口 +- 已完成:`rendering_integration_object_id_scene` + - 新增独立的 object-id integration scene,直接验证 object-id 输出采样点,而不是再维护一张新的大尺寸 GT 图片 + - 覆盖 `CameraRenderer + BuiltinObjectIdPass` 路径,以及 `Forward -> ObjectId -> Copy/Screenshot` 的跨 pass 回归 + - 修正了 `BuiltinObjectIdPass` 在 Vulkan 下的顶点输入步长问题,并让 object-id pass 自己清理/写入 depth,避免依赖前一 pass 的 depth 复用状态 +- 已验证:`rendering_integration_object_id_scene` + - D3D12:通过 + - OpenGL:通过 + - Vulkan:通过 +- 阶段 E 当前状态:`unlit_scene` 与 `object_id_scene` 均已完成并通过三后端验证,本阶段可以收口 ## 7. 当前阶段明确不做 diff --git a/engine/src/Rendering/Passes/BuiltinObjectIdPass.cpp b/engine/src/Rendering/Passes/BuiltinObjectIdPass.cpp index 5a91b0b4..c0981131 100644 --- a/engine/src/Rendering/Passes/BuiltinObjectIdPass.cpp +++ b/engine/src/Rendering/Passes/BuiltinObjectIdPass.cpp @@ -73,7 +73,7 @@ RHI::GraphicsPipelineDesc CreatePipelineDesc( pipelineDesc.blendState.colorWriteMask = static_cast(RHI::ColorWriteMask::All); pipelineDesc.depthStencilState.depthTestEnable = true; - pipelineDesc.depthStencilState.depthWriteEnable = false; + pipelineDesc.depthStencilState.depthWriteEnable = true; pipelineDesc.depthStencilState.depthFunc = static_cast(RHI::ComparisonFunc::LessEqual); const Resources::ShaderBackend backend = ::XCEngine::Rendering::Detail::ToShaderBackend(backendType); @@ -105,6 +105,23 @@ RHI::InputLayoutDesc BuiltinObjectIdPass::BuildInputLayout() { position.inputSlot = 0; position.alignedByteOffset = static_cast(offsetof(Resources::StaticMeshVertex, position)); inputLayout.elements.push_back(position); + + RHI::InputElementDesc normal = {}; + normal.semanticName = "NORMAL"; + normal.semanticIndex = 0; + normal.format = static_cast(RHI::Format::R32G32B32_Float); + normal.inputSlot = 0; + normal.alignedByteOffset = static_cast(offsetof(Resources::StaticMeshVertex, normal)); + inputLayout.elements.push_back(normal); + + RHI::InputElementDesc texcoord = {}; + texcoord.semanticName = "TEXCOORD"; + texcoord.semanticIndex = 0; + texcoord.format = static_cast(RHI::Format::R32G32_Float); + texcoord.inputSlot = 0; + texcoord.alignedByteOffset = static_cast(offsetof(Resources::StaticMeshVertex, uv0)); + inputLayout.elements.push_back(texcoord); + return inputLayout; } @@ -162,6 +179,7 @@ bool BuiltinObjectIdPass::Render( commandList->SetViewport(viewport); commandList->SetScissorRect(scissorRect); commandList->ClearRenderTarget(renderTarget, clearColor, 1, clearRects); + commandList->ClearDepthStencil(surface.GetDepthAttachment(), 1.0f, 0, 1, clearRects); commandList->SetPrimitiveTopology(RHI::PrimitiveTopology::TriangleList); commandList->SetPipelineState(m_pipelineState); @@ -169,6 +187,8 @@ bool BuiltinObjectIdPass::Render( DrawVisibleItem(context, sceneData, visibleItem); } + commandList->EndRenderPass(); + if (surface.IsAutoTransitionEnabled()) { commandList->TransitionBarrier( renderTarget, diff --git a/engine/src/Rendering/Pipelines/BuiltinForwardPipeline.cpp b/engine/src/Rendering/Pipelines/BuiltinForwardPipeline.cpp index 04f81c09..f755ecd3 100644 --- a/engine/src/Rendering/Pipelines/BuiltinForwardPipeline.cpp +++ b/engine/src/Rendering/Pipelines/BuiltinForwardPipeline.cpp @@ -370,6 +370,7 @@ bool BuiltinForwardPipeline::ExecuteForwardOpaquePass(const RenderPassContext& p } if (surface.IsAutoTransitionEnabled()) { + commandList->EndRenderPass(); for (RHI::RHIResourceView* renderTarget : renderTargets) { if (renderTarget != nullptr) { commandList->TransitionBarrier( diff --git a/tests/Rendering/CMakeLists.txt b/tests/Rendering/CMakeLists.txt index 57aa2fe7..f3a4bc85 100644 --- a/tests/Rendering/CMakeLists.txt +++ b/tests/Rendering/CMakeLists.txt @@ -14,6 +14,7 @@ add_custom_target(rendering_integration_tests DEPENDS rendering_integration_textured_quad_scene rendering_integration_unlit_scene + rendering_integration_object_id_scene rendering_integration_backpack_scene rendering_integration_backpack_lit_scene rendering_integration_camera_stack_scene diff --git a/tests/Rendering/integration/CMakeLists.txt b/tests/Rendering/integration/CMakeLists.txt index 236a2a6d..541e3778 100644 --- a/tests/Rendering/integration/CMakeLists.txt +++ b/tests/Rendering/integration/CMakeLists.txt @@ -4,6 +4,7 @@ project(XCEngine_RenderingIntegrationTests) add_subdirectory(textured_quad_scene) add_subdirectory(unlit_scene) +add_subdirectory(object_id_scene) add_subdirectory(backpack_scene) add_subdirectory(backpack_lit_scene) add_subdirectory(camera_stack_scene) diff --git a/tests/Rendering/integration/object_id_scene/CMakeLists.txt b/tests/Rendering/integration/object_id_scene/CMakeLists.txt new file mode 100644 index 00000000..a5ada510 --- /dev/null +++ b/tests/Rendering/integration/object_id_scene/CMakeLists.txt @@ -0,0 +1,57 @@ +cmake_minimum_required(VERSION 3.15) + +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) + +project(rendering_integration_object_id_scene) + +set(ENGINE_ROOT_DIR ${CMAKE_SOURCE_DIR}/engine) +set(PACKAGE_DIR ${CMAKE_SOURCE_DIR}/mvs/OpenGL/package) + +get_filename_component(PROJECT_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../.. ABSOLUTE) + +find_package(Vulkan QUIET) + +add_executable(rendering_integration_object_id_scene + main.cpp + ${CMAKE_SOURCE_DIR}/tests/RHI/integration/fixtures/RHIIntegrationFixture.cpp + ${PACKAGE_DIR}/src/glad.c +) + +target_include_directories(rendering_integration_object_id_scene PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_SOURCE_DIR}/tests/RHI/integration/fixtures + ${ENGINE_ROOT_DIR}/include + ${PACKAGE_DIR}/include + ${PROJECT_ROOT_DIR}/engine/src +) + +target_link_libraries(rendering_integration_object_id_scene PRIVATE + d3d12 + dxgi + d3dcompiler + winmm + opengl32 + XCEngine + GTest::gtest +) + +if(TARGET Vulkan::Vulkan) + target_link_libraries(rendering_integration_object_id_scene PRIVATE Vulkan::Vulkan) + target_compile_definitions(rendering_integration_object_id_scene PRIVATE XCENGINE_SUPPORT_VULKAN) +endif() + +target_compile_definitions(rendering_integration_object_id_scene PRIVATE + UNICODE + _UNICODE + XCENGINE_SUPPORT_OPENGL + XCENGINE_SUPPORT_D3D12 +) + +add_custom_command(TARGET rendering_integration_object_id_scene POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${ENGINE_ROOT_DIR}/third_party/renderdoc/renderdoc.dll + $/ +) + +include(GoogleTest) +gtest_discover_tests(rendering_integration_object_id_scene) diff --git a/tests/Rendering/integration/object_id_scene/main.cpp b/tests/Rendering/integration/object_id_scene/main.cpp new file mode 100644 index 00000000..9b54ef48 --- /dev/null +++ b/tests/Rendering/integration/object_id_scene/main.cpp @@ -0,0 +1,593 @@ +#include + +#include "../RenderingIntegrationMain.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../../RHI/integration/fixtures/RHIIntegrationFixture.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace XCEngine::Components; +using namespace XCEngine::Debug; +using namespace XCEngine::Math; +using namespace XCEngine::Rendering; +using namespace XCEngine::Resources; +using namespace XCEngine::RHI; +using namespace XCEngine::RHI::Integration; + +namespace { + +constexpr const char* kD3D12Screenshot = "object_id_scene_d3d12.ppm"; +constexpr const char* kOpenGLScreenshot = "object_id_scene_opengl.ppm"; +constexpr const char* kVulkanScreenshot = "object_id_scene_vulkan.ppm"; +constexpr uint32_t kFrameWidth = 1280; +constexpr uint32_t kFrameHeight = 720; + +struct PpmImage { + uint32_t width = 0; + uint32_t height = 0; + std::vector rgb; + + std::array GetPixel(uint32_t x, uint32_t y) const { + const size_t index = (static_cast(y) * width + x) * 3u; + return { rgb[index + 0], rgb[index + 1], rgb[index + 2] }; + } +}; + +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* path) { + std::filesystem::path resolved(path); + if (resolved.is_absolute()) { + return resolved; + } + + return GetExecutableDirectory() / resolved; +} + +std::string ReadNextPpmToken(std::istream& stream) { + std::string token; + while (stream >> token) { + if (!token.empty() && token[0] == '#') { + std::string ignored; + std::getline(stream, ignored); + continue; + } + + return token; + } + + return {}; +} + +PpmImage LoadPpmImage(const std::filesystem::path& path) { + std::ifstream file(path, std::ios::binary); + EXPECT_TRUE(file.is_open()) << path.string(); + + PpmImage image; + if (!file.is_open()) { + return image; + } + + const std::string magic = ReadNextPpmToken(file); + EXPECT_EQ(magic, "P6"); + if (magic != "P6") { + return image; + } + + const std::string widthToken = ReadNextPpmToken(file); + const std::string heightToken = ReadNextPpmToken(file); + const std::string maxValueToken = ReadNextPpmToken(file); + EXPECT_FALSE(widthToken.empty()); + EXPECT_FALSE(heightToken.empty()); + EXPECT_FALSE(maxValueToken.empty()); + if (widthToken.empty() || heightToken.empty() || maxValueToken.empty()) { + return image; + } + + image.width = static_cast(std::stoul(widthToken)); + image.height = static_cast(std::stoul(heightToken)); + EXPECT_EQ(std::stoul(maxValueToken), 255u); + + file.get(); + image.rgb.resize(static_cast(image.width) * image.height * 3u); + file.read(reinterpret_cast(image.rgb.data()), static_cast(image.rgb.size())); + EXPECT_EQ(file.gcount(), static_cast(image.rgb.size())); + return image; +} + +std::array EncodeObjectIdToRgb(uint64_t objectId) { + const uint32_t encodedId = EncodeObjectIdToUInt32(objectId); + return { + static_cast((encodedId >> 0u) & 0xFFu), + static_cast((encodedId >> 8u) & 0xFFu), + static_cast((encodedId >> 16u) & 0xFFu) + }; +} + +void ExpectPixelEquals( + const PpmImage& image, + uint32_t x, + uint32_t y, + const std::array& expected, + const char* label) { + ASSERT_LT(x, image.width); + ASSERT_LT(y, image.height); + + const std::array actual = image.GetPixel(x, y); + EXPECT_EQ(actual[0], expected[0]) << label << " @ (" << x << ", " << y << ") red"; + EXPECT_EQ(actual[1], expected[1]) << label << " @ (" << x << ", " << y << ") green"; + EXPECT_EQ(actual[2], expected[2]) << label << " @ (" << x << ", " << y << ") blue"; +} + +Mesh* CreateQuadMesh() { + auto* mesh = new Mesh(); + IResource::ConstructParams params = {}; + params.name = "ObjectIdQuad"; + params.path = "Tests/Rendering/ObjectIdQuad.mesh"; + params.guid = ResourceGUID::Generate(params.path); + mesh->Initialize(params); + + StaticMeshVertex vertices[4] = {}; + vertices[0].position = Vector3(-1.0f, -1.0f, 0.0f); + vertices[0].uv0 = Vector2(0.0f, 1.0f); + vertices[1].position = Vector3(-1.0f, 1.0f, 0.0f); + vertices[1].uv0 = Vector2(0.0f, 0.0f); + vertices[2].position = Vector3(1.0f, -1.0f, 0.0f); + vertices[2].uv0 = Vector2(1.0f, 1.0f); + vertices[3].position = Vector3(1.0f, 1.0f, 0.0f); + vertices[3].uv0 = Vector2(1.0f, 0.0f); + + const uint32_t indices[6] = { 0, 1, 2, 2, 1, 3 }; + mesh->SetVertexData( + vertices, + sizeof(vertices), + 4, + sizeof(StaticMeshVertex), + VertexAttribute::Position | VertexAttribute::UV0); + mesh->SetIndexData(indices, sizeof(indices), 6, true); + + MeshSection section = {}; + section.baseVertex = 0; + section.vertexCount = 4; + section.startIndex = 0; + section.indexCount = 6; + section.materialID = 0; + mesh->AddSection(section); + return mesh; +} + +Texture* CreateSolidTexture() { + auto* texture = new Texture(); + IResource::ConstructParams params = {}; + params.name = "ObjectIdWhite"; + params.path = "Tests/Rendering/ObjectIdWhite.texture"; + params.guid = ResourceGUID::Generate(params.path); + texture->Initialize(params); + + const unsigned char rgba[4] = { 255, 255, 255, 255 }; + texture->Create( + 1, + 1, + 1, + 1, + XCEngine::Resources::TextureType::Texture2D, + XCEngine::Resources::TextureFormat::RGBA8_UNORM, + rgba, + sizeof(rgba)); + return texture; +} + +Material* CreateUnlitMaterial(Texture* texture) { + auto* material = new Material(); + IResource::ConstructParams params = {}; + params.name = "ObjectIdMaterial"; + params.path = "Tests/Rendering/ObjectId.material"; + params.guid = ResourceGUID::Generate(params.path); + material->Initialize(params); + material->SetShader(ResourceManager::Get().Load(GetBuiltinUnlitShaderPath())); + material->SetShaderPass("Unlit"); + material->SetTexture("_MainTex", ResourceHandle(texture)); + return material; +} + +GameObject* CreateQuadObject( + Scene& scene, + const char* name, + Mesh* mesh, + Material* material, + const Vector3& position, + const Vector3& scale) { + GameObject* gameObject = scene.CreateGameObject(name); + gameObject->GetTransform()->SetLocalPosition(position); + gameObject->GetTransform()->SetLocalScale(scale); + + auto* meshFilter = gameObject->AddComponent(); + auto* meshRenderer = gameObject->AddComponent(); + meshFilter->SetMesh(ResourceHandle(mesh)); + meshRenderer->SetMaterial(0, ResourceHandle(material)); + return gameObject; +} + +const char* GetScreenshotFilename(RHIType backendType) { + switch (backendType) { + case RHIType::D3D12: + return kD3D12Screenshot; + case RHIType::Vulkan: + return kVulkanScreenshot; + case RHIType::OpenGL: + default: + return kOpenGLScreenshot; + } +} + +class ObjectIdSceneTest : public RHIIntegrationFixture { +protected: + void SetUp() override; + void TearDown() override; + void RenderFrame() override; + bool RequiresSwapChainAcquire() const override { return false; } + void VerifyObjectIdScreenshot(const char* screenshotFilename); + +private: + void BuildScene(); + RHIResourceView* GetCurrentBackBufferView(); + + std::unique_ptr mScene; + std::unique_ptr mCameraRenderer; + std::vector mBackBufferViews; + RHITexture* mMainColorTexture = nullptr; + RHIResourceView* mMainColorView = nullptr; + RHITexture* mObjectIdColorTexture = nullptr; + RHIResourceView* mObjectIdColorView = nullptr; + RHITexture* mDepthTexture = nullptr; + RHIResourceView* mDepthView = nullptr; + Mesh* mMesh = nullptr; + Material* mMaterial = nullptr; + Texture* mTexture = nullptr; + CameraComponent* mCamera = nullptr; + GameObject* mLeftObject = nullptr; + GameObject* mRightObject = nullptr; + GameObject* mBackObject = nullptr; + GameObject* mFrontObject = nullptr; + ResourceStates mBackBufferState = ResourceStates::Present; +}; + +void ObjectIdSceneTest::SetUp() { + RHIIntegrationFixture::SetUp(); + + mCameraRenderer = std::make_unique(); + mScene = std::make_unique("ObjectIdScene"); + mMesh = CreateQuadMesh(); + mTexture = CreateSolidTexture(); + mMaterial = CreateUnlitMaterial(mTexture); + + BuildScene(); + + TextureDesc colorDesc = {}; + colorDesc.width = kFrameWidth; + colorDesc.height = kFrameHeight; + colorDesc.depth = 1; + colorDesc.mipLevels = 1; + colorDesc.arraySize = 1; + colorDesc.format = static_cast(Format::R8G8B8A8_UNorm); + colorDesc.textureType = static_cast(XCEngine::RHI::TextureType::Texture2D); + colorDesc.sampleCount = 1; + colorDesc.sampleQuality = 0; + colorDesc.flags = 0; + + mMainColorTexture = GetDevice()->CreateTexture(colorDesc); + ASSERT_NE(mMainColorTexture, nullptr); + mObjectIdColorTexture = GetDevice()->CreateTexture(colorDesc); + ASSERT_NE(mObjectIdColorTexture, nullptr); + + ResourceViewDesc colorViewDesc = {}; + colorViewDesc.format = static_cast(Format::R8G8B8A8_UNorm); + colorViewDesc.dimension = ResourceViewDimension::Texture2D; + colorViewDesc.mipLevel = 0; + mMainColorView = GetDevice()->CreateRenderTargetView(mMainColorTexture, colorViewDesc); + ASSERT_NE(mMainColorView, nullptr); + mObjectIdColorView = GetDevice()->CreateRenderTargetView(mObjectIdColorTexture, colorViewDesc); + ASSERT_NE(mObjectIdColorView, nullptr); + + TextureDesc depthDesc = {}; + depthDesc.width = kFrameWidth; + depthDesc.height = kFrameHeight; + depthDesc.depth = 1; + depthDesc.mipLevels = 1; + depthDesc.arraySize = 1; + depthDesc.format = static_cast(Format::D24_UNorm_S8_UInt); + depthDesc.textureType = static_cast(XCEngine::RHI::TextureType::Texture2D); + depthDesc.sampleCount = 1; + depthDesc.sampleQuality = 0; + depthDesc.flags = 0; + mDepthTexture = GetDevice()->CreateTexture(depthDesc); + ASSERT_NE(mDepthTexture, nullptr); + + ResourceViewDesc depthViewDesc = {}; + depthViewDesc.format = static_cast(Format::D24_UNorm_S8_UInt); + depthViewDesc.dimension = ResourceViewDimension::Texture2D; + depthViewDesc.mipLevel = 0; + mDepthView = GetDevice()->CreateDepthStencilView(mDepthTexture, depthViewDesc); + ASSERT_NE(mDepthView, nullptr); + + mBackBufferViews.resize(2, nullptr); + mBackBufferState = ResourceStates::Present; +} + +void ObjectIdSceneTest::TearDown() { + mCameraRenderer.reset(); + + if (mDepthView != nullptr) { + mDepthView->Shutdown(); + delete mDepthView; + mDepthView = nullptr; + } + + if (mDepthTexture != nullptr) { + mDepthTexture->Shutdown(); + delete mDepthTexture; + mDepthTexture = nullptr; + } + + if (mObjectIdColorView != nullptr) { + mObjectIdColorView->Shutdown(); + delete mObjectIdColorView; + mObjectIdColorView = nullptr; + } + + if (mObjectIdColorTexture != nullptr) { + mObjectIdColorTexture->Shutdown(); + delete mObjectIdColorTexture; + mObjectIdColorTexture = nullptr; + } + + if (mMainColorView != nullptr) { + mMainColorView->Shutdown(); + delete mMainColorView; + mMainColorView = nullptr; + } + + if (mMainColorTexture != nullptr) { + mMainColorTexture->Shutdown(); + delete mMainColorTexture; + mMainColorTexture = nullptr; + } + + for (RHIResourceView*& backBufferView : mBackBufferViews) { + if (backBufferView != nullptr) { + backBufferView->Shutdown(); + delete backBufferView; + backBufferView = nullptr; + } + } + mBackBufferViews.clear(); + + mScene.reset(); + + delete mMaterial; + mMaterial = nullptr; + delete mMesh; + mMesh = nullptr; + delete mTexture; + mTexture = nullptr; + + mCamera = nullptr; + mLeftObject = nullptr; + mRightObject = nullptr; + mBackObject = nullptr; + mFrontObject = nullptr; + + RHIIntegrationFixture::TearDown(); +} + +void ObjectIdSceneTest::BuildScene() { + ASSERT_NE(mScene, nullptr); + ASSERT_NE(mMesh, nullptr); + ASSERT_NE(mMaterial, nullptr); + + GameObject* cameraObject = mScene->CreateGameObject("MainCamera"); + mCamera = cameraObject->AddComponent(); + mCamera->SetPrimary(true); + mCamera->SetProjectionType(CameraProjectionType::Orthographic); + mCamera->SetOrthographicSize(2.0f); + mCamera->SetNearClipPlane(0.1f); + mCamera->SetFarClipPlane(10.0f); + mCamera->SetClearColor(XCEngine::Math::Color(0.02f, 0.02f, 0.02f, 1.0f)); + + mLeftObject = CreateQuadObject( + *mScene, + "LeftObject", + mMesh, + mMaterial, + Vector3(-1.8f, 0.0f, 3.0f), + Vector3(1.1f, 1.1f, 1.0f)); + + mRightObject = CreateQuadObject( + *mScene, + "RightObject", + mMesh, + mMaterial, + Vector3(1.8f, 0.0f, 3.0f), + Vector3(1.1f, 1.1f, 1.0f)); + + mBackObject = CreateQuadObject( + *mScene, + "BackObject", + mMesh, + mMaterial, + Vector3(0.0f, 0.0f, 3.5f), + Vector3(1.6f, 1.8f, 1.0f)); + + mFrontObject = CreateQuadObject( + *mScene, + "FrontObject", + mMesh, + mMaterial, + Vector3(0.0f, 0.0f, 2.7f), + Vector3(0.7f, 0.7f, 1.0f)); +} + +RHIResourceView* ObjectIdSceneTest::GetCurrentBackBufferView() { + const int backBufferIndex = GetCurrentBackBufferIndex(); + if (backBufferIndex < 0) { + return nullptr; + } + + if (static_cast(backBufferIndex) >= mBackBufferViews.size()) { + mBackBufferViews.resize(static_cast(backBufferIndex) + 1, nullptr); + } + + if (mBackBufferViews[backBufferIndex] == nullptr) { + ResourceViewDesc viewDesc = {}; + viewDesc.format = static_cast(Format::R8G8B8A8_UNorm); + viewDesc.dimension = ResourceViewDimension::Texture2D; + viewDesc.mipLevel = 0; + mBackBufferViews[backBufferIndex] = GetDevice()->CreateRenderTargetView(GetCurrentBackBuffer(), viewDesc); + } + + return mBackBufferViews[backBufferIndex]; +} + +void ObjectIdSceneTest::RenderFrame() { + ASSERT_NE(mScene, nullptr); + ASSERT_NE(mCameraRenderer, nullptr); + ASSERT_NE(mCamera, nullptr); + ASSERT_NE(mMainColorView, nullptr); + ASSERT_NE(mObjectIdColorView, nullptr); + ASSERT_NE(mDepthView, nullptr); + + RHICommandList* commandList = GetCommandList(); + ASSERT_NE(commandList, nullptr); + + RHIResourceView* backBufferView = GetCurrentBackBufferView(); + ASSERT_NE(backBufferView, nullptr); + + commandList->Reset(); + + RenderSurface mainSurface(kFrameWidth, kFrameHeight); + mainSurface.SetColorAttachment(mMainColorView); + mainSurface.SetDepthAttachment(mDepthView); + mainSurface.SetColorStateBefore(ResourceStates::Common); + mainSurface.SetColorStateAfter(ResourceStates::Common); + + RenderSurface objectIdSurface(kFrameWidth, kFrameHeight); + objectIdSurface.SetColorAttachment(mObjectIdColorView); + objectIdSurface.SetDepthAttachment(mDepthView); + objectIdSurface.SetColorStateBefore(ResourceStates::Common); + objectIdSurface.SetColorStateAfter(ResourceStates::CopySrc); + + RenderContext renderContext = {}; + renderContext.device = GetDevice(); + renderContext.commandList = commandList; + renderContext.commandQueue = GetCommandQueue(); + renderContext.backendType = GetBackendType(); + + CameraRenderRequest request = {}; + request.scene = mScene.get(); + request.camera = mCamera; + request.context = renderContext; + request.surface = mainSurface; + request.objectId.surface = objectIdSurface; + + ASSERT_TRUE(mCameraRenderer->Render(request)); + + commandList->TransitionBarrier(backBufferView, mBackBufferState, ResourceStates::CopyDst); + commandList->CopyResource(backBufferView, mObjectIdColorView); + commandList->TransitionBarrier(mObjectIdColorView, ResourceStates::CopySrc, ResourceStates::Common); + commandList->TransitionBarrier(backBufferView, ResourceStates::CopyDst, ResourceStates::RenderTarget); + commandList->SetRenderTargets(1, &backBufferView, nullptr); + mBackBufferState = ResourceStates::RenderTarget; + + commandList->Close(); + void* commandLists[] = { commandList }; + GetCommandQueue()->ExecuteCommandLists(1, commandLists); +} + +void ObjectIdSceneTest::VerifyObjectIdScreenshot(const char* screenshotFilename) { + ASSERT_NE(mLeftObject, nullptr); + ASSERT_NE(mRightObject, nullptr); + ASSERT_NE(mBackObject, nullptr); + ASSERT_NE(mFrontObject, nullptr); + + const PpmImage image = LoadPpmImage(ResolveRuntimePath(screenshotFilename)); + ASSERT_EQ(image.width, kFrameWidth); + ASSERT_EQ(image.height, kFrameHeight); + + ExpectPixelEquals(image, 60, 60, { 0, 0, 0 }, "background"); + ExpectPixelEquals(image, 320, 360, EncodeObjectIdToRgb(mLeftObject->GetID()), "left object"); + ExpectPixelEquals(image, 960, 360, EncodeObjectIdToRgb(mRightObject->GetID()), "right object"); + ExpectPixelEquals(image, 640, 220, EncodeObjectIdToRgb(mBackObject->GetID()), "back object visible region"); + ExpectPixelEquals(image, 640, 360, EncodeObjectIdToRgb(mFrontObject->GetID()), "front object overlap region"); +} + +TEST_P(ObjectIdSceneTest, RenderObjectIdScene) { + RHICommandQueue* commandQueue = GetCommandQueue(); + const int targetFrameCount = 30; + const char* screenshotFilename = GetScreenshotFilename(GetBackendType()); + + for (int frameCount = 0; frameCount <= targetFrameCount; ++frameCount) { + if (frameCount > 0) { + commandQueue->WaitForPreviousFrame(); + } + + BeginRender(); + RenderFrame(); + + if (frameCount >= targetFrameCount) { + commandQueue->WaitForIdle(); + ASSERT_TRUE(TakeScreenshot(screenshotFilename)); + VerifyObjectIdScreenshot(screenshotFilename); + break; + } + } +} + +} // namespace + +INSTANTIATE_TEST_SUITE_P(D3D12, ObjectIdSceneTest, ::testing::Values(RHIType::D3D12)); +INSTANTIATE_TEST_SUITE_P(OpenGL, ObjectIdSceneTest, ::testing::Values(RHIType::OpenGL)); +#if defined(XCENGINE_SUPPORT_VULKAN) +INSTANTIATE_TEST_SUITE_P(Vulkan, ObjectIdSceneTest, ::testing::Values(RHIType::Vulkan)); +#endif + +GTEST_API_ int main(int argc, char** argv) { + return RunRenderingIntegrationTestMain(argc, argv); +}