diff --git a/docs/used/SRP_AssetRuntime失效与重建接缝计划_完成归档_2026-04-20.md b/docs/used/SRP_AssetRuntime失效与重建接缝计划_完成归档_2026-04-20.md new file mode 100644 index 00000000..bffcae6e --- /dev/null +++ b/docs/used/SRP_AssetRuntime失效与重建接缝计划_完成归档_2026-04-20.md @@ -0,0 +1,152 @@ +# SRP AssetRuntime 失效与重建接缝计划 2026-04-20 + +## 1. 阶段目标 + +上一阶段已经补上了: + +`ScriptableRendererData -> ScriptableRenderer` + +这一层的 invalidation seam。 + +但当前还缺更上面的一刀: + +`ScriptableRenderPipelineAsset -> ManagedRenderPipelineAssetRuntime` + +也就是同一个 managed pipeline asset 还活着、同一个 native asset runtime 也还活着时,如果 asset 自己发生变化,native 侧现在还会继续拿旧缓存工作。 + +这一阶段的目标就是把这条接缝补齐。 + +--- + +## 2. 当前问题 + +### 2.1 asset 本身没有正式 dirty/version seam + +当前 `ScriptableRenderPipelineAsset` 只有: + +1. `CreatePipeline` +2. `ConfigureCameraRenderRequest` +3. `ConfigureCameraFramePlan` +4. `GetPipelineRendererAssetKey` +5. `ReleaseRuntimeResources` + +但没有像 Unity `RenderPipelineAsset` 那样的资源失效入口。 + +### 2.2 native runtime 会缓存旧 pipeline 和旧 renderer asset + +`MonoManagedRenderPipelineAssetRuntime` 当前会缓存: + +1. `m_pipelineHandle` +2. `m_pipelineRendererAssetResolved` +3. `m_pipelineRendererAsset` + +这些缓存现在只会在更大的生命周期边界释放: + +1. asset runtime 销毁 +2. bridge generation 变化 + +如果同一个 managed asset 在运行时修改自身配置,native 仍可能继续使用旧 pipeline。 + +### 2.3 现有测试还没锁住“同 runtime 内 asset dirty 后重建” + +上一阶段锁住的是: + +1. renderer data dirty 后 renderer rebuild +2. asset runtime release 时缓存释放 + +但还没有锁住: + +1. 同一个 asset runtime +2. 同一个 recorder +3. asset 调用 `SetDirty()` +4. native 自动丢弃旧 pipeline 并重建 + +--- + +## 3. 实施方案 + +### 3.1 给 `ScriptableRenderPipelineAsset` 增加正式 asset invalidation seam + +新增: + +1. `SetDirty()` +2. `GetRuntimeResourceVersionInstance()` + +其中: + +1. `SetDirty()` 负责释放 asset runtime resources +2. 同时 bump asset runtime resource version +3. native 通过 version 变化判断缓存是否失效 + +### 3.2 让 native runtime 基于 asset version 自动失效 + +`MonoManagedRenderPipelineAssetRuntime` 在访问下面两类缓存前先同步 version: + +1. `AcquireManagedPipelineHandle()` +2. `GetPipelineRendererAsset()` + +如果 version 变化,则: + +1. dispose 旧 managed pipeline +2. 清掉 renderer asset cache +3. 下次按新 asset 状态重建 + +### 3.3 让 stage recorder 感知 pipeline 重建 + +同一个 recorder 继续复用时,需要在 pipeline handle 变化后清掉旧 method cache,避免还抓着旧 managed pipeline 的方法解析结果。 + +### 3.4 用纯 asset 级 probe 锁住行为 + +不走 URP renderer data,直接增加: + +1. 一个 `ScriptableRenderPipelineAsset` probe +2. 一个按创建时配置决定 `SupportsStageRenderGraph` 的 pipeline probe +3. 一个运行时触发 `SetDirty()` 的观察脚本 + +验证点: + +1. dirty 前只支持 `MainScene` +2. dirty 后同一个 recorder 改为支持 `PostProcess` +3. create/dispose/invalidate 计数正确 + +--- + +## 4. 实施步骤 + +### Step 1:补 managed core seam + +1. 修改 `managed/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipelineAsset.cs` +2. 增加 asset dirty/version 机制 + +### Step 2:补 native runtime invalidation + +1. 修改 `engine/src/Scripting/Mono/MonoScriptRuntime.cpp` +2. 在 runtime 侧检测 asset version +3. 失效旧 pipeline 和 renderer asset cache + +### Step 3:补 probe 与脚本测试 + +1. 修改 `managed/GameScripts/RenderPipelineApiProbe.cs` +2. 修改 `managed/GameScripts/ScriptableRenderContextApiSurfaceProbe.cs` +3. 修改 `tests/scripting/test_mono_script_runtime.cpp` + +### Step 4:验证与收口 + +1. 编译 `XCEditor` +2. 运行 `rendering_unit_tests` +3. 运行 `scripting_tests` +4. 运行旧 editor 10s 冒烟并检查 `SceneReady` +5. 归档 plan +6. 提交推送 + +--- + +## 5. 验收标准 + +完成后应满足: + +1. `ScriptableRenderPipelineAsset` 有正式 dirty/version seam +2. 同一个 native asset runtime 能检测 managed asset 变化 +3. 同一个 recorder 下 asset dirty 后会丢弃旧 pipeline 并重建 +4. API probe 与 scripting test 全部锁住这条行为 +5. `XCEditor`、单测、冒烟全部通过 diff --git a/engine/src/Scripting/Mono/MonoScriptRuntime.cpp b/engine/src/Scripting/Mono/MonoScriptRuntime.cpp index cd6f6fd9..5cc20fdd 100644 --- a/engine/src/Scripting/Mono/MonoScriptRuntime.cpp +++ b/engine/src/Scripting/Mono/MonoScriptRuntime.cpp @@ -656,6 +656,21 @@ bool TryUnboxManagedBoolean(MonoObject* boxedValue, bool& outValue) { return true; } +bool TryUnboxManagedInt32(MonoObject* boxedValue, int32_t& outValue) { + outValue = 0; + if (!boxedValue) { + return false; + } + + void* const rawValue = mono_object_unbox(boxedValue); + if (!rawValue) { + return false; + } + + outValue = *static_cast(rawValue); + return true; +} + struct ManagedFinalColorSettingsData { uint8_t outputTransferMode = 0; uint8_t exposureMode = 0; @@ -828,6 +843,8 @@ public: private: bool EnsureManagedAsset() const; + bool SyncManagedAssetRuntimeState() const; + bool TryGetManagedRuntimeResourceVersion(int32_t& outVersion) const; void ReleaseManagedPipeline() const; void ReleaseManagedAsset() const; MonoObject* GetManagedAssetObject() const; @@ -839,6 +856,8 @@ private: MonoObject* assetObject) const; MonoMethod* ResolveGetDefaultFinalColorSettingsMethod( MonoObject* assetObject) const; + MonoMethod* ResolveGetRuntimeResourceVersionMethod( + MonoObject* assetObject) const; MonoMethod* ResolveGetPipelineRendererAssetKeyMethod( MonoObject* assetObject) const; MonoMethod* ResolveReleaseRuntimeResourcesMethod( @@ -853,10 +872,13 @@ private: mutable MonoMethod* m_configureCameraRenderRequestMethod = nullptr; mutable MonoMethod* m_configureCameraFramePlanMethod = nullptr; mutable MonoMethod* m_getDefaultFinalColorSettingsMethod = nullptr; + mutable MonoMethod* m_getRuntimeResourceVersionMethod = nullptr; mutable MonoMethod* m_getPipelineRendererAssetKeyMethod = nullptr; mutable MonoMethod* m_releaseRuntimeResourcesMethod = nullptr; mutable bool m_ownsManagedAssetHandle = false; mutable bool m_assetCreationAttempted = false; + mutable bool m_runtimeResourceVersionResolved = false; + mutable int32_t m_runtimeResourceVersion = 0; mutable uint32_t m_pipelineHandle = 0; mutable bool m_pipelineCreationAttempted = false; mutable bool m_pipelineRendererAssetResolved = false; @@ -895,8 +917,10 @@ public: if (m_ownedSceneRenderer != nullptr) { m_ownedSceneRenderer->Shutdown(); } + m_ownedPipelineRendererAsset.reset(); m_supportsStageMethod = nullptr; m_recordStageMethod = nullptr; + m_resolvedPipelineHandle = 0; m_boundSceneRenderer = nullptr; } @@ -1006,6 +1030,12 @@ private: return nullptr; } + if (pipelineHandle != m_resolvedPipelineHandle) { + m_supportsStageMethod = nullptr; + m_recordStageMethod = nullptr; + m_resolvedPipelineHandle = pipelineHandle; + } + return m_runtime->GetManagedObject(pipelineHandle); } @@ -1070,15 +1100,20 @@ private: return m_boundSceneRenderer; } - if (m_ownedSceneRenderer == nullptr) { - const std::shared_ptr - pipelineRendererAsset = - m_assetRuntime != nullptr - ? m_assetRuntime->GetPipelineRendererAsset() - : nullptr; + const std::shared_ptr + pipelineRendererAsset = + m_assetRuntime != nullptr + ? m_assetRuntime->GetPipelineRendererAsset() + : nullptr; + if (m_ownedSceneRenderer == nullptr || + pipelineRendererAsset != m_ownedPipelineRendererAsset) { + if (m_ownedSceneRenderer != nullptr) { + m_ownedSceneRenderer->Shutdown(); + } m_ownedSceneRenderer = Rendering::Internal::CreateNativeSceneRendererFromAsset( pipelineRendererAsset); + m_ownedPipelineRendererAsset = pipelineRendererAsset; } return m_ownedSceneRenderer.get(); @@ -1088,8 +1123,11 @@ private: MonoScriptRuntime* m_runtime = nullptr; mutable MonoMethod* m_supportsStageMethod = nullptr; mutable MonoMethod* m_recordStageMethod = nullptr; + mutable uint32_t m_resolvedPipelineHandle = 0; std::vector> m_fullscreenPassPool = {}; Rendering::NativeSceneRenderer* m_boundSceneRenderer = nullptr; + std::shared_ptr + m_ownedPipelineRendererAsset = nullptr; std::unique_ptr m_ownedSceneRenderer = nullptr; }; @@ -1217,16 +1255,16 @@ bool MonoManagedRenderPipelineAssetRuntime::TryGetDefaultFinalColorSettings( std::shared_ptr MonoManagedRenderPipelineAssetRuntime::GetPipelineRendererAsset() const { + if (!SyncManagedAssetRuntimeState()) { + return nullptr; + } + if (m_pipelineRendererAssetResolved) { return m_pipelineRendererAsset; } m_pipelineRendererAssetResolved = true; m_pipelineRendererAsset.reset(); - if (!EnsureManagedAsset()) { - return nullptr; - } - MonoObject* const assetObject = GetManagedAssetObject(); MonoMethod* const method = ResolveGetPipelineRendererAssetKeyMethod(assetObject); @@ -1257,6 +1295,11 @@ MonoManagedRenderPipelineAssetRuntime::GetPipelineRendererAsset() const { bool MonoManagedRenderPipelineAssetRuntime::AcquireManagedPipelineHandle( uint32_t& outPipelineHandle) const { + if (!SyncManagedAssetRuntimeState()) { + outPipelineHandle = 0; + return false; + } + if (m_pipelineHandle != 0) { outPipelineHandle = m_pipelineHandle; return true; @@ -1265,10 +1308,6 @@ bool MonoManagedRenderPipelineAssetRuntime::AcquireManagedPipelineHandle( outPipelineHandle = 0; return false; } - if (!EnsureManagedAsset()) { - outPipelineHandle = 0; - return false; - } m_pipelineCreationAttempted = true; @@ -1370,6 +1409,59 @@ bool MonoManagedRenderPipelineAssetRuntime::EnsureManagedAsset() const { return m_ownsManagedAssetHandle; } +bool MonoManagedRenderPipelineAssetRuntime::SyncManagedAssetRuntimeState() const { + if (!EnsureManagedAsset()) { + return false; + } + + int32_t runtimeResourceVersion = 0; + if (!TryGetManagedRuntimeResourceVersion(runtimeResourceVersion)) { + return false; + } + + if (!m_runtimeResourceVersionResolved) { + m_runtimeResourceVersion = runtimeResourceVersion; + m_runtimeResourceVersionResolved = true; + return true; + } + + if (runtimeResourceVersion == m_runtimeResourceVersion) { + return true; + } + + ReleaseManagedPipeline(); + m_pipelineRendererAsset.reset(); + m_pipelineRendererAssetResolved = false; + m_runtimeResourceVersion = runtimeResourceVersion; + return true; +} + +bool MonoManagedRenderPipelineAssetRuntime::TryGetManagedRuntimeResourceVersion( + int32_t& outVersion) const { + outVersion = 0; + if (!EnsureManagedAsset()) { + return false; + } + + MonoObject* const assetObject = GetManagedAssetObject(); + MonoMethod* const method = + ResolveGetRuntimeResourceVersionMethod(assetObject); + if (assetObject == nullptr || method == nullptr) { + return false; + } + + MonoObject* managedVersion = nullptr; + if (!m_runtime->InvokeManagedMethod( + assetObject, + method, + nullptr, + &managedVersion)) { + return false; + } + + return TryUnboxManagedInt32(managedVersion, outVersion); +} + void MonoManagedRenderPipelineAssetRuntime::ReleaseManagedPipeline() const { m_pipelineCreationAttempted = false; m_disposePipelineMethod = nullptr; @@ -1404,9 +1496,12 @@ void MonoManagedRenderPipelineAssetRuntime::ReleaseManagedAsset() const { m_configureCameraRenderRequestMethod = nullptr; m_configureCameraFramePlanMethod = nullptr; m_getDefaultFinalColorSettingsMethod = nullptr; + m_getRuntimeResourceVersionMethod = nullptr; m_getPipelineRendererAssetKeyMethod = nullptr; m_pipelineRendererAsset.reset(); m_pipelineRendererAssetResolved = false; + m_runtimeResourceVersionResolved = false; + m_runtimeResourceVersion = 0; const bool ownsManagedAssetHandle = m_ownsManagedAssetHandle; m_ownsManagedAssetHandle = false; m_assetCreationAttempted = false; @@ -1508,6 +1603,20 @@ MonoManagedRenderPipelineAssetRuntime::ResolveGetDefaultFinalColorSettingsMethod return m_getDefaultFinalColorSettingsMethod; } +MonoMethod* +MonoManagedRenderPipelineAssetRuntime::ResolveGetRuntimeResourceVersionMethod( + MonoObject* assetObject) const { + if (m_getRuntimeResourceVersionMethod == nullptr) { + m_getRuntimeResourceVersionMethod = + m_runtime->ResolveManagedMethod( + assetObject, + "GetRuntimeResourceVersionInstance", + 0); + } + + return m_getRuntimeResourceVersionMethod; +} + MonoMethod* MonoManagedRenderPipelineAssetRuntime::ResolveGetPipelineRendererAssetKeyMethod( MonoObject* assetObject) const { diff --git a/managed/GameScripts/RenderPipelineApiProbe.cs b/managed/GameScripts/RenderPipelineApiProbe.cs index c00563e2..c73d3b0b 100644 --- a/managed/GameScripts/RenderPipelineApiProbe.cs +++ b/managed/GameScripts/RenderPipelineApiProbe.cs @@ -1332,6 +1332,88 @@ namespace Gameplay } } + internal static class ManagedAssetInvalidationProbeState + { + public static int CreatePipelineCallCount; + public static int DisposePipelineCallCount; + public static int InvalidateAssetCallCount; + public static int LastCreatedSupportedStage; + + public static void Reset() + { + CreatePipelineCallCount = 0; + DisposePipelineCallCount = 0; + InvalidateAssetCallCount = 0; + LastCreatedSupportedStage = + (int)CameraFrameStage.MainScene; + } + } + + internal sealed class ManagedAssetInvalidationProbePipeline + : ScriptableRenderPipeline + { + private readonly CameraFrameStage m_supportedStage; + + public ManagedAssetInvalidationProbePipeline( + CameraFrameStage supportedStage) + { + m_supportedStage = supportedStage; + ManagedAssetInvalidationProbeState + .LastCreatedSupportedStage = + (int)supportedStage; + } + + protected override bool SupportsStageRenderGraph( + CameraFrameStage stage) + { + return stage == m_supportedStage; + } + + protected override void Dispose( + bool disposing) + { + if (disposing) + { + ManagedAssetInvalidationProbeState + .DisposePipelineCallCount++; + } + } + } + + public sealed class ManagedAssetInvalidationProbeAsset + : ScriptableRenderPipelineAsset + { + private CameraFrameStage m_supportedStage = + CameraFrameStage.MainScene; + + public ManagedAssetInvalidationProbeAsset() + { + ManagedAssetInvalidationProbeState.Reset(); + } + + protected override ScriptableRenderPipeline CreatePipeline() + { + ManagedAssetInvalidationProbeState + .CreatePipelineCallCount++; + return new ManagedAssetInvalidationProbePipeline( + m_supportedStage); + } + + public void InvalidatePipelineForTest( + CameraFrameStage supportedStage) + { + if (m_supportedStage == supportedStage) + { + return; + } + + m_supportedStage = supportedStage; + ManagedAssetInvalidationProbeState + .InvalidateAssetCallCount++; + SetDirty(); + } + } + public sealed class ManagedPlannedFullscreenRenderPipelineProbeAsset : UniversalRenderPipelineAsset { @@ -1665,6 +1747,56 @@ namespace Gameplay } } + public sealed class ManagedAssetInvalidationRuntimeSelectionProbe + : MonoBehaviour + { + public void Start() + { + GraphicsSettings.renderPipelineAsset = + new ManagedAssetInvalidationProbeAsset(); + } + } + + public sealed class ManagedAssetInvalidationObservationProbe + : MonoBehaviour + { + public int ObservedCreatePipelineCallCount; + public int ObservedDisposePipelineCallCount; + public int ObservedInvalidateAssetCallCount; + public int ObservedLastCreatedSupportedStage; + + private bool m_requestedInvalidation; + + public void Update() + { + ManagedAssetInvalidationProbeAsset selectedAsset = + GraphicsSettings.renderPipelineAsset + as ManagedAssetInvalidationProbeAsset; + if (!m_requestedInvalidation && + selectedAsset != null && + ManagedAssetInvalidationProbeState + .CreatePipelineCallCount > 0) + { + selectedAsset.InvalidatePipelineForTest( + CameraFrameStage.PostProcess); + m_requestedInvalidation = true; + } + + ObservedCreatePipelineCallCount = + ManagedAssetInvalidationProbeState + .CreatePipelineCallCount; + ObservedDisposePipelineCallCount = + ManagedAssetInvalidationProbeState + .DisposePipelineCallCount; + ObservedInvalidateAssetCallCount = + ManagedAssetInvalidationProbeState + .InvalidateAssetCallCount; + ObservedLastCreatedSupportedStage = + ManagedAssetInvalidationProbeState + .LastCreatedSupportedStage; + } + } + public sealed class RenderPipelineApiProbe : MonoBehaviour { public bool InitialAssetWasNull; diff --git a/managed/GameScripts/ScriptableRenderContextApiSurfaceProbe.cs b/managed/GameScripts/ScriptableRenderContextApiSurfaceProbe.cs index 8eaf07ec..01ad84ae 100644 --- a/managed/GameScripts/ScriptableRenderContextApiSurfaceProbe.cs +++ b/managed/GameScripts/ScriptableRenderContextApiSurfaceProbe.cs @@ -27,6 +27,8 @@ namespace Gameplay public bool HasUniversalRequestContextHasDirectionalShadowExtension; public bool HasUniversalRequestContextClearDirectionalShadowExtension; public bool HasPublicPipelineAssetConfigureCameraFramePlan; + public bool HasPipelineAssetSetDirty; + public bool HasPipelineAssetGetRuntimeResourceVersion; public bool HasPlanningContextType; public bool HasRendererFeatureConfigureCameraFramePlan; public bool HasRendererRecordingContextType; @@ -153,6 +155,16 @@ namespace Gameplay BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) != null; + HasPipelineAssetSetDirty = + pipelineAssetType.GetMethod( + "SetDirty", + BindingFlags.Instance | + BindingFlags.NonPublic) != null; + HasPipelineAssetGetRuntimeResourceVersion = + pipelineAssetType.GetMethod( + "GetRuntimeResourceVersionInstance", + BindingFlags.Instance | + BindingFlags.NonPublic) != null; HasPlanningContextType = contextType.Assembly.GetType( "XCEngine.Rendering.ScriptableRenderPipelinePlanningContext") != null; diff --git a/managed/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipelineAsset.cs b/managed/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipelineAsset.cs index 0a695479..1f79e276 100644 --- a/managed/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipelineAsset.cs +++ b/managed/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipelineAsset.cs @@ -4,6 +4,8 @@ namespace XCEngine.Rendering { public abstract class ScriptableRenderPipelineAsset : RenderPipelineAsset { + private int m_runtimeResourceVersion = 1; + protected ScriptableRenderPipelineAsset() { } @@ -19,6 +21,11 @@ namespace XCEngine.Rendering ConfigureCameraFramePlan(context); } + internal int GetRuntimeResourceVersionInstance() + { + return m_runtimeResourceVersion; + } + protected virtual ScriptableRenderPipeline CreatePipeline() { return null; @@ -47,6 +54,21 @@ namespace XCEngine.Rendering protected virtual void ReleaseRuntimeResources() { } + + protected void SetDirty() + { + ReleaseRuntimeResources(); + + unchecked + { + ++m_runtimeResourceVersion; + } + + if (m_runtimeResourceVersion <= 0) + { + m_runtimeResourceVersion = 1; + } + } } } diff --git a/tests/scripting/test_mono_script_runtime.cpp b/tests/scripting/test_mono_script_runtime.cpp index 247771fc..68d4a4be 100644 --- a/tests/scripting/test_mono_script_runtime.cpp +++ b/tests/scripting/test_mono_script_runtime.cpp @@ -1171,6 +1171,8 @@ TEST_F( bool hasUniversalRequestContextHasDirectionalShadowExtension = false; bool hasUniversalRequestContextClearDirectionalShadowExtension = false; bool hasPublicPipelineAssetConfigureCameraFramePlan = false; + bool hasPipelineAssetSetDirty = false; + bool hasPipelineAssetGetRuntimeResourceVersion = false; bool hasPlanningContextType = false; bool hasRendererFeatureConfigureCameraFramePlan = false; bool hasRendererRecordingContextType = false; @@ -1262,6 +1264,14 @@ TEST_F( selectionScript, "HasPublicPipelineAssetConfigureCameraFramePlan", hasPublicPipelineAssetConfigureCameraFramePlan)); + EXPECT_TRUE(runtime->TryGetFieldValue( + selectionScript, + "HasPipelineAssetSetDirty", + hasPipelineAssetSetDirty)); + EXPECT_TRUE(runtime->TryGetFieldValue( + selectionScript, + "HasPipelineAssetGetRuntimeResourceVersion", + hasPipelineAssetGetRuntimeResourceVersion)); EXPECT_TRUE(runtime->TryGetFieldValue( selectionScript, "HasPlanningContextType", @@ -1338,6 +1348,8 @@ TEST_F( EXPECT_FALSE(hasUniversalRequestContextHasDirectionalShadowExtension); EXPECT_FALSE(hasUniversalRequestContextClearDirectionalShadowExtension); EXPECT_TRUE(hasPublicPipelineAssetConfigureCameraFramePlan); + EXPECT_TRUE(hasPipelineAssetSetDirty); + EXPECT_TRUE(hasPipelineAssetGetRuntimeResourceVersion); EXPECT_TRUE(hasPlanningContextType); EXPECT_TRUE(hasRendererFeatureConfigureCameraFramePlan); EXPECT_TRUE(hasRendererRecordingContextType); @@ -3258,6 +3270,107 @@ TEST_F( recorder->Shutdown(); } +TEST_F( + MonoScriptRuntimeTest, + ManagedRenderPipelineBridgeRebuildsPipelineAfterAssetInvalidation) { + Scene* runtimeScene = + CreateScene("ManagedAssetInvalidationScene"); + GameObject* selectionObject = + runtimeScene->CreateGameObject( + "ManagedAssetInvalidationSelection"); + ScriptComponent* selectionScript = + AddScript( + selectionObject, + "Gameplay", + "ManagedAssetInvalidationRuntimeSelectionProbe"); + ASSERT_NE(selectionScript, nullptr); + GameObject* observationObject = + runtimeScene->CreateGameObject( + "ManagedAssetInvalidationObservation"); + ScriptComponent* observationScript = + AddScript( + observationObject, + "Gameplay", + "ManagedAssetInvalidationObservationProbe"); + ASSERT_NE(observationScript, 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, "ManagedAssetInvalidationProbeAsset"); + + 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)); + ASSERT_TRUE( + recorder->SupportsStageRenderGraph( + XCEngine::Rendering::CameraFrameStage::MainScene)); + ASSERT_FALSE( + recorder->SupportsStageRenderGraph( + XCEngine::Rendering::CameraFrameStage::PostProcess)); + + engine->OnUpdate(0.016f); + + ASSERT_FALSE( + recorder->SupportsStageRenderGraph( + XCEngine::Rendering::CameraFrameStage::MainScene)); + ASSERT_TRUE( + recorder->SupportsStageRenderGraph( + XCEngine::Rendering::CameraFrameStage::PostProcess)); + + engine->OnUpdate(0.016f); + EXPECT_TRUE(runtime->GetLastError().empty()) << runtime->GetLastError(); + + int observedCreatePipelineCallCount = 0; + int observedDisposePipelineCallCount = 0; + int observedInvalidateAssetCallCount = 0; + int observedLastCreatedSupportedStage = 0; + EXPECT_TRUE(runtime->TryGetFieldValue( + observationScript, + "ObservedCreatePipelineCallCount", + observedCreatePipelineCallCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + observationScript, + "ObservedDisposePipelineCallCount", + observedDisposePipelineCallCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + observationScript, + "ObservedInvalidateAssetCallCount", + observedInvalidateAssetCallCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + observationScript, + "ObservedLastCreatedSupportedStage", + observedLastCreatedSupportedStage)); + + EXPECT_EQ(observedCreatePipelineCallCount, 2); + EXPECT_EQ(observedDisposePipelineCallCount, 1); + EXPECT_EQ(observedInvalidateAssetCallCount, 1); + EXPECT_EQ( + observedLastCreatedSupportedStage, + static_cast( + XCEngine::Rendering::CameraFrameStage::PostProcess)); + + recorder->Shutdown(); +} + TEST_F( MonoScriptRuntimeTest, ManagedStageRecorderRecordsMainSceneThroughScriptableRenderContext) {