diff --git a/docs/used/SRP_Universal原生后端Key接缝计划_完成归档_2026-04-19.md b/docs/used/SRP_Universal原生后端Key接缝计划_完成归档_2026-04-19.md new file mode 100644 index 00000000..ab3648c9 --- /dev/null +++ b/docs/used/SRP_Universal原生后端Key接缝计划_完成归档_2026-04-19.md @@ -0,0 +1,202 @@ +# SRP Universal 原生后端 Key 接缝计划 2026-04-19 + +## 1. 阶段目标 + +上一阶段已经把 Mono managed SRP 的: + +1. host renderer +2. stage recorder + +收到了同一条 native backend ownership 上。 + +但当前仍然有一个关键问题没有解决: + +`MonoManagedRenderPipelineAssetRuntime` +现在还是统一硬编码返回: + +`BuiltinForwardPipelineAsset` + +这意味着: + +1. managed asset 自己并没有显式声明“我要哪个 native backend” +2. first-party `UniversalRenderPipelineAsset` 和普通 `ScriptableRenderPipelineAsset` + 在 native backend 选择上没有语义区别 +3. 未来想让不同 SRP/renderer data 走不同 native backend 时, + 还会继续卡在 Mono runtime 的类型外部硬编码 + +本阶段目标就是把这件事改成: + +`ScriptableRenderPipelineAsset` +-> 返回 native backend key +-> Mono runtime 解析 key +-> native factory 映射到 `RenderPipelineAsset` + +先把“谁声明 backend”这件事正式落到 managed asset 自己身上。 + +--- + +## 2. 当前问题 + +### 2.1 Universal 虽然已经有 `rendererData`,但还没有 native backend 声明权 + +当前 `UniversalRenderPipelineAsset` 已经拥有: + +1. `rendererData` +2. `CreatePipeline()` +3. `ConfigureCameraRenderRequest()` + +这些都说明 managed package 侧已经在承担“渲染管线组织”责任。 + +但 native backend 选择仍然没有经过它,而是 Mono runtime 统一硬编码。 + +这不符合我们要的方向: + +1. first-party Universal 应该先成为第一个显式声明 backend 的包 +2. Mono runtime 只负责桥接,不负责替 asset 做产品决策 + +### 2.2 当前 `CreateNativeSceneRendererFromAsset(nullptr)` 语义不干净 + +上一阶段新补的: + +`CreateNativeSceneRendererFromAsset(...)` + +内部现在仍然会走: + +`ResolveRenderPipelineAssetOrDefault(...)` + +这对于“从一个明确 backend asset 创建 native scene renderer”这件事来说语义过重了。 + +如果传入 `nullptr`,它不该再去全局查询 configured render pipeline asset, +更不该重新把当前 managed pipeline asset 自己绕回来。 + +这个点如果不修,后面 backend key seam 加上去以后, +scene recorder 的 fallback 仍然有可能偷偷回到全局 configured asset, +会把 ownership 再次搞脏。 + +### 2.3 当前还缺一组“unknown backend key 也不会崩”的测试 + +我们不仅要验证: + +1. Universal 显式声明 builtin forward key 能被解析 + +还要验证: + +1. key 不存在时 runtime 返回空 asset +2. recorder 仍然能本地 fallback 到默认 native scene renderer +3. 不会因为 unknown key 让 managed stage graph 录制回归 + +--- + +## 3. 本阶段方案 + +### 方案核心 + +新增一条受控的 managed seam: + +`ScriptableRenderPipelineAsset` +-> `GetPipelineRendererAssetKey()` +-> Mono runtime +-> native factory key mapping +-> `RenderPipelineAsset` + +### 第一阶段只落 first-party Universal + +本阶段只让: + +`UniversalRenderPipelineAsset` + +显式返回: + +`BuiltinForward` + +也就是说: + +1. Universal 成为第一个正式声明 native backend 的 managed package +2. 普通 `ScriptableRenderPipelineAsset` 先保持默认不声明 +3. Mono runtime 对“未声明 key”的 asset 返回 `nullptr` +4. host / recorder 再按本地 fallback 兜底 + +### 这样做的原因 + +这样能同时保证两件事: + +1. 我们正式建立了面向未来的 backend key seam +2. 又不会一次性把所有 custom SRP 公共 API 扩太大 + +--- + +## 4. 实施步骤 + +### Step 1:给 managed asset 加 backend key seam + +目标: + +1. 在 `ScriptableRenderPipelineAsset` 新增受保护虚方法 +2. 默认返回空 key +3. `UniversalRenderPipelineAsset` override 返回 `BuiltinForward` + +### Step 2:补 native key -> asset 工厂映射 + +目标: + +1. 在 native factory 层新增统一的 key 解析入口 +2. 当前先支持 `BuiltinForward` +3. 保证未来增加更多 native backend 时,不需要把 if/switch 散落到 Mono runtime + +### Step 3:重构 Mono runtime 的 backend asset 解析 + +目标: + +1. `MonoManagedRenderPipelineAssetRuntime::GetPipelineRendererAsset()` + 不再硬编码 builtin forward +2. 改为调用 managed asset 的 backend key 方法 +3. 按 key 去 native factory 解析 asset +4. 未声明或未知 key 时返回空 asset + +### Step 4:修正 recorder fallback 语义 + +目标: + +1. `CreateNativeSceneRendererFromAsset(nullptr)` 不再回查全局 configured pipeline asset +2. recorder fallback 只在“本地 asset 解析失败”后回到默认 native scene renderer +3. 避免 native backend 选择再次绕回全局 configured managed asset + +### Step 5:补测试并完整验证 + +目标: + +1. 增加 scripting tests,验证 Universal backend key -> builtin forward asset +2. 增加 unknown key fallback 回归测试 +3. 编译 `rendering_unit_tests`、`scripting_tests`、`XCEditor` +4. 旧版 editor 10s 冒烟 +5. 归档 plan、提交、推送 + +--- + +## 5. 验收标准 + +本阶段完成后应满足: + +1. native backend 的声明责任开始落到 managed asset 自己 +2. first-party Universal 是第一个显式声明 native backend 的包 +3. Mono runtime 不再统一硬编码 builtin forward backend +4. recorder fallback 不再回查全局 configured managed asset +5. unknown backend key 不会导致 managed stage recording 崩坏 + +--- + +## 6. 本阶段不做的事 + +本阶段明确不做: + +1. 不开放完整用户自定义 backend key 注册系统 +2. 不做 deferred +3. 不做 lightmap / baking +4. 不做 renderer data 到 native renderer asset 的复杂序列化桥 +5. 不做 editor workflow 扩展 + +这一刀只建立: + +`managed asset -> backend key -> native asset` + +这条正式接缝。 diff --git a/engine/src/Rendering/Internal/RenderPipelineFactory.cpp b/engine/src/Rendering/Internal/RenderPipelineFactory.cpp index 8769b8eb..cb2f3072 100644 --- a/engine/src/Rendering/Internal/RenderPipelineFactory.cpp +++ b/engine/src/Rendering/Internal/RenderPipelineFactory.cpp @@ -41,6 +41,18 @@ std::shared_ptr CreateFallbackRenderPipelineAsset() { return std::make_shared(); } +std::shared_ptr CreatePipelineRendererAssetByKey( + const std::string& key) { + if (key == "BuiltinForward") { + static const std::shared_ptr + s_builtinForwardPipelineAsset = + std::make_shared(); + return s_builtinForwardPipelineAsset; + } + + return nullptr; +} + std::shared_ptr ResolveRenderPipelineAssetOrDefault( std::shared_ptr preferredAsset) { if (preferredAsset != nullptr) { @@ -112,12 +124,10 @@ std::unique_ptr CreateRenderPipelineOrDefault( 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)) { + TryCreateNativeSceneRendererFromAsset(preferredAsset)) { if (outResolvedAsset != nullptr) { - *outResolvedAsset = resolvedAsset; + *outResolvedAsset = preferredAsset; } return sceneRenderer; } diff --git a/engine/src/Rendering/Internal/RenderPipelineFactory.h b/engine/src/Rendering/Internal/RenderPipelineFactory.h index a38d51d3..b2ad9c9f 100644 --- a/engine/src/Rendering/Internal/RenderPipelineFactory.h +++ b/engine/src/Rendering/Internal/RenderPipelineFactory.h @@ -1,5 +1,6 @@ #pragma once +#include #include namespace XCEngine { @@ -13,6 +14,8 @@ namespace Internal { std::shared_ptr CreateConfiguredRenderPipelineAsset(); std::shared_ptr CreateFallbackRenderPipelineAsset(); +std::shared_ptr CreatePipelineRendererAssetByKey( + const std::string& key); std::shared_ptr ResolveRenderPipelineAssetOrDefault( std::shared_ptr preferredAsset); diff --git a/engine/src/Scripting/Mono/MonoScriptRuntime.cpp b/engine/src/Scripting/Mono/MonoScriptRuntime.cpp index ade587c3..cea9881b 100644 --- a/engine/src/Scripting/Mono/MonoScriptRuntime.cpp +++ b/engine/src/Scripting/Mono/MonoScriptRuntime.cpp @@ -15,7 +15,6 @@ #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" @@ -743,6 +742,8 @@ private: MonoObject* assetObject) const; MonoMethod* ResolveGetDefaultFinalColorSettingsMethod( MonoObject* assetObject) const; + MonoMethod* ResolveGetPipelineRendererAssetKeyMethod( + MonoObject* assetObject) const; MonoScriptRuntime* m_runtime = nullptr; std::weak_ptr m_runtimeLifetime; @@ -751,8 +752,12 @@ private: mutable MonoMethod* m_createPipelineMethod = nullptr; mutable MonoMethod* m_configureCameraRenderRequestMethod = nullptr; mutable MonoMethod* m_getDefaultFinalColorSettingsMethod = nullptr; + mutable MonoMethod* m_getPipelineRendererAssetKeyMethod = nullptr; mutable bool m_ownsManagedAssetHandle = false; mutable bool m_assetCreationAttempted = false; + mutable bool m_pipelineRendererAssetResolved = false; + mutable std::shared_ptr + m_pipelineRendererAsset = nullptr; }; class MonoManagedRenderPipelineStageRecorder final @@ -1088,10 +1093,42 @@ bool MonoManagedRenderPipelineAssetRuntime::TryGetDefaultFinalColorSettings( std::shared_ptr MonoManagedRenderPipelineAssetRuntime::GetPipelineRendererAsset() const { - static const std::shared_ptr - s_builtinForwardPipelineAsset = - std::make_shared(); - return s_builtinForwardPipelineAsset; + if (m_pipelineRendererAssetResolved) { + return m_pipelineRendererAsset; + } + + m_pipelineRendererAssetResolved = true; + m_pipelineRendererAsset.reset(); + if (!EnsureManagedAsset()) { + return nullptr; + } + + MonoObject* const assetObject = GetManagedAssetObject(); + MonoMethod* const method = + ResolveGetPipelineRendererAssetKeyMethod(assetObject); + if (assetObject == nullptr || method == nullptr) { + return nullptr; + } + + MonoObject* managedKeyObject = nullptr; + if (!m_runtime->InvokeManagedMethod( + assetObject, + method, + nullptr, + &managedKeyObject)) { + return nullptr; + } + + const std::string pipelineRendererAssetKey = + MonoStringToUtf8(reinterpret_cast(managedKeyObject)); + if (pipelineRendererAssetKey.empty()) { + return nullptr; + } + + m_pipelineRendererAsset = + Rendering::Internal::CreatePipelineRendererAssetByKey( + pipelineRendererAssetKey); + return m_pipelineRendererAsset; } bool MonoManagedRenderPipelineAssetRuntime::CreateManagedPipeline( @@ -1199,6 +1236,9 @@ void MonoManagedRenderPipelineAssetRuntime::ReleaseManagedAsset() const { m_createPipelineMethod = nullptr; m_configureCameraRenderRequestMethod = nullptr; m_getDefaultFinalColorSettingsMethod = nullptr; + m_getPipelineRendererAssetKeyMethod = nullptr; + m_pipelineRendererAsset.reset(); + m_pipelineRendererAssetResolved = false; const bool ownsManagedAssetHandle = m_ownsManagedAssetHandle; m_ownsManagedAssetHandle = false; m_assetCreationAttempted = false; @@ -1262,6 +1302,20 @@ MonoManagedRenderPipelineAssetRuntime::ResolveGetDefaultFinalColorSettingsMethod return m_getDefaultFinalColorSettingsMethod; } +MonoMethod* +MonoManagedRenderPipelineAssetRuntime::ResolveGetPipelineRendererAssetKeyMethod( + MonoObject* assetObject) const { + if (m_getPipelineRendererAssetKeyMethod == nullptr) { + m_getPipelineRendererAssetKeyMethod = + m_runtime->ResolveManagedMethod( + assetObject, + "GetPipelineRendererAssetKey", + 0); + } + + return m_getPipelineRendererAssetKeyMethod; +} + class MonoManagedRenderPipelineBridge final : public Rendering::Pipelines::ManagedRenderPipelineBridge { public: diff --git a/managed/GameScripts/RenderPipelineApiProbe.cs b/managed/GameScripts/RenderPipelineApiProbe.cs index 815416a0..f2e62b2f 100644 --- a/managed/GameScripts/RenderPipelineApiProbe.cs +++ b/managed/GameScripts/RenderPipelineApiProbe.cs @@ -926,6 +926,20 @@ namespace Gameplay } } + public sealed class ManagedUnknownBackendRenderPipelineProbeAsset + : UniversalRenderPipelineAsset + { + public ManagedUnknownBackendRenderPipelineProbeAsset() + { + rendererData = new ManagedRenderPipelineProbeRendererData(); + } + + protected override string GetPipelineRendererAssetKey() + { + return "MissingBackend"; + } + } + public sealed class ManagedUniversalRenderPipelineProbeAsset : UniversalRenderPipelineAsset { diff --git a/managed/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipelineAsset.cs b/managed/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipelineAsset.cs index 215d4d41..7a65694b 100644 --- a/managed/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipelineAsset.cs +++ b/managed/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipelineAsset.cs @@ -22,6 +22,11 @@ namespace XCEngine.Rendering { return FinalColorSettings.CreateDefault(); } + + protected virtual string GetPipelineRendererAssetKey() + { + return string.Empty; + } } } diff --git a/managed/XCEngine.ScriptCore/Rendering/Universal/UniversalRenderPipelineAsset.cs b/managed/XCEngine.ScriptCore/Rendering/Universal/UniversalRenderPipelineAsset.cs index 40b31075..1f968214 100644 --- a/managed/XCEngine.ScriptCore/Rendering/Universal/UniversalRenderPipelineAsset.cs +++ b/managed/XCEngine.ScriptCore/Rendering/Universal/UniversalRenderPipelineAsset.cs @@ -30,6 +30,11 @@ namespace XCEngine.Rendering.Universal } } + protected override string GetPipelineRendererAssetKey() + { + return "BuiltinForward"; + } + private ScriptableRendererData ResolveRendererData() { if (rendererData == null) diff --git a/tests/Rendering/unit/test_camera_scene_renderer.cpp b/tests/Rendering/unit/test_camera_scene_renderer.cpp index 38b5095a..dec476d3 100644 --- a/tests/Rendering/unit/test_camera_scene_renderer.cpp +++ b/tests/Rendering/unit/test_camera_scene_renderer.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -22,6 +23,7 @@ #include #include "Rendering/Execution/Internal/CameraFrameGraph/SurfaceResolver.h" +#include "Rendering/Internal/RenderPipelineFactory.h" #include #include #include @@ -4508,6 +4510,39 @@ TEST(ScriptableRenderPipelineHost_Test, BindsCurrentPipelineRendererIntoStageRec host.GetPipelineRenderer()); } +TEST( + RenderPipelineFactory_Test, + NullPreferredAssetSkipsConfiguredManagedPipelineAssetWhenCreatingNativeSceneRenderer) { + Pipelines::ClearManagedRenderPipelineBridge(); + Pipelines::ClearConfiguredManagedRenderPipelineAssetDescriptor(); + + const Pipelines::ManagedRenderPipelineAssetDescriptor descriptor = { + "GameScripts", + "Gameplay", + "ManagedRenderPipelineProbeAsset" + }; + auto bridgeState = std::make_shared(); + Pipelines::SetManagedRenderPipelineBridge( + std::make_shared(bridgeState)); + Pipelines::SetConfiguredManagedRenderPipelineAssetDescriptor(descriptor); + + std::shared_ptr resolvedAsset = nullptr; + std::unique_ptr sceneRenderer = + Internal::CreateNativeSceneRendererFromAsset( + nullptr, + &resolvedAsset); + + ASSERT_NE(sceneRenderer, nullptr); + EXPECT_EQ(resolvedAsset, nullptr); + EXPECT_NE( + dynamic_cast(sceneRenderer.get()), + nullptr); + EXPECT_EQ(bridgeState->createAssetRuntimeCalls, 0); + + Pipelines::ClearConfiguredManagedRenderPipelineAssetDescriptor(); + Pipelines::ClearManagedRenderPipelineBridge(); +} + TEST( ScriptableRenderPipelineHost_Test, FallsBackToRendererWhenStageRecorderDeclinesRecording) { diff --git a/tests/scripting/test_mono_script_runtime.cpp b/tests/scripting/test_mono_script_runtime.cpp index 70f67bda..c4f0737b 100644 --- a/tests/scripting/test_mono_script_runtime.cpp +++ b/tests/scripting/test_mono_script_runtime.cpp @@ -2462,6 +2462,127 @@ TEST_F( nullptr); } +TEST_F( + MonoScriptRuntimeTest, + ScriptCoreUniversalRenderPipelineAssetExposesBuiltinForwardRendererAsset) { + const auto bridge = + XCEngine::Rendering::Pipelines::GetManagedRenderPipelineBridge(); + ASSERT_NE(bridge, nullptr); + + const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor descriptor = { + "XCEngine.RenderPipelines.Universal", + "XCEngine.Rendering.Universal", + "UniversalRenderPipelineAsset" + }; + + 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, + ManagedRenderPipelineBridgeFallsBackToDefaultSceneRecorderWhenBackendKeyIsUnknown) { + const auto bridge = + XCEngine::Rendering::Pipelines::GetManagedRenderPipelineBridge(); + ASSERT_NE(bridge, nullptr); + + const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor descriptor = { + "GameScripts", + "Gameplay", + "ManagedUnknownBackendRenderPipelineProbeAsset" + }; + + std::shared_ptr + assetRuntime = bridge->CreateAssetRuntime(descriptor); + ASSERT_NE(assetRuntime, nullptr); + EXPECT_EQ(assetRuntime->GetPipelineRendererAsset(), nullptr); + + std::unique_ptr recorder = + assetRuntime->CreateStageRecorder(); + ASSERT_NE(recorder, nullptr); + + const XCEngine::Rendering::RenderContext renderContext = {}; + ASSERT_TRUE(recorder->Initialize(renderContext)); + ASSERT_TRUE( + recorder->SupportsStageRenderGraph( + XCEngine::Rendering::CameraFrameStage::MainScene)); + + XCEngine::Rendering::RenderGraph graph; + XCEngine::Rendering::RenderGraphBuilder graphBuilder(graph); + XCEngine::Rendering::RenderGraphTextureDesc colorDesc = {}; + colorDesc.width = 64u; + colorDesc.height = 64u; + colorDesc.format = + static_cast( + XCEngine::RHI::Format::R8G8B8A8_UNorm); + XCEngine::Rendering::RenderGraphTextureDesc depthDesc = colorDesc; + depthDesc.format = + static_cast( + XCEngine::RHI::Format::D32_Float); + const XCEngine::Rendering::RenderGraphTextureHandle colorTarget = + graphBuilder.CreateTransientTexture("ManagedUnknownBackendColor", colorDesc); + const XCEngine::Rendering::RenderGraphTextureHandle depthTarget = + graphBuilder.CreateTransientTexture("ManagedUnknownBackendDepth", depthDesc); + + const XCEngine::Rendering::RenderSceneData sceneData = {}; + const XCEngine::Rendering::RenderSurface surface(64u, 64u); + bool executionSucceeded = true; + XCEngine::Rendering::RenderGraphBlackboard blackboard = {}; + const XCEngine::Rendering::RenderPipelineStageRenderGraphContext graphContext = { + graphBuilder, + "ManagedUnknownBackendMainScene", + XCEngine::Rendering::CameraFrameStage::MainScene, + renderContext, + sceneData, + surface, + nullptr, + nullptr, + XCEngine::RHI::ResourceStates::Common, + {}, + { colorTarget }, + depthTarget, + {}, + &executionSucceeded, + &blackboard + }; + + EXPECT_TRUE(recorder->RecordStageRenderGraph(graphContext)); + + XCEngine::Rendering::CompiledRenderGraph compiledGraph = {}; + XCEngine::Containers::String errorMessage; + ASSERT_TRUE( + XCEngine::Rendering::RenderGraphCompiler::Compile( + graph, + compiledGraph, + &errorMessage)) + << errorMessage.CStr(); + ASSERT_EQ(compiledGraph.GetPassCount(), 3u); + EXPECT_STREQ( + compiledGraph.GetPassName(0).CStr(), + "ManagedUnknownBackendMainScene.Opaque"); + EXPECT_STREQ( + compiledGraph.GetPassName(1).CStr(), + "ManagedUnknownBackendMainScene.Skybox"); + EXPECT_STREQ( + compiledGraph.GetPassName(2).CStr(), + "ManagedUnknownBackendMainScene.Transparent"); + + recorder->Shutdown(); +} + TEST_F( MonoScriptRuntimeTest, ManagedStageRecorderRecordsMainSceneThroughScriptableRenderContext) {