refactor(srp): add renderer backend registry seam

This commit is contained in:
2026-04-20 12:47:25 +08:00
parent 59f2249e07
commit 10b092d467
6 changed files with 382 additions and 9 deletions

View File

@@ -0,0 +1,119 @@
# SRP RendererBackend 注册与多 Key 接缝计划 2026-04-20
## 1. 阶段目标
上一阶段已经把:
`ScriptableRenderPipelineAsset -> ManagedRenderPipelineAssetRuntime`
这一层的失效与重建接缝补齐了。
当前新的主矛盾不在生命周期,而在 backend ownership
1. managed SRP/Universal 已经能声明 backend key
2. native factory 仍然基本停留在单 key 硬编码
3. future SRP/URP 想挂更多 native backend 时,没有正式 registry
这一阶段的目标,就是把:
`managed backend key -> native renderer backend asset`
从临时硬编码改成正式 registry seam。
---
## 2. 当前问题
### 2.1 native factory 仍是单点硬编码
当前 `CreatePipelineRendererAssetByKey()` 仍然是:
1. 只认 `BuiltinForward`
2. 直接 `if` 分支返回
这意味着后续每多一个 backend都要继续往 factory 里堆硬编码。
### 2.2 多 key 语义还没有真正被锁住
managed 侧虽然已经允许不同 asset / renderer data 返回不同 key
但 native 侧并没有一套正式的“注册、查询、失败回退”模型。
### 2.3 现有测试只锁住单 key 与 unknown fallback
当前已经锁住:
1. `BuiltinForward` 能解析
2. missing key 会 fallback
但还没有锁住:
1. 可以注册额外 key
2. managed asset 返回额外 key 之后native runtime 仍能正常解析 backend asset
---
## 3. 实施方案
### 3.1 在 native factory 引入正式 backend registry
新增 registry 职责:
1. 默认注册 builtin backend
2. 支持额外 key 注册
3. 支持通过 key 查询 backend asset factory
4. unknown key 保持安全失败
### 3.2 builtin forward 改为 registry 默认项
`BuiltinForward` 不再作为 `if` 特判逻辑散落在 factory 主流程里,
而是作为 registry 初始化时的默认注册项。
### 3.3 用测试 alias key 锁住多 key seam
本阶段不强行引入第二套真实 renderer backend
而是通过测试注册一个 alias key映射到 builtin forward asset
用来验证:
1. registry 真的支持额外 key
2. managed runtime 真的沿 key seam 解析 native backend
---
## 4. 实施步骤
### Step 1补 native registry API
1. 修改 `engine/src/Rendering/Internal/RenderPipelineFactory.h/.cpp`
2. 引入 backend asset factory registry
3. builtin forward 作为默认注册项
### Step 2补 unit tests
1. 修改 `tests/Rendering/unit/test_camera_scene_renderer.cpp`
2. 锁住 custom alias key 的注册、解析、native scene renderer 创建
### Step 3补 scripting probe 与 runtime test
1. 修改 `managed/GameScripts/RenderPipelineApiProbe.cs`
2. 修改 `tests/scripting/test_mono_script_runtime.cpp`
3. 锁住 managed asset 返回 alias key 后runtime 仍能解析 builtin forward backend
### Step 4验证与收口
1. 编译 `XCEditor`
2. 运行 `rendering_unit_tests`
3. 运行 `scripting_tests`
4. 旧 editor 冒烟 10s
5. 阶段完成后归档 plan、提交推送
---
## 5. 验收标准
完成后应满足:
1. native backend key 解析走正式 registry而不是单点 `if`
2. `BuiltinForward` 作为默认注册项保留
3. 可以额外注册 alias key 并解析出 backend asset
4. managed asset 返回 alias key 时native runtime 仍能正常工作
5. 编译、单测、冒烟全部通过

View File

@@ -4,12 +4,55 @@
#include "Rendering/Pipelines/ManagedScriptableRenderPipelineAsset.h"
#include "Rendering/Pipelines/ScriptableRenderPipelineHost.h"
#include <mutex>
#include <unordered_map>
#include <unordered_set>
namespace XCEngine {
namespace Rendering {
namespace Internal {
namespace {
std::shared_ptr<const RenderPipelineAsset>
CreateBuiltinForwardPipelineRendererAsset() {
static const std::shared_ptr<const RenderPipelineAsset>
s_builtinForwardPipelineAsset =
std::make_shared<Pipelines::BuiltinForwardPipelineAsset>();
return s_builtinForwardPipelineAsset;
}
using PipelineRendererAssetRegistry =
std::unordered_map<std::string, PipelineRendererAssetFactory>;
PipelineRendererAssetRegistry& GetPipelineRendererAssetRegistry() {
static PipelineRendererAssetRegistry registry = {};
return registry;
}
std::unordered_set<std::string>& GetBuiltinPipelineRendererAssetKeys() {
static std::unordered_set<std::string> builtinKeys = {};
return builtinKeys;
}
std::mutex& GetPipelineRendererAssetRegistryMutex() {
static std::mutex mutex;
return mutex;
}
void EnsureBuiltinPipelineRendererAssetRegistryInitialized() {
static const bool initialized = []() {
PipelineRendererAssetRegistry& registry =
GetPipelineRendererAssetRegistry();
registry.emplace(
"BuiltinForward",
&CreateBuiltinForwardPipelineRendererAsset);
GetBuiltinPipelineRendererAssetKeys().insert("BuiltinForward");
return true;
}();
(void)initialized;
}
std::unique_ptr<NativeSceneRenderer> TryCreateNativeSceneRendererFromAsset(
const std::shared_ptr<const RenderPipelineAsset>& asset) {
if (asset == nullptr) {
@@ -41,16 +84,64 @@ 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;
bool RegisterPipelineRendererAssetFactory(
const std::string& key,
PipelineRendererAssetFactory factory) {
if (key.empty() || !factory) {
return false;
}
return nullptr;
EnsureBuiltinPipelineRendererAssetRegistryInitialized();
std::lock_guard<std::mutex> lock(
GetPipelineRendererAssetRegistryMutex());
PipelineRendererAssetRegistry& registry =
GetPipelineRendererAssetRegistry();
if (registry.find(key) != registry.end()) {
return false;
}
registry.emplace(key, std::move(factory));
return true;
}
bool UnregisterPipelineRendererAssetFactory(const std::string& key) {
if (key.empty()) {
return false;
}
EnsureBuiltinPipelineRendererAssetRegistryInitialized();
std::lock_guard<std::mutex> lock(
GetPipelineRendererAssetRegistryMutex());
if (GetBuiltinPipelineRendererAssetKeys().find(key) !=
GetBuiltinPipelineRendererAssetKeys().end()) {
return false;
}
PipelineRendererAssetRegistry& registry =
GetPipelineRendererAssetRegistry();
return registry.erase(key) != 0u;
}
std::shared_ptr<const RenderPipelineAsset> CreatePipelineRendererAssetByKey(
const std::string& key) {
if (key.empty()) {
return nullptr;
}
EnsureBuiltinPipelineRendererAssetRegistryInitialized();
std::lock_guard<std::mutex> lock(
GetPipelineRendererAssetRegistryMutex());
const PipelineRendererAssetRegistry& registry =
GetPipelineRendererAssetRegistry();
const auto it = registry.find(key);
if (it == registry.end() || !it->second) {
return nullptr;
}
return it->second();
}
std::shared_ptr<const RenderPipelineAsset> ResolveRenderPipelineAssetOrDefault(

View File

@@ -1,7 +1,8 @@
#pragma once
#include <string>
#include <functional>
#include <memory>
#include <string>
namespace XCEngine {
namespace Rendering {
@@ -12,8 +13,15 @@ class RenderPipelineAsset;
namespace Internal {
using PipelineRendererAssetFactory =
std::function<std::shared_ptr<const RenderPipelineAsset>()>;
std::shared_ptr<const RenderPipelineAsset> CreateConfiguredRenderPipelineAsset();
std::shared_ptr<const RenderPipelineAsset> CreateFallbackRenderPipelineAsset();
bool RegisterPipelineRendererAssetFactory(
const std::string& key,
PipelineRendererAssetFactory factory);
bool UnregisterPipelineRendererAssetFactory(const std::string& key);
std::shared_ptr<const RenderPipelineAsset> CreatePipelineRendererAssetByKey(
const std::string& key);

View File

@@ -878,6 +878,20 @@ namespace Gameplay
}
}
internal sealed class ManagedBuiltinForwardAliasProbeRendererData
: ProbeRendererData
{
protected override ScriptableRenderer CreateProbeRenderer()
{
return new ManagedRenderPipelineProbe();
}
protected override string GetPipelineRendererAssetKey()
{
return "BuiltinForwardAlias";
}
}
internal sealed class ManagedRendererReuseProbeRendererData
: ProbeRendererData
{
@@ -1235,6 +1249,18 @@ namespace Gameplay
}
}
public sealed class ManagedBuiltinForwardAliasRenderPipelineProbeAsset
: RendererBackedRenderPipelineAsset
{
public ManagedBuiltinForwardAliasRenderPipelineProbeAsset()
{
rendererDataList = new ScriptableRendererData[]
{
new ManagedBuiltinForwardAliasProbeRendererData()
};
}
}
public sealed class ManagedUniversalRenderPipelineProbeAsset
: UniversalRenderPipelineAsset
{

View File

@@ -50,6 +50,38 @@ CameraFrameStageSourceBinding ResolveStageSourceBinding(
return ResolveCameraFrameStageSourceBinding(stage, plan);
}
std::shared_ptr<const RenderPipelineAsset>
CreateBuiltinForwardPipelineRendererAssetForTest() {
return std::make_shared<Pipelines::BuiltinForwardPipelineAsset>();
}
class ScopedPipelineRendererAssetFactoryRegistration final {
public:
ScopedPipelineRendererAssetFactoryRegistration(
std::string key,
Internal::PipelineRendererAssetFactory factory)
: m_key(std::move(key))
, m_registered(
Internal::RegisterPipelineRendererAssetFactory(
m_key,
std::move(factory))) {
}
~ScopedPipelineRendererAssetFactoryRegistration() {
if (m_registered) {
(void)Internal::UnregisterPipelineRendererAssetFactory(m_key);
}
}
bool IsRegistered() const {
return m_registered;
}
private:
std::string m_key;
bool m_registered = false;
};
struct MockPipelineState {
int initializeCalls = 0;
int shutdownCalls = 0;
@@ -4543,6 +4575,31 @@ TEST(
Pipelines::ClearManagedRenderPipelineBridge();
}
TEST(
RenderPipelineFactory_Test,
RegisteredAliasKeyResolvesPipelineRendererAssetAndNativeSceneRenderer) {
ScopedPipelineRendererAssetFactoryRegistration registration(
"BuiltinForwardAlias",
&CreateBuiltinForwardPipelineRendererAssetForTest);
ASSERT_TRUE(registration.IsRegistered());
std::shared_ptr<const RenderPipelineAsset> rendererAsset =
Internal::CreatePipelineRendererAssetByKey("BuiltinForwardAlias");
ASSERT_NE(rendererAsset, nullptr);
std::shared_ptr<const RenderPipelineAsset> resolvedAsset = nullptr;
std::unique_ptr<NativeSceneRenderer> sceneRenderer =
Internal::CreateNativeSceneRendererFromAsset(
rendererAsset,
&resolvedAsset);
ASSERT_NE(sceneRenderer, nullptr);
EXPECT_EQ(resolvedAsset, rendererAsset);
EXPECT_NE(
dynamic_cast<Pipelines::BuiltinForwardPipeline*>(sceneRenderer.get()),
nullptr);
}
TEST(
ScriptableRenderPipelineHost_Test,
FallsBackToRendererWhenStageRecorderDeclinesRecording) {

View File

@@ -45,6 +45,8 @@
#include <XCEngine/Scripting/ScriptComponent.h>
#include <XCEngine/Scripting/ScriptEngine.h>
#include "Rendering/Internal/RenderPipelineFactory.h"
#include <algorithm>
#include <array>
#include <cstdint>
@@ -75,6 +77,40 @@ void ExpectVector4Near(const XCEngine::Math::Vector4& actual, const XCEngine::Ma
EXPECT_NEAR(actual.w, expected.w, tolerance);
}
std::shared_ptr<const XCEngine::Rendering::RenderPipelineAsset>
CreateBuiltinForwardPipelineRendererAssetForTest() {
return std::make_shared<
XCEngine::Rendering::Pipelines::BuiltinForwardPipelineAsset>();
}
class ScopedPipelineRendererAssetFactoryRegistration final {
public:
ScopedPipelineRendererAssetFactoryRegistration(
std::string key,
XCEngine::Rendering::Internal::PipelineRendererAssetFactory factory)
: m_key(std::move(key))
, m_registered(
XCEngine::Rendering::Internal::RegisterPipelineRendererAssetFactory(
m_key,
std::move(factory))) {
}
~ScopedPipelineRendererAssetFactoryRegistration() {
if (m_registered) {
(void)XCEngine::Rendering::Internal::
UnregisterPipelineRendererAssetFactory(m_key);
}
}
bool IsRegistered() const {
return m_registered;
}
private:
std::string m_key;
bool m_registered = false;
};
class CapturingLogSink final : public XCEngine::Debug::ILogSink {
public:
void Log(const XCEngine::Debug::LogEntry& entry) override {
@@ -2950,6 +2986,42 @@ TEST_F(
nullptr);
}
TEST_F(
MonoScriptRuntimeTest,
ManagedRenderPipelineBridgeResolvesRegisteredAliasBackendKey) {
ScopedPipelineRendererAssetFactoryRegistration registration(
"BuiltinForwardAlias",
&CreateBuiltinForwardPipelineRendererAssetForTest);
ASSERT_TRUE(registration.IsRegistered());
const auto bridge =
XCEngine::Rendering::Pipelines::GetManagedRenderPipelineBridge();
ASSERT_NE(bridge, nullptr);
const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor
descriptor = {
"GameScripts",
"Gameplay",
"ManagedBuiltinForwardAliasRenderPipelineProbeAsset"
};
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,
ManagedRenderPipelineBridgeUsesDefaultRendererSelectionForNativeBackendAsset) {