diff --git a/docs/used/SRP_ManagedFramePlan接缝与Universal规划层计划_2026-04-20.md b/docs/used/SRP_ManagedFramePlan接缝与Universal规划层计划_2026-04-20.md new file mode 100644 index 00000000..4986c74e --- /dev/null +++ b/docs/used/SRP_ManagedFramePlan接缝与Universal规划层计划_2026-04-20.md @@ -0,0 +1,165 @@ +# SRP Managed Frame Plan 接缝与 Universal 规划层计划 2026-04-20 + +## 1. 阶段目标 + +当前 SRP 主线已经有了两条 managed 接缝: + +1. `ConfigureCameraRenderRequest` +2. `RecordStageRenderGraph` + +但真正夹在中间、决定“一帧到底要不要有 post process / final output,以及这些 stage 怎么串”的 +`CameraFramePlan` 仍然完全停留在 native 默认策略里。 + +这一阶段的目标,就是把这条缺口补上: + +1. 给 managed SRP Core 增加正式的 frame-plan planning context +2. 让 `ScriptableRenderPipelineAsset` 能参与 `CameraFramePlan` 配置 +3. 让 Universal 的 `ScriptableRendererData / ScriptableRendererFeature` 能继续把这层规划往下分发 +4. 保持 Render Graph 仍然留在 C++,只把“规划策略”暴露给 C# + +--- + +## 2. 当前问题 + +### 2.1 managed 现在只能改 request,不能改 frame plan + +现在 managed 资产可以在 request 阶段做: + +1. 清理 directional shadow +2. 选择 native backend key +3. 提供 final color 默认值 + +但它还不能做: + +1. 显式请求 post-process stage +2. 显式请求 final-output stage +3. 决定 stage color source +4. 决定 graph-managed output color 的串联关系 + +这导致 managed 管线如果想表达“我需要这两个阶段”,目前只能绕到 +`SupportsStageRenderGraph` 这条被动推断路径。 + +### 2.2 Universal 还没有正式的“规划层”下发通道 + +当前 Universal 已经有: + +`Asset -> RendererData -> Renderer -> Feature -> Pass` + +但只有: + +1. request 配置通道 +2. recording 通道 + +缺少: + +1. plan 配置通道 + +所以 `RendererData` 和 `RendererFeature` 还没法正式参与 stage planning。 + +### 2.3 现在的 fullscreen stage 主要靠 native heuristic 推断 + +当前 `CameraFramePlanBuilder` 里还有一层: + +`ConfigureFullscreenStagesFromPipeline(...)` + +它会根据 pipeline 对 stage 的支持情况补出 fullscreen stage。 + +这条 heuristic 对当前主线有用,但它不是未来 SRP/URP 的最终 authoring 方式。 +真正成熟的方向应该是: + +1. native 负责执行与兜底 +2. managed asset / renderer data / feature 负责更明确的规划策略 + +--- + +## 3. 本阶段方案 + +### 3.1 Core:新增 planning context + +在 managed core 增加: + +1. `ScriptableRenderPipelinePlanningContext` +2. `ScriptableRenderPipelineAsset.ConfigureCameraFramePlan(...)` + +planning context 先只暴露这阶段真正需要的最小能力: + +1. 查询 stage 是否已请求 +2. 查询 stage color source +3. 查询 stage 是否使用 graph-managed output color +4. 请求 fullscreen stage +5. 清理 fullscreen stage + +### 3.2 Native bridge:把 plan 接缝接进 Mono runtime + +在 native/Mono bridge 增加: + +1. planning context state registry +2. managed planning context object 构造 +3. `ManagedRenderPipelineAssetRuntime::ConfigureCameraFramePlan(...)` +4. 对应 internal call + +### 3.3 Universal:补上 renderer data / feature 规划层 + +给 Universal 包增加: + +1. `ScriptableRendererData.ConfigureCameraFramePlanInstance(...)` +2. `ScriptableRendererFeature.ConfigureCameraFramePlan(...)` +3. `UniversalRenderPipelineAsset` 把 planning context 下发到默认 renderer data + +这样 Universal 的 request / plan / record 三层链路就对齐了。 + +--- + +## 4. 实施步骤 + +### Step 1:补 managed core planning API + +目标: + +1. 新增 `ScriptableRenderPipelinePlanningContext` +2. 新增 `ScriptableRenderPipelineAsset.ConfigureCameraFramePlan` +3. 补 internal call 声明 + +### Step 2:补 native planning bridge + +目标: + +1. 扩展 `ManagedRenderPipelineAssetRuntime` +2. `ManagedScriptableRenderPipelineAsset` 调用 managed `ConfigureCameraFramePlan` +3. Mono runtime 能创建并驱动 managed planning context + +### Step 3:把 Universal 接到 planning seam + +目标: + +1. `ScriptableRendererData` 下发 plan 配置 +2. `ScriptableRendererFeature` 能参与 plan 配置 +3. `UniversalRenderPipelineAsset` 使用默认 renderer data 承担这层逻辑 + +### Step 4:补 probe 与回归测试 + +目标: + +1. 更新 API surface probe +2. 新增/更新 managed probe asset,验证 managed plan hook 真正生效 +3. 锁住 Universal feature 的 planning seam + +### Step 5:完整验证与收口 + +目标: + +1. 编译 `rendering_unit_tests`、`scripting_tests`、`XCEditor` +2. 运行相关测试 +3. 运行旧版 `editor/bin/Debug/XCEngine.exe` 至少 10 秒并检查新的 `SceneReady` + +--- + +## 5. 验收标准 + +完成后应满足: + +1. managed `ScriptableRenderPipelineAsset` 可以正式参与 `CameraFramePlan` 配置 +2. `UniversalRenderPipelineAsset -> ScriptableRendererData -> ScriptableRendererFeature` 有完整的 planning 分发链 +3. fullscreen stage 不再只能依赖“pipeline support heuristic”这一条隐式路径 +4. API surface probe 与脚本回归测试锁住新的 public / protected seam +5. 编译、测试、旧版 editor 冒烟全部通过 diff --git a/engine/include/XCEngine/Rendering/Execution/CameraFramePlan.h b/engine/include/XCEngine/Rendering/Execution/CameraFramePlan.h index f3b9f700..da1fb43f 100644 --- a/engine/include/XCEngine/Rendering/Execution/CameraFramePlan.h +++ b/engine/include/XCEngine/Rendering/Execution/CameraFramePlan.h @@ -12,6 +12,7 @@ namespace Rendering { struct CameraFrameFullscreenStagePlan { bool requested = false; bool usesGraphManagedOutputColor = false; + bool explicitlyConfigured = false; CameraFrameColorSource source = CameraFrameColorSource::ExplicitSurface; }; @@ -58,8 +59,13 @@ struct CameraFramePlan { bool RequestFullscreenStage( CameraFrameStage stage, CameraFrameColorSource source, - bool usesGraphManagedOutputColor = false); - void ClearFullscreenStage(CameraFrameStage stage); + bool usesGraphManagedOutputColor = false, + bool explicitlyConfigured = false); + void ClearFullscreenStage( + CameraFrameStage stage, + bool explicitlyConfigured = false); + bool HasExplicitFullscreenStageConfiguration( + CameraFrameStage stage) const; bool IsPostProcessStageValid() const; bool IsFinalOutputStageValid() const; bool HasFrameStage(CameraFrameStage stage) const; diff --git a/engine/include/XCEngine/Rendering/Pipelines/ManagedScriptableRenderPipelineAsset.h b/engine/include/XCEngine/Rendering/Pipelines/ManagedScriptableRenderPipelineAsset.h index ab545318..7be4c1e2 100644 --- a/engine/include/XCEngine/Rendering/Pipelines/ManagedScriptableRenderPipelineAsset.h +++ b/engine/include/XCEngine/Rendering/Pipelines/ManagedScriptableRenderPipelineAsset.h @@ -10,6 +10,7 @@ namespace XCEngine { namespace Rendering { +struct CameraFramePlan; struct CameraRenderRequest; struct DirectionalShadowPlanningSettings; @@ -52,6 +53,7 @@ public: size_t renderedRequestCount, const DirectionalShadowPlanningSettings& directionalShadowSettings) const override; + void ConfigureCameraFramePlan(CameraFramePlan& plan) const override; FinalColorSettings GetDefaultFinalColorSettings() const override; private: @@ -82,6 +84,8 @@ public: size_t, const DirectionalShadowPlanningSettings&) const { } + virtual void ConfigureCameraFramePlan(CameraFramePlan&) const { + } virtual std::shared_ptr GetPipelineRendererAsset() const { diff --git a/engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h b/engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h index 747fce24..0799788d 100644 --- a/engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h +++ b/engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h @@ -266,6 +266,8 @@ private: MonoObject* CreateManagedScriptableRenderContext(uint64_t nativeHandle); MonoObject* CreateManagedCameraRenderRequestContext( uint64_t nativeHandle); + MonoObject* CreateManagedScriptableRenderPipelinePlanningContext( + uint64_t nativeHandle); MonoObject* GetManagedObject(uint32_t gcHandle) const; MonoMethod* ResolveManagedMethod( MonoClass* monoClass, @@ -306,11 +308,14 @@ private: MonoClass* m_scriptableRenderPipelineClass = nullptr; MonoClass* m_scriptableRenderContextClass = nullptr; MonoClass* m_cameraRenderRequestContextClass = nullptr; + MonoClass* m_scriptableRenderPipelinePlanningContextClass = nullptr; MonoClass* m_serializeFieldAttributeClass = nullptr; MonoMethod* m_gameObjectConstructor = nullptr; MonoMethod* m_scriptableRenderContextConstructor = nullptr; MonoMethod* m_cameraRenderRequestContextConstructor = nullptr; + MonoMethod* m_scriptableRenderPipelinePlanningContextConstructor = + nullptr; MonoClassField* m_managedGameObjectUUIDField = nullptr; MonoClassField* m_gameObjectUUIDField = nullptr; MonoClassField* m_scriptComponentUUIDField = nullptr; diff --git a/engine/src/Rendering/Execution/CameraFramePlan.cpp b/engine/src/Rendering/Execution/CameraFramePlan.cpp index 665cb56f..11231797 100644 --- a/engine/src/Rendering/Execution/CameraFramePlan.cpp +++ b/engine/src/Rendering/Execution/CameraFramePlan.cpp @@ -149,7 +149,8 @@ bool CameraFramePlan::IsFullscreenStageRequested(CameraFrameStage stage) const { bool CameraFramePlan::RequestFullscreenStage( CameraFrameStage stage, CameraFrameColorSource source, - bool usesGraphManagedOutputColor) { + bool usesGraphManagedOutputColor, + bool explicitlyConfigured) { CameraFrameFullscreenStagePlan* const fullscreenStagePlan = GetMutableFullscreenStagePlan(*this, stage); FullscreenPassRenderRequest* const fullscreenRequest = @@ -164,6 +165,9 @@ bool CameraFramePlan::RequestFullscreenStage( stage == CameraFrameStage::PostProcess ? usesGraphManagedOutputColor : false; + fullscreenStagePlan->explicitlyConfigured = + fullscreenStagePlan->explicitlyConfigured || + explicitlyConfigured; if (source != CameraFrameColorSource::ExplicitSurface) { fullscreenRequest->sourceSurface = {}; @@ -184,15 +188,21 @@ bool CameraFramePlan::RequestFullscreenStage( return true; } -void CameraFramePlan::ClearFullscreenStage(CameraFrameStage stage) { +void CameraFramePlan::ClearFullscreenStage( + CameraFrameStage stage, + bool explicitlyConfigured) { if (stage == CameraFrameStage::PostProcess) { ClearOwnedPostProcessSequence(); postProcess = {}; colorChain.postProcess = {}; + colorChain.postProcess.explicitlyConfigured = + explicitlyConfigured; } else if (stage == CameraFrameStage::FinalOutput) { ClearOwnedFinalOutputSequence(); finalOutput = {}; colorChain.finalOutput = {}; + colorChain.finalOutput.explicitlyConfigured = + explicitlyConfigured; } else { return; } @@ -200,6 +210,17 @@ void CameraFramePlan::ClearFullscreenStage(CameraFrameStage stage) { RefreshGraphManagedSceneSurfaceState(); } +bool CameraFramePlan::HasExplicitFullscreenStageConfiguration( + CameraFrameStage stage) const { + if (const CameraFrameFullscreenStagePlan* fullscreenStagePlan = + GetFullscreenStagePlan(stage); + fullscreenStagePlan != nullptr) { + return fullscreenStagePlan->explicitlyConfigured; + } + + return false; +} + bool CameraFramePlan::IsPostProcessStageValid() const { if (!IsFullscreenStageRequested(CameraFrameStage::PostProcess)) { return true; diff --git a/engine/src/Rendering/Pipelines/ManagedScriptableRenderPipelineAsset.cpp b/engine/src/Rendering/Pipelines/ManagedScriptableRenderPipelineAsset.cpp index aa029b17..ffbb8c2d 100644 --- a/engine/src/Rendering/Pipelines/ManagedScriptableRenderPipelineAsset.cpp +++ b/engine/src/Rendering/Pipelines/ManagedScriptableRenderPipelineAsset.cpp @@ -100,6 +100,17 @@ void ManagedScriptableRenderPipelineAsset::ConfigureCameraRenderRequest( } } +void ManagedScriptableRenderPipelineAsset::ConfigureCameraFramePlan( + CameraFramePlan& plan) const { + RenderPipelineAsset::ConfigureCameraFramePlan(plan); + + if (const std::shared_ptr runtime = + ResolveManagedAssetRuntime(); + runtime != nullptr) { + runtime->ConfigureCameraFramePlan(plan); + } +} + FinalColorSettings ManagedScriptableRenderPipelineAsset::GetDefaultFinalColorSettings() const { if (const std::shared_ptr runtime = ResolveManagedAssetRuntime(); diff --git a/engine/src/Rendering/Planning/CameraFramePlanBuilder.cpp b/engine/src/Rendering/Planning/CameraFramePlanBuilder.cpp index 2901f861..538941e8 100644 --- a/engine/src/Rendering/Planning/CameraFramePlanBuilder.cpp +++ b/engine/src/Rendering/Planning/CameraFramePlanBuilder.cpp @@ -18,6 +18,12 @@ bool UsesExplicitFullscreenSource( void ConfigureFullscreenStagesFromPipeline( CameraFramePlan& plan, const RenderPipeline* pipeline) { + const bool postProcessExplicitlyConfigured = + plan.HasExplicitFullscreenStageConfiguration( + CameraFrameStage::PostProcess); + const bool finalOutputExplicitlyConfigured = + plan.HasExplicitFullscreenStageConfiguration( + CameraFrameStage::FinalOutput); const bool supportsPostProcess = pipeline != nullptr && pipeline->SupportsStageRenderGraph( @@ -33,7 +39,8 @@ void ConfigureFullscreenStagesFromPipeline( plan.finalOutput.IsRequested() || supportsFinalOutput; - if (hasPostProcess && + if (!postProcessExplicitlyConfigured && + hasPostProcess && !UsesExplicitFullscreenSource( plan.postProcess, plan.ResolveStageColorSource( @@ -50,7 +57,8 @@ void ConfigureFullscreenStagesFromPipeline( plan.postProcess, plan.ResolveStageColorSource( CameraFrameStage::PostProcess)); - if (hasFinalOutput && + if (!finalOutputExplicitlyConfigured && + hasFinalOutput && !UsesExplicitFullscreenSource( plan.finalOutput, plan.ResolveStageColorSource( diff --git a/engine/src/Scripting/Mono/MonoScriptRuntime.cpp b/engine/src/Scripting/Mono/MonoScriptRuntime.cpp index 4376ea75..cd6f6fd9 100644 --- a/engine/src/Scripting/Mono/MonoScriptRuntime.cpp +++ b/engine/src/Scripting/Mono/MonoScriptRuntime.cpp @@ -111,6 +111,11 @@ struct ManagedCameraRenderRequestContextState { size_t renderedRequestCount = 0u; }; +struct ManagedScriptableRenderPipelinePlanningContextState { + uint64_t handle = 0; + Rendering::CameraFramePlan* plan = nullptr; +}; + uint64_t& GetManagedScriptableRenderContextNextHandle() { static uint64_t nextHandle = 1; return nextHandle; @@ -274,6 +279,92 @@ void UnregisterManagedCameraRenderRequestContextState( GetManagedCameraRenderRequestContextRegistry().erase(handle); } +uint64_t& GetManagedScriptableRenderPipelinePlanningContextNextHandle() { + static uint64_t nextHandle = 1; + return nextHandle; +} + +std::unordered_map& +GetManagedScriptableRenderPipelinePlanningContextRegistry() { + static std::unordered_map< + uint64_t, + ManagedScriptableRenderPipelinePlanningContextState*> registry; + return registry; +} + +ManagedScriptableRenderPipelinePlanningContextState* +FindManagedScriptableRenderPipelinePlanningContextState( + uint64_t handle) { + const auto it = + GetManagedScriptableRenderPipelinePlanningContextRegistry().find( + handle); + return it != + GetManagedScriptableRenderPipelinePlanningContextRegistry().end() + ? it->second + : nullptr; +} + +uint64_t RegisterManagedScriptableRenderPipelinePlanningContextState( + ManagedScriptableRenderPipelinePlanningContextState& state) { + uint64_t handle = + GetManagedScriptableRenderPipelinePlanningContextNextHandle()++; + if (handle == 0) { + handle = + GetManagedScriptableRenderPipelinePlanningContextNextHandle()++; + } + + state.handle = handle; + GetManagedScriptableRenderPipelinePlanningContextRegistry()[handle] = + &state; + return handle; +} + +void UnregisterManagedScriptableRenderPipelinePlanningContextState( + uint64_t handle) { + if (handle == 0) { + return; + } + + GetManagedScriptableRenderPipelinePlanningContextRegistry().erase(handle); +} + +bool TryResolveManagedCameraFrameStage( + int32_t value, + Rendering::CameraFrameStage& outStage) { + switch (value) { + case static_cast(Rendering::CameraFrameStage::PreScenePasses): + outStage = Rendering::CameraFrameStage::PreScenePasses; + return true; + case static_cast(Rendering::CameraFrameStage::ShadowCaster): + outStage = Rendering::CameraFrameStage::ShadowCaster; + return true; + case static_cast(Rendering::CameraFrameStage::DepthOnly): + outStage = Rendering::CameraFrameStage::DepthOnly; + return true; + case static_cast(Rendering::CameraFrameStage::MainScene): + outStage = Rendering::CameraFrameStage::MainScene; + return true; + case static_cast(Rendering::CameraFrameStage::PostProcess): + outStage = Rendering::CameraFrameStage::PostProcess; + return true; + case static_cast(Rendering::CameraFrameStage::FinalOutput): + outStage = Rendering::CameraFrameStage::FinalOutput; + return true; + case static_cast(Rendering::CameraFrameStage::ObjectId): + outStage = Rendering::CameraFrameStage::ObjectId; + return true; + case static_cast(Rendering::CameraFrameStage::PostScenePasses): + outStage = Rendering::CameraFrameStage::PostScenePasses; + return true; + case static_cast(Rendering::CameraFrameStage::OverlayPasses): + outStage = Rendering::CameraFrameStage::OverlayPasses; + return true; + default: + outStage = Rendering::CameraFrameStage::MainScene; + return false; + } +} + void CleanupMonoRootDomainAtExit() { MonoRootState& rootState = GetMonoRootState(); if (!rootState.rootDomain) { @@ -715,6 +806,8 @@ public: size_t renderedRequestCount, const Rendering::DirectionalShadowPlanningSettings& directionalShadowSettings) const override; + void ConfigureCameraFramePlan( + Rendering::CameraFramePlan& plan) const override; bool TryGetDefaultFinalColorSettings( Rendering::FinalColorSettings& settings) const override; @@ -742,6 +835,8 @@ private: MonoMethod* ResolveCreatePipelineMethod(MonoObject* assetObject) const; MonoMethod* ResolveConfigureCameraRenderRequestMethod( MonoObject* assetObject) const; + MonoMethod* ResolveConfigureCameraFramePlanMethod( + MonoObject* assetObject) const; MonoMethod* ResolveGetDefaultFinalColorSettingsMethod( MonoObject* assetObject) const; MonoMethod* ResolveGetPipelineRendererAssetKeyMethod( @@ -756,6 +851,7 @@ private: mutable MonoMethod* m_disposePipelineMethod = nullptr; mutable MonoMethod* m_createPipelineMethod = nullptr; mutable MonoMethod* m_configureCameraRenderRequestMethod = nullptr; + mutable MonoMethod* m_configureCameraFramePlanMethod = nullptr; mutable MonoMethod* m_getDefaultFinalColorSettingsMethod = nullptr; mutable MonoMethod* m_getPipelineRendererAssetKeyMethod = nullptr; mutable MonoMethod* m_releaseRuntimeResourcesMethod = nullptr; @@ -1051,6 +1147,44 @@ void MonoManagedRenderPipelineAssetRuntime::ConfigureCameraRenderRequest( requestContextHandle); } +void MonoManagedRenderPipelineAssetRuntime::ConfigureCameraFramePlan( + Rendering::CameraFramePlan& plan) const { + if (!EnsureManagedAsset()) { + return; + } + + MonoObject* const assetObject = GetManagedAssetObject(); + MonoMethod* const method = + ResolveConfigureCameraFramePlanMethod(assetObject); + if (assetObject == nullptr || method == nullptr) { + return; + } + + ManagedScriptableRenderPipelinePlanningContextState + planningContextState = {}; + planningContextState.plan = &plan; + const uint64_t planningContextHandle = + RegisterManagedScriptableRenderPipelinePlanningContextState( + planningContextState); + MonoObject* const planningContextObject = + m_runtime->CreateManagedScriptableRenderPipelinePlanningContext( + planningContextHandle); + if (planningContextObject == nullptr) { + UnregisterManagedScriptableRenderPipelinePlanningContextState( + planningContextHandle); + return; + } + + void* args[1] = { planningContextObject }; + m_runtime->InvokeManagedMethod( + assetObject, + method, + args, + nullptr); + UnregisterManagedScriptableRenderPipelinePlanningContextState( + planningContextHandle); +} + bool MonoManagedRenderPipelineAssetRuntime::TryGetDefaultFinalColorSettings( Rendering::FinalColorSettings& settings) const { settings = {}; @@ -1268,6 +1402,7 @@ void MonoManagedRenderPipelineAssetRuntime::ReleaseManagedAsset() const { m_releaseRuntimeResourcesMethod = nullptr; m_createPipelineMethod = nullptr; m_configureCameraRenderRequestMethod = nullptr; + m_configureCameraFramePlanMethod = nullptr; m_getDefaultFinalColorSettingsMethod = nullptr; m_getPipelineRendererAssetKeyMethod = nullptr; m_pipelineRendererAsset.reset(); @@ -1345,6 +1480,20 @@ MonoManagedRenderPipelineAssetRuntime::ResolveConfigureCameraRenderRequestMethod return m_configureCameraRenderRequestMethod; } +MonoMethod* +MonoManagedRenderPipelineAssetRuntime::ResolveConfigureCameraFramePlanMethod( + MonoObject* assetObject) const { + if (m_configureCameraFramePlanMethod == nullptr) { + m_configureCameraFramePlanMethod = + m_runtime->ResolveManagedMethod( + assetObject, + "ConfigureCameraFramePlanInstance", + 1); + } + + return m_configureCameraFramePlanMethod; +} + MonoMethod* MonoManagedRenderPipelineAssetRuntime::ResolveGetDefaultFinalColorSettingsMethod( MonoObject* assetObject) const { @@ -3803,6 +3952,115 @@ void InternalCall_Rendering_CameraRenderRequestContext_ClearDirectionalShadow( state->request->directionalShadow = {}; } +mono_bool +InternalCall_Rendering_ScriptableRenderPipelinePlanningContext_IsStageRequested( + uint64_t nativeHandle, + int32_t stage) { + const ManagedScriptableRenderPipelinePlanningContextState* const state = + FindManagedScriptableRenderPipelinePlanningContextState(nativeHandle); + if (state == nullptr || state->plan == nullptr) { + return 0; + } + + Rendering::CameraFrameStage resolvedStage = + Rendering::CameraFrameStage::MainScene; + if (!TryResolveManagedCameraFrameStage(stage, resolvedStage)) { + return 0; + } + + return state->plan->HasFrameStage(resolvedStage) ? 1 : 0; +} + +int32_t +InternalCall_Rendering_ScriptableRenderPipelinePlanningContext_GetStageColorSource( + uint64_t nativeHandle, + int32_t stage) { + const ManagedScriptableRenderPipelinePlanningContextState* const state = + FindManagedScriptableRenderPipelinePlanningContextState(nativeHandle); + if (state == nullptr || state->plan == nullptr) { + return static_cast( + Rendering::CameraFrameColorSource::ExplicitSurface); + } + + Rendering::CameraFrameStage resolvedStage = + Rendering::CameraFrameStage::MainScene; + if (!TryResolveManagedCameraFrameStage(stage, resolvedStage)) { + return static_cast( + Rendering::CameraFrameColorSource::ExplicitSurface); + } + + return static_cast( + state->plan->ResolveStageColorSource(resolvedStage)); +} + +mono_bool +InternalCall_Rendering_ScriptableRenderPipelinePlanningContext_GetStageUsesGraphManagedOutputColor( + uint64_t nativeHandle, + int32_t stage) { + const ManagedScriptableRenderPipelinePlanningContextState* const state = + FindManagedScriptableRenderPipelinePlanningContextState(nativeHandle); + if (state == nullptr || state->plan == nullptr) { + return 0; + } + + Rendering::CameraFrameStage resolvedStage = + Rendering::CameraFrameStage::MainScene; + if (!TryResolveManagedCameraFrameStage(stage, resolvedStage)) { + return 0; + } + + return state->plan->UsesGraphManagedOutputColor(resolvedStage) + ? 1 + : 0; +} + +mono_bool +InternalCall_Rendering_ScriptableRenderPipelinePlanningContext_RequestFullscreenStage( + uint64_t nativeHandle, + int32_t stage, + int32_t source, + mono_bool usesGraphManagedOutputColor) { + ManagedScriptableRenderPipelinePlanningContextState* const state = + FindManagedScriptableRenderPipelinePlanningContextState(nativeHandle); + if (state == nullptr || state->plan == nullptr) { + return 0; + } + + Rendering::CameraFrameStage resolvedStage = + Rendering::CameraFrameStage::MainScene; + if (!TryResolveManagedCameraFrameStage(stage, resolvedStage)) { + return 0; + } + + const Rendering::CameraFrameColorSource resolvedSource = + static_cast(source); + return state->plan->RequestFullscreenStage( + resolvedStage, + resolvedSource, + usesGraphManagedOutputColor != 0, + true) + ? 1 + : 0; +} + +void InternalCall_Rendering_ScriptableRenderPipelinePlanningContext_ClearFullscreenStage( + uint64_t nativeHandle, + int32_t stage) { + ManagedScriptableRenderPipelinePlanningContextState* const state = + FindManagedScriptableRenderPipelinePlanningContextState(nativeHandle); + if (state == nullptr || state->plan == nullptr) { + return; + } + + Rendering::CameraFrameStage resolvedStage = + Rendering::CameraFrameStage::MainScene; + if (!TryResolveManagedCameraFrameStage(stage, resolvedStage)) { + return; + } + + state->plan->ClearFullscreenStage(resolvedStage, true); +} + void RegisterInternalCalls() { if (GetInternalCallRegistrationState()) { return; @@ -3986,6 +4244,11 @@ void RegisterInternalCalls() { mono_add_internal_call("XCEngine.InternalCalls::Rendering_ScriptableRenderContext_RecordScenePhase", reinterpret_cast(&InternalCall_Rendering_ScriptableRenderContext_RecordScenePhase)); mono_add_internal_call("XCEngine.InternalCalls::Rendering_ScriptableRenderContext_RecordSceneInjectionPoint", reinterpret_cast(&InternalCall_Rendering_ScriptableRenderContext_RecordSceneInjectionPoint)); mono_add_internal_call("XCEngine.InternalCalls::Rendering_ScriptableRenderContext_RecordFullscreenPass", reinterpret_cast(&InternalCall_Rendering_ScriptableRenderContext_RecordFullscreenPass)); + mono_add_internal_call("XCEngine.InternalCalls::Rendering_ScriptableRenderPipelinePlanningContext_IsStageRequested", reinterpret_cast(&InternalCall_Rendering_ScriptableRenderPipelinePlanningContext_IsStageRequested)); + mono_add_internal_call("XCEngine.InternalCalls::Rendering_ScriptableRenderPipelinePlanningContext_GetStageColorSource", reinterpret_cast(&InternalCall_Rendering_ScriptableRenderPipelinePlanningContext_GetStageColorSource)); + mono_add_internal_call("XCEngine.InternalCalls::Rendering_ScriptableRenderPipelinePlanningContext_GetStageUsesGraphManagedOutputColor", reinterpret_cast(&InternalCall_Rendering_ScriptableRenderPipelinePlanningContext_GetStageUsesGraphManagedOutputColor)); + mono_add_internal_call("XCEngine.InternalCalls::Rendering_ScriptableRenderPipelinePlanningContext_RequestFullscreenStage", reinterpret_cast(&InternalCall_Rendering_ScriptableRenderPipelinePlanningContext_RequestFullscreenStage)); + mono_add_internal_call("XCEngine.InternalCalls::Rendering_ScriptableRenderPipelinePlanningContext_ClearFullscreenStage", reinterpret_cast(&InternalCall_Rendering_ScriptableRenderPipelinePlanningContext_ClearFullscreenStage)); mono_add_internal_call("XCEngine.InternalCalls::Rendering_CameraRenderRequestContext_GetRenderedBaseCameraCount", reinterpret_cast(&InternalCall_Rendering_CameraRenderRequestContext_GetRenderedBaseCameraCount)); mono_add_internal_call("XCEngine.InternalCalls::Rendering_CameraRenderRequestContext_GetRenderedRequestCount", reinterpret_cast(&InternalCall_Rendering_CameraRenderRequestContext_GetRenderedRequestCount)); mono_add_internal_call("XCEngine.InternalCalls::Rendering_CameraRenderRequestContext_GetHasDirectionalShadow", reinterpret_cast(&InternalCall_Rendering_CameraRenderRequestContext_GetHasDirectionalShadow)); @@ -4049,6 +4312,8 @@ void MonoScriptRuntime::Shutdown() { GetManagedScriptableRenderContextNextHandle() = 1; GetManagedCameraRenderRequestContextRegistry().clear(); GetManagedCameraRenderRequestContextNextHandle() = 1; + GetManagedScriptableRenderPipelinePlanningContextRegistry().clear(); + GetManagedScriptableRenderPipelinePlanningContextNextHandle() = 1; ClearManagedInstances(); ClearExternalManagedObjects(); ClearClassCache(); @@ -4066,10 +4331,12 @@ void MonoScriptRuntime::Shutdown() { m_scriptableRenderPipelineClass = nullptr; m_scriptableRenderContextClass = nullptr; m_cameraRenderRequestContextClass = nullptr; + m_scriptableRenderPipelinePlanningContextClass = nullptr; m_serializeFieldAttributeClass = nullptr; m_gameObjectConstructor = nullptr; m_scriptableRenderContextConstructor = nullptr; m_cameraRenderRequestContextConstructor = nullptr; + m_scriptableRenderPipelinePlanningContextConstructor = nullptr; m_managedGameObjectUUIDField = nullptr; m_gameObjectUUIDField = nullptr; m_scriptComponentUUIDField = nullptr; @@ -4863,6 +5130,27 @@ bool MonoScriptRuntime::DiscoverScriptClasses() { return false; } + m_scriptableRenderPipelinePlanningContextClass = mono_class_from_name( + m_coreImage, + kManagedRenderingNamespace, + "ScriptableRenderPipelinePlanningContext"); + if (!m_scriptableRenderPipelinePlanningContextClass) { + SetError( + "Failed to locate the managed ScriptableRenderPipelinePlanningContext type."); + return false; + } + + m_scriptableRenderPipelinePlanningContextConstructor = + mono_class_get_method_from_name( + m_scriptableRenderPipelinePlanningContextClass, + ".ctor", + 1); + if (!m_scriptableRenderPipelinePlanningContextConstructor) { + SetError( + "Failed to locate the managed ScriptableRenderPipelinePlanningContext constructor."); + return false; + } + m_serializeFieldAttributeClass = mono_class_from_name( m_coreImage, m_settings.baseNamespace.c_str(), @@ -5458,6 +5746,46 @@ MonoScriptRuntime::CreateManagedCameraRenderRequestContext( return contextObject; } +MonoObject* +MonoScriptRuntime::CreateManagedScriptableRenderPipelinePlanningContext( + uint64_t nativeHandle) { + if (!m_initialized || + nativeHandle == 0 || + m_scriptableRenderPipelinePlanningContextClass == nullptr || + m_scriptableRenderPipelinePlanningContextConstructor == nullptr) { + return nullptr; + } + + SetCurrentDomain(); + + MonoObject* const contextObject = + mono_object_new( + m_appDomain, + m_scriptableRenderPipelinePlanningContextClass); + if (contextObject == nullptr) { + SetError( + "Mono failed to allocate a managed ScriptableRenderPipelinePlanningContext."); + return nullptr; + } + + void* args[1]; + uint64_t nativeHandleArgument = nativeHandle; + args[0] = &nativeHandleArgument; + + MonoObject* exception = nullptr; + mono_runtime_invoke( + m_scriptableRenderPipelinePlanningContextConstructor, + contextObject, + args, + &exception); + if (exception != nullptr) { + RecordException(exception); + return nullptr; + } + + return contextObject; +} + MonoScriptRuntime::InstanceData* MonoScriptRuntime::FindInstance(const ScriptRuntimeContext& context) { const auto it = m_instances.find(InstanceKey{context.gameObjectUUID, context.scriptComponentUUID}); return it != m_instances.end() ? &it->second : nullptr; diff --git a/managed/CMakeLists.txt b/managed/CMakeLists.txt index 7bb1ec58..913fa20c 100644 --- a/managed/CMakeLists.txt +++ b/managed/CMakeLists.txt @@ -182,6 +182,7 @@ set(XCENGINE_SCRIPT_CORE_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderContext.cs ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipeline.cs ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipelineAsset.cs + ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipelinePlanningContext.cs ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Rendering/Core/CameraRenderRequestContext.cs ) diff --git a/managed/GameScripts/RenderPipelineApiProbe.cs b/managed/GameScripts/RenderPipelineApiProbe.cs index e1bebd5b..757f1772 100644 --- a/managed/GameScripts/RenderPipelineApiProbe.cs +++ b/managed/GameScripts/RenderPipelineApiProbe.cs @@ -899,6 +899,41 @@ namespace Gameplay } } + internal sealed class ManagedFeaturePlannedPostProcessRendererFeature + : ScriptableRendererFeature + { + public override void ConfigureCameraFramePlan( + ScriptableRenderPipelinePlanningContext context) + { + if (context == null || + context.IsStageRequested(CameraFrameStage.PostProcess)) + { + return; + } + + context.RequestFullscreenStage( + CameraFrameStage.PostProcess, + CameraFrameColorSource.MainSceneColor); + } + } + + internal sealed class ManagedFeaturePlannedPostProcessRendererData + : ProbeRendererData + { + protected override ScriptableRenderer CreateProbeRenderer() + { + return new ProbeSceneRenderer(); + } + + protected override ScriptableRendererFeature[] CreateRendererFeatures() + { + return new ScriptableRendererFeature[] + { + new ManagedFeaturePlannedPostProcessRendererFeature() + }; + } + } + internal sealed class ManagedRenderContextCameraDataProbeRendererData : ProbeRendererData { @@ -1206,6 +1241,52 @@ namespace Gameplay } } + public sealed class ManagedFeaturePlannedPostProcessProbeAsset + : UniversalRenderPipelineAsset + { + public ManagedFeaturePlannedPostProcessProbeAsset() + { + rendererDataList = + new ScriptableRendererData[] + { + new ManagedFeaturePlannedPostProcessRendererData() + }; + } + } + + public sealed class ManagedClearedPostProcessRenderPipelineProbeAsset + : UniversalRenderPipelineAsset + { + public ManagedClearedPostProcessRenderPipelineProbeAsset() + { + rendererDataList = + new ScriptableRendererData[] + { + new ManagedPlannedFullscreenRenderPipelineProbeRendererData() + }; + } + + protected override void ConfigureCameraFramePlan( + ScriptableRenderPipelinePlanningContext context) + { + if (context == null) + { + return; + } + + context.ClearFullscreenStage( + CameraFrameStage.PostProcess); + context.RequestFullscreenStage( + CameraFrameStage.FinalOutput, + CameraFrameColorSource.MainSceneColor); + } + + protected override FinalColorSettings GetDefaultFinalColorSettings() + { + return ProbeFinalColorSettingsFactory.Create(); + } + } + internal sealed class ManagedCameraRequestConfiguredRendererData : UniversalRendererData { diff --git a/managed/XCEngine.ScriptCore/InternalCalls.cs b/managed/XCEngine.ScriptCore/InternalCalls.cs index cd7db4be..0ed74740 100644 --- a/managed/XCEngine.ScriptCore/InternalCalls.cs +++ b/managed/XCEngine.ScriptCore/InternalCalls.cs @@ -389,6 +389,38 @@ namespace XCEngine internal static extern int Rendering_ScriptableRenderContext_GetStage( ulong nativeHandle); + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern bool + Rendering_ScriptableRenderPipelinePlanningContext_IsStageRequested( + ulong nativeHandle, + int stage); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern int + Rendering_ScriptableRenderPipelinePlanningContext_GetStageColorSource( + ulong nativeHandle, + int stage); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern bool + Rendering_ScriptableRenderPipelinePlanningContext_GetStageUsesGraphManagedOutputColor( + ulong nativeHandle, + int stage); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern bool + Rendering_ScriptableRenderPipelinePlanningContext_RequestFullscreenStage( + ulong nativeHandle, + int stage, + int source, + bool usesGraphManagedOutputColor); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void + Rendering_ScriptableRenderPipelinePlanningContext_ClearFullscreenStage( + ulong nativeHandle, + int stage); + [MethodImpl(MethodImplOptions.InternalCall)] internal static extern int Rendering_ScriptableRenderContext_GetStageColorSource( diff --git a/managed/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipelineAsset.cs b/managed/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipelineAsset.cs index 4698d900..0a695479 100644 --- a/managed/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipelineAsset.cs +++ b/managed/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipelineAsset.cs @@ -13,6 +13,12 @@ namespace XCEngine.Rendering ReleaseRuntimeResources(); } + internal void ConfigureCameraFramePlanInstance( + ScriptableRenderPipelinePlanningContext context) + { + ConfigureCameraFramePlan(context); + } + protected virtual ScriptableRenderPipeline CreatePipeline() { return null; @@ -23,6 +29,11 @@ namespace XCEngine.Rendering { } + protected virtual void ConfigureCameraFramePlan( + ScriptableRenderPipelinePlanningContext context) + { + } + protected virtual FinalColorSettings GetDefaultFinalColorSettings() { return FinalColorSettings.CreateDefault(); diff --git a/managed/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipelinePlanningContext.cs b/managed/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipelinePlanningContext.cs new file mode 100644 index 00000000..e2955b8e --- /dev/null +++ b/managed/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipelinePlanningContext.cs @@ -0,0 +1,64 @@ +using XCEngine; + +namespace XCEngine.Rendering +{ + public sealed class ScriptableRenderPipelinePlanningContext + { + private readonly ulong m_nativeHandle; + + internal ScriptableRenderPipelinePlanningContext( + ulong nativeHandle) + { + m_nativeHandle = nativeHandle; + } + + public bool IsStageRequested( + CameraFrameStage stage) + { + return InternalCalls + .Rendering_ScriptableRenderPipelinePlanningContext_IsStageRequested( + m_nativeHandle, + (int)stage); + } + + public CameraFrameColorSource GetStageColorSource( + CameraFrameStage stage) + { + return (CameraFrameColorSource)InternalCalls + .Rendering_ScriptableRenderPipelinePlanningContext_GetStageColorSource( + m_nativeHandle, + (int)stage); + } + + public bool UsesGraphManagedOutputColor( + CameraFrameStage stage) + { + return InternalCalls + .Rendering_ScriptableRenderPipelinePlanningContext_GetStageUsesGraphManagedOutputColor( + m_nativeHandle, + (int)stage); + } + + public bool RequestFullscreenStage( + CameraFrameStage stage, + CameraFrameColorSource source, + bool usesGraphManagedOutputColor = false) + { + return InternalCalls + .Rendering_ScriptableRenderPipelinePlanningContext_RequestFullscreenStage( + m_nativeHandle, + (int)stage, + (int)source, + usesGraphManagedOutputColor); + } + + public void ClearFullscreenStage( + CameraFrameStage stage) + { + InternalCalls + .Rendering_ScriptableRenderPipelinePlanningContext_ClearFullscreenStage( + m_nativeHandle, + (int)stage); + } + } +} diff --git a/managed/XCEngine.ScriptCore/Rendering/Universal/ScriptableRendererData.cs b/managed/XCEngine.ScriptCore/Rendering/Universal/ScriptableRendererData.cs index 751bbfd5..2fa8dfea 100644 --- a/managed/XCEngine.ScriptCore/Rendering/Universal/ScriptableRendererData.cs +++ b/managed/XCEngine.ScriptCore/Rendering/Universal/ScriptableRendererData.cs @@ -89,6 +89,28 @@ namespace XCEngine.Rendering.Universal } } + internal void ConfigureCameraFramePlanInstance( + ScriptableRenderPipelinePlanningContext context) + { + ConfigureCameraFramePlan(context); + + ScriptableRendererFeature[] rendererFeatures = + GetRendererFeatures(); + for (int i = 0; i < rendererFeatures.Length; ++i) + { + ScriptableRendererFeature rendererFeature = + rendererFeatures[i]; + if (rendererFeature == null || + !rendererFeature.isActive) + { + continue; + } + + rendererFeature.ConfigureCameraFramePlan( + context); + } + } + protected virtual ScriptableRenderer CreateRenderer() { return null; @@ -99,6 +121,11 @@ namespace XCEngine.Rendering.Universal { } + protected virtual void ConfigureCameraFramePlan( + ScriptableRenderPipelinePlanningContext context) + { + } + protected virtual string GetPipelineRendererAssetKey() { return string.Empty; diff --git a/managed/XCEngine.ScriptCore/Rendering/Universal/ScriptableRendererFeature.cs b/managed/XCEngine.ScriptCore/Rendering/Universal/ScriptableRendererFeature.cs index a9c58bfd..7ef8e86d 100644 --- a/managed/XCEngine.ScriptCore/Rendering/Universal/ScriptableRendererFeature.cs +++ b/managed/XCEngine.ScriptCore/Rendering/Universal/ScriptableRendererFeature.cs @@ -33,6 +33,11 @@ namespace XCEngine.Rendering.Universal { } + public virtual void ConfigureCameraFramePlan( + ScriptableRenderPipelinePlanningContext context) + { + } + public virtual void AddRenderPasses( ScriptableRenderer renderer, RenderingData renderingData) diff --git a/managed/XCEngine.ScriptCore/Rendering/Universal/UniversalRenderPipelineAsset.cs b/managed/XCEngine.ScriptCore/Rendering/Universal/UniversalRenderPipelineAsset.cs index 29797908..6d1867fe 100644 --- a/managed/XCEngine.ScriptCore/Rendering/Universal/UniversalRenderPipelineAsset.cs +++ b/managed/XCEngine.ScriptCore/Rendering/Universal/UniversalRenderPipelineAsset.cs @@ -32,6 +32,18 @@ namespace XCEngine.Rendering.Universal } } + protected override void ConfigureCameraFramePlan( + ScriptableRenderPipelinePlanningContext context) + { + ScriptableRendererData resolvedRendererData = + GetDefaultRendererData(); + if (resolvedRendererData != null) + { + resolvedRendererData.ConfigureCameraFramePlanInstance( + context); + } + } + protected override string GetPipelineRendererAssetKey() { ScriptableRendererData resolvedRendererData = diff --git a/tests/scripting/test_mono_script_runtime.cpp b/tests/scripting/test_mono_script_runtime.cpp index 227747ac..6a424766 100644 --- a/tests/scripting/test_mono_script_runtime.cpp +++ b/tests/scripting/test_mono_script_runtime.cpp @@ -1312,9 +1312,9 @@ TEST_F( EXPECT_FALSE(hasUniversalContextRecordShaderVectorFullscreenPassExtension); EXPECT_FALSE(hasUniversalRequestContextHasDirectionalShadowExtension); EXPECT_FALSE(hasUniversalRequestContextClearDirectionalShadowExtension); - EXPECT_FALSE(hasPublicPipelineAssetConfigureCameraFramePlan); - EXPECT_FALSE(hasPlanningContextType); - EXPECT_FALSE(hasRendererFeatureConfigureCameraFramePlan); + EXPECT_TRUE(hasPublicPipelineAssetConfigureCameraFramePlan); + EXPECT_TRUE(hasPlanningContextType); + EXPECT_TRUE(hasRendererFeatureConfigureCameraFramePlan); EXPECT_FALSE(hasRendererRecordingContextType); EXPECT_FALSE(hasRendererCameraRequestContextType); EXPECT_FALSE(hasRendererBackedRenderPipelineAssetType); @@ -3334,6 +3334,142 @@ TEST_F( EXPECT_TRUE(plan.IsFinalOutputStageValid()); } +TEST_F( + MonoScriptRuntimeTest, + ManagedRendererFeatureConfiguresCameraFramePlanThroughPlanningContext) { + Scene* runtimeScene = + CreateScene("ManagedFeatureFramePlanningScene"); + GameObject* cameraObject = runtimeScene->CreateGameObject("Camera"); + auto* camera = cameraObject->AddComponent(); + ASSERT_NE(camera, nullptr); + camera->SetPrimary(true); + + TestRenderDevice device; + TestRenderCommandList commandList; + TestRenderCommandQueue commandQueue; + TestRenderResourceView colorView( + XCEngine::RHI::ResourceViewType::RenderTarget, + XCEngine::RHI::ResourceViewDimension::Texture2D, + XCEngine::RHI::Format::R8G8B8A8_UNorm); + TestRenderResourceView depthView( + XCEngine::RHI::ResourceViewType::DepthStencil, + XCEngine::RHI::ResourceViewDimension::Texture2D, + XCEngine::RHI::Format::D32_Float); + + const XCEngine::Rendering::RenderContext context = + CreateRenderContext( + device, + commandList, + commandQueue); + XCEngine::Rendering::RenderSurface surface(64u, 64u); + surface.SetColorAttachment(&colorView); + surface.SetDepthAttachment(&depthView); + + const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor + descriptor = { + "GameScripts", + "Gameplay", + "ManagedFeaturePlannedPostProcessProbeAsset" + }; + auto asset = + std::make_shared< + XCEngine::Rendering::Pipelines::ManagedScriptableRenderPipelineAsset>( + descriptor); + + XCEngine::Rendering::CameraRenderRequest request = {}; + request.scene = runtimeScene; + request.camera = camera; + request.context = context; + request.surface = surface; + + XCEngine::Rendering::RenderPipelineHost host(asset); + const std::vector plans = + host.BuildFramePlans({ request }); + + ASSERT_EQ(plans.size(), 1u); + const XCEngine::Rendering::CameraFramePlan& plan = plans[0]; + + EXPECT_TRUE( + plan.IsFullscreenStageRequested( + XCEngine::Rendering::CameraFrameStage::PostProcess)); + EXPECT_EQ( + plan.ResolveStageColorSource( + XCEngine::Rendering::CameraFrameStage::PostProcess), + XCEngine::Rendering::CameraFrameColorSource::MainSceneColor); + EXPECT_FALSE( + plan.UsesGraphManagedOutputColor( + XCEngine::Rendering::CameraFrameStage::PostProcess)); + EXPECT_TRUE(plan.IsPostProcessStageValid()); +} + +TEST_F( + MonoScriptRuntimeTest, + ManagedRenderPipelineAssetPlanningContextCanOverridePipelineStageHeuristic) { + Scene* runtimeScene = + CreateScene("ManagedAssetFramePlanningOverrideScene"); + GameObject* cameraObject = runtimeScene->CreateGameObject("Camera"); + auto* camera = cameraObject->AddComponent(); + ASSERT_NE(camera, nullptr); + camera->SetPrimary(true); + + TestRenderDevice device; + TestRenderCommandList commandList; + TestRenderCommandQueue commandQueue; + TestRenderResourceView colorView( + XCEngine::RHI::ResourceViewType::RenderTarget, + XCEngine::RHI::ResourceViewDimension::Texture2D, + XCEngine::RHI::Format::R8G8B8A8_UNorm); + TestRenderResourceView depthView( + XCEngine::RHI::ResourceViewType::DepthStencil, + XCEngine::RHI::ResourceViewDimension::Texture2D, + XCEngine::RHI::Format::D32_Float); + + const XCEngine::Rendering::RenderContext context = + CreateRenderContext( + device, + commandList, + commandQueue); + XCEngine::Rendering::RenderSurface surface(64u, 64u); + surface.SetColorAttachment(&colorView); + surface.SetDepthAttachment(&depthView); + + const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor + descriptor = { + "GameScripts", + "Gameplay", + "ManagedClearedPostProcessRenderPipelineProbeAsset" + }; + auto asset = + std::make_shared< + XCEngine::Rendering::Pipelines::ManagedScriptableRenderPipelineAsset>( + descriptor); + + XCEngine::Rendering::CameraRenderRequest request = {}; + request.scene = runtimeScene; + request.camera = camera; + request.context = context; + request.surface = surface; + + XCEngine::Rendering::RenderPipelineHost host(asset); + const std::vector plans = + host.BuildFramePlans({ request }); + + ASSERT_EQ(plans.size(), 1u); + const XCEngine::Rendering::CameraFramePlan& plan = plans[0]; + + EXPECT_FALSE( + plan.IsFullscreenStageRequested( + XCEngine::Rendering::CameraFrameStage::PostProcess)); + EXPECT_TRUE( + plan.IsFullscreenStageRequested( + XCEngine::Rendering::CameraFrameStage::FinalOutput)); + EXPECT_EQ( + plan.ResolveStageColorSource( + XCEngine::Rendering::CameraFrameStage::FinalOutput), + XCEngine::Rendering::CameraFrameColorSource::MainSceneColor); + EXPECT_TRUE(plan.IsFinalOutputStageValid()); +} + TEST_F( MonoScriptRuntimeTest, ManagedRenderPipelineAssetConfiguresCameraRequestsThroughRequestContext) {