diff --git a/docs/used/SRP_URP_RendererBlockFormalizationPlan_2026-04-22_完成归档.md b/docs/used/SRP_URP_RendererBlockFormalizationPlan_2026-04-22_完成归档.md new file mode 100644 index 00000000..f3b825e4 --- /dev/null +++ b/docs/used/SRP_URP_RendererBlockFormalizationPlan_2026-04-22_完成归档.md @@ -0,0 +1,90 @@ +# SRP / URP Renderer Block Formalization Plan + +时间:2026-04-22 + +## 背景 + +当前渲染主线已经完成了两件更底层的事情: + +1. C++ 侧的 RenderGraph / runtime resource / stage contract 已经能稳定承接 managed SRP。 +2. managed 侧也已经有 `ScriptableRenderer / RendererFeature / RenderPass` 这套骨架,并且 post-process / final output 的 graph 绑定已经打通。 + +但 renderer 这一层还有一个明显的问题没有收口: + +- `ScriptableRenderer` 现在还是把所有 pass 丢进一个总队列,然后在录制时按 `CameraFrameStage` 逐个扫描、过滤。 +- 这意味着 renderer 的“组织单位”依然是 stage,而不是 renderer 自己拥有的 block。 +- 对后续把阴影、体积、Gaussian、更多 scene feature 逐步迁到 URP 层不够友好,因为缺少稳定的 block 落点。 + +这一步的目标不是继续扩 RenderGraph 功能,也不是开始 deferred,而是先把 managed renderer 的组织方式收紧。 + +## 本阶段目标 + +把 managed `ScriptableRenderer` 从“单队列 + stage 过滤”重构成“显式 renderer block 录制”,并保持当前 C++ stage 边界不变。 + +目标结果: + +1. `ScriptableRenderer` 内部显式识别 `ShadowCaster / DepthPrepass / MainOpaque / MainSkybox / MainTransparent / PostProcess / FinalOutput`。 +2. stage 仍然只是 native 执行边界,renderer 内部真正按 block 组织与录制。 +3. `UniversalRenderer` 后续迁移阴影、体积、Gaussian 等逻辑时,有稳定的 block 入口可以承接。 +4. 不引入新的兼容层,不保留“旧逻辑先留着”的临时路线。 + +## 范围 + +本阶段只做 managed SRP / URP 组织重构,不做: + +- deferred rendering +- shadow 算法升级 +- editor 侧面板与资源工作流 +- ObjectId 这类 editor-only 特性迁移 + +## 实施步骤 + +### 1. 补齐 renderer block 基础类型 + +新增 renderer block 枚举与辅助工具,统一维护: + +- pass event -> renderer block +- renderer block -> camera frame stage +- renderer block 的运行时范围描述 + +### 2. 重构 ScriptableRenderer 的录制入口 + +保留 active pass queue 作为排序输入,但录制逻辑改为: + +1. 先按 `RenderPassEvent` 排序构建 active pass queue +2. 再根据 event range 构建 renderer block range +3. 按当前 stage 对应的 block 顺序执行 + +也就是说: + +- `MainScene` 不再是“扫完整队列找属于 MainScene 的 pass” +- 而是显式录制 `MainOpaque -> MainSkybox -> MainTransparent` + +### 3. 清理 stage-driven 痕迹 + +重点清理: + +- `ScriptableRenderer` 内部 `renderPass.SupportsStage(...)` 式的全队列过滤 +- 把 `SupportsRendererRecording / RecordRenderer` 改成基于 block 判断 + +保留: + +- native C++ 仍然通过 `CameraFrameStage` 调用 managed +- `ScriptableRenderPass.SupportsStage(...)` 作为公共 API 兼容入口,但内部改为基于 block 推导 + +### 4. 验证主线不回退 + +要求: + +1. `XCEditor` Debug 构建通过 +2. old editor 冒烟运行至少 10 秒 +3. `editor.log` 出现 `SceneReady` + +## 完成标准 + +满足以下条件才算本阶段收口: + +1. `ScriptableRenderer` 不再通过“单队列 + stage 过滤”驱动录制。 +2. renderer block 成为 managed renderer 内部的正式组织单位。 +3. `UniversalRenderer` 当前默认路径行为不回退。 +4. `XCEditor` 构建与 old editor 冒烟通过。 diff --git a/managed/CMakeLists.txt b/managed/CMakeLists.txt index 412b0ef7..c73ee732 100644 --- a/managed/CMakeLists.txt +++ b/managed/CMakeLists.txt @@ -235,8 +235,10 @@ set(XCENGINE_RENDER_PIPELINES_UNIVERSAL_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.RenderPipelines.Universal/Rendering/Universal/RenderEnvironmentMode.cs ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.RenderPipelines.Universal/Rendering/Universal/RenderPassEvent.cs ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.RenderPipelines.Universal/Rendering/Universal/RenderObjectsRendererFeature.cs + ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.RenderPipelines.Universal/Rendering/Universal/RendererBlock.cs ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.RenderPipelines.Universal/Rendering/Universal/RendererBackedRenderPipeline.cs ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.RenderPipelines.Universal/Rendering/Universal/RendererBackedRenderPipelineAsset.cs + ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.RenderPipelines.Universal/Rendering/Universal/RendererBlocks.cs ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.RenderPipelines.Universal/Rendering/Universal/RendererCameraRequestContext.cs ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.RenderPipelines.Universal/Rendering/Universal/RendererDrivenRenderPipeline.cs ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.RenderPipelines.Universal/Rendering/Universal/RendererRecordingContext.cs diff --git a/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/RendererBlock.cs b/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/RendererBlock.cs new file mode 100644 index 00000000..f1aabc17 --- /dev/null +++ b/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/RendererBlock.cs @@ -0,0 +1,41 @@ +using XCEngine.Rendering; + +namespace XCEngine.Rendering.Universal +{ + internal enum RendererBlock + { + ShadowCaster = 0, + DepthPrepass = 1, + MainOpaque = 2, + MainSkybox = 3, + MainTransparent = 4, + PostProcess = 5, + FinalOutput = 6, + Count = 7 + } + + internal static class RendererBlockUtility + { + public static CameraFrameStage GetStage( + RendererBlock block) + { + switch (block) + { + case RendererBlock.ShadowCaster: + return CameraFrameStage.ShadowCaster; + case RendererBlock.DepthPrepass: + return CameraFrameStage.DepthOnly; + case RendererBlock.MainOpaque: + case RendererBlock.MainSkybox: + case RendererBlock.MainTransparent: + return CameraFrameStage.MainScene; + case RendererBlock.PostProcess: + return CameraFrameStage.PostProcess; + case RendererBlock.FinalOutput: + return CameraFrameStage.FinalOutput; + default: + return CameraFrameStage.MainScene; + } + } + } +} diff --git a/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/RendererBlocks.cs b/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/RendererBlocks.cs new file mode 100644 index 00000000..49047153 --- /dev/null +++ b/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/RendererBlocks.cs @@ -0,0 +1,89 @@ +using System.Collections.Generic; + +namespace XCEngine.Rendering.Universal +{ + internal sealed class RendererBlocks + { + private struct BlockRange + { + public int firstPassIndex; + public int lastPassIndex; + } + + private readonly BlockRange[] m_ranges = + new BlockRange[(int)RendererBlock.Count]; + + public RendererBlocks() + { + Clear(); + } + + public void Clear() + { + for (int i = 0; i < m_ranges.Length; ++i) + { + m_ranges[i].firstPassIndex = -1; + m_ranges[i].lastPassIndex = -1; + } + } + + public void Build( + IList activePassQueue) + { + Clear(); + + if (activePassQueue == null) + { + return; + } + + for (int i = 0; i < activePassQueue.Count; ++i) + { + ScriptableRenderPass renderPass = + activePassQueue[i]; + if (renderPass == null) + { + continue; + } + + RendererBlock block; + if (!ScriptableRenderPass.TryResolveRendererBlock( + renderPass.renderPassEvent, + out block)) + { + continue; + } + + int blockIndex = (int)block; + BlockRange range = m_ranges[blockIndex]; + if (range.firstPassIndex < 0) + { + range.firstPassIndex = i; + } + + range.lastPassIndex = i; + m_ranges[blockIndex] = range; + } + } + + public bool HasPasses( + RendererBlock block) + { + BlockRange range = m_ranges[(int)block]; + return range.firstPassIndex >= 0 && + range.lastPassIndex >= range.firstPassIndex; + } + + public bool TryGetPassRange( + RendererBlock block, + out int firstPassIndex, + out int lastPassIndex) + { + BlockRange range = m_ranges[(int)block]; + firstPassIndex = range.firstPassIndex; + lastPassIndex = range.lastPassIndex; + return range.firstPassIndex >= 0 && + range.lastPassIndex >= range.firstPassIndex; + } + } +} diff --git a/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/ScriptableRenderPass.cs b/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/ScriptableRenderPass.cs index 715edee8..dadeced9 100644 --- a/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/ScriptableRenderPass.cs +++ b/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/ScriptableRenderPass.cs @@ -23,11 +23,21 @@ namespace XCEngine.Rendering.Universal public virtual bool SupportsStage( CameraFrameStage stage) { - CameraFrameStage resolvedStage; - return TryResolveStage( + RendererBlock block; + return TryResolveRendererBlock( renderPassEvent, - out resolvedStage) && - resolvedStage == stage; + out block) && + RendererBlockUtility.GetStage(block) == stage; + } + + internal bool SupportsRendererBlock( + RendererBlock block) + { + RendererBlock resolvedBlock; + return TryResolveRendererBlock( + renderPassEvent, + out resolvedBlock) && + resolvedBlock == block; } internal bool Record( @@ -209,38 +219,60 @@ namespace XCEngine.Rendering.Universal internal static bool TryResolveStage( RenderPassEvent passEvent, out CameraFrameStage stage) + { + RendererBlock block; + if (TryResolveRendererBlock( + passEvent, + out block)) + { + stage = + RendererBlockUtility.GetStage(block); + return true; + } + + stage = CameraFrameStage.MainScene; + return false; + } + + internal static bool TryResolveRendererBlock( + RenderPassEvent passEvent, + out RendererBlock block) { switch (passEvent) { case RenderPassEvent.BeforeRenderingShadows: case RenderPassEvent.AfterRenderingShadows: - stage = CameraFrameStage.ShadowCaster; + block = RendererBlock.ShadowCaster; return true; case RenderPassEvent.BeforeRenderingPrePasses: case RenderPassEvent.AfterRenderingPrePasses: - stage = CameraFrameStage.DepthOnly; + block = RendererBlock.DepthPrepass; return true; case RenderPassEvent.BeforeRenderingOpaques: case RenderPassEvent.RenderOpaques: case RenderPassEvent.AfterRenderingOpaques: + block = RendererBlock.MainOpaque; + return true; case RenderPassEvent.BeforeRenderingSkybox: case RenderPassEvent.RenderSkybox: case RenderPassEvent.AfterRenderingSkybox: + block = RendererBlock.MainSkybox; + return true; case RenderPassEvent.BeforeRenderingTransparents: case RenderPassEvent.RenderTransparents: case RenderPassEvent.AfterRenderingTransparents: - stage = CameraFrameStage.MainScene; + block = RendererBlock.MainTransparent; return true; case RenderPassEvent.BeforeRenderingPostProcessing: case RenderPassEvent.AfterRenderingPostProcessing: - stage = CameraFrameStage.PostProcess; + block = RendererBlock.PostProcess; return true; case RenderPassEvent.BeforeRenderingFinalOutput: case RenderPassEvent.AfterRenderingFinalOutput: - stage = CameraFrameStage.FinalOutput; + block = RendererBlock.FinalOutput; return true; default: - stage = CameraFrameStage.MainScene; + block = RendererBlock.MainOpaque; return false; } } diff --git a/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/ScriptableRenderer.cs b/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/ScriptableRenderer.cs index 34d44850..f0c2bccb 100644 --- a/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/ScriptableRenderer.cs +++ b/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/ScriptableRenderer.cs @@ -10,6 +10,8 @@ namespace XCEngine.Rendering.Universal new List(); private readonly List m_activePassQueue = new List(); + private readonly RendererBlocks m_rendererBlocks = + new RendererBlocks(); private bool m_disposed; protected ScriptableRenderer() @@ -36,6 +38,7 @@ namespace XCEngine.Rendering.Universal m_features.Clear(); m_activePassQueue.Clear(); + m_rendererBlocks.Clear(); m_disposed = true; } @@ -130,17 +133,7 @@ namespace XCEngine.Rendering.Universal RenderingData renderingData = context.renderingData; BuildPassQueue(renderingData); - for (int i = 0; i < m_activePassQueue.Count; ++i) - { - ScriptableRenderPass renderPass = m_activePassQueue[i]; - if (renderPass != null && - renderPass.SupportsStage(context.stage)) - { - return true; - } - } - - return false; + return SupportsRendererStage(context); } protected virtual bool RecordRenderer( @@ -155,28 +148,7 @@ namespace XCEngine.Rendering.Universal RenderingData renderingData = context.renderingData; BuildPassQueue(renderingData); - - bool recordedAnyPass = false; - for (int i = 0; i < m_activePassQueue.Count; ++i) - { - ScriptableRenderPass renderPass = m_activePassQueue[i]; - if (renderPass == null || - !renderPass.SupportsStage(renderingData.stage)) - { - continue; - } - - if (!renderPass.Record( - context.renderContext, - renderingData)) - { - return false; - } - - recordedAnyPass = true; - } - - return recordedAnyPass; + return RecordRendererStage(context); } protected internal virtual bool SupportsStageRenderGraph( @@ -214,6 +186,147 @@ namespace XCEngine.Rendering.Universal { } + private bool HasRendererBlock( + RendererBlock block) + { + return m_rendererBlocks.HasPasses(block); + } + + protected virtual bool SupportsRendererStage( + RendererRecordingContext context) + { + if (context == null) + { + return false; + } + + switch (context.stage) + { + case CameraFrameStage.ShadowCaster: + return HasRendererBlock( + RendererBlock.ShadowCaster); + case CameraFrameStage.DepthOnly: + return HasRendererBlock( + RendererBlock.DepthPrepass); + case CameraFrameStage.MainScene: + return HasRendererBlock( + RendererBlock.MainOpaque) || + HasRendererBlock( + RendererBlock.MainSkybox) || + HasRendererBlock( + RendererBlock.MainTransparent); + case CameraFrameStage.PostProcess: + return HasRendererBlock( + RendererBlock.PostProcess); + case CameraFrameStage.FinalOutput: + return HasRendererBlock( + RendererBlock.FinalOutput); + default: + return false; + } + } + + protected virtual bool RecordRendererStage( + RendererRecordingContext context) + { + if (context == null || + context.renderContext == null) + { + return false; + } + + bool recordedAnyPass = false; + switch (context.stage) + { + case CameraFrameStage.ShadowCaster: + return RecordRendererBlock( + RendererBlock.ShadowCaster, + context, + ref recordedAnyPass) && + recordedAnyPass; + case CameraFrameStage.DepthOnly: + return RecordRendererBlock( + RendererBlock.DepthPrepass, + context, + ref recordedAnyPass) && + recordedAnyPass; + case CameraFrameStage.MainScene: + return RecordRendererBlock( + RendererBlock.MainOpaque, + context, + ref recordedAnyPass) && + RecordRendererBlock( + RendererBlock.MainSkybox, + context, + ref recordedAnyPass) && + RecordRendererBlock( + RendererBlock.MainTransparent, + context, + ref recordedAnyPass) && + recordedAnyPass; + case CameraFrameStage.PostProcess: + return RecordRendererBlock( + RendererBlock.PostProcess, + context, + ref recordedAnyPass) && + recordedAnyPass; + case CameraFrameStage.FinalOutput: + return RecordRendererBlock( + RendererBlock.FinalOutput, + context, + ref recordedAnyPass) && + recordedAnyPass; + default: + return false; + } + } + + private bool RecordRendererBlock( + RendererBlock block, + RendererRecordingContext context, + ref bool recordedAnyPass) + { + if (context == null || + context.renderContext == null) + { + return false; + } + + int firstPassIndex; + int lastPassIndex; + if (!m_rendererBlocks.TryGetPassRange( + block, + out firstPassIndex, + out lastPassIndex)) + { + return true; + } + + for (int i = firstPassIndex; + i <= lastPassIndex; + ++i) + { + ScriptableRenderPass renderPass = + m_activePassQueue[i]; + if (renderPass == null || + !renderPass.SupportsRendererBlock(block)) + { + continue; + } + + if (!renderPass.Record( + context.renderContext, + context.renderingData)) + { + return false; + } + + recordedAnyPass = true; + } + + return true; + } + private void BuildPassQueue( RenderingData renderingData) { @@ -233,6 +346,7 @@ namespace XCEngine.Rendering.Universal } AddRenderPasses(renderingData); + m_rendererBlocks.Build(m_activePassQueue); } protected virtual void ReleaseRuntimeResources()