feat(srp): add renderer-driven pipeline backbone

Introduce renderer-driven and renderer-backed managed pipeline base types in the Universal package.

Move shared renderer-data/default-renderer ownership out of UniversalRenderPipelineAsset, migrate probe assets onto the generic seam, and expose renderer recording/request context types for future SRP expansion.

Update scripting API-surface expectations and validate with build, unit tests, scripting tests, and old editor smoke.
This commit is contained in:
2026-04-20 02:05:17 +08:00
parent 9e6c473186
commit cd29c8b2bc
12 changed files with 575 additions and 197 deletions

View File

@@ -0,0 +1,161 @@
# SRP Renderer-Driven Pipeline 执行接缝计划 2026-04-20
## 1. 阶段目标
上一阶段已经把 `CameraFramePlan` 的 managed 规划接缝接进来了:
1. `ScriptableRenderPipelineAsset -> ConfigureCameraFramePlan`
2. `UniversalRenderPipelineAsset -> ScriptableRendererData -> ScriptableRendererFeature`
但执行层还有一个明显没收干净的问题:
1. `UniversalRenderPipelineAsset` 仍然自己持有一套 rendererData/defaultRenderer/runtime cleanup 逻辑
2. `UniversalRenderPipeline` 仍然自己做一层很薄的 renderer forwarding
3. managed renderer 虽然已经存在,但还没有被提炼成正式的 renderer-driven pipeline 骨架
这一阶段的目标就是把这层骨架正式抽出来:
1. 增加通用的 `RendererDrivenRenderPipeline`
2. 增加通用的 `RendererBackedRenderPipelineAsset`
3. 增加 `RendererBackedRenderPipeline`
4. 给 Universal 正式迁移到这套骨架
5. 补上 renderer execution 侧的上下文类型,给后续 SRP/URP 继续扩张留出稳定接口
---
## 2. 当前问题
### 2.1 Universal 还在重复持有“通用 renderer 资产”逻辑
当前 `UniversalRenderPipelineAsset` 里同时承担了:
1. `rendererDataList/defaultRendererIndex`
2. `GetRendererData/GetRenderer`
3. `ConfigureCameraRenderRequest`
4. `ConfigureCameraFramePlan`
5. `GetPipelineRendererAssetKey`
6. `ReleaseRendererDataRuntimeResources`
这些其实已经不是 `Universal` 特有逻辑而是“renderer-backed pipeline asset”的通用职责。
### 2.2 Pipeline 执行骨架还没有提升成可复用抽象
当前 `UniversalRenderPipeline` 本质上只是:
1. 拿默认 renderer
2. 转发 `SupportsStageRenderGraph`
3. 转发 `RecordStageRenderGraph`
这层 forwarding 逻辑应该收成通用的 renderer-driven pipeline 基类,而不是继续散落在首方包里。
### 2.3 缺少 renderer execution 层自己的正式上下文对象
当前 renderer 执行时主要直接吃:
1. `ScriptableRenderContext`
2. `RenderingData`
这能工作,但在 API 组织上还缺一个更明确的“renderer 正在做执行决策”的上下文层。
这一阶段先补最小集合:
1. `RendererRecordingContext`
2. `RendererCameraRequestContext`
不扩 raw native API只做 managed 组织骨架。
---
## 3. 实施方案
### 3.1 Universal 包新增通用 renderer-driven 基类
新增:
1. `RendererDrivenRenderPipeline`
2. `RendererBackedRenderPipeline`
3. `RendererBackedRenderPipelineAsset`
职责边界:
1. `RendererDrivenRenderPipeline` 负责 pipeline -> renderer execution delegation
2. `RendererBackedRenderPipeline` 负责从 asset 解析默认 renderer
3. `RendererBackedRenderPipelineAsset` 负责 rendererData 列表、默认选择、request/plan/backend key/runtime cleanup
### 3.2 Universal 迁移到通用骨架
重构:
1. `UniversalRenderPipelineAsset : RendererBackedRenderPipelineAsset`
2. `UniversalRenderPipeline : RendererBackedRenderPipeline`
3. `UniversalRenderPipelineAsset` 只保留 Universal 自己的默认 rendererData 初始化
### 3.3 补 execution context 类型
新增:
1. `RendererRecordingContext`
2. `RendererCameraRequestContext`
这一步先做到:
1. 类型正式存在
2. pipeline / renderer 内部可以开始用它们组织 execution seam
3. 不强行扩大 public raw recording surface
### 3.4 用测试锁住新骨架
验证点:
1. API probe 能看到新类型
2. Universal 行为不回退
3. 通用 renderer-backed 骨架至少被现有 Universal 主链真实经过
---
## 4. 实施步骤
### Step 1新增 renderer-driven 基类
目标:
1. 新增 `RendererDrivenRenderPipeline`
2. 新增 `RendererBackedRenderPipeline`
3. 新增 `RendererBackedRenderPipelineAsset`
### Step 2迁移 Universal
目标:
1. `UniversalRenderPipelineAsset` 下放通用逻辑
2. `UniversalRenderPipeline` 改成继承通用基类
3. 保持现有 runtime / cleanup / default renderer 行为一致
### Step 3补 execution context 类型
目标:
1. 新增 `RendererRecordingContext`
2. 新增 `RendererCameraRequestContext`
3. 在 renderer-driven 骨架里用起来
### Step 4补测试与验证
目标:
1. 更新 `ScriptableRenderContextApiSurfaceProbe`
2. 更新 `test_mono_script_runtime.cpp`
3. 编译 `rendering_unit_tests``scripting_tests``XCEditor`
4. 运行 old editor 10s 冒烟并检查新的 `SceneReady`
---
## 5. 验收标准
完成后应满足:
1. `Universal` 不再自己持有整套通用 renderer-backed asset 逻辑
2. 存在正式的 `RendererDrivenRenderPipeline / RendererBackedRenderPipeline / RendererBackedRenderPipelineAsset`
3. 存在正式的 `RendererRecordingContext / RendererCameraRequestContext`
4. `Universal` 现有 request / plan / record 行为不回退
5. `scripting_tests``rendering_unit_tests``XCEditor`、old editor 冒烟全部通过

View File

@@ -199,6 +199,11 @@ set(XCENGINE_RENDER_PIPELINES_UNIVERSAL_SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Rendering/Universal/RenderClearFlags.cs
${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Rendering/Universal/RenderEnvironmentMode.cs
${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Rendering/Universal/RenderPassEvent.cs
${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Rendering/Universal/RendererBackedRenderPipeline.cs
${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Rendering/Universal/RendererBackedRenderPipelineAsset.cs
${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Rendering/Universal/RendererCameraRequestContext.cs
${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Rendering/Universal/RendererDrivenRenderPipeline.cs
${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Rendering/Universal/RendererRecordingContext.cs
${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Rendering/Universal/RenderingData.cs
${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Rendering/Universal/RenderingDataResolver.cs
${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Rendering/Universal/ShadowData.cs

View File

@@ -1115,7 +1115,7 @@ namespace Gameplay
}
public sealed class ManagedRenderPipelineProbeAsset
: UniversalRenderPipelineAsset
: RendererBackedRenderPipelineAsset
{
public static int CreatePipelineCallCount;
@@ -1135,7 +1135,7 @@ namespace Gameplay
}
public sealed class ManagedPostProcessRenderPipelineProbeAsset
: UniversalRenderPipelineAsset
: RendererBackedRenderPipelineAsset
{
public ManagedPostProcessRenderPipelineProbeAsset()
{
@@ -1148,7 +1148,7 @@ namespace Gameplay
}
public sealed class ManagedUnknownBackendRenderPipelineProbeAsset
: UniversalRenderPipelineAsset
: RendererBackedRenderPipelineAsset
{
public ManagedUnknownBackendRenderPipelineProbeAsset()
{
@@ -1188,7 +1188,7 @@ namespace Gameplay
}
public sealed class ManagedDefaultRendererSelectionProbeAsset
: UniversalRenderPipelineAsset
: RendererBackedRenderPipelineAsset
{
public ManagedDefaultRendererSelectionProbeAsset()
{
@@ -1202,7 +1202,7 @@ namespace Gameplay
}
public sealed class ManagedInvalidDefaultRendererSelectionProbeAsset
: UniversalRenderPipelineAsset
: RendererBackedRenderPipelineAsset
{
public ManagedInvalidDefaultRendererSelectionProbeAsset()
{
@@ -1216,7 +1216,7 @@ namespace Gameplay
}
public sealed class ManagedRendererReuseProbeAsset
: UniversalRenderPipelineAsset
: RendererBackedRenderPipelineAsset
{
public ManagedRendererReuseProbeAsset()
{

View File

@@ -0,0 +1,24 @@
using XCEngine;
namespace XCEngine.Rendering.Universal
{
public class RendererBackedRenderPipeline
: RendererDrivenRenderPipeline
{
private readonly RendererBackedRenderPipelineAsset m_asset;
public RendererBackedRenderPipeline(
RendererBackedRenderPipelineAsset asset)
{
m_asset = asset;
}
protected override ScriptableRenderer ResolveRenderer(
RendererRecordingContext context)
{
return m_asset != null
? m_asset.GetDefaultRenderer()
: null;
}
}
}

View File

@@ -0,0 +1,220 @@
using System;
using XCEngine;
using XCEngine.Rendering;
namespace XCEngine.Rendering.Universal
{
public abstract class RendererBackedRenderPipelineAsset
: ScriptableRenderPipelineAsset
{
public ScriptableRendererData[] rendererDataList =
Array.Empty<ScriptableRendererData>();
public int defaultRendererIndex = 0;
protected RendererBackedRenderPipelineAsset()
{
}
protected override ScriptableRenderPipeline CreatePipeline()
{
return CreateRendererBackedPipeline();
}
protected override void ConfigureCameraRenderRequest(
CameraRenderRequestContext context)
{
ConfigureRendererCameraRequest(
new RendererCameraRequestContext(context));
ScriptableRendererData resolvedRendererData =
GetDefaultRendererData();
if (resolvedRendererData != null)
{
resolvedRendererData
.ConfigureCameraRenderRequestInstance(
context);
}
}
protected override void ConfigureCameraFramePlan(
ScriptableRenderPipelinePlanningContext context)
{
ScriptableRendererData resolvedRendererData =
GetDefaultRendererData();
if (resolvedRendererData != null)
{
resolvedRendererData
.ConfigureCameraFramePlanInstance(
context);
}
}
protected override string GetPipelineRendererAssetKey()
{
ScriptableRendererData resolvedRendererData =
GetDefaultRendererData();
return resolvedRendererData != null
? resolvedRendererData
.GetPipelineRendererAssetKeyInstance()
: string.Empty;
}
protected override void ReleaseRuntimeResources()
{
ReleaseRendererDataRuntimeResources();
}
protected virtual ScriptableRenderPipeline
CreateRendererBackedPipeline()
{
return new RendererBackedRenderPipeline(this);
}
protected virtual void ConfigureRendererCameraRequest(
RendererCameraRequestContext context)
{
}
protected virtual ScriptableRendererData
CreateDefaultRendererData()
{
return null;
}
internal ScriptableRendererData GetDefaultRendererData()
{
return GetRendererData(defaultRendererIndex);
}
internal ScriptableRenderer GetDefaultRenderer()
{
return GetRenderer(defaultRendererIndex);
}
internal ScriptableRendererData GetRendererData(
int rendererIndex)
{
EnsureRendererDataList();
if (rendererDataList.Length == 0)
{
return null;
}
int resolvedRendererIndex =
ResolveRendererIndex(rendererIndex);
if (resolvedRendererIndex < 0)
{
return null;
}
if (rendererDataList[resolvedRendererIndex] == null)
{
rendererDataList[resolvedRendererIndex] =
CreateDefaultRendererData();
}
return rendererDataList[resolvedRendererIndex];
}
internal ScriptableRenderer GetRenderer(
int rendererIndex)
{
ScriptableRendererData rendererData =
GetRendererData(rendererIndex);
return rendererData != null
? rendererData.GetRendererInstance()
: null;
}
protected internal void ReleaseRendererDataRuntimeResources()
{
EnsureRendererDataList();
for (int i = 0; i < rendererDataList.Length; ++i)
{
ScriptableRendererData rendererData =
rendererDataList[i];
if (rendererData == null ||
WasRendererDataReleasedEarlier(i))
{
continue;
}
rendererData.ReleaseRuntimeResourcesInstance();
}
}
private void EnsureRendererDataList()
{
if (rendererDataList != null &&
rendererDataList.Length > 0)
{
return;
}
ScriptableRendererData defaultRendererData =
CreateDefaultRendererData();
rendererDataList =
defaultRendererData != null
? new ScriptableRendererData[]
{
defaultRendererData
}
: Array.Empty<ScriptableRendererData>();
defaultRendererIndex = 0;
}
private bool WasRendererDataReleasedEarlier(
int rendererDataIndex)
{
ScriptableRendererData rendererData =
rendererDataList[rendererDataIndex];
for (int i = 0; i < rendererDataIndex; ++i)
{
if (object.ReferenceEquals(
rendererDataList[i],
rendererData))
{
return true;
}
}
return false;
}
private int ResolveRendererIndex(
int rendererIndex)
{
EnsureRendererDataList();
if (rendererDataList.Length == 0)
{
return -1;
}
if (rendererIndex < 0 ||
rendererIndex >= rendererDataList.Length)
{
return ResolveDefaultRendererIndex();
}
return rendererIndex;
}
private int ResolveDefaultRendererIndex()
{
EnsureRendererDataList();
if (rendererDataList.Length == 0)
{
return -1;
}
if (defaultRendererIndex < 0 ||
defaultRendererIndex >= rendererDataList.Length)
{
defaultRendererIndex = 0;
}
return defaultRendererIndex;
}
}
}

View File

@@ -0,0 +1,28 @@
using XCEngine.Rendering;
namespace XCEngine.Rendering.Universal
{
public sealed class RendererCameraRequestContext
{
private readonly CameraRenderRequestContext m_requestContext;
internal RendererCameraRequestContext(
CameraRenderRequestContext requestContext)
{
m_requestContext = requestContext;
}
public int renderedBaseCameraCount =>
m_requestContext != null
? m_requestContext.renderedBaseCameraCount
: 0;
public int renderedRequestCount =>
m_requestContext != null
? m_requestContext.renderedRequestCount
: 0;
internal CameraRenderRequestContext requestContext =>
m_requestContext;
}
}

View File

@@ -0,0 +1,53 @@
using XCEngine;
using XCEngine.Rendering;
namespace XCEngine.Rendering.Universal
{
public abstract class RendererDrivenRenderPipeline
: ScriptableRenderPipeline
{
protected RendererDrivenRenderPipeline()
{
}
protected sealed override bool SupportsStageRenderGraph(
CameraFrameStage stage)
{
return SupportsRendererRecording(
new RendererRecordingContext(stage));
}
protected sealed override bool RecordStageRenderGraph(
ScriptableRenderContext context)
{
if (context == null)
{
return false;
}
return RecordRenderer(
new RendererRecordingContext(context));
}
protected virtual bool SupportsRendererRecording(
RendererRecordingContext context)
{
ScriptableRenderer renderer =
ResolveRenderer(context);
return renderer != null &&
renderer.SupportsRendererRecording(context);
}
protected virtual bool RecordRenderer(
RendererRecordingContext context)
{
ScriptableRenderer renderer =
ResolveRenderer(context);
return renderer != null &&
renderer.RecordRenderer(context);
}
protected abstract ScriptableRenderer ResolveRenderer(
RendererRecordingContext context);
}
}

View File

@@ -0,0 +1,48 @@
using XCEngine;
using XCEngine.Rendering;
namespace XCEngine.Rendering.Universal
{
public sealed class RendererRecordingContext
{
private readonly ScriptableRenderContext m_renderContext;
private readonly RenderingData m_renderingData;
internal RendererRecordingContext(
CameraFrameStage stage)
: this(
null,
new RenderingData(stage))
{
}
internal RendererRecordingContext(
ScriptableRenderContext renderContext)
: this(
renderContext,
renderContext != null
? new RenderingData(renderContext)
: new RenderingData(CameraFrameStage.MainScene))
{
}
private RendererRecordingContext(
ScriptableRenderContext renderContext,
RenderingData renderingData)
{
m_renderContext = renderContext;
m_renderingData =
renderingData ??
new RenderingData(CameraFrameStage.MainScene);
}
public CameraFrameStage stage =>
m_renderingData.stage;
public RenderingData renderingData =>
m_renderingData;
internal ScriptableRenderContext renderContext =>
m_renderContext;
}
}

View File

@@ -77,6 +77,22 @@ namespace XCEngine.Rendering.Universal
{
}
internal bool SupportsRendererRecording(
RendererRecordingContext context)
{
return context != null &&
SupportsStageRenderGraph(context.stage);
}
internal bool RecordRenderer(
RendererRecordingContext context)
{
return context != null &&
context.renderContext != null &&
RecordStageRenderGraph(
context.renderContext);
}
protected internal virtual bool SupportsStageRenderGraph(
CameraFrameStage stage)
{

View File

@@ -4,37 +4,12 @@ using XCEngine.Rendering;
namespace XCEngine.Rendering.Universal
{
internal sealed class UniversalRenderPipeline
: ScriptableRenderPipeline
: RendererBackedRenderPipeline
{
private readonly UniversalRenderPipelineAsset m_asset;
public UniversalRenderPipeline(
UniversalRenderPipelineAsset asset)
: base(asset)
{
m_asset = asset;
}
protected override bool SupportsStageRenderGraph(
CameraFrameStage stage)
{
ScriptableRenderer renderer = GetDefaultRenderer();
return renderer != null &&
renderer.SupportsStageRenderGraph(stage);
}
protected override bool RecordStageRenderGraph(
ScriptableRenderContext context)
{
ScriptableRenderer renderer = GetDefaultRenderer();
return renderer != null &&
renderer.RecordStageRenderGraph(context);
}
private ScriptableRenderer GetDefaultRenderer()
{
return m_asset != null
? m_asset.GetDefaultRenderer()
: null;
}
}
}

View File

@@ -4,170 +4,18 @@ using XCEngine.Rendering;
namespace XCEngine.Rendering.Universal
{
public class UniversalRenderPipelineAsset
: ScriptableRenderPipelineAsset
: RendererBackedRenderPipelineAsset
{
public ScriptableRendererData[] rendererDataList =
new ScriptableRendererData[]
{
new UniversalRendererData()
};
public int defaultRendererIndex = 0;
protected override ScriptableRenderPipeline CreatePipeline()
protected override ScriptableRenderPipeline
CreateRendererBackedPipeline()
{
return GetDefaultRendererData() != null
? new UniversalRenderPipeline(this)
: null;
return new UniversalRenderPipeline(this);
}
protected override void ConfigureCameraRenderRequest(
CameraRenderRequestContext context)
protected override ScriptableRendererData
CreateDefaultRendererData()
{
ScriptableRendererData resolvedRendererData =
GetDefaultRendererData();
if (resolvedRendererData != null)
{
resolvedRendererData.ConfigureCameraRenderRequestInstance(
context);
}
}
protected override void ConfigureCameraFramePlan(
ScriptableRenderPipelinePlanningContext context)
{
ScriptableRendererData resolvedRendererData =
GetDefaultRendererData();
if (resolvedRendererData != null)
{
resolvedRendererData.ConfigureCameraFramePlanInstance(
context);
}
}
protected override string GetPipelineRendererAssetKey()
{
ScriptableRendererData resolvedRendererData =
GetDefaultRendererData();
return resolvedRendererData != null
? resolvedRendererData.GetPipelineRendererAssetKeyInstance()
: string.Empty;
}
protected override void ReleaseRuntimeResources()
{
ReleaseRendererDataRuntimeResources();
}
internal ScriptableRendererData GetDefaultRendererData()
{
return GetRendererData(defaultRendererIndex);
}
internal ScriptableRenderer GetDefaultRenderer()
{
return GetRenderer(defaultRendererIndex);
}
internal ScriptableRendererData GetRendererData(
int rendererIndex)
{
EnsureRendererDataList();
int resolvedRendererIndex =
ResolveRendererIndex(rendererIndex);
if (rendererDataList[resolvedRendererIndex] == null)
{
rendererDataList[resolvedRendererIndex] =
new UniversalRendererData();
}
return rendererDataList[resolvedRendererIndex];
}
internal ScriptableRenderer GetRenderer(
int rendererIndex)
{
ScriptableRendererData rendererData =
GetRendererData(rendererIndex);
return rendererData != null
? rendererData.GetRendererInstance()
: null;
}
private void EnsureRendererDataList()
{
if (rendererDataList != null &&
rendererDataList.Length > 0)
{
return;
}
rendererDataList =
new ScriptableRendererData[]
{
new UniversalRendererData()
};
defaultRendererIndex = 0;
}
protected void ReleaseRendererDataRuntimeResources()
{
EnsureRendererDataList();
for (int i = 0; i < rendererDataList.Length; ++i)
{
ScriptableRendererData rendererData =
rendererDataList[i];
if (rendererData == null ||
WasRendererDataReleasedEarlier(i))
{
continue;
}
rendererData.ReleaseRuntimeResourcesInstance();
}
}
private bool WasRendererDataReleasedEarlier(
int rendererDataIndex)
{
ScriptableRendererData rendererData =
rendererDataList[rendererDataIndex];
for (int i = 0; i < rendererDataIndex; ++i)
{
if (object.ReferenceEquals(
rendererDataList[i],
rendererData))
{
return true;
}
}
return false;
}
private int ResolveRendererIndex(
int rendererIndex)
{
EnsureRendererDataList();
if (rendererIndex < 0 ||
rendererIndex >= rendererDataList.Length)
{
return ResolveDefaultRendererIndex();
}
return rendererIndex;
}
private int ResolveDefaultRendererIndex()
{
EnsureRendererDataList();
if (defaultRendererIndex < 0 ||
defaultRendererIndex >= rendererDataList.Length)
{
defaultRendererIndex = 0;
}
return defaultRendererIndex;
return new UniversalRendererData();
}
}
}

View File

@@ -1315,11 +1315,11 @@ TEST_F(
EXPECT_TRUE(hasPublicPipelineAssetConfigureCameraFramePlan);
EXPECT_TRUE(hasPlanningContextType);
EXPECT_TRUE(hasRendererFeatureConfigureCameraFramePlan);
EXPECT_FALSE(hasRendererRecordingContextType);
EXPECT_FALSE(hasRendererCameraRequestContextType);
EXPECT_FALSE(hasRendererBackedRenderPipelineAssetType);
EXPECT_FALSE(hasRendererBackedRenderPipelineType);
EXPECT_FALSE(hasRendererDrivenRenderPipelineType);
EXPECT_TRUE(hasRendererRecordingContextType);
EXPECT_TRUE(hasRendererCameraRequestContextType);
EXPECT_TRUE(hasRendererBackedRenderPipelineAssetType);
EXPECT_TRUE(hasRendererBackedRenderPipelineType);
EXPECT_TRUE(hasRendererDrivenRenderPipelineType);
EXPECT_FALSE(hasPublicRendererSupportsStageRenderGraph);
EXPECT_FALSE(hasPublicRendererRecordStageRenderGraph);
}