From cbc0ddbd42e768494e20b29c31b1a118674b52c1 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Mon, 20 Apr 2026 00:16:32 +0800 Subject: [PATCH] refactor(srp): formalize universal renderer selection and caching --- ...例与默认选择收口计划_完成归档_2026-04-20.md | 148 ++++++++++++++++++ managed/GameScripts/RenderPipelineApiProbe.cs | 52 ++++++ .../Universal/ScriptableRendererData.cs | 13 +- .../Universal/UniversalRenderPipeline.cs | 24 ++- .../Universal/UniversalRenderPipelineAsset.cs | 90 ++++++++--- tests/scripting/test_mono_script_runtime.cpp | 90 +++++++++++ 6 files changed, 379 insertions(+), 38 deletions(-) create mode 100644 docs/used/SRP_Universal_Renderer实例与默认选择收口计划_完成归档_2026-04-20.md diff --git a/docs/used/SRP_Universal_Renderer实例与默认选择收口计划_完成归档_2026-04-20.md b/docs/used/SRP_Universal_Renderer实例与默认选择收口计划_完成归档_2026-04-20.md new file mode 100644 index 00000000..c85064ff --- /dev/null +++ b/docs/used/SRP_Universal_Renderer实例与默认选择收口计划_完成归档_2026-04-20.md @@ -0,0 +1,148 @@ +# SRP Universal Renderer实例与默认选择收口计划 2026-04-20 + +## 1. 阶段目标 + +上一阶段已经把: + +`native backend ownership` + +从 `UniversalRenderPipelineAsset` 下放到了: + +`ScriptableRendererData` + +但当前 Universal 运行时还有一个明显没收好的点: + +1. `UniversalRenderPipelineAsset` 只有 renderer data 列表和默认索引 +2. 真正的 renderer 实例却仍然藏在 `UniversalRenderPipeline` 私有字段里 +3. renderer 的默认选择、索引回退、实例缓存没有形成一套统一语义 + +本阶段目标就是把这层收口成更接近 Unity 的形态: + +`UniversalRenderPipelineAsset -> Resolve/GetRenderer(index) -> ScriptableRendererData -> ScriptableRenderer` + +--- + +## 2. 当前问题 + +### 2.1 renderer 实例 ownership 还在 pipeline 对象里 + +现在 `UniversalRenderPipeline` 内部自己持有: + +1. `ScriptableRendererData` +2. `ScriptableRenderer m_renderer` + +这意味着 renderer 的生命周期和 pipeline 对象绑得过紧,不利于后续: + +1. 默认 renderer 选择正式化 +2. 多 renderer data 扩展 +3. 资产级 renderer cache +4. 更像 Unity 的 `GetRenderer(index)` 模型 + +### 2.2 默认 renderer 选择还只是“私有 helper” + +现在 `UniversalRenderPipelineAsset` 只是内部有一个: + +`ResolveDefaultRendererData()` + +但还没有形成: + +1. `ResolveRendererIndex` +2. `GetRendererData(index)` +3. `GetRenderer(index)` + +这导致默认选择语义存在,但没有成为一等运行时接口。 + +### 2.3 renderer cache 还没有落到正确层级 + +如果未来相同 asset 产生多个 pipeline 对象,renderer 实例现在无法通过 asset/data 层复用。 + +Unity 风格下更合理的职责是: + +1. asset 决定当前用哪个 renderer data +2. renderer data 负责产出或缓存 renderer +3. pipeline 只消费“选好的 renderer” + +--- + +## 3. 本阶段方案 + +### 方案核心 + +把 renderer lifecycle 正式归位到: + +`ScriptableRendererData` + +并让: + +`UniversalRenderPipelineAsset` + +提供统一的 renderer 解析入口。 + +### 目标结构 + +1. `ScriptableRendererData` + - 缓存 renderer instance + - 提供 `GetRendererInstance()` +2. `UniversalRenderPipelineAsset` + - 统一处理 renderer index 解析和 fallback + - 提供 default renderer / 指定 index renderer 获取 +3. `UniversalRenderPipeline` + - 不再拥有 renderer data / renderer cache + - 仅通过 asset 解析当前 renderer + +--- + +## 4. 实施步骤 + +### Step 1:把 renderer 实例缓存下放到 ScriptableRendererData + +目标: + +1. 给 `ScriptableRendererData` 增加 renderer instance cache +2. `CreateRenderer()` 只在首次需要时触发 +3. renderer features 的初始化与 renderer instance 生命周期对齐 + +### Step 2:给 UniversalRenderPipelineAsset 增加正式的 renderer 解析接口 + +目标: + +1. 增加 renderer index 解析与 fallback +2. 增加 default renderer data / renderer 获取接口 +3. 把 `CreatePipeline()` 改成传 asset,而不是直接传 renderer data + +### Step 3:收掉 UniversalRenderPipeline 的私有 renderer ownership + +目标: + +1. 移除 pipeline 内部的私有 renderer cache +2. 改为通过 asset 取 default renderer +3. 保证主场景与后处理录制仍然正常 + +### Step 4:补回归测试 + +目标: + +1. 默认 renderer index 选择继续决定 native backend asset +2. 默认 renderer index 越界时 fallback 到 index 0 +3. 同一 managed asset 派生多个 pipeline/recorder 时,renderer 实例不会重复构建 + +### Step 5:验证与收口 + +目标: + +1. 编译 `rendering_unit_tests`、`scripting_tests`、`XCEditor` +2. 跑相关单测 +3. 跑旧版 `XCEditor` 10 秒冒烟并验证新 `SceneReady` +4. 归档 plan、提交、推送 + +--- + +## 5. 验收标准 + +本阶段完成后应满足: + +1. renderer 默认选择语义在 `UniversalRenderPipelineAsset` 层正式成立 +2. renderer 实例 cache 在 `ScriptableRendererData` 层成立 +3. `UniversalRenderPipeline` 退回到轻量 orchestration 角色 +4. 默认索引 fallback 和 renderer reuse 都有测试锁住 +5. 结构继续向 Unity 风格 `Asset -> RendererData -> Renderer` 逼近 diff --git a/managed/GameScripts/RenderPipelineApiProbe.cs b/managed/GameScripts/RenderPipelineApiProbe.cs index 5fce0a68..73480a9a 100644 --- a/managed/GameScripts/RenderPipelineApiProbe.cs +++ b/managed/GameScripts/RenderPipelineApiProbe.cs @@ -878,6 +878,18 @@ namespace Gameplay } } + internal sealed class ManagedRendererReuseProbeRendererData + : ProbeRendererData + { + public static int CreateRendererCallCount; + + protected override ScriptableRenderer CreateProbeRenderer() + { + CreateRendererCallCount++; + return new ManagedRenderPipelineProbe(); + } + } + internal sealed class ManagedPlannedFullscreenRenderPipelineProbeRendererData : ProbeRendererData { @@ -1000,6 +1012,33 @@ namespace Gameplay } } + public sealed class ManagedInvalidDefaultRendererSelectionProbeAsset + : UniversalRenderPipelineAsset + { + public ManagedInvalidDefaultRendererSelectionProbeAsset() + { + rendererDataList = new ScriptableRendererData[] + { + new ManagedRenderPipelineProbeRendererData(), + new ManagedUnknownBackendProbeRendererData() + }; + defaultRendererIndex = 5; + } + } + + public sealed class ManagedRendererReuseProbeAsset + : UniversalRenderPipelineAsset + { + public ManagedRendererReuseProbeAsset() + { + ManagedRendererReuseProbeRendererData.CreateRendererCallCount = 0; + rendererDataList = new ScriptableRendererData[] + { + new ManagedRendererReuseProbeRendererData() + }; + } + } + public sealed class ManagedPlannedFullscreenRenderPipelineProbeAsset : UniversalRenderPipelineAsset { @@ -1214,6 +1253,19 @@ namespace Gameplay { } + public sealed class ManagedRendererReuseObservationProbe + : MonoBehaviour + { + public int ObservedCreateRendererCallCount; + + public void Start() + { + ObservedCreateRendererCallCount = + ManagedRendererReuseProbeRendererData + .CreateRendererCallCount; + } + } + public sealed class RenderPipelineApiProbe : MonoBehaviour { public bool InitialAssetWasNull; diff --git a/managed/XCEngine.ScriptCore/Rendering/Universal/ScriptableRendererData.cs b/managed/XCEngine.ScriptCore/Rendering/Universal/ScriptableRendererData.cs index 83afad47..11b9cd43 100644 --- a/managed/XCEngine.ScriptCore/Rendering/Universal/ScriptableRendererData.cs +++ b/managed/XCEngine.ScriptCore/Rendering/Universal/ScriptableRendererData.cs @@ -7,6 +7,7 @@ namespace XCEngine.Rendering.Universal public abstract class ScriptableRendererData : Object { private ScriptableRendererFeature[] m_rendererFeatures; + private ScriptableRenderer m_rendererInstance; protected ScriptableRendererData() { @@ -14,7 +15,17 @@ namespace XCEngine.Rendering.Universal internal ScriptableRenderer CreateRendererInstance() { - return CreateRenderer(); + return GetRendererInstance(); + } + + internal ScriptableRenderer GetRendererInstance() + { + if (m_rendererInstance == null) + { + m_rendererInstance = CreateRenderer(); + } + + return m_rendererInstance; } internal ScriptableRendererFeature[] CreateRendererFeaturesInstance() diff --git a/managed/XCEngine.ScriptCore/Rendering/Universal/UniversalRenderPipeline.cs b/managed/XCEngine.ScriptCore/Rendering/Universal/UniversalRenderPipeline.cs index 2a53fd5b..7d09da68 100644 --- a/managed/XCEngine.ScriptCore/Rendering/Universal/UniversalRenderPipeline.cs +++ b/managed/XCEngine.ScriptCore/Rendering/Universal/UniversalRenderPipeline.cs @@ -6,19 +6,18 @@ namespace XCEngine.Rendering.Universal internal sealed class UniversalRenderPipeline : ScriptableRenderPipeline { - private readonly ScriptableRendererData m_rendererData; - private ScriptableRenderer m_renderer; + private readonly UniversalRenderPipelineAsset m_asset; public UniversalRenderPipeline( - ScriptableRendererData rendererData) + UniversalRenderPipelineAsset asset) { - m_rendererData = rendererData; + m_asset = asset; } protected override bool SupportsStageRenderGraph( CameraFrameStage stage) { - ScriptableRenderer renderer = GetOrCreateRenderer(); + ScriptableRenderer renderer = GetDefaultRenderer(); return renderer != null && renderer.SupportsStageRenderGraph(stage); } @@ -26,21 +25,16 @@ namespace XCEngine.Rendering.Universal protected override bool RecordStageRenderGraph( ScriptableRenderContext context) { - ScriptableRenderer renderer = GetOrCreateRenderer(); + ScriptableRenderer renderer = GetDefaultRenderer(); return renderer != null && renderer.RecordStageRenderGraph(context); } - private ScriptableRenderer GetOrCreateRenderer() + private ScriptableRenderer GetDefaultRenderer() { - if (m_renderer == null && - m_rendererData != null) - { - m_renderer = - m_rendererData.CreateRendererInstance(); - } - - return m_renderer; + return m_asset != null + ? m_asset.GetDefaultRenderer() + : null; } } } diff --git a/managed/XCEngine.ScriptCore/Rendering/Universal/UniversalRenderPipelineAsset.cs b/managed/XCEngine.ScriptCore/Rendering/Universal/UniversalRenderPipelineAsset.cs index 3ed081d9..c9437bf1 100644 --- a/managed/XCEngine.ScriptCore/Rendering/Universal/UniversalRenderPipelineAsset.cs +++ b/managed/XCEngine.ScriptCore/Rendering/Universal/UniversalRenderPipelineAsset.cs @@ -15,10 +15,8 @@ namespace XCEngine.Rendering.Universal protected override ScriptableRenderPipeline CreatePipeline() { - ScriptableRendererData resolvedRendererData = - ResolveDefaultRendererData(); - return resolvedRendererData != null - ? new UniversalRenderPipeline(resolvedRendererData) + return GetDefaultRendererData() != null + ? new UniversalRenderPipeline(this) : null; } @@ -26,7 +24,7 @@ namespace XCEngine.Rendering.Universal CameraRenderRequestContext context) { ScriptableRendererData resolvedRendererData = - ResolveDefaultRendererData(); + GetDefaultRendererData(); if (resolvedRendererData != null) { resolvedRendererData.ConfigureCameraRenderRequestInstance( @@ -37,38 +35,86 @@ namespace XCEngine.Rendering.Universal protected override string GetPipelineRendererAssetKey() { ScriptableRendererData resolvedRendererData = - ResolveDefaultRendererData(); + GetDefaultRendererData(); return resolvedRendererData != null ? resolvedRendererData.GetPipelineRendererAssetKeyInstance() : string.Empty; } - private ScriptableRendererData ResolveDefaultRendererData() + internal ScriptableRendererData GetDefaultRendererData() { - if (rendererDataList == null || - rendererDataList.Length == 0) + return GetRendererData(defaultRendererIndex); + } + + internal ScriptableRenderer GetDefaultRenderer() + { + return GetRenderer(defaultRendererIndex); + } + + internal ScriptableRendererData GetRendererData( + int rendererIndex) + { + EnsureRendererDataList(); + int resolvedRendererIndex = + ResolveRendererIndex(rendererIndex); + if (rendererDataList[resolvedRendererIndex] == null) { - rendererDataList = - new ScriptableRendererData[] - { - new UniversalRendererData() - }; - defaultRendererIndex = 0; + 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; + } + + 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; } - if (rendererDataList[defaultRendererIndex] == null) - { - rendererDataList[defaultRendererIndex] = - new UniversalRendererData(); - } - - return rendererDataList[defaultRendererIndex]; + return defaultRendererIndex; } } } diff --git a/tests/scripting/test_mono_script_runtime.cpp b/tests/scripting/test_mono_script_runtime.cpp index 9e5f6f05..6133db50 100644 --- a/tests/scripting/test_mono_script_runtime.cpp +++ b/tests/scripting/test_mono_script_runtime.cpp @@ -2522,6 +2522,36 @@ TEST_F( nullptr); } +TEST_F( + MonoScriptRuntimeTest, + ManagedRenderPipelineBridgeFallsBackToFirstRendererWhenDefaultRendererIndexIsInvalid) { + const auto bridge = + XCEngine::Rendering::Pipelines::GetManagedRenderPipelineBridge(); + ASSERT_NE(bridge, nullptr); + + const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor descriptor = { + "GameScripts", + "Gameplay", + "ManagedInvalidDefaultRendererSelectionProbeAsset" + }; + + 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) { @@ -2613,6 +2643,66 @@ TEST_F( recorder->Shutdown(); } +TEST_F( + MonoScriptRuntimeTest, + ManagedRenderPipelineBridgeReusesRendererInstanceAcrossManagedPipelineCreations) { + const auto bridge = + XCEngine::Rendering::Pipelines::GetManagedRenderPipelineBridge(); + ASSERT_NE(bridge, nullptr); + + const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor descriptor = { + "GameScripts", + "Gameplay", + "ManagedRendererReuseProbeAsset" + }; + + std::shared_ptr + assetRuntime = bridge->CreateAssetRuntime(descriptor); + ASSERT_NE(assetRuntime, nullptr); + + const XCEngine::Rendering::RenderContext context = {}; + + std::unique_ptr + firstRecorder = assetRuntime->CreateStageRecorder(); + ASSERT_NE(firstRecorder, nullptr); + ASSERT_TRUE(firstRecorder->Initialize(context)); + ASSERT_TRUE( + firstRecorder->SupportsStageRenderGraph( + XCEngine::Rendering::CameraFrameStage::MainScene)); + + std::unique_ptr + secondRecorder = assetRuntime->CreateStageRecorder(); + ASSERT_NE(secondRecorder, nullptr); + ASSERT_TRUE(secondRecorder->Initialize(context)); + ASSERT_TRUE( + secondRecorder->SupportsStageRenderGraph( + XCEngine::Rendering::CameraFrameStage::MainScene)); + + Scene* runtimeScene = + CreateScene("ManagedRendererReuseObservationScene"); + GameObject* scriptObject = + runtimeScene->CreateGameObject("ManagedRendererReuseObservationProbe"); + ScriptComponent* script = + AddScript( + scriptObject, + "Gameplay", + "ManagedRendererReuseObservationProbe"); + ASSERT_NE(script, nullptr); + + engine->OnRuntimeStart(runtimeScene); + engine->OnUpdate(0.016f); + + int observedCreateRendererCallCount = 0; + EXPECT_TRUE(runtime->TryGetFieldValue( + script, + "ObservedCreateRendererCallCount", + observedCreateRendererCallCount)); + EXPECT_EQ(observedCreateRendererCallCount, 1); + + firstRecorder->Shutdown(); + secondRecorder->Shutdown(); +} + TEST_F( MonoScriptRuntimeTest, ManagedStageRecorderRecordsMainSceneThroughScriptableRenderContext) {