refactor(srp): let universal assets declare native backend keys

This commit is contained in:
2026-04-19 23:32:41 +08:00
parent 21b790c2f8
commit 9c8f2ae84c
9 changed files with 458 additions and 9 deletions

View File

@@ -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`
这条正式接缝。

View File

@@ -41,6 +41,18 @@ std::shared_ptr<const RenderPipelineAsset> CreateFallbackRenderPipelineAsset() {
return std::make_shared<Pipelines::ScriptableRenderPipelineHostAsset>();
}
std::shared_ptr<const RenderPipelineAsset> CreatePipelineRendererAssetByKey(
const std::string& key) {
if (key == "BuiltinForward") {
static const std::shared_ptr<const RenderPipelineAsset>
s_builtinForwardPipelineAsset =
std::make_shared<Pipelines::BuiltinForwardPipelineAsset>();
return s_builtinForwardPipelineAsset;
}
return nullptr;
}
std::shared_ptr<const RenderPipelineAsset> ResolveRenderPipelineAssetOrDefault(
std::shared_ptr<const RenderPipelineAsset> preferredAsset) {
if (preferredAsset != nullptr) {
@@ -112,12 +124,10 @@ std::unique_ptr<RenderPipeline> CreateRenderPipelineOrDefault(
std::unique_ptr<NativeSceneRenderer> CreateNativeSceneRendererFromAsset(
const std::shared_ptr<const RenderPipelineAsset>& preferredAsset,
std::shared_ptr<const RenderPipelineAsset>* outResolvedAsset) {
const std::shared_ptr<const RenderPipelineAsset> resolvedAsset =
ResolveRenderPipelineAssetOrDefault(preferredAsset);
if (std::unique_ptr<NativeSceneRenderer> sceneRenderer =
TryCreateNativeSceneRendererFromAsset(resolvedAsset)) {
TryCreateNativeSceneRendererFromAsset(preferredAsset)) {
if (outResolvedAsset != nullptr) {
*outResolvedAsset = resolvedAsset;
*outResolvedAsset = preferredAsset;
}
return sceneRenderer;
}

View File

@@ -1,5 +1,6 @@
#pragma once
#include <string>
#include <memory>
namespace XCEngine {
@@ -13,6 +14,8 @@ namespace Internal {
std::shared_ptr<const RenderPipelineAsset> CreateConfiguredRenderPipelineAsset();
std::shared_ptr<const RenderPipelineAsset> CreateFallbackRenderPipelineAsset();
std::shared_ptr<const RenderPipelineAsset> CreatePipelineRendererAssetByKey(
const std::string& key);
std::shared_ptr<const RenderPipelineAsset> ResolveRenderPipelineAssetOrDefault(
std::shared_ptr<const RenderPipelineAsset> preferredAsset);

View File

@@ -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<void> 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<const Rendering::RenderPipelineAsset>
m_pipelineRendererAsset = nullptr;
};
class MonoManagedRenderPipelineStageRecorder final
@@ -1088,10 +1093,42 @@ bool MonoManagedRenderPipelineAssetRuntime::TryGetDefaultFinalColorSettings(
std::shared_ptr<const Rendering::RenderPipelineAsset>
MonoManagedRenderPipelineAssetRuntime::GetPipelineRendererAsset() const {
static const std::shared_ptr<const Rendering::RenderPipelineAsset>
s_builtinForwardPipelineAsset =
std::make_shared<Rendering::Pipelines::BuiltinForwardPipelineAsset>();
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<MonoString*>(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:

View File

@@ -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
{

View File

@@ -22,6 +22,11 @@ namespace XCEngine.Rendering
{
return FinalColorSettings.CreateDefault();
}
protected virtual string GetPipelineRendererAssetKey()
{
return string.Empty;
}
}
}

View File

@@ -30,6 +30,11 @@ namespace XCEngine.Rendering.Universal
}
}
protected override string GetPipelineRendererAssetKey()
{
return "BuiltinForward";
}
private ScriptableRendererData ResolveRendererData()
{
if (rendererData == null)

View File

@@ -11,6 +11,7 @@
#include <XCEngine/Rendering/Execution/CameraRenderer.h>
#include <XCEngine/Rendering/Execution/RenderPipelineHost.h>
#include <XCEngine/Rendering/Graph/RenderGraph.h>
#include <XCEngine/Rendering/Pipelines/BuiltinForwardPipeline.h>
#include <XCEngine/Rendering/Pipelines/ManagedScriptableRenderPipelineAsset.h>
#include <XCEngine/Rendering/Pipelines/ScriptableRenderPipelineHost.h>
#include <XCEngine/Rendering/RenderPassGraphContract.h>
@@ -22,6 +23,7 @@
#include <XCEngine/Scene/Scene.h>
#include "Rendering/Execution/Internal/CameraFrameGraph/SurfaceResolver.h"
#include "Rendering/Internal/RenderPipelineFactory.h"
#include <functional>
#include <memory>
#include <string>
@@ -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<MockManagedRenderPipelineBridgeState>();
Pipelines::SetManagedRenderPipelineBridge(
std::make_shared<MockManagedRenderPipelineBridge>(bridgeState));
Pipelines::SetConfiguredManagedRenderPipelineAssetDescriptor(descriptor);
std::shared_ptr<const RenderPipelineAsset> resolvedAsset = nullptr;
std::unique_ptr<NativeSceneRenderer> sceneRenderer =
Internal::CreateNativeSceneRendererFromAsset(
nullptr,
&resolvedAsset);
ASSERT_NE(sceneRenderer, nullptr);
EXPECT_EQ(resolvedAsset, nullptr);
EXPECT_NE(
dynamic_cast<Pipelines::BuiltinForwardPipeline*>(sceneRenderer.get()),
nullptr);
EXPECT_EQ(bridgeState->createAssetRuntimeCalls, 0);
Pipelines::ClearConfiguredManagedRenderPipelineAssetDescriptor();
Pipelines::ClearManagedRenderPipelineBridge();
}
TEST(
ScriptableRenderPipelineHost_Test,
FallsBackToRendererWhenStageRecorderDeclinesRecording) {

View File

@@ -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<const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetRuntime>
assetRuntime = bridge->CreateAssetRuntime(descriptor);
ASSERT_NE(assetRuntime, nullptr);
const std::shared_ptr<const XCEngine::Rendering::RenderPipelineAsset>
rendererAsset = assetRuntime->GetPipelineRendererAsset();
ASSERT_NE(rendererAsset, nullptr);
std::unique_ptr<XCEngine::Rendering::RenderPipeline> pipeline =
rendererAsset->CreatePipeline();
ASSERT_NE(pipeline, nullptr);
EXPECT_NE(
dynamic_cast<XCEngine::Rendering::Pipelines::BuiltinForwardPipeline*>(
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<const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetRuntime>
assetRuntime = bridge->CreateAssetRuntime(descriptor);
ASSERT_NE(assetRuntime, nullptr);
EXPECT_EQ(assetRuntime->GetPipelineRendererAsset(), nullptr);
std::unique_ptr<XCEngine::Rendering::RenderPipelineStageRecorder> 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::Core::uint32>(
XCEngine::RHI::Format::R8G8B8A8_UNorm);
XCEngine::Rendering::RenderGraphTextureDesc depthDesc = colorDesc;
depthDesc.format =
static_cast<XCEngine::Core::uint32>(
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) {