diff --git a/editor/src/Viewport/SceneViewportPostPassPlan.h b/editor/src/Viewport/SceneViewportPostPassPlan.h new file mode 100644 index 00000000..2fc53f40 --- /dev/null +++ b/editor/src/Viewport/SceneViewportPostPassPlan.h @@ -0,0 +1,77 @@ +#pragma once + +#include +#include + +namespace XCEngine { +namespace Editor { + +enum class SceneViewportPostPassStep : uint8_t { + SelectionMask, + ColorToRenderTarget, + InfiniteGrid, + SelectionOutline, + ColorToShaderResource, + SelectionMaskDebug +}; + +struct SceneViewportPostPassPlanInput { + bool overlayValid = false; + bool hasSelection = false; + bool debugSelectionMask = false; + bool hasSelectionMaskRenderTarget = false; + bool hasSelectionMaskShaderView = false; +}; + +struct SceneViewportPostPassPlan { + bool valid = false; + bool usesSelectionMaskSurface = false; + bool usesSelectionMaskShaderView = false; + std::vector steps; +}; + +inline SceneViewportPostPassPlan BuildSceneViewportPostPassPlan( + const SceneViewportPostPassPlanInput& input) { + SceneViewportPostPassPlan plan = {}; + if (!input.overlayValid) { + return plan; + } + + plan.valid = true; + + if (input.hasSelection && input.debugSelectionMask) { + plan.steps = { + SceneViewportPostPassStep::ColorToRenderTarget, + SceneViewportPostPassStep::SelectionMaskDebug, + SceneViewportPostPassStep::ColorToShaderResource + }; + return plan; + } + + const bool canRenderSelectionMask = + input.hasSelection && + input.hasSelectionMaskRenderTarget && + input.hasSelectionMaskShaderView; + const bool canRenderSelectionOutline = + canRenderSelectionMask && + input.hasSelectionMaskShaderView; + + if (canRenderSelectionMask) { + plan.usesSelectionMaskSurface = true; + plan.usesSelectionMaskShaderView = true; + plan.steps.push_back(SceneViewportPostPassStep::SelectionMask); + } + + plan.steps.push_back(SceneViewportPostPassStep::ColorToRenderTarget); + plan.steps.push_back(SceneViewportPostPassStep::InfiniteGrid); + + if (canRenderSelectionOutline) { + plan.steps.push_back(SceneViewportPostPassStep::SelectionOutline); + } + + plan.steps.push_back(SceneViewportPostPassStep::ColorToShaderResource); + return plan; +} + +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/Viewport/ViewportHostService.h b/editor/src/Viewport/ViewportHostService.h index e131aa65..56815a61 100644 --- a/editor/src/Viewport/ViewportHostService.h +++ b/editor/src/Viewport/ViewportHostService.h @@ -7,6 +7,7 @@ #include "SceneViewportPicker.h" #include "SceneViewportCameraController.h" #include "SceneViewportInfiniteGridPass.h" +#include "SceneViewportPostPassPlan.h" #include "SceneViewportSelectionMaskPass.h" #include "SceneViewportSelectionOutlinePass.h" #include "SceneViewportSelectionUtils.h" @@ -534,59 +535,35 @@ private: const Rendering::RenderCameraData& cameraData, const std::vector& selectionRenderables, Rendering::RenderPassSequence& outPostPasses) { - if (!overlay.valid) { + const bool hasSelection = !selectionRenderables.empty(); + const bool hasSelectionMaskRenderTarget = entry.selectionMaskView != nullptr; + const bool hasSelectionMaskShaderView = entry.selectionMaskShaderView != nullptr; + + if (hasSelection && + !kDebugSceneSelectionMask && + (!hasSelectionMaskRenderTarget || !hasSelectionMaskShaderView)) { + SetViewportStatusIfEmpty(entry.statusText, "Scene selection mask target is unavailable"); + } + + const SceneViewportPostPassPlan plan = BuildSceneViewportPostPassPlan({ + overlay.valid, + hasSelection, + kDebugSceneSelectionMask, + hasSelectionMaskRenderTarget, + hasSelectionMaskShaderView + }); + if (!plan.valid) { return false; } - const bool hasSelection = !selectionRenderables.empty(); - if (hasSelection && kDebugSceneSelectionMask) { - outPostPasses.AddPass(MakeLambdaRenderPass( - "SceneColorToRenderTarget", - [&entry](const Rendering::RenderPassContext& context) { - context.renderContext.commandList->TransitionBarrier( - entry.colorView, - context.surface.GetColorStateAfter(), - RHI::ResourceStates::RenderTarget); - entry.colorState = RHI::ResourceStates::RenderTarget; - return true; - })); - outPostPasses.AddPass(MakeLambdaRenderPass( - "SceneSelectionMaskDebug", - [this, &entry, &cameraData, &selectionRenderables]( - const Rendering::RenderPassContext& context) { - const float debugClearColor[4] = { 0.0f, 0.0f, 0.0f, 1.0f }; - RHI::RHIResourceView* colorView = entry.colorView; - context.renderContext.commandList->SetRenderTargets(1, &colorView, entry.depthView); - context.renderContext.commandList->ClearRenderTarget(colorView, debugClearColor); - - const bool rendered = m_sceneSelectionMaskPass.Render( - context.renderContext, - context.surface, - cameraData, - selectionRenderables); - if (!rendered) { - SetViewportStatusIfEmpty(entry.statusText, "Scene selection mask debug pass failed"); - } - return rendered; - })); - outPostPasses.AddPass(MakeLambdaRenderPass( - "SceneColorToShaderResource", - [&entry](const Rendering::RenderPassContext& context) { - context.renderContext.commandList->TransitionBarrier( - entry.colorView, - RHI::ResourceStates::RenderTarget, - context.surface.GetColorStateAfter()); - entry.colorState = context.surface.GetColorStateAfter(); - return true; - })); - return true; + Rendering::RenderSurface selectionMaskSurface = {}; + if (plan.usesSelectionMaskSurface) { + selectionMaskSurface = BuildSelectionMaskSurface(entry); } - if (hasSelection) { - if (entry.selectionMaskView == nullptr || entry.selectionMaskShaderView == nullptr) { - SetViewportStatusIfEmpty(entry.statusText, "Scene selection mask target is unavailable"); - } else { - Rendering::RenderSurface selectionMaskSurface = BuildSelectionMaskSurface(entry); + for (const SceneViewportPostPassStep step : plan.steps) { + switch (step) { + case SceneViewportPostPassStep::SelectionMask: outPostPasses.AddPass(MakeLambdaRenderPass( "SceneSelectionMask", [this, &entry, selectionMaskSurface, &cameraData, &selectionRenderables]( @@ -618,57 +595,85 @@ private: entry.selectionMaskState = RHI::ResourceStates::PixelShaderResource; return rendered; })); + break; + case SceneViewportPostPassStep::ColorToRenderTarget: + outPostPasses.AddPass(MakeLambdaRenderPass( + "SceneColorToRenderTarget", + [&entry](const Rendering::RenderPassContext& context) { + context.renderContext.commandList->TransitionBarrier( + entry.colorView, + context.surface.GetColorStateAfter(), + RHI::ResourceStates::RenderTarget); + entry.colorState = RHI::ResourceStates::RenderTarget; + return true; + })); + break; + case SceneViewportPostPassStep::InfiniteGrid: + outPostPasses.AddPass(MakeLambdaRenderPass( + "SceneInfiniteGrid", + [this, overlay, &entry](const Rendering::RenderPassContext& context) { + const bool rendered = m_sceneGridPass.Render( + context.renderContext, + context.surface, + overlay); + if (!rendered) { + SetViewportStatusIfEmpty(entry.statusText, "Scene grid pass failed"); + } + return rendered; + })); + break; + case SceneViewportPostPassStep::SelectionOutline: + outPostPasses.AddPass(MakeLambdaRenderPass( + "SceneSelectionOutline", + [this, &entry](const Rendering::RenderPassContext& context) { + const bool rendered = m_sceneSelectionOutlinePass.Render( + context.renderContext, + context.surface, + entry.selectionMaskShaderView); + if (!rendered) { + SetViewportStatusIfEmpty(entry.statusText, "Scene selection outline pass failed"); + } + return rendered; + })); + break; + case SceneViewportPostPassStep::ColorToShaderResource: + outPostPasses.AddPass(MakeLambdaRenderPass( + "SceneColorToShaderResource", + [&entry](const Rendering::RenderPassContext& context) { + context.renderContext.commandList->TransitionBarrier( + entry.colorView, + RHI::ResourceStates::RenderTarget, + context.surface.GetColorStateAfter()); + entry.colorState = context.surface.GetColorStateAfter(); + return true; + })); + break; + case SceneViewportPostPassStep::SelectionMaskDebug: + outPostPasses.AddPass(MakeLambdaRenderPass( + "SceneSelectionMaskDebug", + [this, &entry, &cameraData, &selectionRenderables]( + const Rendering::RenderPassContext& context) { + const float debugClearColor[4] = { 0.0f, 0.0f, 0.0f, 1.0f }; + RHI::RHIResourceView* colorView = entry.colorView; + context.renderContext.commandList->SetRenderTargets(1, &colorView, entry.depthView); + context.renderContext.commandList->ClearRenderTarget(colorView, debugClearColor); + + const bool rendered = m_sceneSelectionMaskPass.Render( + context.renderContext, + context.surface, + cameraData, + selectionRenderables); + if (!rendered) { + SetViewportStatusIfEmpty(entry.statusText, "Scene selection mask debug pass failed"); + } + return rendered; + })); + break; + default: + break; } } - outPostPasses.AddPass(MakeLambdaRenderPass( - "SceneColorToRenderTarget", - [&entry](const Rendering::RenderPassContext& context) { - context.renderContext.commandList->TransitionBarrier( - entry.colorView, - context.surface.GetColorStateAfter(), - RHI::ResourceStates::RenderTarget); - entry.colorState = RHI::ResourceStates::RenderTarget; - return true; - })); - outPostPasses.AddPass(MakeLambdaRenderPass( - "SceneInfiniteGrid", - [this, overlay, &entry](const Rendering::RenderPassContext& context) { - const bool rendered = m_sceneGridPass.Render( - context.renderContext, - context.surface, - overlay); - if (!rendered) { - SetViewportStatusIfEmpty(entry.statusText, "Scene grid pass failed"); - } - return rendered; - })); - - if (hasSelection && entry.selectionMaskShaderView != nullptr) { - outPostPasses.AddPass(MakeLambdaRenderPass( - "SceneSelectionOutline", - [this, &entry](const Rendering::RenderPassContext& context) { - const bool rendered = m_sceneSelectionOutlinePass.Render( - context.renderContext, - context.surface, - entry.selectionMaskShaderView); - if (!rendered) { - SetViewportStatusIfEmpty(entry.statusText, "Scene selection outline pass failed"); - } - return rendered; - })); - } - - outPostPasses.AddPass(MakeLambdaRenderPass( - "SceneColorToShaderResource", - [&entry](const Rendering::RenderPassContext& context) { - context.renderContext.commandList->TransitionBarrier( - entry.colorView, - RHI::ResourceStates::RenderTarget, - context.surface.GetColorStateAfter()); - entry.colorState = context.surface.GetColorStateAfter(); - return true; - })); return true; } diff --git a/engine/src/Rendering/CameraRenderer.cpp b/engine/src/Rendering/CameraRenderer.cpp index a22ebfc0..7b37bf6c 100644 --- a/engine/src/Rendering/CameraRenderer.cpp +++ b/engine/src/Rendering/CameraRenderer.cpp @@ -7,6 +7,33 @@ namespace XCEngine { namespace Rendering { +namespace { + +bool InitializePassSequence( + RenderPassSequence* sequence, + const RenderContext& context, + bool& initialized) { + if (sequence == nullptr) { + initialized = false; + return true; + } + + initialized = sequence->Initialize(context); + if (!initialized) { + sequence->Shutdown(); + } + + return initialized; +} + +void ShutdownPassSequence(RenderPassSequence* sequence, bool initialized) { + if (sequence != nullptr && initialized) { + sequence->Shutdown(); + } +} + +} // namespace + CameraRenderer::CameraRenderer() : m_pipeline(std::make_unique()) { } @@ -57,24 +84,42 @@ bool CameraRenderer::Render( sceneData }; - if (request.preScenePasses != nullptr) { - if (!request.preScenePasses->Initialize(request.context) || - !request.preScenePasses->Execute(passContext)) { - return false; - } + bool preScenePassesInitialized = false; + if (!InitializePassSequence( + request.preScenePasses, + request.context, + preScenePassesInitialized)) { + return false; } - - if (!m_pipeline->Render(request.context, request.surface, sceneData)) { + if (request.preScenePasses != nullptr && + !request.preScenePasses->Execute(passContext)) { + ShutdownPassSequence(request.preScenePasses, preScenePassesInitialized); return false; } - if (request.postScenePasses != nullptr) { - if (!request.postScenePasses->Initialize(request.context) || - !request.postScenePasses->Execute(passContext)) { - return false; - } + if (!m_pipeline->Render(request.context, request.surface, sceneData)) { + ShutdownPassSequence(request.preScenePasses, preScenePassesInitialized); + return false; } + bool postScenePassesInitialized = false; + if (!InitializePassSequence( + request.postScenePasses, + request.context, + postScenePassesInitialized)) { + ShutdownPassSequence(request.preScenePasses, preScenePassesInitialized); + return false; + } + if (request.postScenePasses != nullptr && + !request.postScenePasses->Execute(passContext)) { + ShutdownPassSequence(request.postScenePasses, postScenePassesInitialized); + ShutdownPassSequence(request.preScenePasses, preScenePassesInitialized); + return false; + } + + ShutdownPassSequence(request.postScenePasses, postScenePassesInitialized); + ShutdownPassSequence(request.preScenePasses, preScenePassesInitialized); + return true; } diff --git a/tests/Rendering/unit/test_camera_scene_renderer.cpp b/tests/Rendering/unit/test_camera_scene_renderer.cpp index 24176f9f..142246ab 100644 --- a/tests/Rendering/unit/test_camera_scene_renderer.cpp +++ b/tests/Rendering/unit/test_camera_scene_renderer.cpp @@ -19,6 +19,7 @@ struct MockPipelineState { int initializeCalls = 0; int shutdownCalls = 0; int renderCalls = 0; + bool renderResult = true; uint32_t lastSurfaceWidth = 0; uint32_t lastSurfaceHeight = 0; CameraComponent* lastCamera = nullptr; @@ -57,7 +58,7 @@ public: m_state->lastClearFlags = sceneData.cameraData.clearFlags; m_state->renderedCameras.push_back(sceneData.camera); m_state->renderedClearFlags.push_back(sceneData.cameraData.clearFlags); - return true; + return m_state->renderResult; } private: @@ -66,9 +67,15 @@ private: class TrackingPass final : public RenderPass { public: - TrackingPass(std::shared_ptr state, const char* label) + TrackingPass( + std::shared_ptr state, + const char* label, + bool initializeResult = true, + bool executeResult = true) : m_state(std::move(state)) - , m_label(label) { + , m_label(label) + , m_initializeResult(initializeResult) + , m_executeResult(executeResult) { } const char* GetName() const override { @@ -77,17 +84,23 @@ public: bool Initialize(const RenderContext&) override { m_state->eventLog.push_back(std::string("init:") + m_label); - return true; + return m_initializeResult; } bool Execute(const RenderPassContext&) override { m_state->eventLog.push_back(m_label); - return true; + return m_executeResult; + } + + void Shutdown() override { + m_state->eventLog.push_back(std::string("shutdown:") + m_label); } private: std::shared_ptr m_state; const char* m_label = ""; + bool m_initializeResult = true; + bool m_executeResult = true; }; RenderContext CreateValidContext() { @@ -163,7 +176,81 @@ TEST(CameraRenderer_Test, ExecutesInjectedPreAndPostPassSequencesAroundPipelineR ASSERT_TRUE(renderer.Render(request)); EXPECT_EQ( state->eventLog, - (std::vector{ "init:pre", "pre", "pipeline", "init:post", "post" })); + (std::vector{ + "init:pre", + "pre", + "pipeline", + "init:post", + "post", + "shutdown:post", + "shutdown:pre" })); +} + +TEST(CameraRenderer_Test, ShutsDownInitializedPassesWhenPipelineRenderFails) { + Scene scene("CameraRendererFailureScene"); + + GameObject* cameraObject = scene.CreateGameObject("Camera"); + auto* camera = cameraObject->AddComponent(); + camera->SetPrimary(true); + camera->SetDepth(2.0f); + + auto state = std::make_shared(); + state->renderResult = false; + CameraRenderer renderer(std::make_unique(state)); + + RenderPassSequence prePasses; + prePasses.AddPass(std::make_unique(state, "pre")); + + CameraRenderRequest request; + request.scene = &scene; + request.camera = camera; + request.context = CreateValidContext(); + request.surface = RenderSurface(320, 180); + request.cameraDepth = camera->GetDepth(); + request.preScenePasses = &prePasses; + + EXPECT_FALSE(renderer.Render(request)); + EXPECT_EQ( + state->eventLog, + (std::vector{ "init:pre", "pre", "pipeline", "shutdown:pre" })); +} + +TEST(CameraRenderer_Test, ShutsDownSequencesWhenPostPassInitializationFails) { + Scene scene("CameraRendererPostPassInitFailureScene"); + + GameObject* cameraObject = scene.CreateGameObject("Camera"); + auto* camera = cameraObject->AddComponent(); + camera->SetPrimary(true); + camera->SetDepth(4.0f); + + auto state = std::make_shared(); + CameraRenderer renderer(std::make_unique(state)); + + RenderPassSequence prePasses; + prePasses.AddPass(std::make_unique(state, "pre")); + + RenderPassSequence postPasses; + postPasses.AddPass(std::make_unique(state, "post", false, true)); + + CameraRenderRequest request; + request.scene = &scene; + request.camera = camera; + request.context = CreateValidContext(); + request.surface = RenderSurface(512, 512); + request.cameraDepth = camera->GetDepth(); + request.preScenePasses = &prePasses; + request.postScenePasses = &postPasses; + + EXPECT_FALSE(renderer.Render(request)); + EXPECT_EQ( + state->eventLog, + (std::vector{ + "init:pre", + "pre", + "pipeline", + "init:post", + "shutdown:post", + "shutdown:pre" })); } TEST(SceneRenderer_Test, BuildsSingleExplicitRequestFromSelectedCamera) { diff --git a/tests/editor/CMakeLists.txt b/tests/editor/CMakeLists.txt index 037f38e6..88a2747e 100644 --- a/tests/editor/CMakeLists.txt +++ b/tests/editor/CMakeLists.txt @@ -6,6 +6,8 @@ set(EDITOR_TEST_SOURCES test_action_routing.cpp test_scene_viewport_camera_controller.cpp test_scene_viewport_move_gizmo.cpp + test_scene_viewport_rotate_gizmo.cpp + test_scene_viewport_post_pass_plan.cpp test_scene_viewport_picker.cpp test_scene_viewport_overlay_renderer.cpp test_scene_viewport_selection_utils.cpp @@ -14,6 +16,7 @@ set(EDITOR_TEST_SOURCES ${CMAKE_SOURCE_DIR}/editor/src/Managers/ProjectManager.cpp ${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportPicker.cpp ${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportMoveGizmo.cpp + ${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportRotateGizmo.cpp ${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportGrid.cpp ) diff --git a/tests/editor/test_scene_viewport_post_pass_plan.cpp b/tests/editor/test_scene_viewport_post_pass_plan.cpp new file mode 100644 index 00000000..b4a279e5 --- /dev/null +++ b/tests/editor/test_scene_viewport_post_pass_plan.cpp @@ -0,0 +1,104 @@ +#include + +#include "Viewport/SceneViewportPostPassPlan.h" + +#include + +namespace { + +using XCEngine::Editor::BuildSceneViewportPostPassPlan; +using XCEngine::Editor::SceneViewportPostPassPlanInput; +using XCEngine::Editor::SceneViewportPostPassStep; + +TEST(SceneViewportPostPassPlan_Test, ReturnsInvalidPlanWhenOverlayIsUnavailable) { + const auto plan = BuildSceneViewportPostPassPlan({}); + + EXPECT_FALSE(plan.valid); + EXPECT_TRUE(plan.steps.empty()); + EXPECT_FALSE(plan.usesSelectionMaskSurface); + EXPECT_FALSE(plan.usesSelectionMaskShaderView); +} + +TEST(SceneViewportPostPassPlan_Test, BuildsGridOnlyPlanWhenNothingIsSelected) { + SceneViewportPostPassPlanInput input = {}; + input.overlayValid = true; + + const auto plan = BuildSceneViewportPostPassPlan(input); + + ASSERT_TRUE(plan.valid); + EXPECT_EQ( + plan.steps, + (std::vector{ + SceneViewportPostPassStep::ColorToRenderTarget, + SceneViewportPostPassStep::InfiniteGrid, + SceneViewportPostPassStep::ColorToShaderResource + })); + EXPECT_FALSE(plan.usesSelectionMaskSurface); + EXPECT_FALSE(plan.usesSelectionMaskShaderView); +} + +TEST(SceneViewportPostPassPlan_Test, BuildsSelectionOutlinePlanWhenSelectionResourcesExist) { + SceneViewportPostPassPlanInput input = {}; + input.overlayValid = true; + input.hasSelection = true; + input.hasSelectionMaskRenderTarget = true; + input.hasSelectionMaskShaderView = true; + + const auto plan = BuildSceneViewportPostPassPlan(input); + + ASSERT_TRUE(plan.valid); + EXPECT_EQ( + plan.steps, + (std::vector{ + SceneViewportPostPassStep::SelectionMask, + SceneViewportPostPassStep::ColorToRenderTarget, + SceneViewportPostPassStep::InfiniteGrid, + SceneViewportPostPassStep::SelectionOutline, + SceneViewportPostPassStep::ColorToShaderResource + })); + EXPECT_TRUE(plan.usesSelectionMaskSurface); + EXPECT_TRUE(plan.usesSelectionMaskShaderView); +} + +TEST(SceneViewportPostPassPlan_Test, SkipsSelectionSpecificPassesWhenMaskResourcesAreMissing) { + SceneViewportPostPassPlanInput input = {}; + input.overlayValid = true; + input.hasSelection = true; + input.hasSelectionMaskRenderTarget = true; + input.hasSelectionMaskShaderView = false; + + const auto plan = BuildSceneViewportPostPassPlan(input); + + ASSERT_TRUE(plan.valid); + EXPECT_EQ( + plan.steps, + (std::vector{ + SceneViewportPostPassStep::ColorToRenderTarget, + SceneViewportPostPassStep::InfiniteGrid, + SceneViewportPostPassStep::ColorToShaderResource + })); + EXPECT_FALSE(plan.usesSelectionMaskSurface); + EXPECT_FALSE(plan.usesSelectionMaskShaderView); +} + +TEST(SceneViewportPostPassPlan_Test, BuildsDebugMaskPlanWithoutSelectionMaskTarget) { + SceneViewportPostPassPlanInput input = {}; + input.overlayValid = true; + input.hasSelection = true; + input.debugSelectionMask = true; + + const auto plan = BuildSceneViewportPostPassPlan(input); + + ASSERT_TRUE(plan.valid); + EXPECT_EQ( + plan.steps, + (std::vector{ + SceneViewportPostPassStep::ColorToRenderTarget, + SceneViewportPostPassStep::SelectionMaskDebug, + SceneViewportPostPassStep::ColorToShaderResource + })); + EXPECT_FALSE(plan.usesSelectionMaskSurface); + EXPECT_FALSE(plan.usesSelectionMaskShaderView); +} + +} // namespace