diff --git a/docs/used/SRP_ManagedAsset原生后端选择收口计划_完成归档_2026-04-19.md b/docs/used/SRP_ManagedAsset原生后端选择收口计划_完成归档_2026-04-19.md new file mode 100644 index 00000000..81620d81 --- /dev/null +++ b/docs/used/SRP_ManagedAsset原生后端选择收口计划_完成归档_2026-04-19.md @@ -0,0 +1,144 @@ +# SRP ManagedAsset 原生后端选择收口计划 2026-04-19 + +## 1. 阶段目标 + +上一阶段已经把 `Universal` 包里的录制组合边界收紧了,但 native 侧还有一个关键 ownership 没有收干净: + +`ManagedScriptableRenderPipelineAsset` +-> `ScriptableRenderPipelineHostAsset` +-> 默认 `BuiltinForwardPipelineAsset` + +这条链目前是硬编码的。也就是说,只要是 managed pipeline asset,native 执行后端就默认落到同一个 `BuiltinForwardPipeline`。 + +这在当前阶段能工作,但它会直接卡住后续两件事: + +1. first-party `Universal` 包对 native backend 的正式接管 +2. 未来不同 managed pipeline 对不同 native backend 的选择语义 + +本阶段不新增渲染特性,只把这条“managed asset 如何选择 native backend”的 seam 正式化。 + +--- + +## 2. 当前问题 + +### 2.1 `ManagedScriptableRenderPipelineAsset` 现在把执行后端写死了 + +当前 `ManagedScriptableRenderPipelineAsset` 内部持有固定的: + +1. `ScriptableRenderPipelineHostAsset m_executionHostAsset` + +而这个 host asset 默认再去创建: + +1. `BuiltinForwardPipelineAsset` +2. `BuiltinForwardPipeline` + +这意味着 managed runtime 当前只能决定: + +1. stage recorder +2. camera request policy +3. final color defaults + +但**不能决定它自己要挂在哪个 native renderer backend 上**。 + +### 2.2 Mono stage recorder 也在默认依赖 builtin native scene renderer + +当前 `MonoManagedRenderPipelineStageRecorder` 在 scene recording fallback 里,也会默认拿: + +1. `CreateDefaultNativeSceneRenderer()` + +这说明“managed pipeline 默认使用 builtin native scene renderer”已经不是单点实现,而是正在成为一条隐式固定约束。 + +### 2.3 这不符合后续 SRP/URP 的 ownership 方向 + +我们现在要的不是立刻把所有场景绘制搬上 C#,而是先把 ownership 变成清晰的: + +`Managed pipeline asset runtime` +-> 选择 native backend asset +-> `ScriptableRenderPipelineHost` +负责组合 backend + recorder + +而不是: + +`Managed pipeline asset` +-> 固定 builtin forward +-> 后续再到处拆硬编码 + +--- + +## 3. 本阶段方案 + +本阶段采用的方向: + +1. 在 `ManagedRenderPipelineAssetRuntime` 上增加“可选提供 native pipeline renderer asset”的 seam +2. 让 `ManagedScriptableRenderPipelineAsset` 在创建 host 和取默认 final color 时,优先使用 runtime 提供的 renderer asset +3. 如果 runtime 没提供,则保持现有 fallback,不改当前行为 +4. 先把 seam 在 native 层立住,不在这一阶段新增新的 managed C# 公共 API + +这样做的目的很明确: + +1. 先去掉 managed asset 对 builtin forward 的硬编码依赖 +2. 保持现有行为不回退 +3. 为后续 first-party `Universal` 显式选择 backend 铺路 + +--- + +## 4. 实施步骤 + +### Step 1:扩展 managed runtime contract + +目标: + +1. 在 `ManagedRenderPipelineAssetRuntime` 增加可选的 renderer asset 提供接口 +2. 保持默认实现返回空,不影响现有 bridge + +### Step 2:重构 `ManagedScriptableRenderPipelineAsset` 的执行 host 组合方式 + +目标: + +1. 不再把默认 execution host asset 固定成成员常量 +2. 改为按 runtime 解析结果构建 `ScriptableRenderPipelineHostAsset` +3. `CreatePipeline` 与 `GetDefaultFinalColorSettings` 走同一条 backend asset 解析逻辑 + +### Step 3:补测试,锁死新 seam + +目标: + +1. 新增 rendering unit test +2. 验证 runtime 提供 renderer asset 时,host 确实使用该 asset +3. 验证 runtime 未提供时,仍然走现有 fallback +4. 验证默认 final color fallback 会从 runtime 提供的 renderer asset 读取 + +### Step 4:完整验证 + +目标: + +1. 编译 `XCEditor` +2. 运行相关 `rendering_unit_tests` +3. 运行相关 `scripting_tests` +4. 运行相关 `editor_tests` +5. 旧版 `editor/bin/Debug/XCEngine.exe` 10s 冒烟 + +--- + +## 5. 验收标准 + +本阶段完成后应满足: + +1. `ManagedScriptableRenderPipelineAsset` 不再把 builtin forward 当成唯一固定 execution backend +2. managed runtime 可以可选提供 native renderer asset +3. 未提供 runtime renderer asset 时,现有行为不回退 +4. 相关 tests、编译、冒烟全部通过 + +--- + +## 6. 本阶段不做的内容 + +这一阶段明确不做: + +1. 不新增新的 C# public SRP API +2. 不直接把阴影、高斯、体积、后处理 ownership 迁到 managed +3. 不做 deferred +4. 不做 lightmap / baking +5. 不做 editor renderer asset 工作流扩展 + +这一刀只收 `ManagedScriptableRenderPipelineAsset -> native backend asset` 的组合边界。 diff --git a/engine/include/XCEngine/Rendering/Pipelines/ManagedScriptableRenderPipelineAsset.h b/engine/include/XCEngine/Rendering/Pipelines/ManagedScriptableRenderPipelineAsset.h index f4a3206f..ab545318 100644 --- a/engine/include/XCEngine/Rendering/Pipelines/ManagedScriptableRenderPipelineAsset.h +++ b/engine/include/XCEngine/Rendering/Pipelines/ManagedScriptableRenderPipelineAsset.h @@ -57,9 +57,12 @@ public: private: std::shared_ptr ResolveManagedAssetRuntime() const; + std::shared_ptr + ResolvePipelineRendererAsset() const; + ScriptableRenderPipelineHostAsset + CreateExecutionHostAsset() const; ManagedRenderPipelineAssetDescriptor m_descriptor; - ScriptableRenderPipelineHostAsset m_executionHostAsset; mutable std::shared_ptr m_managedAssetRuntime = nullptr; mutable size_t m_managedAssetRuntimeBridgeGeneration = 0u; @@ -80,6 +83,11 @@ public: const DirectionalShadowPlanningSettings&) const { } + virtual std::shared_ptr + GetPipelineRendererAsset() const { + return nullptr; + } + virtual bool TryGetDefaultFinalColorSettings(FinalColorSettings&) const { return false; } diff --git a/engine/src/Rendering/Pipelines/ManagedScriptableRenderPipelineAsset.cpp b/engine/src/Rendering/Pipelines/ManagedScriptableRenderPipelineAsset.cpp index 096ab03d..aa029b17 100644 --- a/engine/src/Rendering/Pipelines/ManagedScriptableRenderPipelineAsset.cpp +++ b/engine/src/Rendering/Pipelines/ManagedScriptableRenderPipelineAsset.cpp @@ -37,9 +37,33 @@ ManagedScriptableRenderPipelineAsset::ResolveManagedAssetRuntime() const { return m_managedAssetRuntime; } +std::shared_ptr +ManagedScriptableRenderPipelineAsset::ResolvePipelineRendererAsset() const { + if (const std::shared_ptr runtime = + ResolveManagedAssetRuntime(); + runtime != nullptr) { + return runtime->GetPipelineRendererAsset(); + } + + return nullptr; +} + +ScriptableRenderPipelineHostAsset +ManagedScriptableRenderPipelineAsset::CreateExecutionHostAsset() const { + if (const std::shared_ptr pipelineRendererAsset = + ResolvePipelineRendererAsset(); + pipelineRendererAsset != nullptr) { + return ScriptableRenderPipelineHostAsset(pipelineRendererAsset); + } + + return ScriptableRenderPipelineHostAsset(); +} + std::unique_ptr ManagedScriptableRenderPipelineAsset::CreatePipeline() const { + const ScriptableRenderPipelineHostAsset executionHostAsset = + CreateExecutionHostAsset(); std::unique_ptr pipeline = - m_executionHostAsset.CreatePipeline(); + executionHostAsset.CreatePipeline(); auto* host = dynamic_cast(pipeline.get()); if (host == nullptr) { return pipeline; @@ -86,7 +110,9 @@ FinalColorSettings ManagedScriptableRenderPipelineAsset::GetDefaultFinalColorSet } } - return m_executionHostAsset.GetDefaultFinalColorSettings(); + const ScriptableRenderPipelineHostAsset executionHostAsset = + CreateExecutionHostAsset(); + return executionHostAsset.GetDefaultFinalColorSettings(); } void SetManagedRenderPipelineBridge( diff --git a/tests/Rendering/unit/test_camera_scene_renderer.cpp b/tests/Rendering/unit/test_camera_scene_renderer.cpp index 5d8238f9..5c92f7b3 100644 --- a/tests/Rendering/unit/test_camera_scene_renderer.cpp +++ b/tests/Rendering/unit/test_camera_scene_renderer.cpp @@ -775,11 +775,13 @@ private: struct MockManagedRenderPipelineAssetRuntimeState { int createStageRecorderCalls = 0; int configureCameraRenderRequestCalls = 0; + int getPipelineRendererAssetCalls = 0; int getDefaultFinalColorSettingsCalls = 0; bool hasDefaultFinalColorSettings = false; FinalColorSettings defaultFinalColorSettings = {}; size_t lastRenderedBaseCameraCount = 0u; size_t lastRenderedRequestCount = 0u; + std::shared_ptr pipelineRendererAsset = nullptr; std::shared_ptr lastCreatedStageRecorderState; std::function + GetPipelineRendererAsset() const override { + ++m_state->getPipelineRendererAssetCalls; + return m_state->pipelineRendererAsset; + } + bool TryGetDefaultFinalColorSettings( FinalColorSettings& settings) const override { ++m_state->getDefaultFinalColorSettingsCalls; @@ -843,6 +851,7 @@ struct MockManagedRenderPipelineBridgeState { std::shared_ptr lastCreatedRuntimeState; bool hasDefaultFinalColorSettings = false; FinalColorSettings defaultFinalColorSettings = {}; + std::shared_ptr pipelineRendererAsset = nullptr; std::functionhasDefaultFinalColorSettings; m_state->lastCreatedRuntimeState->defaultFinalColorSettings = m_state->defaultFinalColorSettings; + m_state->lastCreatedRuntimeState->pipelineRendererAsset = + m_state->pipelineRendererAsset; m_state->lastCreatedRuntimeState->configureCameraRenderRequest = m_state->configureCameraRenderRequest; return std::make_shared( @@ -4591,6 +4602,9 @@ TEST(ManagedScriptableRenderPipelineAsset_Test, CreatesHostWithStageRecorderFrom EXPECT_EQ(bridgeState->lastDescriptor.className, "ManagedRenderPipelineProbeAsset"); ASSERT_NE(bridgeState->lastCreatedRuntimeState, nullptr); + EXPECT_EQ( + bridgeState->lastCreatedRuntimeState->getPipelineRendererAssetCalls, + 1); ASSERT_NE( bridgeState->lastCreatedRuntimeState->lastCreatedStageRecorderState, nullptr); @@ -4602,6 +4616,42 @@ TEST(ManagedScriptableRenderPipelineAsset_Test, CreatesHostWithStageRecorderFrom Pipelines::ClearManagedRenderPipelineBridge(); } +TEST( + ManagedScriptableRenderPipelineAsset_Test, + UsesRuntimeProvidedPipelineRendererAssetForHostComposition) { + Pipelines::ClearManagedRenderPipelineBridge(); + + const Pipelines::ManagedRenderPipelineAssetDescriptor descriptor = { + "GameScripts", + "Gameplay", + "ManagedRenderPipelineProbeAsset" + }; + auto bridgeState = std::make_shared(); + auto rendererAssetState = std::make_shared(); + auto rendererAsset = std::make_shared(rendererAssetState); + bridgeState->pipelineRendererAsset = rendererAsset; + Pipelines::SetManagedRenderPipelineBridge( + std::make_shared(bridgeState)); + + Pipelines::ManagedScriptableRenderPipelineAsset asset(descriptor); + std::unique_ptr pipeline = asset.CreatePipeline(); + ASSERT_NE(pipeline, nullptr); + + auto* host = + dynamic_cast(pipeline.get()); + ASSERT_NE(host, nullptr); + EXPECT_EQ(host->GetPipelineRendererAsset(), rendererAsset.get()); + EXPECT_NE(host->GetPipelineRenderer(), nullptr); + EXPECT_NE(host->GetStageRecorder(), nullptr); + EXPECT_EQ(rendererAssetState->createCalls, 1); + ASSERT_NE(bridgeState->lastCreatedRuntimeState, nullptr); + EXPECT_EQ( + bridgeState->lastCreatedRuntimeState->getPipelineRendererAssetCalls, + 1); + + Pipelines::ClearManagedRenderPipelineBridge(); +} + TEST(ManagedScriptableRenderPipelineAsset_Test, LetsManagedBridgeConfigureCameraRenderRequests) { Pipelines::ClearManagedRenderPipelineBridge(); @@ -4714,6 +4764,58 @@ TEST(ManagedScriptableRenderPipelineAsset_Test, LetsManagedBridgeProvideDefaultF Pipelines::ClearManagedRenderPipelineBridge(); } +TEST( + ManagedScriptableRenderPipelineAsset_Test, + FallsBackToRuntimeProvidedPipelineRendererAssetDefaultFinalColorSettings) { + Pipelines::ClearManagedRenderPipelineBridge(); + + const Pipelines::ManagedRenderPipelineAssetDescriptor descriptor = { + "GameScripts", + "Gameplay", + "ManagedFinalColorRenderPipelineProbeAsset" + }; + auto bridgeState = std::make_shared(); + auto rendererAssetState = std::make_shared(); + rendererAssetState->defaultFinalColorSettings.outputTransferMode = + FinalColorOutputTransferMode::LinearToSRGB; + rendererAssetState->defaultFinalColorSettings.exposureMode = + FinalColorExposureMode::Fixed; + rendererAssetState->defaultFinalColorSettings.exposureValue = 2.25f; + rendererAssetState->defaultFinalColorSettings.toneMappingMode = + FinalColorToneMappingMode::ACES; + rendererAssetState->defaultFinalColorSettings.finalColorScale = + XCEngine::Math::Vector4(1.02f, 0.97f, 0.93f, 1.0f); + bridgeState->pipelineRendererAsset = + std::make_shared(rendererAssetState); + Pipelines::SetManagedRenderPipelineBridge( + std::make_shared(bridgeState)); + + Pipelines::ManagedScriptableRenderPipelineAsset asset(descriptor); + const FinalColorSettings settings = asset.GetDefaultFinalColorSettings(); + + EXPECT_EQ(bridgeState->createAssetRuntimeCalls, 1); + ASSERT_NE(bridgeState->lastCreatedRuntimeState, nullptr); + EXPECT_EQ( + bridgeState->lastCreatedRuntimeState->getDefaultFinalColorSettingsCalls, + 1); + EXPECT_EQ( + bridgeState->lastCreatedRuntimeState->getPipelineRendererAssetCalls, + 1); + EXPECT_EQ(rendererAssetState->createCalls, 0); + EXPECT_EQ( + settings.outputTransferMode, + FinalColorOutputTransferMode::LinearToSRGB); + EXPECT_EQ(settings.exposureMode, FinalColorExposureMode::Fixed); + EXPECT_FLOAT_EQ(settings.exposureValue, 2.25f); + EXPECT_EQ(settings.toneMappingMode, FinalColorToneMappingMode::ACES); + EXPECT_FLOAT_EQ(settings.finalColorScale.x, 1.02f); + EXPECT_FLOAT_EQ(settings.finalColorScale.y, 0.97f); + EXPECT_FLOAT_EQ(settings.finalColorScale.z, 0.93f); + EXPECT_FLOAT_EQ(settings.finalColorScale.w, 1.0f); + + Pipelines::ClearManagedRenderPipelineBridge(); +} + TEST(ManagedScriptableRenderPipelineAsset_Test, ReusesManagedAssetRuntimeAcrossPipelineAndRequestCalls) { Pipelines::ClearManagedRenderPipelineBridge();