diff --git a/docs/used/SRP_URP_RendererFeatureOwnershipLifecyclePlan_2026-04-21_完成归档.md b/docs/used/SRP_URP_RendererFeatureOwnershipLifecyclePlan_2026-04-21_完成归档.md new file mode 100644 index 00000000..44d535a4 --- /dev/null +++ b/docs/used/SRP_URP_RendererFeatureOwnershipLifecyclePlan_2026-04-21_完成归档.md @@ -0,0 +1,78 @@ +# SRP URP Renderer Feature Ownership Lifecycle Plan + +日期:2026-04-21 + +## 背景 + +上一阶段已经把 native 侧收口成了共享 backend substrate: + +- managed `rendererIndex` 负责选择 `ScriptableRenderer` +- native C++ 负责共享 backend / scene draw substrate + +接下来最关键的问题不在 native,而在 managed URP 这一层: + +- `ScriptableRendererFeature` 目前没有 owner 概念 +- feature 自身没有 dirty/version 语义 +- `ScriptableRendererData` 只能在自己显式 `SetDirty()` 时失效 +- `rendererFeatures` 列表替换、feature 激活状态变化、feature 自身 runtime 失效,都没有被完整建模 + +这意味着当前虽然已经能写 `ScriptableRendererFeature`,但它还不算一个真正稳定、可扩展、可长期维护的 Unity 风格定制点。 + +## 本阶段目标 + +把 managed `ScriptableRendererFeature` / `ScriptableRendererData` 收口成更接近 Unity URP 的生命周期语义: + +- `ScriptableRendererFeature` 知道自己属于哪个 `ScriptableRendererData` +- feature 可以显式 `SetDirty()`,并向 owner 传播 invalidation +- feature 具备基本 runtime state version 语义 +- `ScriptableRendererData` 能检测 `rendererFeatures` 集合变化并使 renderer cache 失效 +- 内置 feature 不再绕开 `CreateInstance()` 的生命周期入口 + +## 为什么现在做 + +如果这层不先收口,后面继续做: + +- 用户自定义 `ScriptableRendererFeature` +- renderer data 序列化/编辑器配置 +- 更复杂的 URP renderer feature 组合 +- 最终的 C# 自定义渲染管线体验 + +都会建立在一个“feature 能写,但生命周期和失效语义不完整”的地基上。这样继续堆功能,后面只会越来越难收拾。 + +## 改动范围 + +本阶段只处理 managed URP 的 feature/data 生命周期,不碰 new editor,不改 deferred,不重新打开 native backend 选择问题。 + +主要涉及: + +- `managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/ScriptableRendererFeature.cs` +- `managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/ScriptableRendererData.cs` +- `managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/BuiltinGaussianSplatRendererFeature.cs` +- `managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/BuiltinVolumetricRendererFeature.cs` +- `managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/RenderObjectsRendererFeature.cs` + +## 预期结果 + +收口后,这一层的关系应当变成: + +- `ScriptableRendererData` 拥有 renderer feature 集合,并负责 renderer cache 失效 +- `ScriptableRendererFeature` 是 renderer data 的受管对象,而不是游离对象 +- feature 的激活状态和显式 dirty,都能正确影响 renderer/runtime 生命周期 +- 内置 feature 的创建和释放都走统一生命周期入口 + +## 实施步骤 + +1. 给 `ScriptableRendererFeature` 增加 owner / dirty / runtime state version 语义 +2. 给 `ScriptableRendererData` 增加 feature 集合同步与 invalidation 收口逻辑 +3. 修正内置 feature 的 `Create()` 回退路径,统一走 `CreateInstance()` +4. 编译 `XCEditor` +5. 运行旧版 `editor/bin/Debug/XCEngine.exe` 冒烟至少 10 秒,并确认新的 `SceneReady` +6. 归档计划,提交并推送 + +## 验证标准 + +- `cmake --build . --config Debug --target XCEditor` 成功 +- 运行 `editor/bin/Debug/XCEngine.exe` +- 冒烟至少 10 秒 +- `editor/bin/Debug/editor.log` 出现新的 `SceneReady` +- 本阶段提交只包含 SRP/rendering 主线代码与计划文档 diff --git a/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/BuiltinGaussianSplatRendererFeature.cs b/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/BuiltinGaussianSplatRendererFeature.cs index bc5b2295..83fb8961 100644 --- a/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/BuiltinGaussianSplatRendererFeature.cs +++ b/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/BuiltinGaussianSplatRendererFeature.cs @@ -29,7 +29,7 @@ namespace XCEngine.Rendering.Universal if (m_pass == null) { - Create(); + CreateInstance(); } m_pass.Configure(passEvent); diff --git a/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/BuiltinVolumetricRendererFeature.cs b/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/BuiltinVolumetricRendererFeature.cs index 426d6ed7..9370e2f2 100644 --- a/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/BuiltinVolumetricRendererFeature.cs +++ b/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/BuiltinVolumetricRendererFeature.cs @@ -29,7 +29,7 @@ namespace XCEngine.Rendering.Universal if (m_pass == null) { - Create(); + CreateInstance(); } m_pass.Configure(passEvent); diff --git a/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/RenderObjectsRendererFeature.cs b/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/RenderObjectsRendererFeature.cs index ee5633ab..0bebcc94 100644 --- a/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/RenderObjectsRendererFeature.cs +++ b/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/RenderObjectsRendererFeature.cs @@ -64,7 +64,7 @@ namespace XCEngine.Rendering.Universal if (m_pass == null) { - Create(); + CreateInstance(); } m_pass.Configure( diff --git a/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/RendererBackedRenderPipelineAsset.cs b/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/RendererBackedRenderPipelineAsset.cs index e041e365..d0e2be17 100644 --- a/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/RendererBackedRenderPipelineAsset.cs +++ b/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/RendererBackedRenderPipelineAsset.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.CompilerServices; using XCEngine; using XCEngine.Rendering; @@ -274,6 +275,10 @@ namespace XCEngine.Rendering.Universal rendererDataList[i]; if (rendererData != null) { + hash = + (hash * 31) + + RuntimeHelpers.GetHashCode( + rendererData); hash = (hash * 31) + rendererData diff --git a/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/ScriptableRendererData.cs b/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/ScriptableRendererData.cs index 5e90774f..7ec716dd 100644 --- a/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/ScriptableRendererData.cs +++ b/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/ScriptableRendererData.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.CompilerServices; using XCEngine; using XCEngine.Rendering; @@ -11,6 +12,8 @@ namespace XCEngine.Rendering.Universal private ScriptableRenderer m_rendererInstance; private bool m_rendererInvalidated; private int m_runtimeStateVersion = 1; + private int m_rendererFeatureCollectionHash; + private bool m_rendererFeatureCollectionHashResolved; protected ScriptableRendererData() { @@ -59,8 +62,19 @@ namespace XCEngine.Rendering.Universal SetDirty(); } + internal void InvalidateRendererFeaturesInstance() + { + m_rendererFeatureCollectionHash = + ComputeRendererFeatureCollectionHash( + rendererFeatures ?? + Array.Empty()); + m_rendererFeatureCollectionHashResolved = true; + SetDirty(); + } + internal int GetRuntimeStateVersionInstance() { + SynchronizeRendererFeatureCollectionState(); return m_runtimeStateVersion; } @@ -339,9 +353,92 @@ namespace XCEngine.Rendering.Universal Array.Empty(); } + BindRendererFeatureOwners(m_rendererFeatures); return m_rendererFeatures; } + private void SynchronizeRendererFeatureCollectionState() + { + ScriptableRendererFeature[] configuredRendererFeatures = + rendererFeatures ?? + Array.Empty(); + BindRendererFeatureOwners(configuredRendererFeatures); + + int collectionHash = + ComputeRendererFeatureCollectionHash( + configuredRendererFeatures); + if (!m_rendererFeatureCollectionHashResolved) + { + m_rendererFeatureCollectionHash = collectionHash; + m_rendererFeatureCollectionHashResolved = true; + return; + } + + if (collectionHash == m_rendererFeatureCollectionHash) + { + return; + } + + m_rendererFeatureCollectionHash = collectionHash; + SetDirty(); + } + + private void BindRendererFeatureOwners( + ScriptableRendererFeature[] rendererFeatureCollection) + { + if (rendererFeatureCollection == null) + { + return; + } + + for (int i = 0; i < rendererFeatureCollection.Length; ++i) + { + ScriptableRendererFeature rendererFeature = + rendererFeatureCollection[i]; + if (rendererFeature != null) + { + rendererFeature.BindOwnerInstance(this); + } + } + } + + private static int ComputeRendererFeatureCollectionHash( + ScriptableRendererFeature[] rendererFeatureCollection) + { + unchecked + { + int hash = 17; + if (rendererFeatureCollection == null) + { + return hash; + } + + hash = + (hash * 31) + + rendererFeatureCollection.Length; + for (int i = 0; i < rendererFeatureCollection.Length; ++i) + { + ScriptableRendererFeature rendererFeature = + rendererFeatureCollection[i]; + if (rendererFeature == null) + { + hash = (hash * 31) + 1; + continue; + } + + hash = + (hash * 31) + + RuntimeHelpers.GetHashCode(rendererFeature); + hash = + (hash * 31) + + rendererFeature + .GetRuntimeStateVersionInstance(); + } + + return hash; + } + } + private void ReleaseRendererSetupCache() { if (m_rendererInstance != null) diff --git a/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/ScriptableRendererFeature.cs b/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/ScriptableRendererFeature.cs index 7b60f78a..dfeb3eef 100644 --- a/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/ScriptableRendererFeature.cs +++ b/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/ScriptableRendererFeature.cs @@ -7,12 +7,42 @@ namespace XCEngine.Rendering.Universal { private bool m_disposed; private bool m_runtimeCreated; + private bool m_isActive = true; + private int m_runtimeStateVersion = 1; + private ScriptableRendererData m_owner; protected ScriptableRendererFeature() { } - public bool isActive { get; set; } = true; + public bool isActive + { + get + { + return m_isActive; + } + set + { + if (m_isActive == value) + { + return; + } + + m_isActive = value; + SetDirty(); + } + } + + internal void BindOwnerInstance( + ScriptableRendererData owner) + { + m_owner = owner; + } + + internal int GetRuntimeStateVersionInstance() + { + return m_runtimeStateVersion; + } internal void CreateInstance() { @@ -28,7 +58,8 @@ namespace XCEngine.Rendering.Universal internal void ReleaseRuntimeResourcesInstance() { - if (m_disposed) + if (m_disposed || + !m_runtimeCreated) { return; } @@ -93,6 +124,33 @@ namespace XCEngine.Rendering.Universal protected virtual void ReleaseRuntimeResources() { } + + protected void SetDirty() + { + if (m_runtimeCreated && + !m_disposed) + { + ReleaseRuntimeResources(); + } + + m_disposed = false; + m_runtimeCreated = false; + + unchecked + { + ++m_runtimeStateVersion; + } + + if (m_runtimeStateVersion <= 0) + { + m_runtimeStateVersion = 1; + } + + if (m_owner != null) + { + m_owner.InvalidateRendererFeaturesInstance(); + } + } } }