From 58dde75d3d4b4a77e3b6c3ddcb80ef38742cfa03 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Mon, 20 Apr 2026 01:14:37 +0800 Subject: [PATCH] refactor(srp): add managed lifecycle cleanup seams Invoke managed pipeline disposal and asset runtime cleanup from the native bridge lifecycle. Add Universal renderer and feature cleanup hooks plus regression probes to verify runtime cache teardown semantics. --- ...命周期清理接缝计划_完成归档_2026-04-20.md | 132 +++++++ .../src/Scripting/Mono/MonoScriptRuntime.cpp | 57 +++ managed/GameScripts/RenderPipelineApiProbe.cs | 234 +++++++++++++ .../Core/ScriptableRenderPipeline.cs | 18 + .../Core/ScriptableRenderPipelineAsset.cs | 9 + .../Rendering/Universal/ScriptableRenderer.cs | 28 ++ .../Universal/ScriptableRendererData.cs | 33 ++ .../Universal/ScriptableRendererFeature.cs | 17 + .../Universal/UniversalRenderPipelineAsset.cs | 41 +++ tests/scripting/test_mono_script_runtime.cpp | 331 ++++++++++++++++++ 10 files changed, 900 insertions(+) create mode 100644 docs/used/SRP_Managed生命周期清理接缝计划_完成归档_2026-04-20.md 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) {