diff --git a/engine/include/XCEngine/Rendering/RenderPipelineStageGraphContract.h b/engine/include/XCEngine/Rendering/RenderPipelineStageGraphContract.h index 348cdf1f..e7009296 100644 --- a/engine/include/XCEngine/Rendering/RenderPipelineStageGraphContract.h +++ b/engine/include/XCEngine/Rendering/RenderPipelineStageGraphContract.h @@ -34,5 +34,9 @@ bool RecordRenderPipelineStagePhasePass( RenderPassGraphEndCallback endPassCallback = {}, std::vector additionalReadTextures = {}); +bool RecordRenderPipelineStageFullscreenPassSequence( + const RenderPipelineStageRenderGraphContext& context, + const std::vector& passes); + } // namespace Rendering } // namespace XCEngine diff --git a/engine/src/Rendering/Execution/Internal/CameraFrameGraph/PassRecorder.cpp b/engine/src/Rendering/Execution/Internal/CameraFrameGraph/PassRecorder.cpp index 3dba73b1..74e29204 100644 --- a/engine/src/Rendering/Execution/Internal/CameraFrameGraph/PassRecorder.cpp +++ b/engine/src/Rendering/Execution/Internal/CameraFrameGraph/PassRecorder.cpp @@ -53,8 +53,7 @@ bool TryRecordCameraFramePipelineStageGraphPass( const CameraFrameRenderGraphStageContext& context, bool& handled) { CameraFrameRenderGraphBuilderContext& builder = context.builder; - if (!SupportsCameraFramePipelineGraphRecording(stageState.stage) || - builder.executionState.pipeline == nullptr || + if (builder.executionState.pipeline == nullptr || !builder.executionState.pipeline->SupportsStageRenderGraph( stageState.stage)) { handled = false; diff --git a/engine/src/Rendering/Execution/Internal/CameraFrameGraph/StageDispatch.cpp b/engine/src/Rendering/Execution/Internal/CameraFrameGraph/StageDispatch.cpp index 11a754ca..e4049a02 100644 --- a/engine/src/Rendering/Execution/Internal/CameraFrameGraph/StageDispatch.cpp +++ b/engine/src/Rendering/Execution/Internal/CameraFrameGraph/StageDispatch.cpp @@ -18,9 +18,9 @@ using CameraFrameStageRecordHandler = bool (*)( bool& handled); constexpr std::array kCameraFrameStageRecordHandlers = { + &TryRecordCameraFramePipelineStageGraphPass, &TryRecordCameraFrameStageSequence, - &TryRecordCameraFrameStageStandaloneRenderGraphPass, - &TryRecordCameraFramePipelineStageGraphPass + &TryRecordCameraFrameStageStandaloneRenderGraphPass }; } // namespace diff --git a/engine/src/Rendering/RenderPipelineStageGraphContract.cpp b/engine/src/Rendering/RenderPipelineStageGraphContract.cpp index 7b987228..ab1d2206 100644 --- a/engine/src/Rendering/RenderPipelineStageGraphContract.cpp +++ b/engine/src/Rendering/RenderPipelineStageGraphContract.cpp @@ -1,14 +1,152 @@ #include +#include "Rendering/Execution/Internal/CameraFrameGraph/SurfaceUtils.h" +#include +#include #include #include +#include #include #include namespace XCEngine { namespace Rendering { +namespace { + +struct RenderPipelineStageFullscreenSourceBinding { + const RenderSurface* sourceSurfaceTemplate = nullptr; + RHI::RHIResourceView* sourceColorView = nullptr; + RHI::ResourceStates sourceColorState = RHI::ResourceStates::Common; + RenderGraphTextureHandle sourceColorTexture = {}; +}; + +RenderGraphTextureHandle ResolveRenderPipelineStageDefaultFullscreenSource( + const RenderPipelineStageRenderGraphContext& context) { + const CameraFrameRenderGraphResources* const frameResources = + TryGetCameraFrameRenderGraphResources(context.blackboard); + if (frameResources == nullptr) { + return {}; + } + + switch (context.stage) { + case CameraFrameStage::PostProcess: + return frameResources->mainScene.color; + case CameraFrameStage::FinalOutput: + return frameResources->postProcess.color.IsValid() + ? frameResources->postProcess.color + : frameResources->mainScene.color; + default: + return {}; + } +} + +RenderPipelineStageFullscreenSourceBinding +ResolveRenderPipelineStageFullscreenSourceBinding( + const RenderPipelineStageRenderGraphContext& context) { + RenderPipelineStageFullscreenSourceBinding binding = { + context.sourceSurface, + context.sourceColorView, + context.sourceColorState, + context.sourceColorTexture + }; + + if (!binding.sourceColorTexture.IsValid()) { + const RenderGraphTextureHandle defaultSourceTexture = + ResolveRenderPipelineStageDefaultFullscreenSource(context); + if (defaultSourceTexture.IsValid()) { + binding.sourceSurfaceTemplate = &context.surfaceTemplate; + binding.sourceColorView = nullptr; + binding.sourceColorState = + RHI::ResourceStates::PixelShaderResource; + binding.sourceColorTexture = defaultSourceTexture; + } + } + + if (binding.sourceSurfaceTemplate == nullptr && + binding.sourceColorTexture.IsValid()) { + binding.sourceSurfaceTemplate = &context.surfaceTemplate; + } + + return binding; +} + +RenderGraphTextureHandle ResolveRenderPipelineStagePrimaryColorTarget( + const std::vector& colorTargets) { + const auto it = + std::find_if( + colorTargets.begin(), + colorTargets.end(), + [](RenderGraphTextureHandle handle) { + return handle.IsValid(); + }); + return it != colorTargets.end() + ? *it + : RenderGraphTextureHandle{}; +} + +bool RecordRenderPipelineStageFullscreenPass( + const RenderPipelineStageRenderGraphContext& context, + const Containers::String& passName, + const RenderPipelineStageFullscreenSourceBinding& initialSourceBinding, + RenderGraphTextureHandle finalOutputColor, + size_t passIndex, + size_t passCount, + RenderGraphTextureHandle& currentSourceColor, + RenderPass& pass) { + const bool isLastPass = (passIndex + 1u) == passCount; + const RenderGraphTextureHandle passOutputColor = + isLastPass + ? finalOutputColor + : context.graphBuilder.CreateTransientTexture( + passName + ".Color", + BuildFullscreenTransientTextureDesc(context.surfaceTemplate)); + const RenderGraphRecordingSourceBinding sourceBinding = + MakeRenderGraphRecordingSourceBinding( + passIndex == 0u + ? initialSourceBinding.sourceSurfaceTemplate + : &context.surfaceTemplate, + passIndex == 0u + ? initialSourceBinding.sourceColorView + : nullptr, + passIndex == 0u + ? initialSourceBinding.sourceColorState + : RHI::ResourceStates::PixelShaderResource, + currentSourceColor); + + const RenderGraphRecordingContext baseContext = + BuildRenderGraphRecordingContext(context); + RenderGraphRecordingContextBuildParams recordingParams = {}; + recordingParams.passName = &passName; + recordingParams.overrideSourceBinding = true; + recordingParams.sourceBinding = sourceBinding; + recordingParams.overrideColorTargets = true; + recordingParams.colorTargets = { passOutputColor }; + recordingParams.overrideDepthTarget = true; + recordingParams.depthTarget = {}; + + const RenderPassRenderGraphContext renderGraphContext = + BuildRenderPassRenderGraphContext( + BuildRenderGraphRecordingContext( + baseContext, + std::move(recordingParams))); + const bool recorded = + pass.SupportsRenderGraph() + ? pass.RecordRenderGraph(renderGraphContext) + : RecordRasterRenderPass( + pass, + renderGraphContext, + BuildSourceColorFullscreenRasterPassGraphIO()); + if (recorded) { + currentSourceColor = passOutputColor; + } + + return recorded; +} + +} // namespace + Containers::String BuildRenderPipelineStagePhasePassName( const Containers::String& baseName, ScenePhase scenePhase) { @@ -67,5 +205,58 @@ bool RecordRenderPipelineStagePhasePass( std::move(additionalReadTextures)); } +bool RecordRenderPipelineStageFullscreenPassSequence( + const RenderPipelineStageRenderGraphContext& context, + const std::vector& passes) { + std::vector recordedPasses = {}; + recordedPasses.reserve(passes.size()); + for (RenderPass* const pass : passes) { + if (pass != nullptr) { + recordedPasses.push_back(pass); + } + } + + if (recordedPasses.empty()) { + return true; + } + + const RenderPipelineStageFullscreenSourceBinding sourceBinding = + ResolveRenderPipelineStageFullscreenSourceBinding(context); + if (!sourceBinding.sourceColorTexture.IsValid()) { + return false; + } + + const RenderGraphTextureHandle finalOutputColor = + ResolveRenderPipelineStagePrimaryColorTarget(context.colorTargets); + if (!finalOutputColor.IsValid()) { + return false; + } + + RenderGraphTextureHandle currentSourceColor = + sourceBinding.sourceColorTexture; + for (size_t passIndex = 0u; passIndex < recordedPasses.size(); ++passIndex) { + RenderPass* const pass = recordedPasses[passIndex]; + const Containers::String passName = + recordedPasses.size() == 1u + ? context.passName + : BuildRenderGraphSequencePassName( + context.passName, + passIndex); + if (!RecordRenderPipelineStageFullscreenPass( + context, + passName, + sourceBinding, + finalOutputColor, + passIndex, + recordedPasses.size(), + currentSourceColor, + *pass)) { + return false; + } + } + + return true; +} + } // namespace Rendering } // namespace XCEngine diff --git a/engine/src/Scripting/Mono/MonoScriptRuntime.cpp b/engine/src/Scripting/Mono/MonoScriptRuntime.cpp index d2249974..09048d13 100644 --- a/engine/src/Scripting/Mono/MonoScriptRuntime.cpp +++ b/engine/src/Scripting/Mono/MonoScriptRuntime.cpp @@ -10,9 +10,11 @@ #include "Debug/Logger.h" #include "Input/InputManager.h" #include "Physics/PhysicsWorld.h" +#include "Rendering/Passes/BuiltinColorScalePostProcessPass.h" #include "Rendering/Pipelines/BuiltinForwardPipeline.h" #include "Rendering/Pipelines/BuiltinForwardSceneRecorder.h" #include "Rendering/Pipelines/ManagedScriptableRenderPipelineAsset.h" +#include "Rendering/RenderPipelineStageGraphContract.h" #include "Scene/Scene.h" #include "Scripting/ScriptComponent.h" #include "Scripting/ScriptEngine.h" @@ -91,6 +93,7 @@ struct ManagedScriptableRenderContextState { const Rendering::RenderPipelineStageRenderGraphContext* graphContext = nullptr; Rendering::Pipelines::BuiltinForwardSceneRecorder* builtinForwardSceneRecorder = nullptr; + std::vector queuedBuiltinColorScaleFullscreenPasses = {}; }; uint64_t& GetManagedScriptableRenderContextNextHandle() { @@ -212,6 +215,12 @@ bool TryUnboxManagedBoolean(MonoObject* boxedValue, bool& outValue) { return true; } +bool SupportsManagedRenderPipelineStageGraphRecording( + Rendering::CameraFrameStage stage) { + return Rendering::SupportsCameraFramePipelineGraphRecording(stage) || + Rendering::IsCameraFrameFullscreenSequenceStage(stage); +} + } // namespace class MonoManagedRenderPipelineStageRecorder final @@ -235,6 +244,13 @@ public: } void Shutdown() override { + for (std::unique_ptr& pass : + m_builtinColorScalePassPool) { + if (pass != nullptr) { + pass->Shutdown(); + } + } + m_builtinColorScalePassPool.clear(); m_builtinForwardPipeline.Shutdown(); ReleaseManagedObjects(); m_supportsStageMethod = nullptr; @@ -243,7 +259,7 @@ public: } bool SupportsStageRenderGraph(Rendering::CameraFrameStage stage) const override { - if (!Rendering::SupportsCameraFramePipelineGraphRecording(stage) || + if (!SupportsManagedRenderPipelineStageGraphRecording(stage) || !EnsureManagedPipeline()) { return false; } @@ -313,7 +329,13 @@ public: } bool recorded = false; - return TryUnboxManagedBoolean(result, recorded) && recorded; + if (!(TryUnboxManagedBoolean(result, recorded) && recorded)) { + return false; + } + + return FlushManagedFullscreenPasses( + context, + managedContextState); } private: @@ -441,6 +463,40 @@ private: return m_recordStageMethod; } + bool FlushManagedFullscreenPasses( + const Rendering::RenderPipelineStageRenderGraphContext& context, + const ManagedScriptableRenderContextState& managedContextState) { + if (managedContextState.queuedBuiltinColorScaleFullscreenPasses.empty()) { + return true; + } + if (!Rendering::IsCameraFrameFullscreenSequenceStage(context.stage)) { + return false; + } + + while (m_builtinColorScalePassPool.size() < + managedContextState.queuedBuiltinColorScaleFullscreenPasses.size()) { + m_builtinColorScalePassPool.push_back( + std::make_unique()); + } + + std::vector passes = {}; + passes.reserve( + managedContextState.queuedBuiltinColorScaleFullscreenPasses.size()); + for (size_t passIndex = 0u; + passIndex < managedContextState.queuedBuiltinColorScaleFullscreenPasses.size(); + ++passIndex) { + Rendering::Passes::BuiltinColorScalePostProcessPass* const pass = + m_builtinColorScalePassPool[passIndex].get(); + pass->SetColorScale( + managedContextState.queuedBuiltinColorScaleFullscreenPasses[passIndex]); + passes.push_back(pass); + } + + return Rendering::RecordRenderPipelineStageFullscreenPassSequence( + context, + passes); + } + MonoScriptRuntime* m_runtime = nullptr; std::weak_ptr m_runtimeLifetime; Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor m_descriptor; @@ -449,6 +505,8 @@ private: mutable MonoMethod* m_supportsStageMethod = nullptr; mutable MonoMethod* m_recordStageMethod = nullptr; mutable bool m_pipelineCreationAttempted = false; + std::vector> + m_builtinColorScalePassPool = {}; Rendering::Pipelines::BuiltinForwardPipeline m_builtinForwardPipeline; }; @@ -2232,6 +2290,23 @@ InternalCall_Rendering_ScriptableRenderContext_RecordBuiltinForwardInjectionPoin : 0; } +mono_bool +InternalCall_Rendering_ScriptableRenderContext_RecordBuiltinColorScaleFullscreenPass( + uint64_t nativeHandle, + XCEngine::Math::Vector4* colorScale) { + ManagedScriptableRenderContextState* const state = + FindManagedScriptableRenderContextState(nativeHandle); + if (state == nullptr || + state->graphContext == nullptr || + colorScale == nullptr || + !Rendering::IsCameraFrameFullscreenSequenceStage(state->stage)) { + return 0; + } + + state->queuedBuiltinColorScaleFullscreenPasses.push_back(*colorScale); + return 1; +} + void RegisterInternalCalls() { if (GetInternalCallRegistrationState()) { return; @@ -2364,6 +2439,7 @@ void RegisterInternalCalls() { mono_add_internal_call("XCEngine.InternalCalls::Rendering_ScriptableRenderContext_RecordBuiltinForwardMainScene", reinterpret_cast(&InternalCall_Rendering_ScriptableRenderContext_RecordBuiltinForwardMainScene)); mono_add_internal_call("XCEngine.InternalCalls::Rendering_ScriptableRenderContext_RecordBuiltinForwardScenePhase", reinterpret_cast(&InternalCall_Rendering_ScriptableRenderContext_RecordBuiltinForwardScenePhase)); mono_add_internal_call("XCEngine.InternalCalls::Rendering_ScriptableRenderContext_RecordBuiltinForwardInjectionPoint", reinterpret_cast(&InternalCall_Rendering_ScriptableRenderContext_RecordBuiltinForwardInjectionPoint)); + mono_add_internal_call("XCEngine.InternalCalls::Rendering_ScriptableRenderContext_RecordBuiltinColorScaleFullscreenPass", reinterpret_cast(&InternalCall_Rendering_ScriptableRenderContext_RecordBuiltinColorScaleFullscreenPass)); GetInternalCallRegistrationState() = true; } diff --git a/managed/GameScripts/RenderPipelineApiProbe.cs b/managed/GameScripts/RenderPipelineApiProbe.cs index 914e8e24..048999f7 100644 --- a/managed/GameScripts/RenderPipelineApiProbe.cs +++ b/managed/GameScripts/RenderPipelineApiProbe.cs @@ -21,6 +21,14 @@ namespace Gameplay } } + public sealed class ManagedPostProcessRenderPipelineProbeAsset : ScriptableRenderPipelineAsset + { + protected override ScriptableRenderPipeline CreatePipeline() + { + return new ManagedPostProcessRenderPipelineProbe(); + } + } + public sealed class ManagedRenderPipelineProbe : ScriptableRenderPipeline { public static int SupportsStageCallCount; @@ -57,6 +65,26 @@ namespace Gameplay } } + public sealed class ManagedPostProcessRenderPipelineProbe : ScriptableRenderPipeline + { + protected override bool SupportsStageRenderGraph( + CameraFrameStage stage) + { + return stage == CameraFrameStage.PostProcess; + } + + protected override bool RecordStageRenderGraph( + ScriptableRenderContext context) + { + return context != null && + context.stage == CameraFrameStage.PostProcess && + context.RecordBuiltinColorScaleFullscreenPass( + new Vector4(1.10f, 0.95f, 0.90f, 1.0f)) && + context.RecordBuiltinColorScaleFullscreenPass( + new Vector4(0.95f, 1.05f, 1.10f, 1.0f)); + } + } + public sealed class RenderPipelineApiProbe : MonoBehaviour { public bool InitialTypeWasNull; diff --git a/managed/XCEngine.ScriptCore/InternalCalls.cs b/managed/XCEngine.ScriptCore/InternalCalls.cs index b1e9a0af..2ac7036c 100644 --- a/managed/XCEngine.ScriptCore/InternalCalls.cs +++ b/managed/XCEngine.ScriptCore/InternalCalls.cs @@ -402,5 +402,11 @@ namespace XCEngine Rendering_ScriptableRenderContext_RecordBuiltinForwardInjectionPoint( ulong nativeHandle, int injectionPoint); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern bool + Rendering_ScriptableRenderContext_RecordBuiltinColorScaleFullscreenPass( + ulong nativeHandle, + ref Vector4 colorScale); } } diff --git a/managed/XCEngine.ScriptCore/ScriptableRenderContext.cs b/managed/XCEngine.ScriptCore/ScriptableRenderContext.cs index 4b430fbb..a548097a 100644 --- a/managed/XCEngine.ScriptCore/ScriptableRenderContext.cs +++ b/managed/XCEngine.ScriptCore/ScriptableRenderContext.cs @@ -41,5 +41,14 @@ namespace XCEngine m_nativeHandle, (int)injectionPoint); } + + public bool RecordBuiltinColorScaleFullscreenPass( + Vector4 colorScale) + { + return InternalCalls + .Rendering_ScriptableRenderContext_RecordBuiltinColorScaleFullscreenPass( + m_nativeHandle, + ref colorScale); + } } } diff --git a/tests/Rendering/unit/test_camera_frame_graph_stage_contract.cpp b/tests/Rendering/unit/test_camera_frame_graph_stage_contract.cpp index b289c80c..d10e7b03 100644 --- a/tests/Rendering/unit/test_camera_frame_graph_stage_contract.cpp +++ b/tests/Rendering/unit/test_camera_frame_graph_stage_contract.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include "Rendering/Execution/Internal/CameraFrameGraph/BuilderContext.h" #include "Rendering/Execution/Internal/CameraFrameGraph/StageContract.h" @@ -993,6 +994,137 @@ TEST(CameraFrameRenderGraphStageContract_Test, RecordsFullscreenSequenceStageGra EXPECT_EQ(currentSourceColor.index, stageState.outputColor.index); } +TEST(RenderPipelineStageGraphContract_Test, RecordsFullscreenPassSequenceAndChainsSourceColor) { + RenderGraph graph = {}; + RenderGraphBuilder graphBuilder(graph); + RenderGraphBlackboard blackboard = {}; + bool executionSucceeded = true; + RenderContext renderContext = {}; + RenderSceneData sceneData = {}; + RenderSurface surfaceTemplate(1280, 720); + RenderSurface explicitSourceSurface(1280, 720); + const RenderGraphTextureHandle sourceColor = + graphBuilder.CreateTransientTexture( + "ManagedPostProcess.Source", + BuildTestRenderGraphTextureDesc()); + const RenderGraphTextureHandle outputColor = + graphBuilder.CreateTransientTexture( + "ManagedPostProcess.Output", + BuildTestRenderGraphTextureDesc()); + const RenderPipelineStageRenderGraphContext context = { + graphBuilder, + "ManagedPostProcess", + CameraFrameStage::PostProcess, + renderContext, + sceneData, + surfaceTemplate, + &explicitSourceSurface, + reinterpret_cast(81), + XCEngine::RHI::ResourceStates::CopySrc, + sourceColor, + { outputColor }, + {}, + &executionSucceeded, + &blackboard + }; + + RecordingRenderPass firstPass = {}; + RecordingRenderPass secondPass = {}; + ASSERT_TRUE(RecordRenderPipelineStageFullscreenPassSequence( + context, + std::vector{ &firstPass, &secondPass })); + + ASSERT_EQ(firstPass.recordCalls, 1); + EXPECT_STREQ(firstPass.lastPassName.c_str(), "ManagedPostProcess.Pass0"); + EXPECT_EQ(firstPass.lastSourceSurface, &explicitSourceSurface); + EXPECT_EQ( + firstPass.lastSourceColorView, + reinterpret_cast(81)); + EXPECT_EQ( + firstPass.lastSourceColorState, + XCEngine::RHI::ResourceStates::CopySrc); + EXPECT_EQ(firstPass.lastSourceColorTexture.index, sourceColor.index); + ASSERT_EQ(firstPass.lastColorTargets.size(), 1u); + const RenderGraphTextureHandle intermediateColor = + firstPass.lastColorTargets[0]; + EXPECT_TRUE(intermediateColor.IsValid()); + EXPECT_NE(intermediateColor.index, outputColor.index); + EXPECT_FALSE(firstPass.lastDepthTarget.IsValid()); + + ASSERT_EQ(secondPass.recordCalls, 1); + EXPECT_STREQ(secondPass.lastPassName.c_str(), "ManagedPostProcess.Pass1"); + ASSERT_NE(secondPass.lastSourceSurface, nullptr); + EXPECT_EQ(secondPass.lastSourceSurface->GetWidth(), context.surfaceTemplate.GetWidth()); + EXPECT_EQ(secondPass.lastSourceSurface->GetHeight(), context.surfaceTemplate.GetHeight()); + EXPECT_EQ(secondPass.lastSourceColorView, nullptr); + EXPECT_EQ( + secondPass.lastSourceColorState, + XCEngine::RHI::ResourceStates::PixelShaderResource); + EXPECT_EQ( + secondPass.lastSourceColorTexture.index, + intermediateColor.index); + ASSERT_EQ(secondPass.lastColorTargets.size(), 1u); + EXPECT_EQ(secondPass.lastColorTargets[0].index, outputColor.index); + EXPECT_FALSE(secondPass.lastDepthTarget.IsValid()); +} + +TEST(RenderPipelineStageGraphContract_Test, ResolvesManagedPostProcessSourceFromBlackboard) { + RenderGraph graph = {}; + RenderGraphBuilder graphBuilder(graph); + RenderGraphBlackboard blackboard = {}; + CameraFrameRenderGraphFrameData& frameData = + EmplaceCameraFrameRenderGraphFrameData(blackboard); + bool executionSucceeded = true; + RenderContext renderContext = {}; + RenderSceneData sceneData = {}; + RenderSurface surfaceTemplate(1280, 720); + const RenderGraphTextureHandle sourceColor = + graphBuilder.CreateTransientTexture( + "ManagedPostProcess.Source", + BuildTestRenderGraphTextureDesc()); + const RenderGraphTextureHandle outputColor = + graphBuilder.CreateTransientTexture( + "ManagedPostProcess.Output", + BuildTestRenderGraphTextureDesc()); + frameData.resources.mainScene.color = sourceColor; + + const RenderPipelineStageRenderGraphContext context = { + graphBuilder, + "ManagedPostProcess", + CameraFrameStage::PostProcess, + renderContext, + sceneData, + surfaceTemplate, + nullptr, + nullptr, + XCEngine::RHI::ResourceStates::Common, + {}, + { outputColor }, + {}, + &executionSucceeded, + &blackboard + }; + + RecordingRenderPass pass = {}; + ASSERT_TRUE(RecordRenderPipelineStageFullscreenPassSequence( + context, + std::vector{ &pass })); + + ASSERT_EQ(pass.recordCalls, 1); + EXPECT_STREQ(pass.lastPassName.c_str(), "ManagedPostProcess"); + ASSERT_NE(pass.lastSourceSurface, nullptr); + EXPECT_EQ(pass.lastSourceSurface->GetWidth(), context.surfaceTemplate.GetWidth()); + EXPECT_EQ(pass.lastSourceSurface->GetHeight(), context.surfaceTemplate.GetHeight()); + EXPECT_EQ(pass.lastSourceColorView, nullptr); + EXPECT_EQ( + pass.lastSourceColorState, + XCEngine::RHI::ResourceStates::PixelShaderResource); + EXPECT_EQ(pass.lastSourceColorTexture.index, sourceColor.index); + ASSERT_EQ(pass.lastColorTargets.size(), 1u); + EXPECT_EQ(pass.lastColorTargets[0].index, outputColor.index); + EXPECT_FALSE(pass.lastDepthTarget.IsValid()); +} + TEST(CameraFrameRenderGraphStageContract_Test, KeepsExplicitFullscreenSourceBindingUntouched) { CameraFramePlan plan = {}; RenderSurface stageSurface(320, 180); diff --git a/tests/Rendering/unit/test_camera_scene_renderer.cpp b/tests/Rendering/unit/test_camera_scene_renderer.cpp index d43e0373..63a779ea 100644 --- a/tests/Rendering/unit/test_camera_scene_renderer.cpp +++ b/tests/Rendering/unit/test_camera_scene_renderer.cpp @@ -55,9 +55,13 @@ struct MockPipelineState { int configureDirectionalShadowExecutionStateCalls = 0; int renderCalls = 0; int recordMainSceneCalls = 0; + int recordPostProcessCalls = 0; + int recordFinalOutputCalls = 0; int executeRecordedMainSceneCalls = 0; bool renderResult = true; bool supportsMainSceneRenderGraph = false; + bool supportsPostProcessRenderGraph = false; + bool supportsFinalOutputRenderGraph = false; bool recordMainSceneResult = true; bool lastSurfaceAutoTransitionEnabled = true; uint32_t lastSurfaceWidth = 0; @@ -490,13 +494,33 @@ public: } bool SupportsStageRenderGraph(CameraFrameStage stage) const override { - return SupportsCameraFramePipelineGraphRecording(stage) && - m_state->supportsMainSceneRenderGraph; + switch (stage) { + case CameraFrameStage::MainScene: + return m_state->supportsMainSceneRenderGraph; + case CameraFrameStage::PostProcess: + return m_state->supportsPostProcessRenderGraph; + case CameraFrameStage::FinalOutput: + return m_state->supportsFinalOutputRenderGraph; + default: + return false; + } } bool RecordStageRenderGraph( const RenderPipelineStageRenderGraphContext& context) override { - ++m_state->recordMainSceneCalls; + switch (context.stage) { + case CameraFrameStage::MainScene: + ++m_state->recordMainSceneCalls; + break; + case CameraFrameStage::PostProcess: + ++m_state->recordPostProcessCalls; + break; + case CameraFrameStage::FinalOutput: + ++m_state->recordFinalOutputCalls; + break; + default: + break; + } m_state->lastReceivedRenderGraphBlackboard = context.blackboard != nullptr; if (const CameraFrameRenderGraphResources* frameResources = TryGetCameraFrameRenderGraphResources(context.blackboard)) { @@ -2143,6 +2167,69 @@ TEST(CameraRenderer_Test, ResolvesGraphManagedFullscreenSequenceSourcesFromFrame XCEngine::RHI::ResourceStates::PixelShaderResource); } +TEST(CameraRenderer_Test, PrefersPipelineStageRecordingOverLegacyPostProcessSequence) { + Scene scene("CameraRendererManagedPostProcessScene"); + + GameObject* cameraObject = scene.CreateGameObject("Camera"); + auto* camera = cameraObject->AddComponent(); + camera->SetPrimary(true); + + auto state = std::make_shared(); + auto allocationState = std::make_shared(); + MockShadowDevice device(allocationState); + MockShadowView colorView( + allocationState, + XCEngine::RHI::ResourceViewType::RenderTarget, + XCEngine::RHI::Format::R8G8B8A8_UNorm, + XCEngine::RHI::ResourceViewDimension::Texture2D); + MockShadowView depthView( + allocationState, + XCEngine::RHI::ResourceViewType::DepthStencil, + XCEngine::RHI::Format::D24_UNorm_S8_UInt, + XCEngine::RHI::ResourceViewDimension::Texture2D); + state->supportsMainSceneRenderGraph = true; + state->supportsPostProcessRenderGraph = true; + CameraRenderer renderer(std::make_unique(state)); + + auto postProcessPass = + std::make_unique(state, "postProcess", true, true, true); + TrackingPass* const postProcessPassRaw = postProcessPass.get(); + RenderPassSequence postProcessPasses; + postProcessPasses.AddPass(std::move(postProcessPass)); + + CameraRenderRequest request = {}; + request.scene = &scene; + request.camera = camera; + request.context = CreateValidContext(); + request.context.device = &device; + request.surface = RenderSurface(320, 180); + request.surface.SetColorAttachment(&colorView); + request.surface.SetDepthAttachment(&depthView); + request.cameraDepth = camera->GetDepth(); + request.postProcess.passes = &postProcessPasses; + request.postProcess.destinationSurface = request.surface; + + CameraFramePlan plan = CameraFramePlan::FromRequest(request); + plan.colorChain.usesGraphManagedSceneColor = true; + plan.ConfigureGraphManagedSceneSurface(); + plan.colorChain.postProcess.source = CameraFrameColorSource::MainSceneColor; + + ASSERT_TRUE(plan.IsPostProcessStageValid()); + ASSERT_TRUE(renderer.Render(plan)); + EXPECT_EQ(state->recordMainSceneCalls, 1); + EXPECT_EQ(state->recordPostProcessCalls, 1); + EXPECT_EQ( + state->eventLog, + (std::vector{ + "pipelineGraph", + "pipelineGraph" })); + + ASSERT_NE(postProcessPassRaw, nullptr); + EXPECT_FALSE(postProcessPassRaw->lastRecordedRenderGraphBlackboard); + EXPECT_EQ(postProcessPassRaw->lastSurfaceWidth, 0u); + EXPECT_EQ(postProcessPassRaw->lastSurfaceHeight, 0u); +} + TEST(CameraRenderer_Test, ExecutesShadowCasterAndDepthOnlyRequestsBeforeMainPipeline) { Scene scene("CameraRendererDepthAndShadowScene");