diff --git a/docs/used/SRP_Mono原生后端选择统一收口计划_完成归档_2026-04-19.md b/docs/used/SRP_Mono原生后端选择统一收口计划_完成归档_2026-04-19.md new file mode 100644 index 00000000..b45062a1 --- /dev/null +++ b/docs/used/SRP_Mono原生后端选择统一收口计划_完成归档_2026-04-19.md @@ -0,0 +1,185 @@ +# SRP Mono 原生后端选择统一收口计划 2026-04-19 + +## 1. 阶段目标 + +上一阶段已经把: + +`ManagedScriptableRenderPipelineAsset` +-> `ManagedRenderPipelineAssetRuntime` +-> `GetPipelineRendererAsset()` +-> `ScriptableRenderPipelineHostAsset` + +这条 host 组合链路收出了一个正式 seam。 + +但 Mono 这条 managed SRP 执行链还没有完全跟上: + +`MonoManagedRenderPipelineStageRecorder` +在主场景录制时仍然会直接: + +`CreateDefaultNativeSceneRenderer()` + +这意味着当前仍然存在第二条隐藏 ownership: + +1. host 组合用的是 runtime 提供的 backend asset +2. scene recording fallback 却自己偷偷硬编码 builtin backend + +这会导致“managed pipeline 到底选择哪个 native backend”这件事没有真正收口。 + +本阶段目标就是把 Mono runtime 的 scene recording 也并到同一条 backend asset seam 上。 + +--- + +## 2. 当前问题 + +### 2.1 同一个 managed pipeline,host 和 stage recorder 现在可能不是同一后端语义 + +现在 `ManagedScriptableRenderPipelineAsset` 创建 host 时,会优先问: + +`ManagedRenderPipelineAssetRuntime::GetPipelineRendererAsset()` + +但 `MonoManagedRenderPipelineStageRecorder` 在主场景录制时,并没有复用这份 asset, +而是单独 new 一个默认 native scene renderer。 + +这在当前 builtin forward 只有一种 backend 时暂时不炸,但架构语义是错的: + +1. backend 选择被拆成了两处 +2. 后续 Universal / 自定义 SRP 想切 native backend 时会出现双写点 +3. 未来如果 host renderer 和 recorder fallback 不是同一种 renderer,行为会漂移 + +### 2.2 Mono runtime 还没有显式承担“我选择哪个 native backend asset”的责任 + +虽然基类已经有: + +`GetPipelineRendererAsset()` + +但 Mono runtime 当前并没有正式 override 这件事。 + +结果就是: + +1. 上一阶段刚建好的 seam 在 Mono 这里并没有真正落地 +2. 当前 builtin forward 仍然只是“默认兜底行为”,而不是“runtime 明确选择” + +### 2.3 缺少针对这条 seam 的 Mono 测试 + +当前 scripting tests 覆盖了: + +1. managed bridge 能创建 runtime +2. managed stage recorder 能录主场景和后处理 + +但还没有明确锁死: + +1. Mono runtime 会暴露一个确定的 pipeline renderer asset +2. Mono scene recording 会沿这份 asset 去创建 native scene renderer + +--- + +## 3. 本阶段方案 + +### 方案核心 + +把 Mono managed SRP 的 native backend ownership 统一成: + +`MonoManagedRenderPipelineAssetRuntime` +-> 解析/提供 native pipeline renderer asset +-> host 组合使用它 +-> stage recorder 录 scene 也使用它 + +而不是继续保留: + +host 一条链 +recorder 一条链 + +### 本阶段具体做法 + +1. 在 native factory 层补一个“从 renderer asset 创建 native scene renderer”的帮助函数 +2. 让 `MonoManagedRenderPipelineAssetRuntime` 正式 override `GetPipelineRendererAsset()` +3. 让 `MonoManagedRenderPipelineStageRecorder` 不再直接 `CreateDefaultNativeSceneRenderer()` +4. 改为优先通过 runtime 提供的 renderer asset 创建 scene renderer +5. 如果 asset 不可用或不能产出 `NativeSceneRenderer`,再统一回默认 fallback + +### 当前阶段的明确边界 + +这一刀只做 ownership 统一,不做下面这些内容: + +1. 不新增新的 C# public SRP API +2. 不引入 renderer data 到 native asset 的公开序列化桥 +3. 不做 deferred +4. 不做 lightmap / baking +5. 不做 Universal 包功能扩张 + +--- + +## 4. 实施步骤 + +### Step 1:补 native scene renderer asset 工厂 seam + +目标: + +1. 新增一个从 `RenderPipelineAsset` 创建 `NativeSceneRenderer` 的统一入口 +2. 如果 asset 对应 pipeline 不是 native scene renderer,则安全 fallback +3. 避免 Mono runtime 自己重复写 asset->pipeline->native cast 逻辑 + +### Step 2:让 Mono runtime 显式提供 backend asset + +目标: + +1. `MonoManagedRenderPipelineAssetRuntime` override `GetPipelineRendererAsset()` +2. 当前阶段先显式返回 builtin forward asset +3. 把“Mono managed SRP 当前使用 builtin forward native backend”从隐式默认改成显式选择 + +### Step 3:改 Mono stage recorder 复用 runtime backend asset + +目标: + +1. 删除 recorder 内部对 `CreateDefaultNativeSceneRenderer()` 的直接硬编码依赖 +2. 改为通过 runtime backend asset 解析 native scene renderer +3. 保证 scene recording 与 host 组合落到同一 backend 语义 + +### Step 4:补测试并锁行为 + +目标: + +1. 增加 scripting test,验证 Mono runtime 能提供 renderer asset +2. 验证该 asset 能创建 native scene renderer +3. 保留主场景/后处理 stage recording 回归测试,确保行为不退 + +### Step 5:完整验证并收口 + +目标: + +1. 编译 `XCEditor` +2. 运行相关 `rendering_unit_tests` +3. 运行相关 `scripting_tests` +4. 旧版 `editor/bin/Debug/XCEngine.exe` 10s 冒烟 +5. 归档 plan +6. 按规范提交并推送 + +--- + +## 5. 验收标准 + +本阶段完成后应满足: + +1. Mono managed SRP 的 host backend 选择和 scene recording backend 选择走同一条 seam +2. `MonoManagedRenderPipelineAssetRuntime` 对 native backend asset 有显式选择责任 +3. `MonoManagedRenderPipelineStageRecorder` 不再直接硬编码默认 builtin scene renderer 创建 +4. 相关 tests、编译、冒烟全部通过 + +--- + +## 6. 对后续 SRP 主线的意义 + +这一步完成后,managed SRP 的 backend ownership 会进一步收成: + +`Managed asset runtime` +-> backend asset +-> host renderer +-> scene recorder fallback + +这样下一阶段如果要继续向 Unity 风格推进: + +1. 给 Universal 包引入更明确的 backend asset 语义 +2. 拆分不同 renderer data 对不同 backend 的映射 +3. 再往 renderer feature / renderer data / asset workflow 扩 + +都不会再被 Mono recorder 里的隐式 builtin fallback 卡住。 diff --git a/engine/include/XCEngine/Rendering/Pipelines/ScriptableRenderPipelineHost.h b/engine/include/XCEngine/Rendering/Pipelines/ScriptableRenderPipelineHost.h index 8152aac2..dc430c87 100644 --- a/engine/include/XCEngine/Rendering/Pipelines/ScriptableRenderPipelineHost.h +++ b/engine/include/XCEngine/Rendering/Pipelines/ScriptableRenderPipelineHost.h @@ -47,6 +47,7 @@ public: private: bool EnsureInitialized(const RenderContext& context); + void BindStageRecorderPipelineRenderer(); void ShutdownInitializedComponents(); void ResetInitializationState(); void ClearInitializationContextIfNoComponentsAreInitialized(); diff --git a/engine/include/XCEngine/Rendering/RenderPipeline.h b/engine/include/XCEngine/Rendering/RenderPipeline.h index 73cc20c2..c857faf0 100644 --- a/engine/include/XCEngine/Rendering/RenderPipeline.h +++ b/engine/include/XCEngine/Rendering/RenderPipeline.h @@ -52,6 +52,8 @@ struct RenderPipelineStageRenderGraphContext { DirectionalShadowRenderPlan directionalShadowPlan = {}; }; +class RenderPipelineRenderer; + class RenderPipelineStageRecorder { public: virtual ~RenderPipelineStageRecorder() = default; @@ -60,6 +62,7 @@ public: return true; } virtual void Shutdown() {} + virtual void SetPipelineRenderer(RenderPipelineRenderer*) {} virtual bool SupportsStageRenderGraph(CameraFrameStage) const { return false; } diff --git a/engine/src/Rendering/Internal/RenderPipelineFactory.cpp b/engine/src/Rendering/Internal/RenderPipelineFactory.cpp index 006667fa..8769b8eb 100644 --- a/engine/src/Rendering/Internal/RenderPipelineFactory.cpp +++ b/engine/src/Rendering/Internal/RenderPipelineFactory.cpp @@ -8,6 +8,31 @@ namespace XCEngine { namespace Rendering { namespace Internal { +namespace { + +std::unique_ptr TryCreateNativeSceneRendererFromAsset( + const std::shared_ptr& asset) { + if (asset == nullptr) { + return nullptr; + } + + std::unique_ptr pipeline = asset->CreatePipeline(); + if (pipeline == nullptr) { + return nullptr; + } + + NativeSceneRenderer* const sceneRenderer = + dynamic_cast(pipeline.get()); + if (sceneRenderer == nullptr) { + return nullptr; + } + + (void)pipeline.release(); + return std::unique_ptr(sceneRenderer); +} + +} // namespace + std::shared_ptr CreateConfiguredRenderPipelineAsset() { return Pipelines::CreateConfiguredManagedRenderPipelineAsset(); } @@ -84,6 +109,25 @@ std::unique_ptr CreateRenderPipelineOrDefault( return std::make_unique(); } +std::unique_ptr CreateNativeSceneRendererFromAsset( + const std::shared_ptr& preferredAsset, + std::shared_ptr* outResolvedAsset) { + const std::shared_ptr resolvedAsset = + ResolveRenderPipelineAssetOrDefault(preferredAsset); + if (std::unique_ptr sceneRenderer = + TryCreateNativeSceneRendererFromAsset(resolvedAsset)) { + if (outResolvedAsset != nullptr) { + *outResolvedAsset = resolvedAsset; + } + return sceneRenderer; + } + + if (outResolvedAsset != nullptr) { + outResolvedAsset->reset(); + } + return CreateDefaultNativeSceneRenderer(); +} + std::unique_ptr CreateDefaultNativeSceneRenderer() { return std::make_unique(); } diff --git a/engine/src/Rendering/Internal/RenderPipelineFactory.h b/engine/src/Rendering/Internal/RenderPipelineFactory.h index cd10066b..a38d51d3 100644 --- a/engine/src/Rendering/Internal/RenderPipelineFactory.h +++ b/engine/src/Rendering/Internal/RenderPipelineFactory.h @@ -20,6 +20,9 @@ std::shared_ptr ResolveRenderPipelineAssetOrDefault( std::unique_ptr CreateRenderPipelineOrDefault( const std::shared_ptr& preferredAsset, std::shared_ptr* outResolvedAsset = nullptr); +std::unique_ptr CreateNativeSceneRendererFromAsset( + const std::shared_ptr& preferredAsset, + std::shared_ptr* outResolvedAsset = nullptr); std::unique_ptr CreateDefaultNativeSceneRenderer(); } // namespace Internal diff --git a/engine/src/Rendering/Pipelines/ScriptableRenderPipelineHost.cpp b/engine/src/Rendering/Pipelines/ScriptableRenderPipelineHost.cpp index f9c8c8ce..52625ade 100644 --- a/engine/src/Rendering/Pipelines/ScriptableRenderPipelineHost.cpp +++ b/engine/src/Rendering/Pipelines/ScriptableRenderPipelineHost.cpp @@ -183,6 +183,12 @@ bool ScriptableRenderPipelineHost::EnsureInitialized(const RenderContext& contex return true; } +void ScriptableRenderPipelineHost::BindStageRecorderPipelineRenderer() { + if (m_stageRecorder != nullptr) { + m_stageRecorder->SetPipelineRenderer(m_pipelineRenderer.get()); + } +} + void ScriptableRenderPipelineHost::ShutdownInitializedComponents() { if (m_stageRecorderInitialized && m_stageRecorder != nullptr) { @@ -219,6 +225,7 @@ void ScriptableRenderPipelineHost::ResetStageRecorder( } m_stageRecorder = std::move(stageRecorder); + BindStageRecorderPipelineRenderer(); m_stageRecorderInitialized = false; ClearInitializationContextIfNoComponentsAreInitialized(); } @@ -239,6 +246,7 @@ void ScriptableRenderPipelineHost::ResetPipelineRenderer( } m_pipelineRendererInitialized = false; + BindStageRecorderPipelineRenderer(); ClearInitializationContextIfNoComponentsAreInitialized(); } diff --git a/engine/src/Scripting/Mono/MonoScriptRuntime.cpp b/engine/src/Scripting/Mono/MonoScriptRuntime.cpp index d9391404..ade587c3 100644 --- a/engine/src/Scripting/Mono/MonoScriptRuntime.cpp +++ b/engine/src/Scripting/Mono/MonoScriptRuntime.cpp @@ -15,6 +15,7 @@ #include "Rendering/Internal/RenderPipelineFactory.h" #include "Rendering/Passes/BuiltinVectorFullscreenPass.h" #include "Rendering/Planning/FullscreenPassDesc.h" +#include "Rendering/Pipelines/BuiltinForwardPipeline.h" #include "Rendering/Pipelines/NativeSceneRecorder.h" #include "Rendering/Pipelines/ManagedScriptableRenderPipelineAsset.h" #include "Rendering/RenderPipelineStageGraphContract.h" @@ -718,6 +719,8 @@ public: bool TryGetDefaultFinalColorSettings( Rendering::FinalColorSettings& settings) const override; + std::shared_ptr + GetPipelineRendererAsset() const override; MonoScriptRuntime* GetRuntime() const { return m_runtime; @@ -780,13 +783,20 @@ public: } } m_fullscreenPassPool.clear(); - if (m_defaultSceneRenderer != nullptr) { - m_defaultSceneRenderer->Shutdown(); + if (m_ownedSceneRenderer != nullptr) { + m_ownedSceneRenderer->Shutdown(); } ReleaseManagedObjects(); m_supportsStageMethod = nullptr; m_recordStageMethod = nullptr; m_pipelineCreationAttempted = false; + m_boundSceneRenderer = nullptr; + } + + void SetPipelineRenderer( + Rendering::RenderPipelineRenderer* pipelineRenderer) override { + m_boundSceneRenderer = + dynamic_cast(pipelineRenderer); } bool SupportsStageRenderGraph(Rendering::CameraFrameStage stage) const override { @@ -833,15 +843,13 @@ public: ManagedScriptableRenderContextState managedContextState = {}; managedContextState.stage = context.stage; managedContextState.graphContext = &context; - if (m_defaultSceneRenderer == nullptr) { - m_defaultSceneRenderer = - Rendering::Internal::CreateDefaultNativeSceneRenderer(); - } - if (m_defaultSceneRenderer == nullptr) { + Rendering::NativeSceneRenderer* const sceneRenderer = + ResolveSceneRenderer(); + if (sceneRenderer == nullptr) { return false; } Rendering::Pipelines::NativeSceneRecorder sceneRecorder( - *m_defaultSceneRenderer, + *sceneRenderer, context); managedContextState.sceneRecorder = &sceneRecorder; const uint64_t managedContextHandle = @@ -964,6 +972,25 @@ private: passes); } + Rendering::NativeSceneRenderer* ResolveSceneRenderer() { + if (m_boundSceneRenderer != nullptr) { + return m_boundSceneRenderer; + } + + if (m_ownedSceneRenderer == nullptr) { + const std::shared_ptr + pipelineRendererAsset = + m_assetRuntime != nullptr + ? m_assetRuntime->GetPipelineRendererAsset() + : nullptr; + m_ownedSceneRenderer = + Rendering::Internal::CreateNativeSceneRendererFromAsset( + pipelineRendererAsset); + } + + return m_ownedSceneRenderer.get(); + } + std::shared_ptr m_assetRuntime; MonoScriptRuntime* m_runtime = nullptr; mutable uint32_t m_pipelineHandle = 0; @@ -971,7 +998,9 @@ private: mutable MonoMethod* m_recordStageMethod = nullptr; mutable bool m_pipelineCreationAttempted = false; std::vector> m_fullscreenPassPool = {}; - std::unique_ptr m_defaultSceneRenderer = nullptr; + Rendering::NativeSceneRenderer* m_boundSceneRenderer = nullptr; + std::unique_ptr m_ownedSceneRenderer = + nullptr; }; std::unique_ptr @@ -1057,6 +1086,14 @@ bool MonoManagedRenderPipelineAssetRuntime::TryGetDefaultFinalColorSettings( settings); } +std::shared_ptr +MonoManagedRenderPipelineAssetRuntime::GetPipelineRendererAsset() const { + static const std::shared_ptr + s_builtinForwardPipelineAsset = + std::make_shared(); + return s_builtinForwardPipelineAsset; +} + bool MonoManagedRenderPipelineAssetRuntime::CreateManagedPipeline( uint32_t& outPipelineHandle) const { outPipelineHandle = 0; diff --git a/tests/Rendering/unit/test_camera_scene_renderer.cpp b/tests/Rendering/unit/test_camera_scene_renderer.cpp index 5c92f7b3..38b5095a 100644 --- a/tests/Rendering/unit/test_camera_scene_renderer.cpp +++ b/tests/Rendering/unit/test_camera_scene_renderer.cpp @@ -441,10 +441,12 @@ struct MockPipelineAssetState { struct MockStageRecorderState { int initializeCalls = 0; int shutdownCalls = 0; + int setPipelineRendererCalls = 0; int recordMainSceneCalls = 0; bool supportsMainSceneRenderGraph = false; bool recordMainSceneResult = true; bool lastReceivedRenderGraphBlackboard = false; + RenderPipelineRenderer* lastPipelineRenderer = nullptr; }; class MockPipeline final : public RenderPipeline { @@ -688,6 +690,12 @@ public: ++m_state->shutdownCalls; } + void SetPipelineRenderer(RenderPipelineRenderer* pipelineRenderer) + override { + ++m_state->setPipelineRendererCalls; + m_state->lastPipelineRenderer = pipelineRenderer; + } + bool SupportsStageRenderGraph(CameraFrameStage stage) const override { return SupportsCameraFramePipelineGraphRecording(stage) && m_state->supportsMainSceneRenderGraph; @@ -4477,6 +4485,29 @@ TEST(ScriptableRenderPipelineHost_Test, PrefersStageRecorderBeforeFallbackRender EXPECT_EQ(replacementRecorderState->shutdownCalls, 1); } +TEST(ScriptableRenderPipelineHost_Test, BindsCurrentPipelineRendererIntoStageRecorder) { + auto initialPipelineState = std::make_shared(); + auto replacementPipelineState = std::make_shared(); + auto recorderState = std::make_shared(); + + Pipelines::ScriptableRenderPipelineHost host( + std::make_unique(initialPipelineState)); + + host.SetStageRecorder( + std::make_unique(recorderState)); + EXPECT_EQ(recorderState->setPipelineRendererCalls, 1); + EXPECT_EQ( + recorderState->lastPipelineRenderer, + host.GetPipelineRenderer()); + + host.SetPipelineRenderer( + std::make_unique(replacementPipelineState)); + EXPECT_EQ(recorderState->setPipelineRendererCalls, 2); + EXPECT_EQ( + recorderState->lastPipelineRenderer, + host.GetPipelineRenderer()); +} + TEST( ScriptableRenderPipelineHost_Test, FallsBackToRendererWhenStageRecorderDeclinesRecording) { diff --git a/tests/scripting/test_mono_script_runtime.cpp b/tests/scripting/test_mono_script_runtime.cpp index a37cf6ab..70f67bda 100644 --- a/tests/scripting/test_mono_script_runtime.cpp +++ b/tests/scripting/test_mono_script_runtime.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -2431,6 +2432,36 @@ TEST_F( recorder->Shutdown(); } +TEST_F( + MonoScriptRuntimeTest, + ManagedRenderPipelineBridgeRuntimeExposesBuiltinForwardRendererAsset) { + const auto bridge = + XCEngine::Rendering::Pipelines::GetManagedRenderPipelineBridge(); + ASSERT_NE(bridge, nullptr); + + const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor descriptor = { + "GameScripts", + "Gameplay", + "ManagedRenderPipelineProbeAsset" + }; + + std::shared_ptr + assetRuntime = bridge->CreateAssetRuntime(descriptor); + ASSERT_NE(assetRuntime, nullptr); + + const std::shared_ptr + rendererAsset = assetRuntime->GetPipelineRendererAsset(); + ASSERT_NE(rendererAsset, nullptr); + + std::unique_ptr pipeline = + rendererAsset->CreatePipeline(); + ASSERT_NE(pipeline, nullptr); + EXPECT_NE( + dynamic_cast( + pipeline.get()), + nullptr); +} + TEST_F( MonoScriptRuntimeTest, ManagedStageRecorderRecordsMainSceneThroughScriptableRenderContext) {