diff --git a/docs/used/SRP_Universal_RendererData归位计划_完成归档_2026-04-19.md b/docs/used/SRP_Universal_RendererData归位计划_完成归档_2026-04-19.md new file mode 100644 index 00000000..2f2db3e5 --- /dev/null +++ b/docs/used/SRP_Universal_RendererData归位计划_完成归档_2026-04-19.md @@ -0,0 +1,168 @@ +# SRP Universal RendererData归位计划 2026-04-19 + +## 1. 阶段目标 + +上一阶段已经建立了: + +`managed asset -> backend key -> native RenderPipelineAsset` + +这条正式接缝,但当前 backend ownership 仍然挂在: + +`UniversalRenderPipelineAsset` + +这和 Unity/URP 的真实职责边界还差半步。Unity 里真正决定 renderer 组织方式的不是 pipeline asset 自己,而是它选中的: + +`ScriptableRendererData` + +本阶段目标就是把这半步补完: + +1. 让 native backend key 的声明责任从 `UniversalRenderPipelineAsset` 下放到 `ScriptableRendererData` +2. 让 `UniversalRenderPipelineAsset` 改成通过“默认 renderer 选择”来驱动 +3. 把当前单个 `rendererData` 形态整理成更接近 Unity 的 renderer data 列表 + 默认索引 +4. 保持 C++ bridge 仍然只面向 `RenderPipelineAsset`,不把 Unity 风格 API 细节直接污染到 native 层 + +--- + +## 2. 当前问题 + +### 2.1 ownership 位置仍然偏高 + +现在的结构是: + +`UniversalRenderPipelineAsset.GetPipelineRendererAssetKey() -> "BuiltinForward"` + +这意味着: + +1. backend ownership 仍然是 pipeline asset 在声明 +2. renderer data 虽然已经负责 renderer/pass/features 组合,但还没有负责 native renderer backend 语义 +3. 未来如果一个 pipeline asset 下面要切多个 renderer data,就会开始和 Unity 的职责边界不一致 + +### 2.2 Universal 目前还是“单 rendererData” + +当前 `UniversalRenderPipelineAsset` 只有: + +`rendererData` + +而 Unity/URP 的主流组织方式是: + +1. renderer data 列表 +2. 默认 renderer 索引 +3. pipeline asset 通过默认 renderer 解析当前使用的 renderer data + +现在如果不先把这个模型摆正,后面继续往 SRP/URP 方向扩时,很多测试、序列化、编辑器 UI 都会继续围着单字段长。 + +### 2.3 unknown backend 的探针位置也偏了 + +目前 unknown backend 探针还是通过: + +`ManagedUnknownBackendRenderPipelineProbeAsset` + +override `GetPipelineRendererAssetKey()` + +来模拟错误 backend。 + +如果本阶段要把 ownership 归位到 renderer data,这个探针也应该一起下沉,否则测试覆盖的是旧职责边界,不是新结构。 + +--- + +## 3. 本阶段方案 + +### 方案核心 + +把 backend ownership 下沉为: + +`ScriptableRendererData -> backend key` + +然后由: + +`UniversalRenderPipelineAsset -> ResolveDefaultRendererData() -> rendererData.GetPipelineRendererAssetKeyInstance()` + +这样做之后: + +1. `UniversalRenderPipelineAsset` 只负责“选哪个 renderer data” +2. `ScriptableRendererData` 负责“这个 renderer data 需要哪个 native backend” +3. Mono runtime / native factory 保持现有桥接方式,不需要再扩散 Unity 细节 + +### 数据形态 + +`UniversalRenderPipelineAsset` + +改成: + +1. `ScriptableRendererData[] rendererDataList` +2. `int defaultRendererIndex` +3. 内部统一通过 `ResolveDefaultRendererData()` 取当前默认 renderer data + +本阶段不做完整编辑器序列化工作流,只把运行时模型和测试模型摆正。 + +--- + +## 4. 实施步骤 + +### Step 1:给 ScriptableRendererData 增加 backend key seam + +目标: + +1. 在 `ScriptableRendererData` 增加受保护虚方法 +2. 默认返回空 key +3. `UniversalRendererData` override 返回 `BuiltinForward` + +### Step 2:把 UniversalRenderPipelineAsset 改成默认 renderer 选择模型 + +目标: + +1. 删除单 `rendererData` 字段 +2. 增加 `rendererDataList` 和 `defaultRendererIndex` +3. `CreatePipeline()`、`ConfigureCameraRenderRequest()`、`GetPipelineRendererAssetKey()` 都通过默认 renderer data 解析 +4. 默认空/越界/null 情况下仍然收敛到一个可用的 `UniversalRendererData` + +### Step 3:把 probe 与测试一起归位 + +目标: + +1. 所有 probe asset 改成使用 `rendererDataList` +2. unknown backend 探针改成 custom renderer data 返回错误 key +3. 保证现有 managed stage recorder / host fallback 语义不回退 + +### Step 4:补测试 + +目标: + +1. scripting tests 覆盖 default renderer data -> builtin forward backend +2. scripting tests 覆盖 unknown backend key 来源于 renderer data 时,host/recorder 仍能本地 fallback +3. 如果需要,再补 rendererDataList/defaultRendererIndex 的选择回归 + +### Step 5:阶段验证与收口 + +目标: + +1. 编译 `rendering_unit_tests`、`scripting_tests`、`XCEditor` +2. 运行相关单测 +3. 跑旧版 `XCEditor` 10 秒冒烟并验证新 `SceneReady` +4. 归档 plan、提交、推送 + +--- + +## 5. 验收标准 + +本阶段完成后应满足: + +1. Universal 的 native backend ownership 从 pipeline asset 下放到 renderer data +2. Universal pipeline asset 的职责变成“选择默认 renderer” +3. 探针和测试不再依赖旧的 asset-level backend override +4. C++ bridge 不需要理解 Unity 风格 renderer data 细节,仍然保持桥接层定位 +5. 当前结构继续朝 Unity 风格 `SRP Asset -> RendererData -> Renderer/Features/Passes` 演进 + +--- + +## 6. 本阶段不做的事 + +本阶段明确不做: + +1. renderer data 的完整工程资产化 UI +2. 多 renderer 的编辑器切换体验 +3. deferred renderer +4. lightmap / baking +5. 用户自定义 backend registry 的公开 API + +本阶段只做职责归位和运行时模型整理。 diff --git a/managed/GameScripts/RenderPipelineApiProbe.cs b/managed/GameScripts/RenderPipelineApiProbe.cs index f2e62b2f..5fce0a68 100644 --- a/managed/GameScripts/RenderPipelineApiProbe.cs +++ b/managed/GameScripts/RenderPipelineApiProbe.cs @@ -818,7 +818,7 @@ namespace Gameplay } internal abstract class ProbeRendererData - : ScriptableRendererData + : UniversalRendererData { protected sealed override ScriptableRenderer CreateRenderer() { @@ -864,6 +864,20 @@ namespace Gameplay } } + internal sealed class ManagedUnknownBackendProbeRendererData + : ProbeRendererData + { + protected override ScriptableRenderer CreateProbeRenderer() + { + return new ManagedRenderPipelineProbe(); + } + + protected override string GetPipelineRendererAssetKey() + { + return "MissingBackend"; + } + } + internal sealed class ManagedPlannedFullscreenRenderPipelineProbeRendererData : ProbeRendererData { @@ -906,7 +920,10 @@ namespace Gameplay public ManagedRenderPipelineProbeAsset() { - rendererData = new ManagedRenderPipelineProbeRendererData(); + rendererDataList = new ScriptableRendererData[] + { + new ManagedRenderPipelineProbeRendererData() + }; } protected override ScriptableRenderPipeline CreatePipeline() @@ -921,8 +938,11 @@ namespace Gameplay { public ManagedPostProcessRenderPipelineProbeAsset() { - rendererData = - new ManagedPostProcessRenderPipelineProbeRendererData(); + rendererDataList = + new ScriptableRendererData[] + { + new ManagedPostProcessRenderPipelineProbeRendererData() + }; } } @@ -931,12 +951,10 @@ namespace Gameplay { public ManagedUnknownBackendRenderPipelineProbeAsset() { - rendererData = new ManagedRenderPipelineProbeRendererData(); - } - - protected override string GetPipelineRendererAssetKey() - { - return "MissingBackend"; + rendererDataList = new ScriptableRendererData[] + { + new ManagedUnknownBackendProbeRendererData() + }; } } @@ -948,20 +966,37 @@ namespace Gameplay public ManagedUniversalRenderPipelineProbeAsset() { - rendererData = CreateRendererData(); + rendererDataList = CreateRendererDataList(); } protected override ScriptableRenderPipeline CreatePipeline() { CreatePipelineCallCount++; - rendererData = CreateRendererData(); + rendererDataList = CreateRendererDataList(); return base.CreatePipeline(); } - private ScriptableRendererData CreateRendererData() + private ScriptableRendererData[] CreateRendererDataList() { - return new ManagedUniversalRenderPipelineProbeRendererData( - postProcessScale); + return new ScriptableRendererData[] + { + new ManagedUniversalRenderPipelineProbeRendererData( + postProcessScale) + }; + } + } + + public sealed class ManagedDefaultRendererSelectionProbeAsset + : UniversalRenderPipelineAsset + { + public ManagedDefaultRendererSelectionProbeAsset() + { + rendererDataList = new ScriptableRendererData[] + { + new ManagedUnknownBackendProbeRendererData(), + new ManagedRenderPipelineProbeRendererData() + }; + defaultRendererIndex = 1; } } @@ -970,8 +1005,11 @@ namespace Gameplay { public ManagedPlannedFullscreenRenderPipelineProbeAsset() { - rendererData = - new ManagedPlannedFullscreenRenderPipelineProbeRendererData(); + rendererDataList = + new ScriptableRendererData[] + { + new ManagedPlannedFullscreenRenderPipelineProbeRendererData() + }; } } @@ -998,7 +1036,10 @@ namespace Gameplay { public ManagedCameraRequestConfiguredRenderPipelineProbeAsset() { - rendererData = new ManagedCameraRequestConfiguredRendererData(); + rendererDataList = new ScriptableRendererData[] + { + new ManagedCameraRequestConfiguredRendererData() + }; } } @@ -1007,8 +1048,11 @@ namespace Gameplay { public ManagedRenderContextCameraDataProbeAsset() { - rendererData = - new ManagedRenderContextCameraDataProbeRendererData(); + rendererDataList = + new ScriptableRendererData[] + { + new ManagedRenderContextCameraDataProbeRendererData() + }; } } @@ -1017,7 +1061,10 @@ namespace Gameplay { public ManagedFinalColorRenderPipelineProbeAsset() { - rendererData = new ManagedRenderPipelineProbeRendererData(); + rendererDataList = new ScriptableRendererData[] + { + new ManagedRenderPipelineProbeRendererData() + }; } protected override FinalColorSettings GetDefaultFinalColorSettings() @@ -1031,8 +1078,11 @@ namespace Gameplay { public ManagedRenderContextFinalColorDataProbeAsset() { - rendererData = - new ManagedRenderContextCameraDataProbeRendererData(); + rendererDataList = + new ScriptableRendererData[] + { + new ManagedRenderContextCameraDataProbeRendererData() + }; } protected override FinalColorSettings GetDefaultFinalColorSettings() @@ -1046,8 +1096,11 @@ namespace Gameplay { public ManagedRenderContextStageColorDataProbeAsset() { - rendererData = - new ManagedRenderContextStageColorDataProbeRendererData(); + rendererDataList = + new ScriptableRendererData[] + { + new ManagedRenderContextStageColorDataProbeRendererData() + }; } } @@ -1213,23 +1266,42 @@ namespace Gameplay UniversalRenderPipelineAsset asset = new UniversalRenderPipelineAsset { - rendererData = new UniversalRendererData() + rendererDataList = + new ScriptableRendererData[] + { + new UniversalRendererData() + } }; GraphicsSettings.renderPipelineAsset = asset; UniversalRenderPipelineAsset selectedAsset = GraphicsSettings.renderPipelineAsset as UniversalRenderPipelineAsset; + ScriptableRendererData selectedRendererData = null; + if (selectedAsset != null && + selectedAsset.rendererDataList != null && + selectedAsset.rendererDataList.Length > 0) + { + int rendererIndex = + selectedAsset.defaultRendererIndex; + if (rendererIndex < 0 || + rendererIndex >= selectedAsset.rendererDataList.Length) + { + rendererIndex = 0; + } + + selectedRendererData = + selectedAsset.rendererDataList[rendererIndex]; + } + SelectionRoundTripSucceeded = selectedAsset != null; SelectedPipelineAssetTypeName = selectedAsset != null ? selectedAsset.GetType().FullName ?? string.Empty : string.Empty; - SelectedRendererDataTypeName = - selectedAsset != null && - selectedAsset.rendererData != null - ? selectedAsset.rendererData.GetType().FullName ?? string.Empty - : string.Empty; + SelectedRendererDataTypeName = selectedRendererData != null + ? selectedRendererData.GetType().FullName ?? string.Empty + : string.Empty; } } @@ -1241,7 +1313,11 @@ namespace Gameplay GraphicsSettings.renderPipelineAsset = new UniversalRenderPipelineAsset { - rendererData = new UniversalRendererData() + rendererDataList = + new ScriptableRendererData[] + { + new UniversalRendererData() + } }; } } @@ -1254,12 +1330,16 @@ namespace Gameplay GraphicsSettings.renderPipelineAsset = new UniversalRenderPipelineAsset { - rendererData = new UniversalRendererData - { - renderOpaque = true, - renderSkybox = false, - renderTransparent = false - } + rendererDataList = + new ScriptableRendererData[] + { + new UniversalRendererData + { + renderOpaque = true, + renderSkybox = false, + renderTransparent = false + } + } }; } } @@ -1272,21 +1352,25 @@ namespace Gameplay GraphicsSettings.renderPipelineAsset = new UniversalRenderPipelineAsset { - rendererData = new UniversalRendererData - { - rendererFeatures = - new ScriptableRendererFeature[] + rendererDataList = + new ScriptableRendererData[] + { + new UniversalRendererData { - new ColorScalePostProcessRendererFeature - { - colorScale = new Vector4( - 1.08f, - 0.96f, - 0.92f, - 1.0f) - } + rendererFeatures = + new ScriptableRendererFeature[] + { + new ColorScalePostProcessRendererFeature + { + colorScale = new Vector4( + 1.08f, + 0.96f, + 0.92f, + 1.0f) + } + } } - } + } }; } } @@ -1299,14 +1383,18 @@ namespace Gameplay GraphicsSettings.renderPipelineAsset = new UniversalRenderPipelineAsset { - rendererData = new UniversalRendererData - { - rendererFeatures = - new ScriptableRendererFeature[] + rendererDataList = + new ScriptableRendererData[] + { + new UniversalRendererData { - new DisableDirectionalShadowRendererFeature() + rendererFeatures = + new ScriptableRendererFeature[] + { + new DisableDirectionalShadowRendererFeature() + } } - } + } }; } } diff --git a/managed/XCEngine.ScriptCore/Rendering/Universal/ScriptableRendererData.cs b/managed/XCEngine.ScriptCore/Rendering/Universal/ScriptableRendererData.cs index 0f2c7aa8..83afad47 100644 --- a/managed/XCEngine.ScriptCore/Rendering/Universal/ScriptableRendererData.cs +++ b/managed/XCEngine.ScriptCore/Rendering/Universal/ScriptableRendererData.cs @@ -22,6 +22,11 @@ namespace XCEngine.Rendering.Universal return GetRendererFeatures(); } + internal string GetPipelineRendererAssetKeyInstance() + { + return GetPipelineRendererAssetKey(); + } + internal void ConfigureCameraRenderRequestInstance( CameraRenderRequestContext context) { @@ -54,6 +59,11 @@ namespace XCEngine.Rendering.Universal { } + protected virtual string GetPipelineRendererAssetKey() + { + return string.Empty; + } + protected virtual ScriptableRendererFeature[] CreateRendererFeatures() { return Array.Empty(); diff --git a/managed/XCEngine.ScriptCore/Rendering/Universal/UniversalRenderPipelineAsset.cs b/managed/XCEngine.ScriptCore/Rendering/Universal/UniversalRenderPipelineAsset.cs index 1f968214..3ed081d9 100644 --- a/managed/XCEngine.ScriptCore/Rendering/Universal/UniversalRenderPipelineAsset.cs +++ b/managed/XCEngine.ScriptCore/Rendering/Universal/UniversalRenderPipelineAsset.cs @@ -6,13 +6,17 @@ namespace XCEngine.Rendering.Universal public class UniversalRenderPipelineAsset : ScriptableRenderPipelineAsset { - public ScriptableRendererData rendererData = - new UniversalRendererData(); + public ScriptableRendererData[] rendererDataList = + new ScriptableRendererData[] + { + new UniversalRendererData() + }; + public int defaultRendererIndex = 0; protected override ScriptableRenderPipeline CreatePipeline() { ScriptableRendererData resolvedRendererData = - ResolveRendererData(); + ResolveDefaultRendererData(); return resolvedRendererData != null ? new UniversalRenderPipeline(resolvedRendererData) : null; @@ -22,7 +26,7 @@ namespace XCEngine.Rendering.Universal CameraRenderRequestContext context) { ScriptableRendererData resolvedRendererData = - ResolveRendererData(); + ResolveDefaultRendererData(); if (resolvedRendererData != null) { resolvedRendererData.ConfigureCameraRenderRequestInstance( @@ -32,17 +36,39 @@ namespace XCEngine.Rendering.Universal protected override string GetPipelineRendererAssetKey() { - return "BuiltinForward"; + ScriptableRendererData resolvedRendererData = + ResolveDefaultRendererData(); + return resolvedRendererData != null + ? resolvedRendererData.GetPipelineRendererAssetKeyInstance() + : string.Empty; } - private ScriptableRendererData ResolveRendererData() + private ScriptableRendererData ResolveDefaultRendererData() { - if (rendererData == null) + if (rendererDataList == null || + rendererDataList.Length == 0) { - rendererData = new UniversalRendererData(); + rendererDataList = + new ScriptableRendererData[] + { + new UniversalRendererData() + }; + defaultRendererIndex = 0; } - return rendererData; + if (defaultRendererIndex < 0 || + defaultRendererIndex >= rendererDataList.Length) + { + defaultRendererIndex = 0; + } + + if (rendererDataList[defaultRendererIndex] == null) + { + rendererDataList[defaultRendererIndex] = + new UniversalRendererData(); + } + + return rendererDataList[defaultRendererIndex]; } } } diff --git a/managed/XCEngine.ScriptCore/Rendering/Universal/UniversalRendererData.cs b/managed/XCEngine.ScriptCore/Rendering/Universal/UniversalRendererData.cs index e06a5ede..dc2bad8d 100644 --- a/managed/XCEngine.ScriptCore/Rendering/Universal/UniversalRendererData.cs +++ b/managed/XCEngine.ScriptCore/Rendering/Universal/UniversalRendererData.cs @@ -22,6 +22,11 @@ namespace XCEngine.Rendering.Universal return rendererFeatures ?? Array.Empty(); } + + protected override string GetPipelineRendererAssetKey() + { + return "BuiltinForward"; + } } } diff --git a/tests/scripting/test_mono_script_runtime.cpp b/tests/scripting/test_mono_script_runtime.cpp index c4f0737b..9e5f6f05 100644 --- a/tests/scripting/test_mono_script_runtime.cpp +++ b/tests/scripting/test_mono_script_runtime.cpp @@ -2492,6 +2492,36 @@ TEST_F( nullptr); } +TEST_F( + MonoScriptRuntimeTest, + ManagedRenderPipelineBridgeUsesDefaultRendererSelectionForNativeBackendAsset) { + const auto bridge = + XCEngine::Rendering::Pipelines::GetManagedRenderPipelineBridge(); + ASSERT_NE(bridge, nullptr); + + const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor descriptor = { + "GameScripts", + "Gameplay", + "ManagedDefaultRendererSelectionProbeAsset" + }; + + 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) {