diff --git a/docs/used/SRP_Managed生命周期清理接缝计划_完成归档_2026-04-20.md b/docs/used/SRP_Managed生命周期清理接缝计划_完成归档_2026-04-20.md new file mode 100644 index 00000000..78e1afa5 --- /dev/null +++ b/docs/used/SRP_Managed生命周期清理接缝计划_完成归档_2026-04-20.md @@ -0,0 +1,132 @@ +# SRP Managed生命周期清理接缝计划 2026-04-20 + +## 1. 阶段目标 + +当前 managed SRP/URP 主线里,`RenderPipelineAsset -> active pipeline instance` 的 ownership 已经成立,但生命周期还缺最后一环: + +1. managed `ScriptableRenderPipeline` 没有正式的 dispose/cleanup 接缝 +2. `ScriptableRenderer` / `ScriptableRendererFeature` 也没有 cleanup 语义 +3. `ScriptableRendererData` 虽然缓存了 renderer instance,但没有正式释放路径 +4. native 在释放 managed pipeline handle 时,只是销毁 GC handle,没有先走 managed cleanup + +这一阶段的目标,就是把这条生命周期链补齐,避免后续做资源化 renderer、更多 renderer feature、甚至真正的 SRP 资源对象时继续堆积隐式状态。 + +--- + +## 2. 当前问题 + +### 2.1 active pipeline 能建立,但不能正式销毁 + +现在 `MonoManagedRenderPipelineAssetRuntime` 会缓存并持有 shared managed pipeline instance。 + +但 release 时只是: + +1. native 侧直接销毁 external managed object handle +2. managed pipeline 本身没有机会执行 cleanup + +这意味着“创建”已经像 Unity 了,“释放”还没有。 + +### 2.2 renderer/feature 没有正式生命周期 + +现在 `ScriptableRenderer` 负责: + +1. 持有 feature 列表 +2. 构建 active pass queue +3. 执行 stage record + +但它没有: + +1. renderer cleanup hook +2. feature cleanup hook +3. renderer data 缓存失效/释放 hook + +后面一旦 renderer 或 feature 开始持有真正的资源对象,这里就会成为脏状态积累点。 + +### 2.3 Universal 现在缺少“pipeline dispose -> renderer cache release”这一跳 + +当前 `UniversalRenderPipelineAsset` 已经正式持有: + +1. renderer data 列表 +2. 默认 renderer 选择 + +`ScriptableRendererData` 也已经正式缓存 renderer instance。 + +那就意味着 pipeline 销毁时,应该有一条明确链路把对应 renderer instance cache 收掉;否则 asset/runtime 生命周期和 renderer cache 生命周期仍然不闭合。 + +--- + +## 3. 本阶段方案 + +### 方案核心 + +建立一条完整的 managed 生命周期链: + +1. native release managed pipeline 前,先调用 pipeline dispose +2. `ScriptableRenderPipeline` 提供正式 dispose seam +3. `UniversalRenderPipeline` 在 dispose 时回收 asset 上缓存的 renderer instance +4. `ScriptableRendererData` 提供释放 renderer cache 的内部入口 +5. `ScriptableRenderer` 负责释放 feature 和自身队列状态 +6. `ScriptableRendererFeature` 提供正式 dispose seam + +### 预期结果 + +阶段完成后,生命周期关系会变成: + +`ManagedAssetRuntime -> managed pipeline dispose -> pipeline asset cleanup -> renderer data release -> renderer dispose -> feature dispose` + +--- + +## 4. 实施步骤 + +### Step 1:补 managed core 生命周期 API + +目标: + +1. 给 `ScriptableRenderPipeline` 增加内部 dispose 入口和受保护 cleanup seam +2. 给 `ScriptableRenderer` 增加内部 dispose 入口和 feature release +3. 给 `ScriptableRendererFeature` 增加受保护 dispose seam +4. 给 `ScriptableRendererData` 增加 renderer cache release 入口 + +### Step 2:把 Universal 链接到 cleanup + +目标: + +1. 让 `ScriptableRenderPipelineAsset` 提供内部 pipeline-resource release seam +2. 让 `UniversalRenderPipelineAsset` override 回收 rendererDataList 中的 renderer caches +3. 让 `UniversalRenderPipeline` 在 dispose 时调用 asset cleanup + +### Step 3:native release 前调用 managed dispose + +目标: + +1. `MonoManagedRenderPipelineAssetRuntime` 在释放 shared pipeline handle 前先调用 managed pipeline dispose +2. 保持 runtime ownership 不回退、不重新散乱到 recorder + +### Step 4:补生命周期回归测试 + +目标: + +1. 验证 runtime release 时 managed pipeline dispose 被调用 +2. 验证 Universal renderer/feature cleanup 会沿 dispose 链执行 +3. 保持现有 SRP/URP 创建、复用、录制能力不回归 + +### Step 5:完整验证与收口 + +目标: + +1. 编译 `rendering_unit_tests`、`scripting_tests`、`XCEditor` +2. 运行测试 +3. 运行旧版 `XCEditor` 10 秒冒烟并验证新 `SceneReady` +4. 归档 plan,提交并推送 + +--- + +## 5. 验收标准 + +本阶段完成后应满足: + +1. managed pipeline release 前会先执行正式 dispose +2. `ScriptableRenderer` / `ScriptableRendererFeature` 不再只有创建,没有 cleanup +3. `ScriptableRendererData` 缓存的 renderer instance 有明确释放路径 +4. `UniversalRenderPipeline` 生命周期能闭合到 renderer cache +5. 现有 SRP/URP 主线测试与 editor 冒烟全部通过 diff --git a/engine/src/Scripting/Mono/MonoScriptRuntime.cpp b/engine/src/Scripting/Mono/MonoScriptRuntime.cpp index 956c5fe2..4376ea75 100644 --- a/engine/src/Scripting/Mono/MonoScriptRuntime.cpp +++ b/engine/src/Scripting/Mono/MonoScriptRuntime.cpp @@ -738,6 +738,7 @@ private: void ReleaseManagedPipeline() const; void ReleaseManagedAsset() const; MonoObject* GetManagedAssetObject() const; + MonoMethod* ResolveDisposePipelineMethod(MonoObject* pipelineObject) const; MonoMethod* ResolveCreatePipelineMethod(MonoObject* assetObject) const; MonoMethod* ResolveConfigureCameraRenderRequestMethod( MonoObject* assetObject) const; @@ -745,15 +746,19 @@ private: MonoObject* assetObject) const; MonoMethod* ResolveGetPipelineRendererAssetKeyMethod( MonoObject* assetObject) const; + MonoMethod* ResolveReleaseRuntimeResourcesMethod( + MonoObject* assetObject) const; MonoScriptRuntime* m_runtime = nullptr; std::weak_ptr m_runtimeLifetime; Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor m_descriptor; mutable uint32_t m_assetHandle = 0; + mutable MonoMethod* m_disposePipelineMethod = nullptr; mutable MonoMethod* m_createPipelineMethod = nullptr; mutable MonoMethod* m_configureCameraRenderRequestMethod = nullptr; mutable MonoMethod* m_getDefaultFinalColorSettingsMethod = nullptr; mutable MonoMethod* m_getPipelineRendererAssetKeyMethod = nullptr; + mutable MonoMethod* m_releaseRuntimeResourcesMethod = nullptr; mutable bool m_ownsManagedAssetHandle = false; mutable bool m_assetCreationAttempted = false; mutable uint32_t m_pipelineHandle = 0; @@ -1233,6 +1238,7 @@ bool MonoManagedRenderPipelineAssetRuntime::EnsureManagedAsset() const { void MonoManagedRenderPipelineAssetRuntime::ReleaseManagedPipeline() const { m_pipelineCreationAttempted = false; + m_disposePipelineMethod = nullptr; if (!IsRuntimeAlive()) { m_pipelineHandle = 0; @@ -1240,6 +1246,18 @@ void MonoManagedRenderPipelineAssetRuntime::ReleaseManagedPipeline() const { } if (m_pipelineHandle != 0) { + MonoObject* const pipelineObject = + m_runtime->GetManagedObject(m_pipelineHandle); + MonoMethod* const disposeMethod = + ResolveDisposePipelineMethod(pipelineObject); + if (pipelineObject != nullptr && disposeMethod != nullptr) { + m_runtime->InvokeManagedMethod( + pipelineObject, + disposeMethod, + nullptr, + nullptr); + } + m_runtime->DestroyExternalManagedObject(m_pipelineHandle); m_pipelineHandle = 0; } @@ -1247,6 +1265,7 @@ void MonoManagedRenderPipelineAssetRuntime::ReleaseManagedPipeline() const { void MonoManagedRenderPipelineAssetRuntime::ReleaseManagedAsset() const { ReleaseManagedPipeline(); + m_releaseRuntimeResourcesMethod = nullptr; m_createPipelineMethod = nullptr; m_configureCameraRenderRequestMethod = nullptr; m_getDefaultFinalColorSettingsMethod = nullptr; @@ -1262,6 +1281,17 @@ void MonoManagedRenderPipelineAssetRuntime::ReleaseManagedAsset() const { return; } + MonoObject* const assetObject = GetManagedAssetObject(); + MonoMethod* const releaseRuntimeResourcesMethod = + ResolveReleaseRuntimeResourcesMethod(assetObject); + if (assetObject != nullptr && releaseRuntimeResourcesMethod != nullptr) { + m_runtime->InvokeManagedMethod( + assetObject, + releaseRuntimeResourcesMethod, + nullptr, + nullptr); + } + if (m_assetHandle != 0 && ownsManagedAssetHandle) { m_runtime->DestroyExternalManagedObject(m_assetHandle); } @@ -1275,6 +1305,19 @@ MonoObject* MonoManagedRenderPipelineAssetRuntime::GetManagedAssetObject() const : nullptr; } +MonoMethod* MonoManagedRenderPipelineAssetRuntime::ResolveDisposePipelineMethod( + MonoObject* pipelineObject) const { + if (m_disposePipelineMethod == nullptr) { + m_disposePipelineMethod = + m_runtime->ResolveManagedMethod( + pipelineObject, + "DisposeInstance", + 0); + } + + return m_disposePipelineMethod; +} + MonoMethod* MonoManagedRenderPipelineAssetRuntime::ResolveCreatePipelineMethod( MonoObject* assetObject) const { if (m_createPipelineMethod == nullptr) { @@ -1330,6 +1373,20 @@ MonoManagedRenderPipelineAssetRuntime::ResolveGetPipelineRendererAssetKeyMethod( return m_getPipelineRendererAssetKeyMethod; } +MonoMethod* +MonoManagedRenderPipelineAssetRuntime::ResolveReleaseRuntimeResourcesMethod( + MonoObject* assetObject) const { + if (m_releaseRuntimeResourcesMethod == nullptr) { + m_releaseRuntimeResourcesMethod = + m_runtime->ResolveManagedMethod( + assetObject, + "ReleaseRuntimeResourcesInstance", + 0); + } + + return m_releaseRuntimeResourcesMethod; +} + class MonoManagedRenderPipelineBridge final : public Rendering::Pipelines::ManagedRenderPipelineBridge { public: diff --git a/managed/GameScripts/RenderPipelineApiProbe.cs b/managed/GameScripts/RenderPipelineApiProbe.cs index 704e04a7..e1bebd5b 100644 --- a/managed/GameScripts/RenderPipelineApiProbe.cs +++ b/managed/GameScripts/RenderPipelineApiProbe.cs @@ -917,6 +917,126 @@ namespace Gameplay } } + internal static class ManagedLifecycleProbeState + { + public static int CreatePipelineCallCount; + public static int CreateRendererCallCount; + public static int CreateFeatureCallCount; + public static int ReleaseRendererDataRuntimeResourcesCallCount; + public static int DisposePipelineCallCount; + public static int ReleaseAssetRuntimeResourcesCallCount; + public static int DisposeRendererCallCount; + public static int DisposeFeatureCallCount; + + public static void Reset() + { + CreatePipelineCallCount = 0; + CreateRendererCallCount = 0; + CreateFeatureCallCount = 0; + ReleaseRendererDataRuntimeResourcesCallCount = 0; + DisposePipelineCallCount = 0; + ReleaseAssetRuntimeResourcesCallCount = 0; + DisposeRendererCallCount = 0; + DisposeFeatureCallCount = 0; + } + } + + internal sealed class ManagedLifecycleProbeRendererFeature + : ScriptableRendererFeature + { + protected override void ReleaseRuntimeResources() + { + ManagedLifecycleProbeState.DisposeFeatureCallCount++; + } + } + + internal sealed class ManagedLifecycleProbeRenderer + : ScriptableRenderer + { + public ManagedLifecycleProbeRenderer( + ScriptableRendererFeature[] rendererFeatures) + { + ScriptableRendererFeature[] resolvedRendererFeatures = + rendererFeatures ?? Array.Empty(); + for (int i = 0; i < resolvedRendererFeatures.Length; ++i) + { + AddFeature(resolvedRendererFeatures[i]); + } + } + + protected override bool SupportsStageRenderGraph( + CameraFrameStage stage) + { + return stage == CameraFrameStage.MainScene; + } + + protected override bool RecordStageRenderGraph( + ScriptableRenderContext context) + { + return context != null; + } + + protected override void ReleaseRuntimeResources() + { + ManagedLifecycleProbeState.DisposeRendererCallCount++; + } + } + + internal sealed class ManagedLifecycleProbeRendererData + : ScriptableRendererData + { + protected override ScriptableRenderer CreateRenderer() + { + ManagedLifecycleProbeState.CreateRendererCallCount++; + return new ManagedLifecycleProbeRenderer( + CreateRendererFeatures()); + } + + protected override ScriptableRendererFeature[] CreateRendererFeatures() + { + ManagedLifecycleProbeState.CreateFeatureCallCount++; + return new ScriptableRendererFeature[] + { + new ManagedLifecycleProbeRendererFeature() + }; + } + + protected override string GetPipelineRendererAssetKey() + { + return "BuiltinForward"; + } + + protected override void ReleaseRuntimeResources() + { + ManagedLifecycleProbeState.ReleaseRendererDataRuntimeResourcesCallCount++; + } + } + + internal sealed class ManagedPipelineDisposeProbe + : ScriptableRenderPipeline + { + protected override bool SupportsStageRenderGraph( + CameraFrameStage stage) + { + return stage == CameraFrameStage.MainScene; + } + + protected override bool RecordStageRenderGraph( + ScriptableRenderContext context) + { + return context != null; + } + + protected override void Dispose( + bool disposing) + { + if (disposing) + { + ManagedLifecycleProbeState.DisposePipelineCallCount++; + } + } + } + public sealed class LegacyRenderPipelineApiProbeAsset : RenderPipelineAsset { } @@ -925,6 +1045,40 @@ namespace Gameplay { } + public sealed class ManagedPipelineDisposeProbeAsset + : ScriptableRenderPipelineAsset + { + protected override ScriptableRenderPipeline CreatePipeline() + { + ManagedLifecycleProbeState.CreatePipelineCallCount++; + return new ManagedPipelineDisposeProbe(); + } + } + + public sealed class ManagedUniversalLifecycleProbeAsset + : UniversalRenderPipelineAsset + { + public ManagedUniversalLifecycleProbeAsset() + { + rendererDataList = new ScriptableRendererData[] + { + new ManagedLifecycleProbeRendererData() + }; + } + + protected override ScriptableRenderPipeline CreatePipeline() + { + ManagedLifecycleProbeState.CreatePipelineCallCount++; + return base.CreatePipeline(); + } + + protected override void ReleaseRuntimeResources() + { + ManagedLifecycleProbeState.ReleaseAssetRuntimeResourcesCallCount++; + ReleaseRendererDataRuntimeResources(); + } + } + public sealed class ManagedRenderPipelineProbeAsset : UniversalRenderPipelineAsset { @@ -1323,6 +1477,86 @@ namespace Gameplay } } + public sealed class ManagedLifecycleObservationProbe + : MonoBehaviour + { + public int ObservedCreatePipelineCallCount; + public int ObservedCreateRendererCallCount; + public int ObservedCreateFeatureCallCount; + public int ObservedReleaseRendererDataRuntimeResourcesCallCount; + public int ObservedDisposePipelineCallCount; + public int ObservedReleaseAssetRuntimeResourcesCallCount; + public int ObservedDisposeRendererCallCount; + public int ObservedDisposeFeatureCallCount; + + public void Start() + { + ManagedLifecycleProbeState.Reset(); + } + + public void Update() + { + ObservedCreatePipelineCallCount = + ManagedLifecycleProbeState.CreatePipelineCallCount; + ObservedCreateRendererCallCount = + ManagedLifecycleProbeState.CreateRendererCallCount; + ObservedCreateFeatureCallCount = + ManagedLifecycleProbeState.CreateFeatureCallCount; + ObservedReleaseRendererDataRuntimeResourcesCallCount = + ManagedLifecycleProbeState + .ReleaseRendererDataRuntimeResourcesCallCount; + ObservedDisposePipelineCallCount = + ManagedLifecycleProbeState.DisposePipelineCallCount; + ObservedReleaseAssetRuntimeResourcesCallCount = + ManagedLifecycleProbeState.ReleaseAssetRuntimeResourcesCallCount; + ObservedDisposeRendererCallCount = + ManagedLifecycleProbeState.DisposeRendererCallCount; + ObservedDisposeFeatureCallCount = + ManagedLifecycleProbeState.DisposeFeatureCallCount; + } + } + + public sealed class ManagedUniversalLifecycleRuntimeSelectionProbe + : MonoBehaviour + { + public int ObservedCreatePipelineCallCount; + public int ObservedCreateRendererCallCount; + public int ObservedCreateFeatureCallCount; + public int ObservedReleaseRendererDataRuntimeResourcesCallCount; + public int ObservedDisposePipelineCallCount; + public int ObservedReleaseAssetRuntimeResourcesCallCount; + public int ObservedDisposeRendererCallCount; + public int ObservedDisposeFeatureCallCount; + + public void Start() + { + ManagedLifecycleProbeState.Reset(); + GraphicsSettings.renderPipelineAsset = + new ManagedUniversalLifecycleProbeAsset(); + } + + public void Update() + { + ObservedCreatePipelineCallCount = + ManagedLifecycleProbeState.CreatePipelineCallCount; + ObservedCreateRendererCallCount = + ManagedLifecycleProbeState.CreateRendererCallCount; + ObservedCreateFeatureCallCount = + ManagedLifecycleProbeState.CreateFeatureCallCount; + ObservedReleaseRendererDataRuntimeResourcesCallCount = + ManagedLifecycleProbeState + .ReleaseRendererDataRuntimeResourcesCallCount; + ObservedDisposePipelineCallCount = + ManagedLifecycleProbeState.DisposePipelineCallCount; + ObservedReleaseAssetRuntimeResourcesCallCount = + ManagedLifecycleProbeState.ReleaseAssetRuntimeResourcesCallCount; + ObservedDisposeRendererCallCount = + ManagedLifecycleProbeState.DisposeRendererCallCount; + ObservedDisposeFeatureCallCount = + ManagedLifecycleProbeState.DisposeFeatureCallCount; + } + } + public sealed class ScriptCoreUniversalRenderPipelineSelectionProbe : MonoBehaviour { diff --git a/managed/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipeline.cs b/managed/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipeline.cs index bb786f4e..bb6cdf64 100644 --- a/managed/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipeline.cs +++ b/managed/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipeline.cs @@ -4,10 +4,23 @@ namespace XCEngine.Rendering { public abstract class ScriptableRenderPipeline : Object { + private bool m_disposed; + protected ScriptableRenderPipeline() { } + internal void DisposeInstance() + { + if (m_disposed) + { + return; + } + + Dispose(true); + m_disposed = true; + } + protected virtual bool SupportsStageRenderGraph( CameraFrameStage stage) { @@ -19,6 +32,11 @@ namespace XCEngine.Rendering { return false; } + + protected virtual void Dispose( + bool disposing) + { + } } } diff --git a/managed/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipelineAsset.cs b/managed/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipelineAsset.cs index 7a65694b..4698d900 100644 --- a/managed/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipelineAsset.cs +++ b/managed/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipelineAsset.cs @@ -8,6 +8,11 @@ namespace XCEngine.Rendering { } + internal void ReleaseRuntimeResourcesInstance() + { + ReleaseRuntimeResources(); + } + protected virtual ScriptableRenderPipeline CreatePipeline() { return null; @@ -27,6 +32,10 @@ namespace XCEngine.Rendering { return string.Empty; } + + protected virtual void ReleaseRuntimeResources() + { + } } } diff --git a/managed/XCEngine.ScriptCore/Rendering/Universal/ScriptableRenderer.cs b/managed/XCEngine.ScriptCore/Rendering/Universal/ScriptableRenderer.cs index 07e44411..cf6602df 100644 --- a/managed/XCEngine.ScriptCore/Rendering/Universal/ScriptableRenderer.cs +++ b/managed/XCEngine.ScriptCore/Rendering/Universal/ScriptableRenderer.cs @@ -10,11 +10,35 @@ namespace XCEngine.Rendering.Universal new List(); private readonly List m_activePassQueue = new List(); + private bool m_disposed; protected ScriptableRenderer() { } + internal void ReleaseRuntimeResourcesInstance() + { + if (m_disposed) + { + return; + } + + ReleaseRuntimeResources(); + + for (int i = 0; i < m_features.Count; ++i) + { + ScriptableRendererFeature feature = m_features[i]; + if (feature != null) + { + feature.ReleaseRuntimeResourcesInstance(); + } + } + + m_features.Clear(); + m_activePassQueue.Clear(); + m_disposed = true; + } + public void EnqueuePass( ScriptableRenderPass renderPass) { @@ -126,6 +150,10 @@ namespace XCEngine.Rendering.Universal AddRenderPasses(renderingData); } + + protected virtual void ReleaseRuntimeResources() + { + } } } diff --git a/managed/XCEngine.ScriptCore/Rendering/Universal/ScriptableRendererData.cs b/managed/XCEngine.ScriptCore/Rendering/Universal/ScriptableRendererData.cs index 11b9cd43..751bbfd5 100644 --- a/managed/XCEngine.ScriptCore/Rendering/Universal/ScriptableRendererData.cs +++ b/managed/XCEngine.ScriptCore/Rendering/Universal/ScriptableRendererData.cs @@ -38,6 +38,35 @@ namespace XCEngine.Rendering.Universal return GetPipelineRendererAssetKey(); } + internal void ReleaseRuntimeResourcesInstance() + { + if (m_rendererInstance != null) + { + m_rendererInstance.ReleaseRuntimeResourcesInstance(); + m_rendererInstance = null; + m_rendererFeatures = null; + ReleaseRuntimeResources(); + return; + } + + if (m_rendererFeatures != null) + { + for (int i = 0; i < m_rendererFeatures.Length; ++i) + { + ScriptableRendererFeature rendererFeature = + m_rendererFeatures[i]; + if (rendererFeature != null) + { + rendererFeature.ReleaseRuntimeResourcesInstance(); + } + } + + m_rendererFeatures = null; + } + + ReleaseRuntimeResources(); + } + internal void ConfigureCameraRenderRequestInstance( CameraRenderRequestContext context) { @@ -80,6 +109,10 @@ namespace XCEngine.Rendering.Universal return Array.Empty(); } + protected virtual void ReleaseRuntimeResources() + { + } + protected bool HasDirectionalShadow( CameraRenderRequestContext context) { diff --git a/managed/XCEngine.ScriptCore/Rendering/Universal/ScriptableRendererFeature.cs b/managed/XCEngine.ScriptCore/Rendering/Universal/ScriptableRendererFeature.cs index 3f60ca35..a9c58bfd 100644 --- a/managed/XCEngine.ScriptCore/Rendering/Universal/ScriptableRendererFeature.cs +++ b/managed/XCEngine.ScriptCore/Rendering/Universal/ScriptableRendererFeature.cs @@ -5,12 +5,25 @@ namespace XCEngine.Rendering.Universal { public abstract class ScriptableRendererFeature { + private bool m_disposed; + protected ScriptableRendererFeature() { } public bool isActive { get; set; } = true; + internal void ReleaseRuntimeResourcesInstance() + { + if (m_disposed) + { + return; + } + + ReleaseRuntimeResources(); + m_disposed = true; + } + public virtual void Create() { } @@ -47,6 +60,10 @@ namespace XCEngine.Rendering.Universal .Rendering_CameraRenderRequestContext_ClearDirectionalShadow( context.nativeHandle); } + + protected virtual void ReleaseRuntimeResources() + { + } } } diff --git a/managed/XCEngine.ScriptCore/Rendering/Universal/UniversalRenderPipelineAsset.cs b/managed/XCEngine.ScriptCore/Rendering/Universal/UniversalRenderPipelineAsset.cs index c9437bf1..29797908 100644 --- a/managed/XCEngine.ScriptCore/Rendering/Universal/UniversalRenderPipelineAsset.cs +++ b/managed/XCEngine.ScriptCore/Rendering/Universal/UniversalRenderPipelineAsset.cs @@ -41,6 +41,11 @@ namespace XCEngine.Rendering.Universal : string.Empty; } + protected override void ReleaseRuntimeResources() + { + ReleaseRendererDataRuntimeResources(); + } + internal ScriptableRendererData GetDefaultRendererData() { return GetRendererData(defaultRendererIndex); @@ -92,6 +97,42 @@ namespace XCEngine.Rendering.Universal defaultRendererIndex = 0; } + protected void ReleaseRendererDataRuntimeResources() + { + EnsureRendererDataList(); + + for (int i = 0; i < rendererDataList.Length; ++i) + { + ScriptableRendererData rendererData = + rendererDataList[i]; + if (rendererData == null || + WasRendererDataReleasedEarlier(i)) + { + continue; + } + + rendererData.ReleaseRuntimeResourcesInstance(); + } + } + + private bool WasRendererDataReleasedEarlier( + int rendererDataIndex) + { + ScriptableRendererData rendererData = + rendererDataList[rendererDataIndex]; + for (int i = 0; i < rendererDataIndex; ++i) + { + if (object.ReferenceEquals( + rendererDataList[i], + rendererData)) + { + return true; + } + } + + return false; + } + private int ResolveRendererIndex( int rendererIndex) { diff --git a/tests/scripting/test_mono_script_runtime.cpp b/tests/scripting/test_mono_script_runtime.cpp index be7fc51b..227747ac 100644 --- a/tests/scripting/test_mono_script_runtime.cpp +++ b/tests/scripting/test_mono_script_runtime.cpp @@ -2499,6 +2499,337 @@ TEST_F( secondRecorder->Shutdown(); } +TEST_F( + MonoScriptRuntimeTest, + ManagedRenderPipelineBridgeInvokesManagedPipelineDisposeBeforeAssetRuntimeRelease) { + Scene* runtimeScene = + CreateScene("ManagedLifecycleDisposeObservationScene"); + GameObject* scriptObject = + runtimeScene->CreateGameObject("ManagedLifecycleObservationProbe"); + ScriptComponent* script = + AddScript( + scriptObject, + "Gameplay", + "ManagedLifecycleObservationProbe"); + ASSERT_NE(script, nullptr); + + engine->OnRuntimeStart(runtimeScene); + engine->OnUpdate(0.016f); + + { + const auto bridge = + XCEngine::Rendering::Pipelines::GetManagedRenderPipelineBridge(); + ASSERT_NE(bridge, nullptr); + + const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor descriptor = { + "GameScripts", + "Gameplay", + "ManagedPipelineDisposeProbeAsset" + }; + + std::shared_ptr + assetRuntime = bridge->CreateAssetRuntime(descriptor); + ASSERT_NE(assetRuntime, nullptr); + + std::unique_ptr + recorder = assetRuntime->CreateStageRecorder(); + ASSERT_NE(recorder, nullptr); + + const XCEngine::Rendering::RenderContext context = {}; + ASSERT_TRUE(recorder->Initialize(context)); + EXPECT_TRUE( + recorder->SupportsStageRenderGraph( + XCEngine::Rendering::CameraFrameStage::MainScene)); + + recorder->Shutdown(); + } + + engine->OnUpdate(0.016f); + EXPECT_TRUE(runtime->GetLastError().empty()) << runtime->GetLastError(); + + int observedCreatePipelineCallCount = 0; + int observedCreateRendererCallCount = 0; + int observedCreateFeatureCallCount = 0; + int observedReleaseRendererDataRuntimeResourcesCallCount = 0; + int observedDisposePipelineCallCount = 0; + int observedReleaseAssetRuntimeResourcesCallCount = 0; + int observedDisposeRendererCallCount = 0; + int observedDisposeFeatureCallCount = 0; + EXPECT_TRUE(runtime->TryGetFieldValue( + script, + "ObservedCreatePipelineCallCount", + observedCreatePipelineCallCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + script, + "ObservedCreateRendererCallCount", + observedCreateRendererCallCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + script, + "ObservedCreateFeatureCallCount", + observedCreateFeatureCallCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + script, + "ObservedReleaseRendererDataRuntimeResourcesCallCount", + observedReleaseRendererDataRuntimeResourcesCallCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + script, + "ObservedDisposePipelineCallCount", + observedDisposePipelineCallCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + script, + "ObservedReleaseAssetRuntimeResourcesCallCount", + observedReleaseAssetRuntimeResourcesCallCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + script, + "ObservedDisposeRendererCallCount", + observedDisposeRendererCallCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + script, + "ObservedDisposeFeatureCallCount", + observedDisposeFeatureCallCount)); + + EXPECT_EQ(observedCreatePipelineCallCount, 1); + EXPECT_EQ(observedCreateRendererCallCount, 0); + EXPECT_EQ(observedCreateFeatureCallCount, 0); + EXPECT_EQ(observedReleaseRendererDataRuntimeResourcesCallCount, 0); + EXPECT_EQ(observedDisposePipelineCallCount, 1); + EXPECT_EQ(observedReleaseAssetRuntimeResourcesCallCount, 0); + EXPECT_EQ(observedDisposeRendererCallCount, 0); + EXPECT_EQ(observedDisposeFeatureCallCount, 0); +} + +TEST_F( + MonoScriptRuntimeTest, + ManagedRenderPipelineBridgeReleasesUniversalRendererCachesOnAssetRuntimeRelease) { + Scene* runtimeScene = + CreateScene("ManagedUniversalLifecycleObservationScene"); + GameObject* scriptObject = + runtimeScene->CreateGameObject("ManagedUniversalLifecycleSelectionProbe"); + ScriptComponent* script = + AddScript( + scriptObject, + "Gameplay", + "ManagedUniversalLifecycleRuntimeSelectionProbe"); + ASSERT_NE(script, nullptr); + + engine->OnRuntimeStart(runtimeScene); + engine->OnUpdate(0.016f); + + const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor descriptor = + XCEngine::Rendering::Pipelines::GetConfiguredManagedRenderPipelineAssetDescriptor(); + ASSERT_TRUE(descriptor.IsValid()); + ASSERT_NE(descriptor.managedAssetHandle, 0u); + EXPECT_EQ(descriptor.assemblyName, "GameScripts"); + EXPECT_EQ(descriptor.namespaceName, "Gameplay"); + EXPECT_EQ(descriptor.className, "ManagedUniversalLifecycleProbeAsset"); + + { + const auto bridge = + XCEngine::Rendering::Pipelines::GetManagedRenderPipelineBridge(); + ASSERT_NE(bridge, nullptr); + + std::shared_ptr + assetRuntime = bridge->CreateAssetRuntime(descriptor); + ASSERT_NE(assetRuntime, nullptr); + + std::unique_ptr + recorder = assetRuntime->CreateStageRecorder(); + ASSERT_NE(recorder, nullptr); + + const XCEngine::Rendering::RenderContext context = {}; + ASSERT_TRUE(recorder->Initialize(context)); + EXPECT_TRUE( + recorder->SupportsStageRenderGraph( + XCEngine::Rendering::CameraFrameStage::MainScene)); + + recorder->Shutdown(); + } + + { + const auto bridge = + XCEngine::Rendering::Pipelines::GetManagedRenderPipelineBridge(); + ASSERT_NE(bridge, nullptr); + + std::shared_ptr + assetRuntime = bridge->CreateAssetRuntime(descriptor); + ASSERT_NE(assetRuntime, nullptr); + + std::unique_ptr + recorder = assetRuntime->CreateStageRecorder(); + ASSERT_NE(recorder, nullptr); + + const XCEngine::Rendering::RenderContext context = {}; + ASSERT_TRUE(recorder->Initialize(context)); + EXPECT_TRUE( + recorder->SupportsStageRenderGraph( + XCEngine::Rendering::CameraFrameStage::MainScene)); + + recorder->Shutdown(); + } + + engine->OnUpdate(0.016f); + EXPECT_TRUE(runtime->GetLastError().empty()) << runtime->GetLastError(); + + int observedCreatePipelineCallCount = 0; + int observedCreateRendererCallCount = 0; + int observedCreateFeatureCallCount = 0; + int observedReleaseRendererDataRuntimeResourcesCallCount = 0; + int observedDisposePipelineCallCount = 0; + int observedReleaseAssetRuntimeResourcesCallCount = 0; + int observedDisposeRendererCallCount = 0; + int observedDisposeFeatureCallCount = 0; + EXPECT_TRUE(runtime->TryGetFieldValue( + script, + "ObservedCreatePipelineCallCount", + observedCreatePipelineCallCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + script, + "ObservedCreateRendererCallCount", + observedCreateRendererCallCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + script, + "ObservedCreateFeatureCallCount", + observedCreateFeatureCallCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + script, + "ObservedReleaseRendererDataRuntimeResourcesCallCount", + observedReleaseRendererDataRuntimeResourcesCallCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + script, + "ObservedDisposePipelineCallCount", + observedDisposePipelineCallCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + script, + "ObservedReleaseAssetRuntimeResourcesCallCount", + observedReleaseAssetRuntimeResourcesCallCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + script, + "ObservedDisposeRendererCallCount", + observedDisposeRendererCallCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + script, + "ObservedDisposeFeatureCallCount", + observedDisposeFeatureCallCount)); + + EXPECT_EQ(observedCreatePipelineCallCount, 2); + EXPECT_EQ(observedCreateRendererCallCount, 2); + EXPECT_EQ(observedCreateFeatureCallCount, 2); + EXPECT_EQ(observedReleaseRendererDataRuntimeResourcesCallCount, 2); + EXPECT_EQ(observedDisposePipelineCallCount, 0); + EXPECT_EQ(observedReleaseAssetRuntimeResourcesCallCount, 2); +} + +TEST_F( + MonoScriptRuntimeTest, + ManagedRenderPipelineBridgeReleasesUniversalFeatureCachesWithoutPipelineCreation) { + Scene* runtimeScene = + CreateScene("ManagedUniversalLifecycleRequestObservationScene"); + GameObject* scriptObject = + runtimeScene->CreateGameObject("ManagedUniversalLifecycleSelectionProbe"); + ScriptComponent* script = + AddScript( + scriptObject, + "Gameplay", + "ManagedUniversalLifecycleRuntimeSelectionProbe"); + ASSERT_NE(script, nullptr); + + engine->OnRuntimeStart(runtimeScene); + engine->OnUpdate(0.016f); + + const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor descriptor = + XCEngine::Rendering::Pipelines::GetConfiguredManagedRenderPipelineAssetDescriptor(); + ASSERT_TRUE(descriptor.IsValid()); + ASSERT_NE(descriptor.managedAssetHandle, 0u); + EXPECT_EQ(descriptor.assemblyName, "GameScripts"); + EXPECT_EQ(descriptor.namespaceName, "Gameplay"); + EXPECT_EQ(descriptor.className, "ManagedUniversalLifecycleProbeAsset"); + + { + const auto bridge = + XCEngine::Rendering::Pipelines::GetManagedRenderPipelineBridge(); + ASSERT_NE(bridge, nullptr); + + std::shared_ptr + assetRuntime = bridge->CreateAssetRuntime(descriptor); + ASSERT_NE(assetRuntime, nullptr); + + XCEngine::Rendering::CameraRenderRequest request = {}; + assetRuntime->ConfigureCameraRenderRequest( + request, + 0u, + 0u, + XCEngine::Rendering::DirectionalShadowPlanningSettings{}); + } + + { + const auto bridge = + XCEngine::Rendering::Pipelines::GetManagedRenderPipelineBridge(); + ASSERT_NE(bridge, nullptr); + + std::shared_ptr + assetRuntime = bridge->CreateAssetRuntime(descriptor); + ASSERT_NE(assetRuntime, nullptr); + + XCEngine::Rendering::CameraRenderRequest request = {}; + assetRuntime->ConfigureCameraRenderRequest( + request, + 0u, + 0u, + XCEngine::Rendering::DirectionalShadowPlanningSettings{}); + } + + engine->OnUpdate(0.016f); + EXPECT_TRUE(runtime->GetLastError().empty()) << runtime->GetLastError(); + + int observedCreatePipelineCallCount = 0; + int observedCreateRendererCallCount = 0; + int observedCreateFeatureCallCount = 0; + int observedReleaseRendererDataRuntimeResourcesCallCount = 0; + int observedDisposePipelineCallCount = 0; + int observedReleaseAssetRuntimeResourcesCallCount = 0; + int observedDisposeRendererCallCount = 0; + int observedDisposeFeatureCallCount = 0; + EXPECT_TRUE(runtime->TryGetFieldValue( + script, + "ObservedCreatePipelineCallCount", + observedCreatePipelineCallCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + script, + "ObservedCreateRendererCallCount", + observedCreateRendererCallCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + script, + "ObservedCreateFeatureCallCount", + observedCreateFeatureCallCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + script, + "ObservedReleaseRendererDataRuntimeResourcesCallCount", + observedReleaseRendererDataRuntimeResourcesCallCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + script, + "ObservedDisposePipelineCallCount", + observedDisposePipelineCallCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + script, + "ObservedReleaseAssetRuntimeResourcesCallCount", + observedReleaseAssetRuntimeResourcesCallCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + script, + "ObservedDisposeRendererCallCount", + observedDisposeRendererCallCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + script, + "ObservedDisposeFeatureCallCount", + observedDisposeFeatureCallCount)); + + EXPECT_EQ(observedCreatePipelineCallCount, 0); + EXPECT_EQ(observedCreateRendererCallCount, 0); + EXPECT_EQ(observedCreateFeatureCallCount, 2); + EXPECT_EQ(observedReleaseRendererDataRuntimeResourcesCallCount, 2); + EXPECT_EQ(observedDisposePipelineCallCount, 0); + EXPECT_EQ(observedReleaseAssetRuntimeResourcesCallCount, 2); +} + TEST_F( MonoScriptRuntimeTest, ManagedRenderPipelineBridgeRuntimeExposesBuiltinForwardRendererAsset) {