refactor(srp): tighten renderer feature lifecycle invalidation

This commit is contained in:
2026-04-21 19:43:45 +08:00
parent 97bd990b12
commit ffe4717d8f
7 changed files with 243 additions and 5 deletions

View File

@@ -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 主线代码与计划文档

View File

@@ -29,7 +29,7 @@ namespace XCEngine.Rendering.Universal
if (m_pass == null)
{
Create();
CreateInstance();
}
m_pass.Configure(passEvent);

View File

@@ -29,7 +29,7 @@ namespace XCEngine.Rendering.Universal
if (m_pass == null)
{
Create();
CreateInstance();
}
m_pass.Configure(passEvent);

View File

@@ -64,7 +64,7 @@ namespace XCEngine.Rendering.Universal
if (m_pass == null)
{
Create();
CreateInstance();
}
m_pass.Configure(

View File

@@ -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

View File

@@ -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<ScriptableRendererFeature>());
m_rendererFeatureCollectionHashResolved = true;
SetDirty();
}
internal int GetRuntimeStateVersionInstance()
{
SynchronizeRendererFeatureCollectionState();
return m_runtimeStateVersion;
}
@@ -339,9 +353,92 @@ namespace XCEngine.Rendering.Universal
Array.Empty<ScriptableRendererFeature>();
}
BindRendererFeatureOwners(m_rendererFeatures);
return m_rendererFeatures;
}
private void SynchronizeRendererFeatureCollectionState()
{
ScriptableRendererFeature[] configuredRendererFeatures =
rendererFeatures ??
Array.Empty<ScriptableRendererFeature>();
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)

View File

@@ -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();
}
}
}
}