diff --git a/docs/used/SRP_UniversalRendererStageCapability计划_完成归档_2026-04-21.md b/docs/used/SRP_UniversalRendererStageCapability计划_完成归档_2026-04-21.md new file mode 100644 index 00000000..b92bc6ee --- /dev/null +++ b/docs/used/SRP_UniversalRendererStageCapability计划_完成归档_2026-04-21.md @@ -0,0 +1,138 @@ +# SRP Universal Renderer Stage Capability 计划 2026-04-21 + +## 1. 阶段目标 + +把当前 `SupportsStageRenderGraph` 这条能力查询,从“默认 renderer 的全局能力判断”收成“当前 camera / plan 已解析 renderer 的正式能力判断”。 + +本阶段完成后,必须满足: + +1. `rendererIndex` 不只进入 request / plan / execution,还真正参与 stage support 查询 +2. fullscreen stage 的自动推导和执行前校验,不再被 default renderer 污染 +3. Universal 不同 renderer 以后即使 stage 支持不一致,plan 也能稳定按选中 renderer 走 +4. 旧的项目脚本如果只重写 `SupportsStageRenderGraph(stage)`,仍然能通过回退逻辑继续工作 + +## 2. 当前问题 + +上一阶段虽然已经打通了: + +`camera override -> request.rendererIndex -> plan.rendererIndex -> execution resolve renderer` + +但还有一个口没收干净: + +1. `CameraFramePlanBuilder::ConfigureFullscreenStagesFromPipeline(...)` + 还是通过 `pipeline->SupportsStageRenderGraph(stage)` 做全局判断 +2. `CameraRenderer::Render(...)` + 在执行前做 stage support 校验时,也还是只问“这个 pipeline 支不支持” +3. `PassRecorder.cpp` + 录制 pipeline stage render graph 前,同样还是无上下文查询 +4. `RendererDrivenRenderPipeline` + 默认 support 查询路径仍然走 `rendererIndex = -1` + +这意味着一旦出现: + +1. default renderer 支持 post-process,但相机选中的 renderer 不支持 +2. default renderer 不支持 final output,但相机选中的 renderer 支持 + +当前计划推导和执行前校验都会出现错误判断。 + +## 3. 设计原则 + +这一阶段按下面的责任分层处理: + +1. native `CameraFramePlan` 负责携带当前相机已经解析好的 `rendererIndex` +2. native `RenderPipeline` / `RenderPipelineRenderer` / `RenderPipelineStageRecorder` + 新增 contextual support query seam +3. managed `ScriptableRenderPipeline` + 新增“带 `rendererIndex` 的 stage support 查询”入口 +4. `RendererDrivenRenderPipeline` + 通过 contextual query 按选中的 renderer 构造 `RendererRecordingContext` +5. 旧 API 不直接删,保留回退,避免把当前项目脚本主线打崩 + +核心目标是把: + +`stage support` + +从: + +`pipeline global capability` + +改成: + +`resolved renderer capability for this request` + +## 4. 本阶段范围 + +包括: + +1. C++ 新增 stage support contextual contract +2. 让 plan builder / render-time validation / pass recorder 都使用 contextual query +3. managed `ScriptableRenderPipeline` 新增 contextual support seam +4. Mono stage recorder 优先调用新 seam,找不到时回退旧 seam +5. Universal renderer-driven pipeline 按 `rendererIndex` 做 support 判断 +6. 重新编译 `XCEditor` +7. 运行旧版 `editor/bin/Debug/XCEngine.exe` 冒烟至少 10 秒 + +不包括: + +1. deferred renderer +2. multi-backend renderer family 切换 +3. inspector / editor UI +4. camera stack 完整还原 +5. `UniversalAdditionalCameraData` 更多字段扩张 + +## 5. 实施步骤 + +### Step 1:新增 native contextual support query seam + +做这几件事: + +1. 给 `RenderPipeline` / `RenderPipelineRenderer` / `RenderPipelineStageRecorder` + 增加携带 `stage + rendererIndex` 的 support query context +2. 默认实现继续回退到旧的 `SupportsStageRenderGraph(stage)` +3. 只改调用链,不把现有 builtin pipeline 行为改乱 + +### Step 2:把 plan / render / pass recorder 全部改成 contextual query + +重点改: + +1. `CameraFramePlanBuilder` +2. `CameraRenderer` +3. `CameraFrameGraph/PassRecorder` +4. `ScriptableRenderPipelineHost` + +要求是: + +1. 只要已经有 `plan.request.rendererIndex`,后续所有 support 判断都必须吃这个值 +2. 不再出现“计划按 A renderer,校验却按 default renderer”的双轨行为 + +### Step 3:补齐 managed contextual seam + +做这几件事: + +1. `ScriptableRenderPipeline` 增加新的 contextual support 虚函数 +2. 默认实现回退到旧的 `SupportsStageRenderGraph(stage)` +3. `RendererDrivenRenderPipeline` 覆盖新的 contextual seam +4. `RendererRecordingContext` 能按 `stage + rendererIndex` 构造 +5. Mono stage recorder 优先找新方法,找不到再回退旧方法 + +### Step 4:验证与收口 + +固定流程: + +1. `cmake --build . --config Debug --target XCEditor` +2. 启动 `editor/bin/Debug/XCEngine.exe` +3. 冒烟至少 10 秒 +4. 检查 `editor/bin/Debug/editor.log` +5. 计划归档 +6. 提交并推送 + +## 6. 验收标准 + +本阶段收口后必须满足: + +1. renderer-specific stage capability 成为正式 contract +2. fullscreen stage 推导和执行前校验不再依赖 default renderer 的隐式能力 +3. Universal renderer selection 主链从 request / plan / execution 延伸到 capability query +4. 旧 `SupportsStageRenderGraph(stage)` override 仍然可回退 +5. `XCEditor` 编译通过 +6. 旧版 editor 冒烟通过 diff --git a/engine/include/XCEngine/Rendering/Pipelines/ScriptableRenderPipelineHost.h b/engine/include/XCEngine/Rendering/Pipelines/ScriptableRenderPipelineHost.h index dc430c87..0252db5b 100644 --- a/engine/include/XCEngine/Rendering/Pipelines/ScriptableRenderPipelineHost.h +++ b/engine/include/XCEngine/Rendering/Pipelines/ScriptableRenderPipelineHost.h @@ -37,6 +37,8 @@ public: bool Initialize(const RenderContext& context) override; void Shutdown() override; bool SupportsStageRenderGraph(CameraFrameStage stage) const override; + bool SupportsStageRenderGraph( + const RenderPipelineStageSupportContext& context) const override; bool RecordStageRenderGraph( const RenderPipelineStageRenderGraphContext& context) override; bool Render(const FrameExecutionContext& executionContext) override; diff --git a/engine/include/XCEngine/Rendering/RenderPipeline.h b/engine/include/XCEngine/Rendering/RenderPipeline.h index 6fae6c75..cca7be47 100644 --- a/engine/include/XCEngine/Rendering/RenderPipeline.h +++ b/engine/include/XCEngine/Rendering/RenderPipeline.h @@ -53,6 +53,11 @@ struct RenderPipelineStageRenderGraphContext { int32_t rendererIndex = -1; }; +struct RenderPipelineStageSupportContext { + CameraFrameStage stage = CameraFrameStage::MainScene; + int32_t rendererIndex = -1; +}; + class RenderPipelineRenderer; class RenderPipelineStageRecorder { @@ -67,6 +72,10 @@ public: virtual bool SupportsStageRenderGraph(CameraFrameStage) const { return false; } + virtual bool SupportsStageRenderGraph( + const RenderPipelineStageSupportContext& context) const { + return SupportsStageRenderGraph(context.stage); + } virtual bool RecordStageRenderGraph( const RenderPipelineStageRenderGraphContext&) { return false; @@ -82,6 +91,10 @@ public: virtual bool SupportsStageRenderGraph(CameraFrameStage) const { return false; } + virtual bool SupportsStageRenderGraph( + const RenderPipelineStageSupportContext& context) const { + return SupportsStageRenderGraph(context.stage); + } virtual bool RecordStageRenderGraph( const RenderPipelineStageRenderGraphContext&) { return false; diff --git a/engine/src/Rendering/Execution/CameraRenderer.cpp b/engine/src/Rendering/Execution/CameraRenderer.cpp index 4ad58be7..5bf4dd82 100644 --- a/engine/src/Rendering/Execution/CameraRenderer.cpp +++ b/engine/src/Rendering/Execution/CameraRenderer.cpp @@ -29,6 +29,14 @@ bool IsManagedPipelineAsset( pipelineAsset.get()) != nullptr; } +RenderPipelineStageSupportContext BuildStageSupportContext( + const CameraFramePlan& plan, + CameraFrameStage stage) { + return RenderPipelineStageSupportContext{ + stage, + plan.request.rendererIndex }; +} + } // namespace CameraRenderer::CameraRenderer() @@ -194,7 +202,10 @@ bool CameraRenderer::Render( } if (plan.UsesGraphManagedOutputColor(CameraFrameStage::MainScene) && (m_pipeline == nullptr || - !m_pipeline->SupportsStageRenderGraph(CameraFrameStage::MainScene))) { + !m_pipeline->SupportsStageRenderGraph( + BuildStageSupportContext( + plan, + CameraFrameStage::MainScene)))) { Debug::Logger::Get().Error( Debug::LogCategory::Rendering, "CameraRenderer::Render failed: graph-managed main scene color requires pipeline main-scene render-graph support"); @@ -211,7 +222,10 @@ bool CameraRenderer::Render( { CameraFrameStage::PostProcess, CameraFrameStage::FinalOutput }) { if (RequiresPipelineStageRecording(plan, stage) && (m_pipeline == nullptr || - !m_pipeline->SupportsStageRenderGraph(stage))) { + !m_pipeline->SupportsStageRenderGraph( + BuildStageSupportContext( + plan, + stage)))) { Debug::Logger::Get().Error( Debug::LogCategory::Rendering, Containers::String( diff --git a/engine/src/Rendering/Execution/Internal/CameraFrameGraph/PassRecorder.cpp b/engine/src/Rendering/Execution/Internal/CameraFrameGraph/PassRecorder.cpp index 74e29204..25a17434 100644 --- a/engine/src/Rendering/Execution/Internal/CameraFrameGraph/PassRecorder.cpp +++ b/engine/src/Rendering/Execution/Internal/CameraFrameGraph/PassRecorder.cpp @@ -11,6 +11,18 @@ namespace XCEngine { namespace Rendering { +namespace { + +RenderPipelineStageSupportContext BuildStageSupportContext( + const CameraFrameRenderGraphStageContext& context, + CameraFrameStage stage) { + return RenderPipelineStageSupportContext{ + stage, + context.plan.request.rendererIndex }; +} + +} // namespace + bool TryRecordCameraFrameStageStandaloneRenderGraphPass( const CameraFrameStageGraphBuildState& stageState, const CameraFrameRenderGraphStageContext& context, @@ -55,7 +67,9 @@ bool TryRecordCameraFramePipelineStageGraphPass( CameraFrameRenderGraphBuilderContext& builder = context.builder; if (builder.executionState.pipeline == nullptr || !builder.executionState.pipeline->SupportsStageRenderGraph( - stageState.stage)) { + BuildStageSupportContext( + context, + stageState.stage))) { handled = false; return true; } diff --git a/engine/src/Rendering/Pipelines/ScriptableRenderPipelineHost.cpp b/engine/src/Rendering/Pipelines/ScriptableRenderPipelineHost.cpp index 52625ade..da14ce2e 100644 --- a/engine/src/Rendering/Pipelines/ScriptableRenderPipelineHost.cpp +++ b/engine/src/Rendering/Pipelines/ScriptableRenderPipelineHost.cpp @@ -95,10 +95,16 @@ void ScriptableRenderPipelineHost::Shutdown() { bool ScriptableRenderPipelineHost::SupportsStageRenderGraph( CameraFrameStage stage) const { + return SupportsStageRenderGraph( + RenderPipelineStageSupportContext{ stage, -1 }); +} + +bool ScriptableRenderPipelineHost::SupportsStageRenderGraph( + const RenderPipelineStageSupportContext& context) const { return (m_stageRecorder != nullptr && - m_stageRecorder->SupportsStageRenderGraph(stage)) || + m_stageRecorder->SupportsStageRenderGraph(context)) || (m_pipelineRenderer != nullptr && - m_pipelineRenderer->SupportsStageRenderGraph(stage)); + m_pipelineRenderer->SupportsStageRenderGraph(context)); } bool ScriptableRenderPipelineHost::RecordStageRenderGraph( @@ -108,7 +114,10 @@ bool ScriptableRenderPipelineHost::RecordStageRenderGraph( } if (m_stageRecorder != nullptr && - m_stageRecorder->SupportsStageRenderGraph(context.stage)) { + m_stageRecorder->SupportsStageRenderGraph( + RenderPipelineStageSupportContext{ + context.stage, + context.rendererIndex })) { if (m_stageRecorder->RecordStageRenderGraph(context)) { return true; } diff --git a/engine/src/Rendering/Planning/CameraFramePlanBuilder.cpp b/engine/src/Rendering/Planning/CameraFramePlanBuilder.cpp index 538941e8..41489ddb 100644 --- a/engine/src/Rendering/Planning/CameraFramePlanBuilder.cpp +++ b/engine/src/Rendering/Planning/CameraFramePlanBuilder.cpp @@ -15,6 +15,14 @@ bool UsesExplicitFullscreenSource( HasValidColorTarget(request.sourceSurface); } +RenderPipelineStageSupportContext BuildStageSupportContext( + const CameraFramePlan& plan, + CameraFrameStage stage) { + return RenderPipelineStageSupportContext{ + stage, + plan.request.rendererIndex }; +} + void ConfigureFullscreenStagesFromPipeline( CameraFramePlan& plan, const RenderPipeline* pipeline) { @@ -27,11 +35,15 @@ void ConfigureFullscreenStagesFromPipeline( const bool supportsPostProcess = pipeline != nullptr && pipeline->SupportsStageRenderGraph( - CameraFrameStage::PostProcess); + BuildStageSupportContext( + plan, + CameraFrameStage::PostProcess)); const bool supportsFinalOutput = pipeline != nullptr && pipeline->SupportsStageRenderGraph( - CameraFrameStage::FinalOutput); + BuildStageSupportContext( + plan, + CameraFrameStage::FinalOutput)); const bool hasPostProcess = plan.postProcess.IsRequested() || supportsPostProcess; diff --git a/engine/src/Scripting/Mono/MonoScriptRuntime.cpp b/engine/src/Scripting/Mono/MonoScriptRuntime.cpp index 816b21b5..5358706d 100644 --- a/engine/src/Scripting/Mono/MonoScriptRuntime.cpp +++ b/engine/src/Scripting/Mono/MonoScriptRuntime.cpp @@ -1362,6 +1362,7 @@ public: m_ownedSceneRenderer->Shutdown(); } m_ownedPipelineRendererAsset.reset(); + m_supportsStageContextualMethod = nullptr; m_supportsStageMethod = nullptr; m_recordStageMethod = nullptr; m_resolvedPipelineHandle = 0; @@ -1375,12 +1376,65 @@ public: } bool SupportsStageRenderGraph(Rendering::CameraFrameStage stage) const override { - if (!SupportsManagedRenderPipelineStageGraphRecording(stage) || + return SupportsStageRenderGraph( + Rendering::RenderPipelineStageSupportContext{ + stage, + -1 }); + } + + bool SupportsStageRenderGraph( + const Rendering::RenderPipelineStageSupportContext& context) const override { + if (!SupportsManagedRenderPipelineStageGraphRecording(context.stage) || !IsRuntimeAlive()) { return false; } MonoObject* const pipelineObject = GetManagedPipelineObject(); + if (!pipelineObject) { + return false; + } + + MonoMethod* const contextualMethod = + ResolveSupportsStageContextualMethod( + pipelineObject); + if (contextualMethod != nullptr) { + int32_t managedStage = + static_cast(context.stage); + int32_t rendererIndex = + context.rendererIndex; + void* args[2] = { + &managedStage, + &rendererIndex }; + MonoObject* result = nullptr; + if (!m_runtime->InvokeManagedMethod( + pipelineObject, + contextualMethod, + args, + &result)) { + return false; + } + + bool supportsStage = false; + return TryUnboxManagedBoolean( + result, + supportsStage) && + supportsStage; + } + + return SupportsStageRenderGraphFallback( + pipelineObject, + context.stage); + } + +private: + bool SupportsStageRenderGraphFallback( + MonoObject* pipelineObject, + Rendering::CameraFrameStage stage) const { + if (!SupportsManagedRenderPipelineStageGraphRecording(stage) || + !IsRuntimeAlive()) { + return false; + } + MonoMethod* const method = ResolveSupportsStageMethod(pipelineObject); if (!pipelineObject || !method) { return false; @@ -1401,6 +1455,7 @@ public: return TryUnboxManagedBoolean(result, supportsStage) && supportsStage; } +public: bool RecordStageRenderGraph( const Rendering::RenderPipelineStageRenderGraphContext& context) override { if (!IsRuntimeAlive()) { @@ -1475,6 +1530,7 @@ private: } if (pipelineHandle != m_resolvedPipelineHandle) { + m_supportsStageContextualMethod = nullptr; m_supportsStageMethod = nullptr; m_recordStageMethod = nullptr; m_resolvedPipelineHandle = pipelineHandle; @@ -1483,6 +1539,19 @@ private: return m_runtime->GetManagedObject(pipelineHandle); } + MonoMethod* ResolveSupportsStageContextualMethod( + MonoObject* pipelineObject) const { + if (m_supportsStageContextualMethod == nullptr) { + m_supportsStageContextualMethod = + m_runtime->ResolveManagedMethod( + pipelineObject, + "SupportsStageRenderGraphContextual", + 2); + } + + return m_supportsStageContextualMethod; + } + MonoMethod* ResolveSupportsStageMethod(MonoObject* pipelineObject) const { if (m_supportsStageMethod == nullptr) { m_supportsStageMethod = @@ -1615,6 +1684,7 @@ private: std::shared_ptr m_assetRuntime; MonoScriptRuntime* m_runtime = nullptr; + mutable MonoMethod* m_supportsStageContextualMethod = nullptr; mutable MonoMethod* m_supportsStageMethod = nullptr; mutable MonoMethod* m_recordStageMethod = nullptr; mutable uint32_t m_resolvedPipelineHandle = 0; diff --git a/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/RendererDrivenRenderPipeline.cs b/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/RendererDrivenRenderPipeline.cs index fbae4e7b..5c13e688 100644 --- a/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/RendererDrivenRenderPipeline.cs +++ b/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/RendererDrivenRenderPipeline.cs @@ -12,9 +12,20 @@ namespace XCEngine.Rendering.Universal protected sealed override bool SupportsStageRenderGraph( CameraFrameStage stage) + { + return SupportsStageRenderGraphContextual( + stage, + -1); + } + + protected sealed override bool SupportsStageRenderGraphContextual( + CameraFrameStage stage, + int rendererIndex) { return SupportsRendererRecording( - new RendererRecordingContext(stage)); + new RendererRecordingContext( + stage, + rendererIndex)); } protected sealed override bool RecordStageRenderGraph( diff --git a/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/RendererRecordingContext.cs b/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/RendererRecordingContext.cs index ee4f3408..cc389500 100644 --- a/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/RendererRecordingContext.cs +++ b/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/RendererRecordingContext.cs @@ -10,9 +10,20 @@ namespace XCEngine.Rendering.Universal internal RendererRecordingContext( CameraFrameStage stage) + : this( + stage, + -1) + { + } + + internal RendererRecordingContext( + CameraFrameStage stage, + int rendererIndex) : this( null, - new RenderingData(stage)) + new RenderingData( + stage, + rendererIndex)) { } diff --git a/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/RenderingData.cs b/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/RenderingData.cs index 6b5c7feb..57c28b91 100644 --- a/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/RenderingData.cs +++ b/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/RenderingData.cs @@ -8,7 +8,16 @@ namespace XCEngine.Rendering.Universal internal RenderingData(CameraFrameStage stage) : this( stage, - -1, + -1) + { + } + + internal RenderingData( + CameraFrameStage stage, + int rendererIndex) + : this( + stage, + rendererIndex, CameraData.Default, LightingData.Default, ShadowData.Default, diff --git a/managed/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipeline.cs b/managed/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipeline.cs index bb6cdf64..004beace 100644 --- a/managed/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipeline.cs +++ b/managed/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipeline.cs @@ -27,6 +27,13 @@ namespace XCEngine.Rendering return false; } + protected virtual bool SupportsStageRenderGraphContextual( + CameraFrameStage stage, + int rendererIndex) + { + return SupportsStageRenderGraph(stage); + } + protected virtual bool RecordStageRenderGraph( ScriptableRenderContext context) {