diff --git a/docs/used/SRP_URP_RendererFeatureRuntimeStateSyncPlan_2026-04-21_完成归档.md b/docs/used/SRP_URP_RendererFeatureRuntimeStateSyncPlan_2026-04-21_完成归档.md new file mode 100644 index 00000000..4bf65bbf --- /dev/null +++ b/docs/used/SRP_URP_RendererFeatureRuntimeStateSyncPlan_2026-04-21_完成归档.md @@ -0,0 +1,85 @@ +# SRP URP Renderer Feature Runtime State Sync Plan + +日期:2026-04-21 + +## 背景 + +上一阶段已经把 `ScriptableRendererFeature` / `ScriptableRendererData` 的 owner、dirty、lifecycle 收口了一轮: + +- feature 的激活状态变化会触发 invalidation +- feature 可以向 owner 传播 dirty +- renderer feature 集合变化能使 renderer cache 失效 + +但当前还有一个关键缺口: + +- feature 的普通配置字段变化,系统本身还不会主动感知 + +例如: + +- `RenderObjectsRendererFeature.passEvent` +- `RenderObjectsRendererFeature.overrideMaterialPath` +- `ColorScalePostProcessRendererFeature.colorScale` +- 内置 feature 的 `passEvent` + +这些值如果被代码改掉,当前多数场景只是“碰巧还能工作”,而不是被明确建模成 runtime-state 变化。 + +## 本阶段目标 + +给 `ScriptableRendererFeature` 增加统一的 runtime-state 同步入口,并把当前内置 feature 的配置接到这条链路上: + +- `ScriptableRendererFeature` 自身能同步 runtime state version +- `ScriptableRendererData` 读取 feature version 时会拿到同步后的值 +- 内置 feature 的关键配置字段参与 runtime-state 哈希 +- feature 配置变化会走统一 invalidation 语义,而不是散落在各处的偶然行为 + +## 为什么现在做 + +如果这层不做,后面继续做: + +- 更多 URP feature +- 用户自定义 renderer feature +- editor/序列化 对 feature 字段的编辑 + +都会建立在一个“字段变了,但系统不一定知道这算一次 runtime state 变化”的前提上。 + +这会让后面调试、缓存失效、资源重建的行为越来越难预测。 + +## 改动范围 + +本阶段只处理 managed URP feature 的 runtime-state 同步,不碰 new editor,不改 deferred,不回头扩 native。 + +主要涉及: + +- `managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/ScriptableRendererFeature.cs` +- `managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/RenderObjectsRendererFeature.cs` +- `managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/ColorScalePostProcessRendererFeature.cs` +- `managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/BuiltinGaussianSplatRendererFeature.cs` +- `managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/BuiltinVolumetricRendererFeature.cs` + +如有必要,可加一个小型哈希辅助实现,但保持范围只在 managed URP。 + +## 预期结果 + +收口后: + +- `ScriptableRendererFeature` 会像 `ScriptableRenderPipelineAsset` / `ScriptableRendererData` 一样具备同步 runtime-state 的能力 +- feature 配置字段变化会转化为明确的 version 变化 +- `ScriptableRendererData` 不再只知道 “feature 对象还是不是同一个”,也知道 “同一个 feature 的配置有没有变” +- 后面继续做用户自定义 URP feature 时,失效语义有稳定扩展点 + +## 实施步骤 + +1. 给 `ScriptableRendererFeature` 增加 runtime-state 同步入口 +2. 为当前内置 feature 接入配置哈希/同步逻辑 +3. 保持和上一阶段的 owner/dirty/lifecycle 逻辑兼容 +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 83fb8961..ecd9730f 100644 --- a/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/BuiltinGaussianSplatRendererFeature.cs +++ b/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/BuiltinGaussianSplatRendererFeature.cs @@ -11,6 +11,17 @@ namespace XCEngine.Rendering.Universal private NativeSceneFeaturePass m_pass; + protected override int ComputeRuntimeStateHash() + { + int hash = + base.ComputeRuntimeStateHash(); + hash = + RuntimeStateHashUtility.Combine( + hash, + (int)passEvent); + return hash; + } + public override void Create() { m_pass = new NativeSceneFeaturePass( @@ -27,10 +38,7 @@ namespace XCEngine.Rendering.Universal return; } - if (m_pass == null) - { - CreateInstance(); - } + CreateInstance(); m_pass.Configure(passEvent); renderer.EnqueuePass(m_pass); diff --git a/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/BuiltinVolumetricRendererFeature.cs b/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/BuiltinVolumetricRendererFeature.cs index 9370e2f2..55800df3 100644 --- a/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/BuiltinVolumetricRendererFeature.cs +++ b/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/BuiltinVolumetricRendererFeature.cs @@ -11,6 +11,17 @@ namespace XCEngine.Rendering.Universal private NativeSceneFeaturePass m_pass; + protected override int ComputeRuntimeStateHash() + { + int hash = + base.ComputeRuntimeStateHash(); + hash = + RuntimeStateHashUtility.Combine( + hash, + (int)passEvent); + return hash; + } + public override void Create() { m_pass = new NativeSceneFeaturePass( @@ -27,10 +38,7 @@ namespace XCEngine.Rendering.Universal return; } - if (m_pass == null) - { - CreateInstance(); - } + CreateInstance(); m_pass.Configure(passEvent); renderer.EnqueuePass(m_pass); diff --git a/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/ColorScalePostProcessRendererFeature.cs b/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/ColorScalePostProcessRendererFeature.cs index a7b6b402..2c78212a 100644 --- a/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/ColorScalePostProcessRendererFeature.cs +++ b/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/ColorScalePostProcessRendererFeature.cs @@ -48,6 +48,17 @@ namespace XCEngine.Rendering.Universal 1.0f, 1.0f); + protected override int ComputeRuntimeStateHash() + { + int hash = + base.ComputeRuntimeStateHash(); + hash = + RuntimeStateHashUtility.Combine( + hash, + colorScale); + return hash; + } + public override void Create() { m_pass = @@ -64,10 +75,7 @@ namespace XCEngine.Rendering.Universal return; } - if (m_pass == null) - { - CreateInstance(); - } + CreateInstance(); renderer.EnqueuePass(m_pass); } diff --git a/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/RenderObjectsRendererFeature.cs b/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/RenderObjectsRendererFeature.cs index 0bebcc94..0a88be0c 100644 --- a/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/RenderObjectsRendererFeature.cs +++ b/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/RenderObjectsRendererFeature.cs @@ -28,6 +28,73 @@ namespace XCEngine.Rendering.Universal public bool overrideSortingSettings; public SortingSettings sortingSettings; + protected override int ComputeRuntimeStateHash() + { + int hash = + base.ComputeRuntimeStateHash(); + hash = + RuntimeStateHashUtility.Combine( + hash, + (int)passEvent); + hash = + RuntimeStateHashUtility.Combine( + hash, + (int)scenePhase); + hash = + RuntimeStateHashUtility.Combine( + hash, + (int)rendererListType); + hash = + RuntimeStateHashUtility.Combine( + hash, + overrideRenderQueueRange); + hash = + RuntimeStateHashUtility.Combine( + hash, + renderQueueRange); + hash = + RuntimeStateHashUtility.Combine( + hash, + overrideRenderLayerMask); + hash = + RuntimeStateHashUtility.Combine( + hash, + renderLayerMask); + hash = + RuntimeStateHashUtility.Combine( + hash, + overrideMaterialPath); + hash = + RuntimeStateHashUtility.Combine( + hash, + shaderPassName); + hash = + RuntimeStateHashUtility.Combine( + hash, + overrideRenderStateBlock); + hash = + RuntimeStateHashUtility.Combine( + hash, + renderStateBlock); + hash = + RuntimeStateHashUtility.Combine( + hash, + overrideFilteringSettings); + hash = + RuntimeStateHashUtility.Combine( + hash, + filteringSettings); + hash = + RuntimeStateHashUtility.Combine( + hash, + overrideSortingSettings); + hash = + RuntimeStateHashUtility.Combine( + hash, + sortingSettings); + return hash; + } + public override void Create() { RendererListDesc rendererListDesc = @@ -62,10 +129,7 @@ namespace XCEngine.Rendering.Universal return; } - if (m_pass == null) - { - CreateInstance(); - } + CreateInstance(); m_pass.Configure( passEvent, diff --git a/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/ScriptableRendererFeature.cs b/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/ScriptableRendererFeature.cs index dfeb3eef..f490272f 100644 --- a/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/ScriptableRendererFeature.cs +++ b/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/ScriptableRendererFeature.cs @@ -9,6 +9,8 @@ namespace XCEngine.Rendering.Universal private bool m_runtimeCreated; private bool m_isActive = true; private int m_runtimeStateVersion = 1; + private int m_runtimeStateHash; + private bool m_runtimeStateHashResolved; private ScriptableRendererData m_owner; protected ScriptableRendererFeature() @@ -41,6 +43,7 @@ namespace XCEngine.Rendering.Universal internal int GetRuntimeStateVersionInstance() { + SynchronizeRuntimeStateVersion(); return m_runtimeStateVersion; } @@ -125,8 +128,47 @@ namespace XCEngine.Rendering.Universal { } + protected virtual int ComputeRuntimeStateHash() + { + int hash = 17; + hash = + RuntimeStateHashUtility.Combine( + hash, + m_isActive); + return hash; + } + protected void SetDirty() { + ApplyDirty( + ComputeRuntimeStateHash()); + } + + private void SynchronizeRuntimeStateVersion() + { + int runtimeStateHash = + ComputeRuntimeStateHash(); + if (!m_runtimeStateHashResolved) + { + m_runtimeStateHash = runtimeStateHash; + m_runtimeStateHashResolved = true; + return; + } + + if (runtimeStateHash == m_runtimeStateHash) + { + return; + } + + ApplyDirty(runtimeStateHash); + } + + private void ApplyDirty( + int runtimeStateHash) + { + m_runtimeStateHash = runtimeStateHash; + m_runtimeStateHashResolved = true; + if (m_runtimeCreated && !m_disposed) { @@ -152,5 +194,175 @@ namespace XCEngine.Rendering.Universal } } } + + internal static class RuntimeStateHashUtility + { + public static int Combine( + int hash, + bool value) + { + return Combine( + hash, + value ? 1 : 0); + } + + public static int Combine( + int hash, + byte value) + { + return Combine( + hash, + (int)value); + } + + public static int Combine( + int hash, + uint value) + { + unchecked + { + return (hash * 31) + (int)value; + } + } + + public static int Combine( + int hash, + int value) + { + unchecked + { + return (hash * 31) + value; + } + } + + public static int Combine( + int hash, + float value) + { + return Combine( + hash, + value.GetHashCode()); + } + + public static int Combine( + int hash, + string value) + { + return Combine( + hash, + value != null + ? value.GetHashCode() + : 0); + } + + public static int Combine( + int hash, + Vector4 value) + { + hash = Combine(hash, value.X); + hash = Combine(hash, value.Y); + hash = Combine(hash, value.Z); + hash = Combine(hash, value.W); + return hash; + } + + public static int Combine( + int hash, + RenderQueueRange value) + { + hash = Combine(hash, value.lowerBound); + hash = Combine(hash, value.upperBound); + return hash; + } + + public static int Combine( + int hash, + FilteringSettings value) + { + hash = Combine(hash, value.renderQueueMin); + hash = Combine(hash, value.renderQueueMax); + hash = Combine(hash, value.renderLayerMask); + hash = Combine(hash, value.requireShadowCasting); + hash = Combine(hash, value.requireRenderObjectId); + return hash; + } + + public static int Combine( + int hash, + SortingSettings value) + { + return Combine( + hash, + (int)value.sortMode); + } + + public static int Combine( + int hash, + RendererListDesc value) + { + hash = Combine(hash, (int)value.type); + hash = Combine(hash, value.filtering); + hash = Combine(hash, value.sorting); + return hash; + } + + public static int Combine( + int hash, + DepthState value) + { + hash = Combine(hash, value.writeEnabled); + hash = + Combine(hash, (int)value.compareFunction); + return hash; + } + + public static int Combine( + int hash, + StencilFaceState value) + { + hash = + Combine(hash, (int)value.failOperation); + hash = + Combine(hash, (int)value.passOperation); + hash = + Combine(hash, (int)value.depthFailOperation); + hash = + Combine(hash, (int)value.compareFunction); + return hash; + } + + public static int Combine( + int hash, + StencilState value) + { + hash = Combine(hash, value.enabled); + hash = Combine(hash, value.readMask); + hash = Combine(hash, value.writeMask); + hash = Combine(hash, value.frontFace); + hash = Combine(hash, value.backFace); + return hash; + } + + public static int Combine( + int hash, + RenderStateBlock value) + { + hash = Combine(hash, (int)value.mask); + hash = Combine(hash, value.depthState); + hash = Combine(hash, value.stencilState); + hash = Combine(hash, value.stencilReference); + return hash; + } + + public static int Combine( + int hash, + DrawingSettings value) + { + hash = Combine(hash, value.overrideMaterialPath); + hash = Combine(hash, value.shaderPassName); + hash = Combine(hash, value.renderStateBlock); + return hash; + } + } }