From d196ec92648c4b49e82d74ad2bbb3bb0f14ef4bd Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Mon, 20 Apr 2026 02:48:16 +0800 Subject: [PATCH] refactor(srp): add renderer data invalidation seam Introduce ScriptableRendererData dirty/invalidation support so renderer and feature caches can be released and rebuilt within the same managed asset runtime. Add managed probes and scripting coverage for non-public dirty APIs and for renderer rebuild after invalidation, then archive the completed phase plan. --- ...ndererData失效与重建接缝计划_2026-04-20.md | 139 ++++++++++++++++ managed/GameScripts/RenderPipelineApiProbe.cs | 148 ++++++++++++++++++ .../ScriptableRenderContextApiSurfaceProbe.cs | 13 ++ .../Universal/ScriptableRendererData.cs | 75 ++++++--- tests/scripting/test_mono_script_runtime.cpp | 115 ++++++++++++++ 5 files changed, 466 insertions(+), 24 deletions(-) create mode 100644 docs/used/SRP_RendererData失效与重建接缝计划_2026-04-20.md diff --git a/docs/used/SRP_RendererData失效与重建接缝计划_2026-04-20.md b/docs/used/SRP_RendererData失效与重建接缝计划_2026-04-20.md new file mode 100644 index 00000000..bbba795f --- /dev/null +++ b/docs/used/SRP_RendererData失效与重建接缝计划_2026-04-20.md @@ -0,0 +1,139 @@ +# SRP RendererData 失效与重建接缝计划 2026-04-20 + +## 1. 阶段目标 + +上一阶段已经把 renderer feature setup 正式归位到: + +`ScriptableRendererData -> ScriptableRenderer` + +但现在还缺最后一条关键闭环: + +当同一个 `ScriptableRendererData` 被修改后,旧的 renderer cache 该怎么失效,以及下一次使用时怎么重建。 + +当前系统只在更大的生命周期边界清缓存: + +1. asset runtime 释放 +2. bridge generation 变化 +3. managed pipeline dispose + +这意味着“同一个 asset 还活着,但 renderer data 已经变了”的情况,还没有正式 contract。 + +这一阶段的目标就是补上 Unity 风格的 renderer data invalidation seam。 + +--- + +## 2. 当前问题 + +### 2.1 renderer cache 只能在 runtime release 时整体释放 + +现在 `ScriptableRendererData` 已经缓存: + +1. renderer instance +2. renderer feature cache + +但没有一个正式的“我变脏了,请把这套 renderer setup 丢掉”的入口。 + +### 2.2 同 asset、同 runtime 内部缺少 renderer rebuild 语义 + +对于后续编辑器和 SRP/URP 包来说,很常见的场景是: + +1. 还在用同一个 pipeline asset +2. 只是改了 renderer data +3. 希望下一次录制时重建 renderer + +这一层现在还只能靠 asset/runtime 整体重建绕过去。 + +### 2.3 测试还没锁住“同一个 runtime 内触发 renderer rebuild” + +现有测试已经锁了: + +1. renderer reuse +2. 生命周期 release +3. setup seam + +但还没锁“同一个 asset runtime 内,dirty 之后旧 renderer dispose,新 renderer rebuild”。 + +--- + +## 3. 实施方案 + +### 3.1 给 `ScriptableRendererData` 增加正式 invalidation seam + +新增非 public / protected 语义: + +1. `SetDirty()` +2. `isInvalidated` + +`SetDirty()` 负责: + +1. 释放当前 renderer cache +2. 释放当前 feature cache +3. 标记 data 已失效 + +### 3.2 renderer 下一次解析时自动重建 + +保持调用面不变: + +`GetRendererInstance()` + +但在 dirty 之后,下一次解析 renderer 时应: + +1. 重新创建 renderer +2. 重新 setup renderer +3. 清掉 invalidated 状态 + +### 3.3 用 probe 锁住同 asset runtime 内的 rebuild 行为 + +新增一个专用 probe: + +1. 第一次 `SupportsStageRenderGraph` 创建 renderer +2. 运行时通过同一个 managed asset 调用 renderer data invalidation +3. 第二次 `SupportsStageRenderGraph` 在同一个 asset runtime 里重建 renderer + +--- + +## 4. 实施步骤 + +### Step 1:补 core invalidation seam + +目标: + +1. 修改 `ScriptableRendererData` +2. 引入 renderer/feature cache 的单独释放辅助逻辑 +3. 增加 `SetDirty()` 与 `isInvalidated` + +### Step 2:补 invalidation probe + +目标: + +1. 在 `RenderPipelineApiProbe.cs` 增加 renderer invalidation 专用 asset / renderer data / renderer / feature / 观察脚本 +2. 用同一个 managed asset handle 触发 invalidation + +### Step 3:补 API probe 与脚本测试 + +目标: + +1. 更新 `ScriptableRenderContextApiSurfaceProbe.cs` +2. 更新 `test_mono_script_runtime.cpp` +3. 锁住 dirty 之后的 create/setup/dispose 计数 + +### Step 4:验证与收口 + +目标: + +1. 编译 `rendering_unit_tests`、`scripting_tests`、`XCEditor` +2. 跑测试 +3. 跑旧版 `editor/bin/Debug/XCEngine.exe` 10s 冒烟并检查新的 `SceneReady` +4. 归档 plan,提交并推送 + +--- + +## 5. 验收标准 + +完成后应满足: + +1. `ScriptableRendererData` 存在正式 dirty / invalidated seam +2. dirty 之后旧 renderer cache 和 feature cache 会被释放 +3. 同一个 asset runtime 内下一次解析 renderer 时会自动重建 +4. probe / scripting test 锁住重建行为 +5. old editor 编译、测试、冒烟全部通过 diff --git a/managed/GameScripts/RenderPipelineApiProbe.cs b/managed/GameScripts/RenderPipelineApiProbe.cs index fa93a979..c00563e2 100644 --- a/managed/GameScripts/RenderPipelineApiProbe.cs +++ b/managed/GameScripts/RenderPipelineApiProbe.cs @@ -898,6 +898,76 @@ namespace Gameplay } } + internal static class ManagedRendererInvalidationProbeState + { + public static int CreateRendererCallCount; + public static int SetupRendererCallCount; + public static int CreateFeatureCallCount; + public static int DisposeRendererCallCount; + public static int DisposeFeatureCallCount; + public static int InvalidateRendererCallCount; + + public static void Reset() + { + CreateRendererCallCount = 0; + SetupRendererCallCount = 0; + CreateFeatureCallCount = 0; + DisposeRendererCallCount = 0; + DisposeFeatureCallCount = 0; + InvalidateRendererCallCount = 0; + } + } + + internal sealed class ManagedRendererInvalidationProbeFeature + : ScriptableRendererFeature + { + protected override void ReleaseRuntimeResources() + { + ManagedRendererInvalidationProbeState.DisposeFeatureCallCount++; + } + } + + internal sealed class ManagedRendererInvalidationProbeRenderer + : ProbeSceneRenderer + { + protected override void ReleaseRuntimeResources() + { + ManagedRendererInvalidationProbeState.DisposeRendererCallCount++; + } + } + + internal sealed class ManagedRendererInvalidationProbeRendererData + : ProbeRendererData + { + protected override ScriptableRenderer CreateProbeRenderer() + { + ManagedRendererInvalidationProbeState.CreateRendererCallCount++; + return new ManagedRendererInvalidationProbeRenderer(); + } + + protected override void SetupRenderer( + ScriptableRenderer renderer) + { + ManagedRendererInvalidationProbeState.SetupRendererCallCount++; + base.SetupRenderer(renderer); + } + + protected override ScriptableRendererFeature[] CreateRendererFeatures() + { + ManagedRendererInvalidationProbeState.CreateFeatureCallCount++; + return new ScriptableRendererFeature[] + { + new ManagedRendererInvalidationProbeFeature() + }; + } + + public void InvalidateForTest() + { + ManagedRendererInvalidationProbeState.InvalidateRendererCallCount++; + SetDirty(); + } + } + internal sealed class ManagedPlannedFullscreenRenderPipelineProbeRendererData : ProbeRendererData { @@ -1235,6 +1305,33 @@ namespace Gameplay } } + public sealed class ManagedRendererInvalidationProbeAsset + : RendererBackedRenderPipelineAsset + { + private readonly ManagedRendererInvalidationProbeRendererData + m_rendererData; + + public ManagedRendererInvalidationProbeAsset() + { + ManagedRendererInvalidationProbeState.Reset(); + m_rendererData = new ManagedRendererInvalidationProbeRendererData(); + rendererDataList = new ScriptableRendererData[] + { + m_rendererData + }; + } + + public void InvalidateDefaultRendererForTest() + { + if (m_rendererData == null) + { + return; + } + + m_rendererData.InvalidateForTest(); + } + } + public sealed class ManagedPlannedFullscreenRenderPipelineProbeAsset : UniversalRenderPipelineAsset { @@ -1517,6 +1614,57 @@ namespace Gameplay } } + public sealed class ManagedRendererInvalidationRuntimeSelectionProbe + : MonoBehaviour + { + public void Start() + { + GraphicsSettings.renderPipelineAsset = + new ManagedRendererInvalidationProbeAsset(); + } + } + + public sealed class ManagedRendererInvalidationObservationProbe + : MonoBehaviour + { + public int ObservedCreateRendererCallCount; + public int ObservedSetupRendererCallCount; + public int ObservedCreateFeatureCallCount; + public int ObservedDisposeRendererCallCount; + public int ObservedDisposeFeatureCallCount; + public int ObservedInvalidateRendererCallCount; + + private bool m_requestedInvalidation; + + public void Update() + { + ManagedRendererInvalidationProbeAsset selectedAsset = + GraphicsSettings.renderPipelineAsset + as ManagedRendererInvalidationProbeAsset; + if (!m_requestedInvalidation && + selectedAsset != null && + ManagedRendererInvalidationProbeState + .CreateRendererCallCount > 0) + { + selectedAsset.InvalidateDefaultRendererForTest(); + m_requestedInvalidation = true; + } + + ObservedCreateRendererCallCount = + ManagedRendererInvalidationProbeState.CreateRendererCallCount; + ObservedSetupRendererCallCount = + ManagedRendererInvalidationProbeState.SetupRendererCallCount; + ObservedCreateFeatureCallCount = + ManagedRendererInvalidationProbeState.CreateFeatureCallCount; + ObservedDisposeRendererCallCount = + ManagedRendererInvalidationProbeState.DisposeRendererCallCount; + ObservedDisposeFeatureCallCount = + ManagedRendererInvalidationProbeState.DisposeFeatureCallCount; + ObservedInvalidateRendererCallCount = + ManagedRendererInvalidationProbeState.InvalidateRendererCallCount; + } + } + public sealed class RenderPipelineApiProbe : MonoBehaviour { public bool InitialAssetWasNull; diff --git a/managed/GameScripts/ScriptableRenderContextApiSurfaceProbe.cs b/managed/GameScripts/ScriptableRenderContextApiSurfaceProbe.cs index 271a3a91..8eaf07ec 100644 --- a/managed/GameScripts/ScriptableRenderContextApiSurfaceProbe.cs +++ b/managed/GameScripts/ScriptableRenderContextApiSurfaceProbe.cs @@ -35,6 +35,8 @@ namespace Gameplay public bool HasRendererBackedRenderPipelineType; public bool HasRendererDrivenRenderPipelineType; public bool HasRendererDataSetupRenderer; + public bool HasRendererDataSetDirty; + public bool HasRendererDataIsInvalidated; public bool HasRendererSupportsRendererRecording; public bool HasRendererRecordRenderer; public bool HasPublicRendererSupportsStageRenderGraph; @@ -180,6 +182,17 @@ namespace Gameplay "SetupRenderer", BindingFlags.Instance | BindingFlags.NonPublic) != null; + HasRendererDataSetDirty = + rendererDataType.GetMethod( + "SetDirty", + BindingFlags.Instance | + BindingFlags.NonPublic) != null; + HasRendererDataIsInvalidated = + rendererDataType.GetProperty( + "isInvalidated", + BindingFlags.Instance | + BindingFlags.NonPublic | + BindingFlags.Public) != null; HasRendererSupportsRendererRecording = rendererType.GetMethod( "SupportsRendererRecording", diff --git a/managed/XCEngine.ScriptCore/Rendering/Universal/ScriptableRendererData.cs b/managed/XCEngine.ScriptCore/Rendering/Universal/ScriptableRendererData.cs index 7742ad3d..f0e37651 100644 --- a/managed/XCEngine.ScriptCore/Rendering/Universal/ScriptableRendererData.cs +++ b/managed/XCEngine.ScriptCore/Rendering/Universal/ScriptableRendererData.cs @@ -8,6 +8,7 @@ namespace XCEngine.Rendering.Universal { private ScriptableRendererFeature[] m_rendererFeatures; private ScriptableRenderer m_rendererInstance; + private bool m_rendererInvalidated; protected ScriptableRendererData() { @@ -26,6 +27,7 @@ namespace XCEngine.Rendering.Universal if (m_rendererInstance != null) { SetupRendererInstance(m_rendererInstance); + m_rendererInvalidated = false; } } @@ -48,6 +50,11 @@ namespace XCEngine.Rendering.Universal SetupRenderer(renderer); } + internal void SetDirtyInstance() + { + SetDirty(); + } + internal string GetPipelineRendererAssetKeyInstance() { return GetPipelineRendererAssetKey(); @@ -55,31 +62,9 @@ namespace XCEngine.Rendering.Universal internal void ReleaseRuntimeResourcesInstance() { - if (m_rendererInstance != null) - { - m_rendererInstance.ReleaseRuntimeResourcesInstance(); - m_rendererInstance = null; - m_rendererFeatures = null; - ReleaseRuntimeResources(); - return; - } - - if (m_rendererFeatures != null) - { - for (int i = 0; i < m_rendererFeatures.Length; ++i) - { - ScriptableRendererFeature rendererFeature = - m_rendererFeatures[i]; - if (rendererFeature != null) - { - rendererFeature.ReleaseRuntimeResourcesInstance(); - } - } - - m_rendererFeatures = null; - } - + ReleaseRendererSetupCache(); ReleaseRuntimeResources(); + m_rendererInvalidated = false; } internal void ConfigureCameraRenderRequestInstance( @@ -163,6 +148,14 @@ namespace XCEngine.Rendering.Universal { } + protected bool isInvalidated + { + get + { + return m_rendererInvalidated; + } + } + protected bool HasDirectionalShadow( CameraRenderRequestContext context) { @@ -185,6 +178,12 @@ namespace XCEngine.Rendering.Universal context.nativeHandle); } + protected void SetDirty() + { + ReleaseRendererSetupCache(); + m_rendererInvalidated = true; + } + protected void AddRendererFeature( ScriptableRenderer renderer, ScriptableRendererFeature rendererFeature) @@ -227,6 +226,34 @@ namespace XCEngine.Rendering.Universal return m_rendererFeatures; } + + private void ReleaseRendererSetupCache() + { + if (m_rendererInstance != null) + { + m_rendererInstance.ReleaseRuntimeResourcesInstance(); + m_rendererInstance = null; + m_rendererFeatures = null; + return; + } + + if (m_rendererFeatures == null) + { + return; + } + + for (int i = 0; i < m_rendererFeatures.Length; ++i) + { + ScriptableRendererFeature rendererFeature = + m_rendererFeatures[i]; + if (rendererFeature != null) + { + rendererFeature.ReleaseRuntimeResourcesInstance(); + } + } + + m_rendererFeatures = null; + } } } diff --git a/tests/scripting/test_mono_script_runtime.cpp b/tests/scripting/test_mono_script_runtime.cpp index 32a866c4..247771fc 100644 --- a/tests/scripting/test_mono_script_runtime.cpp +++ b/tests/scripting/test_mono_script_runtime.cpp @@ -1179,6 +1179,8 @@ TEST_F( bool hasRendererBackedRenderPipelineType = false; bool hasRendererDrivenRenderPipelineType = false; bool hasRendererDataSetupRenderer = false; + bool hasRendererDataSetDirty = false; + bool hasRendererDataIsInvalidated = false; bool hasRendererSupportsRendererRecording = false; bool hasRendererRecordRenderer = false; bool hasPublicRendererSupportsStageRenderGraph = false; @@ -1292,6 +1294,14 @@ TEST_F( selectionScript, "HasRendererDataSetupRenderer", hasRendererDataSetupRenderer)); + EXPECT_TRUE(runtime->TryGetFieldValue( + selectionScript, + "HasRendererDataSetDirty", + hasRendererDataSetDirty)); + EXPECT_TRUE(runtime->TryGetFieldValue( + selectionScript, + "HasRendererDataIsInvalidated", + hasRendererDataIsInvalidated)); EXPECT_TRUE(runtime->TryGetFieldValue( selectionScript, "HasRendererSupportsRendererRecording", @@ -1336,6 +1346,8 @@ TEST_F( EXPECT_TRUE(hasRendererBackedRenderPipelineType); EXPECT_TRUE(hasRendererDrivenRenderPipelineType); EXPECT_TRUE(hasRendererDataSetupRenderer); + EXPECT_TRUE(hasRendererDataSetDirty); + EXPECT_TRUE(hasRendererDataIsInvalidated); EXPECT_TRUE(hasRendererSupportsRendererRecording); EXPECT_TRUE(hasRendererRecordRenderer); EXPECT_FALSE(hasPublicRendererSupportsStageRenderGraph); @@ -3143,6 +3155,109 @@ TEST_F( secondRecorder->Shutdown(); } +TEST_F( + MonoScriptRuntimeTest, + ManagedRenderPipelineBridgeRebuildsRendererAfterRendererDataInvalidation) { + Scene* runtimeScene = + CreateScene("ManagedRendererInvalidationScene"); + GameObject* selectionObject = + runtimeScene->CreateGameObject( + "ManagedRendererInvalidationSelection"); + ScriptComponent* selectionScript = + AddScript( + selectionObject, + "Gameplay", + "ManagedRendererInvalidationRuntimeSelectionProbe"); + ASSERT_NE(selectionScript, nullptr); + GameObject* observationObject = + runtimeScene->CreateGameObject( + "ManagedRendererInvalidationObservation"); + ScriptComponent* observationScript = + AddScript( + observationObject, + "Gameplay", + "ManagedRendererInvalidationObservationProbe"); + ASSERT_NE(observationScript, nullptr); + + engine->OnRuntimeStart(runtimeScene); + engine->OnUpdate(0.016f); + + const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor descriptor = + XCEngine::Rendering::Pipelines::GetConfiguredManagedRenderPipelineAssetDescriptor(); + ASSERT_TRUE(descriptor.IsValid()); + ASSERT_NE(descriptor.managedAssetHandle, 0u); + EXPECT_EQ(descriptor.assemblyName, "GameScripts"); + EXPECT_EQ(descriptor.namespaceName, "Gameplay"); + EXPECT_EQ(descriptor.className, "ManagedRendererInvalidationProbeAsset"); + + const auto bridge = + XCEngine::Rendering::Pipelines::GetManagedRenderPipelineBridge(); + ASSERT_NE(bridge, nullptr); + + std::shared_ptr + assetRuntime = bridge->CreateAssetRuntime(descriptor); + ASSERT_NE(assetRuntime, nullptr); + + std::unique_ptr + recorder = assetRuntime->CreateStageRecorder(); + ASSERT_NE(recorder, nullptr); + + const XCEngine::Rendering::RenderContext context = {}; + ASSERT_TRUE(recorder->Initialize(context)); + ASSERT_TRUE( + recorder->SupportsStageRenderGraph( + XCEngine::Rendering::CameraFrameStage::MainScene)); + + engine->OnUpdate(0.016f); + + ASSERT_TRUE( + recorder->SupportsStageRenderGraph( + XCEngine::Rendering::CameraFrameStage::MainScene)); + + engine->OnUpdate(0.016f); + EXPECT_TRUE(runtime->GetLastError().empty()) << runtime->GetLastError(); + + int observedCreateRendererCallCount = 0; + int observedSetupRendererCallCount = 0; + int observedCreateFeatureCallCount = 0; + int observedDisposeRendererCallCount = 0; + int observedDisposeFeatureCallCount = 0; + int observedInvalidateRendererCallCount = 0; + EXPECT_TRUE(runtime->TryGetFieldValue( + observationScript, + "ObservedCreateRendererCallCount", + observedCreateRendererCallCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + observationScript, + "ObservedSetupRendererCallCount", + observedSetupRendererCallCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + observationScript, + "ObservedCreateFeatureCallCount", + observedCreateFeatureCallCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + observationScript, + "ObservedDisposeRendererCallCount", + observedDisposeRendererCallCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + observationScript, + "ObservedDisposeFeatureCallCount", + observedDisposeFeatureCallCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + observationScript, + "ObservedInvalidateRendererCallCount", + observedInvalidateRendererCallCount)); + + EXPECT_EQ(observedCreateRendererCallCount, 2); + EXPECT_EQ(observedSetupRendererCallCount, 2); + EXPECT_EQ(observedCreateFeatureCallCount, 2); + EXPECT_EQ(observedDisposeRendererCallCount, 1); + EXPECT_EQ(observedDisposeFeatureCallCount, 1); + EXPECT_EQ(observedInvalidateRendererCallCount, 1); + + recorder->Shutdown(); +} + TEST_F( MonoScriptRuntimeTest, ManagedStageRecorderRecordsMainSceneThroughScriptableRenderContext) {