diff --git a/docs/reference/rendering_module_overview_rendergraph_srp_2026-04-17.md b/docs/reference/rendering_module_overview_rendergraph_srp_2026-04-17.md new file mode 100644 index 00000000..c2c12119 --- /dev/null +++ b/docs/reference/rendering_module_overview_rendergraph_srp_2026-04-17.md @@ -0,0 +1,2494 @@ +# XCEngine 当前渲染模块详解:从 SceneRenderer 到 RenderGraph,再到 SRP 主线 + +## 这份文档是给谁看的 + +这份文档是写给“引擎开发新手”看的。 + +目标不是炫术语,而是把你现在仓库里这条真实存在的渲染主链讲清楚: + +1. 当前渲染模块到底是怎么跑起来的。 +2. `RenderGraph` 在你这个项目里到底是什么,不是什么。 +3. 现在的 `BuiltinForwardPipeline`、`ScriptableRenderPipelineHost`、managed C# 代码分别做到哪一步了。 +4. 下一步为什么应该先做 SRP runtime,而不是直接开做一个“URP 包”。 + +这篇文档只按你仓库当前代码说话,不按理想状态脑补。 + +--- + +## 先记住 8 句话 + +1. 你现在这套渲染模块,已经不是“一个大函数直接画完一帧”的老式写法了。 +2. 它现在的主链是:`Scene -> CameraRenderRequest -> CameraFramePlan -> RenderGraph Recording -> RenderGraph Compile -> RenderGraph Execute`。 +3. `SceneRenderer` 不负责画东西,它更像一个总调度入口。 +4. `CameraFramePlan` 是当前渲染层里非常关键的“每相机执行计划”。 +5. `RenderGraph` 现在已经是可用的原生图系统,但它还是轻量版,不是完整大而全的工业终态。 +6. `BuiltinForwardPipeline` 已经不只是“直接执行 forward”,它还会把 `MainScene` 阶段录进 `RenderGraph`。 +7. `ScriptableRenderPipelineHost` 已经是 SRP 的 native 接缝,但 managed C# 那边现在还只是骨架,不是完整 runtime。 +8. 你接下来最该做的,不是马上做 URP 包,而是把 managed SRP runtime 真正打通。 + +--- + +## 一眼看懂当前整条主链 + +你现在看到的不是一堆零散类,而是一条完整主链: + +```text +SceneRenderer + -> SceneRenderRequestPlanner + -> RenderPipelineHost + -> CameraFramePlanBuilder + -> CameraRenderer + -> DirectionalShadowRuntime + -> RenderSceneExtractor + -> ExecuteCameraFrameRenderGraphPlan + -> RecordCameraFrameRenderGraphStages + -> 每个 Stage 录制到 RenderGraph + -> RenderGraphCompiler::Compile + -> RenderGraphExecutor::Execute +``` + +如果你脑子里先有这张图,后面很多类名就不会看乱。 + +--- + +## 当前模块大致怎么分层 + +你当前 `Rendering` 目录,职责大致已经分出来了: + +- `Execution/`:一帧怎么跑,stage 怎么调度,graph 怎么录。 +- `Planning/`:场景相机请求怎么变成可执行的 frame plan。 +- `Graph/`:`RenderGraph` 本体,包含 builder / compiler / executor / blackboard。 +- `Pipelines/`:具体渲染管线实现,以及面向 SRP 的 host。 +- `Passes/`:具体 render pass。 +- `Features/`:更接近“renderer feature”的注入式效果。 +- `Shadow/`:阴影运行时和数据。 +- `Extraction/`:从场景抽取 `RenderSceneData`。 +- `FrameData/`:一帧里绘制所需的可见物体、相机、光照等结构化数据。 + +所以从“模块分工”角度说,你现在已经不是一坨文件混在一起了。真正复杂的地方,不是有没有分目录,而是“调度关系”和“职责边界”能不能持续稳定。 + +--- + +## 1. 一帧是从哪里开始的 + +### 1.1 `SceneRenderer` 是总入口,但它不负责具体绘制 + +先看头文件: + +```cpp +class SceneRenderer { +public: + SceneRenderer(); + explicit SceneRenderer(std::unique_ptr pipeline); + explicit SceneRenderer(std::shared_ptr pipelineAsset); + ~SceneRenderer(); + + void SetPipeline(std::unique_ptr pipeline); + void SetPipelineAsset(std::shared_ptr pipelineAsset); + RenderPipeline* GetPipeline() const { return m_pipelineHost.GetPipeline(); } + const RenderPipelineAsset* GetPipelineAsset() const { return m_pipelineHost.GetPipelineAsset(); } + + std::vector BuildFramePlans( + const Components::Scene& scene, + Components::CameraComponent* overrideCamera, + const RenderContext& context, + const RenderSurface& surface); + + bool Render(const CameraFramePlan& plan); + bool Render(const std::vector& plans); + bool Render( + const Components::Scene& scene, + Components::CameraComponent* overrideCamera, + const RenderContext& context, + const RenderSurface& surface); + + SceneRenderRequestPlanner m_requestPlanner; + RenderPipelineHost m_pipelineHost; +}; +``` + +再看实现: + +```cpp +std::vector SceneRenderer::BuildFramePlans( + const Components::Scene& scene, + Components::CameraComponent* overrideCamera, + const RenderContext& context, + const RenderSurface& surface) { + const std::vector requests = + m_requestPlanner.BuildRequests( + scene, + overrideCamera, + context, + surface, + m_pipelineHost.GetPipelineAsset()); + return m_pipelineHost.BuildFramePlans(requests); +} + +bool SceneRenderer::Render(const CameraFramePlan& plan) { + return m_pipelineHost.Render(plan); +} + +bool SceneRenderer::Render(const std::vector& plans) { + return m_pipelineHost.Render(plans); +} + +bool SceneRenderer::Render( + const Components::Scene& scene, + Components::CameraComponent* overrideCamera, + const RenderContext& context, + const RenderSurface& surface) { + return Render(BuildFramePlans(scene, overrideCamera, context, surface)); +} +``` + +这段代码表达得很清楚: + +- `SceneRenderer` 不直接发 draw call。 +- 它先让 `SceneRenderRequestPlanner` 生成每个相机的 `CameraRenderRequest`。 +- 再让 `RenderPipelineHost` 把 request 变成 `CameraFramePlan`。 +- 最后再交给 `RenderPipelineHost` 执行。 + +所以你现在的最顶层入口,已经是“先规划,再执行”,而不是“看到相机就直接渲染”。 + +--- + +## 2. `CameraRenderRequest` 和 `CameraFramePlan`:当前渲染层最关键的两层数据 + +很多新手一开始会把这两个结构看成差不多,其实不是。 + +- `CameraRenderRequest` 更像“想渲什么”。 +- `CameraFramePlan` 更像“这一帧这个相机最终准备怎么渲”。 + +### 2.1 `CameraRenderRequest`:来自场景和相机的原始请求 + +```cpp +struct CameraRenderRequest { + const Components::Scene* scene = nullptr; + Components::CameraComponent* camera = nullptr; + RenderContext context; + RenderSurface surface; + DepthOnlyRenderRequest depthOnly; + ShadowCasterRenderRequest shadowCaster; + DirectionalShadowRenderPlan directionalShadow; + PostProcessRenderRequest postProcess; + FinalOutputRenderRequest finalOutput; + ResolvedFinalColorPolicy finalColorPolicy = {}; + ObjectIdRenderRequest objectId; + float cameraDepth = 0.0f; + uint8_t cameraStackOrder = 0; + RenderClearFlags clearFlags = RenderClearFlags::All; + bool hasClearColorOverride = false; + Math::Color clearColorOverride = Math::Color::Black(); + RenderPassSequence* preScenePasses = nullptr; + RenderPassSequence* postScenePasses = nullptr; + RenderPassSequence* overlayPasses = nullptr; + + bool IsValid() const { + return scene != nullptr && + camera != nullptr && + context.IsValid(); + } +}; +``` + +这个结构里有几个重点: + +- 它已经把“主场景渲染、阴影、后处理、最终输出、ObjectId”等请求都装进来了。 +- 它还带了 `RenderPassSequence*`,说明当前系统已经支持在相机主流程周围挂自定义 pass 序列。 +- 这一步还是“请求”,还不是最终确定的图资源和 stage 输出关系。 + +### 2.2 `SceneRenderRequestPlanner`:先收集相机,再让 pipeline asset 改请求 + +```cpp +std::vector SceneRenderRequestPlanner::BuildRequests( + const Components::Scene& scene, + Components::CameraComponent* overrideCamera, + const RenderContext& context, + const RenderSurface& surface, + const RenderPipelineAsset* pipelineAsset) const { + std::vector requests; + const std::vector cameras = + CollectCameras(scene, overrideCamera); + + size_t renderedBaseCameraCount = 0; + for (Components::CameraComponent* camera : cameras) { + CameraRenderRequest request; + if (!SceneRenderRequestUtils::BuildCameraRenderRequest( + scene, + *camera, + context, + surface, + renderedBaseCameraCount, + requests.size(), + request)) { + continue; + } + + if (pipelineAsset != nullptr) { + pipelineAsset->ConfigureCameraRenderRequest( + request, + renderedBaseCameraCount, + requests.size(), + m_directionalShadowPlanningSettings); + } else { + ApplyDefaultRenderPipelineAssetCameraRenderRequestPolicy( + request, + renderedBaseCameraCount, + requests.size(), + m_directionalShadowPlanningSettings); + } + + requests.push_back(request); + if (camera->GetStackType() == Components::CameraStackType::Base) { + ++renderedBaseCameraCount; + } + } + + return requests; +} +``` + +这段代码非常重要,因为它已经体现出“pipeline asset 能参与 planning”这个 SRP 方向的关键思想: + +- 先有一个通用相机请求。 +- 然后交给 `RenderPipelineAsset` 做策略配置。 +- 如果没有自定义 asset,就走默认策略。 + +这和 Unity 的思路是对的。未来 SRP/URP 的很多策略,本质上都应该发生在这一层或下一层,而不是散落到各种具体 pass 里。 + +### 2.3 `RenderPipelineAsset` 现在已经有两个很关键的 planning hook + +```cpp +class RenderPipelineAsset { +public: + virtual ~RenderPipelineAsset() = default; + + virtual std::unique_ptr CreatePipeline() const = 0; + virtual void ConfigurePipeline(RenderPipeline&) const {} + virtual void ConfigureCameraRenderRequest( + CameraRenderRequest& request, + size_t renderedBaseCameraCount, + size_t renderedRequestCount, + const DirectionalShadowPlanningSettings& directionalShadowSettings) const; + virtual FinalColorSettings GetDefaultFinalColorSettings() const { return {}; } + virtual void ConfigureCameraFramePlan(CameraFramePlan& plan) const; +}; +``` + +实现也很关键: + +```cpp +void RenderPipelineAsset::ConfigureCameraRenderRequest( + CameraRenderRequest& request, + size_t renderedBaseCameraCount, + size_t renderedRequestCount, + const DirectionalShadowPlanningSettings& directionalShadowSettings) const { + ApplyDefaultRenderPipelineAssetCameraRenderRequestPolicy( + request, + renderedBaseCameraCount, + renderedRequestCount, + directionalShadowSettings); +} + +void RenderPipelineAsset::ConfigureCameraFramePlan(CameraFramePlan& plan) const { + ApplyDefaultRenderPipelineAssetCameraFramePlanPolicy( + plan, + GetDefaultFinalColorSettings()); +} +``` + +这说明你现在 native 侧已经不是“asset 只负责创建 pipeline”了。 + +它已经开始负责: + +- 配置 `CameraRenderRequest` +- 配置 `CameraFramePlan` + +这就是后面 SRP/URP asset 最该承接的东西。 + +### 2.4 `CameraFramePlan`:每相机最终执行计划 + +```cpp +struct CameraFramePlan { + static RenderSurface BuildGraphManagedIntermediateSurfaceTemplate( + const RenderSurface& surface); + + CameraRenderRequest request = {}; + ShadowCasterRenderRequest shadowCaster = {}; + DirectionalShadowRenderPlan directionalShadow = {}; + PostProcessRenderRequest postProcess = {}; + FinalOutputRenderRequest finalOutput = {}; + ResolvedFinalColorPolicy finalColorPolicy = {}; + RenderPassSequence* preScenePasses = nullptr; + RenderPassSequence* postScenePasses = nullptr; + RenderPassSequence* overlayPasses = nullptr; + CameraFrameColorChainPlan colorChain = {}; + RenderSurface graphManagedSceneSurface = {}; + + static CameraFramePlan FromRequest(const CameraRenderRequest& request); + + bool IsValid() const; + void ConfigureGraphManagedSceneSurface(); + void ClearOwnedPostProcessSequence(); + void SetOwnedPostProcessSequence(std::shared_ptr sequence); + const std::shared_ptr& GetOwnedPostProcessSequence() const { + return m_ownedPostProcessSequence; + } + void ClearOwnedFinalOutputSequence(); + void SetOwnedFinalOutputSequence(std::shared_ptr sequence); + const std::shared_ptr& GetOwnedFinalOutputSequence() const { + return m_ownedFinalOutputSequence; + } + bool UsesGraphManagedSceneColor() const; + bool UsesGraphManagedOutputColor(CameraFrameStage stage) const; + CameraFrameColorSource ResolveStageColorSource(CameraFrameStage stage) const; + bool IsPostProcessStageValid() const; + bool IsFinalOutputStageValid() const; + bool HasFrameStage(CameraFrameStage stage) const; + RenderPassSequence* GetPassSequence(CameraFrameStage stage) const; + const CameraFrameFullscreenStagePlan* GetFullscreenStagePlan(CameraFrameStage stage) const; + const FullscreenPassRenderRequest* GetFullscreenPassRequest(CameraFrameStage stage) const; + const ScenePassRenderRequest* GetScenePassRequest(CameraFrameStage stage) const; + const ObjectIdRenderRequest* GetObjectIdRequest(CameraFrameStage stage) const; + const RenderSurface* GetSharedStageOutputSurface(CameraFrameStage stage) const; + const RenderSurface& GetMainSceneSurface() const; + const RenderSurface& GetFinalCompositedSurface() const; + bool RequiresIntermediateSceneColor() const; + +private: + std::shared_ptr m_ownedPostProcessSequence = {}; + std::shared_ptr m_ownedFinalOutputSequence = {}; +}; +``` + +这个结构不是简单数据包,它其实定义了: + +- 本相机有哪些 stage。 +- 哪些 stage 是 fullscreen chain。 +- 主场景颜色是直接写到最终 surface,还是先写到 graph-managed 中间纹理。 +- 后处理、最终输出分别从哪一个颜色源读取。 + +简单说,`CameraFramePlan` 已经是“这一帧这个相机的渲染组织图纸”。 + +### 2.5 后处理链现在已经不是硬编码死写,它是通过 plan 算出来的 + +```cpp +void PlanCameraFrameFullscreenStages(CameraFramePlan& plan) { + plan.ClearOwnedPostProcessSequence(); + plan.ClearOwnedFinalOutputSequence(); + + if (plan.request.camera == nullptr || + plan.request.context.device == nullptr || + !HasValidColorTarget(plan.request.surface)) { + return; + } + + std::unique_ptr postProcessSequence = + BuildCameraPostProcessPassSequence(plan.request.camera->GetPostProcessPasses()); + std::unique_ptr finalOutputSequence = + BuildFinalColorPassSequence(plan.finalColorPolicy); + + const bool hasPostProcess = + postProcessSequence != nullptr && postProcessSequence->GetPassCount() > 0u; + const bool hasFinalOutput = + finalOutputSequence != nullptr && finalOutputSequence->GetPassCount() > 0u; + if (!hasPostProcess && !hasFinalOutput) { + return; + } + + if (plan.request.surface.GetSampleCount() > 1u) { + Debug::Logger::Get().Error( + Debug::LogCategory::Rendering, + "SceneRenderer fullscreen post-process/final-output chain currently requires a single-sample main scene surface"); + return; + } + + if (hasPostProcess) { + plan.SetOwnedPostProcessSequence( + SharePassSequence(std::move(postProcessSequence))); + plan.colorChain.usesGraphManagedSceneColor = true; + plan.colorChain.postProcess.source = CameraFrameColorSource::MainSceneColor; + plan.colorChain.postProcess.usesGraphManagedOutputColor = hasFinalOutput; + if (!hasFinalOutput) { + plan.postProcess.destinationSurface = plan.request.surface; + } + } + + if (hasFinalOutput) { + plan.SetOwnedFinalOutputSequence( + SharePassSequence(std::move(finalOutputSequence))); + plan.colorChain.usesGraphManagedSceneColor = true; + plan.colorChain.finalOutput.source = + hasPostProcess + ? CameraFrameColorSource::PostProcessColor + : CameraFrameColorSource::MainSceneColor; + plan.finalOutput.destinationSurface = plan.request.surface; + } + + if (plan.UsesGraphManagedOutputColor(CameraFrameStage::MainScene)) { + plan.ConfigureGraphManagedSceneSurface(); + } +} +``` + +这段代码说明当前系统已经有“颜色链规划”的概念: + +- 如果要后处理,主场景颜色不应该直接落到最终 backbuffer。 +- 它先变成 graph-managed scene color。 +- 后处理从 `MainSceneColor` 读。 +- 如果还有 final output,则后处理结果也可以继续留在 graph-managed 输出里。 + +也就是说,你现在已经不是“后处理 pass 直接绑死到主渲染函数后面”了。 + +这对将来做 URP 风格 renderer 很重要。 + +--- + +## 3. 为什么要把一帧拆成 `CameraFrameStage` + +### 3.1 现在的 stage 枚举 + +```cpp +enum class CameraFrameStage : uint8_t { + PreScenePasses, + ShadowCaster, + DepthOnly, + MainScene, + PostProcess, + FinalOutput, + ObjectId, + PostScenePasses, + OverlayPasses +}; + +enum class CameraFrameStageExecutionKind : uint8_t { + Sequence, + StandalonePass, + MainScenePipeline +}; +``` + +再看 stage 分类逻辑: + +```cpp +inline constexpr CameraFrameStageExecutionKind GetCameraFrameStageExecutionKind( + CameraFrameStage stage) { + switch (stage) { + case CameraFrameStage::PreScenePasses: + case CameraFrameStage::PostProcess: + case CameraFrameStage::FinalOutput: + case CameraFrameStage::PostScenePasses: + case CameraFrameStage::OverlayPasses: + return CameraFrameStageExecutionKind::Sequence; + case CameraFrameStage::ShadowCaster: + case CameraFrameStage::DepthOnly: + case CameraFrameStage::ObjectId: + return CameraFrameStageExecutionKind::StandalonePass; + default: + return CameraFrameStageExecutionKind::MainScenePipeline; + } +} +``` + +这段代码特别值得新手反复看几遍。 + +它说明你现在的“每相机渲染流程”不是一串散装 if/else,而是有明确 stage 类型的: + +- `Sequence`:一串 pass 序列。 +- `StandalonePass`:单个独立 pass,比如阴影、深度、ObjectId。 +- `MainScenePipeline`:主场景阶段,由整个 pipeline 负责。 + +也就是说,`MainScene` 不是普通 pass,它是“管线主体”。 + +这个抽象是对的。未来 SRP 的主入口,大概率也应该接在这里。 + +### 3.2 每个 stage 对 graph 的资源语义也不一样 + +```cpp +inline constexpr bool DoesCameraFrameStageGraphOwnColorTransitions( + CameraFrameStage stage) { + return stage == CameraFrameStage::MainScene || + stage == CameraFrameStage::PostProcess || + stage == CameraFrameStage::FinalOutput || + stage == CameraFrameStage::ObjectId; +} + +inline constexpr bool DoesCameraFrameStageGraphOwnDepthTransitions( + CameraFrameStage stage) { + return stage == CameraFrameStage::ShadowCaster || + stage == CameraFrameStage::DepthOnly || + stage == CameraFrameStage::MainScene || + stage == CameraFrameStage::ObjectId; +} +``` + +这说明 stage 不只是“逻辑分段”,还决定了 graph 是否接管状态转换。 + +换句话说,你现在的系统已经在 stage 层面开始表达资源所有权。 + +--- + +## 4. `CameraRenderer`:真正把一个 `CameraFramePlan` 跑起来的人 + +### 4.1 `CameraRenderer::Render` 的职责 + +```cpp +bool CameraRenderer::Render( + const CameraFramePlan& plan) { + if (!plan.IsValid() || m_pipeline == nullptr) { + Debug::Logger::Get().Error( + Debug::LogCategory::Rendering, + "CameraRenderer::Render failed: plan invalid or pipeline missing"); + return false; + } + + const RenderSurface& mainSceneSurface = plan.GetMainSceneSurface(); + if (mainSceneSurface.GetRenderAreaWidth() == 0 || + mainSceneSurface.GetRenderAreaHeight() == 0) { + Debug::Logger::Get().Error( + Debug::LogCategory::Rendering, + "CameraRenderer::Render failed: main scene surface render area is empty"); + return false; + } + if (plan.request.depthOnly.IsRequested() && + !plan.request.depthOnly.IsValid()) { + Debug::Logger::Get().Error( + Debug::LogCategory::Rendering, + "CameraRenderer::Render failed: depth-only request invalid"); + return false; + } + if (plan.postProcess.IsRequested() && + !plan.IsPostProcessStageValid()) { + Debug::Logger::Get().Error( + Debug::LogCategory::Rendering, + "CameraRenderer::Render failed: post-process request invalid"); + return false; + } + if (plan.UsesGraphManagedOutputColor(CameraFrameStage::MainScene) && + (m_pipeline == nullptr || + !m_pipeline->SupportsStageRenderGraph(CameraFrameStage::MainScene))) { + Debug::Logger::Get().Error( + Debug::LogCategory::Rendering, + "CameraRenderer::Render failed: graph-managed main scene color requires pipeline main-scene render-graph support"); + return false; + } + if (plan.finalOutput.IsRequested() && + !plan.IsFinalOutputStageValid()) { + Debug::Logger::Get().Error( + Debug::LogCategory::Rendering, + "CameraRenderer::Render failed: final-output request invalid"); + return false; + } + if (plan.request.objectId.IsRequested() && + !plan.request.objectId.IsValid()) { + Debug::Logger::Get().Error( + Debug::LogCategory::Rendering, + "CameraRenderer::Render failed: object-id request invalid"); + return false; + } + + DirectionalShadowExecutionState shadowState = {}; + if (m_directionalShadowRuntime == nullptr || + !m_directionalShadowRuntime->ResolveExecutionState( + plan, + *m_pipeline, + shadowState)) { + Debug::Logger::Get().Error( + Debug::LogCategory::Rendering, + "CameraRenderer::Render failed: DirectionalShadowRuntime::ResolveExecutionState returned false"); + return false; + } + + RenderSceneData sceneData = {}; + if (!BuildSceneDataForPlan(plan, shadowState, sceneData)) { + Debug::Logger::Get().Error( + Debug::LogCategory::Rendering, + "CameraRenderer::Render failed: BuildSceneDataForPlan returned false"); + return false; + } + + if (!ExecuteRenderPlan(plan, shadowState, sceneData)) { + Debug::Logger::Get().Error( + Debug::LogCategory::Rendering, + "CameraRenderer::Render failed: ExecuteRenderPlan returned false"); + return false; + } + + return true; +} +``` + +这段代码告诉你: + +- `CameraRenderer` 先做合法性检查。 +- 再解析阴影执行状态。 +- 再从场景抽取 `RenderSceneData`。 +- 最后把 plan 丢给 `RenderGraph` 执行主链。 + +所以 `CameraRenderer` 的本质是: + +> 把“某个相机的一帧计划”转换成“可执行的渲染数据和图执行流程”。 + +### 4.2 阴影不是直接写死在主渲染里,而是先解析成执行状态 + +```cpp +bool DirectionalShadowRuntime::ResolveExecutionState( + const CameraFramePlan& plan, + const RenderPipeline& pipeline, + DirectionalShadowExecutionState& outShadowState) { + outShadowState = {}; + outShadowState.shadowCasterRequest = plan.shadowCaster; + + if (outShadowState.shadowCasterRequest.IsRequested()) { + return outShadowState.shadowCasterRequest.IsValid(); + } + + if (!plan.directionalShadow.IsValid()) { + return true; + } + + const DirectionalShadowSurfaceAllocation* shadowAllocation = + m_surfaceCache.Resolve(plan.request.context, plan.directionalShadow); + if (shadowAllocation == nullptr || !shadowAllocation->IsValid()) { + return false; + } + + return pipeline.ConfigureDirectionalShadowExecutionState( + plan, + *shadowAllocation, + outShadowState); +} +``` + +这说明当前阴影执行也已经不是“主渲染里顺手写几行阴影代码”。 + +它现在已经有: + +- `DirectionalShadowRenderPlan` +- `DirectionalShadowRuntime` +- `DirectionalShadowExecutionState` + +这三个层次。 + +这也是为什么我一直认为你后面把阴影策略上移到 URP-like 包层是可行的,因为 native 侧已经在往“执行内核”和“组织策略”分离。 + +--- + +## 5. 什么是 `RenderGraph` + +如果你是新手,我先不用术语,先用一句人话讲: + +> `RenderGraph` 就是“先声明这一帧要用哪些纹理、哪些 pass 读写它们,再由系统统一算顺序、生命周期、状态切换,最后统一执行”。 + +和传统写法的区别是: + +- 传统写法:你边想边画,顺手自己做 barrier、自己管中间 RT。 +- `RenderGraph`:你先把“这帧要发生什么”录下来,然后系统再编译执行。 + +### 5.1 你的 `RenderGraph` 现在的核心数据结构 + +```cpp +class RenderGraph { +public: + void Reset(); + + size_t GetTextureCount() const { + return m_textures.size(); + } + + size_t GetPassCount() const { + return m_passes.size(); + } + +private: + struct TextureResource { + Containers::String name; + RenderGraphTextureDesc desc = {}; + RenderGraphTextureKind kind = RenderGraphTextureKind::Transient; + RHI::RHIResourceView* importedView = nullptr; + RenderGraphImportedTextureOptions importedOptions = {}; + }; + + struct TextureAccess { + RenderGraphTextureHandle texture = {}; + RenderGraphAccessMode mode = RenderGraphAccessMode::Read; + RenderGraphTextureAspect aspect = RenderGraphTextureAspect::Color; + }; + + struct PassNode { + Containers::String name; + RenderGraphPassType type = RenderGraphPassType::Raster; + std::vector accesses; + RenderGraphExecuteCallback executeCallback = {}; + }; + + std::vector m_textures; + std::vector m_passes; +}; +``` + +这段代码已经把你的 graph 本质暴露得很清楚了: + +- 图里现在的资源对象主要是 `Texture`。 +- 资源分两类:`Imported` 和 `Transient`。 +- pass 记录的是“访问声明”和“执行回调”。 + +翻成人话: + +- `Imported`:外面已经存在的纹理,比如 swapchain/backbuffer、已有 depth。 +- `Transient`:这帧 graph 临时创建的中间纹理。 + +### 5.2 `RenderGraph` 的 handle 和描述结构 + +```cpp +struct RenderGraphTextureHandle { + Core::uint32 index = kInvalidRenderGraphHandle; + + bool IsValid() const { + return index != kInvalidRenderGraphHandle; + } +}; + +struct RenderGraphTextureDesc { + Core::uint32 width = 0u; + Core::uint32 height = 0u; + Core::uint32 format = static_cast(RHI::Format::Unknown); + Core::uint32 textureType = static_cast(RHI::TextureType::Texture2D); + Core::uint32 sampleCount = 1u; + Core::uint32 sampleQuality = 0u; + + bool IsValid() const { + return width > 0u && + height > 0u && + format != static_cast(RHI::Format::Unknown) && + sampleCount > 0u; + } +}; + +struct RenderGraphImportedTextureOptions { + RHI::ResourceStates initialState = RHI::ResourceStates::Common; + RHI::ResourceStates finalState = RHI::ResourceStates::Common; + bool graphOwnsTransitions = false; +}; +``` + +这里有两个关键点: + +1. graph 对资源的引用是 handle,不是裸指针到处飞。 +2. imported 资源可以声明“graph 是否接管状态转换”。 + +这就是为什么你现在可以把一部分旧 surface 继续接进来,同时又让 graph 接管新建 transient 纹理。 + +### 5.3 builder API:先记录,不立即执行 + +```cpp +class RenderGraphPassBuilder { +public: + void ReadTexture(RenderGraphTextureHandle texture); + void WriteTexture(RenderGraphTextureHandle texture); + void ReadDepthTexture(RenderGraphTextureHandle texture); + void WriteDepthTexture(RenderGraphTextureHandle texture); + void SetExecuteCallback(RenderGraphExecuteCallback callback); +}; + +class RenderGraphBuilder { +public: + explicit RenderGraphBuilder(RenderGraph& graph) + : m_graph(graph) { + } + + void Reset(); + + RenderGraphTextureHandle ImportTexture( + const Containers::String& name, + const RenderGraphTextureDesc& desc, + RHI::RHIResourceView* importedView = nullptr, + const RenderGraphImportedTextureOptions& importedOptions = {}); + + RenderGraphTextureHandle CreateTransientTexture( + const Containers::String& name, + const RenderGraphTextureDesc& desc); + + RenderGraphPassHandle AddRasterPass( + const Containers::String& name, + const std::function& setup); + + RenderGraphPassHandle AddComputePass( + const Containers::String& name, + const std::function& setup); +}; +``` + +实现也很直接: + +```cpp +RenderGraphPassHandle RenderGraphBuilder::AddPass( + const Containers::String& name, + RenderGraphPassType type, + const std::function& setup) { + RenderGraph::PassNode pass = {}; + pass.name = name; + pass.type = type; + m_graph.m_passes.push_back(pass); + + RenderGraphPassHandle handle = {}; + handle.index = static_cast(m_graph.m_passes.size() - 1u); + + if (setup) { + RenderGraphPassBuilder passBuilder(&m_graph, handle); + setup(passBuilder); + } + + return handle; +} +``` + +这说明 `RenderGraphBuilder` 现在干的事很朴素: + +- 建一个 pass 节点。 +- 在 setup 里登记读写依赖。 +- 记录执行回调。 + +这里没有魔法。 + +### 5.4 compiler 做了什么 + +`RenderGraphCompiler` 的核心不是“生成 draw call”,而是: + +1. 校验资源描述是否合法。 +2. 根据读写关系建立 pass 依赖。 +3. 拓扑排序。 +4. 计算每个纹理的生命周期。 +5. 计算每次访问需要的资源状态。 +6. 给出状态转换计划。 + +下面这一段是最核心的依赖构建逻辑: + +```cpp +for (Core::uint32 passIndex = 0u; passIndex < static_cast(passCount); ++passIndex) { + const RenderGraph::PassNode& pass = graph.m_passes[passIndex]; + for (const RenderGraph::TextureAccess& access : pass.accesses) { + if (!access.texture.IsValid() || access.texture.index >= textureCount) { + WriteError( + Containers::String("RenderGraph pass '") + pass.name + + "' references an invalid texture handle", + outErrorMessage); + return false; + } + + const RenderGraph::TextureResource& texture = graph.m_textures[access.texture.index]; + std::vector& readers = lastReaders[access.texture.index]; + Core::uint32& writer = lastWriter[access.texture.index]; + + if (access.mode == RenderGraphAccessMode::Read) { + if (texture.kind == RenderGraphTextureKind::Transient && + writer == kInvalidRenderGraphHandle) { + WriteError( + Containers::String("RenderGraph transient texture '") + texture.name + + "' is read before any pass writes it", + outErrorMessage); + return false; + } + + addEdge(writer, passIndex); + addUniqueReader(readers, passIndex); + continue; + } + + addEdge(writer, passIndex); + for (Core::uint32 readerPassIndex : readers) { + addEdge(readerPassIndex, passIndex); + } + readers.clear(); + writer = passIndex; + } +} +``` + +它的意思非常直白: + +- 读一个纹理,就依赖上一次写它的 pass。 +- 写一个纹理,就依赖上一次写它的 pass,也依赖所有还没被新写覆盖的 reader。 + +这就是一个最基础但正确的读写依赖模型。 + +然后做拓扑排序: + +```cpp +while (executionOrder.size() < passCount) { + bool progressed = false; + for (Core::uint32 passIndex = 0u; passIndex < static_cast(passCount); ++passIndex) { + if (emitted[passIndex] || incomingEdgeCount[passIndex] != 0u) { + continue; + } + + emitted[passIndex] = true; + executionOrder.push_back(passIndex); + for (Core::uint32 dependentPassIndex : outgoingEdges[passIndex]) { + if (incomingEdgeCount[dependentPassIndex] > 0u) { + --incomingEdgeCount[dependentPassIndex]; + } + } + + progressed = true; + break; + } + + if (!progressed) { + WriteError( + "RenderGraph failed to compile because pass dependencies contain a cycle", + outErrorMessage); + outCompiledGraph.Reset(); + return false; + } +} +``` + +如果你是新手,你只要记住: + +> 编译器负责把“记录顺序”变成“正确执行顺序”。 + +### 5.5 executor 做了什么 + +执行器的主函数不长: + +```cpp +bool RenderGraphExecutor::Execute( + const CompiledRenderGraph& graph, + const RenderContext& renderContext, + Containers::String* outErrorMessage) { + if (outErrorMessage != nullptr) { + outErrorMessage->Clear(); + } + + RenderGraphRuntimeResources runtimeResources(graph); + if (!runtimeResources.Initialize(renderContext, outErrorMessage)) { + return false; + } + + RenderGraphExecutionContext executionContext = { + renderContext, + &runtimeResources + }; + for (const CompiledRenderGraph::CompiledPass& pass : graph.m_passes) { + if (!runtimeResources.TransitionPassResources(pass, renderContext, outErrorMessage)) { + return false; + } + + if (pass.executeCallback) { + pass.executeCallback(executionContext); + } + } + + if (!runtimeResources.TransitionGraphOwnedImportsToFinalStates( + renderContext, + outErrorMessage)) { + return false; + } + + return true; +} +``` + +它做三件事: + +1. 初始化 runtime 资源。 +2. 每个 pass 执行前先做资源状态切换。 +3. 执行 pass callback。 +4. 结束时把 graph 接管的 imported 资源转到声明的最终状态。 + +而 transient 纹理的创建逻辑在这里: + +```cpp +if (texture.kind != RenderGraphTextureKind::Transient || !lifetime.used) { + continue; +} + +if (renderContext.device == nullptr) { + if (outErrorMessage != nullptr) { + *outErrorMessage = + Containers::String("RenderGraph cannot allocate transient texture without a valid device: ") + + texture.name; + } + Reset(); + return false; +} + +if (!CreateTransientTexture( + renderContext, + static_cast(textureIndex), + texture, + m_textureAllocations[textureIndex])) { + if (outErrorMessage != nullptr) { + *outErrorMessage = + Containers::String("RenderGraph failed to allocate transient texture: ") + + texture.name; + } + Reset(); + return false; +} +``` + +所以你现在这个 graph 已经真的会: + +- 分配 transient RT +- 创建 RTV/DSV/SRV/UAV +- 自动做 barrier +- 执行 pass callback + +它不是“只有接口壳子”。 + +### 5.6 `RenderGraphBlackboard` 是什么 + +```cpp +class RenderGraphBlackboard { +public: + template + T& Emplace(Args&&... args) { + using StorageType = std::remove_cv_t>; + auto value = std::make_shared(std::forward(args)...); + StorageType& reference = *value; + m_entries[std::type_index(typeid(StorageType))] = std::move(value); + return reference; + } + + template + T* TryGet() { + using StorageType = std::remove_cv_t>; + const auto entryIt = m_entries.find(std::type_index(typeid(StorageType))); + return entryIt != m_entries.end() + ? static_cast(entryIt->second.get()) + : nullptr; + } + + void Clear() { + m_entries.clear(); + } + +private: + std::unordered_map> m_entries; +}; +``` + +你可以把 blackboard 理解成: + +> 这一帧 graph 录制期间的“共享笔记本”。 + +谁往里写? + +- 某个 stage 把自己产出的颜色、深度、阴影贴图 handle 发布进去。 + +谁从里读? + +- 后面的 stage、feature、pipeline graph builder。 + +### 5.7 现在这个 `RenderGraph` 已经做了什么,还没做什么 + +已经做了: + +- texture import / transient texture +- raster / compute pass 记录 +- 读写依赖分析 +- 拓扑排序 +- 生命周期计算 +- 状态切换 +- transient 资源创建 +- blackboard + +还没做或者还很轻量的: + +- buffer 资源图管理 +- pass culling +- 资源别名复用 +- barrier batching/优化 +- async compute / multi-queue +- subresource 级别状态跟踪 +- 更强的 lifetime aliasing 优化 + +所以当前结论应该很准确: + +> 你已经有一个可用的 native `RenderGraph` 内核,但它还是 v1,而不是终态。 + +--- + +## 6. 当前项目是怎么把一帧相机录进 `RenderGraph` 的 + +这一段是你当前渲染模块最核心、也最容易把新人看晕的地方。 + +我把它拆成 5 步讲。 + +### 6.1 第一步:先创建 graph,再录制所有 stage + +```cpp +bool ExecuteCameraFrameRenderGraphPlan( + const CameraFramePlan& plan, + const DirectionalShadowExecutionState& shadowState, + const RenderSceneData& sceneData, + RenderPipeline* pipeline) { + RenderGraph graph = {}; + RenderGraphBuilder graphBuilder(graph); + RenderGraphBlackboard blackboard = {}; + + CameraFrameExecutionState executionState = {}; + executionState.pipeline = pipeline; + + bool stageExecutionSucceeded = true; + if (!RecordCameraFrameRenderGraphStages( + plan, + shadowState, + sceneData, + executionState, + graphBuilder, + blackboard, + stageExecutionSucceeded)) { + return false; + } + + CompiledRenderGraph compiledGraph = {}; + Containers::String errorMessage; + if (!RenderGraphCompiler::Compile(graph, compiledGraph, &errorMessage)) { + Debug::Logger::Get().Error( + Debug::LogCategory::Rendering, + Containers::String("CameraRenderer::Render failed: RenderGraph compile failed: ") + + errorMessage); + return false; + } + + if (!RenderGraphExecutor::Execute(compiledGraph, plan.request.context, &errorMessage)) { + Debug::Logger::Get().Error( + Debug::LogCategory::Rendering, + Containers::String("CameraRenderer::Render failed: RenderGraph execute failed: ") + + errorMessage); + return false; + } + + return stageExecutionSucceeded; +} +``` + +所以当前每个相机的执行过程,其实就是: + +1. 建图。 +2. 让各 stage 往图里录。 +3. 编译。 +4. 执行。 + +### 6.2 第二步:按 stage 顺序录 + +```cpp +bool RecordCameraFrameRenderGraphStages( + const CameraFramePlan& plan, + const DirectionalShadowExecutionState& shadowState, + const RenderSceneData& sceneData, + CameraFrameExecutionState& executionState, + RenderGraphBuilder& graphBuilder, + RenderGraphBlackboard& blackboard, + bool& stageExecutionSucceeded) { + CameraFrameRenderGraphFrameData& frameData = + EmplaceCameraFrameRenderGraphFrameData(blackboard); + RenderGraphImportedTextureRegistry importedTextures = {}; + CameraFrameRenderGraphBuilderContext builderContext = { + graphBuilder, + blackboard, + frameData, + importedTextures, + executionState, + stageExecutionSucceeded + }; + const CameraFrameRenderGraphStageContext context = { + plan, + shadowState, + sceneData, + builderContext + }; + for (const CameraFrameStageInfo& stageInfo : kOrderedCameraFrameStages) { + if (!plan.HasFrameStage(stageInfo.stage)) { + continue; + } + + if (!RecordCameraFrameRenderGraphStage(stageInfo.stage, context)) { + return false; + } + } + + return true; +} +``` + +注意这里不是“看谁想录就录”,而是严格按 `kOrderedCameraFrameStages` 顺序。 + +这意味着: + +- stage 顺序是引擎定义的 frame contract。 +- 各 stage 再在内部决定自己是 sequence、standalone pass、pipeline graph 还是 fallback。 + +### 6.3 第三步:每个 stage 先建立自己的 graph build state + +```cpp +CameraFrameStageGraphBuildState BuildCameraFrameStageGraphBuildState( + CameraFrameStage stage, + const CameraFrameRenderGraphStageContext& context) { + CameraFrameStageGraphBuildState stageState = {}; + stageState.stage = stage; + stageState.stageName = Containers::String(GetCameraFrameStageName(stage)); + stageState.stageSequence = context.plan.GetPassSequence(stage); + + const RenderPassContext stagePassContext = + BuildCameraFrameStagePassContext( + stage, + context.plan, + context.shadowState, + context.sceneData); + stageState.surfaceTemplate = stagePassContext.surface; + stageState.hasSourceSurface = stagePassContext.sourceSurface != nullptr; + if (stageState.hasSourceSurface) { + stageState.sourceSurfaceTemplate = *stagePassContext.sourceSurface; + } + stageState.sourceColorView = stagePassContext.sourceColorView; + stageState.sourceColorState = stagePassContext.sourceColorState; + stageState.sourceSurface = + ImportRenderGraphSurface( + context.builder.graphBuilder, + context.builder.importedTextures, + stageState.stageName + ".Source", + stagePassContext.sourceSurface, + RenderGraphSurfaceImportUsage::Source, + IsCameraFrameFullscreenSequenceStage(stage)); + stageState.outputSurface = + ImportRenderGraphSurface( + context.builder.graphBuilder, + context.builder.importedTextures, + stageState.stageName + ".Output", + &stagePassContext.surface, + RenderGraphSurfaceImportUsage::Output, + DoesCameraFrameStageGraphOwnColorTransitions(stage), + DoesCameraFrameStageGraphOwnDepthTransitions(stage)); + stageState.outputColor = + ResolveStageOutputColorHandle( + stage, + context.plan, + stageState.stageName, + stagePassContext, + stageState.outputSurface, + context.builder.graphBuilder); + return stageState; +} +``` + +这一步你一定要看懂,因为它解释了当前 graph 录制时的资源来源: + +- `sourceSurface`:这个 stage 读谁。 +- `outputSurface`:这个 stage 写谁。 +- `outputColor`:这个 stage 的主颜色输出 handle。 + +而 `outputColor` 不一定是 imported 的 surface color,它也可能是 graph 新建的 transient texture。 + +### 6.4 第四步:stage 先发布资源,再决定怎么录 + +```cpp +bool RecordCameraFrameRenderGraphStage( + CameraFrameStage stage, + const CameraFrameRenderGraphStageContext& context) { + const CameraFrameStageGraphBuildState stageState = + BuildCameraFrameStageGraphBuildState( + stage, + context); + PublishCameraFrameStageGraphResources(stageState, context); + + for (CameraFrameStageRecordHandler handler : kCameraFrameStageRecordHandlers) { + bool stageHandled = false; + if (!handler(stageState, context, stageHandled)) { + return false; + } + if (stageHandled) { + return true; + } + } + + AddCameraFrameStageFallbackRasterPass(stageState, context); + return true; +} +``` + +这里的核心思想是: + +1. 先把资源语义建立起来。 +2. 再看这个 stage 是由哪种方式录入 graph。 +3. 如果都不支持,就走 fallback raster pass adapter。 + +这是一种很好的“渐进式迁移”结构。 + +### 6.5 第五步:现在一共有 4 种录制路径 + +#### 路径 A:sequence stage + +```cpp +bool TryRecordCameraFrameStageSequence( + const CameraFrameStageGraphBuildState& stageState, + const CameraFrameRenderGraphStageContext& context, + bool& handled) { + CameraFrameRenderGraphBuilderContext& builder = context.builder; + if (stageState.stageSequence == nullptr) { + handled = false; + return true; + } + + handled = true; + const CameraFrameRenderGraphSourceBinding sourceBinding = + BuildCameraFrameStageGraphSourceBinding(stageState); + const bool recordResult = + IsCameraFrameFullscreenSequenceStage(stageState.stage) + ? [&]() { + RenderGraphTextureHandle currentSourceColor = {}; + const CameraFrameRenderGraphSourceBinding fullscreenBinding = + ResolveCameraFrameFullscreenStageGraphSourceBinding( + context.plan, + stageState.stage, + stageState.surfaceTemplate, + sourceBinding.sourceSurfaceTemplate, + sourceBinding.sourceColorView, + sourceBinding.sourceColorState, + sourceBinding.sourceColor, + &builder.blackboard); + currentSourceColor = fullscreenBinding.sourceColor; + return RecordStageSequencePasses( + stageState.stage, + stageState.stageName, + stageState.stageSequence, + builder.executionState, + context.plan.request.context, + builder.stageExecutionSucceeded, + [&context, &stageState, &fullscreenBinding, ¤tSourceColor]( + RenderPass& pass, + size_t passIndex, + const Containers::String& passName, + const RenderPassGraphBeginCallback& beginSequencePass) { + return RecordCameraFrameFullscreenSequenceStageGraphPass( + context, + stageState, + passName, + fullscreenBinding, + stageState.outputColor, + passIndex, + stageState.stageSequence->GetPassCount(), + currentSourceColor, + beginSequencePass, + pass); + }); + }() + : RecordStageSequencePasses( + stageState.stage, + stageState.stageName, + stageState.stageSequence, + builder.executionState, + context.plan.request.context, + builder.stageExecutionSucceeded, + [&context, &stageState]( + RenderPass& pass, + size_t, + const Containers::String& passName, + const RenderPassGraphBeginCallback& beginSequencePass) { + return RecordCameraFrameRegularSequenceStageRenderGraphPass( + context, + stageState, + passName, + stageState.outputSurface, + beginSequencePass, + pass); + }); + if (!recordResult) { + Debug::Logger::Get().Error( + Debug::LogCategory::Rendering, + Containers::String("CameraRenderer::Render failed: pass-sequence graph recording returned false for ") + + stageState.stageName); + return false; + } + + return true; +} +``` + +这段代码有两个重点: + +1. 普通 sequence 和 fullscreen sequence 是分开处理的。 +2. fullscreen sequence 会维护 `currentSourceColor`,也就是“上一段 pass 的输出接下一段 pass 的输入”。 + +这就是后处理链在 graph 里的真正组织方式。 + +#### 路径 B:standalone render pass + +```cpp +bool TryRecordCameraFrameStageStandaloneRenderGraphPass( + const CameraFrameStageGraphBuildState& stageState, + const CameraFrameRenderGraphStageContext& context, + bool& handled) { + CameraFrameRenderGraphBuilderContext& builder = context.builder; + RenderPass* const standaloneStagePass = + ResolveCameraFrameStandaloneStagePass( + stageState.stage, + builder.executionState); + if (standaloneStagePass == nullptr || + !standaloneStagePass->SupportsRenderGraph()) { + handled = false; + return true; + } + + handled = true; + const RenderSceneData stageSceneData = + BuildCameraFrameStandaloneStageSceneData( + stageState.stage, + context, + stageState.surfaceTemplate); + if (!RecordCameraFrameStandaloneStageRenderGraphPass( + context, + stageState, + stageSceneData, + *standaloneStagePass, + builder.stageExecutionSucceeded)) { + Debug::Logger::Get().Error( + Debug::LogCategory::Rendering, + Containers::String("CameraRenderer::Render failed: RenderPass::RecordRenderGraph returned false for ") + + stageState.stageName); + return false; + } + + return true; +} +``` + +这条路径主要对应: + +- `ShadowCaster` +- `DepthOnly` +- `ObjectId` + +它们通常不是整个 pipeline 主体,但可以由某个单独 pass 实现。 + +#### 路径 C:pipeline stage graph + +```cpp +bool TryRecordCameraFramePipelineStageGraphPass( + const CameraFrameStageGraphBuildState& stageState, + const CameraFrameRenderGraphStageContext& context, + bool& handled) { + CameraFrameRenderGraphBuilderContext& builder = context.builder; + if (!SupportsCameraFramePipelineGraphRecording(stageState.stage) || + builder.executionState.pipeline == nullptr || + !builder.executionState.pipeline->SupportsStageRenderGraph( + stageState.stage)) { + handled = false; + return true; + } + + handled = true; + if (!RecordCameraFramePipelineStageGraphPass( + context, + stageState, + *builder.executionState.pipeline)) { + Debug::Logger::Get().Error( + Debug::LogCategory::Rendering, + "CameraRenderer::Render failed: RenderPipeline::RecordStageRenderGraph returned false"); + return false; + } + + return true; +} +``` + +当前只有 `MainScene` 会走这条路径。 + +这也是你现在 native SRP 接缝最关键的落点。 + +#### 路径 D:fallback raster pass adapter + +```cpp +void AddCameraFrameStageFallbackRasterPass( + const CameraFrameStageGraphBuildState& stageState, + const CameraFrameRenderGraphStageContext& context) { + CameraFrameRenderGraphBuilderContext& builder = context.builder; + const CameraFrameStageGraphBuildState capturedStageState = stageState; + const CameraFrameRenderGraphStageContext capturedContext = context; + CameraFrameExecutionState* const executionState = &builder.executionState; + bool* const stageExecutionSucceeded = &builder.stageExecutionSucceeded; + builder.graphBuilder.AddRasterPass( + capturedStageState.stageName, + [capturedStageState, capturedContext, executionState, stageExecutionSucceeded]( + RenderGraphPassBuilder& passBuilder) { + RecordCameraFrameStageFallbackPassIO( + capturedStageState, + passBuilder); + passBuilder.SetExecuteCallback( + [capturedStageState, capturedContext, executionState, stageExecutionSucceeded]( + const RenderGraphExecutionContext& executionContext) { + if (!*stageExecutionSucceeded) { + return; + } + + *stageExecutionSucceeded = + ExecuteCameraFrameStageFallbackPass( + capturedStageState, + capturedContext, + *executionState, + executionContext); + }); + }); +} +``` + +这个 fallback 很重要,它意味着: + +> 即使某个 pass 还没完全 graph-native 化,也能先挂进整个相机级别的 RenderGraph 主链里。 + +这个设计非常实用,因为它支持渐进重构,而不是一刀切重写所有 pass。 + +--- + +## 7. `RenderPassGraphContract`:旧式 pass 如何接进 graph + +很多人第一次看 graph 系统时会有一个问题: + +> 如果老 pass 只有 `Execute()`,那它怎么进 graph? + +答案就是这个 adapter 层。 + +### 7.1 `RenderPass` 接口本身同时支持立即执行和 graph 录制 + +```cpp +class RenderPass { +public: + virtual ~RenderPass() = default; + + virtual const char* GetName() const = 0; + + virtual bool Initialize(const RenderContext&) { + return true; + } + + virtual void Shutdown() { + } + + virtual bool SupportsRenderGraph() const { + return false; + } + + virtual bool RecordRenderGraph( + const RenderPassRenderGraphContext&) { + return false; + } + + virtual bool Execute(const RenderPassContext& context) = 0; +}; +``` + +这个接口设计得很关键: + +- 老 pass 只实现 `Execute()` 也能活。 +- 新 pass 可以实现 `RecordRenderGraph()`。 +- 引擎中间层可以决定走哪条路径。 + +### 7.2 adapter 的核心逻辑 + +```cpp +bool RecordCallbackRasterRenderPass( + const RenderPassRenderGraphContext& context, + const RenderPassGraphIO& io, + RenderPassGraphExecutePassCallback executePassCallback, + std::vector additionalReadTextures) { + if (!executePassCallback) { + return false; + } + + const Containers::String passName = context.passName; + const RenderContext renderContext = context.renderContext; + const std::shared_ptr sceneData = + std::make_shared(context.sceneData); + const RenderSurface surface = context.surface; + const bool hasSourceSurface = context.sourceSurface != nullptr; + const RenderSurface sourceSurface = + hasSourceSurface ? *context.sourceSurface : RenderSurface(); + RHI::RHIResourceView* const sourceColorView = context.sourceColorView; + const RHI::ResourceStates sourceColorState = context.sourceColorState; + const RenderGraphTextureHandle sourceColorTexture = context.sourceColorTexture; + const std::vector colorTargets = context.colorTargets; + const RenderGraphTextureHandle depthTarget = context.depthTarget; + bool* const executionSucceeded = context.executionSucceeded; + const RenderPassGraphBeginCallback beginPassCallback = context.beginPassCallback; + const RenderPassGraphEndCallback endPassCallback = context.endPassCallback; + context.graphBuilder.AddRasterPass( + passName, + [renderContext, + sceneData, + surface, + hasSourceSurface, + sourceSurface, + sourceColorView, + sourceColorState, + sourceColorTexture, + colorTargets, + depthTarget, + executionSucceeded, + beginPassCallback, + endPassCallback, + executePassCallback, + additionalReadTextures, + io]( + RenderGraphPassBuilder& passBuilder) { + if (io.readSourceColor && sourceColorTexture.IsValid()) { + passBuilder.ReadTexture(sourceColorTexture); + } + + for (RenderGraphTextureHandle readTexture : additionalReadTextures) { + if (readTexture.IsValid()) { + passBuilder.ReadTexture(readTexture); + } + } + + if (io.writeColor) { + for (RenderGraphTextureHandle colorTarget : colorTargets) { + if (colorTarget.IsValid()) { + passBuilder.WriteTexture(colorTarget); + } + } + } + + if (io.writeDepth && depthTarget.IsValid()) { + passBuilder.WriteDepthTexture(depthTarget); + } + + passBuilder.SetExecuteCallback( + [renderContext, + sceneData, + surface, + hasSourceSurface, + sourceSurface, + sourceColorView, + sourceColorState, + sourceColorTexture, + colorTargets, + depthTarget, + executionSucceeded, + beginPassCallback, + endPassCallback, + executePassCallback, + io]( + const RenderGraphExecutionContext& executionContext) { + const RenderSurface* resolvedSourceSurface = + hasSourceSurface ? &sourceSurface : nullptr; + RHI::RHIResourceView* resolvedSourceColorView = sourceColorView; + RHI::ResourceStates resolvedSourceColorState = sourceColorState; + RenderSurface graphManagedSourceSurface = {}; + if (!ResolveGraphManagedSourceSurface( + hasSourceSurface ? &sourceSurface : nullptr, + sourceColorView, + sourceColorState, + sourceColorTexture, + executionContext, + io, + resolvedSourceSurface, + resolvedSourceColorView, + resolvedSourceColorState, + graphManagedSourceSurface)) { + if (executionSucceeded != nullptr) { + *executionSucceeded = false; + } + return; + } + + const RenderSurface* resolvedSurface = &surface; + RenderSurface graphManagedSurface = {}; + if (!ResolveGraphManagedOutputSurface( + surface, + colorTargets, + depthTarget, + executionContext, + io, + resolvedSurface, + graphManagedSurface)) { + if (executionSucceeded != nullptr) { + *executionSucceeded = false; + } + return; + } + + const RenderPassContext passContext = { + renderContext, + *resolvedSurface, + *sceneData, + resolvedSourceSurface, + resolvedSourceColorView, + resolvedSourceColorState + }; + const bool executeResult = executePassCallback(passContext); + if (endPassCallback) { + endPassCallback(passContext); + } + if (executionSucceeded != nullptr) { + *executionSucceeded = executeResult; + } + }); + }); + return true; +} +``` + +这段代码的意思就是: + +- graph 阶段先声明 IO。 +- 真执行时,把 graph-managed 纹理重新还原成一个 `RenderPassContext`。 +- 然后照样调用老的 `Execute()`。 + +所以它本质上是一个“把 immediate pass 包装成 graph pass 的桥”。 + +这是你当前渲染层能逐步演进到 SRP/URP 的关键基础设施之一。 + +--- + +## 8. `BuiltinForwardPipeline` 现在在这条链里处于什么位置 + +很多人会误会当前 `BuiltinForwardPipeline` 只是一个“老式 forward renderer”。 + +实际上现在不是。 + +### 8.1 它既能直接渲染,也能录制 `MainScene` stage graph + +```cpp +bool BuiltinForwardPipeline::SupportsStageRenderGraph( + CameraFrameStage stage) const { + return SupportsCameraFramePipelineGraphRecording(stage); +} + +bool BuiltinForwardPipeline::RecordStageRenderGraph( + const RenderPipelineStageRenderGraphContext& context) { + return context.stage == CameraFrameStage::MainScene && + Internal::BuiltinForwardStageGraphBuilder::Record(*this, context); +} + +bool BuiltinForwardPipeline::Render( + const FrameExecutionContext& executionContext) { + return ExecuteForwardSceneFrame(executionContext, true); +} +``` + +这说明它有两种工作方式: + +1. 老路径:直接 `Render(...)`。 +2. 新路径:对 `MainScene` 调 `RecordStageRenderGraph(...)`。 + +所以它已经是“双栈并存”的状态。 + +### 8.2 它的主场景内部也已经分成 scene phase 和 injection point + +```cpp +const std::array& GetBuiltinForwardSceneSteps() { + static constexpr std::array kForwardSceneSteps = { + MakeForwardSceneInjectionStep(SceneRenderInjectionPoint::BeforeOpaque), + MakeForwardSceneBuiltinPhaseStep(ScenePhase::Opaque), + MakeForwardSceneInjectionStep(SceneRenderInjectionPoint::AfterOpaque), + MakeForwardSceneInjectionStep(SceneRenderInjectionPoint::BeforeSkybox), + MakeForwardSceneBuiltinPhaseStep(ScenePhase::Skybox), + MakeForwardSceneInjectionStep(SceneRenderInjectionPoint::AfterSkybox), + MakeForwardSceneInjectionStep(SceneRenderInjectionPoint::BeforeTransparent), + MakeForwardSceneBuiltinPhaseStep(ScenePhase::Transparent), + MakeForwardSceneInjectionStep(SceneRenderInjectionPoint::AfterTransparent) + }; + return kForwardSceneSteps; +} +``` + +这说明当前 forward 主场景不是“一个 opaque 函数 + 一个 transparent 函数”那么简单,而是已经有: + +- phase +- injection point +- feature host + +这已经很接近 Unity `RendererFeature` 那类组织方式了,只不过现在还是 native C++ 版本。 + +### 8.3 现在确实有一些“未来 URP 层更应该负责的东西”还留在 C++ 里 + +比如 builtin feature 的注册: + +```cpp +void RegisterBuiltinForwardSceneFeatures(SceneRenderFeatureHost& featureHost) { + featureHost.AddFeaturePass(std::make_unique()); + featureHost.AddFeaturePass(std::make_unique()); +} +``` + +这段代码就很能说明当前状态: + +- `BuiltinGaussianSplatPass` +- `BuiltinVolumetricPass` + +这些东西今天还挂在 native builtin forward 里。 + +从长期架构看,这些更像 URP-like renderer feature 层应该组织的内容,而不是底层内核必须永久持有的内容。 + +### 8.4 `BuiltinForwardStageGraphBuilder` 怎么把主场景录进 graph + +```cpp +bool BuiltinForwardStageGraphBuilder::Record( + BuiltinForwardPipeline& pipeline, + const RenderPipelineStageRenderGraphContext& context) { + const RenderSurface graphManagedSurface = + BuildRenderGraphManagedSurfaceTemplate(context.surfaceTemplate); + const RenderGraphRecordingContext baseRecordingContext = + BuildRenderGraphRecordingContext(context); + RenderGraphRecordingContextBuildParams recordingParams = {}; + recordingParams.surface = &graphManagedSurface; + recordingParams.overrideSourceBinding = true; + recordingParams.sourceBinding = + BuildRenderGraphRecordingSourceBinding(baseRecordingContext); + RenderGraphRecordingContext recordingContext = + BuildRenderGraphRecordingContext( + baseRecordingContext, + std::move(recordingParams)); + const RenderPipelineStageRenderGraphContext graphContext = + BuildRenderPipelineStageRenderGraphContext( + recordingContext, + CameraFrameStage::MainScene); + const CameraFrameRenderGraphResources* const frameResources = + TryGetCameraFrameRenderGraphResources(recordingContext.blackboard); + const RenderGraphTextureHandle mainDirectionalShadowTexture = + frameResources != nullptr + ? frameResources->mainDirectionalShadow + : RenderGraphTextureHandle{}; + bool* const executionSucceeded = recordingContext.executionSucceeded; + const std::shared_ptr graphExecutionState = + std::make_shared(); + bool clearAttachments = true; + for (const ForwardSceneStep& step : GetBuiltinForwardSceneSteps()) { + if (step.type == ForwardSceneStepType::InjectionPoint) { + bool recordedAnyPass = false; + if (!::XCEngine::Rendering::RecordRenderPipelineStageFeaturePasses( + graphContext, + pipeline.m_forwardSceneFeatureHost, + step.injectionPoint, + clearAttachments, + beginRecordedPass, + endRecordedPass, + &recordedAnyPass)) { + return false; + } + + if (recordedAnyPass) { + clearAttachments = false; + } + continue; + } + + const std::vector additionalReadTextures = + ScenePhaseSamplesMainDirectionalShadow(step.scenePhase) && + mainDirectionalShadowTexture.IsValid() + ? std::vector{ mainDirectionalShadowTexture } + : std::vector{}; + if (!::XCEngine::Rendering::RecordRenderPipelineStagePhasePass( + graphContext, + step.scenePhase, + [&pipeline, scenePhase = step.scenePhase](const RenderPassContext& passContext) { + const FrameExecutionContext executionContext( + passContext.renderContext, + passContext.surface, + passContext.sceneData, + passContext.sourceSurface, + passContext.sourceColorView, + passContext.sourceColorState); + const ScenePhaseExecutionContext scenePhaseExecutionContext = + pipeline.BuildScenePhaseExecutionContext(executionContext, scenePhase); + return pipeline.ExecuteBuiltinScenePhase(scenePhaseExecutionContext); + }, + beginPhasePass, + endRecordedPass, + additionalReadTextures)) { + return false; + } + clearAttachments = false; + } + + return true; +} +``` + +这段代码特别值得你记住,因为它说明: + +> 现在 `BuiltinForwardPipeline` 已经不是“自己包办所有流程”,而是在 `MainScene` 这个 stage 里,把内部 phase 和 feature 逐步录成 graph pass。 + +这是一个很重要的架构转折点。 + +--- + +## 9. `SceneRenderFeatureHost`:你现在其实已经有了 native 版 renderer feature 宿主 + +头文件: + +```cpp +class SceneRenderFeatureHost { +public: + void AddFeaturePass(std::unique_ptr featurePass); + size_t GetFeaturePassCount() const; + SceneRenderFeaturePass* GetFeaturePass(size_t index) const; + + bool Initialize(const RenderContext& context); + void Shutdown(); + bool Prepare(const FrameExecutionContext& executionContext) const; + bool Record( + const SceneRenderFeaturePassRenderGraphContext& context, + SceneRenderInjectionPoint injectionPoint, + bool* recordedAnyPass = nullptr) const; + bool Execute( + const FrameExecutionContext& executionContext, + SceneRenderInjectionPoint injectionPoint) const; + +private: + std::vector> m_featurePasses; +}; +``` + +录制逻辑: + +```cpp +bool SceneRenderFeatureHost::Record( + const SceneRenderFeaturePassRenderGraphContext& context, + SceneRenderInjectionPoint injectionPoint, + bool* recordedAnyPass) const { + bool hasRecordedPass = false; + bool clearAttachments = context.clearAttachments; + + for (size_t featureIndex = 0u; featureIndex < m_featurePasses.size(); ++featureIndex) { + const std::unique_ptr& featurePassOwner = m_featurePasses[featureIndex]; + SceneRenderFeaturePass* featurePass = featurePassOwner.get(); + if (featurePass == nullptr || + !featurePass->SupportsInjectionPoint(injectionPoint) || + !featurePass->IsActive(context.sceneData)) { + continue; + } + + const SceneRenderFeaturePassRenderGraphContext featureContext = + CloneSceneRenderFeaturePassRenderGraphContext( + context, + BuildFeatureGraphPassName( + context.passName, + injectionPoint, + *featurePass, + featureIndex), + clearAttachments); + if (!featurePass->RecordRenderGraph(featureContext)) { + Debug::Logger::Get().Error( + Debug::LogCategory::Rendering, + (Containers::String("SceneRenderFeatureHost record failed at injection point '") + + ToString(injectionPoint) + + "': " + + featurePass->GetName()).CStr()); + return false; + } + + hasRecordedPass = true; + clearAttachments = false; + } + + if (recordedAnyPass != nullptr) { + *recordedAnyPass = hasRecordedPass; + } + return true; +} +``` + +这就是一个非常典型的“feature 注入宿主”: + +- 按 injection point 过滤。 +- 按 sceneData 判断激活状态。 +- 给每个 feature 生成 pass name。 +- 逐个录入 graph。 + +所以如果有人问“你现在是不是已经有一点 URP 的味道了”,答案是: + +> 有,而且不止一点。只不过现在这些能力还主要在 native C++ 层。 + +--- + +## 10. 当前 SRP native 接缝做到哪一步了 + +### 10.1 `ScriptableRenderPipelineHost` 是现在最重要的 native SRP 边界 + +先看头文件: + +```cpp +class ScriptableRenderPipelineHost final : public RenderPipeline { +public: + ScriptableRenderPipelineHost(); + explicit ScriptableRenderPipelineHost( + std::unique_ptr pipelineRenderer); + explicit ScriptableRenderPipelineHost( + std::shared_ptr pipelineRendererAsset); + ~ScriptableRenderPipelineHost() override; + + using RenderPipeline::Render; + + void SetStageRecorder(std::unique_ptr stageRecorder); + void SetPipelineRenderer(std::unique_ptr pipelineRenderer); + void SetPipelineRendererAsset( + std::shared_ptr pipelineRendererAsset); + + bool Initialize(const RenderContext& context) override; + void Shutdown() override; + bool SupportsStageRenderGraph(CameraFrameStage stage) const override; + bool RecordStageRenderGraph( + const RenderPipelineStageRenderGraphContext& context) override; + bool Render(const FrameExecutionContext& executionContext) override; + bool Render( + const RenderContext& context, + const RenderSurface& surface, + const RenderSceneData& sceneData) override; +}; +``` + +再看最关键的两个函数: + +```cpp +bool ScriptableRenderPipelineHost::SupportsStageRenderGraph( + CameraFrameStage stage) const { + return (m_stageRecorder != nullptr && + m_stageRecorder->SupportsStageRenderGraph(stage)) || + (m_pipelineRenderer != nullptr && + m_pipelineRenderer->SupportsStageRenderGraph(stage)); +} + +bool ScriptableRenderPipelineHost::RecordStageRenderGraph( + const RenderPipelineStageRenderGraphContext& context) { + if (!EnsureInitialized(context.renderContext)) { + return false; + } + + if (m_stageRecorder != nullptr && + m_stageRecorder->SupportsStageRenderGraph(context.stage)) { + return m_stageRecorder->RecordStageRenderGraph(context); + } + + return m_pipelineRenderer != nullptr && + m_pipelineRenderer->RecordStageRenderGraph(context); +} +``` + +这说明 `ScriptableRenderPipelineHost` 的架构意图非常明确: + +- 底下有一个 fallback renderer。 +- 上面可以再挂一个 stage recorder。 +- 如果 recorder 支持某 stage,就优先 recorder。 +- 否则回退到底层 renderer。 + +这个设计就是 native SRP host seam。 + +### 10.2 它还会给宿主管线挂上默认 standalone pass + +```cpp +void ScriptableRenderPipelineHostAsset::ConfigurePipeline( + RenderPipeline& pipeline) const { + pipeline.SetCameraFrameStandalonePass( + CameraFrameStage::ObjectId, + std::make_unique()); + pipeline.SetCameraFrameStandalonePass( + CameraFrameStage::DepthOnly, + std::make_unique()); + pipeline.SetCameraFrameStandalonePass( + CameraFrameStage::ShadowCaster, + std::make_unique()); +} +``` + +也就是说,当前 host 并不是“空壳”。 + +它已经在承接一部分基础管线职责,只不过主场景 recorder 这部分还没真正切到 managed。 + +--- + +## 11. managed C# 侧当前到底做到哪一步了 + +结论先说: + +> 现在的 managed SRP 还只是骨架,还远远不是“Unity 那种 SRP 运行时”。 + +### 11.1 C# 的 `ScriptableRenderPipelineAsset` 现在几乎还是空壳 + +```csharp +namespace XCEngine +{ + public abstract class ScriptableRenderPipelineAsset : RenderPipelineAsset + { + protected ScriptableRenderPipelineAsset() + { + } + + protected internal virtual ScriptableRenderPipeline CreatePipeline() + { + return null; + } + } +} +``` + +### 11.2 C# 的 `ScriptableRenderPipeline` 也只是最小骨架 + +```csharp +namespace XCEngine +{ + public abstract class ScriptableRenderPipeline : Object + { + protected ScriptableRenderPipeline() + { + } + + protected internal virtual bool SupportsStageRenderGraph( + CameraFrameStage stage) + { + return false; + } + + protected internal virtual bool RecordStageRenderGraph( + CameraFrameStage stage) + { + return false; + } + } +} +``` + +请注意,这里连真正的 graph/context 参数都没有。 + +也就是说,C# 现在甚至还拿不到足够完整的录制上下文。 + +### 11.3 `GraphicsSettings` 现在只是记录“某个 C# 类型名” + +```csharp +public static class GraphicsSettings +{ + public static Type renderPipelineAssetType + { + get + { + string assemblyQualifiedName = + InternalCalls.Rendering_GetRenderPipelineAssetTypeName(); + if (string.IsNullOrEmpty(assemblyQualifiedName)) + { + return null; + } + + return Type.GetType(assemblyQualifiedName, throwOnError: false); + } + set + { + if (value != null && + !typeof(ScriptableRenderPipelineAsset).IsAssignableFrom(value)) + { + throw new ArgumentException( + "GraphicsSettings.renderPipelineAssetType must derive from ScriptableRenderPipelineAsset.", + nameof(value)); + } + + InternalCalls.Rendering_SetRenderPipelineAssetType(value); + } + } +} +``` + +当前这个 API 的语义是: + +- 把一个“类型信息”告诉 native。 +- 还不是创建并持有一个真实 managed asset 实例。 + +### 11.4 native 侧的 `ManagedScriptableRenderPipelineAsset` 也只是桥接骨架 + +头文件: + +```cpp +class ManagedScriptableRenderPipelineAsset final : public RenderPipelineAsset { +public: + explicit ManagedScriptableRenderPipelineAsset( + ManagedRenderPipelineAssetDescriptor descriptor); + + std::unique_ptr CreatePipeline() const override; + FinalColorSettings GetDefaultFinalColorSettings() const override; + +private: + ManagedRenderPipelineAssetDescriptor m_descriptor; + ScriptableRenderPipelineHostAsset m_fallbackAsset; +}; + +class ManagedRenderPipelineBridge { +public: + virtual ~ManagedRenderPipelineBridge() = default; + + virtual std::unique_ptr CreateStageRecorder( + const ManagedRenderPipelineAssetDescriptor&) const { + return nullptr; + } +}; +``` + +实现: + +```cpp +std::unique_ptr ManagedScriptableRenderPipelineAsset::CreatePipeline() const { + std::unique_ptr pipeline = m_fallbackAsset.CreatePipeline(); + auto* host = dynamic_cast(pipeline.get()); + if (host == nullptr) { + return pipeline; + } + + const std::shared_ptr bridge = + GetManagedRenderPipelineBridgeStorage(); + if (bridge != nullptr) { + host->SetStageRecorder( + bridge->CreateStageRecorder(m_descriptor)); + } + + return pipeline; +} +``` + +这里的真实含义是: + +- 先创建一个 native fallback host。 +- 如果 bridge 存在,就给 host 塞一个 stage recorder。 + +注意,这里还不是: + +- native 持有真实 managed pipeline 实例 +- managed pipeline 拥有完整生命周期 +- managed pipeline 拥有完整 render context + +所以现在还不能说“SRP 已经通了”,只能说: + +> native 侧已经为 SRP runtime 留好了接缝。 + +--- + +## 12. 当前架构的准确评价 + +### 12.1 现在已经做对了的地方 + +1. 顶层主链已经从“直接渲染”切成了“planning + execution + graph”。 +2. `CameraFramePlan` 已经是一个真正有意义的每相机执行计划,不是样板结构。 +3. `RenderGraph` 已经可用,不是空壳。 +4. `MainScene` 已经能 graph-record,而不是只有 immediate render。 +5. `RenderPassGraphContract` 让旧 pass 可以渐进接入 graph。 +6. `SceneRenderFeatureHost` 已经提供了 renderer feature 风格的注入模型。 +7. `ScriptableRenderPipelineHost` 已经是对的 native SRP 边界。 + +### 12.2 现在还没完全收口的地方 + +1. managed SRP runtime 还没真正存在。 +2. C# pipeline API 还拿不到足够强的上下文。 +3. 现在很多“未来更应该属于 URP-like 包层”的组织逻辑还在 C++ builtin forward 里。 +4. `RenderGraph` 还是轻量版,没有做更强的优化能力。 + +### 12.3 当前哪些东西其实已经有点“URP 包层味道” + +这个问题你之前已经问过很多次,这里给一个最准确的判断: + +当前 C++ 渲染层里,确实已经做了一些未来更应该上移到 URP-like 包层的事情,比如: + +- 主场景 phase 组织 +- feature injection point 组织 +- 高斯/体积 feature 的注册 +- 后处理和 final output 颜色链规划 +- 默认阴影执行策略的一部分组织 + +但这不代表这些东西今天就必须马上搬走。 + +更准确的说法是: + +> 现在 native 层里已经有了一套“可跑的 builtin renderer 组织层”,后面应该逐步把“组织权”上移,而不是立刻把所有底层实现都搬去 C#。 + +--- + +## 13. 下一步为什么不是直接开做 URP,而是先做 SRP runtime + +因为你现在最根上的缺口不是某个 pass,也不是某个阴影算法。 + +最根上的缺口是: + +> managed pipeline 在运行时还没有真正存在。 + +如果这个问题不先解决,后面所有这些东西都会变空中楼阁: + +- `UniversalRenderPipelineAsset` +- `RendererFeature` +- 自定义 renderer +- 延迟渲染管线 +- 光照贴图接入 +- 阴影/体积/高斯等组织权上移 + +原因很简单: + +- 现在 C# 还不能创建和持有真实 pipeline 实例。 +- 也没有真正的 `ScriptableRenderContext`。 +- 也拿不到一帧 graph 录制所需的上下文。 + +所以你今天最该切的是 **SRP runtime 主线**,不是 URP 包层。 + +--- + +## 14. 我建议的下一步 SRP 主线计划 + +下面这套顺序,是我认为最稳、最符合你现在代码状态的方案。 + +### 阶段 1:打通真实的 managed pipeline runtime + +目标: + +1. native 不再只知道一个 C# 类型名。 +2. native 能创建并持有真实 managed asset / pipeline 实例。 +3. `ManagedRenderPipelineBridge` 不再只是测试桩式接缝。 +4. pipeline 生命周期可控,可初始化、可释放、可切换。 + +这一步做完,你才算真正拥有“SRP runtime 的入口”。 + +### 阶段 2:定义 `ScriptableRenderContext v1` + +这里不要一上来暴露整套 RHI。 + +应该给 managed 提供受控的 native 能力包装,比如: + +1. 录制一个 raster/compute graph pass。 +2. 访问当前 camera frame 的 source / target / blackboard。 +3. 调用 native scene renderer 画 `Opaque / Skybox / Transparent`。 +4. 调用 fullscreen / blit。 +5. 访问当前 stage 和 frame 语义。 + +这里的核心原则是: + +> C# 负责组织,C++ 负责底层执行内核。 + +### 阶段 3:做一个最小可用的 managed forward pipeline + +目标不是一步到位做 URP。 + +目标是: + +1. C# 能创建 pipeline。 +2. C# 能参与 `MainScene` stage graph 录制。 +3. C# 能调 native scene renderer 画主场景。 +4. 这条 managed pipeline 能真实替换现在 builtin forward 的主场景组织。 + +这一步一旦打通,SRP 主线就真正开始跑了。 + +### 阶段 4:把 builtin forward 退化为 native renderer backend + +这里的方向不是删掉 builtin forward,而是改变它的角色。 + +从: + +- 一个完整的“默认整条渲染管线” + +变成: + +- 一个 native scene renderer backend +- 一个供 managed SRP 调度的默认 renderer + +也就是说,未来 C# SRP 不应该直接依赖 `BuiltinForwardPipeline` 的整管线语义,而应该依赖一组更稳定的 native renderer contract。 + +### 阶段 5:再开做 URP-like package + +等前四步完成后,再开: + +- `UniversalRenderPipelineAsset` +- `UniversalRenderer` +- `RendererFeature` +- `RenderPassEvent` +- 阴影/后处理/体积/高斯等上层组织 + +到那时你做的就不是“在空壳上搭房子”,而是“在已经存在的 runtime 上做官方包层”。 + +--- + +## 15. 到 SRP 阶段时,哪些东西应该留在 C++,哪些东西应该上移到 C# + +### 应该稳定留在 C++ 的 + +1. `RHI` +2. `RenderGraph` +3. graph compiler / executor +4. scene extraction / culling / frame data +5. render surface / resource lifetime / barrier +6. native draw/fullscreen/backend renderer contract + +### 应该逐步上移到 managed SRP / URP-like 的 + +1. pipeline asset 组织 +2. renderer asset / renderer feature / render pass 的上层调度 +3. 阴影默认策略 +4. 后处理链组织 +5. 体积、高斯、自定义效果的注入时机和排序 +6. 用户自定义管线的组合逻辑 + +### `RenderGraph` 要不要做到 C++ 层 + +答案是:**要,而且应该留在 C++ native 层。** + +但 managed 层需要拿到它的“受控包装”。 + +不要把 managed 直接变成: + +- 手搓 RHI barrier +- 手搓 native texture 视图 +- 手搓所有底层状态机 + +正确方向是: + +- C++ 保留真正的 graph 内核。 +- C# 通过 `ScriptableRenderContext`/wrapper 参与 graph 录制和资源引用。 + +这才是小引擎里最稳、也最面向未来的方案。 + +--- + +## 16. 如果你现在要继续读源码,建议按这个顺序 + +### 第一轮,只看主链 + +1. `SceneRenderer` +2. `SceneRenderRequestPlanner` +3. `RenderPipelineHost` +4. `CameraFramePlanBuilder` +5. `CameraFramePlan` +6. `CameraRenderer` +7. `ExecuteCameraFrameRenderGraphPlan` + +### 第二轮,看 `RenderGraph` + +1. `RenderGraph.h` +2. `RenderGraph.cpp` +3. `RenderGraphCompiler.cpp` +4. `RenderGraphExecutor.cpp` +5. `RenderGraphBlackboard.h` + +### 第三轮,看“相机一帧怎么录图” + +1. `Recorder.cpp` +2. `StageDispatch.cpp` +3. `State.cpp` +4. `StageContract.cpp` +5. `SequenceRecorder.cpp` +6. `PassRecorder.cpp` +7. `SurfaceUtils.cpp` + +### 第四轮,看 builtin pipeline 和 SRP 接缝 + +1. `BuiltinForwardPipeline.h` +2. `BuiltinForwardPipelineFrame.cpp` +3. `BuiltinForwardSceneSetup.cpp` +4. `BuiltinForwardStageGraphBuilder.cpp` +5. `SceneRenderFeatureHost.cpp` +6. `ScriptableRenderPipelineHost.cpp` +7. `ManagedScriptableRenderPipelineAsset.cpp` +8. `managed/XCEngine.ScriptCore/ScriptableRenderPipeline*.cs` + +--- + +## 最后总结一句 + +你当前这套渲染模块,真实状态可以概括成一句话: + +> native C++ 侧已经搭好了一个“基于 `CameraFramePlan` 和 `RenderGraph` 的渲染执行内核”,并且已经留出了 `ScriptableRenderPipelineHost` 这个 SRP 接缝;但 managed C# 侧还没有真正形成可运行的 SRP runtime,所以现在最正确的下一步不是直接做 URP 包,而是先把 managed SRP runtime 和 `ScriptableRenderContext v1` 打通。 + +如果你把这句话吃透,后面不管是做 SRP、URP-like、延迟渲染,还是把阴影/体积/高斯逐步上移,方向都不会跑偏。