refactor(srp): tighten renderer feature lifecycle invalidation
This commit is contained in:
@@ -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 主线代码与计划文档
|
||||
@@ -29,7 +29,7 @@ namespace XCEngine.Rendering.Universal
|
||||
|
||||
if (m_pass == null)
|
||||
{
|
||||
Create();
|
||||
CreateInstance();
|
||||
}
|
||||
|
||||
m_pass.Configure(passEvent);
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace XCEngine.Rendering.Universal
|
||||
|
||||
if (m_pass == null)
|
||||
{
|
||||
Create();
|
||||
CreateInstance();
|
||||
}
|
||||
|
||||
m_pass.Configure(passEvent);
|
||||
|
||||
@@ -64,7 +64,7 @@ namespace XCEngine.Rendering.Universal
|
||||
|
||||
if (m_pass == null)
|
||||
{
|
||||
Create();
|
||||
CreateInstance();
|
||||
}
|
||||
|
||||
m_pass.Configure(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user