#include #include "Viewport/SceneViewportEditorOverlayData.h" #include "Viewport/SceneViewportRenderPlan.h" #include "Viewport/ViewportHostRenderFlowUtils.h" #include #include namespace { using XCEngine::Editor::ApplySceneViewportRenderRequestSetup; using XCEngine::Editor::ApplySceneViewportRenderPlan; using XCEngine::Editor::ApplyViewportFailureStatus; using XCEngine::Editor::BuildGameViewportRenderFailurePolicy; using XCEngine::Editor::BuildSceneViewportGridPassData; using XCEngine::Editor::BuildSceneViewportRenderPlan; using XCEngine::Editor::BuildSceneViewportRenderFailurePolicy; using XCEngine::Editor::BuildSceneViewportSelectionOutlineStyle; using XCEngine::Editor::BuildViewportRenderTargetUnavailablePolicy; using XCEngine::Editor::GameViewportRenderFailure; using XCEngine::Editor::MarkGameViewportRenderSuccess; using XCEngine::Editor::MarkSceneViewportRenderSuccess; using XCEngine::Editor::SceneViewportOverlayFrameData; using XCEngine::Editor::SceneViewportOverlayLinePrimitive; using XCEngine::Editor::SceneViewportRenderFailure; using XCEngine::Editor::SceneViewportOverlayData; using XCEngine::Editor::SceneViewportGridPassData; using XCEngine::Editor::SceneViewportRenderPlan; using XCEngine::Editor::SceneViewportSelectionOutlineStyle; using XCEngine::Editor::ViewportRenderTargets; using XCEngine::RHI::Format; using XCEngine::RHI::RHIResourceView; using XCEngine::RHI::ResourceStates; using XCEngine::RHI::ResourceViewDimension; using XCEngine::RHI::ResourceViewType; using XCEngine::Rendering::RenderPass; using XCEngine::Rendering::RenderPassContext; using XCEngine::Rendering::RenderPassSequence; using XCEngine::Rendering::RenderSurface; class DummyResourceView final : public RHIResourceView { public: explicit DummyResourceView( ResourceViewType viewType = ResourceViewType::RenderTarget, Format format = Format::R8G8B8A8_UNorm) : m_viewType(viewType) , m_format(format) { } void Shutdown() override { } void* GetNativeHandle() override { return nullptr; } bool IsValid() const override { return true; } ResourceViewType GetViewType() const override { return m_viewType; } ResourceViewDimension GetDimension() const override { return ResourceViewDimension::Texture2D; } Format GetFormat() const override { return m_format; } private: ResourceViewType m_viewType = ResourceViewType::RenderTarget; Format m_format = Format::R8G8B8A8_UNorm; }; class NoopRenderPass final : public RenderPass { public: const char* GetName() const override { return "NoopRenderPass"; } bool Execute(const RenderPassContext&) override { return true; } }; SceneViewportOverlayData CreateValidOverlay() { SceneViewportOverlayData overlay = {}; overlay.valid = true; overlay.cameraPosition = XCEngine::Math::Vector3(1.0f, 2.0f, 3.0f); overlay.cameraForward = XCEngine::Math::Vector3::Forward(); overlay.cameraRight = XCEngine::Math::Vector3::Right(); overlay.cameraUp = XCEngine::Math::Vector3::Up(); overlay.verticalFovDegrees = 70.0f; overlay.nearClipPlane = 0.1f; overlay.farClipPlane = 500.0f; overlay.orbitDistance = 9.0f; return overlay; } SceneViewportOverlayFrameData CreateOverlayFrameDataWithLine( const SceneViewportOverlayData& overlay, const XCEngine::Math::Vector3& start, const XCEngine::Math::Vector3& end) { SceneViewportOverlayFrameData frameData = {}; frameData.overlay = overlay; SceneViewportOverlayLinePrimitive& line = frameData.worldLines.emplace_back(); line.startWorld = start; line.endWorld = end; line.color = XCEngine::Math::Color::White(); return frameData; } TEST(ViewportRenderFlowUtilsTest, BuildFailurePoliciesExposeExpectedStatusAndClearBehavior) { const auto targetUnavailable = BuildViewportRenderTargetUnavailablePolicy(); EXPECT_STREQ(targetUnavailable.statusText, "Viewport render target is unavailable"); EXPECT_FALSE(targetUnavailable.shouldClear); EXPECT_TRUE(targetUnavailable.invalidateObjectIdFrame); const auto missingSceneCamera = BuildSceneViewportRenderFailurePolicy(SceneViewportRenderFailure::MissingSceneViewCamera); EXPECT_STREQ(missingSceneCamera.statusText, "Scene view camera is unavailable"); EXPECT_TRUE(missingSceneCamera.shouldClear); EXPECT_FLOAT_EQ(missingSceneCamera.clearColor.r, 0.18f); EXPECT_FLOAT_EQ(missingSceneCamera.clearColor.g, 0.07f); EXPECT_FLOAT_EQ(missingSceneCamera.clearColor.b, 0.07f); EXPECT_FALSE(missingSceneCamera.setStatusIfEmpty); const auto sceneRendererFailed = BuildSceneViewportRenderFailurePolicy(SceneViewportRenderFailure::SceneRendererFailed); EXPECT_STREQ(sceneRendererFailed.statusText, "Scene renderer failed"); EXPECT_TRUE(sceneRendererFailed.shouldClear); EXPECT_TRUE(sceneRendererFailed.setStatusIfEmpty); const auto noGameCamera = BuildGameViewportRenderFailurePolicy(GameViewportRenderFailure::NoCameraInScene); EXPECT_STREQ(noGameCamera.statusText, "No camera in scene"); EXPECT_TRUE(noGameCamera.shouldClear); EXPECT_FLOAT_EQ(noGameCamera.clearColor.r, 0.10f); EXPECT_FLOAT_EQ(noGameCamera.clearColor.g, 0.09f); EXPECT_FLOAT_EQ(noGameCamera.clearColor.b, 0.08f); } TEST(ViewportRenderFlowUtilsTest, ApplyViewportFailureStatusRespectsSetIfEmptyBehavior) { std::string statusText; ApplyViewportFailureStatus( statusText, BuildSceneViewportRenderFailurePolicy(SceneViewportRenderFailure::SceneRendererFailed)); EXPECT_EQ(statusText, "Scene renderer failed"); statusText = "Scene object id shader view is unavailable"; ApplyViewportFailureStatus( statusText, BuildSceneViewportRenderFailurePolicy(SceneViewportRenderFailure::SceneRendererFailed)); EXPECT_EQ(statusText, "Scene object id shader view is unavailable"); ApplyViewportFailureStatus( statusText, BuildGameViewportRenderFailurePolicy(GameViewportRenderFailure::NoActiveScene)); EXPECT_EQ(statusText, "No active scene"); } TEST(ViewportRenderFlowUtilsTest, BuildSceneViewportGridPassDataCopiesSceneCameraState) { const SceneViewportOverlayData overlay = CreateValidOverlay(); const auto gridPassData = BuildSceneViewportGridPassData(overlay); EXPECT_TRUE(gridPassData.valid); EXPECT_FLOAT_EQ(gridPassData.cameraPosition.x, overlay.cameraPosition.x); EXPECT_FLOAT_EQ(gridPassData.cameraPosition.y, overlay.cameraPosition.y); EXPECT_FLOAT_EQ(gridPassData.cameraPosition.z, overlay.cameraPosition.z); EXPECT_FLOAT_EQ(gridPassData.verticalFovDegrees, overlay.verticalFovDegrees); EXPECT_FLOAT_EQ(gridPassData.orbitDistance, overlay.orbitDistance); } TEST(ViewportRenderFlowUtilsTest, BuildSceneViewportSelectionOutlineStyleAppliesSceneDefaults) { const auto style = BuildSceneViewportSelectionOutlineStyle(false); const auto debugStyle = BuildSceneViewportSelectionOutlineStyle(true); EXPECT_FLOAT_EQ(style.outlineColor.r, 1.0f); EXPECT_FLOAT_EQ(style.outlineColor.g, 0.4f); EXPECT_FLOAT_EQ(style.outlineColor.b, 0.0f); EXPECT_FLOAT_EQ(style.outlineWidthPixels, 2.0f); EXPECT_FALSE(style.debugSelectionMask); EXPECT_TRUE(debugStyle.debugSelectionMask); } TEST(ViewportRenderFlowUtilsTest, ApplySceneRenderRequestSetupAttachesOptionalPassesAndObjectIdSurface) { DummyResourceView depthView(ResourceViewType::DepthStencil, Format::D24_UNorm_S8_UInt); DummyResourceView objectIdDepthView(ResourceViewType::DepthStencil, Format::D24_UNorm_S8_UInt); DummyResourceView objectIdView(ResourceViewType::RenderTarget); DummyResourceView objectIdShaderView(ResourceViewType::ShaderResource); ViewportRenderTargets targets = {}; targets.width = 800; targets.height = 600; targets.depthView = &depthView; targets.objectIdDepthView = &objectIdDepthView; targets.objectIdView = &objectIdView; targets.objectIdShaderView = &objectIdShaderView; targets.objectIdState = ResourceStates::Common; RenderPassSequence postPasses; postPasses.AddPass(std::make_unique()); XCEngine::Rendering::CameraRenderRequest request = {}; request.surface = RenderSurface(800, 600); request.surface.SetRenderArea(XCEngine::Math::RectInt(64, 32, 320, 240)); ApplySceneViewportRenderRequestSetup( targets, &postPasses, request); EXPECT_EQ(request.postScenePasses, &postPasses); EXPECT_TRUE(request.objectId.IsRequested()); ASSERT_EQ(request.objectId.surface.GetColorAttachments().size(), 1u); EXPECT_EQ(request.objectId.surface.GetColorAttachments()[0], &objectIdView); EXPECT_EQ(request.objectId.surface.GetDepthAttachment(), &objectIdDepthView); const auto requestArea = request.surface.GetRenderArea(); const auto objectIdArea = request.objectId.surface.GetRenderArea(); EXPECT_EQ(objectIdArea.x, requestArea.x); EXPECT_EQ(objectIdArea.y, requestArea.y); EXPECT_EQ(objectIdArea.width, requestArea.width); EXPECT_EQ(objectIdArea.height, requestArea.height); } TEST(ViewportRenderFlowUtilsTest, ApplySceneRenderRequestSetupSkipsUnavailableOptionalAttachments) { ViewportRenderTargets targets = {}; targets.width = 800; targets.height = 600; RenderPassSequence postPasses; XCEngine::Rendering::CameraRenderRequest request = {}; request.postScenePasses = reinterpret_cast(static_cast(0x1)); request.objectId.surface = RenderSurface(1, 1); request.objectId.surface.SetColorAttachment( reinterpret_cast(static_cast(0x2))); ApplySceneViewportRenderRequestSetup(targets, &postPasses, request); EXPECT_EQ(request.postScenePasses, nullptr); EXPECT_FALSE(request.objectId.IsRequested()); } TEST(ViewportRenderFlowUtilsTest, BuildSceneViewportRenderPlanCollectsPostSceneAndOverlayPasses) { DummyResourceView depthView(ResourceViewType::DepthStencil, Format::D24_UNorm_S8_UInt); DummyResourceView depthShaderView(ResourceViewType::ShaderResource, Format::D24_UNorm_S8_UInt); DummyResourceView selectionMaskView(ResourceViewType::RenderTarget); DummyResourceView selectionMaskShaderView(ResourceViewType::ShaderResource); DummyResourceView objectIdShaderView(ResourceViewType::ShaderResource); ViewportRenderTargets targets = {}; targets.depthView = &depthView; targets.depthShaderView = &depthShaderView; targets.selectionMaskView = &selectionMaskView; targets.selectionMaskShaderView = &selectionMaskShaderView; targets.objectIdShaderView = &objectIdShaderView; const SceneViewportOverlayData overlay = CreateValidOverlay(); const SceneViewportOverlayFrameData editorOverlayFrameData = CreateOverlayFrameDataWithLine( overlay, XCEngine::Math::Vector3::Zero(), XCEngine::Math::Vector3::Right()); size_t factoryCallCount = 0u; size_t combinedWorldLineCount = 0u; size_t gridPassFactoryCallCount = 0u; size_t selectionOutlinePassFactoryCallCount = 0u; const auto result = BuildSceneViewportRenderPlan( targets, overlay, { 7u, 11u }, editorOverlayFrameData, [&gridPassFactoryCallCount](const SceneViewportGridPassData& data) { ++gridPassFactoryCallCount; EXPECT_TRUE(data.valid); return std::make_unique(); }, [&selectionOutlinePassFactoryCallCount]( ViewportRenderTargets* outlineTargets, const std::vector& selectedObjectIds, const SceneViewportSelectionOutlineStyle& style) { ++selectionOutlinePassFactoryCallCount; EXPECT_NE(outlineTargets, nullptr); if (outlineTargets == nullptr) { return std::unique_ptr(); } EXPECT_NE(outlineTargets->selectionMaskShaderView, nullptr); EXPECT_NE(outlineTargets->depthShaderView, nullptr); EXPECT_EQ(selectedObjectIds.size(), 2u); EXPECT_FLOAT_EQ(style.outlineWidthPixels, 2.0f); return std::make_unique(); }, [&factoryCallCount, &combinedWorldLineCount](const SceneViewportOverlayFrameData& frameData) { ++factoryCallCount; combinedWorldLineCount = frameData.worldLines.size(); return std::make_unique(); }, false); EXPECT_EQ(result.plan.postScenePasses.GetPassCount(), 2u); EXPECT_EQ(result.plan.overlayPasses.GetPassCount(), 1u); EXPECT_EQ(factoryCallCount, 1u); EXPECT_EQ(gridPassFactoryCallCount, 1u); EXPECT_EQ(selectionOutlinePassFactoryCallCount, 1u); EXPECT_EQ(combinedWorldLineCount, 1u); EXPECT_EQ(result.warningStatusText, nullptr); } TEST(ViewportRenderFlowUtilsTest, BuildSceneViewportRenderPlanSkipsRhiOverlayPassWhenFrameContainsOnlySceneIcons) { const SceneViewportOverlayData overlay = CreateValidOverlay(); ViewportRenderTargets targets = {}; SceneViewportOverlayFrameData editorOverlayFrameData = {}; editorOverlayFrameData.overlay = overlay; auto& sprite = editorOverlayFrameData.worldSprites.emplace_back(); sprite.worldPosition = XCEngine::Math::Vector3::Zero(); sprite.sizePixels = XCEngine::Math::Vector2(32.0f, 32.0f); size_t overlayFactoryCallCount = 0u; const auto result = BuildSceneViewportRenderPlan( targets, overlay, {}, editorOverlayFrameData, [](const SceneViewportGridPassData&) { return std::make_unique(); }, []( ViewportRenderTargets*, const std::vector&, const SceneViewportSelectionOutlineStyle&) { return std::make_unique(); }, [&overlayFactoryCallCount](const SceneViewportOverlayFrameData&) { ++overlayFactoryCallCount; return std::make_unique(); }, false); EXPECT_EQ(result.plan.postScenePasses.GetPassCount(), 1u); EXPECT_EQ(result.plan.overlayPasses.GetPassCount(), 0u); EXPECT_EQ(overlayFactoryCallCount, 0u); } TEST(ViewportRenderFlowUtilsTest, BuildSceneViewportRenderPlanWarnsWhenSelectionOutlineCannotAccessObjectIdTexture) { const SceneViewportOverlayData overlay = CreateValidOverlay(); ViewportRenderTargets targets = {}; const auto result = BuildSceneViewportRenderPlan( targets, overlay, { 42u }, {}, [](const SceneViewportGridPassData&) { return std::make_unique(); }, []( ViewportRenderTargets*, const std::vector&, const SceneViewportSelectionOutlineStyle&) { return std::make_unique(); }, [](const SceneViewportOverlayFrameData&) { return std::make_unique(); }, false); EXPECT_EQ(result.plan.postScenePasses.GetPassCount(), 1u); EXPECT_STREQ(result.warningStatusText, "Scene selection outline resources are unavailable"); } TEST(ViewportRenderFlowUtilsTest, ApplySceneViewportRenderPlanAttachesPlannedPassesAndClearState) { DummyResourceView depthView(ResourceViewType::DepthStencil, Format::D24_UNorm_S8_UInt); DummyResourceView objectIdDepthView(ResourceViewType::DepthStencil, Format::D24_UNorm_S8_UInt); DummyResourceView objectIdView(ResourceViewType::RenderTarget); DummyResourceView objectIdShaderView(ResourceViewType::ShaderResource); ViewportRenderTargets targets = {}; targets.width = 800; targets.height = 600; targets.depthView = &depthView; targets.objectIdDepthView = &objectIdDepthView; targets.objectIdView = &objectIdView; targets.objectIdShaderView = &objectIdShaderView; SceneViewportRenderPlan plan = {}; plan.postScenePasses.AddPass(std::make_unique()); plan.overlayPasses.AddPass(std::make_unique()); plan.clearColorOverride = XCEngine::Math::Color(0.1f, 0.2f, 0.3f, 1.0f); XCEngine::Rendering::CameraRenderRequest request = {}; request.surface = RenderSurface(800, 600); request.surface.SetRenderArea(XCEngine::Math::RectInt(10, 20, 300, 200)); ApplySceneViewportRenderPlan(targets, plan, request); EXPECT_EQ(request.postScenePasses, &plan.postScenePasses); EXPECT_EQ(request.overlayPasses, &plan.overlayPasses); EXPECT_TRUE(request.objectId.IsRequested()); EXPECT_TRUE(request.hasClearColorOverride); EXPECT_FLOAT_EQ(request.clearColorOverride.r, 0.1f); EXPECT_FLOAT_EQ(request.clearColorOverride.g, 0.2f); EXPECT_FLOAT_EQ(request.clearColorOverride.b, 0.3f); } TEST(ViewportRenderFlowUtilsTest, MarkSceneRenderSuccessMovesTargetsToShaderResourceState) { DummyResourceView depthView(ResourceViewType::DepthStencil, Format::D24_UNorm_S8_UInt); DummyResourceView objectIdView(ResourceViewType::RenderTarget); DummyResourceView objectIdDepthView(ResourceViewType::DepthStencil, Format::D24_UNorm_S8_UInt); DummyResourceView selectionMaskView(ResourceViewType::RenderTarget); ViewportRenderTargets targets = {}; targets.width = 640; targets.height = 360; targets.depthView = &depthView; targets.objectIdDepthView = &objectIdDepthView; targets.objectIdView = &objectIdView; targets.selectionMaskView = &selectionMaskView; targets.colorState = ResourceStates::Common; targets.objectIdState = ResourceStates::Common; targets.selectionMaskState = ResourceStates::Common; XCEngine::Rendering::CameraRenderRequest request = {}; request.surface = RenderSurface(640, 360); ApplySceneViewportRenderRequestSetup(targets, nullptr, request); MarkSceneViewportRenderSuccess(targets, request); EXPECT_EQ(targets.colorState, ResourceStates::PixelShaderResource); EXPECT_EQ(targets.objectIdState, ResourceStates::PixelShaderResource); EXPECT_EQ(targets.selectionMaskState, ResourceStates::PixelShaderResource); EXPECT_TRUE(targets.hasValidObjectIdFrame); ViewportRenderTargets noObjectIdTargets = {}; noObjectIdTargets.colorState = ResourceStates::Common; noObjectIdTargets.objectIdState = ResourceStates::Common; noObjectIdTargets.selectionMaskState = ResourceStates::Common; XCEngine::Rendering::CameraRenderRequest noObjectIdRequest = {}; MarkSceneViewportRenderSuccess(noObjectIdTargets, noObjectIdRequest); EXPECT_EQ(noObjectIdTargets.colorState, ResourceStates::PixelShaderResource); EXPECT_EQ(noObjectIdTargets.objectIdState, ResourceStates::PixelShaderResource); EXPECT_EQ(noObjectIdTargets.selectionMaskState, ResourceStates::PixelShaderResource); EXPECT_FALSE(noObjectIdTargets.hasValidObjectIdFrame); } TEST(ViewportRenderFlowUtilsTest, MarkGameRenderSuccessClearsObjectIdFrameAndUpdatesColorState) { ViewportRenderTargets targets = {}; targets.colorState = ResourceStates::Common; targets.hasValidObjectIdFrame = true; MarkGameViewportRenderSuccess(targets); EXPECT_EQ(targets.colorState, ResourceStates::PixelShaderResource); EXPECT_FALSE(targets.hasValidObjectIdFrame); } } // namespace