From 87df14f47bd65e33a54ecfe9025e16195baa846f Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Mon, 27 Apr 2026 23:12:36 +0800 Subject: [PATCH] Freeze URP renderer frame plans --- engine/include/XCEngine/Rendering/AGENTS.md | 18 ++- managed/GameScripts/RenderPipelineApiProbe.cs | 110 ++++++++++++++ .../Universal/ScriptableRenderPass.cs | 10 ++ .../Rendering/Universal/ScriptableRenderer.cs | 136 +++++++++++------- tests/scripting/test_mono_script_runtime.cpp | 129 +++++++++++++++++ 5 files changed, 342 insertions(+), 61 deletions(-) diff --git a/engine/include/XCEngine/Rendering/AGENTS.md b/engine/include/XCEngine/Rendering/AGENTS.md index 0b1091ec..09e00ec0 100644 --- a/engine/include/XCEngine/Rendering/AGENTS.md +++ b/engine/include/XCEngine/Rendering/AGENTS.md @@ -246,11 +246,15 @@ package。 仍是兼容和高级策略 hook,但它不能单独声明 shadow、depth、post 或 final-output stage;没有被 pass queue 覆盖到的 side/fullscreen stage 必须在最终 plan 中清掉。 - `ConfigurePassQueueCameraFramePlanInstance` 必须在 planning 阶段为当前 renderer 和 `framePlanId` - 生成一次 per-camera active pass queue 快照,并用同一份 queue 派生 stage manifest。`SupportsStageRenderGraph` - 和 `RecordStageRenderGraph` 在 `framePlanId != 0` 时必须消费这个快照,不得重新运行 + 生成一次 per-camera `RendererFramePlan`,冻结 active pass queue 中每个 pass 的当帧状态,并用同一份 plan + 派生 stage manifest。`SupportsStageRenderGraph` + 和 `RecordStageRenderGraph` 在 `framePlanId != 0` 时必须消费这个 plan,不得重新运行 feature `SetupRenderPasses`、feature `AddRenderPasses` 或 renderer-owned `AddRenderPasses` 来重建队列。 - 找不到匹配 `framePlanId` 和 renderer index 的快照时必须返回不支持或 record 失败,让上层暴露错误; + 找不到匹配 `framePlanId` 和 renderer index 的 plan 时必须返回不支持或 record 失败,让上层暴露错误; 不要退回 legacy queue rebuild 或 built-in fallback。 +- `RendererFramePlan` 不得只是 pass 对象引用列表。`ScriptableRenderPass.CreateFramePlanSnapshot` 是冻结 + pass 状态的边界;新增 mutable pass state 时必须确认它能被 frame-plan snapshot 捕获,避免后续 camera + planning 改写前一个 camera 的 recording。 - URP features 的 `AddRenderPasses` 是 per-camera 声明点,不是 per-stage 回调。不要通过 `RenderingData.stage` 分阶段重复 enqueue;pass 所属 stage 应由 `RenderPassEvent -> RendererBlock` 映射决定,renderer recording 再按 block range 消费同一份 queue。 @@ -385,12 +389,14 @@ Scene data 每个 camera frame 提取一次,然后由 pipeline 调整。 test views 必须提供 texture-backed view。 - URP 现在已有 renderer data、renderer features、renderer pass queueing、renderer blocks、renderer-index resolution 和 per-stage recording。 -- URP stage planning 已收口到 renderer active pass queue 派生的 per-`framePlanId` 单 queue 快照和 - stage manifest。Stage support 和 stage recording 现在消费 planning 阶段保存的同一份 queue 快照, +- URP stage planning 已收口到 renderer active pass queue 派生的 per-`framePlanId` `RendererFramePlan` 和 + stage manifest。Stage support 和 stage recording 现在消费 planning 阶段保存的同一份冻结 plan, 关闭了 feature planning hook、support probe、recording 和各 stage 分别重建 pass queue 的重复事实源。 +- `ScriptableRenderPass.CreateFramePlanSnapshot` 已接入 `RendererFramePlan` 生成路径。多 camera planning 会冻结 + 每个 camera 当时的 pass 状态,后续 camera 对复用 pass 实例的 `Configure` 不应污染已生成的 frame plan。 - URP runtime-state invalidation 已覆盖 asset-level shadow/final-color settings 和 renderer-data-level main-scene/shadow-caster/depth-prepass block settings。配置变更会通过 runtime resource version 释放 - renderer caches,并让后续 planning 重新生成 pass queue 快照和 stage manifest。 + renderer caches,并让后续 planning 重新生成 `RendererFramePlan` 和 stage manifest。 - Public managed RenderGraph raster authoring 已存在;internal fullscreen kernels 仍是 URP implementation details。 - Public managed `SetRenderFunc` 已从 recording-time 调用改为 RenderGraph execution-time 调用; diff --git a/managed/GameScripts/RenderPipelineApiProbe.cs b/managed/GameScripts/RenderPipelineApiProbe.cs index 0097cbe4..2306f8b5 100644 --- a/managed/GameScripts/RenderPipelineApiProbe.cs +++ b/managed/GameScripts/RenderPipelineApiProbe.cs @@ -244,6 +244,57 @@ namespace Gameplay } } + internal sealed class MutableFullscreenPass : ScriptableRenderPass + { + private string m_passName = string.Empty; + + public void Configure( + RenderPassEvent passEvent, + string passName) + { + renderPassEvent = passEvent; + m_passName = passName ?? string.Empty; + } + + protected override bool RecordRenderGraph( + ScriptableRenderContext context, + RenderingData renderingData) + { + if (context == null || + renderingData == null) + { + return false; + } + + RenderGraphTextureHandle sourceColor = + context.sourceColorTexture; + RenderGraphTextureHandle outputColor = + context.primaryColorTarget; + if (!sourceColor.isValid || + !outputColor.isValid) + { + return false; + } + + return context + .AddRasterPass(ResolvePassName()) + .UseColorSource(sourceColor) + .SetColorAttachment(outputColor) + .SetRenderFunc( + rasterContext => + { + }) + .Commit(); + } + + private string ResolvePassName() + { + return string.IsNullOrEmpty(m_passName) + ? "Gameplay.MutableFullscreenRasterPass" + : m_passName; + } + } + internal sealed class RenderPassLifecycleApiProbePass : ScriptableRenderPass { @@ -1749,6 +1800,52 @@ namespace Gameplay } } + internal sealed class ManagedPassQueueIsolationProbeFeature + : ScriptableRendererFeature + { + private readonly MutableFullscreenPass m_pass = + new MutableFullscreenPass(); + private int m_addRenderPassesCallCount; + + public override void AddRenderPasses( + ScriptableRenderer renderer, + RenderingData renderingData) + { + if (renderer == null || + renderingData == null) + { + return; + } + + m_addRenderPassesCallCount++; + m_pass.Configure( + RenderPassEvent.BeforeRenderingPostProcessing, + m_addRenderPassesCallCount == 1 + ? "ManagedPassQueueIsolation.First" + : "ManagedPassQueueIsolation.Second"); + renderer.EnqueuePass(m_pass); + } + } + + internal sealed class ManagedPassQueueIsolationProbeRendererData + : ProbeRendererData + { + public ManagedPassQueueIsolationProbeRendererData() + : base(false) + { + rendererFeatures = + ProbeScriptableObjectFactory + .CreateRendererFeatureList( + ProbeScriptableObjectFactory + .Create()); + } + + protected override ScriptableRenderer CreateProbeRenderer() + { + return new ProbeSceneRenderer(); + } + } + internal sealed class ManagedRenderContextCameraDataProbeRendererData : ProbeRendererData { @@ -2433,6 +2530,19 @@ namespace Gameplay } } + public sealed class ManagedPassQueueIsolationProbeAsset + : UniversalRenderPipelineAsset + { + public ManagedPassQueueIsolationProbeAsset() + { + rendererDataList = + ProbeScriptableObjectFactory + .CreateRendererDataList( + ProbeScriptableObjectFactory + .Create()); + } + } + public sealed class ManagedInvalidFullscreenStagePlanningProbeAsset : UniversalRenderPipelineAsset { diff --git a/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/ScriptableRenderPass.cs b/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/ScriptableRenderPass.cs index 6decbaf8..56f3540d 100644 --- a/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/ScriptableRenderPass.cs +++ b/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/ScriptableRenderPass.cs @@ -108,6 +108,11 @@ namespace XCEngine.Rendering.Universal } } + internal ScriptableRenderPass CreateFramePlanSnapshot() + { + return CloneForFramePlan() ?? this; + } + public virtual void Execute( ScriptableRenderContext context, ref RenderingData renderingData) @@ -136,6 +141,11 @@ namespace XCEngine.Rendering.Universal { } + protected virtual ScriptableRenderPass CloneForFramePlan() + { + return (ScriptableRenderPass)MemberwiseClone(); + } + protected virtual bool RecordRenderGraph( ScriptableRenderContext context, RenderingData renderingData) diff --git a/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/ScriptableRenderer.cs b/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/ScriptableRenderer.cs index 3ca072a6..b25184fb 100644 --- a/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/ScriptableRenderer.cs +++ b/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/ScriptableRenderer.cs @@ -17,9 +17,9 @@ namespace XCEngine.Rendering.Universal public bool finalOutput; } - private sealed class PassQueueFrameSnapshot + private sealed class RendererFramePlan { - public PassQueueFrameSnapshot( + public RendererFramePlan( ulong framePlanId, int rendererIndex, List activePassQueue) @@ -43,9 +43,9 @@ namespace XCEngine.Rendering.Universal new List(); private readonly RendererBlocks m_rendererBlocks = new RendererBlocks(); - private readonly Dictionary - m_framePlanSnapshots = - new Dictionary(); + private readonly Dictionary + m_framePlans = + new Dictionary(); private readonly CommandBuffer m_finishCameraStackCommandBuffer = new CommandBuffer("ScriptableRenderer.FinishCameraStack"); private bool m_disposed; @@ -75,7 +75,7 @@ namespace XCEngine.Rendering.Universal m_features.Clear(); m_activePassQueue.Clear(); m_rendererBlocks.Clear(); - m_framePlanSnapshots.Clear(); + m_framePlans.Clear(); m_disposed = true; } @@ -151,10 +151,10 @@ namespace XCEngine.Rendering.Universal return; } - PassQueueFrameSnapshot snapshot = - BuildPassQueueFrameSnapshot(context); - ApplyPassQueueStageManifest(context, snapshot.manifest); - StorePassQueueFrameSnapshot(snapshot); + RendererFramePlan framePlan = + BuildRendererFramePlan(context); + ApplyPassQueueStageManifest(context, framePlan.manifest); + StoreRendererFramePlan(framePlan); ClearPassQueue(); } @@ -195,14 +195,14 @@ namespace XCEngine.Rendering.Universal RenderingData renderingData = context.renderingData; - PassQueueFrameSnapshot snapshot; - if (TryGetPassQueueFrameSnapshot( + RendererFramePlan framePlan; + if (TryGetRendererFramePlan( context, - out snapshot)) + out framePlan)) { - return SupportsRendererRecordingFromSnapshot( + return SupportsRendererRecordingFromFramePlan( context, - snapshot); + framePlan); } if (context.framePlanId != 0UL) @@ -232,14 +232,14 @@ namespace XCEngine.Rendering.Universal RenderingData renderingData = context.renderingData; - PassQueueFrameSnapshot snapshot; - if (TryGetPassQueueFrameSnapshot( + RendererFramePlan framePlan; + if (TryGetRendererFramePlan( context, - out snapshot)) + out framePlan)) { - return RecordRendererFromSnapshot( + return RecordRendererFromFramePlan( context, - snapshot); + framePlan); } if (context.framePlanId != 0UL) @@ -422,7 +422,7 @@ namespace XCEngine.Rendering.Universal } } - private PassQueueFrameSnapshot BuildPassQueueFrameSnapshot( + private RendererFramePlan BuildRendererFramePlan( ScriptableRenderPipelinePlanningContext context) { bool finalColorRequiresProcessing = @@ -436,18 +436,18 @@ namespace XCEngine.Rendering.Universal finalColorRequiresProcessing, framePlanId)); List activePassQueue = - new List( + SnapshotActivePassQueue( m_activePassQueue); RendererBlocks rendererBlocks = new RendererBlocks(); rendererBlocks.Build(activePassQueue); - PassQueueFrameSnapshot snapshot = - new PassQueueFrameSnapshot( + RendererFramePlan framePlan = + new RendererFramePlan( framePlanId, rendererIndex, activePassQueue); - snapshot.manifest = new PassQueueStageManifest + framePlan.manifest = new PassQueueStageManifest { shadowCaster = HasRendererStageBlocks( CameraFrameStage.ShadowCaster, @@ -463,7 +463,7 @@ namespace XCEngine.Rendering.Universal rendererBlocks) }; - return snapshot; + return framePlan; } private static void ApplyPassQueueStageManifest( @@ -716,24 +716,24 @@ namespace XCEngine.Rendering.Universal return true; } - private void StorePassQueueFrameSnapshot( - PassQueueFrameSnapshot snapshot) + private void StoreRendererFramePlan( + RendererFramePlan framePlan) { - if (snapshot == null || - snapshot.framePlanId == 0UL) + if (framePlan == null || + framePlan.framePlanId == 0UL) { return; } - if (!m_framePlanSnapshots.ContainsKey( - snapshot.framePlanId) && - m_framePlanSnapshots.Count >= + if (!m_framePlans.ContainsKey( + framePlan.framePlanId) && + m_framePlans.Count >= kMaxFramePlanSnapshotCount) { ulong oldestFramePlanId = 0UL; bool hasOldestFramePlanId = false; foreach (ulong framePlanId in - m_framePlanSnapshots.Keys) + m_framePlans.Keys) { oldestFramePlanId = framePlanId; hasOldestFramePlanId = true; @@ -742,31 +742,31 @@ namespace XCEngine.Rendering.Universal if (hasOldestFramePlanId) { - m_framePlanSnapshots.Remove( + m_framePlans.Remove( oldestFramePlanId); } } - m_framePlanSnapshots[snapshot.framePlanId] = - snapshot; + m_framePlans[framePlan.framePlanId] = + framePlan; } - private bool TryGetPassQueueFrameSnapshot( + private bool TryGetRendererFramePlan( RendererRecordingContext context, - out PassQueueFrameSnapshot frameSnapshot) + out RendererFramePlan framePlan) { - frameSnapshot = null; + framePlan = null; if (context == null || context.framePlanId == 0UL) { return false; } - if (!m_framePlanSnapshots.TryGetValue( + if (!m_framePlans.TryGetValue( context.framePlanId, - out frameSnapshot) || - frameSnapshot == null || - frameSnapshot.rendererIndex != + out framePlan) || + framePlan == null || + framePlan.rendererIndex != context.rendererIndex) { return false; @@ -775,11 +775,11 @@ namespace XCEngine.Rendering.Universal return true; } - private bool SupportsRendererRecordingFromSnapshot( + private bool SupportsRendererRecordingFromFramePlan( RendererRecordingContext context, - PassQueueFrameSnapshot snapshot) + RendererFramePlan framePlan) { - LoadPassQueueFrameSnapshot(snapshot); + LoadRendererFramePlan(framePlan); try { return SupportsRendererStage(context); @@ -790,11 +790,11 @@ namespace XCEngine.Rendering.Universal } } - private bool RecordRendererFromSnapshot( + private bool RecordRendererFromFramePlan( RendererRecordingContext context, - PassQueueFrameSnapshot snapshot) + RendererFramePlan framePlan) { - LoadPassQueueFrameSnapshot(snapshot); + LoadRendererFramePlan(framePlan); try { bool recorded; @@ -821,21 +821,47 @@ namespace XCEngine.Rendering.Universal } } - private void LoadPassQueueFrameSnapshot( - PassQueueFrameSnapshot snapshot) + private void LoadRendererFramePlan( + RendererFramePlan framePlan) { ClearPassQueue(); - if (snapshot == null || - snapshot.activePassQueue == null) + if (framePlan == null || + framePlan.activePassQueue == null) { return; } m_activePassQueue.AddRange( - snapshot.activePassQueue); + framePlan.activePassQueue); m_rendererBlocks.Build(m_activePassQueue); } + private static List SnapshotActivePassQueue( + IList activePassQueue) + { + List snapshot = + new List(); + if (activePassQueue == null) + { + return snapshot; + } + + for (int i = 0; i < activePassQueue.Count; ++i) + { + ScriptableRenderPass renderPass = + activePassQueue[i]; + if (renderPass == null) + { + continue; + } + + snapshot.Add( + renderPass.CreateFramePlanSnapshot()); + } + + return snapshot; + } + private void BuildPassQueue( RenderingData renderingData) { diff --git a/tests/scripting/test_mono_script_runtime.cpp b/tests/scripting/test_mono_script_runtime.cpp index 1cf91416..f39b2148 100644 --- a/tests/scripting/test_mono_script_runtime.cpp +++ b/tests/scripting/test_mono_script_runtime.cpp @@ -5229,6 +5229,135 @@ TEST_F( pipelineHost->Shutdown(); } +TEST_F( + MonoScriptRuntimeTest, + ManagedRendererFramePlanFreezesMutablePassStatePerCamera) { + const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor descriptor = { + "GameScripts", + "Gameplay", + "ManagedPassQueueIsolationProbeAsset" + }; + + auto asset = + std::make_shared< + XCEngine::Rendering::Pipelines::ManagedScriptableRenderPipelineAsset>( + descriptor); + + TestRenderDevice device; + TestRenderCommandList commandList; + TestRenderCommandQueue commandQueue; + const XCEngine::Rendering::RenderContext renderContext = + CreateRenderContext( + device, + commandList, + commandQueue); + + XCEngine::Rendering::CameraRenderRequest request = {}; + request.context = renderContext; + request.surface = XCEngine::Rendering::RenderSurface(64u, 64u); + TestRenderResourceView colorAttachmentView( + XCEngine::RHI::ResourceViewType::RenderTarget, + XCEngine::RHI::ResourceViewDimension::Texture2D, + XCEngine::RHI::Format::R8G8B8A8_UNorm); + TestRenderResourceView depthAttachmentView( + XCEngine::RHI::ResourceViewType::DepthStencil, + XCEngine::RHI::ResourceViewDimension::Texture2D, + XCEngine::RHI::Format::D32_Float); + request.surface.SetColorAttachment( + &colorAttachmentView); + request.surface.SetDepthAttachment( + &depthAttachmentView); + + XCEngine::Rendering::RenderPipelineHost host(asset); + const std::vector plans = + host.BuildFramePlans({ request, request }); + + ASSERT_EQ(plans.size(), 2u); + const XCEngine::Rendering::CameraFramePlan& firstPlan = plans[0]; + ASSERT_NE(firstPlan.framePlanId, 0u); + ASSERT_TRUE( + firstPlan.IsFullscreenStageRequested( + XCEngine::Rendering::CameraFrameStage::PostProcess)); + + auto* pipelineHost = + dynamic_cast( + host.GetPipeline()); + ASSERT_NE(pipelineHost, nullptr); + ASSERT_TRUE( + pipelineHost->Initialize( + renderContext)) + << runtime->GetLastError(); + + XCEngine::Rendering::RenderGraph graph; + XCEngine::Rendering::RenderGraphBuilder graphBuilder(graph); + XCEngine::Rendering::RenderGraphTextureDesc colorDesc = {}; + colorDesc.width = 64u; + colorDesc.height = 64u; + colorDesc.format = + static_cast( + XCEngine::RHI::Format::R8G8B8A8_UNorm); + TestRenderResourceView sourceColorView( + XCEngine::RHI::ResourceViewType::ShaderResource, + XCEngine::RHI::ResourceViewDimension::Texture2D, + XCEngine::RHI::Format::R8G8B8A8_UNorm); + const XCEngine::Rendering::RenderGraphTextureHandle sourceColor = + graphBuilder.ImportTexture( + "ManagedPassQueueIsolationSource", + colorDesc, + &sourceColorView, + {}); + const XCEngine::Rendering::RenderGraphTextureHandle outputColor = + graphBuilder.CreateTransientTexture( + "ManagedPassQueueIsolationOutput", + colorDesc); + + const XCEngine::Rendering::RenderSceneData sceneData = {}; + const XCEngine::Rendering::RenderSurface surface(64u, 64u); + bool executionSucceeded = true; + XCEngine::Rendering::RenderGraphBlackboard blackboard = {}; + XCEngine::Rendering::EmplaceCameraFrameRenderGraphFrameData(blackboard) + .resources.mainScene.color = sourceColor; + XCEngine::Rendering::RenderPipelineStageRenderGraphContext graphContext = { + graphBuilder, + "ManagedPassQueueIsolation", + XCEngine::Rendering::CameraFrameStage::PostProcess, + renderContext, + sceneData, + surface, + nullptr, + nullptr, + XCEngine::RHI::ResourceStates::Common, + {}, + { outputColor }, + {}, + {}, + &executionSucceeded, + &blackboard + }; + graphContext.rendererIndex = firstPlan.request.rendererIndex; + graphContext.framePlanId = firstPlan.framePlanId; + + EXPECT_TRUE( + pipelineHost->RecordStageRenderGraph( + graphContext)) + << runtime->GetLastError(); + + XCEngine::Rendering::CompiledRenderGraph compiledGraph = {}; + XCEngine::Containers::String errorMessage; + ASSERT_TRUE( + XCEngine::Rendering::RenderGraphCompiler::Compile( + graph, + compiledGraph, + &errorMessage)) + << errorMessage.CStr(); + ASSERT_EQ(compiledGraph.GetPassCount(), 1u); + EXPECT_STREQ( + compiledGraph.GetPassName(0).CStr(), + "ManagedPassQueueIsolation.First"); + + pipelineHost->Shutdown(); +} + TEST_F( MonoScriptRuntimeTest, ManagedPlanningContextRejectsNonFullscreenStageRequests) {